diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index eeafcd99f..d4f4e59b8 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -1991,6 +1991,22 @@ Global Configuration + + Hooks + hooks + Create a Hook + List Hooks + + + + + + hooks/{hookId} + + Retrieve a Hook + Update a Hook + Delete a Hook + Codes codes @@ -2446,6 +2462,10 @@ users/template Retrieve User Template + + hooks/template + Retrieve Hooks Template + audits/searchtemplate Retrieve Audit Search Template @@ -13335,6 +13355,300 @@ Request Body: +   +
+
+

Hooks

+

Hooks are a mechanism to trigger custom code on the occurence of events.

+

Each template during hook creation represents custom behaviour on what actions should be taken on the triggering of a registered event for a hook. The action taken might be firing a HTTP request to another server or execute internal code. +

+

In order for MifosX to send webhook payloads, your server needs to be accessible from the Internet. MifosX will send along a few HTTP headers to differentiate between event types.

+

X-Mifos-Entity - The entity type that was triggered.
+ X-Mifos-Action - The action triggered on the entity type.
+ X-Mifos-Platform-TenantId - The tenant which experienced the event.

+
+
+ +   +
+
+

Create a Hook

+

The following parameters can be passed for the creation of a hook :- +

+
+ +POST https://DomainName/api/v1/hooks + + +POST hooks +Content-Type: application/json +Request Body: +{ + "name": "Web", + "isActive": true, + "displayName": "Kremlin", + "events": [ + { + "actionName": "DISBURSE", + "entityName": "LOAN" + }, + { + "actionName": "REPAYMENT", + "entityName": "LOAN" + } + ], + "config": { + "Payload URL": "http://example.com/webhook", + "Content Type": "json" + } +} + + +{ + "resourceId": 4 +} + +
+
+ +   +
+
+

Retrieve Hooks

+

Returns the list of hooks.

+

Example Requests:

+
hooks
+
+
+ +GET https://DomainName/api/v1/hooks + + +[ + { + "id": 1, + "name": "Web", + "displayName": "Kremlin", + "isActive": true, + "createdAt": [2014, 9, 16], + "updatedAt": [2014, 9, 16], + "events": [ + { + "actionName": "DISBURSE", + "entityName": "LOAN" + }, + { + "actionName": "REPAYMENT", + "entityName": "LOAN" + } + ], + "config": [ + { + "fieldName": "Content Type", + "fieldValue": "json" + }, + { + "fieldName": "Payload URL", + "fieldValue": "https://abc.com/api" + } + ] + } +] + +
+
+ +   +
+
+

Retrieve a Hook

+

Returns the details of a Hook.

+

Example Requests:

+
hooks/1
+
+
+ +GET https://DomainName/api/v1/hooks/{hookId} + + +{ + "id": 1, + "name": "Web", + "displayName": "Kremlin", + "isActive": true, + "createdAt": [2014, 9, 16], + "updatedAt": [2014, 9, 16], + "events": [ + { + "actionName": "DISBURSE", + "entityName": "LOAN" + }, + { + "actionName": "REPAYMENT", + "entityName": "LOAN" + } + ], + "config": [ + { + "fieldName": "Content Type", + "fieldValue": "json" + }, + { + "fieldName": "Payload URL", + "fieldValue": "https://abc.com/api" + } + ] +} + +
+
+ +   +
+
+

Update a Hook

+

Updates the details of a hook.

+
+
+ +PUT https://DomainName/api/v1/hooks/{hookId} + + +PUT hooks/4 +Content-Type: application/json +Request Body: +{ + "name": "Web", + "isActive": true, + "displayName": "Local SMS Provider", + "events": [ + { + "actionName": "DISBURSE", + "entityName": "LOAN" + } + ], + "config": { + "Payload URL": "http://changed.com/sms", + "Content Type": "json" + } +} + + +{ + "resourceId": 4, + "changes": { + "displayName": "Local SMS Provider", + "events": [ + { + "actionName": "DISBURSE", + "entityName": "LOAN" + } + ], + "config": { + "Payload URL": "http://changed.com/sms", + "Content Type": "json" + } + } +} + +
+
+ +   +
+
+

Delete a Hook

+

Deletes a hook.

+
+
+ +DELETE https://DomainName/api/v1/hooks/{hookId} + + +DELETE hooks/4 +Content-Type: application/json + + +{ + "resourceId": 4 +} + +
+
+ +   +
+
+

Retrieve Hooks Template

+

This is a convenience resource. It can be useful when + building maintenance user interface screens for client + applications. The template data returned consists of any or all + of: +

+

+

Example Request:

+
hooks/template
+
+
+ +GET https://DomainName/api/v1/hooks/template + + +{ + "templates": [ + { + "name": "Web", + "schema": [ + { + "fieldName": "Content Type", + "fieldType": "string", + "optional": true, + "placeholder": "json / form" + }, + { + "fieldName": "Payload URL", + "fieldType": "string", + "optional": false + } + ] + }, + { + },... + ], + "groupings": [ + { + "name": "jobs", + "entities": [ + { + "name": "SCHEDULER", + "actions": ["EXECUTEJOB", "UPDATE"] + }, + { + },... + ] + }, + { + },... + ] +} + +
+
 
