diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/HookIntegrationTest.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/HookIntegrationTest.java new file mode 100644 index 000000000..23ce8c167 --- /dev/null +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/HookIntegrationTest.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.integrationtests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.apache.http.conn.HttpHostConnectException; +import org.junit.Before; +import org.junit.Test; +import org.mifosplatform.integrationtests.common.HookHelper; +import org.mifosplatform.integrationtests.common.OfficeHelper; +import org.mifosplatform.integrationtests.common.Utils; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.builder.ResponseSpecBuilder; +import com.jayway.restassured.http.ContentType; +import com.jayway.restassured.path.json.JsonPath; +import com.jayway.restassured.specification.RequestSpecification; +import com.jayway.restassured.specification.ResponseSpecification; + +public class HookIntegrationTest { + + private RequestSpecification requestSpec; + private ResponseSpecification responseSpec; + + private HookHelper hookHelper; + private OfficeHelper officeHelper; + + @Before + public void setUp() throws Exception { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.hookHelper = new HookHelper(this.requestSpec, this.responseSpec); + this.officeHelper = new OfficeHelper(this.requestSpec, this.responseSpec); + } + + @Test + public void shouldSendOfficeCreationNotification() { + //Subject to https://echo-webhook.herokuapp.com being up + //See http://www.jamesward.com/2014/06/11/testing-webhooks-was-a-pain-so-i-fixed-the-glitch + final String payloadURL = "http://echo-webhook.herokuapp.com:80/Z7RXoCBdLSFMDrpn?"; + this.hookHelper.createHook(payloadURL); + final Integer createdOfficeID = this.officeHelper.createOffice("01 January 2012"); + try { + final String json = RestAssured.get(payloadURL.replace("?", "")).asString(); + final Integer notificationOfficeId = JsonPath.with(json).get("officeId"); + assertEquals("Equality check for created officeId and hook received payload officeId", + createdOfficeID, notificationOfficeId); + System.out.println("Notification Office Id - " + notificationOfficeId); + } catch (final Exception e) { + if (e instanceof HttpHostConnectException) { + fail("Failed to connect to https://echo-webhook.herokuapp.com platform"); + } + throw new RuntimeException(e); + } + + } + +} diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/HookHelper.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/HookHelper.java new file mode 100644 index 000000000..a9f546b9d --- /dev/null +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/HookHelper.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.integrationtests.common; + +import java.util.ArrayList; +import java.util.HashMap; + +import com.google.gson.Gson; +import com.jayway.restassured.specification.RequestSpecification; +import com.jayway.restassured.specification.ResponseSpecification; + +public class HookHelper { + + private final RequestSpecification requestSpec; + private final ResponseSpecification responseSpec; + + private static final String CREATE_HOOK_URL = "/mifosng-provider/api/v1/hooks?" + Utils.TENANT_IDENTIFIER; + + public HookHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { + this.requestSpec = requestSpec; + this.responseSpec = responseSpec; + } + + public Integer createHook(final String payloadURL) { + System.out.println("---------------------------------CREATING A HOOK---------------------------------------------"); + return Utils.performServerPost(requestSpec, responseSpec, CREATE_HOOK_URL, getTestHookAsJson(payloadURL), + "resourceId"); + } + + public String getTestHookAsJson(final String payloadURL) { + final HashMap map = new HashMap<>(); + map.put("name", "Web"); + map.put("displayName", Utils.randomNameGenerator("Hook_DisplayName_", 5)); + map.put("isActive", "true"); + final HashMap config = new HashMap<>(); + config.put("Content Type", "json"); + config.put("Payload URL", payloadURL); + map.put("config", config); + final ArrayList> events = new ArrayList<>(); + final HashMap createOfficeEvent = new HashMap<>(); + createOfficeEvent.put("actionName", "CREATE"); + createOfficeEvent.put("entityName", "OFFICE"); + events.add(createOfficeEvent); + map.put("events", events); + System.out.println("map : " + map); + return new Gson().toJson(map); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/boot/WebXmlConfiguration.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/boot/WebXmlConfiguration.java index 87d8a184b..2998cec0b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/boot/WebXmlConfiguration.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/boot/WebXmlConfiguration.java @@ -41,6 +41,7 @@ public class WebXmlConfiguration { jerseyServletRegistration.addInitParameter("com.sun.jersey.api.json.POJOMappingFeature", "true"); jerseyServletRegistration.addInitParameter("com.sun.jersey.spi.container.ContainerResponseFilters", ResponseCorsFilter.class.getName()); + jerseyServletRegistration.addInitParameter("com.sun.jersey.config.feature.DisableWADL", "true"); // debugging for development: // jerseyServletRegistration.addInitParameter("com.sun.jersey.spi.container.ContainerRequestFilters", // LoggingFilter.class.getName()); 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 index 2c57592c9..6b0e16692 100644 --- 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 @@ -65,8 +65,10 @@ public class Hook extends AbstractAuditableCustom { final Set events) { final String displayName = command .stringValueOfParameterNamed(displayNameParamName); - final Boolean isActive = command + Boolean isActive = command .booleanObjectValueOfParameterNamed(isActiveParamName); + if(isActive == null) + isActive = false; return new Hook(template, displayName, isActive, config, events); } 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 index 63c45311f..e2831c953 100644 --- 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 @@ -10,11 +10,11 @@ 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.hooks.service.HookReadPlatformService; import org.mifosplatform.infrastructure.security.service.TenantDetailsService; import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; @@ -24,14 +24,14 @@ import org.springframework.stereotype.Service; public class MifosHookListener implements HookListener { private final HookProcessorProvider hookProcessorProvider; - private final HookRepository hookRepository; + private final HookReadPlatformService hookReadPlatformService; private final TenantDetailsService tenantDetailsService; @Autowired public MifosHookListener(final HookProcessorProvider hookProcessorProvider, - final HookRepository hookRepository, + final HookReadPlatformService hookReadPlatformService, final TenantDetailsService tenantDetailsService) { - this.hookRepository = hookRepository; + this.hookReadPlatformService = hookReadPlatformService; this.hookProcessorProvider = hookProcessorProvider; this.tenantDetailsService = tenantDetailsService; } @@ -51,10 +51,11 @@ public class MifosHookListener implements HookListener { final String entityName = hookEventSource.getEntityName(); final String actionName = hookEventSource.getActionName(); final String payload = event.getPayload(); + + final List hooks = this.hookReadPlatformService + .retrieveHooksByEvent(hookEventSource.getEntityName(), + hookEventSource.getActionName()); - final List hooks = this.hookRepository - .findAllHooksListeningToEvent(hookEventSource.getEntityName(), - hookEventSource.getActionName()); for (final Hook hook : hooks) { final HookProcessor processor = this.hookProcessorProvider .getProcessor(hook); 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 index aeb599198..b7f336213 100644 --- 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 @@ -57,7 +57,7 @@ public class WebHookProcessor implements HookProcessor { private void sendRequest(final String url, final String contentType, final String payload, final String entityName, final String actionName, final String tenantIdentifier, - final String authToken) { + @SuppressWarnings("unused") final String authToken) { final String mifosEndpointUrl = System.getProperty("baseUrl"); final WebHookService service = ProcessorHelper @@ -70,13 +70,11 @@ public class WebHookProcessor implements HookProcessor { || 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 index c4e0d69cd..0a2916ee9 100644 --- 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 @@ -14,6 +14,7 @@ import retrofit.client.Response; import retrofit.http.Body; import retrofit.http.FieldMap; import retrofit.http.FormUrlEncoded; +import retrofit.http.GET; import retrofit.http.Header; import retrofit.http.POST; @@ -27,6 +28,10 @@ public interface WebHookService { final static String ENDPOINT_HEADER = "X-Mifos-Endpoint"; final static String API_KEY_HEADER = "X-Mifos-API-Key"; + // Ping + @GET("/") + Response sendEmptyRequest(); + // Template - Web @POST("/") void sendJsonRequest(@Header(ENTITY_HEADER) String entityHeader, 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 index 27b07de14..934621195 100644 --- 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 @@ -6,8 +6,10 @@ package org.mifosplatform.infrastructure.hooks.service; import java.util.Collection; +import java.util.List; import org.mifosplatform.infrastructure.hooks.data.HookData; +import org.mifosplatform.infrastructure.hooks.domain.Hook; public interface HookReadPlatformService { @@ -15,5 +17,7 @@ public interface HookReadPlatformService { HookData retrieveHook(Long hookId); - HookData retrieveNewHookDetails(String templateName); + List retrieveHooksByEvent(final String actionName, final String entityName); + + 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 index dcd846658..bd12ce661 100644 --- 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 @@ -19,9 +19,12 @@ 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.domain.Hook; +import org.mifosplatform.infrastructure.hooks.domain.HookRepository; import org.mifosplatform.infrastructure.hooks.exception.HookNotFoundException; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -31,12 +34,15 @@ import org.springframework.stereotype.Service; public class HookReadPlatformServiceImpl implements HookReadPlatformService { private final JdbcTemplate jdbcTemplate; + private final HookRepository hookRepository; private final PlatformSecurityContext context; @Autowired public HookReadPlatformServiceImpl(final PlatformSecurityContext context, + final HookRepository hookRepository, final RoutingDataSource dataSource) { this.context = context; + this.hookRepository = hookRepository; this.jdbcTemplate = new JdbcTemplate(dataSource); } @@ -64,6 +70,15 @@ public class HookReadPlatformServiceImpl implements HookReadPlatformService { } + @Override + @Cacheable(value = "hooks", key = "T(org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('HK')") + public List retrieveHooksByEvent(final String actionName, + final String entityName) { + + return this.hookRepository.findAllHooksListeningToEvent(actionName, + entityName); + } + @Override public HookData retrieveNewHookDetails(final String templateName) { 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 index c961736f2..f837ffc7a 100644 --- 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 @@ -7,9 +7,11 @@ 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.contentTypeName; 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.payloadURLName; import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.webTemplateName; import java.util.ArrayList; @@ -36,13 +38,18 @@ 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.processor.ProcessorHelper; +import org.mifosplatform.infrastructure.hooks.processor.WebHookService; import org.mifosplatform.infrastructure.hooks.serialization.HookCommandFromApiJsonDeserializer; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import retrofit.RetrofitError; + import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -72,6 +79,7 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements @Transactional @Override + @CacheEvict(value = "hooks", allEntries = true) public CommandProcessingResult createHook(final JsonCommand command) { try { @@ -90,7 +98,7 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements final Hook hook = Hook.fromJson(command, template, config, allEvents); - validateHookRules(template, config); + validateHookRules(template, config, allEvents); this.hookRepository.save(hook); @@ -105,6 +113,7 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements @Transactional @Override + @CacheEvict(value = "hooks", allEntries = true) public CommandProcessingResult updateHook(final Long hookId, final JsonCommand command) { @@ -156,6 +165,7 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements @Transactional @Override + @CacheEvict(value = "hooks", allEntries = true) public CommandProcessingResult deleteHook(final Long hookId) { this.context.authenticatedUser(); @@ -200,7 +210,9 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements for (final Entry configEntry : hookConfig.entrySet()) { for (final Schema field : fields) { - if (field.getFieldName().equalsIgnoreCase(configEntry.getKey())) { + final String fieldName = field.getFieldName(); + if (fieldName.equalsIgnoreCase(configEntry.getKey())) { + final HookConfiguration config = HookConfiguration .createNewWithoutHook(field.getFieldType(), configEntry.getKey(), @@ -237,7 +249,7 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements } private void validateHookRules(final HookTemplate template, - final Set config) { + final Set config, Set events) { final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder( @@ -250,6 +262,43 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements errorMessage); } + for (final HookConfiguration conf : config) { + final String fieldValue = conf.getFieldValue(); + if (conf.getFieldName().equals(contentTypeName)) { + if (!(fieldValue.equalsIgnoreCase("json") || fieldValue + .equalsIgnoreCase("form"))) { + final String errorMessage = "content.type.must.be.json.or.form"; + baseDataValidator.reset() + .failWithCodeNoParameterAddedToErrorCode( + errorMessage); + } + } + + if (conf.getFieldName().equals(payloadURLName)) { + try { + final WebHookService service = ProcessorHelper + .createWebHookService(fieldValue); + service.sendEmptyRequest(); + } catch (RetrofitError re) { + // Swallow error if it's because of method not supported or + // if url throws 404 - required for integration test, + // url generated on 1st POST request + if (re.getResponse() == null) { + String errorMessage = "url.invalid"; + baseDataValidator.reset() + .failWithCodeNoParameterAddedToErrorCode( + errorMessage); + } + } + } + } + + if (events == null || events.isEmpty()) { + final String errorMessage = "registered.events.cannot.be.empty"; + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode( + errorMessage); + } + final Set fields = template.getSchema(); for (final Schema field : fields) { if (!field.isOptional()) { @@ -261,8 +310,8 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements } if (!found) { final String errorMessage = "required.config.field." - + field.getFieldName() + ".not.provided"; - baseDataValidator.reset() + + "not.provided"; + baseDataValidator.reset().value(field.getFieldName()) .failWithCodeNoParameterAddedToErrorCode( errorMessage); } diff --git a/mifosng-provider/src/main/resources/META-INF/spring/ehcache.xml b/mifosng-provider/src/main/resources/META-INF/spring/ehcache.xml index 2b8e8e135..c4601f9c4 100644 --- a/mifosng-provider/src/main/resources/META-INF/spring/ehcache.xml +++ b/mifosng-provider/src/main/resources/META-INF/spring/ehcache.xml @@ -34,4 +34,6 @@ overflowToDisk="false" /> + \ No newline at end of file