Merge pull request #968 from rishy/Batch-API

added "UpdateClient" and "ApplyLoan" commands with dependency resolution
This commit is contained in:
Vishwas Babu A J
2014-06-23 15:16:39 -07:00
15 changed files with 794 additions and 103 deletions
+3 -2
View File
@@ -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
}
@@ -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<BatchResponse> response = BatchHelper.createRequest(this.requestSpec, this.responseSpec, br);
final List<BatchResponse> 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<BatchResponse> response = BatchHelper.createRequest(this.requestSpec, this.responseSpec, br);
final List<BatchResponse> 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<BatchRequest> batchRequests = new ArrayList<>();
@@ -103,7 +124,85 @@ public class BatchApiTest {
final List<BatchResponse> 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<BatchRequest> batchRequests = new ArrayList<>();
batchRequests.add(br1);
batchRequests.add(br2);
final String jsonifiedRequest = BatchHelper.toJsonString(batchRequests);
final List<BatchResponse> 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<BatchRequest> batchRequests = new ArrayList<>();
batchRequests.add(br1);
batchRequests.add(br2);
final String jsonifiedRequest = BatchHelper.toJsonString(batchRequests);
final List<BatchResponse> 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());
}
}
@@ -62,7 +62,7 @@ public class BatchHelper {
*/
public static List<BatchResponse> 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<BatchResponse>
*/
public static List<BatchResponse> createRequest(final RequestSpecification requestSpec,
public static List<BatchResponse> postWithSingleRequest(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec,final BatchRequest br) {
final List<BatchRequest> 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);
}
}
@@ -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<BatchResponse> 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<BatchResponse>
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<BatchResponse> 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 {
@@ -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() {
@@ -52,6 +52,12 @@ public class CommandStrategyProvider {
return (CommandStrategy) this.applicationContext.getBean(this.commandStrategies.get(commandContext));
}
for(ConcurrentHashMap.Entry<CommandContext,String> 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");
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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
@@ -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;
}
}
@@ -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");
}
}
@@ -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;
}
}
@@ -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<BatchResponse>
*/
private List<BatchResponse> handleBatchRequests(final List<BatchRequest> requestList) {
final List<BatchResponse> responseList = new ArrayList<>(requestList.size());
final List<BatchResponse> responseList = new ArrayList<>();
for(final BatchRequest br: requestList) {
final ConcurrentHashMap<Long, ArrayList<BatchRequest>> requestMap = this.resolutionHelper.getDependencyMap(requestList);
for(ConcurrentHashMap.Entry<Long, ArrayList<BatchRequest>> entry : requestMap.entrySet()) {
final CommandStrategy commandStrategy = this.strategyProvider.getCommandStrategy(CommandContext.
resource(br.getRelativeUrl()).method(br.getMethod()).build());
final ArrayList<BatchRequest> 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<BatchResponse> handleBatchRequestsWithoutEnclosingTransaction(final
List<BatchRequest> requestList) {
@@ -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<Long, List<BatchRequest>>
*/
public ConcurrentHashMap<Long, ArrayList<BatchRequest>> getDependencyMap(final List<BatchRequest> requestList) {
//Create a map with a '-1L' which will contain a list of independent requests
final ConcurrentHashMap<Long, ArrayList<BatchRequest>> dependencyMap = new ConcurrentHashMap<>();
dependencyMap.put(-1L, new ArrayList<BatchRequest>());
//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<BatchRequest> 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<BatchRequest> 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<Long, ArrayList<BatchRequest>> entry: dependencyMap.entrySet()) {
if(entry.getValue().contains(parentRequest)) {
final ArrayList<BatchRequest> 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<BatchRequest> 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<String, JsonElement> 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;
}
}
@@ -35,13 +35,11 @@
org.mifosplatform.template.*,
org.mifosplatform.template.service.*,
org.mifosplatform.useradministration.*,
org.mifosplatform.batch.*">
org.mifosplatform.batch">
<context:exclude-filter expression="org.springframework.stereotype.Controller"
type="annotation" />
</context:component-scan>
<bean id="CreateClientCommandStrategy"
class="org.mifosplatform.batch.command.internal.CreateClientCommandStrategy"/>
<bean id="auditorAware"
class="org.mifosplatform.infrastructure.core.domain.AuditorAwareImpl" />
<jpa:auditing auditor-aware-ref="auditorAware" />
@@ -55,8 +53,7 @@
<jpa:repositories base-package="org.mifosplatform.mix.domain" />
<jpa:repositories base-package="org.mifosplatform.scheduledjobs.domain" />
<jpa:repositories base-package="org.mifosplatform.template.domain" />
<jpa:repositories base-package="org.mifosplatform.batch.domain"/>
<import resource="infrastructure.xml" />
<import resource="securityContext.xml" />