diff --git a/mifosng-db/migrations/core_db/V201__webhooks.sql b/mifosng-db/migrations/core_db/V201__webhooks.sql new file mode 100644 index 000000000..0e782d7f2 --- /dev/null +++ b/mifosng-db/migrations/core_db/V201__webhooks.sql @@ -0,0 +1,108 @@ +-- ----------------------------------------------------- +-- Table `m_hook_templates` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `m_hook_templates` ( + `id` SMALLINT(6) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(45) NOT NULL, + PRIMARY KEY (`id`)) +ENGINE = InnoDB DEFAULT CHARSET=utf8; + + +-- ----------------------------------------------------- +-- Table `m_hook` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `m_hook` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `template_id` SMALLINT(6) NOT NULL, + `is_active` SMALLINT(3) NOT NULL DEFAULT 1, + `name` VARCHAR(45) NOT NULL, + `createdby_id` bigint(20) DEFAULT NULL, + `created_date` datetime DEFAULT NULL, + `lastmodifiedby_id` bigint(20) DEFAULT NULL, + `lastmodified_date` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `fk_template_id_idx` (`template_id` ASC), + CONSTRAINT `fk_template_id` + FOREIGN KEY (`template_id`) + REFERENCES `m_hook_templates` (`id`)) +ENGINE = InnoDB DEFAULT CHARSET=utf8; + + +-- ----------------------------------------------------- +-- Table `m_hook_schema` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `m_hook_schema` ( + `id` SMALLINT(6) NOT NULL AUTO_INCREMENT, + `hook_template_id` SMALLINT(6) NOT NULL, + `field_type` VARCHAR(45) NOT NULL, + `field_name` VARCHAR(100) NOT NULL, + `placeholder` VARCHAR(100) DEFAULT NULL, + `optional` TINYINT(3) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + INDEX `fk_hook_template_id_idx` (`hook_template_id` ASC), + CONSTRAINT `fk_hook_template_id` + FOREIGN KEY (`hook_template_id`) + REFERENCES `m_hook_templates` (`id`)) +ENGINE = InnoDB DEFAULT CHARSET=utf8; + + +-- ----------------------------------------------------- +-- Table `m_hook_registered_events` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `m_hook_registered_events` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `hook_id` BIGINT(20) NOT NULL, + `entity_name` VARCHAR(45) NOT NULL, + `action_name` VARCHAR(45) NOT NULL, + PRIMARY KEY (`id`), + INDEX `fk_hook_id_idx` (`hook_id` ASC), + CONSTRAINT `fk_hook_idc` + FOREIGN KEY (`hook_id`) + REFERENCES `m_hook` (`id`)) +ENGINE = InnoDB DEFAULT CHARSET=utf8; + + +-- ----------------------------------------------------- +-- Table `m_hook_configuration` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `m_hook_configuration` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `hook_id` BIGINT(20) NULL, + `field_type` VARCHAR(45) NOT NULL, + `field_name` VARCHAR(100) NOT NULL, + `field_value` VARCHAR(100) NOT NULL, + PRIMARY KEY (`id`), + INDEX `fk_hook_id_idx` (`hook_id` ASC), + CONSTRAINT `fk_hook_id_cfg` + FOREIGN KEY (`hook_id`) + REFERENCES `m_hook` (`id`)) +ENGINE = InnoDB DEFAULT CHARSET=utf8; + +insert into `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('configuration', 'CREATE_HOOK', 'HOOK', 'CREATE', 0); +insert into `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('configuration', 'READ_HOOK', 'HOOK', 'READ', 0); +insert into `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('configuration', 'UPDATE_HOOK', 'HOOK', 'UPDATE', 0); +insert into `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) +VALUES ('configuration', 'DELETE_HOOK', 'HOOK', 'DELETE', 0); + +insert into m_hook_templates values(1, "Web"); +insert into m_hook_templates values(2, "SMS Bridge"); + +INSERT INTO `m_hook_schema` (`hook_template_id`, `field_type`, `field_name`, `optional`) +VALUES (1, 'string', 'Payload URL', 0); +INSERT INTO `m_hook_schema` (`hook_template_id`, `field_type`, `field_name`, `placeholder`, `optional`) +VALUES (1, 'string', 'Content Type', 'json / form', 0); +INSERT INTO `m_hook_schema` (`hook_template_id`, `field_type`, `field_name`, `optional`) +VALUES (2, 'string', 'Payload URL', 0); +INSERT INTO `m_hook_schema` (`hook_template_id`, `field_type`, `field_name`, `optional`) +VALUES (2, 'string', 'SMS Provider', 0); +INSERT INTO `m_hook_schema` (`hook_template_id`, `field_type`, `field_name`, `optional`) +VALUES (2, 'string', 'Phone Number', 0); +INSERT INTO `m_hook_schema` (`hook_template_id`, `field_type`, `field_name`, `optional`) +VALUES (2, 'string', 'SMS Provider Token', 0); +INSERT INTO `m_hook_schema` (`hook_template_id`, `field_type`, `field_name`, `optional`) +VALUES (2, 'string', 'SMS Provider Account Id', 0); + + diff --git a/mifosng-provider/build.gradle b/mifosng-provider/build.gradle index 3095438cf..fa166f6b1 100644 --- a/mifosng-provider/build.gradle +++ b/mifosng-provider/build.gradle @@ -90,6 +90,11 @@ dependencies { [group: 'com.sun.jersey.contribs', name: 'jersey-spring', version: jerseyVersion], [group: 'com.sun.jersey.contribs', name: 'jersey-multipart', version: jerseyVersion], + [group: 'com.squareup.retrofit', name: 'retrofit', version: '1.6.1'], + [group: 'com.squareup.okhttp', name: 'okhttp', version: '2.0.0'], + [group: 'com.squareup.okhttp', name: 'okhttp-urlconnection', version: '2.0.0'], + + [group: 'com.google.code.gson', name: 'gson', version: '2.2.4'], [group: 'com.google.guava', name: 'guava', version: '15.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 c7167c3ff..dd201c989 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -233,6 +233,10 @@ public class CommandWrapper { return this.entityName.equalsIgnoreCase("USER"); } + public boolean isHookResource() { + return this.entityName.equalsIgnoreCase("HOOK"); + } + public boolean isCurrencyResource() { return this.entityName.equalsIgnoreCase("CURRENCY"); } 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 f2f3f3093..c63e1219c 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -302,6 +302,30 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder createHook() { + this.actionName = "CREATE"; + this.entityName = "HOOK"; + this.entityId = null; + this.href = "/hooks/template"; + return this; + } + + public CommandWrapperBuilder updateHook(final Long hookId) { + this.actionName = "UPDATE"; + this.entityName = "HOOK"; + this.entityId = hookId; + this.href = "/hooks/" + hookId; + return this; + } + + public CommandWrapperBuilder deleteHook(final Long hookId) { + this.actionName = "DELETE"; + this.entityName = "HOOK"; + this.entityId = hookId; + this.href = "/hooks/" + hookId; + return this; + } + public CommandWrapperBuilder createCharge() { this.actionName = "CREATE"; this.entityName = "CHARGE"; 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 8dd8fe6bb..2e7e942ba 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -19,6 +19,9 @@ 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.core.serialization.ToApiJsonSerializer; +import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; +import org.mifosplatform.infrastructure.hooks.event.HookEvent; +import org.mifosplatform.infrastructure.hooks.event.HookEventSource; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; @@ -32,17 +35,21 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer private PlatformSecurityContext context; private final ApplicationContext applicationContext; private final ToApiJsonSerializer> toApiJsonSerializer; + private final ToApiJsonSerializer toApiResultJsonSerializer; private CommandSourceRepository commandSourceRepository; private final ConfigurationDomainService configurationDomainService; @Autowired public SynchronousCommandProcessingService(final PlatformSecurityContext context, final ApplicationContext applicationContext, - final ToApiJsonSerializer> toApiJsonSerializer, final CommandSourceRepository commandSourceRepository, + final ToApiJsonSerializer> toApiJsonSerializer, + final ToApiJsonSerializer toApiResultJsonSerializer, + final CommandSourceRepository commandSourceRepository, final ConfigurationDomainService configurationDomainService) { this.context = context; this.context = context; this.applicationContext = applicationContext; this.toApiJsonSerializer = toApiJsonSerializer; + this.toApiResultJsonSerializer = toApiResultJsonSerializer; this.commandSourceRepository = commandSourceRepository; this.commandSourceRepository = commandSourceRepository; this.configurationDomainService = configurationDomainService; @@ -52,7 +59,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer @Override public CommandProcessingResult processAndLogCommand(final CommandWrapper wrapper, final JsonCommand command, final boolean isApprovedByChecker) { - + final boolean rollbackTransaction = this.configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName()); final NewCommandSourceHandler handler = findCommandHandler(wrapper); @@ -101,6 +108,9 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(commandSourceResult); } result.setRollbackTransaction(null); + + publishEvent(wrapper.entityName(), wrapper.actionName(), result); + return result; } @@ -223,6 +233,16 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer } else { throw new UnsupportedCommandException(wrapper.commandName()); } + } else if (wrapper.isHookResource()) { + if (wrapper.isCreate()) { + handler = this.applicationContext.getBean("createHookCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isUpdate()) { + handler = this.applicationContext.getBean("updateHookCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isDelete()) { + handler = this.applicationContext.getBean("deleteHookCommandHandler", NewCommandSourceHandler.class); + } else { + throw new UnsupportedCommandException(wrapper.commandName()); + } } else if (wrapper.isStaffResource()) { if (wrapper.isCreate()) { handler = this.applicationContext.getBean("createStaffCommandHandler", NewCommandSourceHandler.class); @@ -851,4 +871,20 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer user.validateHasPermissionTo(commandWrapper.getTaskPermissionName()); return rollbackTransaction; } + + private void publishEvent(final String entityName, final String actionName, final CommandProcessingResult result) { + + final String authToken = ThreadLocalContextUtil.getAuthToken(); + final String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier(); + final AppUser appUser = this.context.authenticatedUser(); + + final HookEventSource hookEventSource = new HookEventSource(entityName, actionName); + + final String serializedResult = this.toApiResultJsonSerializer.serialize(result); + + final HookEvent applicationEvent = + new HookEvent(hookEventSource, serializedResult, tenantIdentifier, appUser, authToken); + + applicationContext.publishEvent(applicationEvent); + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/CommandProcessingResult.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/CommandProcessingResult.java index 4677264ed..0da7ee1ee 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/CommandProcessingResult.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/CommandProcessingResult.java @@ -5,6 +5,7 @@ */ package org.mifosplatform.infrastructure.core.data; +import java.io.Serializable; import java.util.HashMap; import java.util.Map; @@ -12,7 +13,7 @@ import java.util.Map; * Represents the successful result of an REST API call that results in * processing a command. */ -public class CommandProcessingResult { +public class CommandProcessingResult implements Serializable { private Long commandId; private Long officeId; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ThreadLocalContextUtil.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ThreadLocalContextUtil.java index 6a90141a2..8b1edb0e8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ThreadLocalContextUtil.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/ThreadLocalContextUtil.java @@ -18,6 +18,8 @@ public class ThreadLocalContextUtil { private static final ThreadLocal contextHolder = new ThreadLocal<>(); private static final ThreadLocal tenantcontext = new ThreadLocal<>(); + + private static final ThreadLocal authTokenContext = new ThreadLocal<>(); public static void setTenant(final MifosPlatformTenant tenant) { Assert.notNull(tenant, "tenant cannot be null"); @@ -43,5 +45,13 @@ public class ThreadLocalContextUtil { public static void clearDataSourceContext() { contextHolder.remove(); } + + public static void setAuthToken(final String authToken) { + authTokenContext.set(authToken); + } + + public static String getAuthToken() { + return authTokenContext.get(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/api/HookApiConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/api/HookApiConstants.java new file mode 100644 index 000000000..6350d71f8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/api/HookApiConstants.java @@ -0,0 +1,56 @@ +/** + * 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.hooks.api; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class HookApiConstants { + + public static final String HOOK_RESOURCE_NAME = "HOOK"; + + public static final String nameParamName = "name"; + + public static final String displayNameParamName = "displayName"; + + public static final String isActiveParamName = "isActive"; + + public static final String webTemplateName = "Web"; + + public static final String smsTemplateName = "SMS Bridge"; + + public static final String payloadURLName = "Payload URL"; + + public static final String contentTypeName = "Content Type"; + + public static final String smsProviderName = "SMS Provider"; + + public static final String smsProviderAccountIdName = "SMS Provider Account Id"; + + public static final String smsProviderTokenIdName = "SMS Provider Token"; + + public static final String phoneNumberName = "Phone Number"; + + public static final String apiKeyName = "Api Key"; + + public static final String configParamName = "config"; + + public static final String eventsParamName = "events"; + + public static final String entityNameParamName = "entityName"; + + public static final String actionNameParamName = "actionName"; + + public static final Set RESPONSE_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(nameParamName, displayNameParamName, + isActiveParamName, configParamName, eventsParamName)); + + public static final Set UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(nameParamName, displayNameParamName, + isActiveParamName, configParamName, eventsParamName)); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/api/HookApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/api/HookApiResource.java new file mode 100644 index 000000000..12d6588db --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/api/HookApiResource.java @@ -0,0 +1,159 @@ +/** + * 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.hooks.api; + +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.HOOK_RESOURCE_NAME; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.RESPONSE_DATA_PARAMETERS; + +import java.util.Collection; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import org.mifosplatform.commands.domain.CommandWrapper; +import org.mifosplatform.commands.service.CommandWrapperBuilder; +import org.mifosplatform.commands.service.PortfolioCommandSourceWritePlatformService; +import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.mifosplatform.infrastructure.hooks.data.HookData; +import org.mifosplatform.infrastructure.hooks.service.HookReadPlatformService; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Path("/hooks") +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +@Component +@Scope("singleton") +public class HookApiResource { + + private final PlatformSecurityContext context; + private final HookReadPlatformService readPlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final ApiRequestParameterHelper apiRequestParameterHelper; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + + @Autowired + public HookApiResource( + final PlatformSecurityContext context, + final HookReadPlatformService readPlatformService, + final DefaultToApiJsonSerializer toApiJsonSerializer, + final ApiRequestParameterHelper apiRequestParameterHelper, + final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService) { + this.context = context; + this.readPlatformService = readPlatformService; + this.toApiJsonSerializer = toApiJsonSerializer; + this.apiRequestParameterHelper = apiRequestParameterHelper; + this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; + } + + @GET + public String retrieveHooks(@Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission( + HOOK_RESOURCE_NAME); + + final Collection hooks = this.readPlatformService + .retrieveAllHooks(); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper + .process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, hooks, + RESPONSE_DATA_PARAMETERS); + } + + @GET + @Path("{hookId}") + public String retrieveHook(@PathParam("hookId") final Long hookId, + @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission( + HOOK_RESOURCE_NAME); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper + .process(uriInfo.getQueryParameters()); + + HookData hook = this.readPlatformService.retrieveHook(hookId); + + if (settings.isTemplate()) { + final HookData hookData = this.readPlatformService + .retrieveNewHookDetails(hook.getTemplateName()); + hook = HookData.templateExisting(hook, hookData.getTemplates(), + hookData.getGroupings()); + } + return this.toApiJsonSerializer.serialize(settings, hook, + RESPONSE_DATA_PARAMETERS); + } + + @GET + @Path("template") + public String template(@Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission( + HOOK_RESOURCE_NAME); + + final HookData hook = this.readPlatformService + .retrieveNewHookDetails(null); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper + .process(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serialize(settings, hook, + RESPONSE_DATA_PARAMETERS); + } + + @POST + public String createHook(final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .createHook().withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService + .logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @PUT + @Path("{hookId}") + public String updateHook(@PathParam("hookId") final Long hookId, + final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .updateHook(hookId).withJson(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService + .logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + + @DELETE + @Path("{hookId}") + public String deleteHook(@PathParam("hookId") final Long hookId) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .deleteHook(hookId).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService + .logCommandSource(commandRequest); + + return this.toApiJsonSerializer.serialize(result); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Entity.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Entity.java new file mode 100644 index 000000000..395f82e14 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Entity.java @@ -0,0 +1,30 @@ +/** + * 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.hooks.data; + +import java.io.Serializable; +import java.util.List; + +@SuppressWarnings("unused") +public class Entity implements Serializable { + + private String name; + + private List actions; + + public void setName(final String name) { + this.name = name; + } + + public void setActions(final List actions) { + this.actions = actions; + } + + public String getName() { + return this.name; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Event.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Event.java new file mode 100644 index 000000000..a994ed8b9 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Event.java @@ -0,0 +1,33 @@ +/** + * 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.hooks.data; + +import java.io.Serializable; + +public class Event implements Serializable { + + private final String actionName; + private final String entityName; + + public static Event instance(final String actionName, + final String entityName) { + return new Event(actionName, entityName); + } + + private Event(final String actionName, final String entityName) { + this.actionName = actionName; + this.entityName = entityName; + } + + public String getActionName() { + return this.actionName; + } + + public String getEntityName() { + return this.entityName; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/EventResultSetExtractor.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/EventResultSetExtractor.java new file mode 100644 index 000000000..83b338f46 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/EventResultSetExtractor.java @@ -0,0 +1,96 @@ +/** + * 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.hooks.data; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.ResultSetExtractor; + +public class EventResultSetExtractor implements + ResultSetExtractor> { + + @Override + public List extractData(final ResultSet rs) throws SQLException, + DataAccessException { + final List groupings = new ArrayList<>(); + + final Map>> groupToEntityMapping = new HashMap<>(); + Map> entityToActionMapping = new HashMap<>(); + + while (rs.next()) { + final String groupingName = rs.getString("grouping"); + final String entityName = rs.getString("entity_name"); + final String actionName = rs.getString("action_name"); + Map> entities = groupToEntityMapping + .get(groupingName); + List actions = entityToActionMapping.get(entityName); + + if (entities == null) { + entityToActionMapping = new HashMap<>(); + } + + if (actions == null) { + actions = new ArrayList<>(); + } + actions.add(actionName); + entityToActionMapping.put(entityName, actions); + + if (entities == null) { + entities = new HashMap<>(); + } + entities.putAll(entityToActionMapping); + groupToEntityMapping.put(groupingName, entities); + } + + for (final Entry>> groupingEntry : groupToEntityMapping + .entrySet()) { + final List entities = new ArrayList<>(); + final Grouping group = new Grouping(); + group.setName(groupingEntry.getKey()); + for (final Entry> entityEntry : groupingEntry + .getValue().entrySet()) { + final List actions = new ArrayList<>(); + final Entity entity = new Entity(); + entity.setName(entityEntry.getKey()); + for (final String action : entityEntry.getValue()) { + actions.add(action); + } + Collections.sort(actions); + entity.setActions(actions); + entities.add(entity); + } + + Collections.sort(entities, new Comparator() { + @Override + public int compare(final Entity entity1, final Entity entity2) { + return entity1.getName().compareTo(entity2.getName()); + } + }); + group.setEntities(entities); + groupings.add(group); + } + + Collections.sort(groupings, new Comparator() { + @Override + public int compare(final Grouping grouping1, + final Grouping grouping2) { + return grouping1.getName().compareTo(grouping2.getName()); + } + }); + + return groupings; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Field.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Field.java new file mode 100644 index 000000000..767ad28d4 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Field.java @@ -0,0 +1,50 @@ +/** + * 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.hooks.data; + +@SuppressWarnings("unused") +public class Field { + + private final String fieldName; + private final String fieldValue; + private final String fieldType; + private final Boolean optional; + private final String placeholder; + + public static Field fromConfig(final String fieldName, + final String fieldValue) { + return new Field(null, fieldName, fieldValue, null, null); + } + + public static Field fromSchema(final String fieldType, + final String fieldName, final Boolean optional, + final String placeholder) { + return new Field(fieldType, fieldName, null, optional, placeholder); + } + + private Field(final String fieldType, final String fieldName, + final String fieldValue, final Boolean optional, + final String placeholder) { + this.fieldType = fieldType; + this.fieldName = fieldName; + this.fieldValue = fieldValue; + this.optional = optional; + this.placeholder = placeholder; + } + + public String getFieldName() { + return this.fieldName; + } + + public String getFieldValue() { + return this.fieldValue; + } + + public String getFieldType() { + return this.fieldType; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Grouping.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Grouping.java new file mode 100644 index 000000000..72661936c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/Grouping.java @@ -0,0 +1,30 @@ +/** + * 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.hooks.data; + +import java.io.Serializable; +import java.util.List; + +@SuppressWarnings("unused") +public class Grouping implements Serializable { + + private String name; + + private List entities; + + public void setName(final String name) { + this.name = name; + } + + public void setEntities(final List entities) { + this.entities = entities; + } + + public String getName() { + return this.name; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/HookData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/HookData.java new file mode 100644 index 000000000..d13e544b1 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/HookData.java @@ -0,0 +1,91 @@ +/** + * 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.hooks.data; + +import java.io.Serializable; +import java.util.List; + +import org.joda.time.LocalDate; + +public class HookData implements Serializable { + + private final Long id; + private final String name; + private final String displayName; + private final Boolean isActive; + private final LocalDate createdAt; + private final LocalDate updatedAt; + + // associations + private final List events; + private final List config; + + // template data + private final List templates; + private final List groupings; + + public static HookData instance(final Long id, final String name, + final String displayName, final boolean isActive, + final LocalDate createdAt, final LocalDate updatedAt, + final List registeredEvents, final List config) { + return new HookData(id, name, displayName, isActive, createdAt, + updatedAt, registeredEvents, config, null, null); + } + + public static HookData template(final List templates, + final List groupings) { + return new HookData(null, null, null, null, null, null, null, null, + templates, groupings); + } + + public static HookData templateExisting(final HookData hookData, + final List templates, + final List groupings) { + return new HookData(hookData.id, hookData.name, hookData.displayName, + hookData.isActive, hookData.createdAt, hookData.updatedAt, + hookData.events, hookData.config, templates, groupings); + } + + private HookData(final Long id, final String name, + final String displayName, final Boolean isActive, + final LocalDate createdAt, final LocalDate updatedAt, + final List events, final List config, + final List templates, + final List groupings) { + this.id = id; + this.name = name; + this.displayName = displayName; + this.isActive = isActive; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + + // associations + this.events = events; + this.config = config; + + // template + this.templates = templates; + this.groupings = groupings; + + } + + public Long getHookId() { + return this.id; + } + + public List getTemplates() { + return this.templates; + } + + public List getGroupings() { + return this.groupings; + } + + public String getTemplateName() { + return this.name; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/HookTemplateData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/HookTemplateData.java new file mode 100644 index 000000000..d53d5ffd1 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/data/HookTemplateData.java @@ -0,0 +1,38 @@ +/** + * 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.hooks.data; + +import java.io.Serializable; +import java.util.List; + +@SuppressWarnings("unused") +public class HookTemplateData implements Serializable { + + private final Long id; + private final String name; + + // associations + private final List schema; + + public static HookTemplateData instance(final Long id, final String name, + final List schema) { + return new HookTemplateData(id, name, schema); + } + + private HookTemplateData(final Long id, final String name, + final List schema) { + this.id = id; + this.name = name; + + // associations + this.schema = schema; + } + + public Long getServiceId() { + return this.id; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/Hook.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/Hook.java new file mode 100644 index 000000000..2c57592c9 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/Hook.java @@ -0,0 +1,185 @@ +/** + * 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.hooks.domain; + +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.configParamName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.displayNameParamName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.eventsParamName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.isActiveParamName; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +import org.apache.commons.lang.StringUtils; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.domain.AbstractAuditableCustom; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.util.CollectionUtils; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +@Entity +@Table(name = "m_hook") +public class Hook extends AbstractAuditableCustom { + + @Column(name = "name", nullable = false, length = 100) + private String name; + + @Column(name = "is_active", nullable = false) + private Boolean isActive; + + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "hook", orphanRemoval = true) + private Set events = new HashSet<>(); + + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "hook", orphanRemoval = true) + private Set config = new HashSet<>(); + + @ManyToOne(optional = true) + @JoinColumn(name = "template_id", referencedColumnName = "id", nullable = false) + private HookTemplate template; + + protected Hook() { + // + } + + public static Hook fromJson(final JsonCommand command, + final HookTemplate template, final Set config, + final Set events) { + final String displayName = command + .stringValueOfParameterNamed(displayNameParamName); + final Boolean isActive = command + .booleanObjectValueOfParameterNamed(isActiveParamName); + return new Hook(template, displayName, isActive, config, events); + } + + private Hook(final HookTemplate template, final String displayName, + final Boolean isActive, final Set config, + final Set events) { + + this.template = template; + + if (StringUtils.isNotBlank(displayName)) { + this.name = displayName.trim(); + } else { + this.name = template.getName(); + } + this.isActive = isActive; + if (!CollectionUtils.isEmpty(config)) { + this.config = associateConfigWithThisHook(config); + } + if (!CollectionUtils.isEmpty(events)) { + this.events = associateEventsWithThisHook(events); + } + + } + + private Set associateConfigWithThisHook( + final Set config) { + for (final HookConfiguration hookConfiguration : config) { + hookConfiguration.update(this); + } + return config; + } + + private Set associateEventsWithThisHook( + final Set events) { + for (final HookResource hookResource : events) { + hookResource.update(this); + } + return events; + } + + public HookTemplate getHookTemplate() { + return this.template; + } + + public Set getHookConfig() { + return this.config; + } + + public Map update(final JsonCommand command) { + + final Map actualChanges = new LinkedHashMap<>(4); + + if (command.isChangeInStringParameterNamed(displayNameParamName, + this.name)) { + final String newValue = command + .stringValueOfParameterNamed(displayNameParamName); + actualChanges.put(displayNameParamName, newValue); + this.name = newValue; + } + + if (command.isChangeInBooleanParameterNamed(isActiveParamName, + this.isActive)) { + final Boolean newValue = command + .booleanObjectValueOfParameterNamed(isActiveParamName); + actualChanges.put(isActiveParamName, newValue); + this.isActive = newValue; + } + + // events + if (command.hasParameter(eventsParamName)) { + final JsonArray jsonArray = command + .arrayOfParameterNamed(eventsParamName); + if (jsonArray != null) { + actualChanges.put(eventsParamName, jsonArray); + } + } + + // config + if (command.hasParameter(configParamName)) { + final JsonElement element = command.parsedJson().getAsJsonObject() + .get(configParamName); + if (element != null) { + actualChanges.put(configParamName, element); + } + } + + return actualChanges; + } + + public boolean updateEvents(final Set newHookEvents) { + if (newHookEvents == null) { + return false; + } + + if (this.events == null) { + this.events = new HashSet<>(); + } + this.events.clear(); + this.events.addAll(associateEventsWithThisHook(newHookEvents)); + return true; + } + + public boolean updateConfig(final Set newHookConfig) { + if (newHookConfig == null) { + return false; + } + + if (this.config == null) { + this.config = new HashSet<>(); + } + this.config.clear(); + this.config.addAll(associateConfigWithThisHook(newHookConfig)); + return true; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookConfiguration.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookConfiguration.java new file mode 100644 index 000000000..d34652390 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookConfiguration.java @@ -0,0 +1,72 @@ +/** + * 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.hooks.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_hook_configuration") +public class HookConfiguration extends AbstractPersistable { + + @ManyToOne(optional = false) + @JoinColumn(name = "hook_id", referencedColumnName = "id", nullable = false) + private Hook hook; + + @Column(name = "field_type", nullable = false, length = 20) + private String fieldType; + + @Column(name = "field_name", nullable = false, length = 100) + private String fieldName; + + @Column(name = "field_value", nullable = false, length = 100) + private String fieldValue; + + public static HookConfiguration createNewWithoutHook( + final String fieldType, final String fieldName, + final String fieldValue) { + return new HookConfiguration(null, fieldType, fieldName, fieldValue); + } + + public static HookConfiguration createNew(final Hook hook, final String fieldType, + final String fieldName, final String fieldValue) { + return new HookConfiguration(hook, fieldType, fieldName, fieldValue); + } + + protected HookConfiguration() { + // + } + + private HookConfiguration(final Hook hook, final String fieldType, + final String fieldName, final String fieldValue) { + this.hook = hook; + this.fieldType = fieldType; + this.fieldName = fieldName; + this.fieldValue = fieldValue; + } + + public String getFieldName() { + return this.fieldName; + } + + public String getFieldType() { + return this.fieldType; + } + + public String getFieldValue() { + return this.fieldValue; + } + + public void update(final Hook hook) { + this.hook = hook; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookConfigurationRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookConfigurationRepository.java new file mode 100644 index 000000000..70d066c91 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookConfigurationRepository.java @@ -0,0 +1,21 @@ +/** + * 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.hooks.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface HookConfigurationRepository extends + JpaRepository, + JpaSpecificationExecutor { + + @Query("select config.fieldValue from HookConfiguration config where config.hook.id = :hookId and config.fieldName = :fieldName") + String findOneByHookIdAndFieldName(@Param("hookId") Long hookId, + @Param("fieldName") String fieldName); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookRepository.java new file mode 100644 index 000000000..52d69f8f4 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookRepository.java @@ -0,0 +1,26 @@ +/** + * 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.hooks.domain; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface HookRepository extends JpaRepository, + JpaSpecificationExecutor { + + @Query("select hook from Hook hook inner join hook.events event where event.entityName = :entityName and event.actionName = :actionName and hook.isActive = 1") + List findAllHooksListeningToEvent( + @Param("entityName") String entityName, + @Param("actionName") String actionName); + + @Query("from Hook hook where hook.template.id = :templateId ") + Hook findOneByTemplateId(@Param("templateId") Long templateId); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookResource.java new file mode 100644 index 000000000..aa851a4db --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookResource.java @@ -0,0 +1,49 @@ +/** + * 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.hooks.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_hook_registered_events") +public class HookResource extends AbstractPersistable { + + @ManyToOne(optional = false) + @JoinColumn(name = "hook_id", referencedColumnName = "id", nullable = false) + private Hook hook; + + @Column(name = "entity_name", nullable = false, length = 45) + private String entityName; + + @Column(name = "action_name", nullable = false, length = 45) + private String actionName; + + protected HookResource() { + // + } + + public static HookResource createNewWithoutHook(final String entityName, + final String actionName) { + return new HookResource(null, entityName, actionName); + } + + private HookResource(final Hook hook, final String entityName, + final String actionName) { + this.hook = hook; + this.entityName = entityName; + this.actionName = actionName; + } + + public void update(final Hook hook) { + this.hook = hook; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookTemplate.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookTemplate.java new file mode 100644 index 000000000..d9c074748 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookTemplate.java @@ -0,0 +1,61 @@ +/** + * 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.hooks.domain; + +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.nameParamName; + +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +import org.apache.commons.lang.StringUtils; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_hook_templates") +public class HookTemplate extends AbstractPersistable { + + @Column(name = "name", nullable = false, length = 100) + private String name; + + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "template", orphanRemoval = true) + private final Set fields = new HashSet<>(); + + private HookTemplate(final String name) { + + if (StringUtils.isNotBlank(name)) { + this.name = name.trim(); + } else { + this.name = null; + } + } + + protected HookTemplate() { + + } + + public static HookTemplate fromJson(final JsonCommand command) { + final String name = command.stringValueOfParameterNamed(nameParamName); + return new HookTemplate(name); + } + + public String getName() { + return this.name; + } + + public Set getSchema() { + return this.fields; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookTemplateRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookTemplateRepository.java new file mode 100644 index 000000000..75481b7a1 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/HookTemplateRepository.java @@ -0,0 +1,19 @@ +/** + * 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.hooks.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface HookTemplateRepository extends + JpaRepository, + JpaSpecificationExecutor { + + @Query("from HookTemplate template where template.name = :name") + HookTemplate findOne(@Param("name") String name); +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/Schema.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/Schema.java new file mode 100644 index 000000000..384d30f2a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/domain/Schema.java @@ -0,0 +1,52 @@ +/** + * 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.hooks.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_hook_schema") +public class Schema extends AbstractPersistable { + + @ManyToOne(optional = false) + @JoinColumn(name = "hook_template_id", referencedColumnName = "id", nullable = false) + private HookTemplate template; + + @Column(name = "field_type", nullable = false, length = 20) + private String fieldType; + + @Column(name = "field_name", nullable = false, length = 100) + private String fieldName; + + @Column(name = "placeholder", length = 100) + private String placeholder; + + @Column(name = "optional", nullable = false) + private final boolean optional = false; + + public Schema() { + // + } + + public String getFieldName() { + return this.fieldName; + } + + public String getFieldType() { + return this.fieldType; + } + + public boolean isOptional() { + return this.optional; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/event/HookEvent.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/event/HookEvent.java new file mode 100644 index 000000000..0b81263a5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/event/HookEvent.java @@ -0,0 +1,52 @@ +/** + * 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.hooks.event; + +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.context.ApplicationEvent; + +public class HookEvent extends ApplicationEvent { + + private final String payload; + + private final String tenantIdentifier; + + private final AppUser appUser; + + private final String authToken; + + public HookEvent(final HookEventSource source, final String payload, + final String tenantIdentifier, final AppUser appUser, + final String authToken) { + super(source); + this.payload = payload; + this.tenantIdentifier = tenantIdentifier; + this.appUser = appUser; + this.authToken = authToken; + } + + public String getPayload() { + return this.payload; + } + + @Override + public HookEventSource getSource() { + return (HookEventSource) super.source; + } + + public String getTenantIdentifier() { + return this.tenantIdentifier; + } + + public AppUser getAppUser() { + return this.appUser; + } + + public String getAuthToken() { + return this.authToken; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/event/HookEventSource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/event/HookEventSource.java new file mode 100644 index 000000000..c1c6a89aa --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/event/HookEventSource.java @@ -0,0 +1,30 @@ +/** + * 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.hooks.event; + +import java.io.Serializable; + +public class HookEventSource implements Serializable { + + private final String entityName; + + private final String actionName; + + public HookEventSource(final String entityName, final String actionName) { + super(); + this.entityName = entityName; + this.actionName = actionName; + } + + public String getEntityName() { + return this.entityName; + } + + public String getActionName() { + return this.actionName; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/exception/HookNotFoundException.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/exception/HookNotFoundException.java new file mode 100644 index 000000000..0fd4aef16 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/exception/HookNotFoundException.java @@ -0,0 +1,22 @@ +/** + * 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.hooks.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class HookNotFoundException extends + AbstractPlatformResourceNotFoundException { + + public HookNotFoundException(final String name) { + super("error.msg.hook.not.found", "Hook with name `" + name + + "` does not exist", name); + } + + public HookNotFoundException(final Long hookId) { + super("error.msg.hook.identifier.not.found", "Hook with identifier `" + + hookId + "` does not exist", hookId); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/exception/HookTemplateNotFoundException.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/exception/HookTemplateNotFoundException.java new file mode 100644 index 000000000..6c0702201 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/exception/HookTemplateNotFoundException.java @@ -0,0 +1,24 @@ +/** + * 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.hooks.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class HookTemplateNotFoundException extends + AbstractPlatformResourceNotFoundException { + + public HookTemplateNotFoundException(final String name) { + super("error.msg.template.not.found", "Template with name `" + name + + "` does not exist", name); + } + + public HookTemplateNotFoundException(final Long templateId) { + super("error.msg.template.identifier.not.found", + "Template with identifier `" + templateId + "` does not exist", + templateId); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/handler/CreateHookCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/handler/CreateHookCommandHandler.java new file mode 100644 index 000000000..a9519a6cd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/handler/CreateHookCommandHandler.java @@ -0,0 +1,34 @@ +/** + * 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.hooks.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.hooks.service.HookWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class CreateHookCommandHandler implements NewCommandSourceHandler { + + private final HookWritePlatformService writePlatformService; + + @Autowired + public CreateHookCommandHandler( + final HookWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.createHook(command); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/handler/DeleteHookCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/handler/DeleteHookCommandHandler.java new file mode 100644 index 000000000..f7be20643 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/handler/DeleteHookCommandHandler.java @@ -0,0 +1,34 @@ +/** + * 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.hooks.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.hooks.service.HookWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class DeleteHookCommandHandler implements NewCommandSourceHandler { + + private final HookWritePlatformService writePlatformService; + + @Autowired + public DeleteHookCommandHandler( + final HookWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.deleteHook(command.entityId()); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/handler/UpdateHookCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/handler/UpdateHookCommandHandler.java new file mode 100644 index 000000000..131edbefa --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/handler/UpdateHookCommandHandler.java @@ -0,0 +1,35 @@ +/** + * 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.hooks.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.hooks.service.HookWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UpdateHookCommandHandler implements NewCommandSourceHandler { + + private final HookWritePlatformService writePlatformService; + + @Autowired + public UpdateHookCommandHandler( + final HookWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService + .updateHook(command.entityId(), command); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/listener/HookListener.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/listener/HookListener.java new file mode 100644 index 000000000..ec97979f0 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/listener/HookListener.java @@ -0,0 +1,13 @@ +/** + * 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.hooks.listener; + +import org.mifosplatform.infrastructure.hooks.event.HookEvent; +import org.springframework.context.ApplicationListener; + +public interface HookListener extends ApplicationListener { + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/listener/MifosHookListener.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/listener/MifosHookListener.java new file mode 100644 index 000000000..63c45311f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/listener/MifosHookListener.java @@ -0,0 +1,66 @@ +/** + * 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.hooks.listener; + +import java.util.List; + +import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant; +import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; +import org.mifosplatform.infrastructure.hooks.domain.Hook; +import org.mifosplatform.infrastructure.hooks.domain.HookRepository; +import org.mifosplatform.infrastructure.hooks.event.HookEvent; +import org.mifosplatform.infrastructure.hooks.event.HookEventSource; +import org.mifosplatform.infrastructure.hooks.processor.HookProcessor; +import org.mifosplatform.infrastructure.hooks.processor.HookProcessorProvider; +import org.mifosplatform.infrastructure.security.service.TenantDetailsService; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class MifosHookListener implements HookListener { + + private final HookProcessorProvider hookProcessorProvider; + private final HookRepository hookRepository; + private final TenantDetailsService tenantDetailsService; + + @Autowired + public MifosHookListener(final HookProcessorProvider hookProcessorProvider, + final HookRepository hookRepository, + final TenantDetailsService tenantDetailsService) { + this.hookRepository = hookRepository; + this.hookProcessorProvider = hookProcessorProvider; + this.tenantDetailsService = tenantDetailsService; + } + + @Override + public void onApplicationEvent(final HookEvent event) { + + final String tenantIdentifier = event.getTenantIdentifier(); + final MifosPlatformTenant tenant = this.tenantDetailsService + .loadTenantById(tenantIdentifier); + ThreadLocalContextUtil.setTenant(tenant); + + final AppUser appUser = event.getAppUser(); + final String authToken = event.getAuthToken(); + + final HookEventSource hookEventSource = event.getSource(); + final String entityName = hookEventSource.getEntityName(); + final String actionName = hookEventSource.getActionName(); + final String payload = event.getPayload(); + + final List hooks = this.hookRepository + .findAllHooksListeningToEvent(hookEventSource.getEntityName(), + hookEventSource.getActionName()); + for (final Hook hook : hooks) { + final HookProcessor processor = this.hookProcessorProvider + .getProcessor(hook); + processor.process(hook, appUser, payload, entityName, actionName, + tenantIdentifier, authToken); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/HookProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/HookProcessor.java new file mode 100644 index 000000000..17d2ff2d4 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/HookProcessor.java @@ -0,0 +1,16 @@ +/** + * 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.hooks.processor; + +import org.mifosplatform.infrastructure.hooks.domain.Hook; +import org.mifosplatform.useradministration.domain.AppUser; + +public interface HookProcessor { + + void process(Hook hook, AppUser appUser, String payload, String entityName, + String actionName, String tenantIdentifier, String authToken); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/HookProcessorProvider.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/HookProcessorProvider.java new file mode 100644 index 000000000..2c3fccfea --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/HookProcessorProvider.java @@ -0,0 +1,43 @@ +/** + * 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.hooks.processor; + +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.smsTemplateName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.webTemplateName; + +import org.mifosplatform.infrastructure.hooks.domain.Hook; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Service; + +@Service +public class HookProcessorProvider implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext( + final ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + public HookProcessor getProcessor(final Hook hook) { + HookProcessor processor; + final String templateName = hook.getHookTemplate().getName(); + if (templateName.equalsIgnoreCase(smsTemplateName)) { + processor = this.applicationContext.getBean("twilioHookProcessor", + TwilioHookProcessor.class); + } else if (templateName.equals(webTemplateName)) { + processor = this.applicationContext.getBean("webHookProcessor", + WebHookProcessor.class); + } else { + processor = null; + } + return processor; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/ProcessorHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/ProcessorHelper.java new file mode 100644 index 000000000..27dcbe43e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/ProcessorHelper.java @@ -0,0 +1,110 @@ +/** + * 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.hooks.processor; + +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import retrofit.Callback; +import retrofit.RestAdapter; +import retrofit.RetrofitError; +import retrofit.client.OkClient; +import retrofit.client.Response; + +import com.squareup.okhttp.OkHttpClient; + +@SuppressWarnings("unused") +public class ProcessorHelper { + + private final static Logger logger = LoggerFactory + .getLogger(ProcessorHelper.class); + + @SuppressWarnings("null") + public static OkHttpClient configureClient(final OkHttpClient client) { + final TrustManager[] certs = new TrustManager[] { new X509TrustManager() { + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkServerTrusted(final X509Certificate[] chain, + final String authType) throws CertificateException { + } + + @Override + public void checkClientTrusted(final X509Certificate[] chain, + final String authType) throws CertificateException { + } + } }; + + SSLContext ctx = null; + try { + ctx = SSLContext.getInstance("TLS"); + ctx.init(null, certs, new SecureRandom()); + } catch (final java.security.GeneralSecurityException ex) { + } + + try { + final HostnameVerifier hostnameVerifier = new HostnameVerifier() { + @Override + public boolean verify(final String hostname, + final SSLSession session) { + return true; + } + }; + client.setHostnameVerifier(hostnameVerifier); + client.setSslSocketFactory(ctx.getSocketFactory()); + } catch (final Exception e) { + } + + return client; + } + + public static OkHttpClient createClient() { + final OkHttpClient client = new OkHttpClient(); + return configureClient(client); + } + + @SuppressWarnings("rawtypes") + public static Callback createCallback(final String url) { + + return new Callback() { + @Override + public void success(final Object o, final Response response) { + logger.info("URL : " + url + "\tStatus : " + + response.getStatus()); + } + + @Override + public void failure(final RetrofitError retrofitError) { + logger.info(retrofitError.getMessage()); + } + }; + } + + public static WebHookService createWebHookService(final String url) { + + final OkHttpClient client = ProcessorHelper.createClient(); + + final RestAdapter restAdapter = new RestAdapter.Builder() + .setEndpoint(url).setClient(new OkClient(client)).build(); + + return restAdapter.create(WebHookService.class); + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/TwilioHookProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/TwilioHookProcessor.java new file mode 100644 index 000000000..667bd7388 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/TwilioHookProcessor.java @@ -0,0 +1,83 @@ +/** + * 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.hooks.processor; + +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.apiKeyName; + +import org.mifosplatform.infrastructure.hooks.domain.Hook; +import org.mifosplatform.infrastructure.hooks.domain.HookConfiguration; +import org.mifosplatform.infrastructure.hooks.domain.HookConfigurationRepository; +import org.mifosplatform.infrastructure.hooks.processor.data.SmsProviderData; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import retrofit.Callback; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +@Service +public class TwilioHookProcessor implements HookProcessor { + + private final HookConfigurationRepository hookConfigurationRepository; + + @Autowired + public TwilioHookProcessor( + final HookConfigurationRepository hookConfigurationRepository) { + this.hookConfigurationRepository = hookConfigurationRepository; + } + + @Override + public void process(final Hook hook, + @SuppressWarnings("unused") final AppUser appUser, + final String payload, final String entityName, + final String actionName, final String tenantIdentifier, + final String authToken) { + + final SmsProviderData smsProviderData = new SmsProviderData( + hook.getHookConfig()); + + sendRequest(smsProviderData, payload, entityName, actionName, + tenantIdentifier, authToken, hook); + } + + @SuppressWarnings("unchecked") + private void sendRequest(final SmsProviderData smsProviderData, + final String payload, final String entityName, + final String actionName, final String tenantIdentifier, + final String authToken, final Hook hook) { + + final WebHookService service = ProcessorHelper + .createWebHookService(smsProviderData.getUrl()); + + @SuppressWarnings("rawtypes") + final Callback callback = ProcessorHelper + .createCallback(smsProviderData.getUrl()); + + String apiKey = this.hookConfigurationRepository + .findOneByHookIdAndFieldName(hook.getId(), apiKeyName); + if (apiKey == null) { + smsProviderData.setUrl(null); + smsProviderData.setEndpoint(System.getProperty("baseUrl")); + smsProviderData.setTenantId(tenantIdentifier); + smsProviderData.setMifosToken(authToken); + apiKey = service.sendSmsBridgeConfigRequest(smsProviderData); + final HookConfiguration apiKeyEntry = HookConfiguration.createNew( + hook, "string", apiKeyName, apiKey); + this.hookConfigurationRepository.save(apiKeyEntry); + } + + if (apiKey != null && !apiKey.equals("")) { + final JsonObject json = new JsonParser().parse(payload) + .getAsJsonObject(); + service.sendSmsBridgeRequest(entityName, actionName, + tenantIdentifier, apiKey, json, callback); + } + + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/WebHookProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/WebHookProcessor.java new file mode 100644 index 000000000..aeb599198 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/WebHookProcessor.java @@ -0,0 +1,86 @@ +/** + * 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.hooks.processor; + +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.contentTypeName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.payloadURLName; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.mifosplatform.infrastructure.hooks.domain.Hook; +import org.mifosplatform.infrastructure.hooks.domain.HookConfiguration; +import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.stereotype.Service; + +import retrofit.Callback; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +@Service +public class WebHookProcessor implements HookProcessor { + + @Override + public void process(final Hook hook, + @SuppressWarnings("unused") final AppUser appUser, + final String payload, final String entityName, + final String actionName, final String tenantIdentifier, + final String authToken) { + + final Set config = hook.getHookConfig(); + + String url = ""; + String contentType = ""; + + for (final HookConfiguration conf : config) { + final String fieldName = conf.getFieldName(); + if (fieldName.equals(payloadURLName)) { + url = conf.getFieldValue(); + } + if (fieldName.equals(contentTypeName)) { + contentType = conf.getFieldValue(); + } + } + + sendRequest(url, contentType, payload, entityName, actionName, + tenantIdentifier, authToken); + + } + + @SuppressWarnings("unchecked") + private void sendRequest(final String url, final String contentType, + final String payload, final String entityName, + final String actionName, final String tenantIdentifier, + final String authToken) { + + final String mifosEndpointUrl = System.getProperty("baseUrl"); + final WebHookService service = ProcessorHelper + .createWebHookService(url); + + @SuppressWarnings("rawtypes") + final Callback callback = ProcessorHelper.createCallback(url); + + if (contentType.equalsIgnoreCase("json") + || contentType.contains("json")) { + final JsonObject json = new JsonParser().parse(payload) + .getAsJsonObject(); + json.addProperty("mifosToken", authToken); + service.sendJsonRequest(entityName, actionName, tenantIdentifier, + mifosEndpointUrl, json, callback); + } else { + Map map = new HashMap<>(); + map = new Gson().fromJson(payload, map.getClass()); + map.put("mifosToken", authToken); + service.sendFormRequest(entityName, actionName, tenantIdentifier, + mifosEndpointUrl, map, callback); + } + + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/WebHookService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/WebHookService.java new file mode 100644 index 000000000..c4e0d69cd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/WebHookService.java @@ -0,0 +1,57 @@ +/** + * 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.hooks.processor; + +import java.util.Map; + +import org.mifosplatform.infrastructure.hooks.processor.data.SmsProviderData; + +import retrofit.Callback; +import retrofit.client.Response; +import retrofit.http.Body; +import retrofit.http.FieldMap; +import retrofit.http.FormUrlEncoded; +import retrofit.http.Header; +import retrofit.http.POST; + +import com.google.gson.JsonObject; + +public interface WebHookService { + + final static String ENTITY_HEADER = "X-Mifos-Entity"; + final static String ACTION_HEADER = "X-Mifos-Action"; + final static String TENANT_HEADER = "X-Mifos-Platform-TenantId"; + final static String ENDPOINT_HEADER = "X-Mifos-Endpoint"; + final static String API_KEY_HEADER = "X-Mifos-API-Key"; + + // Template - Web + @POST("/") + void sendJsonRequest(@Header(ENTITY_HEADER) String entityHeader, + @Header(ACTION_HEADER) String actionHeader, + @Header(TENANT_HEADER) String tenantHeader, + @Header(ENDPOINT_HEADER) String endpointHeader, + @Body JsonObject result, Callback callBack); + + @FormUrlEncoded + @POST("/") + void sendFormRequest(@Header(ENTITY_HEADER) String entityHeader, + @Header(ACTION_HEADER) String actionHeader, + @Header(TENANT_HEADER) String tenantHeader, + @Header(ENDPOINT_HEADER) String endpointHeader, + @FieldMap Map params, Callback callBack); + + // Template - SMS Bridge + @POST("/") + void sendSmsBridgeRequest(@Header(ENTITY_HEADER) String entityHeader, + @Header(ACTION_HEADER) String actionHeader, + @Header(TENANT_HEADER) String tenantHeader, + @Header(API_KEY_HEADER) String apiKeyHeader, + @Body JsonObject result, Callback callBack); + + @POST("/configuration") + String sendSmsBridgeConfigRequest(@Body SmsProviderData config); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/data/SmsProviderData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/data/SmsProviderData.java new file mode 100644 index 000000000..85260bef6 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/processor/data/SmsProviderData.java @@ -0,0 +1,99 @@ +/** + * 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.hooks.processor.data; + +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.payloadURLName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.phoneNumberName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.smsProviderAccountIdName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.smsProviderName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.smsProviderTokenIdName; + +import java.util.Set; + +import org.mifosplatform.infrastructure.hooks.domain.HookConfiguration; + +public class SmsProviderData { + + private String url; + private String phoneNo; + private String smsProvider; + private String smsProviderAccountId; + private String smsProviderToken; + + private String tenantId; + private String mifosToken; + private String endpoint; + + public SmsProviderData(final Set config) { + + for (final HookConfiguration conf : config) { + final String fieldName = conf.getFieldName(); + if (fieldName.equals(payloadURLName)) { + this.url = conf.getFieldValue(); + } + if (fieldName.equals(smsProviderName)) { + this.smsProvider = conf.getFieldValue(); + } + if (fieldName.equals(smsProviderAccountIdName)) { + this.smsProviderAccountId = conf.getFieldValue(); + } + if (fieldName.equals(smsProviderTokenIdName)) { + this.smsProviderToken = conf.getFieldValue(); + } + if (fieldName.equals(phoneNumberName)) { + this.phoneNo = conf.getFieldValue(); + } + } + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getPhoneNumber() { + return phoneNo; + } + + public String getSmsProvider() { + return smsProvider; + } + + public String getSmsProviderAccountId() { + return smsProviderAccountId; + } + + public String getSmsProviderTokenId() { + return smsProviderToken; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getMifosToken() { + return mifosToken; + } + + public void setMifosToken(String mifosToken) { + this.mifosToken = mifosToken; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/serialization/HookCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/serialization/HookCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..575270c05 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/serialization/HookCommandFromApiJsonDeserializer.java @@ -0,0 +1,102 @@ +/** + * 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.hooks.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.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + +@Component +public class HookCommandFromApiJsonDeserializer { + + /** + * The parameters supported for this command. + */ + private final Set supportedParameters = new HashSet<>( + Arrays.asList("name", "displayName", "isActive", "events", "config")); + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public HookCommandFromApiJsonDeserializer( + 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(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + this.supportedParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder( + dataValidationErrors).resource("hook"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final String name = this.fromApiJsonHelper.extractStringNamed("name", + element); + baseDataValidator.reset().parameter("name").value(name).notBlank() + .notExceedingLengthOf(100); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateForUpdate(final String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() { + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + this.supportedParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder( + dataValidationErrors).resource("hook"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + if (this.fromApiJsonHelper.parameterExists("name", element)) { + final String name = this.fromApiJsonHelper.extractStringNamed( + "name", element); + baseDataValidator.reset().parameter("name").value(name).notBlank() + .notExceedingLengthOf(100); + } + + 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/hooks/service/HookReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookReadPlatformService.java new file mode 100644 index 000000000..27b07de14 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookReadPlatformService.java @@ -0,0 +1,19 @@ +/** + * 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.hooks.service; + +import java.util.Collection; + +import org.mifosplatform.infrastructure.hooks.data.HookData; + +public interface HookReadPlatformService { + + Collection retrieveAllHooks(); + + HookData retrieveHook(Long hookId); + + HookData retrieveNewHookDetails(String templateName); +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookReadPlatformServiceImpl.java new file mode 100644 index 000000000..dcd846658 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookReadPlatformServiceImpl.java @@ -0,0 +1,241 @@ +/** + * 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.hooks.service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.core.domain.JdbcSupport; +import org.mifosplatform.infrastructure.core.service.RoutingDataSource; +import org.mifosplatform.infrastructure.hooks.data.Event; +import org.mifosplatform.infrastructure.hooks.data.EventResultSetExtractor; +import org.mifosplatform.infrastructure.hooks.data.Field; +import org.mifosplatform.infrastructure.hooks.data.Grouping; +import org.mifosplatform.infrastructure.hooks.data.HookData; +import org.mifosplatform.infrastructure.hooks.data.HookTemplateData; +import org.mifosplatform.infrastructure.hooks.exception.HookNotFoundException; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +public class HookReadPlatformServiceImpl implements HookReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final PlatformSecurityContext context; + + @Autowired + public HookReadPlatformServiceImpl(final PlatformSecurityContext context, + final RoutingDataSource dataSource) { + this.context = context; + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public Collection retrieveAllHooks() { + this.context.authenticatedUser(); + final HookMapper rm = new HookMapper(this.jdbcTemplate); + final String sql = "select " + rm.schema() + " order by h.name"; + + return this.jdbcTemplate.query(sql, rm, new Object[] {}); + } + + @Override + public HookData retrieveHook(final Long hookId) { + try { + this.context.authenticatedUser(); + final HookMapper rm = new HookMapper(this.jdbcTemplate); + final String sql = "select " + rm.schema() + " where h.id = ?"; + + return this.jdbcTemplate.queryForObject(sql, rm, + new Object[] { hookId }); + } catch (final EmptyResultDataAccessException e) { + throw new HookNotFoundException(hookId); + } + + } + + @Override + public HookData retrieveNewHookDetails(final String templateName) { + + this.context.authenticatedUser(); + final TemplateMapper rm = new TemplateMapper(this.jdbcTemplate); + final String sql; + List templateData; + + if (templateName == null) { + sql = "select " + rm.schema() + " order by s.name"; + templateData = this.jdbcTemplate.query(sql, rm, new Object[] {}); + } else { + sql = "select " + rm.schema() + " where s.name = ? order by s.name"; + templateData = this.jdbcTemplate.query(sql, rm, + new Object[] { templateName }); + } + + final List events = getTemplateForEvents(); + + return HookData.template(templateData, events); + } + + private List getTemplateForEvents() { + final String sql = "select p.grouping, p.entity_name, p.action_name from m_permission p " + + " where p.action_name NOT LIKE '%CHECKER%' AND p.action_name NOT LIKE '%READ%' " + + " order by p.grouping, p.entity_name "; + final EventResultSetExtractor extractor = new EventResultSetExtractor(); + return this.jdbcTemplate.query(sql, extractor); + } + + private static final class HookMapper implements RowMapper { + + private final JdbcTemplate jdbcTemplate; + + public HookMapper(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public String schema() { + return " h.id, s.name as name, h.name as display_name, h.is_active, h.created_date, h.lastmodified_date " + + " from m_hook h inner join m_hook_templates s on h.template_id = s.id "; + } + + @Override + public HookData mapRow(final ResultSet rs, + @SuppressWarnings("unused") final int rowNum) + throws SQLException { + + final Long id = rs.getLong("id"); + final String name = rs.getString("name"); + final String displayname = rs.getString("display_name"); + final boolean isActive = rs.getBoolean("is_active"); + final LocalDate createdAt = JdbcSupport.getLocalDate(rs, + "created_date"); + final LocalDate updatedAt = JdbcSupport.getLocalDate(rs, + "lastmodified_date"); + final List registeredEvents = retrieveEvents(id); + final List config = retrieveConfig(id); + + return HookData.instance(id, name, displayname, isActive, + createdAt, updatedAt, registeredEvents, config); + } + + private List retrieveEvents(final Long hookId) { + + final HookEventMapper rm = new HookEventMapper(); + final String sql = "select " + rm.schema() + " where h.id= ?"; + + return this.jdbcTemplate.query(sql, rm, new Object[] { hookId }); + } + + private List retrieveConfig(final Long hookId) { + + final HookConfigMapper rm = new HookConfigMapper(); + final String sql = "select " + rm.schema() + + " where h.id= ? order by hc.field_name"; + + final List fields = this.jdbcTemplate.query(sql, rm, + new Object[] { hookId }); + + return fields; + } + } + + private static final class HookEventMapper implements RowMapper { + + public String schema() { + return " re.action_name, re.entity_name from m_hook h inner join m_hook_registered_events re on h.id = re.hook_id "; + } + + @Override + public Event mapRow(final ResultSet rs, + @SuppressWarnings("unused") final int rowNum) + throws SQLException { + final String actionName = rs.getString("action_name"); + final String entityName = rs.getString("entity_name"); + return Event.instance(actionName, entityName); + } + } + + private static final class HookConfigMapper implements RowMapper { + + public String schema() { + return " hc.field_name, hc.field_value from m_hook h inner join m_hook_configuration hc on h.id = hc.hook_id "; + } + + @Override + public Field mapRow(final ResultSet rs, + @SuppressWarnings("unused") final int rowNum) + throws SQLException { + final String fieldName = rs.getString("field_name"); + final String fieldValue = rs.getString("field_value"); + return Field.fromConfig(fieldName, fieldValue); + } + } + + private static final class TemplateMapper implements + RowMapper { + + private final JdbcTemplate jdbcTemplate; + + public TemplateMapper(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public String schema() { + return " s.id, s.name from m_hook_templates s "; + } + + @Override + public HookTemplateData mapRow(final ResultSet rs, + @SuppressWarnings("unused") final int rowNum) + throws SQLException { + + final Long id = rs.getLong("id"); + final String name = rs.getString("name"); + final List schema = retrieveSchema(id); + + return HookTemplateData.instance(id, name, schema); + } + + private List retrieveSchema(final Long templateId) { + + final TemplateSchemaMapper rm = new TemplateSchemaMapper(); + final String sql = "select " + rm.schema() + + " where s.id= ? order by hs.field_name "; + + final List fields = this.jdbcTemplate.query(sql, rm, + new Object[] { templateId }); + + return fields; + } + } + + private static final class TemplateSchemaMapper implements RowMapper { + + public String schema() { + return " hs.field_type, hs.field_name, hs.placeholder, hs.optional from m_hook_templates s " + + " inner join m_hook_schema hs on s.id = hs.hook_template_id "; + } + + @Override + public Field mapRow(final ResultSet rs, + @SuppressWarnings("unused") final int rowNum) + throws SQLException { + final String fieldName = rs.getString("field_name"); + final String fieldType = rs.getString("field_type"); + final Boolean optional = rs.getBoolean("optional"); + final String placeholder = rs.getString("placeholder"); + return Field + .fromSchema(fieldType, fieldName, optional, placeholder); + } + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookWritePlatformService.java new file mode 100644 index 000000000..da1fd215e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookWritePlatformService.java @@ -0,0 +1,19 @@ +/** + * 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.hooks.service; + +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; + +public interface HookWritePlatformService { + + CommandProcessingResult createHook(JsonCommand command); + + CommandProcessingResult updateHook(Long hookId, JsonCommand command); + + CommandProcessingResult deleteHook(Long hookId); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookWritePlatformServiceJpaRepositoryImpl.java new file mode 100644 index 000000000..c961736f2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/hooks/service/HookWritePlatformServiceJpaRepositoryImpl.java @@ -0,0 +1,292 @@ +/** + * 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.hooks.service; + +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.actionNameParamName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.configParamName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.entityNameParamName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.eventsParamName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.nameParamName; +import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.webTemplateName; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; +import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; +import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; +import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.mifosplatform.infrastructure.hooks.domain.Hook; +import org.mifosplatform.infrastructure.hooks.domain.HookConfiguration; +import org.mifosplatform.infrastructure.hooks.domain.HookRepository; +import org.mifosplatform.infrastructure.hooks.domain.HookResource; +import org.mifosplatform.infrastructure.hooks.domain.HookTemplate; +import org.mifosplatform.infrastructure.hooks.domain.HookTemplateRepository; +import org.mifosplatform.infrastructure.hooks.domain.Schema; +import org.mifosplatform.infrastructure.hooks.exception.HookNotFoundException; +import org.mifosplatform.infrastructure.hooks.exception.HookTemplateNotFoundException; +import org.mifosplatform.infrastructure.hooks.serialization.HookCommandFromApiJsonDeserializer; +import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +@Service +public class HookWritePlatformServiceJpaRepositoryImpl implements + HookWritePlatformService { + + private final PlatformSecurityContext context; + private final HookRepository hookRepository; + private final HookTemplateRepository hookTemplateRepository; + private final HookCommandFromApiJsonDeserializer fromApiJsonDeserializer; + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public HookWritePlatformServiceJpaRepositoryImpl( + final PlatformSecurityContext context, + final HookRepository hookRepository, + final HookTemplateRepository hookTemplateRepository, + final HookCommandFromApiJsonDeserializer fromApiJsonDeserializer, + final FromJsonHelper fromApiJsonHelper) { + this.context = context; + this.hookRepository = hookRepository; + this.hookTemplateRepository = hookTemplateRepository; + this.fromApiJsonDeserializer = fromApiJsonDeserializer; + this.fromApiJsonHelper = fromApiJsonHelper; + } + + @Transactional + @Override + public CommandProcessingResult createHook(final JsonCommand command) { + + try { + this.context.authenticatedUser(); + + this.fromApiJsonDeserializer.validateForCreate(command.json()); + + final HookTemplate template = retrieveHookTemplateBy(command + .stringValueOfParameterNamed(nameParamName)); + final String configJson = command.jsonFragment(configParamName); + final Set config = assembleConfig( + command.mapValueOfParameterNamed(configJson), template); + final JsonArray events = command + .arrayOfParameterNamed(eventsParamName); + final Set allEvents = assembleSetOfEvents(events); + final Hook hook = Hook.fromJson(command, template, config, + allEvents); + + validateHookRules(template, config); + + this.hookRepository.save(hook); + + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withEntityId(hook.getId()).build(); + } catch (final DataIntegrityViolationException dve) { + handleHookDataIntegrityIssues(command, dve); + return CommandProcessingResult.empty(); + } + } + + @Transactional + @Override + public CommandProcessingResult updateHook(final Long hookId, + final JsonCommand command) { + + try { + this.context.authenticatedUser(); + + this.fromApiJsonDeserializer.validateForUpdate(command.json()); + + final Hook hook = retrieveHookBy(hookId); + final HookTemplate template = hook.getHookTemplate(); + final Map changes = hook.update(command); + + if (!changes.isEmpty()) { + + if (changes.containsKey(eventsParamName)) { + final Set events = assembleSetOfEvents(command + .arrayOfParameterNamed(eventsParamName)); + final boolean updated = hook.updateEvents(events); + if (!updated) { + changes.remove(eventsParamName); + } + } + + if (changes.containsKey(configParamName)) { + final String configJson = command + .jsonFragment(configParamName); + final Set config = assembleConfig( + command.mapValueOfParameterNamed(configJson), + template); + final boolean updated = hook.updateConfig(config); + if (!updated) { + changes.remove(configParamName); + } + } + + this.hookRepository.saveAndFlush(hook); + } + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(hookId) // + .with(changes) // + .build(); + } catch (final DataIntegrityViolationException dve) { + handleHookDataIntegrityIssues(command, dve); + return null; + } + } + + @Transactional + @Override + public CommandProcessingResult deleteHook(final Long hookId) { + + this.context.authenticatedUser(); + + final Hook hook = retrieveHookBy(hookId); + + try { + this.hookRepository.delete(hook); + this.hookRepository.flush(); + } catch (final DataIntegrityViolationException e) { + throw new PlatformDataIntegrityException( + "error.msg.unknown.data.integrity.issue", + "Unknown data integrity issue with resource: " + + e.getMostSpecificCause()); + } + return new CommandProcessingResultBuilder().withEntityId(hookId) + .build(); + } + + private Hook retrieveHookBy(final Long hookId) { + final Hook hook = this.hookRepository.findOne(hookId); + if (hook == null) { + throw new HookNotFoundException(hookId.toString()); + } + return hook; + } + + private HookTemplate retrieveHookTemplateBy(final String templateName) { + final HookTemplate template = this.hookTemplateRepository + .findOne(templateName); + if (template == null) { + throw new HookTemplateNotFoundException(templateName); + } + return template; + } + + private Set assembleConfig( + final Map hookConfig, final HookTemplate template) { + + final Set configuration = new HashSet<>(); + final Set fields = template.getSchema(); + + for (final Entry configEntry : hookConfig.entrySet()) { + for (final Schema field : fields) { + if (field.getFieldName().equalsIgnoreCase(configEntry.getKey())) { + final HookConfiguration config = HookConfiguration + .createNewWithoutHook(field.getFieldType(), + configEntry.getKey(), + configEntry.getValue()); + configuration.add(config); + break; + } + } + + } + + return configuration; + } + + private Set assembleSetOfEvents(final JsonArray eventsArray) { + + final Set allEvents = new HashSet<>(); + + for (int i = 0; i < eventsArray.size(); i++) { + + final JsonObject eventElement = eventsArray.get(i) + .getAsJsonObject(); + + final String entityName = this.fromApiJsonHelper + .extractStringNamed(entityNameParamName, eventElement); + final String actionName = this.fromApiJsonHelper + .extractStringNamed(actionNameParamName, eventElement); + final HookResource event = HookResource.createNewWithoutHook( + entityName, actionName); + allEvents.add(event); + } + + return allEvents; + } + + private void validateHookRules(final HookTemplate template, + final Set config) { + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder( + dataValidationErrors).resource("hook"); + + if (!template.getName().equalsIgnoreCase(webTemplateName) + && this.hookRepository.findOneByTemplateId(template.getId()) != null) { + final String errorMessage = "multiple.non.web.template.hooks.not.supported"; + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + errorMessage); + } + + final Set fields = template.getSchema(); + for (final Schema field : fields) { + if (!field.isOptional()) { + boolean found = false; + for (final HookConfiguration conf : config) { + if (field.getFieldName().equals(conf.getFieldName())) { + found = true; + } + } + if (!found) { + final String errorMessage = "required.config.field." + + field.getFieldName() + ".not.provided"; + baseDataValidator.reset() + .failWithCodeNoParameterAddedToErrorCode( + errorMessage); + } + } + } + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + private void handleHookDataIntegrityIssues(final JsonCommand command, + final DataIntegrityViolationException dve) { + final Throwable realCause = dve.getMostSpecificCause(); + if (realCause.getMessage().contains("hook_name")) { + final String name = command.stringValueOfParameterNamed("name"); + throw new PlatformDataIntegrityException( + "error.msg.hook.duplicate.name", "A hook with name '" + + name + "' already exists", "name", name); + } + + throw new PlatformDataIntegrityException( + "error.msg.unknown.data.integrity.issue", + "Unknown data integrity issue with resource: " + + realCause.getMessage()); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java index 9283f1044..652e09fb7 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/filter/TenantAwareBasicAuthenticationFilter.java @@ -53,7 +53,7 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil private static boolean firstRequestProcessed = false; private final static Logger logger = LoggerFactory.getLogger(TenantAwareBasicAuthenticationFilter.class); - + private final BasicAuthTenantDetailsService basicAuthTenantDetailsService; private final ToApiJsonSerializer toApiJsonSerializer; private final ConfigurationDomainService configurationDomainService; @@ -103,8 +103,16 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil final MifosPlatformTenant tenant = this.basicAuthTenantDetailsService.loadTenantById(tenantIdentifier); ThreadLocalContextUtil.setTenant(tenant); + String authToken = request.getHeader("Authorization"); + + if (authToken != null && authToken.startsWith("Basic ")) { + ThreadLocalContextUtil.setAuthToken(authToken.replaceFirst("Basic ", "")); + } if (!firstRequestProcessed) { + final String baseUrl = request.getRequestURL().toString().replace(request.getPathInfo(), "/"); + System.setProperty("baseUrl", baseUrl); + final boolean ehcacheEnabled = this.configurationDomainService.isEhcacheEnabled(); if (ehcacheEnabled) { this.cacheWritePlatformService.switchToCache(CacheType.SINGLE_NODE); @@ -114,7 +122,7 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil TenantAwareBasicAuthenticationFilter.firstRequestProcessed = true; } } - + super.doFilter(req, res, chain); } catch (final InvalidTenantIdentiferException e) { // deal with exception at low level diff --git a/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml b/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml index 292572b3d..8034f32ef 100644 --- a/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml +++ b/mifosng-provider/src/main/resources/META-INF/spring/appContext.xml @@ -72,4 +72,10 @@ + + + + + +