From 636b33196b5ca79baeec0339a25eec75f2d05e97 Mon Sep 17 00:00:00 2001 From: Arnold Galovics Date: Mon, 24 Apr 2023 13:18:18 +0200 Subject: [PATCH] FINERACT-1724: More batch support for datatables APIs --- .../command/CommandStrategyProvider.java | 5 +- ...pTableIdAndDataTableIdCommandStrategy.java | 95 ++++++++++ .../command/CommandStrategyProviderTest.java | 8 +- ...leIdAndDataTableIdCommandStrategyTest.java | 165 ++++++++++++++++++ .../integrationtests/BatchApiTest.java | 11 +- .../integrationtests/common/BatchHelper.java | 32 ++++ 6 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategyTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java index 24a2532f6..7b422f076 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java @@ -50,7 +50,7 @@ public class CommandStrategyProvider { /** * Regex pattern for specifying query params */ - private static final String MANDATORY_QUERY_PARAM_REGEX = "(\\?(\\w+(?:\\=[\\w,]+|&)+)+)"; + private static final String MANDATORY_QUERY_PARAM_REGEX = "(\\?(\\w+(?:\\=[\\w\\-,]+|&)+)+)"; /** * Regex pattern for specifying any query param that has key = 'command' or not specific anything. @@ -202,6 +202,9 @@ public class CommandStrategyProvider { commandStrategies.put(CommandContext .resource("datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX + OPTIONAL_QUERY_PARAM_REGEX) .method(GET).build(), "getDatatableEntryByAppTableIdCommandStrategy"); + commandStrategies.put(CommandContext.resource("datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX + "\\/" + + NUMBER_REGEX + OPTIONAL_QUERY_PARAM_REGEX).method(GET).build(), + "getDatatableEntryByAppTableIdAndDataTableIdCommandStrategy"); commandStrategies.put(CommandContext.resource("loans\\/" + NUMBER_REGEX + OPTIONAL_COMMAND_PARAM_REGEX).method(PUT).build(), "modifyLoanApplicationCommandStrategy"); commandStrategies.put( diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy.java new file mode 100644 index 000000000..c1d3e4cad --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy.java @@ -0,0 +1,95 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.batch.command.internal; + +import java.util.Map; +import javax.ws.rs.core.UriInfo; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.batch.command.CommandStrategy; +import org.apache.fineract.batch.command.CommandStrategyUtils; +import org.apache.fineract.batch.domain.BatchRequest; +import org.apache.fineract.batch.domain.BatchResponse; +import org.apache.fineract.infrastructure.core.api.MutableUriInfo; +import org.apache.fineract.infrastructure.dataqueries.api.DatatablesApiResource; +import org.apache.http.HttpStatus; +import org.springframework.stereotype.Component; + +/** + * Implements {@link CommandStrategy} and get datatable by appTableId by data table id. It passes the contents of the + * body from the BatchRequest to {@link DatatablesApiResource} and gets back the response. This class will also catch + * any errors raised by {@link DatatablesApiResource} and map those errors to appropriate status codes in BatchResponse. + */ +@Component +@RequiredArgsConstructor +public class GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy implements CommandStrategy { + + /** + * Data table api resource {@link DatatablesApiResource}. + */ + private final DatatablesApiResource dataTablesApiResource; + + @Override + public BatchResponse execute(final BatchRequest request, @SuppressWarnings("unused") UriInfo uriInfo) { + final MutableUriInfo parameterizedUriInfo = new MutableUriInfo(uriInfo); + final BatchResponse response = new BatchResponse(); + final String responseBody; + + response.setRequestId(request.getRequestId()); + response.setHeaders(request.getHeaders()); + + final String relativeUrl = request.getRelativeUrl(); + final String relativeUrlSubString = StringUtils.substringAfter(relativeUrl, "/"); + + // uriInfo will contain the query parameter value(s) that are sent in the actual batch uri. + // for example: batches?enclosingTransaction=true + // But the query parameters that are sent in the batch relative url has to be sent to + // datatablesApiResource.getDatatable + // To use the relative url query parameters + // - Parse and fetch the query parameters sent in the relative url + // (datatables/dt_123/1/2?genericResultSet=false) + // - Add them to the MutableUriInfo query parameters list + // - Call datatablesApiResource.getDatatableManyEntry(dataTable, appTableId, dataTableId, null, false, generic + // resultSet, uriInfo) + final String[] resources = StringUtils.split(relativeUrlSubString, "/"); + final String dataTableName = resources[0]; + final Long appTableId = Long.parseLong(resources[1]); + final Long dataTableId = Long.parseLong(StringUtils.substringBefore(resources[2], "?")); + boolean genericResultSet = false; + if (relativeUrl.indexOf('?') > 0) { + Map queryParameters = CommandStrategyUtils.getQueryParameters(relativeUrl); + // Add the query parameters sent in the relative URL to MutableUriInfo + if (queryParameters.containsKey("genericResultSet")) { + genericResultSet = Boolean.parseBoolean(queryParameters.get("genericResultSet")); + } + CommandStrategyUtils.addQueryParametersToUriInfo(parameterizedUriInfo, queryParameters); + } + // Calls 'getDatatableManyEntry' function from 'DatatablesApiResource' to + // get the datatable details based on the appTableId + responseBody = dataTablesApiResource.getDatatableManyEntry(dataTableName, appTableId, dataTableId, null, genericResultSet, + parameterizedUriInfo); + + response.setStatusCode(HttpStatus.SC_OK); + + // Sets the response after retrieving the datatable + response.setBody(responseBody); + + return response; + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/batch/command/CommandStrategyProviderTest.java b/fineract-provider/src/test/java/org/apache/fineract/batch/command/CommandStrategyProviderTest.java index cd9cff795..a4c7f9bf0 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/batch/command/CommandStrategyProviderTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/batch/command/CommandStrategyProviderTest.java @@ -45,6 +45,7 @@ import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandSt import org.apache.fineract.batch.command.internal.DisburseLoanCommandStrategy; import org.apache.fineract.batch.command.internal.GetChargeByChargeExternalIdCommandStrategy; import org.apache.fineract.batch.command.internal.GetChargeByIdCommandStrategy; +import org.apache.fineract.batch.command.internal.GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy; import org.apache.fineract.batch.command.internal.GetDatatableEntryByAppTableIdCommandStrategy; import org.apache.fineract.batch.command.internal.GetDatatableEntryByQueryCommandStrategy; import org.apache.fineract.batch.command.internal.GetLoanByExternalIdCommandStrategy; @@ -174,6 +175,9 @@ public class CommandStrategyProviderTest { mock(GetDatatableEntryByAppTableIdCommandStrategy.class)), Arguments.of("datatables/test_dt_table/123?genericResultSet=true", HttpMethod.GET, "getDatatableEntryByAppTableIdCommandStrategy", mock(GetDatatableEntryByAppTableIdCommandStrategy.class)), + Arguments.of("datatables/test_dt_table/123/1?genericResultSet=true", HttpMethod.GET, + "getDatatableEntryByAppTableIdAndDataTableIdCommandStrategy", + mock(GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy.class)), Arguments.of("datatables/test_dt_table/123", HttpMethod.POST, "createDatatableEntryCommandStrategy", mock(CreateDatatableEntryCommandStrategy.class)), Arguments.of("datatables/test_dt_table/123/1", HttpMethod.PUT, "updateDatatableEntryOneToManyCommandStrategy", @@ -185,7 +189,9 @@ public class CommandStrategyProviderTest { Arguments.of("loans/123", HttpMethod.PUT, "modifyLoanApplicationCommandStrategy", mock(ModifyLoanApplicationCommandStrategy.class)), Arguments.of("datatables/test_dt_table/query?columnFilter=id&valueFilter=12&resultColumns=id", HttpMethod.GET, - "getDatatableEntryByQueryCommandStrategy", mock(GetDatatableEntryByQueryCommandStrategy.class))); + "getDatatableEntryByQueryCommandStrategy", mock(GetDatatableEntryByQueryCommandStrategy.class)), + Arguments.of("datatables/test_dt_table/query?columnFilter=custom_id&valueFilter=10a62-d438-2319&resultColumns=id", + HttpMethod.GET, "getDatatableEntryByQueryCommandStrategy", mock(GetDatatableEntryByQueryCommandStrategy.class))); } /** diff --git a/fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategyTest.java b/fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategyTest.java new file mode 100644 index 000000000..c068b3a8c --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/batch/command/internal/GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategyTest.java @@ -0,0 +1,165 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.batch.command.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import java.util.List; +import java.util.stream.Stream; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.UriInfo; +import org.apache.commons.lang3.RandomUtils; +import org.apache.fineract.batch.domain.BatchRequest; +import org.apache.fineract.batch.domain.BatchResponse; +import org.apache.fineract.infrastructure.core.api.MutableUriInfo; +import org.apache.fineract.infrastructure.dataqueries.api.DatatablesApiResource; +import org.apache.http.HttpStatus; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests {GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy}. + */ +public class GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategyTest { + + /** + * Query parameter provider. + * + * @return the test data stream + */ + private static Stream provideQueryParameters() { + return Stream.of(Arguments.of(null, 0), Arguments.of("genericResultSet=true", 1)); + } + + /** + * Test {@link GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy#execute} happy path scenario. + * + * @param queryParameter + * the query parameter + * @param numberOfQueryParams + * number of query params are provided + */ + @ParameterizedTest + @MethodSource("provideQueryParameters") + public void testExecuteSuccessScenario(final String queryParameter, final int numberOfQueryParams) { + final TestContext testContext = new TestContext(); + + final Long loanId = RandomUtils.nextLong(); + final Long datatableId = RandomUtils.nextLong(); + final String datatableName = "dt_loan_xyz"; + final BatchRequest request = getBatchRequest(loanId, datatableId, queryParameter, datatableName); + final String responseBody = "{\\\"columnHeaders\\\":[{}],\\\"data\\\":\\\"{}\\\"}"; + Boolean genericResultSet = false; + if (queryParameter != null && queryParameter.contains("genericResultSet=true")) { + genericResultSet = Boolean.TRUE; + } + given(testContext.dataTableApiResource.getDatatableManyEntry(eq(datatableName), eq(loanId), eq(datatableId), eq(null), + eq(genericResultSet), any(UriInfo.class))).willReturn(responseBody); + + final BatchResponse response = testContext.subjectToTest.execute(request, testContext.uriInfo); + assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + assertEquals(request.getRequestId(), response.getRequestId()); + assertEquals(request.getHeaders(), response.getHeaders()); + assertEquals(responseBody, response.getBody()); + + verify(testContext.dataTableApiResource).getDatatableManyEntry(eq(datatableName), eq(loanId), eq(datatableId), eq(null), + eq(genericResultSet), testContext.uriInfoCaptor.capture()); + MutableUriInfo mutableUriInfo = testContext.uriInfoCaptor.getValue(); + assertThat(mutableUriInfo.getAdditionalQueryParameters()).hasSize(numberOfQueryParams); + if (numberOfQueryParams > 0) { + List param = mutableUriInfo.getAdditionalQueryParameters().get("genericResultSet"); + assertEquals(param.get(0), "true"); + } + } + + /** + * Creates and returns a request with the given loan id. + * + * @param loanId + * the loan id + * @param queryParameter + * the query parameter + * @param datatableName + * the datatable name + * @return the {@link BatchRequest} + */ + private BatchRequest getBatchRequest(final Long loanId, final Long datatableId, final String queryParameter, + final String datatableName) { + final BatchRequest br = new BatchRequest(); + String relativeUrl = String.format("datatables/%s/%s/%s", datatableName, loanId, datatableId); + if (queryParameter != null) { + relativeUrl = relativeUrl + "?" + queryParameter; + } + + br.setRequestId(RandomUtils.nextLong()); + br.setRelativeUrl(relativeUrl); + br.setMethod(HttpMethod.GET); + br.setReference(RandomUtils.nextLong()); + br.setBody("{}"); + + return br; + } + + /** + * Private test context class used since testng runs in parallel to avoid state between tests + */ + private static final class TestContext { + + /** + * The subject under test. + */ + private final GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy subjectToTest; + + /** + * Mock of {@link UriInfo} + */ + @Mock + private UriInfo uriInfo; + + /** + * Captor of {@link MutableUriInfo}. + */ + @Captor + private ArgumentCaptor uriInfoCaptor; + + /** + * {@link DatatablesApiResource} mock. + */ + @Mock + private DatatablesApiResource dataTableApiResource; + + /** + * Constructor. + */ + private TestContext() { + MockitoAnnotations.openMocks(this); + subjectToTest = new GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy(dataTableApiResource); + } + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java index 39789ddbb..77aa09b8b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java @@ -43,6 +43,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; import org.apache.fineract.batch.command.internal.AdjustTransactionCommandStrategy; import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandStrategy; +import org.apache.fineract.batch.command.internal.GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy; import org.apache.fineract.batch.domain.BatchRequest; import org.apache.fineract.batch.domain.BatchResponse; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; @@ -1326,6 +1327,7 @@ public class BatchApiTest { * @see org.apache.fineract.batch.command.internal.CreateDatatableEntryCommandStrategy * @see org.apache.fineract.batch.command.internal.UpdateDatatableEntryOneToManyCommandStrategy * @see org.apache.fineract.batch.command.internal.GetDatatableEntryByAppTableIdCommandStrategy + * @see GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy */ @Test public void shouldReturnOkStatusOnSuccessfulCreateDataTableEntry() { @@ -1373,8 +1375,12 @@ public class BatchApiTest { final BatchRequest getDatatableEntriesRequest = BatchHelper.getDatatableByIdRequest(loanId, datatableName, null, updateDatatableEntryByEntryIdRequest.getReference()); + // Get datatable entry by app table id batch request + final BatchRequest getDatatableEntryByIdRequest = BatchHelper.getDatatableEntryByIdRequest(loanId, datatableName, "$.resourceId", + null, updateDatatableEntryByEntryIdRequest.getReference()); + final List batchRequestsDatatableEntries = Arrays.asList(createDatatableEntryRequest, - updateDatatableEntryByEntryIdRequest, getDatatableEntriesRequest); + updateDatatableEntryByEntryIdRequest, getDatatableEntriesRequest, getDatatableEntryByIdRequest); LOG.info("Batch Request : {}", BatchHelper.toJsonString(batchRequestsDatatableEntries)); final List responseDatatableBatch = BatchHelper.postBatchRequestsWithEnclosingTransaction(this.requestSpec, @@ -1385,9 +1391,12 @@ public class BatchApiTest { final BatchResponse batchResponse1 = responseDatatableBatch.get(0); final BatchResponse batchResponse2 = responseDatatableBatch.get(1); final BatchResponse batchResponse3 = responseDatatableBatch.get(2); + final BatchResponse batchResponse4 = responseDatatableBatch.get(3); + Assertions.assertEquals(HttpStatus.SC_OK, batchResponse1.getStatusCode(), "Verify Status Code 200 for create datatable entry"); Assertions.assertEquals(HttpStatus.SC_OK, batchResponse2.getStatusCode(), "Verify Status Code 200 for update datatable entry"); Assertions.assertEquals(HttpStatus.SC_OK, batchResponse3.getStatusCode(), "Verify Status Code 200 for get datatable entries"); + Assertions.assertEquals(HttpStatus.SC_OK, batchResponse4.getStatusCode(), "Verify Status Code 200 for get datatable entry by id"); final String getDatatableEntriesResponse = batchResponse3.getBody(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java index 6aabe2372..faaa0091e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java @@ -1166,6 +1166,38 @@ public final class BatchHelper { return br; } + /** + * Creates and returns a batch request to get datatable entry. + * + * @param loanId + * the loan id + * @param datatableName + * the name of datatable + * @param appTableId + * the app table id + * @param queryParameter + * the query parameters + * @param referenceId + * the reference id + * @return the {@link BatchRequest} + */ + public static BatchRequest getDatatableEntryByIdRequest(final Long loanId, final String datatableName, final String appTableId, + final String queryParameter, final Long referenceId) { + final BatchRequest br = new BatchRequest(); + String relativeUrl = String.format("datatables/%s/%s/%s", datatableName, loanId, appTableId); + if (queryParameter != null) { + relativeUrl = relativeUrl + "?" + queryParameter; + } + + br.setRequestId(4572L); + br.setReference(referenceId); + br.setRelativeUrl(relativeUrl); + br.setMethod(HttpMethod.GET); + br.setBody("{}"); + + return br; + } + /** * Creates and returns a batch request to create datatable entry. *