Package org.jboss.soa.esb.actions

Source Code of org.jboss.soa.esb.actions.Aggregator

/*
* 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() ;
            }
        }
    }
}
TOP

Related Classes of org.jboss.soa.esb.actions.Aggregator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.