/*
* Copyright 2002-2017 the original author or authors.
*
* 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.springframework.integration.aggregator;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.locks.Lock;
import org.aopalliance.aop.Advice;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.Lifecycle;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.handler.AbstractMessageProducingHandler;
import org.springframework.integration.handler.DiscardingMessageHandler;
import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.store.MessageGroupStore;
import org.springframework.integration.store.MessageStore;
import org.springframework.integration.store.SimpleMessageGroup;
import org.springframework.integration.store.SimpleMessageStore;
import org.springframework.integration.support.locks.DefaultLockRegistry;
import org.springframework.integration.support.locks.LockRegistry;
import org.springframework.integration.util.UUIDConverter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Abstract Message handler that holds a buffer of correlated messages in a
* {@link MessageStore}. This class takes care of correlated groups of messages
* that can be completed in batches. It is useful for custom implementation of
* MessageHandlers that require correlation and is used as a base class for Aggregator -
* {@link AggregatingMessageHandler} and Resequencer - {@link ResequencingMessageHandler},
* or custom implementations requiring correlation.
* <p>
* To customize this handler inject {@link CorrelationStrategy},
* {@link ReleaseStrategy}, and {@link MessageGroupProcessor} implementations as
* you require.
* <p>
* By default the {@link CorrelationStrategy} will be a
* {@link HeaderAttributeCorrelationStrategy} and the {@link ReleaseStrategy} will be a
* {@link SequenceSizeReleaseStrategy}.
*
* @author Iwein Fuld
* @author Dave Syer
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Artem Bilan
* @author David Liu
* @author Enrique Rodriguez
* @since 2.0
*/
public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageProducingHandler
implements DiscardingMessageHandler, DisposableBean, ApplicationEventPublisherAware, Lifecycle {
protected final Log logger = LogFactory.getLog(getClass());
private final Comparator<Message<?>> sequenceNumberComparator = new SequenceNumberComparator();
private final Map<UUID, ScheduledFuture<?>> expireGroupScheduledFutures = new HashMap<>();
private MessageGroupProcessor outputProcessor;
private volatile MessageGroupStore messageStore;
private volatile CorrelationStrategy correlationStrategy;
private volatile ReleaseStrategy releaseStrategy;
private volatile boolean releaseStrategySet;
private volatile MessageChannel discardChannel;
private volatile String discardChannelName;
private boolean sendPartialResultOnExpiry = false;
private volatile boolean sequenceAware = false;
private volatile LockRegistry lockRegistry = new DefaultLockRegistry();
private boolean lockRegistrySet = false;
private volatile long minimumTimeoutForEmptyGroups;
private volatile boolean releasePartialSequences;
private volatile Expression groupTimeoutExpression;
private volatile List<Advice> forceReleaseAdviceChain;
private MessageGroupProcessor forceReleaseProcessor = new ForceReleaseMessageGroupProcessor();
private EvaluationContext evaluationContext;
private volatile ApplicationEventPublisher applicationEventPublisher;
private volatile boolean expireGroupsUponTimeout = true;
private volatile boolean running;
public AbstractCorrelatingMessageHandler(MessageGroupProcessor processor, MessageGroupStore store,
CorrelationStrategy correlationStrategy, ReleaseStrategy releaseStrategy) {
Assert.notNull(processor, "'processor' must not be null");
Assert.notNull(store, "'store' must not be null");
setMessageStore(store);
this.outputProcessor = processor;
this.correlationStrategy = (correlationStrategy == null
? new HeaderAttributeCorrelationStrategy(IntegrationMessageHeaderAccessor.CORRELATION_ID)
: correlationStrategy);
this.releaseStrategy = releaseStrategy == null ? new SimpleSequenceSizeReleaseStrategy() : releaseStrategy;
this.sequenceAware = this.releaseStrategy instanceof SequenceSizeReleaseStrategy;
}
public AbstractCorrelatingMessageHandler(MessageGroupProcessor processor, MessageGroupStore store) {
this(processor, store, null, null);
}
public AbstractCorrelatingMessageHandler(MessageGroupProcessor processor) {
this(processor, new SimpleMessageStore(0), null, null);
}
public void setLockRegistry(LockRegistry lockRegistry) {
Assert.isTrue(!this.lockRegistrySet, "'this.lockRegistry' can not be reset once its been set");
Assert.notNull(lockRegistry, "'lockRegistry' must not be null");
this.lockRegistry = lockRegistry;
this.lockRegistrySet = true;
}
public final void setMessageStore(MessageGroupStore store) {
this.messageStore = store;
store.registerMessageGroupExpiryCallback(
(messageGroupStore, group) -> this.forceReleaseProcessor.processMessageGroup(group));
}
public void setCorrelationStrategy(CorrelationStrategy correlationStrategy) {
Assert.notNull(correlationStrategy, "'correlationStrategy' must not be null");
this.correlationStrategy = correlationStrategy;
}
public void setReleaseStrategy(ReleaseStrategy releaseStrategy) {
Assert.notNull(releaseStrategy, "'releaseStrategy' must not be null");
this.releaseStrategy = releaseStrategy;
this.sequenceAware = this.releaseStrategy instanceof SequenceSizeReleaseStrategy;
this.releaseStrategySet = true;
}
public void setGroupTimeoutExpression(Expression groupTimeoutExpression) {
this.groupTimeoutExpression = groupTimeoutExpression;
}
public void setForceReleaseAdviceChain(List<Advice> forceReleaseAdviceChain) {
Assert.notNull(forceReleaseAdviceChain, "forceReleaseAdviceChain must not be null");
this.forceReleaseAdviceChain = forceReleaseAdviceChain;
}
/**
* Specify a {@link MessageGroupProcessor} for the output function.
* @param outputProcessor the {@link MessageGroupProcessor} to use
* @since 5.0
*/
public void setOutputProcessor(MessageGroupProcessor outputProcessor) {
Assert.notNull(outputProcessor, "'processor' must not be null");
this.outputProcessor = outputProcessor;
}
@Override
public void setTaskScheduler(TaskScheduler taskScheduler) {
super.setTaskScheduler(taskScheduler);
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
protected void onInit() throws Exception {
super.onInit();
Assert.state(!(this.discardChannelName != null && this.discardChannel != null),
"'discardChannelName' and 'discardChannel' are mutually exclusive.");
BeanFactory beanFactory = this.getBeanFactory();
if (beanFactory != null) {
if (this.outputProcessor instanceof BeanFactoryAware) {
((BeanFactoryAware) this.outputProcessor).setBeanFactory(beanFactory);
}
if (this.correlationStrategy instanceof BeanFactoryAware) {
((BeanFactoryAware) this.correlationStrategy).setBeanFactory(beanFactory);
}
if (this.releaseStrategy instanceof BeanFactoryAware) {
((BeanFactoryAware) this.releaseStrategy).setBeanFactory(beanFactory);
}
}
if (this.discardChannel == null) {
this.discardChannel = new NullChannel();
}
if (this.releasePartialSequences) {
Assert.isInstanceOf(SequenceSizeReleaseStrategy.class, this.releaseStrategy,
"Release strategy of type [" + this.releaseStrategy.getClass().getSimpleName() +
"] cannot release partial sequences. Use a SequenceSizeReleaseStrategy instead.");
((SequenceSizeReleaseStrategy) this.releaseStrategy)
.setReleasePartialSequences(this.releasePartialSequences);
}
if (this.evaluationContext == null) {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
}
if (this.sequenceAware) {
this.logger.warn("Using a SequenceSizeReleaseStrategy with large groups may not perform well, consider "
+ "using a SimpleSequenceSizeReleaseStrategy");
}
/*
* Disallow any further changes to the lock registry
* (checked in the setter).
*/
this.lockRegistrySet = true;
this.forceReleaseProcessor = createGroupTimeoutProcessor();
}
private MessageGroupProcessor createGroupTimeoutProcessor() {
MessageGroupProcessor processor = new ForceReleaseMessageGroupProcessor();
if (this.groupTimeoutExpression != null && !CollectionUtils.isEmpty(this.forceReleaseAdviceChain)) {
ProxyFactory proxyFactory = new ProxyFactory(processor);
this.forceReleaseAdviceChain.forEach(proxyFactory::addAdvice);
return (MessageGroupProcessor) proxyFactory.getProxy(getApplicationContext().getClassLoader());
}
return processor;
}
public void setDiscardChannel(MessageChannel discardChannel) {
Assert.notNull(discardChannel, "'discardChannel' cannot be null");
this.discardChannel = discardChannel;
}
public void setDiscardChannelName(String discardChannelName) {
Assert.hasText(discardChannelName, "'discardChannelName' must not be empty");
this.discardChannelName = discardChannelName;
}
public void setSendPartialResultOnExpiry(boolean sendPartialResultOnExpiry) {
this.sendPartialResultOnExpiry = sendPartialResultOnExpiry;
}
/**
* By default, when a MessageGroupStoreReaper is configured to expire partial
* groups, empty groups are also removed. Empty groups exist after a group
* is released normally. This is to enable the detection and discarding of
* late-arriving messages. If you wish to expire empty groups on a longer
* schedule than expiring partial groups, set this property. Empty groups will
* then not be removed from the MessageStore until they have not been modified
* for at least this number of milliseconds.
* @param minimumTimeoutForEmptyGroups The minimum timeout.
*/
public void setMinimumTimeoutForEmptyGroups(long minimumTimeoutForEmptyGroups) {
this.minimumTimeoutForEmptyGroups = minimumTimeoutForEmptyGroups;
}
/**
* Set {@code releasePartialSequences} on an underlying default
* {@link SequenceSizeReleaseStrategy}. Ignored for other release strategies.
* @param releasePartialSequences true to allow release.
*/
public void setReleasePartialSequences(boolean releasePartialSequences) {
if (!this.releaseStrategySet && releasePartialSequences) {
setReleaseStrategy(new SequenceSizeReleaseStrategy());
}
this.releasePartialSequences = releasePartialSequences;
}
/**
* Expire (completely remove) a group if it is completed due to timeout.
* Default true
* @param expireGroupsUponTimeout the expireGroupsUponTimeout to set
* @since 4.1
*/
public void setExpireGroupsUponTimeout(boolean expireGroupsUponTimeout) {
this.expireGroupsUponTimeout = expireGroupsUponTimeout;
}
@Override
public String getComponentType() {
return "aggregator";
}
public MessageGroupStore getMessageStore() {
return this.messageStore;
}
protected Map<UUID, ScheduledFuture<?>> getExpireGroupScheduledFutures() {
return this.expireGroupScheduledFutures;
}
protected MessageGroupProcessor getOutputProcessor() {
return this.outputProcessor;
}
protected CorrelationStrategy getCorrelationStrategy() {
return this.correlationStrategy;
}
protected ReleaseStrategy getReleaseStrategy() {
return this.releaseStrategy;
}
@Override
public MessageChannel getDiscardChannel() {
if (this.discardChannelName != null) {
synchronized (this) {
if (this.discardChannelName != null) {
this.discardChannel = getChannelResolver().resolveDestination(this.discardChannelName);
this.discardChannelName = null;
}
}
}
return this.discardChannel;
}
protected String getDiscardChannelName() {
return this.discardChannelName;
}
protected boolean isSendPartialResultOnExpiry() {
return this.sendPartialResultOnExpiry;
}
protected boolean isSequenceAware() {
return this.sequenceAware;
}
protected LockRegistry getLockRegistry() {
return this.lockRegistry;
}
protected boolean isLockRegistrySet() {
return this.lockRegistrySet;
}
protected long getMinimumTimeoutForEmptyGroups() {
return this.minimumTimeoutForEmptyGroups;
}
protected boolean isReleasePartialSequences() {
return this.releasePartialSequences;
}
protected Expression getGroupTimeoutExpression() {
return this.groupTimeoutExpression;
}
protected EvaluationContext getEvaluationContext() {
return this.evaluationContext;
}
@Override
protected void handleMessageInternal(Message<?> message) throws Exception {
Object correlationKey = this.correlationStrategy.getCorrelationKey(message);
Assert.state(correlationKey != null, "Null correlation not allowed. Maybe the CorrelationStrategy is failing?");
if (this.logger.isDebugEnabled()) {
this.logger.debug("Handling message with correlationKey [" + correlationKey + "]: " + message);
}
UUID groupIdUuid = UUIDConverter.getUUID(correlationKey);
Lock lock = this.lockRegistry.obtain(groupIdUuid.toString());
lock.lockInterruptibly();
try {
ScheduledFuture<?> scheduledFuture = this.expireGroupScheduledFutures.remove(groupIdUuid);
if (scheduledFuture != null) {
boolean canceled = scheduledFuture.cancel(true);
if (canceled && this.logger.isDebugEnabled()) {
this.logger.debug("Cancel 'ScheduledFuture' for MessageGroup with Correlation Key [ "
+ correlationKey + "].");
}
}
MessageGroup messageGroup = this.messageStore.getMessageGroup(correlationKey);
if (this.sequenceAware) {
messageGroup = new SequenceAwareMessageGroup(messageGroup);
}
if (!messageGroup.isComplete() && messageGroup.canAdd(message)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Adding message to group [ " + messageGroup + "]");
}
messageGroup = this.store(correlationKey, message);
if (this.releaseStrategy.canRelease(messageGroup)) {
Collection<Message<?>> completedMessages = null;
try {
completedMessages = this.completeGroup(message, correlationKey, messageGroup);
}
finally {
// Always clean up even if there was an exception
// processing messages
this.afterRelease(messageGroup, completedMessages);
}
if (!isExpireGroupsUponCompletion() && this.minimumTimeoutForEmptyGroups > 0) {
removeEmptyGroupAfterTimeout(messageGroup, this.minimumTimeoutForEmptyGroups);
}
}
else {
scheduleGroupToForceComplete(messageGroup);
}
}
else {
discardMessage(message);
}
}
finally {
lock.unlock();
}
}
protected boolean isExpireGroupsUponCompletion() {
return false;
}
private void removeEmptyGroupAfterTimeout(MessageGroup messageGroup, long timeout) {
Object groupId = messageGroup.getGroupId();
UUID groupUuid = UUIDConverter.getUUID(groupId);
ScheduledFuture<?> scheduledFuture = getTaskScheduler()
.schedule(() -> {
Lock lock = this.lockRegistry.obtain(groupUuid.toString());
try {
lock.lockInterruptibly();
try {
this.expireGroupScheduledFutures.remove(groupUuid);
/*
* Obtain a fresh state for group from the MessageStore,
* since it could be changed while we have waited for lock.
*/
MessageGroup groupNow = this.messageStore.getMessageGroup(groupUuid);
boolean removeGroup = groupNow.size() == 0 &&
groupNow.getLastModified()
<= (System.currentTimeMillis() - this.minimumTimeoutForEmptyGroups);
if (removeGroup) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Removing empty group: " + groupUuid);
}
this.messageStore.removeMessageGroup(groupId);
}
}
finally {
lock.unlock();
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Thread was interrupted while trying to obtain lock."
+ "Rescheduling empty MessageGroup [ " + groupId + "] for removal.");
}
removeEmptyGroupAfterTimeout(messageGroup, timeout);
}
}, new Date(System.currentTimeMillis() + timeout));
if (this.logger.isDebugEnabled()) {
this.logger.debug("Schedule empty MessageGroup [ " + groupId + "] for removal.");
}
this.expireGroupScheduledFutures.put(groupUuid, scheduledFuture);
}
private void scheduleGroupToForceComplete(MessageGroup messageGroup) {
final Long groupTimeout = obtainGroupTimeout(messageGroup);
/*
* When 'groupTimeout' is evaluated to 'null' we do nothing.
* The 'MessageGroupStoreReaper' can be used to 'forceComplete' message groups.
*/
if (groupTimeout != null && groupTimeout >= 0) {
if (groupTimeout > 0) {
final Object groupId = messageGroup.getGroupId();
ScheduledFuture<?> scheduledFuture = getTaskScheduler()
.schedule(() -> {
try {
processForceRelease(groupId);
}
catch (MessageDeliveryException e) {
if (AbstractCorrelatingMessageHandler.this.logger.isWarnEnabled()) {
AbstractCorrelatingMessageHandler.this.logger.warn("The MessageGroup ["
+ groupId + "] is rescheduled by the reason of:", e);
}
scheduleGroupToForceComplete(groupId);
}
}, new Date(System.currentTimeMillis() + groupTimeout));
if (this.logger.isDebugEnabled()) {
this.logger.debug("Schedule MessageGroup [ " + messageGroup + "] to 'forceComplete'.");
}
this.expireGroupScheduledFutures.put(UUIDConverter.getUUID(groupId), scheduledFuture);
}
else {
this.forceReleaseProcessor.processMessageGroup(messageGroup);
}
}
}
private void scheduleGroupToForceComplete(Object groupId) {
MessageGroup messageGroup = this.messageStore.getMessageGroup(groupId);
scheduleGroupToForceComplete(messageGroup);
}
private void processForceRelease(Object groupId) {
MessageGroup messageGroup = this.messageStore.getMessageGroup(groupId);
this.forceReleaseProcessor.processMessageGroup(messageGroup);
}
private void discardMessage(Message<?> message) {
this.messagingTemplate.send(getDiscardChannel(), message);
}
/**
* Allows you to provide additional logic that needs to be performed after the MessageGroup was released.
* @param group The group.
* @param completedMessages The completed messages.
*/
protected abstract void afterRelease(MessageGroup group, Collection<Message<?>> completedMessages);
/**
* Subclasses may override if special action is needed because the group was released or discarded
* due to a timeout. By default, {@link #afterRelease(MessageGroup, Collection)} is invoked.
* @param group The group.
* @param completedMessages The completed messages.
* @param timeout True if the release/discard was due to a timeout.
*/
protected void afterRelease(MessageGroup group, Collection<Message<?>> completedMessages, boolean timeout) {
afterRelease(group, completedMessages);
}
protected void forceComplete(MessageGroup group) {
Object correlationKey = group.getGroupId();
// UUIDConverter is no-op if already converted
Lock lock = this.lockRegistry.obtain(UUIDConverter.getUUID(correlationKey).toString());
boolean removeGroup = true;
try {
lock.lockInterruptibly();
try {
ScheduledFuture<?> scheduledFuture =
this.expireGroupScheduledFutures.remove(UUIDConverter.getUUID(correlationKey));
if (scheduledFuture != null) {
boolean canceled = scheduledFuture.cancel(false);
if (canceled && this.logger.isDebugEnabled()) {
this.logger.debug("Cancel 'forceComplete' scheduling for MessageGroup [ " + group + "].");
}
}
MessageGroup groupNow = group;
/*
* If the group argument is not already complete,
* re-fetch it because it might have changed while we were waiting on
* its lock. If the last modified timestamp changed, defer the completion
* because the selection condition may have changed such that the group
* would no longer be eligible. If the timestamp changed, it's a completely new
* group and should not be reaped on this cycle.
*
* If the group argument is already complete, do not re-fetch.
* Note: not all message stores provide a direct reference to its internal
* group so the initial 'isComplete()` will only return true for those stores if
* the group was already complete at the time of its selection as a candidate.
*
* If the group is marked complete, only consider it
* for reaping if it's empty (and both timestamps are unaltered).
*/
if (!group.isComplete()) {
groupNow = this.messageStore.getMessageGroup(correlationKey);
}
long lastModifiedNow = groupNow.getLastModified();
int groupSize = groupNow.size();
if ((!groupNow.isComplete() || groupSize == 0)
&& group.getLastModified() == lastModifiedNow
&& group.getTimestamp() == groupNow.getTimestamp()) {
if (groupSize > 0) {
if (this.releaseStrategy.canRelease(groupNow)) {
completeGroup(correlationKey, groupNow);
}
else {
expireGroup(correlationKey, groupNow);
}
if (!this.expireGroupsUponTimeout) {
afterRelease(groupNow, groupNow.getMessages(), true);
removeGroup = false;
}
}
else {
/*
* By default empty groups are removed on the same schedule as non-empty
* groups. A longer timeout for empty groups can be enabled by
* setting minimumTimeoutForEmptyGroups.
*/
removeGroup = lastModifiedNow <= (System.currentTimeMillis() - this.minimumTimeoutForEmptyGroups);
if (removeGroup && this.logger.isDebugEnabled()) {
this.logger.debug("Removing empty group: " + correlationKey);
}
}
}
else {
removeGroup = false;
if (this.logger.isDebugEnabled()) {
this.logger.debug("Group expiry candidate (" + correlationKey +
") has changed - it may be reconsidered for a future expiration");
}
}
}
catch (MessageDeliveryException e) {
removeGroup = false;
if (this.logger.isDebugEnabled()) {
this.logger.debug("Group expiry candidate (" + correlationKey +
") has been affected by MessageDeliveryException - " +
"it may be reconsidered for a future expiration one more time");
}
throw e;
}
finally {
try {
if (removeGroup) {
this.remove(group);
}
}
finally {
lock.unlock();
}
}
}
catch (InterruptedException ie) {
Thread.currentThread().interrupt();
this.logger.debug("Thread was interrupted while trying to obtain lock");
}
}
void remove(MessageGroup group) {
Object correlationKey = group.getGroupId();
this.messageStore.removeMessageGroup(correlationKey);
}
protected int findLastReleasedSequenceNumber(Object groupId, Collection<Message<?>> partialSequence) {
Message<?> lastReleasedMessage = Collections.max(partialSequence, this.sequenceNumberComparator);
return new IntegrationMessageHeaderAccessor(lastReleasedMessage).getSequenceNumber();
}
protected MessageGroup store(Object correlationKey, Message<?> message) {
return this.messageStore.addMessageToGroup(correlationKey, message);
}
protected void expireGroup(Object correlationKey, MessageGroup group) {
if (this.logger.isInfoEnabled()) {
this.logger.info("Expiring MessageGroup with correlationKey[" + correlationKey + "]");
}
if (this.sendPartialResultOnExpiry) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Prematurely releasing partially complete group with key ["
+ correlationKey + "] to: " + getOutputChannel());
}
completeGroup(correlationKey, group);
}
else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Discarding messages of partially complete group with key ["
+ correlationKey + "] to: "
+ (this.discardChannelName != null ? this.discardChannelName : this.discardChannel));
}
for (Message<?> message : group.getMessages()) {
discardMessage(message);
}
}
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(new MessageGroupExpiredEvent(this, correlationKey, group
.size(), new Date(group.getLastModified()), new Date(), !this.sendPartialResultOnExpiry));
}
}
protected void completeGroup(Object correlationKey, MessageGroup group) {
Message<?> first = null;
if (group != null) {
first = group.getOne();
}
completeGroup(first, correlationKey, group);
}
@SuppressWarnings("unchecked")
protected Collection<Message<?>> completeGroup(Message<?> message, Object correlationKey, MessageGroup group) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Completing group with correlationKey [" + correlationKey + "]");
}
Object result = this.outputProcessor.processMessageGroup(group);
Collection<Message<?>> partialSequence = null;
if (result instanceof Collection<?>) {
this.verifyResultCollectionConsistsOfMessages((Collection<?>) result);
partialSequence = (Collection<Message<?>>) result;
}
this.sendOutputs(result, message);
return partialSequence;
}
protected void verifyResultCollectionConsistsOfMessages(Collection<?> elements) {
Class<?> commonElementType = CollectionUtils.findCommonElementType(elements);
Assert.isAssignable(Message.class, commonElementType,
"The expected collection of Messages contains non-Message element: " + commonElementType);
}
protected Long obtainGroupTimeout(MessageGroup group) {
return this.groupTimeoutExpression != null
? this.groupTimeoutExpression.getValue(this.evaluationContext, group, Long.class) : null;
}
@Override
public void destroy() throws Exception {
for (ScheduledFuture<?> future : this.expireGroupScheduledFutures.values()) {
future.cancel(true);
}
}
@Override
public void start() {
if (!this.running) {
this.running = true;
if (this.outputProcessor instanceof Lifecycle) {
((Lifecycle) this.outputProcessor).start();
}
if (this.releaseStrategy instanceof Lifecycle) {
((Lifecycle) this.releaseStrategy).start();
}
}
}
@Override
public void stop() {
if (this.running) {
this.running = false;
if (this.outputProcessor instanceof Lifecycle) {
((Lifecycle) this.outputProcessor).stop();
}
if (this.releaseStrategy instanceof Lifecycle) {
((Lifecycle) this.releaseStrategy).stop();
}
}
}
@Override
public boolean isRunning() {
return this.running;
}
protected static class SequenceAwareMessageGroup extends SimpleMessageGroup {
private final SimpleMessageGroup sourceGroup;
public SequenceAwareMessageGroup(MessageGroup messageGroup) {
/*
* Since this group is temporary, and never added to, we simply use the
* supplied group's message collection for the lookup rather than creating a
* new group.
*/
super(messageGroup.getMessages(), null, messageGroup.getGroupId(), messageGroup.getTimestamp(),
messageGroup.isComplete(), true);
if (messageGroup instanceof SimpleMessageGroup) {
this.sourceGroup = (SimpleMessageGroup) messageGroup;
}
else {
this.sourceGroup = null;
}
}
/**
* This method determines whether messages have been added to this group that
* supersede the given message based on its sequence id. This can be helpful to
* avoid ending up with sequences larger than their required sequence size or
* sequences that are missing certain sequence numbers.
*/
@Override
public boolean canAdd(Message<?> message) {
if (this.size() == 0) {
return true;
}
Integer messageSequenceNumber = message.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER,
Integer.class);
if (messageSequenceNumber != null && messageSequenceNumber > 0) {
Integer messageSequenceSize = message.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE,
Integer.class);
if (messageSequenceSize == null) {
messageSequenceSize = 0;
}
return messageSequenceSize.equals(getSequenceSize())
&& !(this.sourceGroup != null ? this.sourceGroup.containsSequence(messageSequenceNumber)
: containsSequenceNumber(this.getMessages(), messageSequenceNumber));
}
return true;
}
private boolean containsSequenceNumber(Collection<Message<?>> messages, Integer messageSequenceNumber) {
for (Message<?> member : messages) {
if (messageSequenceNumber.equals(member.getHeaders().get(
IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, Integer.class))) {
return true;
}
}
return false;
}
}
private class ForceReleaseMessageGroupProcessor implements MessageGroupProcessor {
ForceReleaseMessageGroupProcessor() {
super();
}
@Override
public Object processMessageGroup(MessageGroup group) {
forceComplete(group);
return null;
}
}
}