MIFOSX-365: Simplify 'Data Tables' functionality

This commit is contained in:
Lech Rozanski
2013-05-28 17:17:51 +02:00
parent 2d4050de12
commit 721a12c2fa
16 changed files with 809 additions and 17 deletions
@@ -0,0 +1,10 @@
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES
('configuration', 'CREATE_DATATABLE', 'DATATABLE', 'CREATE', 0),
('configuration', 'CREATE_DATATABLE_CHECKER', 'DATATABLE', 'CREATE', 0),
('configuration', 'UPDATE_DATATABLE', 'DATATABLE', 'UPDATE', 0),
('configuration', 'UPDATE_DATATABLE_CHECKER', 'DATATABLE', 'UPDATE', 0),
('configuration', 'DELETE_DATATABLE', 'DATATABLE', 'DELETE', 0),
('configuration', 'DELETE_DATATABLE_CHECKER', 'DATATABLE', 'DELETE', 0);
@@ -108,6 +108,21 @@ public class CommandWrapper {
return this.actionName.equalsIgnoreCase("CREATE");
}
public boolean isCreateDatatable() {
return (this.actionName.equalsIgnoreCase("CREATE") && this.href.startsWith("/datatables/")
&& this.entityId == null);
}
public boolean isDeleteDatatable() {
return (this.actionName.equalsIgnoreCase("DELETE") && this.href.startsWith("/datatables/")
&& this.entityId == null);
}
public boolean isUpdateDatatable() {
return (this.actionName.equalsIgnoreCase("UPDATE") && this.href.startsWith("/datatables/")
&& this.entityId == null);
}
public String getTaskPermissionName() {
return this.taskPermissionName;
}
@@ -356,6 +356,33 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createDBDatatable(final String json) {
this.actionName = "CREATE";
this.entityName = "DATATABLE";
this.entityId = null;
this.href = "/datatables/";
this.json = json;
return this;
}
public CommandWrapperBuilder updateDBDatatable(final String datatable, final String json) {
this.actionName = "UPDATE";
this.entityName = "DATATABLE";
this.entityId = null;
this.href = "/datatables/" + datatable;
this.json = json;
return this;
}
public CommandWrapperBuilder deleteDBDatatable(final String datatable, final String json) {
this.actionName = "DELETE";
this.entityName = "DATATABLE";
this.entityId = null;
this.href = "/datatables/" + datatable;
this.json = json;
return this;
}
public CommandWrapperBuilder createDatatable(final String datatable, final Long apptableId, final Long datatableId) {
this.actionName = "CREATE";
commonDatatableSettings(datatable, apptableId, datatableId);
@@ -63,7 +63,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
final JsonCommand command = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(),
wrapper.getEntityId(), wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(),
wrapper.getSavingsId(), wrapper.getCodeId(), wrapper.getSupportedEntityType(), wrapper.getSupportedEntityId(),
wrapper.getTransactionId());
wrapper.getTransactionId(), wrapper.getHref());
result = this.processAndLogCommandService.processAndLogCommand(wrapper, command, isApprovedByChecker);
} catch (RollbackTransactionAsCommandIsNotApprovedByCheckerException e) {
@@ -86,7 +86,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.json());
final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.json(), parsedCommand,
this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.resourceId(),
commandSourceInput.subresourceId());
commandSourceInput.subresourceId(), commandSourceInput.getResourceGetUrl());
final boolean makerCheckerApproval = true;
return this.processAndLogCommandService.processAndLogCommand(wrapper, command, makerCheckerApproval);
@@ -78,7 +78,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
commandSourceResult.updateJsonTo(changesOnlyJson);
}
if (!result.hasChanges() && wrapper.isUpdateOperation()) {
if (!result.hasChanges() && wrapper.isUpdateOperation() && !wrapper.isUpdateDatatable()) {
commandSourceResult.updateJsonTo(null);
}
@@ -108,7 +108,13 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
if (wrapper.isConfigurationResource()) {
handler = applicationContext.getBean("updateGlobalConfigurationCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isDatatableResource()) {
if (wrapper.isCreate()) {
if (wrapper.isCreateDatatable()) {
handler = applicationContext.getBean("createDatatableCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isDeleteDatatable()) {
handler = applicationContext.getBean("deleteDatatableCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUpdateDatatable()) {
handler = applicationContext.getBean("updateDatatableCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isCreate()) {
handler = applicationContext.getBean("createDatatableEntryCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUpdateMultiple()) {
handler = applicationContext.getBean("updateOneToManyDatatableEntryCommandHandler", NewCommandSourceHandler.class);
@@ -45,24 +45,27 @@ public final class JsonCommand {
private final String supportedEntityType;
private final Long supportedEntityId;
private final String transactionId;
private final String url;
public static JsonCommand from(final String jsonCommand, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper,
final String entityName, final Long resourceId, final Long subresourceId, final Long groupId, final Long clientId,
final Long loanId, final Long savingsId, final Long codeId, final String supportedEntityType, final Long supportedEntityId,
final String transactionId) {
final String transactionId, final String url) {
return new JsonCommand(null, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, groupId,
clientId, loanId, savingsId, codeId, supportedEntityType, supportedEntityId, transactionId);
clientId, loanId, savingsId, codeId, supportedEntityType, supportedEntityId, transactionId, url);
}
public static JsonCommand fromExistingCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand,
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId) {
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId,
final String url) {
return new JsonCommand(commandId, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, null, null,
null, null, null, null, null, null);
null, null, null, null, null, null, url);
}
public JsonCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand,
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId,
final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final Long codeId,
final String supportedEntityType, final Long supportedEntityId, final String transactionId) {
final String supportedEntityType, final Long supportedEntityId, final String transactionId,
final String url) {
this.commandId = commandId;
this.jsonCommand = jsonCommand;
this.parsedCommand = parsedCommand;
@@ -78,6 +81,7 @@ public final class JsonCommand {
this.supportedEntityType = supportedEntityType;
this.supportedEntityId = supportedEntityId;
this.transactionId = transactionId;
this.url = url;
}
public String json() {
@@ -144,6 +148,10 @@ public final class JsonCommand {
public String getTransactionId() {
return this.transactionId;
}
public String getUrl() {
return this.url;
}
private boolean differenceExists(final LocalDate baseValue, final LocalDate workingCopyValue) {
boolean differenceExists = false;
@@ -221,6 +221,27 @@ public class DataValidatorBuilder {
}
return this;
}
public DataValidatorBuilder isNotOneOfTheseValues(final Object... values) {
if (value == null && ignoreNullValue) { return this; }
if (value != null) {
List<Object> valuesList = Arrays.asList(values);
if (valuesList.contains(this.value)) {
StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter)
.append(".is.one.of.expected.enumerations");
StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append(" must not be one of [ ")
.append(values).append(" ] ").append(".");
ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
defaultEnglishMessage.toString(), parameter, this.value, values);
dataValidationErrors.add(error);
}
}
return this;
}
public DataValidatorBuilder positiveAmount() {
if (value == null && ignoreNullValue) { return this; }
@@ -533,4 +554,23 @@ public class DataValidatorBuilder {
}
return this;
}
public DataValidatorBuilder matchesRegularExpression(final String expression) {
if (value == null && ignoreNullValue) { return this; }
if (!value.toString().matches(expression)) {
StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter)
.append(".does.not.match.regexp");
StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter)
.append(" must match the provided regular expression ")
.append(expression).append(" ] ").append(".");
ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
defaultEnglishMessage.toString(), parameter, this.value, expression);
dataValidationErrors.add(error);
}
return this;
}
}
@@ -0,0 +1,202 @@
package org.mifosplatform.infrastructure.core.serialization;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.InvalidJsonException;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
@Component
public class DatatableCommandFromApiJsonDeserializer {
private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$";
/**
* The parameters supported for this command.
*/
private final Set<String> supportedParametersForCreate = new HashSet<String>(Arrays.asList(
"datatableName", "apptableName", "multiRow", "columns"));
private final Set<String> supportedParametersForCreateColumns = new HashSet<String>(Arrays.asList(
"name", "type", "length", "mandatory"));
private final Set<String> supportedParametersForUpdate = new HashSet<String>(Arrays.asList(
"apptableName", "changeColumns", "addColumns", "dropColumns"));
private final Set<String> supportedParametersForAddColumns = new HashSet<String>(Arrays.asList(
"name", "type", "length", "mandatory", "after"));
private final Set<String> supportedParametersForChangeColumns = new HashSet<String>(Arrays.asList(
"name", "newName", "type", "length", "mandatory", "after"));
private final Set<String> supportedParametersForDropColumns = new HashSet<String>(Arrays.asList(
"name"));
private final Object[] supportedColumnTypes = { "String", "Number", "Decimal", "Date" };
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public DatatableCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validateForCreate(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForCreate);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable");
final JsonElement element = fromApiJsonHelper.parse(json);
final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element);
final String fkColumnName = apptableName.substring(2) + "_id";
baseDataValidator.reset().parameter("apptableName").value(apptableName).notNull().notBlank().notExceedingLengthOf(50);
final String datatableName = fromApiJsonHelper.extractStringNamed("datatableName", element);
baseDataValidator.reset().parameter("datatableName").value(datatableName).notNull().notBlank().notExceedingLengthOf(50)
.matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
final Boolean multiRow = fromApiJsonHelper.extractBooleanNamed("multiRow", element);
baseDataValidator.reset().parameter("multiRow").value(multiRow).ignoreIfNull().trueOrFalseRequired(multiRow);
final JsonArray columns = fromApiJsonHelper.extractJsonArrayNamed("columns", element);
baseDataValidator.reset().parameter("columns").value(columns).notNull();
for (JsonElement column : columns) {
fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForCreateColumns);
final String name = fromApiJsonHelper.extractStringNamed("name", column);
baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50)
.isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
final String type = fromApiJsonHelper.extractStringNamed("type", column);
baseDataValidator.reset().parameter("type").value(type).notNull().notBlank().isOneOfTheseValues(supportedColumnTypes);
final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column);
if (type.equals("String")) {
baseDataValidator.reset().parameter("length").value(length).notNull().notBlank()
.zeroOrPositiveAmount();
} else {
baseDataValidator.reset().parameter("length").value(length).ignoreIfNull().notBlank()
.zeroOrPositiveAmount();
}
final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column);
baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().trueOrFalseRequired(mandatory);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateForUpdate(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForUpdate);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable");
final JsonElement element = fromApiJsonHelper.parse(json);
final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element);
final String fkColumnName = apptableName.substring(2) + "_id";
baseDataValidator.reset().parameter("apptableName").value(apptableName).notBlank().notExceedingLengthOf(50);
if (fromApiJsonHelper.parameterExists("changeColumns", element)) {
final JsonArray changeColumns = fromApiJsonHelper.extractJsonArrayNamed("changeColumns", element);
baseDataValidator.reset().parameter("changeColumns").value(changeColumns).ignoreIfNull();
for (JsonElement column : changeColumns) {
fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForChangeColumns);
final String name = fromApiJsonHelper.extractStringNamed("name", column);
baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50)
.isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
final String newName = fromApiJsonHelper.extractStringNamed("newName", column);
baseDataValidator.reset().parameter("newName").value(newName).ignoreIfNull().notBlank()
.notExceedingLengthOf(50).isNotOneOfTheseValues("id", fkColumnName)
.matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
final String type = fromApiJsonHelper.extractStringNamed("type", column);
baseDataValidator.reset().parameter("type").value(type).notNull().notBlank()
.isOneOfTheseValues(supportedColumnTypes);
final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column);
if (type.equals("String")) {
baseDataValidator.reset().parameter("length").value(length).notNull().notBlank()
.zeroOrPositiveAmount();
} else {
baseDataValidator.reset().parameter("length").value(length).ignoreIfNull().notBlank()
.zeroOrPositiveAmount();
}
final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column);
baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().trueOrFalseRequired(mandatory);
final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column);
baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank();
}
}
if (fromApiJsonHelper.parameterExists("addColumns", element)) {
final JsonArray addColumns = fromApiJsonHelper.extractJsonArrayNamed("addColumns", element);
baseDataValidator.reset().parameter("addColumns").value(addColumns).ignoreIfNull();
for (JsonElement column : addColumns) {
fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForAddColumns);
final String name = fromApiJsonHelper.extractStringNamed("name", column);
baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50)
.isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
final String type = fromApiJsonHelper.extractStringNamed("type", column);
baseDataValidator.reset().parameter("type").value(type).notNull().notBlank()
.isOneOfTheseValues(supportedColumnTypes);
final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column);
if (type.equals("String")) {
baseDataValidator.reset().parameter("length").value(length).notNull().notBlank()
.zeroOrPositiveAmount();
} else {
baseDataValidator.reset().parameter("length").value(length).ignoreIfNull().notBlank()
.zeroOrPositiveAmount();
}
final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column);
baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().trueOrFalseRequired(mandatory);
final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column);
baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank();
}
}
if (fromApiJsonHelper.parameterExists("dropColumns", element)) {
final JsonArray dropColumns = fromApiJsonHelper.extractJsonArrayNamed("dropColumns", element);
baseDataValidator.reset().parameter("dropColumns").value(dropColumns).ignoreIfNull();
for (JsonElement column : dropColumns) {
fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForDropColumns);
final String name = fromApiJsonHelper.extractStringNamed("name", column);
baseDataValidator.reset().parameter("name").value(name).notNull().notBlank().notExceedingLengthOf(50)
.isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
}
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
}
@@ -7,11 +7,13 @@ package org.mifosplatform.infrastructure.core.serialization;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
@@ -78,6 +80,21 @@ public class FromJsonHelper {
if (!unsupportedParameterList.isEmpty()) { throw new UnsupportedParameterException(unsupportedParameterList); }
}
public void checkForUnsupportedParameters(final JsonObject object, final Set<String> supportedParams) {
if (object == null) { throw new InvalidParameterException(); }
Set<Entry<String, JsonElement>> entries = object.entrySet();
List<String> unsupportedParameterList = new ArrayList<String>();
for (Entry<String, JsonElement> providedParameter : entries) {
if (!supportedParams.contains(providedParameter.getKey())) {
unsupportedParameterList.add(providedParameter.getKey());
}
}
if (!unsupportedParameterList.isEmpty()) { throw new UnsupportedParameterException(unsupportedParameterList); }
}
public JsonElement parse(final String json) {
return parser.parse(json);
@@ -70,6 +70,46 @@ public class DatatablesApiResource {
return this.toApiJsonSerializer.serializePretty(prettyPrint, result);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String createDatatable(final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.createDBDatatable(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@PUT
@Path("{datatableName}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String updateDatatable(@PathParam("datatableName") final String datatableName,
final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.updateDBDatatable(datatableName, apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@DELETE
@Path("{datatableName}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String deleteDatatable(@PathParam("datatableName") final String datatableName,
final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.deleteDBDatatable(datatableName, apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("register/{datatable}/{apptable}")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -95,6 +135,18 @@ public class DatatablesApiResource {
return this.toApiJsonSerializer.serialize(result);
}
@GET
@Path("{datatable}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String getDatatable(@PathParam("datatable") final String datatable, @Context final UriInfo uriInfo) {
final DatatableData result = this.readWriteNonCoreDataService.retrieveDatatable(datatable);
final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serializePretty(prettyPrint, result);
}
@GET
@Path("{datatable}/{apptableId}")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -5,6 +5,8 @@
*/
package org.mifosplatform.infrastructure.dataqueries.data;
import java.util.List;
/**
* Immutable data object representing datatable data.
*/
@@ -14,13 +16,18 @@ public class DatatableData {
private final String applicationTableName;
@SuppressWarnings("unused")
private final String registeredTableName;
public static DatatableData create(final String applicationTableName, final String registeredTableName) {
return new DatatableData(applicationTableName, registeredTableName);
@SuppressWarnings("unused")
private final List<ResultsetColumnHeaderData> columnHeaderData;
public static DatatableData create(final String applicationTableName, final String registeredTableName,
final List<ResultsetColumnHeaderData> columnHeaderData) {
return new DatatableData(applicationTableName, registeredTableName, columnHeaderData);
}
private DatatableData(final String applicationTableName, final String registeredTableName) {
private DatatableData(final String applicationTableName, final String registeredTableName,
final List<ResultsetColumnHeaderData> columnHeaderData) {
this.applicationTableName = applicationTableName;
this.registeredTableName = registeredTableName;
this.columnHeaderData = columnHeaderData;
}
}
@@ -0,0 +1,32 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.dataqueries.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CreateDatatableCommandHandler implements NewCommandSourceHandler {
private final ReadWriteNonCoreDataService writePlatformService;
@Autowired
public CreateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createDatatable(command);
}
}
@@ -0,0 +1,40 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.dataqueries.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DeleteDatatableCommandHandler implements NewCommandSourceHandler {
private final ReadWriteNonCoreDataService writePlatformService;
@Autowired
public DeleteDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final String datatableName = command.getUrl().replaceAll("/datatables/", "");
this.writePlatformService.deleteDatatable(datatableName);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withResourceIdAsString(datatableName)
.build();
}
}
@@ -0,0 +1,40 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.dataqueries.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UpdateDatatableCommandHandler implements NewCommandSourceHandler {
private final ReadWriteNonCoreDataService writePlatformService;
@Autowired
public UpdateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final String datatableName = command.getUrl().replaceAll("/datatables/", "");
this.writePlatformService.updateDatatable(datatableName, command);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withResourceIdAsString(datatableName)
.build();
}
}
@@ -16,7 +16,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
public interface ReadWriteNonCoreDataService {
List<DatatableData> retrieveDatatableNames(String appTable);
DatatableData retrieveDatatable(String datatable);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'REGISTER_DATATABLE')")
void registerDatatable(String datatable, String appTable);
@@ -25,6 +27,12 @@ public interface ReadWriteNonCoreDataService {
GenericResultsetData retrieveDataTableGenericResultSet(String datatable, Long appTableId, String order, Long id);
CommandProcessingResult createDatatable(JsonCommand command);
void updateDatatable(String datatableName, JsonCommand command);
void deleteDatatable(String datatableName);
CommandProcessingResult createNewDatatableEntry(String datatable, Long appTableId, JsonCommand command);
CommandProcessingResult updateDatatableEntryOneToOne(String datatable, Long appTableId, JsonCommand command);
@@ -25,6 +25,7 @@ import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException;
import org.mifosplatform.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.infrastructure.core.serialization.JsonParserHelper;
import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource;
@@ -47,12 +48,20 @@ import org.springframework.jdbc.support.rowset.SqlRowSetMetaData;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
@Service
public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataService {
private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$";
private final static Logger logger = LoggerFactory.getLogger(ReadWriteNonCoreDataServiceImpl.class);
private final static HashMap<String, String> apiTypeToMySQL = new HashMap<String, String>() {{
put("String", "VARCHAR"); put("Number", "INT"); put("Decimal", "DECIMAL"); put("Date", "DATE");
}};
private final JdbcTemplate jdbcTemplate;
private final DataSource dataSource;
@@ -60,16 +69,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
private final FromJsonHelper fromJsonHelper;
private final JsonParserHelper helper;
private final GenericDataService genericDataService;
private final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer;
@Autowired
public ReadWriteNonCoreDataServiceImpl(final TenantAwareRoutingDataSource dataSource, final PlatformSecurityContext context,
final FromJsonHelper fromJsonHelper, final GenericDataService genericDataService) {
final FromJsonHelper fromJsonHelper, final GenericDataService genericDataService,
final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer) {
this.dataSource = dataSource;
this.jdbcTemplate = new JdbcTemplate(this.dataSource);
this.context = context;
this.fromJsonHelper = fromJsonHelper;
this.helper = new JsonParserHelper();
this.genericDataService = genericDataService;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
}
@Override
@@ -96,12 +108,41 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
while (rs.next()) {
final String appTableName = rs.getString("application_table_name");
final String registeredDatatableName = rs.getString("registered_table_name");
datatables.add(DatatableData.create(appTableName, registeredDatatableName));
final List<ResultsetColumnHeaderData> columnHeaderData =
this.genericDataService.fillResultsetColumnHeaders(registeredDatatableName);
datatables.add(DatatableData.create(appTableName, registeredDatatableName, columnHeaderData));
}
return datatables;
}
@Override
public DatatableData retrieveDatatable(final String datatable) {
// PERMITTED datatables
final String sql = "select application_table_name, registered_table_name" + " from x_registered_table " + " where exists"
+ " (select 'f'" + " from m_appuser_role ur " + " join m_role r on r.id = ur.role_id"
+ " left join m_role_permission rp on rp.role_id = r.id" + " left join m_permission p on p.id = rp.permission_id"
+ " where ur.appuser_id = " + context.authenticatedUser().getId()
+ " and registered_table_name='" + datatable + "'"
+ " and (p.code in ('ALL_FUNCTIONS', 'ALL_FUNCTIONS_READ') or p.code = concat('READ_', registered_table_name))) "
+ " order by application_table_name, registered_table_name";
final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql);
DatatableData datatableData = null;
while (rs.next()) {
final String appTableName = rs.getString("application_table_name");
final String registeredDatatableName = rs.getString("registered_table_name");
final List<ResultsetColumnHeaderData> columnHeaderData =
this.genericDataService.fillResultsetColumnHeaders(registeredDatatableName);
datatableData = DatatableData.create(appTableName, registeredDatatableName, columnHeaderData);
}
return datatableData;
}
private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) {
logger.error(dve.getMessage(), dve);
@@ -240,6 +281,253 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
}
}
private Boolean datatableExists(final String datatable) {
String sql = "SELECT COUNT(*) FROM `x_registered_table` WHERE `registered_table_name`='" + datatable + "'";
Integer rowCount = this.jdbcTemplate.queryForInt(sql);
return (rowCount > 0);
}
private void validateDatatableExists(final String name) {
if (!datatableExists(name)) {
throw new PlatformDataIntegrityException("error.msg.datatables.missing.table.name",
"Table '" + name + "' does not exist.", name);
}
}
private void validateDatatableNotExists(final String name) {
if (datatableExists(name)) {
throw new PlatformDataIntegrityException("error.msg.datatables.duplicate.table.name",
"Table '" + name + "' already exists.", name);
}
}
private void validateDatatableName(final String name) {
if (name == null || name.isEmpty()) {
throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name",
"Data table name must not be blank.");
} else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) {
throw new PlatformDataIntegrityException("error.msg.datatables.datatable.invalid.name.regex",
"Invalid data table name.", name);
}
}
private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder) {
String name = (column.has("name")) ? column.get("name").getAsString() : null;
String type = (column.has("type")) ? column.get("type").getAsString() : null;
Integer length = (column.has("length")) ? column.get("length").getAsInt() : null;
Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false;
String mysqlType = apiTypeToMySQL.get(type);
sqlBuilder = sqlBuilder.append("`" + name + "` " + mysqlType);
if (type != null)
{
if (type.equals("String")) {
sqlBuilder = sqlBuilder.append("(" + length + ")");
} else if (type.equals("Decimal")) {
sqlBuilder = sqlBuilder.append("(19,6)");
}
}
if (mandatory != null) {
if (mandatory) {
sqlBuilder = sqlBuilder.append(" NOT NULL");
} else {
sqlBuilder = sqlBuilder.append(" DEFAULT NULL");
}
}
sqlBuilder = sqlBuilder.append(", ");
}
@Transactional
@Override
public CommandProcessingResult createDatatable(final JsonCommand command) {
context.authenticatedUser();
this.fromApiJsonDeserializer.validateForCreate(command.json());
JsonElement element = this.fromJsonHelper.parse(command.json());
JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed("columns", element);
String datatableName = this.fromJsonHelper.extractStringNamed("datatableName", element);
String apptable = this.fromJsonHelper.extractStringNamed("apptableName", element);
Boolean multiRow = this.fromJsonHelper.extractBooleanNamed("multiRow", element);
if (multiRow == null) {
multiRow = false;
}
validateDatatableName(datatableName);
validateDatatableName(apptable);
validateDatatableNotExists(datatableName);
String fkColumnName = apptable.substring(2) + "_id";
String fkName = datatableName.toLowerCase().replaceAll("\\s", "_");
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder = sqlBuilder.append("CREATE TABLE `" + datatableName + "` (");
if (multiRow) {
sqlBuilder = sqlBuilder
.append("`id` BIGINT(20) NOT NULL AUTO_INCREMENT, ")
.append("`" + fkColumnName + "` BIGINT(20) NOT NULL, ");
} else {
sqlBuilder = sqlBuilder
.append("`" + fkColumnName + "` BIGINT(20) NOT NULL, ");
}
for (JsonElement column : columns) {
parseDatatableColumnObjectForCreate(column.getAsJsonObject(), sqlBuilder);
}
// Remove trailing comma and space
sqlBuilder = sqlBuilder.delete(sqlBuilder.length() - 2, sqlBuilder.length());
if (multiRow) {
sqlBuilder = sqlBuilder
.append(", PRIMARY KEY (`id`)")
.append(", KEY `fk_" + apptable + "` (`" + fkColumnName + "`)")
.append(", CONSTRAINT `fk_" + fkName + "` ")
.append("FOREIGN KEY (`" + fkColumnName + "`) ")
.append("REFERENCES `" + apptable + "` (`id`)");
} else {
sqlBuilder = sqlBuilder
.append(", PRIMARY KEY (`" + fkColumnName + "`)")
.append(", CONSTRAINT `fk_" + fkName + "` ")
.append("FOREIGN KEY (`" + fkColumnName + "`) ")
.append("REFERENCES `" + apptable + "` (`id`)");
}
sqlBuilder = sqlBuilder.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8;");
this.jdbcTemplate.execute(sqlBuilder.toString());
registerDatatable(datatableName, apptable);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withResourceIdAsString(datatableName)
.build();
}
private void parseDatatableColumnForUpdate(final JsonObject column, StringBuilder sqlBuilder) {
String name = (column.has("name")) ? column.get("name").getAsString() : null;
String newName = (column.has("newName")) ? column.get("newName").getAsString() : name;
String type = (column.has("type")) ? column.get("type").getAsString() : null;
Integer length = (column.has("length")) ? column.get("length").getAsInt() : null;
Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false;
String after = (column.has("after")) ? column.get("after").getAsString() : null;
String mysqlType = apiTypeToMySQL.get(type);
sqlBuilder = sqlBuilder.append(", CHANGE `" + name + "` `" + newName + "` " + mysqlType);
if (type != null) {
if (type.equals("String") && length != null) {
sqlBuilder = sqlBuilder.append("(" + length + ")");
} else if (type.equals("Decimal")) {
sqlBuilder = sqlBuilder.append("(19,6)");
}
}
if (mandatory != null) {
if (mandatory) {
sqlBuilder = sqlBuilder.append(" NOT NULL");
} else {
sqlBuilder = sqlBuilder.append(" DEFAULT NULL");
}
}
if (after != null) {
sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`");
}
}
private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder) {
String name = (column.has("name")) ? column.get("name").getAsString() : null;
String type = (column.has("type")) ? column.get("type").getAsString() : null;
Integer length = (column.has("length")) ? column.get("length").getAsInt() : null;
Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false;
String after = (column.has("after")) ? column.get("after").getAsString() : null;
String mysqlType = apiTypeToMySQL.get(type);
sqlBuilder = sqlBuilder.append(", ADD `" + name + "` " + mysqlType);
if (type != null) {
if (type.equals("String") && length != null) {
sqlBuilder = sqlBuilder.append("(" + length + ")");
} else if (type.equals("Decimal")) {
sqlBuilder = sqlBuilder.append("(19,6)");
}
}
if (mandatory != null) {
if (mandatory) {
sqlBuilder = sqlBuilder.append(" NOT NULL");
} else {
sqlBuilder = sqlBuilder.append(" DEFAULT NULL");
}
}
if (after != null) {
sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`");
}
}
private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder) {
String name = (column.has("name")) ? column.get("name").getAsString() : null;
sqlBuilder = sqlBuilder.append(", DROP COLUMN `" + name + "`");
}
@Transactional
@Override
public void updateDatatable(final String datatableName, final JsonCommand command) {
context.authenticatedUser();
this.fromApiJsonDeserializer.validateForUpdate(command.json());
JsonElement element = this.fromJsonHelper.parse(command.json());
JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed("changeColumns", element);
JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed("addColumns", element);
JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed("dropColumns", element);
validateDatatableName(datatableName);
validateDatatableExists(datatableName);
StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`");
for (JsonElement column : changeColumns) {
parseDatatableColumnForUpdate(column.getAsJsonObject(), sqlBuilder);
}
for (JsonElement column : addColumns) {
parseDatatableColumnForAdd(column.getAsJsonObject(), sqlBuilder);
}
for (JsonElement column : dropColumns) {
parseDatatableColumnForDrop(column.getAsJsonObject(), sqlBuilder);
}
// Remove the first comma, right after ALTER TABLE `datatable`
sqlBuilder = sqlBuilder.deleteCharAt(sqlBuilder.indexOf(","));
this.jdbcTemplate.execute(sqlBuilder.toString());
}
@Transactional
@Override
public void deleteDatatable(final String datatableName) {
context.authenticatedUser();
validateDatatableName(datatableName);
validateDatatableExists(datatableName);
deregisterDatatable(datatableName);
String sql = "DROP TABLE `" + datatableName + "`";
this.jdbcTemplate.execute(sql);
}
@Transactional
@Override
public CommandProcessingResult updateDatatableEntryOneToOne(final String dataTableName, final Long appTableId, final JsonCommand command) {