org.finra.herd.service.helper.DefaultNotificationMessageBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.finra.herd.service.helper.DefaultNotificationMessageBuilder.java

Source

/*
* Copyright 2015 herd contributors
*
* Licensed 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.finra.herd.service.helper;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;

import org.finra.herd.core.HerdDateUtils;
import org.finra.herd.core.helper.ConfigurationHelper;
import org.finra.herd.dao.helper.HerdDaoSecurityHelper;
import org.finra.herd.dao.helper.JavaPropertiesHelper;
import org.finra.herd.model.api.xml.BusinessObjectDataKey;
import org.finra.herd.model.api.xml.MessageHeaderDefinition;
import org.finra.herd.model.api.xml.NotificationMessageDefinition;
import org.finra.herd.model.api.xml.NotificationMessageDefinitions;
import org.finra.herd.model.dto.ConfigurationValue;
import org.finra.herd.model.dto.MessageHeader;
import org.finra.herd.model.dto.NotificationMessage;
import org.finra.herd.model.jpa.BusinessObjectDataAttributeDefinitionEntity;
import org.finra.herd.model.jpa.BusinessObjectDataAttributeEntity;
import org.finra.herd.model.jpa.BusinessObjectDataEntity;
import org.finra.herd.model.jpa.MessageTypeEntity;

/**
 * Default implementation of the builder for notification messages. Constructs an ESB message based on given data. To use a different implementation overwrite
 * the bean defined in ServiceSpringModuleConfig.sqsMessageBuilder().
 */
@Component
public class DefaultNotificationMessageBuilder implements NotificationMessageBuilder {
    @Autowired
    private BusinessObjectDataDaoHelper businessObjectDataDaoHelper;

    @Autowired
    private BusinessObjectFormatHelper businessObjectFormatHelper;

    @Autowired
    private ConfigurationDaoHelper configurationDaoHelper;

    @Autowired
    private ConfigurationHelper configurationHelper;

    private DocumentBuilder documentBuilder;

    @Autowired
    private HerdDaoSecurityHelper herdDaoSecurityHelper;

    @Autowired
    private JavaPropertiesHelper javaPropertiesHelper;

    @Autowired
    private VelocityHelper velocityHelper;

    private XPath xpath;

    public DefaultNotificationMessageBuilder() throws ParserConfigurationException {
        documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        xpath = XPathFactory.newInstance().newXPath();
    }

    @Override
    public List<NotificationMessage> buildBusinessObjectDataStatusChangeMessages(
            BusinessObjectDataKey businessObjectDataKey, String newBusinessObjectDataStatus,
            String oldBusinessObjectDataStatus) {
        // Create a result list.
        List<NotificationMessage> notificationMessages = new ArrayList<>();

        // Get notification message definitions.
        NotificationMessageDefinitions notificationMessageDefinitions = configurationDaoHelper
                .getXmlClobPropertyAndUnmarshallToObject(NotificationMessageDefinitions.class,
                        ConfigurationValue.HERD_NOTIFICATION_BUSINESS_OBJECT_DATA_STATUS_CHANGE_MESSAGE_DEFINITIONS
                                .getKey());

        // Continue processing if notification message definitions are configured.
        if (notificationMessageDefinitions != null
                && CollectionUtils.isNotEmpty(notificationMessageDefinitions.getNotificationMessageDefinitions())) {
            // Create a context map of values that can be used when building the message.
            Map<String, Object> velocityContextMap = getVelocityContextMap(businessObjectDataKey,
                    newBusinessObjectDataStatus, oldBusinessObjectDataStatus);

            // Generate notification message for each notification message definition.
            for (NotificationMessageDefinition notificationMessageDefinition : notificationMessageDefinitions
                    .getNotificationMessageDefinitions()) {
                // Validate the notification message type.
                if (StringUtils.isBlank(notificationMessageDefinition.getMessageType())) {
                    throw new IllegalStateException(String.format(
                            "Notification message type must be specified. Please update \"%s\" configuration entry.",
                            ConfigurationValue.HERD_NOTIFICATION_BUSINESS_OBJECT_DATA_STATUS_CHANGE_MESSAGE_DEFINITIONS
                                    .getKey()));
                }

                // Validate the notification message destination.
                if (StringUtils.isBlank(notificationMessageDefinition.getMessageDestination())) {
                    throw new IllegalStateException(String.format(
                            "Notification message destination must be specified. Please update \"%s\" configuration entry.",
                            ConfigurationValue.HERD_NOTIFICATION_BUSINESS_OBJECT_DATA_STATUS_CHANGE_MESSAGE_DEFINITIONS
                                    .getKey()));
                }

                // Evaluate the template to generate the message text.
                String messageText = evaluateVelocityTemplate(
                        notificationMessageDefinition.getMessageVelocityTemplate(), velocityContextMap,
                        "businessObjectDataStatusChangeEvent");

                // Build a list of optional message headers.
                List<MessageHeader> messageHeaders = new ArrayList<>();
                if (CollectionUtils.isNotEmpty(notificationMessageDefinition.getMessageHeaderDefinitions())) {
                    for (MessageHeaderDefinition messageHeaderDefinition : notificationMessageDefinition
                            .getMessageHeaderDefinitions()) {
                        messageHeaders.add(new MessageHeader(messageHeaderDefinition.getKey(),
                                evaluateVelocityTemplate(messageHeaderDefinition.getValueVelocityTemplate(),
                                        velocityContextMap,
                                        String.format("businessObjectDataStatusChangeEvent_messageHeader_%s",
                                                messageHeaderDefinition.getKey()))));
                    }
                }

                // Create a notification message and add it to the result list.
                notificationMessages.add(new NotificationMessage(notificationMessageDefinition.getMessageType(),
                        notificationMessageDefinition.getMessageDestination(), messageText, messageHeaders));
            }
        }

        // Return the results.
        return notificationMessages;
    }

