MIFOSX-97: ability to support maker checker on create, update, delete of client.

This commit is contained in:
Keith Woodlock
2012-11-15 21:58:19 +00:00
parent e046d86e06
commit d7ea5d8169
28 changed files with 471 additions and 290 deletions
@@ -0,0 +1,19 @@
drop table `m_maker_checker`;
drop table `m_command_event_source`;
drop table `m_portfolio_command_source`;
CREATE TABLE `m_portfolio_command_source` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`api_operation` varchar(20) NOT NULL,
`api_resource` varchar(20) NOT NULL,
`resource_id` bigint(20) DEFAULT NULL,
`command_as_json` varchar(1000) NOT NULL,
`maker_id` bigint(20) NOT NULL,
`made_on_date` date NOT NULL,
`checker_id` bigint(20) DEFAULT NULL,
`checked_on_date` date DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_m_maker_m_appuser` (`maker_id`),
KEY `FK_m_checker_m_appuser` (`checker_id`),
CONSTRAINT `FK_m_maker_m_appuser` FOREIGN KEY (`maker_id`) REFERENCES `m_appuser` (`id`),
CONSTRAINT `FK_m_checker_m_appuser` FOREIGN KEY (`checker_id`) REFERENCES `m_appuser` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
@@ -28,8 +28,8 @@ import org.mifosng.platform.api.commands.ClientCommand;
import org.mifosng.platform.api.commands.NoteCommand;
import org.mifosng.platform.api.data.ClientAccountSummaryCollectionData;
import org.mifosng.platform.api.data.ClientData;
import org.mifosng.platform.api.data.CommandSourceData;
import org.mifosng.platform.api.data.EntityIdentifier;
import org.mifosng.platform.api.data.MakerCheckerData;
import org.mifosng.platform.api.data.NoteData;
import org.mifosng.platform.api.data.OfficeLookup;
import org.mifosng.platform.api.infrastructure.PortfolioApiDataConversionService;
@@ -42,8 +42,8 @@ import org.mifosng.platform.common.FileUtils;
import org.mifosng.platform.exceptions.ClientNotFoundException;
import org.mifosng.platform.exceptions.ImageNotFoundException;
import org.mifosng.platform.infrastructure.api.ApiParameterHelper;
import org.mifosng.platform.makerchecker.service.PortfolioMakerCheckerReadPlatformService;
import org.mifosng.platform.makerchecker.service.PortfolioMakerCheckerWriteService;
import org.mifosng.platform.makerchecker.service.PortfolioCommandSourceWritePlatformService;
import org.mifosng.platform.makerchecker.service.PortfolioCommandsReadPlatformService;
import org.mifosng.platform.organisation.service.OfficeReadPlatformService;
import org.mifosng.platform.security.PlatformSecurityContext;
import org.slf4j.Logger;
@@ -69,22 +69,22 @@ public class ClientsApiResource {
@Autowired
private ClientWritePlatformService clientWritePlatformService;
@Autowired
private PortfolioCommandsReadPlatformService commandSourceReadPlatformService;
@Autowired
private PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@Autowired
private OfficeReadPlatformService officeReadPlatformService;
@Autowired
private PortfolioApiDataConversionService apiDataConversionService;
@Autowired
private PortfolioMakerCheckerWriteService portfolioMakerCheckerService;
@Autowired
private OfficeReadPlatformService officeReadPlatformService;
@Autowired
private PortfolioApiJsonSerializerService apiJsonSerializerService;
@Autowired
private PortfolioMakerCheckerReadPlatformService makerCheckerReadPlatformService;
@Autowired
private PlatformSecurityContext context;
@@ -169,12 +169,19 @@ public class ClientsApiResource {
final Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
final boolean template = ApiParameterHelper.template(uriInfo.getQueryParameters());
final Long makerCheckerId = ApiParameterHelper.makerCheckerId(uriInfo.getQueryParameters());
ClientData clientData = this.clientReadPlatformService.retrieveIndividualClient(clientId);
if (template) {
final List<OfficeLookup> allowedOffices = new ArrayList<OfficeLookup>(officeReadPlatformService.retrieveAllOfficesForLookup());
clientData = ClientData.templateOnTop(clientData, allowedOffices);
}
if (makerCheckerId != null) {
final CommandSourceData entry = this.commandSourceReadPlatformService.retrieveById(makerCheckerId);
final ClientCommand command = this.apiDataConversionService.convertJsonToClientCommand(null, entry.json(), false);
clientData = ClientData.integrateChanges(clientData, command);
}
return this.apiJsonSerializerService.serializeClientDataToJson(prettyPrint, responseParameters, clientData);
}
@@ -193,10 +200,10 @@ public class ClientsApiResource {
ClientData clientData = this.clientReadPlatformService.retrieveNewClientDetails();
if (makerCheckerId != null) {
final MakerCheckerData entry = this.makerCheckerReadPlatformService.retrieveById(makerCheckerId);
final ClientCommand command = this.apiDataConversionService.convertJsonToClientCommand(null, entry.json());
final CommandSourceData entry = this.commandSourceReadPlatformService.retrieveById(makerCheckerId);
final ClientCommand command = this.apiDataConversionService.convertJsonToClientCommand(null, entry.json(), false);
clientData = ClientData.mergeWithCommand(clientData, command);
clientData = ClientData.integrateChanges(clientData, command);
}
return this.apiJsonSerializerService.serializeClientDataToJson(prettyPrint, responseParameters, clientData);
@@ -205,24 +212,11 @@ public class ClientsApiResource {
@POST
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public String enrollClient(final String jsonRequestBody) {
public String createClient(final String jsonRequestBody) {
final ClientCommand command = this.apiDataConversionService.convertJsonToClientCommand(null, jsonRequestBody);
command.validateForCreate();
final EntityIdentifier result = this.commandsSourceWritePlatformService.logCommandSource("CREATE", "clients", null, jsonRequestBody);
String jsonResult = "";
// hardcode to false before checking in until configuration for enabling is done.
final boolean makerCheckerEnabledForTask = false;
if (makerCheckerEnabledForTask) {
final Long makerCheckerId = this.portfolioMakerCheckerService.logNewEntry("CREATE_CLIENT", jsonRequestBody);
jsonResult = this.apiJsonSerializerService.serializeEntityIdentifier(EntityIdentifier.makerChecker(makerCheckerId));
} else {
final Long clientId = this.clientWritePlatformService.enrollClient(command);
jsonResult = this.apiJsonSerializerService.serializeEntityIdentifier(new EntityIdentifier(clientId));
}
return jsonResult;
return this.apiJsonSerializerService.serializeEntityIdentifier(result);
}
@PUT
@@ -231,11 +225,9 @@ public class ClientsApiResource {
@Produces({MediaType.APPLICATION_JSON})
public String updateClient(@PathParam("clientId") final Long clientId, final String jsonRequestBody) {
final ClientCommand command = this.apiDataConversionService.convertJsonToClientCommand(clientId, jsonRequestBody);
final EntityIdentifier identifier = this.clientWritePlatformService.updateClientDetails(command);
return this.apiJsonSerializerService.serializeEntityIdentifier(identifier);
final EntityIdentifier result = this.commandsSourceWritePlatformService.logCommandSource("UPDATE", "clients", clientId, jsonRequestBody);
return this.apiJsonSerializerService.serializeEntityIdentifier(result);
}
@DELETE
@@ -244,9 +236,9 @@ public class ClientsApiResource {
@Produces({MediaType.APPLICATION_JSON})
public String deleteClient(@PathParam("clientId") final Long clientId) {
this.clientWritePlatformService.deleteClient(clientId);
return this.apiJsonSerializerService.serializeEntityIdentifier(new EntityIdentifier(clientId));
final EntityIdentifier result = this.commandsSourceWritePlatformService.logCommandSource("DELETE", "clients", clientId, "{}");
return this.apiJsonSerializerService.serializeEntityIdentifier(result);
}
@GET
@@ -4,6 +4,7 @@ import java.util.Collection;
import java.util.Set;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
@@ -16,27 +17,27 @@ import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang.StringUtils;
import org.mifosng.platform.api.data.EntityIdentifier;
import org.mifosng.platform.api.data.MakerCheckerData;
import org.mifosng.platform.api.data.CommandSourceData;
import org.mifosng.platform.api.infrastructure.PortfolioApiJsonSerializerService;
import org.mifosng.platform.exceptions.UnrecognizedQueryParamException;
import org.mifosng.platform.infrastructure.api.ApiParameterHelper;
import org.mifosng.platform.makerchecker.service.PortfolioMakerCheckerReadPlatformService;
import org.mifosng.platform.makerchecker.service.PortfolioMakerCheckerWriteService;
import org.mifosng.platform.security.PlatformSecurityContext;
import org.mifosng.platform.makerchecker.service.PortfolioCommandsReadPlatformService;
import org.mifosng.platform.makerchecker.service.PortfolioCommandSourceWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Path("/makerchecker")
@Path("/commands")
@Component
@Scope("singleton")
public class MakerCheckerApiResource {
public class CommandsApiResource {
@Autowired
private PortfolioMakerCheckerReadPlatformService readPlatformService;
private PortfolioCommandsReadPlatformService readPlatformService;
@Autowired
private PortfolioMakerCheckerWriteService writePlatformService;
private PortfolioCommandSourceWritePlatformService writePlatformService;
@Autowired
private PortfolioApiJsonSerializerService apiJsonSerializerService;
@@ -55,7 +56,7 @@ public class MakerCheckerApiResource {
final Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
final Collection<MakerCheckerData> entries = this.readPlatformService.retrieveAllEntriesToBeChecked();
final Collection<CommandSourceData> entries = this.readPlatformService.retrieveAllEntriesToBeChecked();
return this.apiJsonSerializerService.serializeMakerCheckerDataToJson(prettyPrint, responseParameters, entries);
}
@@ -68,18 +69,30 @@ public class MakerCheckerApiResource {
@PathParam("makerCheckerId") final Long makerCheckerId,
@QueryParam("command") final String commandParam) {
Long id = null;
EntityIdentifier result = null;
if (is(commandParam, "approve")) {
id = this.writePlatformService.approveEntry(makerCheckerId);
result = this.writePlatformService.approveEntry(makerCheckerId);
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.apiJsonSerializerService.serializeEntityIdentifier(EntityIdentifier.makerChecker(id));
return this.apiJsonSerializerService.serializeEntityIdentifier(result);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam)
&& commandParam.trim().equalsIgnoreCase(commandValue);
}
@DELETE
@Path("{makerCheckerId}")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public String deleteMakerCheckerEntry(
@PathParam("makerCheckerId") final Long makerCheckerId) {
final Long id = this.writePlatformService.deleteEntry(makerCheckerId);
return this.apiJsonSerializerService.serializeEntityIdentifier(EntityIdentifier.makerChecker(id));
}
}
@@ -3,8 +3,10 @@ package org.mifosng.platform.api.commands;
import java.util.Set;
import org.joda.time.LocalDate;
import org.mifosng.platform.client.service.ClientCommandValidator;
/**
*
*/
public class ClientCommand {
private final Long id;
@@ -14,11 +16,11 @@ public class ClientCommand {
private final String clientOrBusinessName;
private final Long officeId;
private final LocalDate joiningDate;
private final Set<String> modifiedParameters;
private final ClientCommandValidator validator;
// made transient as dont want them to be serialized/converted to JSON
private final transient boolean makerCheckerApproval;
private final transient Set<String> parametersPassedInRequest;
public ClientCommand(
final Set<String> modifiedParameters,
final Long id,
@@ -27,8 +29,9 @@ public class ClientCommand {
final String lastname,
final String clientOrBusinessName,
final Long officeId,
final LocalDate joiningDate) {
this.modifiedParameters = modifiedParameters;
final LocalDate joiningDate,
final boolean makerCheckerApproval) {
this.parametersPassedInRequest = modifiedParameters;
this.id = id;
this.externalId = externalId;
this.firstname = firstname;
@@ -36,7 +39,7 @@ public class ClientCommand {
this.clientOrBusinessName = clientOrBusinessName;
this.officeId = officeId;
this.joiningDate = joiningDate;
this.validator = new ClientCommandValidator(this);
this.makerCheckerApproval = makerCheckerApproval;
}
public Long getId() {
@@ -68,30 +71,30 @@ public class ClientCommand {
}
public boolean isFirstnameChanged() {
return this.modifiedParameters.contains("firstname");
return this.parametersPassedInRequest.contains("firstname");
}
public boolean isLastnameChanged() {
return this.modifiedParameters.contains("lastname");
return this.parametersPassedInRequest.contains("lastname");
}
public boolean isClientOrBusinessNameChanged() {
return this.modifiedParameters.contains("clientOrBusinessName");
return this.parametersPassedInRequest.contains("clientOrBusinessName");
}
public boolean isExternalIdChanged() {
return this.modifiedParameters.contains("externalId");
return this.parametersPassedInRequest.contains("externalId");
}
public boolean isJoiningDateChanged() {
return this.modifiedParameters.contains("joiningDate");
return this.parametersPassedInRequest.contains("joiningDate");
}
public boolean isOfficeChanged() {
return this.modifiedParameters.contains("officeId");
return this.parametersPassedInRequest.contains("officeId");
}
public void validateForCreate() {
this.validator.validateForCreate();
public boolean isApprovedByChecker() {
return this.makerCheckerApproval;
}
}
@@ -2,7 +2,6 @@ package org.mifosng.platform.api.data;
import java.util.List;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosng.platform.api.commands.ClientCommand;
@@ -24,27 +23,53 @@ final public class ClientData {
private final String imageKey;
@SuppressWarnings("unused")
private final boolean imagePresent;
private final List<OfficeLookup> allowedOffices;
// essential override existing data with data from command
public static ClientData mergeWithCommand(final ClientData clientData, final ClientCommand command) {
final Long officeId = (Long) ObjectUtils.defaultIfNull(command.getId(), clientData.officeId);
final String firstname = (String) ObjectUtils.defaultIfNull(command.getFirstname(), clientData.firstname);
final String lastname = (String) ObjectUtils.defaultIfNull(command.getLastname(), clientData.lastname);
final String clientOrBusinessName = (String) ObjectUtils.defaultIfNull(command.getClientOrBusinessName(), clientData.clientOrBusinessName);
final String externalId = (String) ObjectUtils.defaultIfNull(command.getExternalId(), clientData.externalId);
final LocalDate joinedDate = (LocalDate) ObjectUtils.defaultIfNull(command.getJoiningDate(), clientData.joinedDate);
return new ClientData(officeId, clientData.officeName, clientData.id, firstname, lastname, clientOrBusinessName, externalId, joinedDate, clientData.imageKey, clientData.allowedOffices);
@SuppressWarnings("unused")
private final ClientCommand changes;
private static String buildDisplayNameFrom(final String firstname, final String lastname) {
String displayName = null;
StringBuilder displayNameBuilder = new StringBuilder();
if (StringUtils.isNotBlank(firstname)) {
displayNameBuilder.append(firstname).append(' ');
}
if (StringUtils.isNotBlank(lastname)) {
displayNameBuilder.append(lastname);
displayName = displayNameBuilder.toString();
}
return displayName;
}
public static ClientData integrateChanges(final ClientData clientData, final ClientCommand command) {
String firstname = clientData.firstname;
String lastname = clientData.lastname;
if (StringUtils.isNotBlank(clientData.clientOrBusinessName)) {
firstname = null;
lastname = clientData.clientOrBusinessName;
}
final String displayName = buildDisplayNameFrom(firstname, lastname);
return new ClientData(clientData.officeId, clientData.officeName, clientData.id, firstname, lastname, displayName,
clientData.externalId, clientData.joinedDate, clientData.imageKey, clientData.allowedOffices, command);
}
public static ClientData template(final Long officeId, final LocalDate joinedDate, final List<OfficeLookup> allowedOffices) {
return new ClientData(officeId, null, null, null, null, null, null, joinedDate, null, allowedOffices);
return new ClientData(officeId, null, null, null, null, null, null, joinedDate, null, allowedOffices, null);
}
public static ClientData templateOnTop(final ClientData clientData, final List<OfficeLookup> allowedOffices) {
return new ClientData(clientData.officeId, clientData.officeName, clientData.id, clientData.firstname, clientData.lastname, clientData.clientOrBusinessName, clientData.externalId, clientData.joinedDate, clientData.imageKey, allowedOffices);
String firstname = clientData.firstname;
String lastname = clientData.lastname;
if (StringUtils.isNotBlank(clientData.clientOrBusinessName)) {
firstname = null;
lastname = clientData.clientOrBusinessName;
}
final String displayName = buildDisplayNameFrom(firstname, lastname);
return new ClientData(clientData.officeId, clientData.officeName, clientData.id, firstname, lastname, displayName,
clientData.externalId, clientData.joinedDate, clientData.imageKey, allowedOffices, null);
}
public static ClientData clientIdentifier(
@@ -54,46 +79,43 @@ final public class ClientData {
final Long officeId,
final String officeName) {
StringBuilder displayNameBuilder = new StringBuilder(firstname);
if (StringUtils.isNotBlank(displayNameBuilder.toString())) {
displayNameBuilder.append(' ');
}
displayNameBuilder.append(lastname);
final String displayName = buildDisplayNameFrom(firstname, lastname);
return new ClientData(officeId, officeName, id, firstname, lastname, displayNameBuilder.toString(), null, null, null, null);
return new ClientData(officeId, officeName, id, firstname, lastname, displayName, null, null, null, null, null);
}
public ClientData(final Long officeId, final String officeName,
final Long id, final String firstname, final String lastname,
final String displayName, final String externalId,
public ClientData(
final Long officeId,
final String officeName,
final Long id,
final String firstname,
final String lastname,
final String displayName,
final String externalId,
final LocalDate joinedDate,
final String imageKey,
final List<OfficeLookup> allowedOffices) {
final List<OfficeLookup> allowedOffices,
final ClientCommand changes) {
this.officeId = officeId;
this.officeName = officeName;
this.id = id;
this.firstname = firstname;
this.displayName = displayName;
/*** unset last name for business name **/
if (StringUtils.isBlank(this.firstname)) {
if (StringUtils.isBlank(firstname)) {
this.lastname = null;
this.clientOrBusinessName = lastname;
} else {
this.lastname = lastname;
this.clientOrBusinessName = null;
}
this.displayName = displayName;
this.externalId = externalId;
this.joinedDate = joinedDate;
this.imageKey = imageKey;
if (imageKey == null) {
imagePresent = false;
}else{
imagePresent = true;
}
this.imagePresent = (imageKey != null);
this.allowedOffices = allowedOffices;
this.changes = changes;
}
public String displayName() {
@@ -0,0 +1,39 @@
package org.mifosng.platform.api.data;
import org.joda.time.LocalDate;
/**
* Immutable data object representing maker-checker entry
*/
final public class CommandSourceData {
@SuppressWarnings("unused")
private Long id;
@SuppressWarnings("unused")
private String apiOperation;
@SuppressWarnings("unused")
private String resource;
@SuppressWarnings("unused")
private final Long resourceId;
private String commandJson;
@SuppressWarnings("unused")
private final LocalDate madeOnDate;
public CommandSourceData(final Long id, final String apiOperation, final String resource, final Long resourceId, final String commandJson, final LocalDate madeOnDate) {
this.id = id;
this.apiOperation = apiOperation;
this.resource = resource;
this.resourceId = resourceId;
this.commandJson = commandJson;
this.madeOnDate = madeOnDate;
}
public String json() {
return this.commandJson;
}
}
@@ -13,6 +13,10 @@ public class EntityIdentifier {
return new EntityIdentifier(null, makerCheckerId);
}
public static EntityIdentifier makerChecker(final Long resourceId, final Long makerCheckerId) {
return new EntityIdentifier(resourceId, makerCheckerId);
}
public EntityIdentifier() {
//
}
@@ -1,31 +0,0 @@
package org.mifosng.platform.api.data;
import org.joda.time.LocalDate;
/**
* Immutable data object representing maker-checker entry
*/
final public class MakerCheckerData {
@SuppressWarnings("unused")
private Long id;
@SuppressWarnings("unused")
private String taskName;
private String taskJson;
@SuppressWarnings("unused")
private final LocalDate madeOnDate;
public MakerCheckerData(final Long id, final String taskName, final String taskJson, final LocalDate madeOnDate) {
this.id = id;
this.taskName = taskName;
this.taskJson = taskJson;
this.madeOnDate = madeOnDate;
}
public String json() {
return this.taskJson;
}
}
@@ -30,7 +30,7 @@ import org.mifosng.platform.api.data.LoanChargeData;
import org.mifosng.platform.api.data.LoanProductData;
import org.mifosng.platform.api.data.LoanReassignmentData;
import org.mifosng.platform.api.data.LoanTransactionData;
import org.mifosng.platform.api.data.MakerCheckerData;
import org.mifosng.platform.api.data.CommandSourceData;
import org.mifosng.platform.api.data.NoteData;
import org.mifosng.platform.api.data.OfficeData;
import org.mifosng.platform.api.data.OfficeTransactionData;
@@ -729,12 +729,12 @@ public class GoogleGsonPortfolioApiJsonSerializerService implements PortfolioApi
public String serializeMakerCheckerDataToJson(
final boolean prettyPrint,
final Set<String> responseParameters,
final Collection<MakerCheckerData> entries) {
final Collection<CommandSourceData> entries) {
final Gson gsonDeserializer = helper
.createGsonBuilderWithParameterExclusionSerializationStrategy(
MAKER_CHECKER_DATA_PARAMETERS, prettyPrint, responseParameters);
return helper.serializedJsonFrom(gsonDeserializer,
entries.toArray(new MakerCheckerData[entries.size()]));
entries.toArray(new CommandSourceData[entries.size()]));
}
@Override
@@ -49,7 +49,7 @@ public interface PortfolioApiDataConversionService {
DepositProductCommand convertJsonToDepositProductCommand(Long resourceIdentifier, String json);
ClientCommand convertJsonToClientCommand(Long resourceIdentifier, String json);
ClientCommand convertJsonToClientCommand(Long resourceIdentifier, String json, boolean makerCheckerApproval);
GroupCommand convertJsonToGroupCommand(Long resourceIdentifier, String json);
@@ -351,30 +351,33 @@ public class PortfolioApiDataConversionServiceImpl implements PortfolioApiDataCo
}
@Override
public ClientCommand convertJsonToClientCommand(final Long resourceIdentifier, final String json) {
public ClientCommand convertJsonToClientCommand(final Long resourceIdentifier, final String json, final boolean makerCheckerApproval) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
Type typeOfMap = new TypeToken<Map<String, String>>(){}.getType();
Map<String, String> requestMap = gsonConverter.fromJson(json, typeOfMap);
final Type typeOfMap = new TypeToken<Map<String, String>>(){}.getType();
final Map<String, String> requestMap = gsonConverter.fromJson(json, typeOfMap);
Set<String> supportedParams = new HashSet<String>(
final Set<String> supportedParams = new HashSet<String>(
Arrays.asList("externalId", "firstname", "lastname", "clientOrBusinessName", "officeId", "joiningDate", "locale", "dateFormat")
);
checkForUnsupportedParameters(requestMap, supportedParams);
Set<String> modifiedParameters = new HashSet<String>();
String externalId = extractStringParameter("externalId", requestMap, modifiedParameters);
Long officeId = extractLongParameter("officeId", requestMap, modifiedParameters);
LocalDate joiningDate = extractLocalDateParameter("joiningDate", requestMap, modifiedParameters);
String firstname = extractStringParameter("firstname", requestMap, modifiedParameters);
String lastname = extractStringParameter("lastname", requestMap, modifiedParameters);
String clientOrBusinessName = extractStringParameter("clientOrBusinessName", requestMap, modifiedParameters);
final JsonParser parser = new JsonParser();
final JsonElement element = parser.parse(json);
final JsonParserHelper helper = new JsonParserHelper();
final Set<String> parametersPassedInRequest = new HashSet<String>();
return new ClientCommand(modifiedParameters, resourceIdentifier, externalId, firstname, lastname, clientOrBusinessName, officeId, joiningDate);
final Long officeId = helper.extractLongNamed("officeId", element, parametersPassedInRequest);
final String externalId = helper.extractStringNamed("externalId", element, parametersPassedInRequest);
final String firstname = helper.extractStringNamed("firstname", element, parametersPassedInRequest);
final String lastname = helper.extractStringNamed("lastname", element, parametersPassedInRequest);
final String clientOrBusinessName = helper.extractStringNamed("clientOrBusinessName", element, parametersPassedInRequest);
final LocalDate joiningDate = helper.extractLocalDateNamed("joiningDate", element, parametersPassedInRequest);
return new ClientCommand(parametersPassedInRequest, resourceIdentifier, externalId, firstname, lastname, clientOrBusinessName, officeId, joiningDate, makerCheckerApproval);
}
@Override
@@ -28,7 +28,7 @@ import org.mifosng.platform.api.data.LoanChargeData;
import org.mifosng.platform.api.data.LoanProductData;
import org.mifosng.platform.api.data.LoanReassignmentData;
import org.mifosng.platform.api.data.LoanTransactionData;
import org.mifosng.platform.api.data.MakerCheckerData;
import org.mifosng.platform.api.data.CommandSourceData;
import org.mifosng.platform.api.data.NoteData;
import org.mifosng.platform.api.data.OfficeData;
import org.mifosng.platform.api.data.OfficeTransactionData;
@@ -136,7 +136,7 @@ public interface PortfolioApiJsonSerializerService {
String serializeCodeDataToJson(boolean prettyPrint,Set<String> responseParameters, CodeData code);
String serializeMakerCheckerDataToJson(boolean prettyPrint,Set<String> responseParameters, Collection<MakerCheckerData> entries);
String serializeMakerCheckerDataToJson(boolean prettyPrint,Set<String> responseParameters, Collection<CommandSourceData> entries);
String serializeSavingAccountsDataToJson(boolean prettyPrint, Set<String> responseParameters, SavingAccountData account);
@@ -133,7 +133,7 @@ public class Client extends AbstractAuditableCustom<AppUser, Long> {
}
if (command.isExternalIdChanged()) {
this.externalId = StringUtils.defaultIfEmpty(command.getExternalId().trim(), null);
this.externalId = StringUtils.defaultIfEmpty(command.getExternalId(), null);
}
}
@@ -164,7 +164,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
final String officeName = rs.getString("officeName");
return new ClientData(officeId, officeName, id, firstname,
lastname, displayName, externalId, joinedDate,imageKey, null);
lastname, displayName, externalId, joinedDate,imageKey, null, null);
}
}
@@ -12,13 +12,13 @@ import org.springframework.security.access.prepost.PreAuthorize;
public interface ClientWritePlatformService {
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'CREATE_CLIENT')")
Long enrollClient(ClientCommand command);
Long createClient(ClientCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'UPDATE_CLIENT')")
EntityIdentifier updateClientDetails(ClientCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'DELETE_CLIENT')")
EntityIdentifier deleteClient(Long clientId);
EntityIdentifier deleteClient(ClientCommand command);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'PORTFOLIO_MANAGEMENT_SUPER_USER', 'CREATE_CLIENTIMAGE')")
EntityIdentifier saveOrUpdateClientImage(Long clientId, String imageName, InputStream inputStream);
@@ -50,6 +50,9 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
private final NoteRepository noteRepository;
private final CodeValueRepository codeValueRepository;
// FIXME - kw - this value is hardcoded for now but will be retrieved from coinfiguration
private boolean makerCheckerEnabledForTask = false;
@Autowired
public ClientWritePlatformServiceJpaRepositoryImpl(
final PlatformSecurityContext context,
@@ -68,8 +71,9 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
@Transactional
@Override
public EntityIdentifier deleteClient(final Long clientId) {
public EntityIdentifier deleteClient(final ClientCommand command) {
final Long clientId = command.getId();
final Client client = this.clientRepository.findOne(clientId);
if (client == null || client.isDeleted()) {
throw new ClientNotFoundException(clientId);
@@ -77,6 +81,10 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
client.delete();
this.clientRepository.save(client);
if (this.makerCheckerEnabledForTask && !command.isApprovedByChecker()) {
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException();
}
return new EntityIdentifier(client.getId());
}
@@ -105,7 +113,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
@Transactional
@Override
public Long enrollClient(final ClientCommand command) {
public Long createClient(final ClientCommand command) {
try {
context.authenticatedUser();
@@ -125,12 +133,16 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
firstname = null;
}
Client newClient = Client
final Client newClient = Client
.newClient(clientOffice, firstname, lastname,
command.getJoiningDate(), command.getExternalId());
this.clientRepository.save(newClient);
if (this.makerCheckerEnabledForTask && !command.isApprovedByChecker()) {
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException();
}
return newClient.getId();
} catch (DataIntegrityViolationException dve) {
handleDataIntegrityIssues(command, dve);
@@ -164,6 +176,10 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
clientForUpdate.update(clientOffice, command);
this.clientRepository.saveAndFlush(clientForUpdate);
if (this.makerCheckerEnabledForTask && !command.isApprovedByChecker()) {
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException();
}
return new EntityIdentifier(clientForUpdate.getId());
} catch (DataIntegrityViolationException dve) {
@@ -0,0 +1,5 @@
package org.mifosng.platform.client.service;
public class RollbackTransactionAsCommandIsNotApprovedByCheckerException extends RuntimeException {
}
@@ -1,7 +1,19 @@
package org.mifosng.platform.common;
/**
* @deprecated - KW - in general would like to avoid the whole throw it in common approach and dont want to see
* - this file attract more application constants.
*/
@Deprecated
public class ApplicationConstants {
/**
* The intention of m_code table usage is not to use the id but the unique name of the code.
*
* This will stop reserved magic numbers spreading through application for one and it
* more natural to use the code name like you would use a table name in a query.
*/
@Deprecated
public static final Long CLIENT_IDENTITY_CODE_ID = 9L;
// TODO:Vishwas Need to move these settings to the Database
@@ -42,13 +42,13 @@ public class JsonParserHelper {
}
public Long extractLongNamed(final String parameterName,
final JsonElement element, final Set<String> modifiedParameters) {
final JsonElement element, final Set<String> parametersPassedInRequest) {
Long longValue = null;
if (element.isJsonObject()) {
JsonObject object = element.getAsJsonObject();
if (object.has(parameterName)
&& object.get(parameterName).isJsonPrimitive()) {
modifiedParameters.add(parameterName);
parametersPassedInRequest.add(parameterName);
JsonPrimitive primitive = object.get(parameterName)
.getAsJsonPrimitive();
final String stringValue = primitive.getAsString();
@@ -60,12 +60,12 @@ public class JsonParserHelper {
return longValue;
}
public String extractStringNamed(final String parameterName, final JsonElement element, final Set<String> modifiedParameters) {
public String extractStringNamed(final String parameterName, final JsonElement element, final Set<String> parametersPassedInRequest) {
String stringValue = null;
if (element.isJsonObject()) {
JsonObject object = element.getAsJsonObject();
if (object.has(parameterName) && object.get(parameterName).isJsonPrimitive()) {
modifiedParameters.add(parameterName);
parametersPassedInRequest.add(parameterName);
final JsonPrimitive primitive = object.get(parameterName).getAsJsonPrimitive();
final String valueAsString = primitive.getAsString();
if (StringUtils.isNotBlank(valueAsString)) {
@@ -16,15 +16,20 @@ import org.mifosng.platform.user.domain.AppUser;
import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity
@Table(name = "m_maker_checker")
public class MakerChecker extends AbstractPersistable<Long> {
@Table(name = "m_portfolio_command_source")
public class CommandSource extends AbstractPersistable<Long> {
@SuppressWarnings("unused")
@Column(name = "task_name", length = 100)
private String taskName;
@Column(name = "task_json", length = 1000)
private String taskJson;
@Column(name = "api_operation", length = 20)
private String apiOperation;
@Column(name = "api_resource", length = 20)
private String resource;
@Column(name = "resource_id")
private Long resourceId;
@Column(name = "command_as_json", length = 1000)
private String commandAsJson;
// should maker and checker be a staff member as opposed to just any application user?
@SuppressWarnings("unused")
@@ -47,25 +52,31 @@ public class MakerChecker extends AbstractPersistable<Long> {
@Temporal(TemporalType.DATE)
private Date checkedOnDate;
public static MakerChecker makerEntry(
final String taskName,
public static CommandSource createdBy(
final String apiOperation,
final String resource,
final Long resourceId,
final String jsonRequestBody,
final AppUser maker,
final LocalDate madeOnDate) {
return new MakerChecker(taskName, jsonRequestBody, maker, madeOnDate);
return new CommandSource(apiOperation, resource, resourceId, jsonRequestBody, maker, madeOnDate);
}
protected MakerChecker() {
protected CommandSource() {
//
}
private MakerChecker(
final String taskName,
final String taskJson,
private CommandSource(
final String apiOperation,
final String resource,
final Long resourceId,
final String jsonRequestBody,
final AppUser maker,
final LocalDate madeOnDate) {
this.taskName = StringUtils.defaultIfEmpty(taskName, null);
this.taskJson = StringUtils.defaultIfEmpty(taskJson, null);
this.apiOperation = StringUtils.defaultIfEmpty(apiOperation, null);
this.resource = StringUtils.defaultIfEmpty(resource, null);
this.resourceId = resourceId;
this.commandAsJson = StringUtils.defaultIfEmpty(jsonRequestBody, null);
this.maker = maker;
this.madeOnDate = madeOnDate.toDate();
}
@@ -74,8 +85,28 @@ public class MakerChecker extends AbstractPersistable<Long> {
this.checker = checker;
this.checkedOnDate = checkedOnDate.toDate();
}
public Long resourceId() {
return this.resourceId;
}
public String json() {
return this.taskJson;
return this.commandAsJson;
}
public boolean isClientResource() {
return this.resource.equalsIgnoreCase("CLIENTS");
}
public boolean isCreate() {
return this.apiOperation.equalsIgnoreCase("CREATE");
}
public boolean isUpdate() {
return this.apiOperation.equalsIgnoreCase("UPDATE") && this.resourceId != null;
}
public boolean isDelete() {
return this.apiOperation.equalsIgnoreCase("DELETE") && this.resourceId != null;
}
}
@@ -3,8 +3,8 @@ package org.mifosng.platform.makerchecker.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface MakerCheckerRepository extends
JpaRepository<MakerChecker, Long>,
JpaSpecificationExecutor<MakerChecker> {
public interface CommandSourceRepository extends
JpaRepository<CommandSource, Long>,
JpaSpecificationExecutor<CommandSource> {
// no added behaviour
}
@@ -0,0 +1,12 @@
package org.mifosng.platform.makerchecker.service;
import org.mifosng.platform.api.data.EntityIdentifier;
public interface PortfolioCommandSourceWritePlatformService {
EntityIdentifier logCommandSource(String apiOperation, String resource, Long resourceId, String jsonRequestBody);
EntityIdentifier approveEntry(Long id);
Long deleteEntry(Long makerCheckerId);
}
@@ -0,0 +1,121 @@
package org.mifosng.platform.makerchecker.service;
import org.joda.time.LocalDate;
import org.mifosng.platform.api.commands.ClientCommand;
import org.mifosng.platform.api.data.EntityIdentifier;
import org.mifosng.platform.api.infrastructure.PortfolioApiDataConversionService;
import org.mifosng.platform.client.service.ClientWritePlatformService;
import org.mifosng.platform.client.service.RollbackTransactionAsCommandIsNotApprovedByCheckerException;
import org.mifosng.platform.makerchecker.domain.CommandSource;
import org.mifosng.platform.makerchecker.domain.CommandSourceRepository;
import org.mifosng.platform.security.PlatformSecurityContext;
import org.mifosng.platform.user.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
*
*/
@Service
public class PortfolioCommandSourceWriteServiceImpl implements PortfolioCommandSourceWritePlatformService {
private final PlatformSecurityContext context;
private final CommandSourceRepository commandSourceRepository;
private final PortfolioApiDataConversionService apiDataConversionService;
private final ClientWritePlatformService clientWritePlatformService;
@Autowired
public PortfolioCommandSourceWriteServiceImpl(final PlatformSecurityContext context, final CommandSourceRepository makerCheckerRepository,
final PortfolioApiDataConversionService apiDataConversionService,
final ClientWritePlatformService clientWritePlatformService) {
this.context = context;
this.commandSourceRepository = makerCheckerRepository;
this.apiDataConversionService = apiDataConversionService;
this.clientWritePlatformService = clientWritePlatformService;
}
// NOT TRANSACTIONAL BY DESIGN FOR NOW
@Override
public EntityIdentifier logCommandSource(final String taskOperation, final String taskEntity, final Long entityId, final String taskJson) {
final AppUser maker = context.authenticatedUser();
final LocalDate asToday = new LocalDate();
final CommandSource entity = CommandSource.createdBy(taskOperation, taskEntity, entityId, taskJson, maker, asToday);
final boolean makerCheckerApproval = false;
Long resourceId = null;
if (entity.isClientResource()) {
final ClientCommand command = this.apiDataConversionService.convertJsonToClientCommand(entityId, entity.json(), makerCheckerApproval);
if (entity.isCreate()) {
try {
resourceId = this.clientWritePlatformService.createClient(command);
entity.markAsChecked(maker, asToday);
} catch (RollbackTransactionAsCommandIsNotApprovedByCheckerException e) {
// swallow this rollback transaction by design
}
} else if (entity.isUpdate()) {
try {
EntityIdentifier result = this.clientWritePlatformService.updateClientDetails(command);
resourceId = result.getEntityId();
entity.markAsChecked(maker, asToday);
} catch (RollbackTransactionAsCommandIsNotApprovedByCheckerException e) {
// swallow this rollback transaction by design
}
} else if (entity.isDelete()) {
try {
EntityIdentifier result = this.clientWritePlatformService.deleteClient(command);
resourceId = result.getEntityId();
entity.markAsChecked(maker, asToday);
} catch (RollbackTransactionAsCommandIsNotApprovedByCheckerException e) {
// swallow this rollback transaction by design
}
}
}
commandSourceRepository.save(entity);
return EntityIdentifier.makerChecker(resourceId, entity.getId());
}
@Transactional
@Override
public EntityIdentifier approveEntry(final Long id) {
final AppUser checker = context.authenticatedUser();
final CommandSource entity = this.commandSourceRepository.findOne(id);
entity.markAsChecked(checker, new LocalDate());
Long resourceId = null;
if (entity.isClientResource()) {
final ClientCommand command = this.apiDataConversionService.convertJsonToClientCommand(entity.resourceId(), entity.json(), true);
if (entity.isCreate()) {
resourceId = this.clientWritePlatformService.createClient(command);
} else if (entity.isUpdate()) {
EntityIdentifier result = this.clientWritePlatformService.updateClientDetails(command);
resourceId = result.getEntityId();
} else if (entity.isDelete()) {
EntityIdentifier result = this.clientWritePlatformService.deleteClient(command);
resourceId = result.getEntityId();
}
}
commandSourceRepository.save(entity);
return EntityIdentifier.makerChecker(resourceId, entity.getId());
}
@Transactional
@Override
public Long deleteEntry(final Long makerCheckerId) {
context.authenticatedUser();
this.commandSourceRepository.delete(makerCheckerId);
return makerCheckerId;
}
}
@@ -0,0 +1,13 @@
package org.mifosng.platform.makerchecker.service;
import java.util.Collection;
import org.mifosng.platform.api.data.CommandSourceData;
public interface PortfolioCommandsReadPlatformService {
Collection<CommandSourceData> retrieveAllEntriesToBeChecked();
CommandSourceData retrieveById(Long id);
}
@@ -5,7 +5,7 @@ import java.sql.SQLException;
import java.util.Collection;
import org.joda.time.LocalDate;
import org.mifosng.platform.api.data.MakerCheckerData;
import org.mifosng.platform.api.data.CommandSourceData;
import org.mifosng.platform.infrastructure.JdbcSupport;
import org.mifosng.platform.infrastructure.TenantAwareRoutingDataSource;
import org.mifosng.platform.security.PlatformSecurityContext;
@@ -15,53 +15,56 @@ import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
public class PortfolioMakerCheckerReadPlatformServiceImpl implements PortfolioMakerCheckerReadPlatformService {
public class PortfolioCommandsReadPlatformServiceImpl implements PortfolioCommandsReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private final PlatformSecurityContext context;
@Autowired
public PortfolioMakerCheckerReadPlatformServiceImpl(
public PortfolioCommandsReadPlatformServiceImpl(
final PlatformSecurityContext context,
final TenantAwareRoutingDataSource dataSource) {
this.context = context;
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
private static final class MakerCheckerMapper implements RowMapper<MakerCheckerData> {
private static final class CommandSourceMapper implements RowMapper<CommandSourceData> {
public String schema() {
return " mc.id as id, mc.task_name as taskName, mc.task_json as taskJson, mc.made_on_date as madeOnDate from m_maker_checker mc ";
return " mc.id as id, mc.api_operation as taskOperation, mc.api_resource as taskEntity, mc.resource_id as entityId," +
"mc.command_as_json as taskJson, mc.made_on_date as madeOnDate from m_portfolio_command_source mc ";
}
@Override
public MakerCheckerData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum)
public CommandSourceData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum)
throws SQLException {
final Long id = rs.getLong("id");
final String taskName = rs.getString("taskName");
final String taskOperation = rs.getString("taskOperation");
final String taskEntity = rs.getString("taskEntity");
final Long entityId = JdbcSupport.getLong(rs, "entityId");
final String taskJson = rs.getString("taskJson");
final LocalDate madeOnDate = JdbcSupport.getLocalDate(rs, "madeOnDate");
return new MakerCheckerData(id, taskName, taskJson, madeOnDate);
return new CommandSourceData(id, taskOperation, taskEntity, entityId, taskJson, madeOnDate);
}
}
@Override
public Collection<MakerCheckerData> retrieveAllEntriesToBeChecked() {
public Collection<CommandSourceData> retrieveAllEntriesToBeChecked() {
context.authenticatedUser();
final MakerCheckerMapper rm = new MakerCheckerMapper();
final String sql = "select " + rm.schema() + " order by mc.made_on_date DESC, mc.task_name ASC";
final CommandSourceMapper rm = new CommandSourceMapper();
final String sql = "select " + rm.schema() + " where mc.checker_id is null order by mc.made_on_date DESC, mc.api_resource ASC, mc.api_operation ASC";
return this.jdbcTemplate.query(sql, rm, new Object[] {});
}
@Override
public MakerCheckerData retrieveById(final Long id) {
public CommandSourceData retrieveById(final Long id) {
final MakerCheckerMapper rm = new MakerCheckerMapper();
final CommandSourceMapper rm = new CommandSourceMapper();
final String sql = "select " + rm.schema() + " where mc.id=?";
return this.jdbcTemplate.queryForObject(sql, rm, new Object[] {id});
@@ -1,13 +0,0 @@
package org.mifosng.platform.makerchecker.service;
import java.util.Collection;
import org.mifosng.platform.api.data.MakerCheckerData;
public interface PortfolioMakerCheckerReadPlatformService {
Collection<MakerCheckerData> retrieveAllEntriesToBeChecked();
MakerCheckerData retrieveById(Long id);
}
@@ -1,67 +0,0 @@
package org.mifosng.platform.makerchecker.service;
import org.joda.time.LocalDate;
import org.mifosng.platform.api.commands.ClientCommand;
import org.mifosng.platform.api.infrastructure.PortfolioApiDataConversionService;
import org.mifosng.platform.client.service.ClientWritePlatformService;
import org.mifosng.platform.makerchecker.domain.MakerChecker;
import org.mifosng.platform.makerchecker.domain.MakerCheckerRepository;
import org.mifosng.platform.security.PlatformSecurityContext;
import org.mifosng.platform.user.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
*
*/
@Service
public class PortfolioMakerCheckerWriteServiceImpl implements PortfolioMakerCheckerWriteService {
private final PlatformSecurityContext context;
private final MakerCheckerRepository makerCheckerRepository;
private final PortfolioApiDataConversionService apiDataConversionService;
private final ClientWritePlatformService clientWritePlatformService;
@Autowired
public PortfolioMakerCheckerWriteServiceImpl(final PlatformSecurityContext context, final MakerCheckerRepository makerCheckerRepository,
final PortfolioApiDataConversionService apiDataConversionService,
final ClientWritePlatformService clientWritePlatformService) {
this.context = context;
this.makerCheckerRepository = makerCheckerRepository;
this.apiDataConversionService = apiDataConversionService;
this.clientWritePlatformService = clientWritePlatformService;
}
@Transactional
@Override
public Long logNewEntry(final String taskName, final String taskJson) {
final AppUser maker = context.authenticatedUser();
final MakerChecker entity = MakerChecker.makerEntry(taskName, taskJson, maker, new LocalDate());
makerCheckerRepository.save(entity);
return entity.getId();
}
@Transactional
@Override
public Long approveEntry(final Long id) {
final AppUser checker = context.authenticatedUser();
final MakerChecker entity = this.makerCheckerRepository.findOne(id);
entity.markAsChecked(checker, new LocalDate());
makerCheckerRepository.save(entity);
// assume client creation for now
final ClientCommand command = this.apiDataConversionService.convertJsonToClientCommand(null, entity.json());
this.clientWritePlatformService.enrollClient(command);
return entity.getId();
}
}
-16
View File
@@ -1,16 +0,0 @@
MSYS-1.0.12 Build:2011-07-20 17:52
Exception: STATUS_ACCESS_VIOLATION at eip=68085E34
eax=68560000 ebx=00005D7C ecx=0000175F edx=00000000 esi=00000000 edi=68560000
ebp=0023FE80 esp=0023FE74 program=us
cs=001B ds=0023 es=0023 fs=003B gs=0000 ss=0023
Stack trace:
Frame Function Args
0023FE80 68085E34 (68560000, 00000000, 00005D7C, 00000004)
0023FED0 68001CB0 (00020C58, 00000001, 0023FF20, 680044EA)
0023FF20 680045C7 (8600D670, FFDFF538, 0023FF60, 00405F80)
0023FF40 68004C5F (00402318, 00000000, 804FA14C, 0000D670)
0023FF60 68004C98 (00000000, 00000000, A78A3C6C, 00000000)
0023FF90 00405E90 (00402318, 00000000, 00000001, 00000006)
0023FFC0 0040103D (00000000, 00000000, 7FFDE000, 8054A6ED)
0023FFF0 7C816FE7 (00401000, 00000000, 78746341, 00000020)
End of stack trace