FINERACT-1241-elastic-web-hook-update (#1488)

This commit is contained in:
Manoj
2020-11-10 03:44:27 +05:30
committed by GitHub
parent ef3e2e3c92
commit 169b01d72b
9 changed files with 245 additions and 14 deletions
+2 -1
View File
@@ -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'
@@ -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);
}
}
}
@@ -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,
@@ -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";
@@ -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);
}
}
}
@@ -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;
}
@@ -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);
}
};
}
}
@@ -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;
@@ -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);