From 721a12c2fa72df149979146b6346cce49e413f1c Mon Sep 17 00:00:00 2001 From: Lech Rozanski Date: Tue, 28 May 2013 17:17:51 +0200 Subject: [PATCH] MIFOSX-365: Simplify 'Data Tables' functionality --- .../core_db/V46__extend_datatables_api.sql | 10 + .../commands/domain/CommandWrapper.java | 15 + .../service/CommandWrapperBuilder.java | 27 ++ ...CommandSourceWritePlatformServiceImpl.java | 4 +- .../SynchronousCommandProcessingService.java | 10 +- .../infrastructure/core/api/JsonCommand.java | 18 +- .../core/data/DataValidatorBuilder.java | 40 +++ ...tatableCommandFromApiJsonDeserializer.java | 202 ++++++++++++ .../core/serialization/FromJsonHelper.java | 17 + .../api/DatatablesApiResource.java | 52 ++++ .../dataqueries/data/DatatableData.java | 15 +- .../CreateDatatableCommandHandler.java | 32 ++ .../DeleteDatatableCommandHandler.java | 40 +++ .../UpdateDatatableCommandHandler.java | 40 +++ .../service/ReadWriteNonCoreDataService.java | 10 +- .../ReadWriteNonCoreDataServiceImpl.java | 294 +++++++++++++++++- 16 files changed, 809 insertions(+), 17 deletions(-) create mode 100644 mifosng-db/migrations/core_db/V46__extend_datatables_api.sql create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java create mode 100644 mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java diff --git a/mifosng-db/migrations/core_db/V46__extend_datatables_api.sql b/mifosng-db/migrations/core_db/V46__extend_datatables_api.sql new file mode 100644 index 000000000..5bd0a585e --- /dev/null +++ b/mifosng-db/migrations/core_db/V46__extend_datatables_api.sql @@ -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); + diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java index ef609f991..613188160 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -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; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java index 6ff9c0957..e275acbe6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -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); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java index b792caa1f..c68ca88c8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java @@ -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); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java index a65f0c1a9..6fd1d3d66 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -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); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java index b174ced3e..54f97801a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java @@ -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; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java index 0fac1f6ce..35e0db5fd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java @@ -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 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; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..5ed9cf5a2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java @@ -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 supportedParametersForCreate = new HashSet(Arrays.asList( + "datatableName", "apptableName", "multiRow", "columns")); + private final Set supportedParametersForCreateColumns = new HashSet(Arrays.asList( + "name", "type", "length", "mandatory")); + private final Set supportedParametersForUpdate = new HashSet(Arrays.asList( + "apptableName", "changeColumns", "addColumns", "dropColumns")); + private final Set supportedParametersForAddColumns = new HashSet(Arrays.asList( + "name", "type", "length", "mandatory", "after")); + private final Set supportedParametersForChangeColumns = new HashSet(Arrays.asList( + "name", "newName", "type", "length", "mandatory", "after")); + private final Set supportedParametersForDropColumns = new HashSet(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>() {}.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForCreate); + + final List dataValidationErrors = new ArrayList(); + 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>() {}.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForUpdate); + + final List dataValidationErrors = new ArrayList(); + 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 dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); } + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java index 2e93eb625..23d2f2eba 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java @@ -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 supportedParams) { + if (object == null) { throw new InvalidParameterException(); } + + Set> entries = object.entrySet(); + List unsupportedParameterList = new ArrayList(); + + for (Entry 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); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java index 39a9771d8..b1ac99140 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java @@ -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 }) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java index cf7ca9774..99a5c59b5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java @@ -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 columnHeaderData; + + public static DatatableData create(final String applicationTableName, final String registeredTableName, + final List 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 columnHeaderData) { this.applicationTableName = applicationTableName; this.registeredTableName = registeredTableName; + this.columnHeaderData = columnHeaderData; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java new file mode 100644 index 000000000..9f41e004e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java @@ -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); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java new file mode 100644 index 000000000..4b7d067fc --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java @@ -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(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java new file mode 100644 index 000000000..b96a8bfc5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java @@ -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(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java index b01e52f06..5f67f4768 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java @@ -16,7 +16,9 @@ import org.springframework.security.access.prepost.PreAuthorize; public interface ReadWriteNonCoreDataService { List 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); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java index 54b5b8bba..e36b0033d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java @@ -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 apiTypeToMySQL = new HashMap() {{ + 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 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 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) {