FINERACT-1241-elastic-web-hook-update (#1488)
This commit is contained in:
@@ -79,7 +79,8 @@ dependencies {
|
||||
'io.swagger.core.v3:swagger-annotations',
|
||||
'org.webjars:webjars-locator-core',
|
||||
|
||||
'com.google.cloud.sql:mysql-socket-factory-connector-j-8:1.1.0'
|
||||
'com.google.cloud.sql:mysql-socket-factory-connector-j-8:1.1.0',
|
||||
'com.squareup.retrofit2:converter-gson'
|
||||
)
|
||||
implementation ('org.apache.activemq:activemq-broker') {
|
||||
exclude group: 'org.apache.geronimo.specs'
|
||||
|
||||
+83
-13
@@ -18,8 +18,15 @@
|
||||
*/
|
||||
package org.apache.fineract.commands.service;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.apache.fineract.batch.exception.ErrorHandler;
|
||||
import org.apache.fineract.batch.exception.ErrorInfo;
|
||||
import org.apache.fineract.commands.domain.CommandSource;
|
||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
@@ -38,6 +45,8 @@ import org.apache.fineract.infrastructure.hooks.event.HookEvent;
|
||||
import org.apache.fineract.infrastructure.hooks.event.HookEventSource;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -46,6 +55,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Service
|
||||
public class SynchronousCommandProcessingService implements CommandProcessingService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SynchronousCommandProcessingService.class);
|
||||
private PlatformSecurityContext context;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final ToApiJsonSerializer<Map<String, Object>> toApiJsonSerializer;
|
||||
@@ -80,7 +90,14 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
||||
|
||||
final NewCommandSourceHandler handler = findCommandHandler(wrapper);
|
||||
|
||||
final CommandProcessingResult result = handler.processCommand(command);
|
||||
final CommandProcessingResult result;
|
||||
try {
|
||||
result = handler.processCommand(command);
|
||||
} catch (Throwable t) {
|
||||
// publish error event
|
||||
publishErrorEvent(wrapper, command, t);
|
||||
throw t;
|
||||
}
|
||||
|
||||
final AppUser maker = this.context.authenticatedUser(wrapper);
|
||||
|
||||
@@ -125,7 +142,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
||||
}
|
||||
result.setRollbackTransaction(null);
|
||||
|
||||
publishEvent(wrapper.entityName(), wrapper.actionName(), result);
|
||||
publishEvent(wrapper.entityName(), wrapper.actionName(), command, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -206,18 +223,71 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
||||
return rollbackTransaction;
|
||||
}
|
||||
|
||||
private void publishEvent(final String entityName, final String actionName, final CommandProcessingResult result) {
|
||||
private void publishErrorEvent(CommandWrapper wrapper, JsonCommand command, Throwable t) {
|
||||
|
||||
final String authToken = ThreadLocalContextUtil.getAuthToken();
|
||||
final String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
|
||||
final AppUser appUser = this.context.authenticatedUser(CommandWrapper.wrap(actionName, entityName, null, null));
|
||||
ErrorInfo ex;
|
||||
if (t instanceof RuntimeException) {
|
||||
final RuntimeException e = (RuntimeException) t;
|
||||
ex = ErrorHandler.handler(e);
|
||||
} else {
|
||||
ex = new ErrorInfo(500, 9999, "{\"Exception\": " + t.toString() + "}");
|
||||
}
|
||||
|
||||
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);
|
||||
publishEvent(wrapper.entityName(), wrapper.actionName(), command, ex);
|
||||
}
|
||||
|
||||
private void publishEvent(final String entityName, final String actionName, JsonCommand command, final Object result) {
|
||||
Gson gson = new Gson();
|
||||
try {
|
||||
final String authToken = ThreadLocalContextUtil.getAuthToken();
|
||||
final String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
|
||||
final AppUser appUser = this.context.authenticatedUser(CommandWrapper.wrap(actionName, entityName, null, null));
|
||||
|
||||
final HookEventSource hookEventSource = new HookEventSource(entityName, actionName);
|
||||
|
||||
// TODO: Add support for publishing array events
|
||||
if (command.json() != null && command.json().startsWith("{")) {
|
||||
Type type = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
Map<String, Object> myMap = gson.fromJson(command.json(), type);
|
||||
|
||||
Map<String, Object> reqmap = new HashMap<>();
|
||||
reqmap.put("entityName", entityName);
|
||||
reqmap.put("actionName", actionName);
|
||||
reqmap.put("createdBy", context.authenticatedUser().getId());
|
||||
reqmap.put("createdByName", context.authenticatedUser().getUsername());
|
||||
reqmap.put("createdByFullName", context.authenticatedUser().getDisplayName());
|
||||
|
||||
reqmap.put("request", myMap);
|
||||
if (result instanceof CommandProcessingResult) {
|
||||
CommandProcessingResult resultCopy = CommandProcessingResult
|
||||
.fromCommandProcessingResult((CommandProcessingResult) result);
|
||||
|
||||
reqmap.put("officeId", resultCopy.getOfficeId());
|
||||
reqmap.put("clientId", resultCopy.getClientId());
|
||||
resultCopy.setOfficeId(null);
|
||||
reqmap.put("response", resultCopy);
|
||||
} else if (result instanceof ErrorInfo) {
|
||||
ErrorInfo ex = (ErrorInfo) result;
|
||||
reqmap.put("status", "Exception");
|
||||
|
||||
Map<String, Object> errorMap = gson.fromJson(ex.getMessage(), type);
|
||||
errorMap.put("errorCode", ex.getErrorCode());
|
||||
errorMap.put("statusCode", ex.getStatusCode());
|
||||
|
||||
reqmap.put("response", errorMap);
|
||||
}
|
||||
|
||||
reqmap.put("timestamp", Instant.now().toString());
|
||||
|
||||
final String serializedResult = this.toApiResultJsonSerializer.serialize(reqmap);
|
||||
|
||||
final HookEvent applicationEvent = new HookEvent(hookEventSource, serializedResult, tenantIdentifier, appUser, authToken);
|
||||
|
||||
applicationContext.publishEvent(applicationEvent);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -44,6 +44,13 @@ public class CommandProcessingResult implements Serializable {
|
||||
private final Long glimId;
|
||||
private Boolean rollbackTransaction;
|
||||
|
||||
public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult) {
|
||||
return new CommandProcessingResult(commandResult.commandId, commandResult.officeId, commandResult.groupId, commandResult.clientId,
|
||||
commandResult.loanId, commandResult.savingsId, commandResult.resourceIdentifier, commandResult.resourceId,
|
||||
commandResult.transactionId, commandResult.changes, commandResult.productId, commandResult.gsimId, commandResult.glimId,
|
||||
commandResult.rollbackTransaction, commandResult.subResourceId);
|
||||
}
|
||||
|
||||
public static CommandProcessingResult fromDetails(final Long commandId, final Long officeId, final Long groupId, final Long clientId,
|
||||
final Long loanId, final Long savingsId, final String resourceIdentifier, final Long entityId, final Long gsimId,
|
||||
final Long glimId, final String transactionId, final Map<String, Object> changes, final Long productId,
|
||||
|
||||
+2
@@ -38,6 +38,8 @@ public final class HookApiConstants {
|
||||
|
||||
public static final String webTemplateName = "Web";
|
||||
|
||||
public static final String elasticSearchTemplateName = "Elastic Search";
|
||||
|
||||
public static final String smsTemplateName = "SMS Bridge";
|
||||
|
||||
public static final String payloadURLName = "Payload URL";
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.hooks.processor;
|
||||
|
||||
import static org.apache.fineract.infrastructure.hooks.api.HookApiConstants.contentTypeName;
|
||||
import static org.apache.fineract.infrastructure.hooks.api.HookApiConstants.payloadURLName;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.hooks.domain.Hook;
|
||||
import org.apache.fineract.infrastructure.hooks.domain.HookConfiguration;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import retrofit2.Callback;
|
||||
|
||||
@Service
|
||||
public class ElasticSearchHookProcessor implements HookProcessor {
|
||||
|
||||
@Autowired
|
||||
private ProcessorHelper processorHelper;
|
||||
|
||||
@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<HookConfiguration> 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, @SuppressWarnings("unused") final String authToken) {
|
||||
|
||||
final String fineractEndpointUrl = System.getProperty("baseUrl");
|
||||
final WebHookService service = processorHelper.createWebHookService(url);
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
final Callback callback = processorHelper.createCallback(url, payload);
|
||||
|
||||
if (contentType.equalsIgnoreCase("json") || contentType.contains("json")) {
|
||||
final JsonObject json = new Gson().fromJson(payload, JsonObject.class);
|
||||
service.sendJsonRequest(entityName, actionName, tenantIdentifier, fineractEndpointUrl, json).enqueue(callback);
|
||||
} else {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map = new Gson().fromJson(payload, map.getClass());
|
||||
service.sendFormRequest(entityName, actionName, tenantIdentifier, fineractEndpointUrl, map).enqueue(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.hooks.processor;
|
||||
|
||||
import static org.apache.fineract.infrastructure.hooks.api.HookApiConstants.elasticSearchTemplateName;
|
||||
import static org.apache.fineract.infrastructure.hooks.api.HookApiConstants.smsTemplateName;
|
||||
import static org.apache.fineract.infrastructure.hooks.api.HookApiConstants.webTemplateName;
|
||||
|
||||
@@ -44,6 +45,8 @@ public class HookProcessorProvider implements ApplicationContextAware {
|
||||
processor = this.applicationContext.getBean("twilioHookProcessor", TwilioHookProcessor.class);
|
||||
} else if (templateName.equals(webTemplateName)) {
|
||||
processor = this.applicationContext.getBean("webHookProcessor", WebHookProcessor.class);
|
||||
} else if (templateName.equals(elasticSearchTemplateName)) {
|
||||
processor = this.applicationContext.getBean("elasticSearchHookProcessor", ElasticSearchHookProcessor.class);
|
||||
} else {
|
||||
processor = null;
|
||||
}
|
||||
|
||||
+19
@@ -34,6 +34,7 @@ import org.springframework.stereotype.Service;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
@Service
|
||||
public final class ProcessorHelper {
|
||||
@@ -116,7 +117,25 @@ public final class ProcessorHelper {
|
||||
final Retrofit.Builder retrofitBuilder = new Retrofit.Builder();
|
||||
retrofitBuilder.baseUrl(url);
|
||||
retrofitBuilder.client(client);
|
||||
retrofitBuilder.addConverterFactory(GsonConverterFactory.create());
|
||||
final Retrofit retrofit = retrofitBuilder.build();
|
||||
return retrofit.create(WebHookService.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public Callback createCallback(final String url, String payload) {
|
||||
|
||||
return new Callback() {
|
||||
|
||||
@Override
|
||||
public void onResponse(@SuppressWarnings("unused") Call call, retrofit2.Response response) {
|
||||
LOG.info("URL: {} - Status: {}", url, response.code());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@SuppressWarnings("unused") Call call, Throwable t) {
|
||||
LOG.error("URL: {} - Retrofit failure occured", url, t);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -39,6 +39,7 @@ import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
@@ -400,6 +401,17 @@ public class AppUser extends AbstractPersistableCustom implements PlatformUser {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
if (this.staff != null && StringUtils.isNotBlank(this.staff.displayName())) {
|
||||
return this.staff.displayName();
|
||||
}
|
||||
String firstName = StringUtils.isNotBlank(this.firstname) ? this.firstname : "";
|
||||
if (StringUtils.isNotBlank(this.lastname)) {
|
||||
return firstName + " " + this.lastname;
|
||||
}
|
||||
return firstName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return this.accountNonExpired;
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
--
|
||||
-- Licensed to the Apache Software Foundation (ASF) under one
|
||||
-- or more contributor license agreements. See the NOTICE file
|
||||
-- distributed with this work for additional information
|
||||
-- regarding copyright ownership. The ASF licenses this file
|
||||
-- to you under the Apache License, Version 2.0 (the
|
||||
-- "License"); you may not use this file except in compliance
|
||||
-- with the License. You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing,
|
||||
-- software distributed under the License is distributed on an
|
||||
-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
-- KIND, either express or implied. See the License for the
|
||||
-- specific language governing permissions and limitations
|
||||
-- under the License.
|
||||
--
|
||||
|
||||
|
||||
INSERT INTO m_hook_templates
|
||||
( name)
|
||||
VALUES('Elastic Search');
|
||||
|
||||
INSERT INTO m_hook_schema
|
||||
(hook_template_id, field_type, field_name, placeholder, optional)
|
||||
VALUES( (select id from m_hook_templates mht where name = 'Elastic Search'), 'string', 'Payload URL', 'http://<host>/<index name>/<type name>', 0);
|
||||
INSERT INTO m_hook_schema
|
||||
( hook_template_id, field_type, field_name, placeholder, optional)
|
||||
VALUES( (select id from m_hook_templates mht where name = 'Elastic Search'), 'string', 'Content Type', 'json', 0);
|
||||
INSERT INTO m_hook_schema
|
||||
( hook_template_id, field_type, field_name, placeholder, optional)
|
||||
VALUES( (select id from m_hook_templates mht where name = 'Elastic Search'), 'string', 'Index Name', NULL, 1);
|
||||
Reference in New Issue
Block a user