    @Override
    public NotificationMessage buildSystemMonitorResponse(String systemMonitorRequestPayload) {
        // Get velocity template.
        String velocityTemplate = configurationHelper
                .getProperty(ConfigurationValue.HERD_NOTIFICATION_SQS_SYS_MONITOR_RESPONSE_VELOCITY_TEMPLATE);

        // Continue processing if velocity template is configured.
        if (StringUtils.isNotBlank(velocityTemplate)) {
            // Evaluate the template to generate the message text.
            String messageText = evaluateVelocityTemplate(velocityTemplate,
                    getIncomingMessageValueMap(systemMonitorRequestPayload,
                            ConfigurationValue.HERD_NOTIFICATION_SQS_SYS_MONITOR_REQUEST_XPATH_PROPERTIES),
                    "systemMonitorResponse");

            // Create a new notification message and return it.
            return new NotificationMessage(MessageTypeEntity.MessageEventTypes.SQS.name(), getSqsQueueName(),
                    messageText, null);
        } else {
            return null;
        }
    }

    /**
     * Evaluates a velocity template if one is defined for the specified configuration value. If velocity template is null, null will be returned.
     *
     * @param velocityTemplate the optional velocity template, may be null
     * @param contextMap the optional context map of additional keys and values to place in the velocity context. This can be useful if you have values from an
     * incoming request message you want to make available to velocity to use in the building of the outgoing response message.
     * @param velocityTemplateName the velocity template name used when Velocity logs error messages.
     *
     * @return the evaluated velocity template
     */
    private String evaluateVelocityTemplate(String velocityTemplate, Map<String, Object> contextMap,
            String velocityTemplateName) {
        // Initialize the message text to null which will cause a message to not be sent.
        String messageText = null;

        // Process velocity template if it configured.
        if (StringUtils.isNotBlank(velocityTemplate)) {
            // Create and populate the velocity context with dynamic values. Note that we can't use periods within the context keys since they can't
            // be referenced in the velocity template (i.e. they're used to separate fields with the context object being referenced).
            Map<String, Object> context = new HashMap<>();
            context.put(ConfigurationValue.HERD_ENVIRONMENT.getKey().replace('.', '_'),
                    configurationHelper.getProperty(ConfigurationValue.HERD_ENVIRONMENT));
            context.put(ConfigurationValue.HERD_NOTIFICATION_SQS_ENVIRONMENT.getKey().replace('.', '_'),
                    configurationHelper.getProperty(ConfigurationValue.HERD_NOTIFICATION_SQS_ENVIRONMENT));
            context.put("current_time", HerdDateUtils.now().toString());
            context.put("uuid", UUID.randomUUID().toString());
            context.put("username", herdDaoSecurityHelper.getCurrentUsername());
            context.put("StringUtils", StringUtils.class);
            context.put("CollectionUtils", CollectionUtils.class);
            context.put("Collections", Collections.class);

            // Populate the context map entries into the velocity context.
            for (Map.Entry<String, Object> mapEntry : contextMap.entrySet()) {
                context.put(mapEntry.getKey(), mapEntry.getValue());
            }

            messageText = velocityHelper.evaluate(velocityTemplate, context, velocityTemplateName);
        }

        // Return the message text.
        return messageText;
    }

