/*
* 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.HashMap;
import java.util.LinkedHashMap;
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.dto.ConfigurationValue;
import org.finra.herd.model.jpa.BusinessObjectDataAttributeDefinitionEntity;
import org.finra.herd.model.jpa.BusinessObjectDataAttributeEntity;
import org.finra.herd.model.jpa.BusinessObjectDataEntity;
/**
* Default implementation of the builder for SQS messages. Constructs an ESB message based on given data. To use a different implementation overwrite the bean
* defined in ServiceSpringModuleConfig.sqsMessageBuilder().
*/
@Component
public class DefaultSqsMessageBuilder implements SqsMessageBuilder
{
@Autowired
private BusinessObjectDataDaoHelper businessObjectDataDaoHelper;
@Autowired
private BusinessObjectFormatHelper businessObjectFormatHelper;
@Autowired
private ConfigurationHelper configurationHelper;
private DocumentBuilder documentBuilder;
@Autowired
private HerdDaoSecurityHelper herdDaoSecurityHelper;
@Autowired
private JavaPropertiesHelper javaPropertiesHelper;
@Autowired
private VelocityHelper velocityHelper;
private XPath xpath;
public DefaultSqsMessageBuilder() throws ParserConfigurationException
{
documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
xpath = XPathFactory.newInstance().newXPath();
}
@Override
public String buildBusinessObjectDataStatusChangeMessage(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);
// Evaluate the template and return the value.
return evaluateVelocityTemplate(ConfigurationValue.HERD_NOTIFICATION_SQS_BUSINESS_OBJECT_DATA_STATUS_CHANGE_VELOCITY_TEMPLATE, velocityContextMap,
"businessObjectDataStatusChangeEvent");
}
@Override
public String buildSystemMonitorResponse(String systemMonitorRequestPayload)
{
return evaluateVelocityTemplate(ConfigurationValue.HERD_NOTIFICATION_SQS_SYS_MONITOR_RESPONSE_VELOCITY_TEMPLATE,
getIncomingMessageValueMap(systemMonitorRequestPayload, ConfigurationValue.HERD_NOTIFICATION_SQS_SYS_MONITOR_REQUEST_XPATH_PROPERTIES),
"systemMonitorResponse");
}
/**
* Evaluates a velocity template if one is defined for the specified configuration value.
*
* @param configurationValue the configuration value of the optional velocity template. If the configuration value returned is null, null will be returned.
* @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(ConfigurationValue configurationValue, Map<String, Object> contextMap, String velocityTemplateName)
{
// Initialize the message text to null which will cause a message to not be sent.
String messageText = null;
// Get the velocity template and only process it if one is configured.
String velocityTemplate = configurationHelper.getProperty(configurationValue);
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_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);
// 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;
}
}