/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
/**
* Routes the Message argument to a fixed list of services ([category,name])
* @author <a href="mailto:schifest@heuristica.com.ar">schifest@heuristica.com.ar</a>
* @since Version 4.0
*/
package org.jboss.soa.esb.actions;
import org.apache.log4j.Logger;
import org.jboss.soa.esb.ConfigurationException;
import org.jboss.soa.esb.addressing.Call;
import org.jboss.soa.esb.addressing.EPR;
import org.jboss.soa.esb.client.ServiceInvoker;
import org.jboss.soa.esb.common.ModulePropertyManager;
import static org.jboss.soa.esb.client.ServiceInvoker.DEAD_LETTER_SERVICE_NAME;
import static org.jboss.soa.esb.client.ServiceInvoker.INTERNAL_SERVICE_CATEGORY;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.listeners.message.MessageDeliverException;
import org.jboss.soa.esb.message.Header;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.message.format.MessageFactory;
import org.jboss.soa.esb.services.registry.RegistryException;
import org.jboss.soa.esb.util.Util;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.text.DateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Simple Aggregator. The aggregator relies on 'aggregatorTags'. To puzzle the individual
* back together. The aggregatorTag is set in the MessageRouter.deliverAsync() method. The aggregator
* adds a collected message in the series as attachements to a new message. When all
* messages are received or if we are timeout an aggregated message is returned.
* In all other cases null is returned.
*
* Future enhancement should be
* - all sort of waitfor/timout algorithms.
* - persisting the map so no messages get lost.
* - make the map managable.
*
* @author kurt.stam@redhat.com
*
*/
public class Aggregator extends AbstractActionPipelineProcessor
{
public final static String AGGEGRATOR_TAG = "aggregatorTag";
public final static String SPLITTER_TIME_STAMP = "splitterTimeStamp";
public final static String AGGREGATOR_ON_PROPERTIES = "org.jboss.soa.esb.aggregator.setOnProperties";
private Map<String, Map< Integer, Message > > aggregatedMessageMap
= new ConcurrentHashMap< String, Map< Integer, Message > >(); // can probably change this to a vanilla Map now
private TimeoutChecker _timeoutChecker=null;
protected ConfigTree config;
private static Logger logger = Logger.getLogger(Aggregator.class);
private Long timeoutInMillis=null;
private Set<String> receivedSplits = new HashSet<String>();
private String splitId;
private boolean aggregatorOnProperties = false; // By default... set on context.
private Aggregator(){}
public Aggregator(ConfigTree config) throws ConfigurationException, RegistryException
{
this.config = config;
String timeoutAttr = config.getAttribute("timeoutInMillies", null);
/*
* http://jira.jboss.com/jira/browse/JBESB-1552
*/
if (timeoutAttr != null)
logger.warn("Aggregator config: timeoutInMillies is deprecated. Use timeoutInMillis in future.");
else
timeoutAttr = config.getAttribute("timeoutInMillis", null);
if (timeoutAttr != null)
{
try
{
timeoutInMillis = Long.valueOf(timeoutAttr);
}
catch (NumberFormatException ex)
{
logger.error("Invalid value for timeoutInMillis: "+timeoutAttr);
throw new ConfigurationException(ex);
}
}
logger.debug("Aggregator config: timeoutInMillis=" + timeoutInMillis);
splitId = config.getAttribute("splitId");
aggregatorOnProperties = Aggregator.aggregatorOnProperties(config);
}
/**
* Initialise the action instance.
* <p/>
* This method is called after the action instance has been instantiated so that
* configuration options can be validated.
*
* @throws ActionLifecycleException for errors during initialisation.
*/
public void initialise()
throws ActionLifecycleException
{
_timeoutChecker = new TimeoutChecker();
_timeoutChecker.start();
}
public Map<String, Map<Integer, Message>> getAggregatedMessageMap() {
return aggregatedMessageMap;
}
/**
* Destroy the action instance.
* <p/>
* This method is called prior to the release of the action instance. All
* resources associated with this action instance should be released as the
* instance will no longer be used.
*/
public void destroy()
throws ActionLifecycleException
{
_timeoutChecker.terminate() ;
_timeoutChecker = null ;
}
/**
* Processes an incoming message, aggregates messages in a set and returns a aggregated message
* when isComplete() is satisfied. The aggregated messages are set as attachments to this message.
* Next the message can be send to a transformer to do the second part of the aggregation which is
* to convert the attachement messages into to one message.
*
* @param message
* @return a aggregated message, or null if the aggregation has not been completed.
* @throws ActionProcessingException
*/
@SuppressWarnings("unchecked")
public Message process(Message message) throws ActionProcessingException
{
List<String> aggregatorTags = getAggregatorTags(message, aggregatorOnProperties);
if (aggregatorTags != null && aggregatorTags.size() > 0) {
String aggregatorTag = aggregatorTags.get(aggregatorTags.size()-1);
AggregationDetails aggrDetails = new AggregationDetails(aggregatorTag);
assertAggregationDetailsOK(aggrDetails);
// Set the timestamp on the message... used by the TimeoutChecker...
message.getProperties().setProperty(SPLITTER_TIME_STAMP, aggrDetails.getSeriesTimestamp());
synchronized (aggregatedMessageMap)
{
Map<Integer, Message> messageMap = aggregatedMessageMap.get(aggrDetails.getSeriesUuid());
if (isTimedOut(aggrDetails.getSeriesTimestamp())) {
if (messageMap != null) {
//add the message in if we don't already have it
if (!messageMap.containsKey(aggrDetails.getMessageNumber())) {
messageMap.put(aggrDetails.getMessageNumber(), message);
}
//Just going to send out what we have this far, which will remove this key
//so subsequent messages with this uuId will be ignored
message = createAggregateMessage(aggrDetails.getSeriesUuid(), messageMap);
} else {
logger.debug("Ignoring this message since we are already timedout on this message.");
//ignoring this message
message = null;
}
} else {
//Get the messageMap with this uuId, or create it if not found
if (messageMap == null) {
messageMap = new ConcurrentHashMap<Integer, Message>();
aggregatedMessageMap.put(aggrDetails.getSeriesUuid(), messageMap);
}
if (messageMap.containsKey(aggrDetails.getMessageNumber())) {
logger.warn("Received duplicate message, ignoring it but this should not happen.");
} else {
messageMap.put(aggrDetails.getMessageNumber(), message);
}
if (messageMap.size() == aggrDetails.getSeriesSize()) {
message = createAggregateMessage(aggrDetails.getSeriesUuid(), messageMap);
} else {
message = null;
}
}
}
} else {
// Just aggregate the single message...
message = createAggregateMessage(message);
}
return message;
}
private void assertAggregationDetailsOK(AggregationDetails aggrDetails) throws ActionProcessingException {
if(splitId != null) {
if(!splitId.equals(aggrDetails.getSplitId())) {
throw new ActionProcessingException("Invalid aggregation config on aggregator '" + config.getAttribute("action") + "' . " +
"This aggregator is configured " +
"to only aggregate message with an aggregation 'spliId' of '" + splitId + "'. " +
"The splitId on the received message is '" + aggrDetails.getSplitId() + "'. " +
"A nested aggregation point may be missing, or may have been bypassed.");
}
} else {
// log a warning if this aggregator starts receiving messages with
// different splitIds. This would suggest an error in the nested split configuration.
if(!receivedSplits.contains(aggrDetails.getSplitId())) {
receivedSplits.add(aggrDetails.getSplitId());
}
if(receivedSplits.size() > 1) {
logger.warn("Aggregator action '" + config.getAttribute("action") + "' has received " +
"split messages from more multiple 'splitId' sources: " + receivedSplits + "\n" +
"You may need to configure an intermediate/nested aggregator at " +
"some point in the message flow.");
}
}
}
/**
* @deprecated Use {@link #getAggregatorTags(Message, boolean)}.
*/
public static List<String> getAggregatorTags(Message message) {
return getAggregatorTags(message, false);
}
public static List<String> getAggregatorTags(Message message, boolean aggregatorOnProperties) {
if(aggregatorOnProperties) {
return (List<String>) message.getProperties().getProperty(Aggregator.AGGEGRATOR_TAG);
} else {
return (List<String>) message.getContext().getContext(Aggregator.AGGEGRATOR_TAG);
}
}
/**
* @deprecated Use {@link #setAggregatorTags(Message, List, boolean)}.
*/
public static void setAggregatorTags(Message message, List<String> tags) {
setAggregatorTags(message, tags, false);
}
public static void setAggregatorTags(Message message, List<String> tags, boolean aggregatorOnProperties) {
if(aggregatorOnProperties) {
if(tags != null) {
message.getProperties().setProperty(Aggregator.AGGEGRATOR_TAG, tags);
} else {
message.getProperties().remove(Aggregator.AGGEGRATOR_TAG);
}
} else {
if(tags != null) {
message.getContext().setContext(Aggregator.AGGEGRATOR_TAG, tags);
} else {
message.getContext().removeContext(Aggregator.AGGEGRATOR_TAG);
}
}
}
/**
* @deprecated Use {@link #getAggregatorDetails(Message, int, boolean)}.
*/
public static AggregationDetails getAggregatorDetails(Message message, int tagIndex) throws ActionProcessingException {
return getAggregatorDetails(message, tagIndex, false);
}
public static AggregationDetails getAggregatorDetails(Message message, int tagIndex, boolean aggregatorOnProperties) throws ActionProcessingException {
List<String> tags = getAggregatorTags(message, aggregatorOnProperties);
if(tags == null || tags.isEmpty()) {
return null;
} else {
return new AggregationDetails(tags.get(tagIndex));
}
}
public static boolean aggregatorOnProperties(ConfigTree configTree) {
String value = configTree.getAttribute(AGGREGATOR_ON_PROPERTIES);
if(value != null) {
return configTree.getBooleanAttribute(AGGREGATOR_ON_PROPERTIES, false);
} else {
value = ModulePropertyManager.getPropertyManager(ModulePropertyManager.TRANSPORTS_MODULE).getProperty(AGGREGATOR_ON_PROPERTIES);
if(value != null) {
try {
return Boolean.parseBoolean(value.trim());
} catch(NumberFormatException e) {
logger.error("Invalid value '" + value + "' for property '" + AGGREGATOR_ON_PROPERTIES + "'. Must be an boolean value. Returning default value 'false'.");
}
}
}
return false;
}
public static void decorate(Message message) {
}
/**
* Aggregates a single message into an aggregated message.
* <p/>
* This method is called for messages that are recived without aggregation tags.
*
* @param message
* @return the aggregated message
*/
@SuppressWarnings("unchecked")
public Message createAggregateMessage(Message message) throws ActionProcessingException {
// Create an aggregated message
Message aggregatedMessage = MessageFactory.getInstance().getMessage();
setAggregatorTags(message, null, aggregatorOnProperties);
try {
aggregatedMessage.getAttachment().addItem(Util.serialize(message));
} catch (ParserConfigurationException e) {
throw new ActionProcessingException("Message attachment serialization failure", e);
} catch (IOException e) {
throw new ActionProcessingException("Message attachment serialization failure", e);
}
return aggregatedMessage;
}
/**
* Aggregates the messages into 1 new message with an attachment for each message.
*
* @param uuId
* @param messageMap
* @return the aggregated message
*/
@SuppressWarnings("unchecked")
public Message createAggregateMessage(String uuId, Map<Integer, Message> messageMap) throws ActionProcessingException {
// Create an aggregated message
Message aggregatedMessage = MessageFactory.getInstance().getMessage();
//Push additional AggregatorTags onto the new message, so we can aggregate in case of nested splits.
//Only need to get it from the first message, should be the same for the others.
List<String> aggregatedMessageTags = copyAggregationTags(messageMap);
setAggregatorTags(aggregatedMessage, aggregatedMessageTags, false);
Collection<Message> attachmentMessages = messageMap.values();
for (Message attachmentMessage : attachmentMessages) {
//Add the individual messages as attachments
try {
// Clear the aggregation tags from the attachment message. Any future aggregation
// on the payload of these messages should be done within the context of the
// outer/aggregated message and it's tags.
setAggregatorTags(attachmentMessage, null, false);
aggregatedMessage.getAttachment().addItem(Util.serialize(attachmentMessage));
} catch (ParserConfigurationException e) {
throw new ActionProcessingException("Message attachment serialization failure", e);
} catch (IOException e) {
throw new ActionProcessingException("Message attachment serialization failure", e);
}
}
// Map the call details from the attached messages...
mapCallDetails(attachmentMessages, aggregatedMessage, uuId);
synchronized (aggregatedMessageMap)
{
aggregatedMessageMap.remove(uuId);
}
//TODO remove messageMap from permanent storage, or do that per message in the loop above using value of the aggregatorTag
//remove from the notificationMap if it is in there.
return aggregatedMessage;
}
protected static void mapCallDetails(Collection<Message> attachmentMessages, Message aggregatedMessage, String uuId) {
Set<EPR> replyToEPRs = new HashSet<EPR>();
int eprCount = 0;
for (Message attachmentMessage : attachmentMessages) {
Header header = attachmentMessage.getHeader();
if(header != null) {
Call call = header.getCall();
if(call != null) {
replyToEPRs.add(call.getReplyTo());
eprCount++;
}
}
}
// Only map the replyTo EPR if all attachments have the same replyTo EPR...
if(replyToEPRs.size() == 1 && eprCount == attachmentMessages.size()) {
Call call = aggregatedMessage.getHeader().getCall();
if(call == null) {
call = new Call();
aggregatedMessage.getHeader().setCall(call);
}
call.setReplyTo(replyToEPRs.iterator().next());
} else if(logger.isDebugEnabled()) {
if(replyToEPRs.size() == 0) {
logger.debug("Not mapping replyTo EPR for aggregate message '" + uuId + "'. No replyTo EPR to be mapped.");
} else if(replyToEPRs.size() > 1) {
logger.debug("Not mapping replyTo EPR for aggregate message '" + uuId + "'. Not all replyTo EPRs are the same: " + replyToEPRs);
} else {
logger.debug("Not mapping replyTo EPR for aggregate message '" + uuId + "'. Not all attachment messages have a replyTo EPR.");
}
}
// TODO: How about mapping faultTo etc...
}
private List<String> copyAggregationTags(Map<Integer, Message> messageMap) {
// Get the tags from the first message...
List<String> nestedAggregationTags = getAggregatorTags(messageMap.values().iterator().next(), aggregatorOnProperties);
if(nestedAggregationTags != null && nestedAggregationTags.size() > 1) {
// clone the tags, just incase they get zapped from the source list...
List<String> aggregatedMessageTags = new ArrayList<String>();
aggregatedMessageTags.addAll(nestedAggregationTags);
// remove the last one because that's related to the nested aggregation...
aggregatedMessageTags.remove(aggregatedMessageTags.size() - 1);
return aggregatedMessageTags;
}
return null;
}
/**
* If the aggregation process is complete then return true. This depends on the configuration.
*
* @param splitterTimeStamp The splitter timestamp.
* @return True if the message is timed out, otherwise false.
*/
private boolean isTimedOut(long splitterTimeStamp)
{
if (timeoutInMillis!=null) {
long now = new Date().getTime();
long expiration = splitterTimeStamp + timeoutInMillis;
if (logger.isDebugEnabled()) {
DateFormat dateFormat = DateFormat.getTimeInstance();
logger.debug("Current time=" + dateFormat.format(new Date(now))
+ " message expiration=" + dateFormat.format(new Date(expiration)));
}
if ( now > expiration) {
logger.debug("message expired.");
return true;
} else {
logger.debug("message is alive.");
}
}
return false;
}
/**
* Checks for message that are timed out. If we find that one, drop it in the DLQ and delete
* it from the map.
*
* @author kstam
*
*/
class TimeoutChecker extends Thread {
private final Lock terminateLock = new ReentrantLock() ;
private final Condition terminateCondition = terminateLock.newCondition() ;
private boolean terminated ;
ServiceInvoker dlQueueInvoker;
@SuppressWarnings("unchecked")
public void run() {
try {
dlQueueInvoker = new ServiceInvoker(INTERNAL_SERVICE_CATEGORY, DEAD_LETTER_SERVICE_NAME);
} catch (MessageDeliverException e) {
logger.error("Unable to initialise Dead Letter Channel Service Invoker for Aggregation timeout checker. Not using Dead Letter Channel.", e);
}
boolean running = true ;
while(running) {
//no need to check if no timeout is set
if (timeoutInMillis!=null) {
// is a snapshot so no lock needed
for (Map<Integer, Message > messageMap : aggregatedMessageMap.values()) {
//Check the first message, they all have the same time stamp
Message message = messageMap.values().iterator().next();
long timeStamp = (Long) message.getProperties().getProperty(SPLITTER_TIME_STAMP);
if (isTimedOut(timeStamp)) {
//We found a timed-out message. Let's go notify ourselves about it by resending a message,
//if we haven't done so already
List<String> aggregatorTag = getAggregatorTags(message, aggregatorOnProperties);
if(aggregatorTag != null && !aggregatorTag.isEmpty()) {
AggregationDetails aggrDetails = new AggregationDetails(aggregatorTag.get(aggregatorTag.size() - 1));
try {
logger.info("Deleting message aggregation series: " + aggrDetails.getSeriesUuid());
if(dlQueueInvoker != null) {
Message aggregateMessage = createAggregateMessage(aggrDetails.getSeriesUuid(), messageMap);
dlQueueInvoker.deliverAsync(aggregateMessage);
}
} catch(Throwable e) {
logger.error("Error delivering timed out aggregation message to Dead Letter Service.", e);
} finally {
synchronized (aggregatedMessageMap)
{
aggregatedMessageMap.remove(aggrDetails.getSeriesUuid());
}
}
}
}
}
}
terminateLock.lock() ;
try {
if (!terminated) {
terminateCondition.await(500, TimeUnit.MILLISECONDS) ;
}
running = !terminated ;
} catch (InterruptedException ie){}
finally {
terminateLock.unlock() ;
}
}
}
public void terminate() {
terminateLock.lock() ;
try
{
terminated = true ;
terminateCondition.signalAll() ;
}
finally
{
terminateLock.unlock() ;
}
}
}
}