    /**
     * Gets an incoming message value map from a message payload.
     *
     * @param payload the incoming message payload.
     * @param configurationValue the configuration value for the XPath expression properties.
     *
     * @return the incoming message value map.
     */
    private Map<String, Object> getIncomingMessageValueMap(String payload, ConfigurationValue configurationValue) {
        // This method is generic and could be placed in a generic helper, but since it's use is limited to only getting values from an incoming message
        // to produce an outgoing message, it is fine in this class.

        Properties xpathProperties;
        try {
            String xpathPropertiesString = configurationHelper.getProperty(configurationValue);
            xpathProperties = javaPropertiesHelper.getProperties(xpathPropertiesString);
        } catch (Exception e) {
            throw new IllegalStateException("Unable to load XPath properties from configuration with key '"
                    + configurationValue.getKey() + "'", e);
        }

        // Create a map that will house keys that map to values retrieved from the incoming message via the XPath expressions.
        Map<String, Object> incomingMessageValuesMap = new HashMap<>();

        // Evaluate all the XPath expressions on the incoming message and store the results in the map.
        // If validation is desired, an XPath expression can be used to verify that the incoming message contains a valid path in the payload.
        // If no XPath expressions are used, then it is assumed that the message is valid and the message will be processed
        // with no incoming values.
        Document document;
        try {
            document = documentBuilder.parse(new InputSource(new StringReader(payload)));
        } catch (Exception e) {
            throw new IllegalArgumentException("Payload is not valid XML:\n" + payload, e);
        }

        for (String key : xpathProperties.stringPropertyNames()) {
            // Get the XPath expression.
            String xpathExpression = xpathProperties.getProperty(key);
            try {
                // Evaluate the expression and store the result in the map keyed by the XPath expression key.
                // A document is required here as opposed to an input source since an input source yields a bug when the input XML contains a namespace.
                incomingMessageValuesMap.put(key, xpath.evaluate(xpathExpression, document));
            } catch (Exception ex) {
                // If any XPath expressions couldn't be evaluated against the incoming payload, throw an exception.
                // If the caller is the incoming JMS processing logic, it will log a debug message because it doesn't know which message it is
                // processing. If this exception is thrown, it assumes the incoming message isn't the one it is processing and moves on to the next message
                // processing routine for a different incoming message.
                // If the XPath expression configured is incorrect (i.e. an internal server error), then the incoming JMS processing logic will eventually
                // find no successful handler and will log an error which can be looked into further. That debug message would then be useful.
                throw new IllegalStateException("XPath expression \"" + xpathExpression
                        + "\" could not be evaluated against payload \"" + payload + "\".", ex);
            }
        }

        return incomingMessageValuesMap;
    }

    /**
     * Returns the SQS queue name. Throws {@link IllegalStateException} if SQS queue name is undefined.
     *
     * @return the sqs queue name
     */
    private String getSqsQueueName() {
        String sqsQueueName = configurationHelper
                .getProperty(ConfigurationValue.HERD_NOTIFICATION_SQS_OUTGOING_QUEUE_NAME);

        if (StringUtils.isBlank(sqsQueueName)) {
            throw new IllegalStateException(
                    String.format("SQS queue name not found. Ensure the \"%s\" configuration entry is configured.",
                            ConfigurationValue.HERD_NOTIFICATION_SQS_OUTGOING_QUEUE_NAME.getKey()));
        }

        return sqsQueueName;
    }

    /**
     * Returns Velocity context map of additional keys and values to place in the velocity context
     *
     * @param businessObjectDataKey the business object data key for the object whose status changed
     * @param newBusinessObjectDataStatus the new business object data status
     * @param oldBusinessObjectDataStatus the old business object data status
     *
     * @return the Velocity context map
     */
    private Map<String, Object> getVelocityContextMap(BusinessObjectDataKey businessObjectDataKey,
            String newBusinessObjectDataStatus, String oldBusinessObjectDataStatus) {
        // Create a context map of values that can be used when building the message.
        Map<String, Object> velocityContextMap = new HashMap<>();
        velocityContextMap.put("businessObjectDataKey", businessObjectDataKey);
        velocityContextMap.put("newBusinessObjectDataStatus", newBusinessObjectDataStatus);
        velocityContextMap.put("oldBusinessObjectDataStatus", oldBusinessObjectDataStatus);

        // Retrieve business object data entity and business object data id to the context.
        BusinessObjectDataEntity businessObjectDataEntity = businessObjectDataDaoHelper
                .getBusinessObjectDataEntity(businessObjectDataKey);
        velocityContextMap.put("businessObjectDataId", businessObjectDataEntity.getId());

        // Load all attribute definitions for this business object data in a map for easy access.
        Map<String, BusinessObjectDataAttributeDefinitionEntity> attributeDefinitionEntityMap = businessObjectFormatHelper
                .getAttributeDefinitionEntities(businessObjectDataEntity.getBusinessObjectFormat());

        // Build an ordered map of business object data attributes that are flagged to be published in notification messages.
        Map<String, String> businessObjectDataAttributes = new LinkedHashMap<>();
        if (!attributeDefinitionEntityMap.isEmpty()) {
            for (BusinessObjectDataAttributeEntity attributeEntity : businessObjectDataEntity.getAttributes()) {
                if (attributeDefinitionEntityMap.containsKey(attributeEntity.getName().toUpperCase())) {
                    BusinessObjectDataAttributeDefinitionEntity attributeDefinitionEntity = attributeDefinitionEntityMap
                            .get(attributeEntity.getName().toUpperCase());

                    if (BooleanUtils.isTrue(attributeDefinitionEntity.getPublish())) {
                        businessObjectDataAttributes.put(attributeEntity.getName(), attributeEntity.getValue());
                    }
                }
            }
        }

        // Add the map of business object data attributes to the context.
        velocityContextMap.put("businessObjectDataAttributes", businessObjectDataAttributes);

        return velocityContextMap;
    }
}