diff --git a/mifosng-provider/build.gradle b/mifosng-provider/build.gradle index 37b91e4f9..62ed5b8eb 100644 --- a/mifosng-provider/build.gradle +++ b/mifosng-provider/build.gradle @@ -136,8 +136,8 @@ dependencies { [group: 'org.quartz-scheduler', name: 'quartz', version: '2.1.7'], [group: 'com.amazonaws', name: 'aws-java-sdk', version: '1.2.1'], [group: 'net.sf.ehcache', name: 'ehcache', version: '2.7.2'], - [group: 'com.github.spullara.mustache.java', name: 'compiler', version: '0.8.12'] - ) + [group: 'com.github.spullara.mustache.java', name: 'compiler', version: '0.8.12'], + [group: 'com.jayway.jsonpath', name: 'json-path', version: '0.9.1']) testCompile 'junit:junit:4.10', 'junit:junit-dep:4.10', @@ -401,3 +401,4 @@ def qualifyVersionIfNecessary(version) { } return version } + diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/BatchApiTest.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/BatchApiTest.java index bb72f4ee1..857a44476 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/BatchApiTest.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/BatchApiTest.java @@ -8,9 +8,12 @@ import org.junit.Before; import org.junit.Test; import org.mifosplatform.batch.domain.BatchRequest; import org.mifosplatform.batch.domain.BatchResponse; +import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.integrationtests.common.BatchHelper; import org.mifosplatform.integrationtests.common.Utils; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.builder.ResponseSpecBuilder; import com.jayway.restassured.http.ContentType; @@ -49,7 +52,12 @@ public class BatchApiTest { this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); } - //Tests for the unimplemented command Strategies by returning 501 status code + /** + * Tests for the unimplemented command Strategies by returning 501 status code. + * For a unknownRequest a statusCode 501 is returned back with response. + * + * @see org.mifosplatform.batch.command.internal.UnknownCommandStrategy + */ @Test public void shouldReturnStatusNotImplementedUnknownCommand() { @@ -58,7 +66,7 @@ public class BatchApiTest { br.setRelativeUrl("/nirvana"); br.setMethod("POST"); - final List response = BatchHelper.createRequest(this.requestSpec, this.responseSpec, br); + final List response = BatchHelper.postWithSingleRequest(this.requestSpec, this.responseSpec, br); //Verify that only 501 is returned as the status code for(BatchResponse resp : response) { @@ -66,13 +74,18 @@ public class BatchApiTest { } } - //Tests for the successful response for a createClient request from createClientCommand + /** + * Tests for the successful response for a createClient request from createClientCommand. + * A successful response with statusCode '200' is returned back. + * + * @see org.mifosplatform.batch.command.internal.CreateClientCommandStrategy + */ @Test public void shouldReturnOkStatusForCreateClientCommand() { final BatchRequest br = BatchHelper.createClientRequest(4712L, ""); - final List response = BatchHelper.createRequest(this.requestSpec, this.responseSpec, br); + final List response = BatchHelper.postWithSingleRequest(this.requestSpec, this.responseSpec, br); //Verify that a 200 response is returned as the status code for(BatchResponse resp : response) { @@ -80,18 +93,26 @@ public class BatchApiTest { } } - //Tests for an erroneous response with statusCode 501 if transaction fails + /** + * Tests for an erroneous response with statusCode '501' if transaction fails. + * If Query Parameter 'enclosingTransaction' is set to 'true' and if one of the + * request in BatchRequest fails then all transactions are rolled back. + * + * @see org.mifosplatform.batch.command.internal.CreateClientCommandStrategy + * @see org.mifosplatform.batch.api.BatchApiResource + * @see org.mifosplatform.batch.service.BatchApiService + */ @Test public void shouldRollBackAllTransactionsOnFailure() { - //Create first client - final BatchRequest br1 = BatchHelper.createClientRequest(4713L, "TestExtId1"); + //Create first client request + final BatchRequest br1 = BatchHelper.createClientRequest(4713L, "TestExtId11"); - //Create second client - final BatchRequest br2 = BatchHelper.createClientRequest(4714L, "TestExtId2"); + //Create second client request + final BatchRequest br2 = BatchHelper.createClientRequest(4714L, "TestExtId12"); - //Create third client, having same externalID as second client, hence cause of error - final BatchRequest br3 = BatchHelper.createClientRequest(4715L, "TestExtId2"); + //Create third client request, having same externalID as second client, hence cause of error + final BatchRequest br3 = BatchHelper.createClientRequest(4715L, "TestExtId11"); final List batchRequests = new ArrayList<>(); @@ -103,7 +124,85 @@ public class BatchApiTest { final List response = BatchHelper.postBatchRequestsWithEnclosingTransaction(this.requestSpec, this.responseSpec, jsonifiedRequest); - Assert.assertEquals(response.size(), 1); + //Verifies that none of the client in BatchRequest is created on the server + BatchHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, "TestExtId11"); + BatchHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, "TestExtId12"); + + //Asserts that all the transactions have been successfully rolled back + Assert.assertEquals(response.size(), 1); Assert.assertEquals("Verifying Status code 400",(long) 400,(long) response.get(0).getStatusCode()); } + + /** + * Tests that a client information was successfully updated through updateClientCommand. + * A 'changes' parameter is returned in the response after successful update of client + * information. + * + * @see org.mifosplatform.batch.command.internal.UpdateClientCommandStrategy + */ + @Test + public void shouldReflectChangesOnClientUpdate() { + + //Create a createClient Request + final BatchRequest br1 = BatchHelper.createClientRequest(4716L, ""); + + //Create a clientUpdate Request + final BatchRequest br2 = BatchHelper.updateClientRequest(4717L, 4716L); + + final List batchRequests = new ArrayList<>(); + + batchRequests.add(br1); + batchRequests.add(br2); + + final String jsonifiedRequest = BatchHelper.toJsonString(batchRequests); + + final List response = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(this.requestSpec, + this.responseSpec, jsonifiedRequest); + + //Get the changes parameter from updateClient Response + final JsonObject changes = new FromJsonHelper().parse(response.get(1).getBody()) + .getAsJsonObject().get("changes").getAsJsonObject(); + + //Asserts the client information is successfully updated + Assert.assertEquals("Firstname updated", "TestFirstName",changes.get("firstname").getAsString()); + Assert.assertEquals("Lastname updated", "TestLastName", changes.get("lastname").getAsString()); + } + + /** + * Tests that a ApplyLoanCommand was successfully executed and returned a 200(OK) + * status. It creates a new client and apply a loan to that client. This also + * verifies the successful resolution of dependencies among two requests. + * + * @see org.mifosplatform.batch.command.internal.ApplyLoanCommandStrategy + */ + @Test + public void shouldReturnOkStatusForApplyLoanCommand(){ + + //Create a createClient Request + final BatchRequest br1 = BatchHelper.createClientRequest(4718L, ""); + + //Create a ApplyLoan Request + final BatchRequest br2 = BatchHelper.applyLoanRequest(4719L, 4718L); + + final List batchRequests = new ArrayList<>(); + + batchRequests.add(br1); + batchRequests.add(br2); + + final String jsonifiedRequest = BatchHelper.toJsonString(batchRequests); + + final List response = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(this.requestSpec, + this.responseSpec, jsonifiedRequest); + + //Get the clientId parameter from createClient Response + final JsonElement clientId = new FromJsonHelper().parse(response.get(0).getBody()) + .getAsJsonObject().get("clientId"); + + //Get the loanId parameter from applyLoan Response + final JsonElement loanId = new FromJsonHelper().parse(response.get(1).getBody()) + .getAsJsonObject().get("loanId"); + + Assert.assertEquals("Loan Successfully applied to client " + clientId.getAsString() + + " with loanId: " + loanId.getAsString(), 200L, (long) response.get(1).getStatusCode()); + } } \ No newline at end of file diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/BatchHelper.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/BatchHelper.java index 0d11923f1..471d0c200 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/BatchHelper.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/BatchHelper.java @@ -62,7 +62,7 @@ public class BatchHelper { */ public static List postBatchRequestsWithoutEnclosingTransaction(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final String jsonifiedBatchRequests) { - final String response = Utils.performServerPost(requestSpec, responseSpec, BATCH_API_URL, jsonifiedBatchRequests, null); + final String response = Utils.performServerPost(requestSpec, responseSpec, BATCH_API_URL, jsonifiedBatchRequests, null); return BatchHelper.fromJsonString(response); } @@ -82,44 +82,13 @@ public class BatchHelper { } /** - * Creates a new Client Request as one of the request in Batch. - * - * @param reqId - * @param externalId - * @return BatchRequest - */ - public static BatchRequest createClientRequest(final Long reqId, final String externalId) { - - final BatchRequest br = new BatchRequest(); - br.setRequestId(reqId); - br.setRelativeUrl("clients"); - br.setMethod("POST"); - - final String extId; - if(externalId.equals("")) { - extId = "ext" + String.valueOf((10000 * Math.random())) + String.valueOf((10000 * Math.random())); - } - else { - extId = externalId; - } - - final String body = "{ \"officeId\": 1, \"firstname\": \"Petra\", \"lastname\": \"Yton\"," + - "\"externalId\": " + extId + ", \"dateFormat\": \"dd MMMM yyyy\", \"locale\": \"en\"," + - "\"active\": false, \"activationDate\": \"04 March 2009\", \"submittedOnDate\": \"04 March 2009\"}"; - - br.setBody(body); - - return br; - } - - /** * Returns a BatchResponse based on the given BatchRequest, by posting the request * to the server. * * @param BatchRequest * @return List */ - public static List createRequest(final RequestSpecification requestSpec, + public static List postWithSingleRequest(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,final BatchRequest br) { final List batchRequests = new ArrayList<>(); @@ -135,4 +104,102 @@ public class BatchHelper { return response; } + + /** + * Creates and returns a {@link org.mifosplatform.batch.command.internal.CreateClientCommandStrategy} + * Request as one of the request in Batch. + * + * @param reqId + * @param externalId + * @return BatchRequest + */ + public static BatchRequest createClientRequest(final Long requestId, final String externalId) { + + final BatchRequest br = new BatchRequest(); + br.setRequestId(requestId); + br.setRelativeUrl("clients"); + br.setMethod("POST"); + + final String extId; + if(externalId.equals("")) { + extId = "ext" + String.valueOf((10000 * Math.random())) + String.valueOf((10000 * Math.random())); + } + else { + extId = externalId; + } + + final String body = "{ \"officeId\": 1, \"firstname\": \"Petra\", \"lastname\": \"Yton\"," + + "\"externalId\": " + extId + ", \"dateFormat\": \"dd MMMM yyyy\", \"locale\": \"en\"," + + "\"active\": true, \"activationDate\": \"04 March 2009\", \"submittedOnDate\": \"04 March 2009\"}"; + + br.setBody(body); + + return br; + } + + /** + * Creates and returns a {@link org.mifosplatform.batch.command.internal.UpdateClientCommandStrategy} + * Request with given requestId and reference. + * + * @param reqId + * @param clientId + * @return BatchRequest + */ + public static BatchRequest updateClientRequest(final Long requestId, final Long reference) { + + final BatchRequest br = new BatchRequest(); + + br.setRequestId(requestId); + br.setRelativeUrl("clients/$.clientId"); + br.setMethod("PUT"); + br.setReference(reference); + br.setBody("{\"firstname\": \"TestFirstName\", \"lastname\": \"TestLastName\"}"); + + return br; + } + + /** + * Creates and returns a {@link org.mifosplatform.batch.command.internal.ApplyLoanCommandStrategy} + * Request with given requestId and reference. + * + * @param requestId + * @param reference + * @return BatchRequest + */ + public static BatchRequest applyLoanRequest(final Long requestId, final Long reference) { + + final BatchRequest br = new BatchRequest(); + + br.setRequestId(requestId); + br.setRelativeUrl("loans"); + br.setMethod("POST"); + br.setReference(reference); + + //NOTE: Set the value productId according to your db else will result in a '404' response + final String body = "{\"dateFormat\": \"dd MMMM yyyy\", \"locale\": \"en_GB\", \"clientId\": \"$.clientId\"," + + "\"productId\": 26, \"principal\": \"10,000.00\", \"loanTermFrequency\": 12," + + "\"loanTermFrequencyType\": 2, \"loanType\": \"individual\", \"numberOfRepayments\": 10," + + "\"repaymentEvery\": 1, \"repaymentFrequencyType\": 2, \"interestRatePerPeriod\": 10," + + "\"amortizationType\": 1, \"interestType\": 0, \"interestCalculationPeriodType\": 1," + + "\"transactionProcessingStrategyId\": 1, \"expectedDisbursementDate\": \"10 Jun 2013\"," + + "\"submittedOnDate\": \"10 Jun 2013\"}"; + br.setBody(body); + + return br; + } + + /** + * Checks that the client with given externalId is not created on the server. + * + * @param requestSpec + * @param responseSpec + * @param externalId + */ + public static void verifyClientCreatedOnServer(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String externalId) { + System.out.println("------------------------------CHECK CLIENT DETAILS------------------------------------\n"); + final String CLIENT_URL = "/mifosng-provider/api/v1/clients?externalId=" + externalId + "&tenantIdentifier=default"; + final Integer responseRecords = Utils.performServerGet(requestSpec, responseSpec, CLIENT_URL, "totalFilteredRecords"); + Assert.assertEquals("No records found with given externalId",(long) responseRecords, (long) 0); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/api/BatchApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/api/BatchApiResource.java index 44323d73a..6f80daeb4 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/batch/api/BatchApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/api/BatchApiResource.java @@ -41,8 +41,7 @@ import org.springframework.stereotype.Component; public class BatchApiResource { private final PlatformSecurityContext context; - @SuppressWarnings("rawtypes") - private final ToApiJsonSerializer toApiJsonSerializer; + private final ToApiJsonSerializer toApiJsonSerializer; private final BatchApiService service; private final BatchRequestJsonHelper batchRequestJsonHelper; @@ -56,9 +55,8 @@ public class BatchApiResource { * @param batchRequestJsonHelper */ @Autowired - public BatchApiResource(final PlatformSecurityContext context, @SuppressWarnings("rawtypes") - final ToApiJsonSerializer toApiJsonSerializer, final BatchApiService service, - final BatchRequestJsonHelper batchRequestJsonHelper) { + public BatchApiResource(final PlatformSecurityContext context, final ToApiJsonSerializer + toApiJsonSerializer, final BatchApiService service, final BatchRequestJsonHelper batchRequestJsonHelper) { this.context = context; this.toApiJsonSerializer = toApiJsonSerializer; @@ -88,7 +86,7 @@ public class BatchApiResource { //Gets back the consolidated BatchResponse from BatchApiservice List result = new ArrayList<>(); - //if the request is to be handled as a Transaction. All requests will be rolled back on error + //If the request is to be handled as a Transaction. All requests will be rolled back on error if(enclosingTransaction) { try { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/command/CommandContext.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/CommandContext.java index 31a782e0e..f4e291c0b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/batch/command/CommandContext.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/CommandContext.java @@ -48,6 +48,21 @@ public class CommandContext { public static Builder resource(final String resource) { return new Builder(resource); } + + /** + * Returns a boolean value if the relativeUrl 'matches' one of the regex keys in + * the available commandStrategies. It take CommandContext object as parameter + * which contains a 'resource' member as a regex key for available commandStrategies. + * + * @param other + * @return boolean + */ + public boolean matcher(CommandContext other) { + if(this.resource.matches(other.resource) && this.method.equals(other.method)) { + return true; + } + return false; + } @Override public int hashCode() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/command/CommandStrategyProvider.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/CommandStrategyProvider.java index 1190b7b58..c53f3e76c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/batch/command/CommandStrategyProvider.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/CommandStrategyProvider.java @@ -52,6 +52,12 @@ public class CommandStrategyProvider { return (CommandStrategy) this.applicationContext.getBean(this.commandStrategies.get(commandContext)); } + for(ConcurrentHashMap.Entry entry : this.commandStrategies.entrySet()) { + if(commandContext.matcher(entry.getKey())) { + return (CommandStrategy) this.applicationContext.getBean(this.commandStrategies.get(entry.getKey())); + } + } + return new UnknownCommandStrategy(); } @@ -61,7 +67,9 @@ public class CommandStrategyProvider { * within the constructor. */ private void init() { - this.commandStrategies.put(CommandContext.resource("clients").method("POST").build(), "CreateClientCommandStrategy"); + this.commandStrategies.put(CommandContext.resource("clients").method("POST").build(), "createClientCommandStrategy"); + this.commandStrategies.put(CommandContext.resource("clients\\/\\d+").method("PUT").build(), "updateClientCommandStrategy"); + this.commandStrategies.put(CommandContext.resource("loans").method("POST").build(), "applyLoanCommandStrategy"); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/ApplyLoanCommandStrategy.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/ApplyLoanCommandStrategy.java new file mode 100644 index 000000000..463c68bc5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/ApplyLoanCommandStrategy.java @@ -0,0 +1,69 @@ +package org.mifosplatform.batch.command.internal; + +import org.mifosplatform.batch.command.CommandStrategy; +import org.mifosplatform.batch.domain.BatchRequest; +import org.mifosplatform.batch.domain.BatchResponse; +import org.mifosplatform.batch.exception.ErrorHandler; +import org.mifosplatform.batch.exception.ErrorInfo; +import org.mifosplatform.portfolio.loanaccount.api.LoansApiResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.Gson; + +/** + * Implements {@link org.mifosplatform.batch.command.CommandStrategy} and applies a new loan + * on an existing client. It passes the contents of the body from the BatchRequest + * to {@link org.mifosplatform.portfolio.client.api.LoansApiResource} and gets back + * the response. This class will also catch any errors raised by + * {@link org.mifosplatform.portfolio.client.api.LoansApiResource} and map those errors + * to appropriate status codes in BatchResponse. + * + * @author Rishabh Shukla + * + * @see org.mifosplatform.batch.command.CommandStrategy + * @see org.mifosplatform.batch.domain.BatchRequest + * @see org.mifosplatform.batch.domain.BatchResponse + */ +@Component +public class ApplyLoanCommandStrategy implements CommandStrategy{ + + private final LoansApiResource loansApiResource; + + @Autowired + public ApplyLoanCommandStrategy(final LoansApiResource loansApiResource) { + this.loansApiResource = loansApiResource; + } + + @Override + public BatchResponse execute(BatchRequest request) { + + final BatchResponse response = new BatchResponse(); + final String responseBody; + + response.setRequestId(request.getRequestId()); + response.setHeaders(request.getHeaders()); + + //Try-catch blocks to map exceptions to appropriate status codes + try { + + //Calls 'SubmitLoanFunction' function from 'LoansApiResource' to Apply Loan to an existing client + responseBody = loansApiResource.calculateLoanScheduleOrSubmitLoanApplication(null, null, request.getBody()); + + response.setStatusCode(200); + //Sets the body of the response after loan is successfully applied + response.setBody(responseBody); + + } + catch (RuntimeException e) { + + //Gets an object of type ErrorInfo, containing information about raised exception + ErrorInfo ex = ErrorHandler.handler(e); + + response.setStatusCode(ex.getStatusCode()); + response.setBody(new Gson().toJson(ex)); + } + + return response; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/CreateClientCommandStrategy.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/CreateClientCommandStrategy.java index bcf4c10ab..913c0a926 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/CreateClientCommandStrategy.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/CreateClientCommandStrategy.java @@ -3,13 +3,13 @@ package org.mifosplatform.batch.command.internal; import org.mifosplatform.batch.command.CommandStrategy; import org.mifosplatform.batch.domain.BatchRequest; import org.mifosplatform.batch.domain.BatchResponse; -import org.mifosplatform.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; -import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; -import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; -import org.mifosplatform.infrastructure.core.exception.PlatformInternalServerException; -import org.mifosplatform.infrastructure.core.exception.UnsupportedParameterException; +import org.mifosplatform.batch.exception.ErrorHandler; +import org.mifosplatform.batch.exception.ErrorInfo; import org.mifosplatform.portfolio.client.api.ClientsApiResource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.Gson; /** * Implements {@link org.mifosplatform.batch.command.CommandStrategy} to handle @@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; * @see org.mifosplatform.batch.domain.BatchRequest * @see org.mifosplatform.batch.domain.BatchResponse */ +@Component public class CreateClientCommandStrategy implements CommandStrategy{ private final ClientsApiResource clientsApiResource; @@ -32,51 +33,36 @@ public class CreateClientCommandStrategy implements CommandStrategy{ @Autowired public CreateClientCommandStrategy(final ClientsApiResource clientsApiResource) { this.clientsApiResource = clientsApiResource; - } + } @Override public BatchResponse execute(final BatchRequest request) { final BatchResponse response = new BatchResponse(); - final String responseBody; + final String responseBody; + + response.setRequestId(request.getRequestId()); + response.setHeaders(request.getHeaders()); - // try-catch blocks to map exceptions to appropriate status codes + //Try-catch blocks to map exceptions to appropriate status codes try { - //calls 'create' function from 'ClientsApiResource' to create a new client + //Calls 'create' function from 'ClientsApiResource' to create a new client responseBody = clientsApiResource.create(request.getBody()); - response.setRequestId(request.getRequestId()); - response.setHeaders(request.getHeaders()); response.setStatusCode(200); - //sets the body of the response after the successful creation of the client + //Sets the body of the response after the successful creation of the client response.setBody(responseBody); - } catch (AbstractPlatformResourceNotFoundException e) { + } + catch (RuntimeException e) { - response.setStatusCode(404); - response.setBody("error : " + e.toString()); + //Gets an object of type ErrorInfo, containing information about raised exception + ErrorInfo ex = ErrorHandler.handler(e); - } catch (UnsupportedParameterException e) { - - response.setStatusCode(400); - response.setBody("error : " + e.toString()); - - } catch(PlatformApiDataValidationException e) { - - response.setStatusCode(400); - response.setBody("error : " + e.toString()); - - } catch (PlatformDataIntegrityException e) { - - response.setStatusCode(403); - response.setBody("error : " + e.toString()); - - } catch (PlatformInternalServerException e) { - - response.setStatusCode(500); - response.setBody("error : " + e.toString()); - } + response.setStatusCode(ex.getStatusCode()); + response.setBody(new Gson().toJson(ex)); + } return response; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/UnknownCommandStrategy.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/UnknownCommandStrategy.java index 21d783d10..27b9f7a1f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/UnknownCommandStrategy.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/UnknownCommandStrategy.java @@ -3,6 +3,7 @@ package org.mifosplatform.batch.command.internal; import org.mifosplatform.batch.command.CommandStrategy; import org.mifosplatform.batch.domain.BatchRequest; import org.mifosplatform.batch.domain.BatchResponse; +import org.springframework.stereotype.Component; /** * Provides a default CommandStrategy by implementing @@ -12,6 +13,7 @@ import org.mifosplatform.batch.domain.BatchResponse; * * @author Rishabh Shukla */ +@Component public class UnknownCommandStrategy implements CommandStrategy { @Override diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/UpdateClientCommandStrategy.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/UpdateClientCommandStrategy.java new file mode 100644 index 000000000..1bb9532c2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/command/internal/UpdateClientCommandStrategy.java @@ -0,0 +1,73 @@ +package org.mifosplatform.batch.command.internal; + +import org.mifosplatform.batch.command.CommandStrategy; +import org.mifosplatform.batch.domain.BatchRequest; +import org.mifosplatform.batch.domain.BatchResponse; +import org.mifosplatform.batch.exception.ErrorHandler; +import org.mifosplatform.batch.exception.ErrorInfo; +import org.mifosplatform.portfolio.client.api.ClientsApiResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.Gson; + +/** + * Implements {@link org.mifosplatform.batch.command.CommandStrategy} and updates the + * information of an existing client. It passes the contents of the body from the BatchRequest + * to {@link org.mifosplatform.portfolio.client.api.ClientsApiResource} and gets back + * the response. This class will also catch any errors raised by + * {@link org.mifosplatform.portfolio.client.api.ClientsApiResource} and map those errors + * to appropriate status codes in BatchResponse. + * + * @author Rishabh Shukla + * + * @see org.mifosplatform.batch.command.CommandStrategy + * @see org.mifosplatform.batch.domain.BatchRequest + * @see org.mifosplatform.batch.domain.BatchResponse + */ +@Component +public class UpdateClientCommandStrategy implements CommandStrategy { + + private final ClientsApiResource clientsApiResource; + + @Autowired + public UpdateClientCommandStrategy(final ClientsApiResource clientsApiResource) { + this.clientsApiResource = clientsApiResource; + } + + @Override + public BatchResponse execute(BatchRequest request) { + + final BatchResponse response = new BatchResponse(); + final String responseBody; + + response.setRequestId(request.getRequestId()); + response.setHeaders(request.getHeaders()); + + //Get the clientID + final String relativeUrl = request.getRelativeUrl(); + final Long clientId = Long.parseLong(relativeUrl.substring(relativeUrl.indexOf('/') + 1)); + + //Try-catch blocks to map exceptions to appropriate status codes + try { + + //Calls 'update' function from 'ClientsApiResource' to update a client + responseBody = clientsApiResource.update(clientId, request.getBody()); + + response.setStatusCode(200); + //Sets the body of the response after the successful update of client information + response.setBody(responseBody); + + } catch (RuntimeException e) { + + //Gets an object of type ErrorInfo, containing information about raised exception + ErrorInfo ex = ErrorHandler.handler(e); + + response.setStatusCode(ex.getStatusCode()); + response.setBody(new Gson().toJson(ex)); + } + + return response; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/exception/ErrorHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/exception/ErrorHandler.java new file mode 100644 index 000000000..acd43abce --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/exception/ErrorHandler.java @@ -0,0 +1,50 @@ +package org.mifosplatform.batch.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; +import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; +import org.mifosplatform.infrastructure.core.exception.UnsupportedParameterException; + +/** + * Provides an Error Handler method that returns an object of type {@link ErrorInfo} + * to the CommandStrategy which raised the exception. This class uses various subclasses + * of RuntimeException to check the kind of exception raised and provide appropriate + * status and error codes for each one of the raised exception. + * + * @author Rishabh Shukla + * + * @see org.mifosplatform.batch.command.CommandStrategy + * @see org.mifosplatform.batch.command.internal.CreateClientCommandStrategy + */ +public class ErrorHandler extends RuntimeException{ + + /** + * Sole Constructor + */ + ErrorHandler() { + super(); + } + + /** + * Returns an object of ErrorInfo type containing the information regarding + * the raised error. + * + * @param exception + * @return ErrorInfo + */ + public static ErrorInfo handler(RuntimeException exception) { + + if(exception instanceof AbstractPlatformResourceNotFoundException) { + return new ErrorInfo(404, 1001, "Requested Resource was not Found"); + } else if(exception instanceof UnsupportedParameterException) { + return new ErrorInfo(400, 2001, "UnsupportedParameterException: Check for any unsupported Parameters"); + } else if(exception instanceof PlatformApiDataValidationException) { + return new ErrorInfo(400, 2002, "ApiDataValidationException: Re-Check the input data"); + } else if(exception instanceof PlatformDataIntegrityException) { + return new ErrorInfo(403, 3001, "DataIntegrityException: Check for any duplicate data"); + } + + return new ErrorInfo(500, 9999, "Internal Server Error"); + + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/exception/ErrorInfo.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/exception/ErrorInfo.java new file mode 100644 index 000000000..8240bb4f1 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/exception/ErrorInfo.java @@ -0,0 +1,93 @@ +package org.mifosplatform.batch.exception; + +/** + * Provides members to hold the basic information about the exceptions raised in + * commandStrategy classes. + * + * @author Rishabh Shukla + * + * @see ErrorHandler + */ +public final class ErrorInfo { + + private Integer statusCode; + private Integer errorCode; + private String message; + + /** + * Constructor to initialize the members of this class. + * + * @param statusCode + * @param errorCode + * @param message + */ + public ErrorInfo(final Integer statusCode, final Integer errorCode, final String message) { + super(); + this.statusCode = statusCode; + this.errorCode = errorCode; + this.message = message; + } + + /** + * Constructor so JSON serialization will work with out special Serialiazer + */ + ErrorInfo() { + super(); + } + + /** + * Getter method to provide the statusCode for an object of this type. + * + * @return Integer + */ + public Integer getStatusCode() { + return this.statusCode; + } + + /** + * Setter method to set the statusCode for an object of this type. + * + * @param statusCode + */ + public void setStatusCode(final Integer statusCode) { + this.statusCode = statusCode; + } + + /** + * Getter method to provide the errorCode for an object of this type. + * + * @return Integer + */ + public Integer getErrorCode() { + return this.errorCode; + } + + /** + * Setter method to set the errorCode for an object of this type. + * + * @param errorCode + */ + public void setErrorCode(final Integer errorCode) { + this.errorCode = errorCode; + } + + /** + * Getter method to provide the message of the error for an object + * of this type. + * + * @return String + */ + public String getMessage() { + return this.message; + } + + /** + * Setter method to set the message of the error for an object + * of this type. + * + * @param message + */ + public void setMessage(final String message) { + this.message = message; + } + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/service/BatchApiServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/service/BatchApiServiceImpl.java index 96604924f..ce6c2ae08 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/batch/service/BatchApiServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/service/BatchApiServiceImpl.java @@ -2,16 +2,22 @@ package org.mifosplatform.batch.service; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import javax.ws.rs.core.Response.Status; import org.mifosplatform.batch.command.CommandContext; import org.mifosplatform.batch.command.CommandStrategy; import org.mifosplatform.batch.command.CommandStrategyProvider; import org.mifosplatform.batch.domain.BatchRequest; import org.mifosplatform.batch.domain.BatchResponse; +import org.mifosplatform.batch.exception.ErrorInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.google.gson.Gson; + /** * Implementation for {@link BatchApiService} to iterate through all the incoming * requests and obtain the appropriate CommandStrategy from CommandStrategyProvider. @@ -26,6 +32,7 @@ import org.springframework.transaction.annotation.Transactional; public class BatchApiServiceImpl implements BatchApiService{ private final CommandStrategyProvider strategyProvider; + private final ResolutionHelper resolutionHelper; /** * Constructs a 'BatchApiServiceImpl' with an argument of @@ -34,27 +41,77 @@ public class BatchApiServiceImpl implements BatchApiService{ * @param strategyProvider */ @Autowired - public BatchApiServiceImpl(final CommandStrategyProvider strategyProvider) { + public BatchApiServiceImpl(final CommandStrategyProvider strategyProvider, + final ResolutionHelper resolutionHelper) { this.strategyProvider = strategyProvider; + this.resolutionHelper = resolutionHelper; } + /** + * Returns the response list by getting a proper {@link org.mifosplatform.batch.command.CommandStrategy}. + * execute() method of acquired commandStrategy is then provided with the separate Request. + * + * @param requestList + * @return List + */ private List handleBatchRequests(final List requestList) { - final List responseList = new ArrayList<>(requestList.size()); + final List responseList = new ArrayList<>(); - for(final BatchRequest br: requestList) { + final ConcurrentHashMap> requestMap = this.resolutionHelper.getDependencyMap(requestList); + + for(ConcurrentHashMap.Entry> entry : requestMap.entrySet()) { - final CommandStrategy commandStrategy = this.strategyProvider.getCommandStrategy(CommandContext. - resource(br.getRelativeUrl()).method(br.getMethod()).build()); + final ArrayList mappedList = entry.getValue(); - final BatchResponse response = commandStrategy.execute(br); + for(BatchRequest br: mappedList) { + + //If this request is dependent on any other request in the list + if(br.getReference() != null) { + + BatchResponse parentResponse = new BatchResponse(); + + //Fetch the response of the request on which it is dependent from responseList + for(final BatchResponse res: responseList) { + + if(res.getRequestId().equals(br.getReference())) { + parentResponse = res; + } + } + + //If parentResponse has returned a successful request + if(parentResponse.getStatusCode() == 200) { + br = this.resolutionHelper.resoluteRequest(br, parentResponse); + } else { + //Something went wrong with the parent request, create a response with status code 409 + final BatchResponse response = new BatchResponse(); + response.setRequestId(br.getRequestId()); + response.setStatusCode(Status.CONFLICT.getStatusCode()); + + //Some detail information about the error + final ErrorInfo conflictError = new ErrorInfo(Status.CONFLICT.getStatusCode(), 8001, + "Parent request with id " + parentResponse.getRequestId() + " was erroneous!"); + response.setBody(new Gson().toJson(conflictError)); + responseList.add(response); + + //Proceed with next list entry + continue; + } + } + + final CommandStrategy commandStrategy = this.strategyProvider.getCommandStrategy(CommandContext. + resource(br.getRelativeUrl()).method(br.getMethod()).build()); + + final BatchResponse response = commandStrategy.execute(br); + + responseList.add(response); + } - responseList.add(response); } - + return responseList; } - + @Override public List handleBatchRequestsWithoutEnclosingTransaction(final List requestList) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/batch/service/ResolutionHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/batch/service/ResolutionHelper.java new file mode 100644 index 000000000..24512c873 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/batch/service/ResolutionHelper.java @@ -0,0 +1,176 @@ +package org.mifosplatform.batch.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import org.mifosplatform.batch.domain.BatchRequest; +import org.mifosplatform.batch.domain.BatchResponse; +import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.jayway.jsonpath.JsonModel; + +/** + * Provides methods to create dependency map among the various batchRequests. + * It also provides method that takes care of dependency resolution among + * related requests. + * + * @author Rishabh Shukla + * @see BatchApiServiceImpl + */ +@Component +public class ResolutionHelper { + + private FromJsonHelper fromJsonHelper; + + @Autowired + public ResolutionHelper(final FromJsonHelper fromJsonHelper) { + this.fromJsonHelper = fromJsonHelper; + } + + /** + * Returns a map containing requests that are divided in accordance of dependency relations + * among them. Each different list is identified with a "Key" which is the "requestId" of + * the request at topmost level in dependency hierarchy of that particular list. + * + * @param requestList + * @return ConcurrentHashMap> + */ + public ConcurrentHashMap> getDependencyMap(final List requestList) { + + //Create a map with a '-1L' which will contain a list of independent requests + final ConcurrentHashMap> dependencyMap = new ConcurrentHashMap<>(); + + dependencyMap.put(-1L, new ArrayList()); + + //For every request in the requestList + for(final BatchRequest br : requestList) { + + final Long reference = br.getReference(); + + //If current request is dependent on some other request + if(reference != null) { + + //Then find the request on which br is dependent on + for(final BatchRequest parentRequest : requestList) { + + //If request on which br is dependent is found + if(parentRequest.getRequestId().equals(reference)) { + + //Then start the process of adding it to one of the key in the dependencyMap + + //If referenced request is in the list with key "-1L" + if(dependencyMap.get(-1L).contains(parentRequest)) { + + //Then create a new list with key = reference + final ArrayList newList = new ArrayList<>(); + newList.add(parentRequest); + newList.add(br); + + //And add this list to the map + dependencyMap.put(reference, newList); + + //Also remove parentRequest from the list at '-1L' + final ArrayList listCopy = dependencyMap.get(-1L); + + listCopy.remove(parentRequest); + + dependencyMap.replace(-1L, listCopy); + } + //If referenced request is not in key '-1L' + else { + //Find the list in which it is present + for(final ConcurrentHashMap.Entry> entry: dependencyMap.entrySet()) { + if(entry.getValue().contains(parentRequest)) { + final ArrayList listCopy = entry.getValue(); + listCopy.add(br); + + //Add this request to this list + dependencyMap.replace(entry.getKey(), listCopy); + } + } + } + } + + } + } + //If request is independent then add it to '-1L' + else { + final ArrayList listCopy = dependencyMap.get(-1L); + listCopy.add(br); + + dependencyMap.replace(-1L, listCopy); + } + + } + + return dependencyMap; + } + + /** + * Returns a BatchRequest after dependency resolution. It takes a request and the + * response of the request it is dependent upon as its arguments and change the body + * or relativeUrl of the request according to parent Request. + * + * @param request + * @param lastResponse + * @return BatchRequest + */ + public BatchRequest resoluteRequest(final BatchRequest request,final BatchResponse parentResponse) { + + //Create a duplicate request + final BatchRequest br = request; + + final JsonModel responseJsonModel = JsonModel.model(parentResponse.getBody()); + + //Gets the body from current Request as a JsonObject + final JsonObject jsonRequestBody = this.fromJsonHelper.parse(request.getBody()).getAsJsonObject(); + + JsonObject jsonResultBody = new JsonObject(); + + //Iterate through each element in the requestBody to find dependent parameter + for(Entry element: jsonRequestBody.entrySet()) { + + String paramVal = element.getValue().getAsString(); + + //If a dependent parameter is found + if(paramVal.contains("$.")) { + + //Get the value of the parameter from parent response + final String resParamValue = responseJsonModel.get(paramVal).toString(); + + //Add the value after dependency resolution + jsonResultBody.add(element.getKey(), this.fromJsonHelper.parse(resParamValue)); + } + else { + jsonResultBody.add(element.getKey(), element.getValue()); + } + } + + //Set the body after dependency resolution + br.setBody(jsonResultBody.toString()); + + //Also check the relativeUrl for any dependency resolution + String relativeUrl = request.getRelativeUrl(); + + if(relativeUrl.contains("$.")) { + + final String parameter = relativeUrl.substring(relativeUrl.indexOf('/') + 1, relativeUrl.length()); + + //Get the value of the parameter from last response + final String resParamValue = responseJsonModel.get(parameter).toString(); + + relativeUrl = relativeUrl.replace(parameter, resParamValue); + + br.setRelativeUrl(relativeUrl); + } + + return br; + } + +} diff --git a/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml b/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml index 9a9d760d1..5986dd911 100644 --- a/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml +++ b/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml @@ -35,13 +35,11 @@ org.mifosplatform.template.*, org.mifosplatform.template.service.*, org.mifosplatform.useradministration.*, - org.mifosplatform.batch.*"> + org.mifosplatform.batch"> - @@ -55,8 +53,7 @@ - - +