| /** |
| * 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.service; |
| |
| import com.google.gson.JsonArray; |
| import com.google.gson.JsonObject; |
| |
| import org.apache.fineract.infrastructure.core.api.JsonCommand; |
| import org.apache.fineract.infrastructure.core.data.ApiParameterError; |
| import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; |
| import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; |
| import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; |
| import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; |
| import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; |
| import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; |
| import org.apache.fineract.infrastructure.hooks.domain.*; |
| import org.apache.fineract.infrastructure.hooks.exception.HookNotFoundException; |
| import org.apache.fineract.infrastructure.hooks.exception.HookTemplateNotFoundException; |
| import org.apache.fineract.infrastructure.hooks.processor.ProcessorHelper; |
| import org.apache.fineract.infrastructure.hooks.processor.WebHookService; |
| import org.apache.fineract.infrastructure.hooks.serialization.HookCommandFromApiJsonDeserializer; |
| import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; |
| import org.apache.fineract.template.domain.Template; |
| import org.apache.fineract.template.domain.TemplateRepository; |
| import org.apache.fineract.template.exception.TemplateNotFoundException; |
| 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 java.util.*; |
| import java.util.Map.Entry; |
| |
| import static org.apache.fineract.infrastructure.hooks.api.HookApiConstants.*; |
| |
| @Service |
| public class HookWritePlatformServiceJpaRepositoryImpl |
| implements |
| HookWritePlatformService { |
| |
| private final PlatformSecurityContext context; |
| private final HookRepository hookRepository; |
| private final HookTemplateRepository hookTemplateRepository; |
| private final TemplateRepository ugdTemplateRepository; |
| private final HookCommandFromApiJsonDeserializer fromApiJsonDeserializer; |
| private final FromJsonHelper fromApiJsonHelper; |
| |
| @Autowired |
| public HookWritePlatformServiceJpaRepositoryImpl( |
| final PlatformSecurityContext context, |
| final HookRepository hookRepository, |
| final HookTemplateRepository hookTemplateRepository, |
| final TemplateRepository ugdTemplateRepository, |
| final HookCommandFromApiJsonDeserializer fromApiJsonDeserializer, |
| final FromJsonHelper fromApiJsonHelper) { |
| this.context = context; |
| this.hookRepository = hookRepository; |
| this.hookTemplateRepository = hookTemplateRepository; |
| this.ugdTemplateRepository = ugdTemplateRepository; |
| this.fromApiJsonDeserializer = fromApiJsonDeserializer; |
| this.fromApiJsonHelper = fromApiJsonHelper; |
| } |
| |
| @Transactional |
| @Override |
| @CacheEvict(value = "hooks", allEntries = true) |
| 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<HookConfiguration> config = assembleConfig( |
| command.mapValueOfParameterNamed(configJson), template); |
| final JsonArray events = command |
| .arrayOfParameterNamed(eventsParamName); |
| final Set<HookResource> allEvents = assembleSetOfEvents(events); |
| Template ugdTemplate = null; |
| if (command.hasParameter(templateIdParamName)) { |
| final Long ugdTemplateId = command |
| .longValueOfParameterNamed(templateIdParamName); |
| ugdTemplate = this.ugdTemplateRepository.findOne(ugdTemplateId); |
| if (ugdTemplate == null) { |
| throw new TemplateNotFoundException(ugdTemplateId); |
| } |
| } |
| final Hook hook = Hook.fromJson(command, template, config, |
| allEvents, ugdTemplate); |
| |
| validateHookRules(template, config, allEvents); |
| |
| 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 |
| @CacheEvict(value = "hooks", allEntries = true) |
| 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<String, Object> changes = hook.update(command); |
| |
| if (!changes.isEmpty()) { |
| |
| if (changes.containsKey(templateIdParamName)) { |
| final Long ugdTemplateId = command |
| .longValueOfParameterNamed(templateIdParamName); |
| final Template ugdTemplate = this.ugdTemplateRepository |
| .findOne(ugdTemplateId); |
| if (ugdTemplate == null) { |
| changes.remove(templateIdParamName); |
| throw new TemplateNotFoundException(ugdTemplateId); |
| } |
| hook.updateUgdTemplate(ugdTemplate); |
| } |
| |
| if (changes.containsKey(eventsParamName)) { |
| final Set<HookResource> 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<HookConfiguration> 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 |
| @CacheEvict(value = "hooks", allEntries = true) |
| 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); |
| } |
| 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<HookConfiguration> assembleConfig( |
| final Map<String, String> hookConfig, final HookTemplate template) { |
| |
| final Set<HookConfiguration> configuration = new HashSet<>(); |
| final Set<Schema> fields = template.getSchema(); |
| |
| for (final Entry<String, String> configEntry : hookConfig.entrySet()) { |
| for (final Schema field : fields) { |
| final String fieldName = field.getFieldName(); |
| if (fieldName.equalsIgnoreCase(configEntry.getKey())) { |
| |
| final HookConfiguration config = HookConfiguration |
| .createNewWithoutHook(field.getFieldType(), |
| configEntry.getKey(), |
| configEntry.getValue()); |
| configuration.add(config); |
| break; |
| } |
| } |
| |
| } |
| |
| return configuration; |
| } |
| |
| private Set<HookResource> assembleSetOfEvents(final JsonArray eventsArray) { |
| |
| final Set<HookResource> 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<HookConfiguration> config, Set<HookResource> events) { |
| |
| final List<ApiParameterError> 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); |
| } |
| |
| 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<Schema> 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." |
| + "not.provided"; |
| baseDataValidator |
| .reset() |
| .value(field.getFieldName()) |
| .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()); |
| } |
| } |