diff --git a/.gitignore b/.gitignore index ed33517e7f..742cbcc685 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ si.java.hsp spring-integration-jms/activemq-data/ spring-integration-samples/loanshark/application.log* target -vf.gf.dmn-*.cfg +vf.gf.dmn-* \ No newline at end of file diff --git a/build.gradle b/build.gradle index 060323abdc..f494af88ee 100644 --- a/build.gradle +++ b/build.gradle @@ -126,7 +126,7 @@ configure(javaprojects) { springAmqpVersion = '1.0.0.RELEASE' springDataMongoVersion = '1.0.0.M4' springDataRedisVersion = '1.0.0.M4' - springGemfireVersion = '1.1.0.M2' + springGemfireVersion = '1.1.0.M3' springSecurityVersion = '3.0.6.RELEASE' springWsVersion = '2.0.2.RELEASE' diff --git a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractAggregatingMessageGroupProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractAggregatingMessageGroupProcessor.java index 2937f065c5..16bb1afe11 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractAggregatingMessageGroupProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractAggregatingMessageGroupProcessor.java @@ -41,6 +41,7 @@ public abstract class AbstractAggregatingMessageGroupProcessor implements Messag public final Object processMessageGroup(MessageGroup group) { Assert.notNull(group, "MessageGroup must not be null"); + Map headers = this.aggregateHeaders(group); Object payload = this.aggregatePayloads(group, headers); MessageBuilder builder; @@ -50,6 +51,7 @@ public abstract class AbstractAggregatingMessageGroupProcessor implements Messag else { builder = MessageBuilder.withPayload(payload).copyHeadersIfAbsent(headers); } + return builder.popSequenceDetails().build(); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/CorrelatingMessageHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java similarity index 70% rename from spring-integration-core/src/main/java/org/springframework/integration/aggregator/CorrelatingMessageHandler.java rename to spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java index 7b6393c0c1..2b10a70739 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/CorrelatingMessageHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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 @@ -13,7 +13,10 @@ package org.springframework.integration.aggregator; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -34,34 +37,36 @@ import org.springframework.integration.store.MessageGroupStore; import org.springframework.integration.store.MessageStore; import org.springframework.integration.store.SimpleMessageStore; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** - * Message handler that holds a buffer of correlated messages in a + * 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 aggregating, resequencing, - * or custom implementations requiring correlation. + * 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. *

* To customize this handler inject {@link CorrelationStrategy}, * {@link ReleaseStrategy}, and {@link MessageGroupProcessor} implementations as * you require. *

- * By default the CorrelationStrategy will be a - * HeaderAttributeCorrelationStrategy and the ReleaseStrategy will be a - * SequenceSizeReleaseStrategy. + * 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 * @since 2.0 */ -public class CorrelatingMessageHandler extends AbstractMessageHandler implements MessageProducer { +public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageHandler implements MessageProducer { - private static final Log logger = LogFactory.getLog(CorrelatingMessageHandler.class); + private static final Log logger = LogFactory.getLog(AbstractCorrelatingMessageHandler.class); public static final long DEFAULT_SEND_TIMEOUT = 1000L; - - private MessageGroupStore messageStore; + protected volatile MessageGroupStore messageStore; private final MessageGroupProcessor outputProcessor; @@ -80,9 +85,15 @@ public class CorrelatingMessageHandler extends AbstractMessageHandler implements private final Object correlationLocksMonitor = new Object(); private final ConcurrentMap locks = new ConcurrentHashMap(); + + protected volatile boolean keepReleasedMessages = true; - public CorrelatingMessageHandler(MessageGroupProcessor processor, MessageGroupStore store, + public void setKeepReleasedMessages(boolean keepReleasedMessages) { + this.keepReleasedMessages = keepReleasedMessages; + } + + public AbstractCorrelatingMessageHandler(MessageGroupProcessor processor, MessageGroupStore store, CorrelationStrategy correlationStrategy, ReleaseStrategy releaseStrategy) { Assert.notNull(processor); Assert.notNull(store); @@ -94,11 +105,11 @@ public class CorrelatingMessageHandler extends AbstractMessageHandler implements this.messagingTemplate.setSendTimeout(DEFAULT_SEND_TIMEOUT); } - public CorrelatingMessageHandler(MessageGroupProcessor processor, MessageGroupStore store) { + public AbstractCorrelatingMessageHandler(MessageGroupProcessor processor, MessageGroupStore store) { this(processor, store, null, null); } - public CorrelatingMessageHandler(MessageGroupProcessor processor) { + public AbstractCorrelatingMessageHandler(MessageGroupProcessor processor) { this(processor, new SimpleMessageStore(0), null, null); } @@ -159,72 +170,59 @@ public class CorrelatingMessageHandler extends AbstractMessageHandler implements public String getComponentType() { return "aggregator"; } + + protected MessageGroupStore getMessageStore() { + return messageStore; + } - @SuppressWarnings("rawtypes") @Override protected void handleMessageInternal(Message message) throws Exception { Object correlationKey = correlationStrategy.getCorrelationKey(message); Assert.state(correlationKey!=null, "Null correlation not allowed. Maybe the CorrelationStrategy is failing?"); if (logger.isDebugEnabled()) { - logger.debug("Handling message with correlationKey [" - + correlationKey + "]: " + message); + logger.debug("Handling message with correlationKey [" + correlationKey + "]: " + message); } // TODO: INT-1117 - make the lock global? Object lock = getLock(correlationKey); synchronized (lock) { - MessageGroup group = messageStore.getMessageGroup(correlationKey); - if (group.canAdd(message)) { + MessageGroup messageGroup = messageStore.getMessageGroup(correlationKey); + if (!messageGroup.isComplete() && messageGroup.canAdd(message)) { if (logger.isTraceEnabled()) { - logger.trace("Adding message to group [ " + group + "]"); + logger.trace("Adding message to group [ " + messageGroup + "]"); } - group = store(correlationKey, message); - if (releaseStrategy.canRelease(group)) { - Collection completedMessages = null; + messageGroup = store(correlationKey, message); + + if (releaseStrategy.canRelease(messageGroup)) { + Collection> completedMessages = null; try { - completedMessages = completeGroup(message, correlationKey, group); + completedMessages = completeGroup(message, correlationKey, messageGroup); } finally { // Always clean up even if there was an exception - // processing messages - cleanUpForReleasedGroup(group, completedMessages); - } - } else if (group.isComplete()) { - try { - // If not releasing any messages the group might still - // be complete - for (Message discard : group.getUnmarked()) { - discardChannel.send(discard); + // processing messages + this.afterRelease(messageGroup, completedMessages); + + synchronized(correlationLocksMonitor){ + locks.remove(messageGroup.getGroupId()); } } - finally { - remove(group); - } - } - } else { + } + } + else { discardChannel.send(message); } } } - @SuppressWarnings("rawtypes") - private void cleanUpForReleasedGroup(MessageGroup group, Collection completedMessages) { - if (group.isComplete() || group.getSequenceSize() == 0) { - // The group is complete or else there is no - // sequence so there is no more state to track - remove(group); - } else { - // Mark these messages as processed, but do not - // remove the group from store - if (completedMessages == null) { - mark(group); - } else { - mark(group, completedMessages); - } - } - } + /** + * Allows you to provide additional logic that needs to be performed after the MessageGroup was released. + * @param group + * @param completedMessages + */ + protected abstract void afterRelease(MessageGroup group, Collection> completedMessages); private final boolean forceComplete(MessageGroup group) { @@ -235,13 +233,14 @@ public class CorrelatingMessageHandler extends AbstractMessageHandler implements if (group.size() > 0) { try { if (releaseStrategy.canRelease(group)) { - completeGroup(correlationKey, group); - } else { - expireGroup(group, correlationKey); + this.completeGroup(correlationKey, group); + } + else { + this.expireGroup(correlationKey, group); } } finally { - remove(group); + this.remove(group); } return true; } @@ -256,31 +255,25 @@ public class CorrelatingMessageHandler extends AbstractMessageHandler implements } } - private void mark(MessageGroup group) { - messageStore.markMessageGroup(group); - } - - @SuppressWarnings("rawtypes") - private void mark(MessageGroup group, Collection partialSequence) { - Object id = group.getGroupId(); - for (Message message : partialSequence) { - messageStore.markMessageFromGroup(id, message); - } - } - - private void remove(MessageGroup group) { + void remove(MessageGroup group) { Object correlationKey = group.getGroupId(); messageStore.removeMessageGroup(correlationKey); - synchronized(correlationLocksMonitor){ - locks.remove(correlationKey); - } + } + + protected int findLastReleasedSequenceNumber(Object groupId, Collection> partialSequence){ + List> sorted = new ArrayList>((Collection>)partialSequence); + Collections.sort(sorted, new SequenceNumberComparator()); + + Message lastReleasedMessage = sorted.get(partialSequence.size()-1); + + return lastReleasedMessage.getHeaders().getSequenceNumber(); } private MessageGroup store(Object correlationKey, Message message) { return messageStore.addMessageToGroup(correlationKey, message); } - private void expireGroup(MessageGroup group, Object correlationKey) { + private void expireGroup(Object correlationKey, MessageGroup group) { if (logger.isInfoEnabled()) { logger.info("Expiring MessageGroup with correlationKey[" + correlationKey + "]"); } @@ -309,21 +302,25 @@ public class CorrelatingMessageHandler extends AbstractMessageHandler implements completeGroup(first, correlationKey, group); } - @SuppressWarnings({ "rawtypes", "unchecked" }) - private Collection completeGroup(Message message, Object correlationKey, MessageGroup group) { + @SuppressWarnings("unchecked") + private Collection> completeGroup(Message message, Object correlationKey, MessageGroup group) { if (logger.isDebugEnabled()) { - logger.debug("Completing group with correlationKey [" - + correlationKey + "]"); + logger.debug("Completing group with correlationKey [" + correlationKey + "]"); } Object result = outputProcessor.processMessageGroup(group); - Collection partialSequence = null; + Collection> partialSequence = null; if (result instanceof Collection) { - //Taking a risk here because of Type Erasure. This is covered in the processor contract - partialSequence = (Collection) result; + this.verifyResultCollectionConsistsOfMessages((Collection) result); + partialSequence = (Collection>) result; } this.sendReplies(result, message); return partialSequence; } + + private void verifyResultCollectionConsistsOfMessages(Collection elements){ + Class commonElementType = CollectionUtils.findCommonElementType(elements); + Assert.isAssignable(Message.class, commonElementType, "The expected collection of Messages contains non-Message element: " + commonElementType); + } @SuppressWarnings("rawtypes") private void sendReplies(Object processorResult, Message message) { diff --git a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AggregatingMessageHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AggregatingMessageHandler.java new file mode 100644 index 0000000000..fd35d0f65d --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AggregatingMessageHandler.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2011 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.Iterator; + +import org.springframework.integration.Message; +import org.springframework.integration.store.MessageGroup; +import org.springframework.integration.store.MessageGroupStore; + +/** + * Aggregator specific implementation of {@link AbstractCorrelatingMessageHandler}. + * Will remove {@link MessageGroup}s only if 'expireGroupsUponCompletion' flag is set to 'true'. + * + * @author Oleg Zhurakousky + * @since 2.1 + */ +public class AggregatingMessageHandler extends AbstractCorrelatingMessageHandler { + + private volatile boolean expireGroupsUponCompletion = false; + + + public AggregatingMessageHandler(MessageGroupProcessor processor, MessageGroupStore store, + CorrelationStrategy correlationStrategy, ReleaseStrategy releaseStrategy) { + super(processor, store, correlationStrategy, releaseStrategy); + } + + public AggregatingMessageHandler(MessageGroupProcessor processor, MessageGroupStore store) { + super(processor, store); + } + + public AggregatingMessageHandler(MessageGroupProcessor processor) { + super(processor); + } + + /** + * Will set the 'expireGroupsUponCompletion' flag and if it is + * set to 'true' it will also remove all 'complete' {@link MessageGroup}s + * @param expireGroupsUponCompletion + */ + public void setExpireGroupsUponCompletion(boolean expireGroupsUponCompletion) { + this.expireGroupsUponCompletion = expireGroupsUponCompletion; + if (expireGroupsUponCompletion) { + Iterator messageGroups = this.messageStore.iterator(); + while (messageGroups.hasNext()) { + MessageGroup messageGroup = messageGroups.next(); + if (messageGroup.isComplete()) { + remove(messageGroup); + } + } + } + } + + @Override + protected void afterRelease(MessageGroup messageGroup, Collection> completedMessages) { + this.messageStore.completeGroup(messageGroup.getGroupId()); + + if (this.expireGroupsUponCompletion) { + remove(messageGroup); + } + else { + if (this.keepReleasedMessages){ + messageStore.markMessageGroup(messageGroup); + } + else { + for (Message message : messageGroup.getMarked()) { + this.messageStore.removeMessageFromGroup(messageGroup.getGroupId(), message); + } + for (Message message : messageGroup.getUnmarked()) { + this.messageStore.removeMessageFromGroup(messageGroup.getGroupId(), message); + } + } + } + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/ResequencingMessageGroupProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/ResequencingMessageGroupProcessor.java index 01d8752862..9fb9839b5a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/ResequencingMessageGroupProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/ResequencingMessageGroupProcessor.java @@ -23,6 +23,7 @@ import java.util.*; * * @author Iwein Fuld * @author Dave Syer + * @author Oleg Zhurakousky * @since 2.0 */ public class ResequencingMessageGroupProcessor implements MessageGroupProcessor { @@ -37,15 +38,14 @@ public class ResequencingMessageGroupProcessor implements MessageGroupProcessor public void setComparator(Comparator> comparator) { this.comparator = comparator; } - - @SuppressWarnings("rawtypes") + public Object processMessageGroup(MessageGroup group) { Collection> messages = group.getUnmarked(); if (messages.size() > 0) { List> sorted = new ArrayList>(messages); Collections.sort(sorted, this.comparator); - ArrayList partialSequence = new ArrayList(); + ArrayList> partialSequence = new ArrayList>(); int previousSequence = extractSequenceNumber(sorted.get(0)); int currentSequence = previousSequence; for (Message message : sorted) { @@ -57,6 +57,7 @@ public class ResequencingMessageGroupProcessor implements MessageGroupProcessor } partialSequence.add(message); } + return partialSequence; } return null; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/ResequencingMessageHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/ResequencingMessageHandler.java new file mode 100644 index 0000000000..829363593f --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/ResequencingMessageHandler.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2011 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 org.springframework.integration.Message; +import org.springframework.integration.store.MessageGroup; +import org.springframework.integration.store.MessageGroupStore; + +/** + * Resequencer specific implementation of {@link AbstractCorrelatingMessageHandler}. + * Will remove {@link MessageGroup}s only if 'sequenceSize' is provided and reached. + * + * @author Oleg Zhurakousky + * @since 2.1 + */ +public class ResequencingMessageHandler extends AbstractCorrelatingMessageHandler { + + public ResequencingMessageHandler(MessageGroupProcessor processor, + MessageGroupStore store, CorrelationStrategy correlationStrategy, + ReleaseStrategy releaseStrategy) { + super(processor, store, correlationStrategy, releaseStrategy); + } + + + public ResequencingMessageHandler(MessageGroupProcessor processor, + MessageGroupStore store) { + super(processor, store); + } + + + public ResequencingMessageHandler(MessageGroupProcessor processor) { + super(processor); + } + + @Override + protected void afterRelease(MessageGroup messageGroup, Collection> completedMessages) { + + int size = messageGroup.getUnmarked().size() + messageGroup.getMarked().size(); + int sequenceSize = 0; + Message message = messageGroup.getOne(); + if (message != null){ + sequenceSize = message.getHeaders().getSequenceSize(); + } + // If there is no sequence then it must be incomplete or unbounded + if (sequenceSize > 0 && sequenceSize == size){ + remove(messageGroup); + } + else { + if (completedMessages != null){ + int lastReleasedSequenceNumber = this.findLastReleasedSequenceNumber(messageGroup.getGroupId(), completedMessages); + messageStore.setLastReleasedSequenceNumberForGroup(messageGroup.getGroupId(), lastReleasedSequenceNumber); + + if (this.keepReleasedMessages){ + Object id = messageGroup.getGroupId(); + for (Message msg : completedMessages) { + messageStore.markMessageFromGroup(id, msg); + } + } + else { + for (Message msg : completedMessages) { + this.messageStore.removeMessageFromGroup(messageGroup.getGroupId(), msg); + } + } + } + } + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/SequenceSizeReleaseStrategy.java b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/SequenceSizeReleaseStrategy.java index b3fea73515..f76b7e1e58 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/SequenceSizeReleaseStrategy.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/SequenceSizeReleaseStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2011 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. @@ -16,17 +16,17 @@ package org.springframework.integration.aggregator; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.integration.Message; -import org.springframework.integration.store.MessageGroup; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.integration.Message; +import org.springframework.integration.store.MessageGroup; + /** * An implementation of {@link ReleaseStrategy} that simply compares the current size of the message list to the * expected 'sequenceSize'. @@ -35,6 +35,7 @@ import java.util.List; * @author Marius Bogoevici * @author Dave Syer * @author Iwein Fuld + * @author Oleg Zhurakousky */ public class SequenceSizeReleaseStrategy implements ReleaseStrategy { @@ -62,24 +63,42 @@ public class SequenceSizeReleaseStrategy implements ReleaseStrategy { this.releasePartialSequences = releasePartialSequences; } - public boolean canRelease(MessageGroup messages) { - if (releasePartialSequences) { - Collection> unmarked = messages.getUnmarked(); - if (!unmarked.isEmpty()) { - if (logger.isTraceEnabled()) { - logger.trace("Considering partial release of group [" + messages + "]"); - } - List> sorted = new ArrayList>(unmarked); - Collections.sort(sorted, comparator); - int tail = sorted.get(0).getHeaders().getSequenceNumber() - 1; - boolean release = tail == messages.getMarked().size(); - if (logger.isTraceEnabled() && release) { - logger.trace("Release imminent because tail [" + tail + "] is next in line."); - } - return release; + public boolean canRelease(MessageGroup messageGroup) { + + boolean canRelease = false; + + Collection> unmarked = messageGroup.getUnmarked(); + + if (releasePartialSequences && !unmarked.isEmpty()) { + + if (logger.isTraceEnabled()) { + logger.trace("Considering partial release of group [" + messageGroup + "]"); } + List> sorted = new ArrayList>(unmarked); + Collections.sort(sorted, comparator); + + int nextSequenceNumber = sorted.get(0).getHeaders().getSequenceNumber(); + int lastReleasedMessageSequence = messageGroup.getLastReleasedMessageSequenceNumber(); + + if (nextSequenceNumber - lastReleasedMessageSequence == 1){ + canRelease = true;; + } } - return messages.isComplete(); + else { + int size = messageGroup.getUnmarked().size(); + + if (size == 0){ + canRelease = true; + } + else { + int sequenceSize = messageGroup.getOne().getHeaders().getSequenceSize(); + // If there is no sequence then it must be incomplete.... + if (sequenceSize == size){ + canRelease = true; + } + } + } + return canRelease; } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/annotation/Aggregator.java b/spring-integration-core/src/main/java/org/springframework/integration/annotation/Aggregator.java index e8d20404b9..b0a22dcb71 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/annotation/Aggregator.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/annotation/Aggregator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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. @@ -22,7 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.integration.aggregator.CorrelatingMessageHandler; +import org.springframework.integration.aggregator.AbstractCorrelatingMessageHandler; /** * Indicates that a method is capable of aggregating messages. @@ -32,6 +32,7 @@ import org.springframework.integration.aggregator.CorrelatingMessageHandler; * Message or a single Object to be used as a Message payload. * * @author Marius Bogoevici + * @author Oleg Zhurakousky */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @@ -56,7 +57,7 @@ public @interface Aggregator { /** * timeout for sending results to the reply target (in milliseconds) */ - long sendTimeout() default CorrelatingMessageHandler.DEFAULT_SEND_TIMEOUT; + long sendTimeout() default AbstractCorrelatingMessageHandler.DEFAULT_SEND_TIMEOUT; /** * indicates whether to send an incomplete aggregate on expiry of the message group diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AggregatorAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AggregatorAnnotationPostProcessor.java index 4987a76693..86f2328411 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AggregatorAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AggregatorAnnotationPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2011 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. @@ -23,7 +23,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.integration.MessageChannel; -import org.springframework.integration.aggregator.CorrelatingMessageHandler; +import org.springframework.integration.aggregator.AggregatingMessageHandler; import org.springframework.integration.aggregator.MethodInvokingCorrelationStrategy; import org.springframework.integration.aggregator.MethodInvokingMessageGroupProcessor; import org.springframework.integration.aggregator.MethodInvokingReleaseStrategy; @@ -40,6 +40,7 @@ import org.springframework.util.StringUtils; * Post-processor for the {@link Aggregator @Aggregator} annotation. * * @author Mark Fisher + * @author Oleg Zhurakousky */ public class AggregatorAnnotationPostProcessor extends AbstractMethodAnnotationPostProcessor { @@ -53,7 +54,7 @@ public class AggregatorAnnotationPostProcessor extends AbstractMethodAnnotationP MethodInvokingMessageGroupProcessor processor = new MethodInvokingMessageGroupProcessor(bean, method); MethodInvokingReleaseStrategy releaseStrategy = getReleaseStrategy(bean); MethodInvokingCorrelationStrategy correlationStrategy = getCorrelationStrategy(bean); - CorrelatingMessageHandler handler = new CorrelatingMessageHandler(processor, new SimpleMessageStore(), correlationStrategy, releaseStrategy); + AggregatingMessageHandler handler = new AggregatingMessageHandler(processor, new SimpleMessageStore(), correlationStrategy, releaseStrategy); String discardChannelName = annotation.discardChannel(); if (StringUtils.hasText(discardChannelName)) { MessageChannel discardChannel = this.channelResolver.resolveChannelName(discardChannelName); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AggregatorParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AggregatorParser.java index ff6466939b..9766c1e2be 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AggregatorParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/AggregatorParser.java @@ -21,6 +21,8 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.aggregator.AggregatingMessageHandler; +import org.springframework.integration.aggregator.MethodInvokingMessageGroupProcessor; import org.springframework.util.StringUtils; import org.w3c.dom.Element; @@ -60,6 +62,10 @@ public class AggregatorParser extends AbstractConsumerEndpointParser { private static final String RELEASE_STRATEGY_PROPERTY = "releaseStrategy"; private static final String CORRELATION_STRATEGY_PROPERTY = "correlationStrategy"; + + private static final String EXPIRE_GROUPS_UPON_COMPLETION = "expire-groups-upon-completion"; + + private static final String KEEP_RELEASED_MESSAGES = "keep-released-messages"; @Override protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) { @@ -68,14 +74,12 @@ public class AggregatorParser extends AbstractConsumerEndpointParser { String ref = element.getAttribute(REF_ATTRIBUTE); BeanDefinitionBuilder builder; - builder = BeanDefinitionBuilder.genericBeanDefinition(IntegrationNamespaceUtils.BASE_PACKAGE - + ".aggregator.CorrelatingMessageHandler"); + builder = BeanDefinitionBuilder.genericBeanDefinition(AggregatingMessageHandler.class); BeanDefinitionBuilder processorBuilder = null; BeanMetadataElement processor = null; if (innerHandlerDefinition != null || StringUtils.hasText(ref)) { - processorBuilder = BeanDefinitionBuilder.genericBeanDefinition(IntegrationNamespaceUtils.BASE_PACKAGE - + ".aggregator.MethodInvokingMessageGroupProcessor"); + processorBuilder = BeanDefinitionBuilder.genericBeanDefinition(MethodInvokingMessageGroupProcessor.class); builder.addConstructorArgValue(processorBuilder.getBeanDefinition()); if (innerHandlerDefinition != null) { processor = innerHandlerDefinition; @@ -110,8 +114,10 @@ public class AggregatorParser extends AbstractConsumerEndpointParser { IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, DISCARD_CHANNEL_ATTRIBUTE); IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, OUTPUT_CHANNEL_ATTRIBUTE); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, SEND_TIMEOUT_ATTRIBUTE); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, EXPIRE_GROUPS_UPON_COMPLETION); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, SEND_PARTIAL_RESULT_ON_EXPIRY_ATTRIBUTE); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-startup"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, KEEP_RELEASED_MESSAGES); this.injectPropertyWithAdapter(RELEASE_STRATEGY_REF_ATTRIBUTE, RELEASE_STRATEGY_METHOD_ATTRIBUTE, RELEASE_STRATEGY_EXPRESSION_ATTRIBUTE, RELEASE_STRATEGY_PROPERTY, "ReleaseStrategy", element, builder, processor, parserContext); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ResequencerParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ResequencerParser.java index c069a88bee..0a2d06dcf1 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ResequencerParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ResequencerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2011 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 @@ -18,6 +18,8 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.aggregator.ResequencingMessageGroupProcessor; +import org.springframework.integration.aggregator.ResequencingMessageHandler; import org.springframework.util.StringUtils; import org.w3c.dom.Element; @@ -27,6 +29,7 @@ import org.w3c.dom.Element; * @author Marius Bogoevici * @author Dave Syer * @author Iwein Fuld + * @author Oleg Zhurakousky */ public class ResequencerParser extends AbstractConsumerEndpointParser { @@ -53,15 +56,14 @@ public class ResequencerParser extends AbstractConsumerEndpointParser { private static final String RELEASE_STRATEGY_EXPRESSION_ATTRIBUTE = "release-strategy-expression"; private static final String RELEASE_PARTIAL_SEQUENCES_ATTRIBUTE = "release-partial-sequences"; + + private static final String KEEP_RELEASED_MESSAGES = "keep-released-messages"; @Override protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) { - BeanDefinitionBuilder builder = BeanDefinitionBuilder - .genericBeanDefinition(IntegrationNamespaceUtils.BASE_PACKAGE + ".aggregator.CorrelatingMessageHandler"); - BeanDefinitionBuilder processorBuilder = BeanDefinitionBuilder - .genericBeanDefinition(IntegrationNamespaceUtils.BASE_PACKAGE - + ".aggregator.ResequencingMessageGroupProcessor"); + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ResequencingMessageHandler.class); + BeanDefinitionBuilder processorBuilder = BeanDefinitionBuilder.genericBeanDefinition(ResequencingMessageGroupProcessor.class); // Comparator IntegrationNamespaceUtils.setReferenceIfAttributeDefined(processorBuilder, element, COMPARATOR_REF_ATTRIBUTE); @@ -86,6 +88,7 @@ public class ResequencerParser extends AbstractConsumerEndpointParser { IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, SEND_TIMEOUT_ATTRIBUTE); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, SEND_PARTIAL_RESULT_ON_EXPIRY_ATTRIBUTE); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, RELEASE_PARTIAL_SEQUENCES_ATTRIBUTE); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, KEEP_RELEASED_MESSAGES); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-startup"); return builder; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractKeyValueMessageStore.java b/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractKeyValueMessageStore.java new file mode 100644 index 0000000000..61f386626f --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractKeyValueMessageStore.java @@ -0,0 +1,233 @@ +/* + * Copyright 2002-2011 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.store; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.UUID; + +import org.springframework.integration.Message; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.util.Assert; + +/** + * Base class for implementations of Key/Value style {@link MessageGroupStore} and {@link MessageStore} + * + * @author Oleg Zhurakousky + * @since 2.1 + */ +public abstract class AbstractKeyValueMessageStore extends AbstractMessageGroupStore implements MessageStore{ + + protected static final String MESSAGE_KEY_PREFIX = "MESSAGE_"; + + protected static final String MESSAGE_GROUP_KEY_PREFIX = "MESSAGE_GROUP_"; + + + // MessageStore methods + + public Message getMessage(UUID id) { + Assert.notNull(id, "'id' must not be null"); + Object message = this.doRetrieve(MESSAGE_KEY_PREFIX + id); + if (message != null) { + Assert.isInstanceOf(Message.class, message); + } + return (Message) message; + } + + @SuppressWarnings("unchecked") + public Message addMessage(Message message) { + Assert.notNull(message, "'message' must not be null"); + UUID messageId = message.getHeaders().getId(); + this.doStore(MESSAGE_KEY_PREFIX + messageId, message); + return (Message) this.getMessage(messageId); + } + + public Message removeMessage(UUID id) { + Assert.notNull(id, "'id' must not be null"); + Object message = this.doRemove(MESSAGE_KEY_PREFIX + id); + if (message != null) { + Assert.isInstanceOf(Message.class, message); + } + return (Message) message; + } + + @ManagedAttribute + public long getMessageCount() { + Collection messageIds = this.doListKeys(MESSAGE_KEY_PREFIX + "*"); + return (messageIds != null) ? messageIds.size() : 0; + } + + + // MessageGroupStore methods + + /** + * Will create a new instance of SimpleMessageGroup if necessary. + */ + public MessageGroup getMessageGroup(Object groupId) { + Assert.notNull(groupId, "'groupId' must not be null"); + Object mgm = this.doRetrieve(MESSAGE_GROUP_KEY_PREFIX + groupId); + if (mgm != null) { + Assert.isInstanceOf(MessageGroupMetadata.class, mgm); + MessageGroupMetadata messageGroupMetadata = (MessageGroupMetadata) mgm; + ArrayList> markedMessages = new ArrayList>(); + for (UUID uuid : messageGroupMetadata.getMarkedMessageIds()) { + markedMessages.add(this.getMessage(uuid)); + } + ArrayList> unmarkedMessages = new ArrayList>(); + for (UUID uuid : messageGroupMetadata.getUnmarkedMessageIds()) { + unmarkedMessages.add(this.getMessage(uuid)); + } + SimpleMessageGroup messageGroup = new SimpleMessageGroup(unmarkedMessages, markedMessages, + groupId, messageGroupMetadata.getTimestamp(), messageGroupMetadata.isComplete()); + if (messageGroupMetadata.getLastReleasedMessageSequenceNumber() > 0) { + messageGroup.setLastReleasedMessageSequenceNumber(messageGroupMetadata.getLastReleasedMessageSequenceNumber()); + } + return messageGroup; + } + else { + return new SimpleMessageGroup(groupId); + } + } + + /** + * Add a Message to the group with the provided group ID. + */ + public MessageGroup addMessageToGroup(Object groupId, Message message) { + Assert.notNull(groupId, "'groupId' must not be null"); + Assert.notNull(message, "'message' must not be null"); + SimpleMessageGroup messageGroup = this.getSimpleMessageGroup(this.getMessageGroup(groupId)); + messageGroup.add(message); + this.doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, new MessageGroupMetadata(messageGroup)); + this.addMessage(message); + return messageGroup; + } + + /** + * Mark all messages in the provided group. + */ + public MessageGroup markMessageGroup(MessageGroup group) { + Assert.notNull(group, "'group' must not be null"); + Object groupId = group.getGroupId(); + SimpleMessageGroup messageGroup = this.getSimpleMessageGroup(group); + messageGroup.markAll(); + this.doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, new MessageGroupMetadata(messageGroup)); + return messageGroup; + } + + /** + * Remove a Message from the group with the provided group ID. + */ + public MessageGroup removeMessageFromGroup(Object groupId, Message messageToRemove) { + Assert.notNull(groupId, "'groupId' must not be null"); + Assert.notNull(messageToRemove, "'messageToRemove' must not be null"); + SimpleMessageGroup messageGroup = this.getSimpleMessageGroup(this.getMessageGroup(groupId)); + messageGroup.remove(messageToRemove); + this.doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, new MessageGroupMetadata(messageGroup)); + return messageGroup; + } + + /** + * Mark the given Message within the group corresponding to the provided group ID. + */ + public MessageGroup markMessageFromGroup(Object groupId, Message messageToMark) { + Assert.notNull(groupId, "'groupId' must not be null"); + Assert.notNull(messageToMark, "'messageToMark' must not be null"); + SimpleMessageGroup messageGroup = this.getSimpleMessageGroup(this.getMessageGroup(groupId)); + messageGroup.mark(messageToMark); + this.doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, new MessageGroupMetadata(messageGroup)); + return messageGroup; + } + + public void completeGroup(Object groupId) { + Assert.notNull(groupId, "'groupId' must not be null"); + SimpleMessageGroup messageGroup = this.getSimpleMessageGroup(this.getMessageGroup(groupId)); + messageGroup.complete(); + this.doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, new MessageGroupMetadata(messageGroup)); + } + + /** + * Remove the MessageGroup with the provided group ID. + */ + public void removeMessageGroup(Object groupId) { + Assert.notNull(groupId, "'groupId' must not be null"); + Object mgm = this.doRemove(MESSAGE_GROUP_KEY_PREFIX + groupId); + if (mgm != null) { + Assert.isInstanceOf(MessageGroupMetadata.class, mgm); + MessageGroupMetadata messageGroupMetadata = (MessageGroupMetadata) mgm; + for (UUID messageId : messageGroupMetadata.getMarkedMessageIds()) { + this.removeMessage(messageId); + } + for (UUID messageId : messageGroupMetadata.getUnmarkedMessageIds()) { + this.removeMessage(messageId); + } + } + } + + public void setLastReleasedSequenceNumberForGroup(Object groupId, int sequenceNumber) { + Assert.notNull(groupId, "'groupId' must not be null"); + SimpleMessageGroup messageGroup = this.getSimpleMessageGroup(this.getMessageGroup(groupId)); + messageGroup.setLastReleasedMessageSequenceNumber(sequenceNumber); + this.doStore(MESSAGE_GROUP_KEY_PREFIX + groupId, new MessageGroupMetadata(messageGroup)); + } + + public Iterator iterator() { + final Iterator idIterator = this.doListKeys(MESSAGE_GROUP_KEY_PREFIX + "*").iterator(); + return new MessageGroupIterator(idIterator); + } + + private SimpleMessageGroup getSimpleMessageGroup(MessageGroup messageGroup){ + if (messageGroup instanceof SimpleMessageGroup){ + return (SimpleMessageGroup) messageGroup; + } + else { + return new SimpleMessageGroup(messageGroup); + } + } + + protected abstract Object doRetrieve(Object id); + + protected abstract void doStore(Object id, Object objectToStore); + + protected abstract Object doRemove(Object id); + + protected abstract Collection doListKeys(String keyPattern); + + + private class MessageGroupIterator implements Iterator { + + private final Iterator idIterator; + + private MessageGroupIterator(Iterator idIterator) { + this.idIterator = idIterator; + } + + public boolean hasNext() { + return idIterator.hasNext(); + } + + public MessageGroup next() { + Object messageGroupId = idIterator.next(); + return getMessageGroup(messageGroupId); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractMessageGroupStore.java b/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractMessageGroupStore.java index 1efc03d8e2..0336f7a0f4 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractMessageGroupStore.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/store/AbstractMessageGroupStore.java @@ -14,7 +14,6 @@ package org.springframework.integration.store; import java.util.Collection; -import java.util.Iterator; import java.util.LinkedHashSet; import org.apache.commons.logging.Log; @@ -67,8 +66,6 @@ public abstract class AbstractMessageGroupStore implements MessageGroupStore, It } return count; } - - public abstract Iterator iterator(); @ManagedAttribute public int getMessageCountForAllMessageGroups() { diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroup.java b/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroup.java index 34a764d490..43e9f29263 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroup.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroup.java @@ -16,10 +16,10 @@ package org.springframework.integration.store; -import org.springframework.integration.Message; - import java.util.Collection; +import org.springframework.integration.Message; + /** * A group of messages that are correlated with each other and should be processed in the same context. The group is * divided into marked and unmarked messages. The marked messages are typically already processed, the unmarked messages @@ -49,11 +49,21 @@ public interface MessageGroup { * @return the key that links these messages together */ Object getGroupId(); + + /** + * Returns the sequenceNumber of the last released message. Used in Resequencer use cases only + */ + int getLastReleasedMessageSequenceNumber(); /** * @return true if the group is complete (i.e. no more messages are expected to be added) */ boolean isComplete(); + + /** + * + */ + void complete(); /** * @return the size of the sequence expected 0 if unknown diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroupMetadata.java b/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroupMetadata.java new file mode 100644 index 0000000000..4a7413e3e8 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroupMetadata.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2011 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.store; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.springframework.integration.Message; +import org.springframework.util.Assert; + +/** + * Immutable Value Object holding metadata about a MessageGroup. + * + * @author Oleg Zhurakousky + * @since 2.1 + */ +public class MessageGroupMetadata implements Serializable{ + + private static final long serialVersionUID = 1L; + + + private final Object groupId; + + private final List markedMessageIds; + + private final List unmarkedMessageIds; + + private final boolean complete; + + private final long timestamp; + + private final int lastReleasedMessageSequenceNumber; + + + public MessageGroupMetadata(MessageGroup messageGroup) { + Assert.notNull(messageGroup, "'messageGroup' must not be null"); + this.groupId = messageGroup.getGroupId(); + this.markedMessageIds = new ArrayList(); + for (Message message : messageGroup.getMarked()) { + this.markedMessageIds.add(message.getHeaders().getId()); + } + this.unmarkedMessageIds = new ArrayList(); + for (Message message : messageGroup.getUnmarked()) { + this.unmarkedMessageIds.add(message.getHeaders().getId()); + } + this.complete = messageGroup.isComplete(); + this.timestamp = messageGroup.getTimestamp(); + this.lastReleasedMessageSequenceNumber = messageGroup.getLastReleasedMessageSequenceNumber(); + } + + + public Object getGroupId() { + return this.groupId; + } + + public List getMarkedMessageIds() { + return Collections.unmodifiableList(markedMessageIds); + } + + public List getUnmarkedMessageIds() { + return Collections.unmodifiableList(this.unmarkedMessageIds); + } + + public boolean isComplete() { + return this.complete; + } + + public long getTimestamp() { + return this.timestamp; + } + + public int getLastReleasedMessageSequenceNumber() { + return this.lastReleasedMessageSequenceNumber; + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroupStore.java b/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroupStore.java index 4b4d1a697f..ac115027a5 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroupStore.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/store/MessageGroupStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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 @@ -12,6 +12,8 @@ */ package org.springframework.integration.store; +import java.util.Iterator; + import org.springframework.integration.Message; import org.springframework.jmx.export.annotation.ManagedAttribute; @@ -19,6 +21,7 @@ import org.springframework.jmx.export.annotation.ManagedAttribute; * Interface for storage operations on groups of messages linked by a group id. * * @author Dave Syer + * @author Oleg Zhurakousky * * @since 2.0 * @@ -121,4 +124,22 @@ public interface MessageGroupStore { * @see #registerMessageGroupExpiryCallback(MessageGroupCallback) */ int expireMessageGroups(long timeout); + + /** + * Allows you to set the sequence number of the last released Message. Used for Resequencing use cases + * @param sequenceNumber + */ + void setLastReleasedSequenceNumberForGroup(Object groupId, int sequenceNumber); + + /** + * Returns the iterator of currently accumulated {@link MessageGroup}s + */ + Iterator iterator(); + + /** + * Completes this MessageGroup. Completion of the MessageGroup generally means + * that this group should not be allowing any more mutating operation to be performed on it. + * For example any attempt to add/remove new Message form the group should not be allowed. + */ + void completeGroup(Object groupId); } \ No newline at end of file diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/SimpleMessageGroup.java b/spring-integration-core/src/main/java/org/springframework/integration/store/SimpleMessageGroup.java index 294ddf5dd6..90c4de4068 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/store/SimpleMessageGroup.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/store/SimpleMessageGroup.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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 @@ -13,13 +13,13 @@ package org.springframework.integration.store; -import org.springframework.integration.Message; - import java.util.Collection; import java.util.Collections; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import org.springframework.integration.Message; + /** * Represents a mutable group of correlated messages that is bound to a certain {@link MessageStore} and group id. The * group will grow during its lifetime, when messages are added to it. This MessageGroup is thread safe. @@ -41,22 +41,27 @@ public class SimpleMessageGroup implements MessageGroup { // @GuardedBy(lock) public final BlockingQueue> unmarked = new LinkedBlockingQueue>(); + + private volatile int lastReleasedMessageSequence; private final long timestamp; + + private volatile boolean complete; public SimpleMessageGroup(Object groupId) { this(Collections.> emptyList(), Collections.> emptyList(), groupId, System - .currentTimeMillis()); + .currentTimeMillis(), false); } public SimpleMessageGroup(Collection> unmarked, Object groupId) { - this(unmarked, Collections.> emptyList(), groupId, System.currentTimeMillis()); + this(unmarked, Collections.> emptyList(), groupId, System.currentTimeMillis(), false); } public SimpleMessageGroup(Collection> unmarked, Collection> marked, - Object groupId, long timestamp) { + Object groupId, long timestamp, boolean complete) { this.groupId = groupId; this.timestamp = timestamp; + this.complete = complete; synchronized (lock) { for (Message message : unmarked) { addUnmarked(message); @@ -69,6 +74,7 @@ public class SimpleMessageGroup implements MessageGroup { public SimpleMessageGroup(MessageGroup template) { this.groupId = template.getGroupId(); + this.complete = template.isComplete(); synchronized (lock) { // Explicit iteration to work around bug in JDK (before 1.6.0_20 for (Message message : template.getMarked()) { @@ -84,6 +90,8 @@ public class SimpleMessageGroup implements MessageGroup { } this.timestamp = template.getTimestamp(); } + + public long getTimestamp() { return timestamp; @@ -103,6 +111,10 @@ public class SimpleMessageGroup implements MessageGroup { unmarked.remove(message); } } + + public int getLastReleasedMessageSequenceNumber() { + return lastReleasedMessageSequence; + } private boolean addUnmarked(Message message) { if (isMember(message)) { @@ -127,6 +139,10 @@ public class SimpleMessageGroup implements MessageGroup { return Collections.unmodifiableCollection(unmarked); } } + + public void setLastReleasedMessageSequenceNumber(int sequenceNumber){ + this.lastReleasedMessageSequence = sequenceNumber; + } public Collection> getMarked() { synchronized (lock) { @@ -139,14 +155,13 @@ public class SimpleMessageGroup implements MessageGroup { } public boolean isComplete() { - if (size() == 0) { - return true; - } - int sequenceSize = getSequenceSize(); - // If there is no sequence then it must be incomplete.... - return sequenceSize > 0 && sequenceSize == size(); + return this.complete; } - + + public void complete() { + this.complete = true; + } + public int getSequenceSize() { if (size() == 0) { return 0; @@ -183,6 +198,11 @@ public class SimpleMessageGroup implements MessageGroup { } return one; } + + public void clear(){ + this.marked.clear(); + this.unmarked.clear(); + } /** * This method determines whether messages have been added to this group that supersede the given message based on diff --git a/spring-integration-core/src/main/java/org/springframework/integration/store/SimpleMessageStore.java b/spring-integration-core/src/main/java/org/springframework/integration/store/SimpleMessageStore.java index 7abbb3edc4..8b1560073f 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/store/SimpleMessageStore.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/store/SimpleMessageStore.java @@ -148,10 +148,14 @@ public class SimpleMessageStore extends AbstractMessageGroupStore implements Mes return group; } - @Override public Iterator iterator() { return new HashSet(groupIdToMessageGroup.values()).iterator(); } + + public void setLastReleasedSequenceNumberForGroup(Object groupId, int sequenceNumber) { + SimpleMessageGroup group = getMessageGroupInternal(groupId); + group.setLastReleasedMessageSequenceNumber(sequenceNumber); + } private SimpleMessageGroup getMessageGroupInternal(Object groupId) { if (!groupIdToMessageGroup.containsKey(groupId)) { @@ -160,4 +164,9 @@ public class SimpleMessageStore extends AbstractMessageGroupStore implements Mes return groupIdToMessageGroup.get(groupId); } + public void completeGroup(Object groupId) { + SimpleMessageGroup group = getMessageGroupInternal(groupId); + group.complete(); + } + } diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.1.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.1.xsd index 40c95cea11..dd8c1ec35b 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.1.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.1.xsd @@ -2692,6 +2692,33 @@ endpoint itself is a Polling Consumer for a channel with a queue. + + + + Boolean flag specifying if MessageGroup should be removed once completed. Useful for + handling late arrival use cases where messages arriving with the correlationKey that + is the same as the completed MessageGroup will be discarded. Default is 'false' + + + + + + + + + + + + A method defined on the bean referenced by release-strategy, that implements the completion + decision algorithm. + + + + + + A SpEL expression to apply to the message group (e.g, payload.size() > 6) + + @@ -2713,22 +2740,9 @@ endpoint itself is a Polling Consumer for a channel with a queue. - + - - - - - - - A method defined on the bean referenced by release-strategy, that implements the completion - decision algorithm. - - - - - - A SpEL expression to apply to the message group (e.g, payload.size() > 6) + Will store messages after their release. Mainly used for monitoring purposes. Default is 'true' diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AggregatorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AggregatorTests.java index cda94b6b63..1cd83b08b9 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AggregatorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AggregatorTests.java @@ -42,14 +42,14 @@ import org.springframework.integration.support.MessageBuilder; */ public class AggregatorTests { - private CorrelatingMessageHandler aggregator; + private AggregatingMessageHandler aggregator; private SimpleMessageStore store = new SimpleMessageStore(50); @Before public void configureAggregator() { - this.aggregator = new CorrelatingMessageHandler(new MultiplyingProcessor(), store); + this.aggregator = new AggregatingMessageHandler(new MultiplyingProcessor(), store); } @@ -211,7 +211,7 @@ public class AggregatorTests { @Test public void testNullReturningAggregator() throws InterruptedException { - this.aggregator = new CorrelatingMessageHandler(new NullReturningMessageProcessor(), new SimpleMessageStore(50)); + this.aggregator = new AggregatingMessageHandler(new NullReturningMessageProcessor(), new SimpleMessageStore(50)); QueueChannel replyChannel = new QueueChannel(); Message message1 = createMessage(3, "ABC", 3, 1, replyChannel, null); Message message2 = createMessage(5, "ABC", 3, 2, replyChannel, null); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/ConcurrentAggregatorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/ConcurrentAggregatorTests.java index 508f0839aa..bacb102732 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/ConcurrentAggregatorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/ConcurrentAggregatorTests.java @@ -16,19 +16,12 @@ package org.springframework.integration.aggregator; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; - import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.TaskExecutor; import org.springframework.integration.Message; @@ -42,6 +35,13 @@ import org.springframework.integration.store.MessageGroupStore; import org.springframework.integration.store.SimpleMessageStore; import org.springframework.integration.support.MessageBuilder; +import static org.hamcrest.CoreMatchers.is; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + /** * @author Mark Fisher * @author Marius Bogoevici @@ -51,7 +51,7 @@ public class ConcurrentAggregatorTests { private TaskExecutor taskExecutor; - private CorrelatingMessageHandler aggregator; + private AggregatingMessageHandler aggregator; private MessageGroupStore store = new SimpleMessageStore(); @@ -59,7 +59,7 @@ public class ConcurrentAggregatorTests { @Before public void configureAggregator() { this.taskExecutor = new SimpleAsyncTaskExecutor(); - this.aggregator = new CorrelatingMessageHandler(new MultiplyingProcessor(), store); + this.aggregator = new AggregatingMessageHandler(new MultiplyingProcessor(), store); } @@ -274,7 +274,7 @@ public class ConcurrentAggregatorTests { @Test public void testNullReturningAggregator() throws InterruptedException { - this.aggregator = new CorrelatingMessageHandler(new NullReturningMessageProcessor(), new SimpleMessageStore( + this.aggregator = new AggregatingMessageHandler(new NullReturningMessageProcessor(), new SimpleMessageStore( 50)); QueueChannel replyChannel = new QueueChannel(); Message message1 = createMessage(3, "ABC", 3, 1, replyChannel, null); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/CorrelatingMessageHandlerIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/CorrelatingMessageHandlerIntegrationTests.java index 5abbae95b4..284e22cb86 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/CorrelatingMessageHandlerIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/CorrelatingMessageHandlerIntegrationTests.java @@ -16,21 +16,20 @@ package org.springframework.integration.aggregator; -import static org.mockito.Mockito.isA; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import org.junit.Before; import org.junit.Test; - import org.springframework.integration.Message; import org.springframework.integration.MessageChannel; import org.springframework.integration.store.MessageGroupStore; import org.springframework.integration.store.SimpleMessageStore; import org.springframework.integration.support.MessageBuilder; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + public class CorrelatingMessageHandlerIntegrationTests { private MessageGroupStore store = new SimpleMessageStore(100); @@ -39,7 +38,7 @@ public class CorrelatingMessageHandlerIntegrationTests { private MessageGroupProcessor processor = new PassThroughMessageGroupProcessor(); - private CorrelatingMessageHandler defaultHandler = new CorrelatingMessageHandler(processor, store); + private AggregatingMessageHandler defaultHandler = new AggregatingMessageHandler(processor, store); @Before public void setupHandler() { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/CorrelatingMessageHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/CorrelatingMessageHandlerTests.java index b509425d85..70de3e5eae 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/CorrelatingMessageHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/CorrelatingMessageHandlerTests.java @@ -16,13 +16,6 @@ package org.springframework.integration.aggregator; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.isA; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -35,7 +28,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.internal.stubbing.answers.ThrowsException; import org.mockito.runners.MockitoJUnitRunner; - import org.springframework.integration.Message; import org.springframework.integration.MessageChannel; import org.springframework.integration.MessageHandlingException; @@ -45,6 +37,14 @@ import org.springframework.integration.store.SimpleMessageStore; import org.springframework.integration.support.MessageBuilder; import org.springframework.test.util.ReflectionTestUtils; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + /** * @author Iwein Fuld * @author Dave Syer @@ -52,7 +52,7 @@ import org.springframework.test.util.ReflectionTestUtils; @RunWith(MockitoJUnitRunner.class) public class CorrelatingMessageHandlerTests { - private CorrelatingMessageHandler handler; + private AggregatingMessageHandler handler; @Mock private CorrelationStrategy correlationStrategy; @@ -70,7 +70,7 @@ public class CorrelatingMessageHandlerTests { @Before public void initializeSubject() { - handler = new CorrelatingMessageHandler(processor, store, correlationStrategy, ReleaseStrategy); + handler = new AggregatingMessageHandler(processor, store, correlationStrategy, ReleaseStrategy); handler.setOutputChannel(outputChannel); } @@ -94,7 +94,7 @@ public class CorrelatingMessageHandlerTests { verify(processor).processMessageGroup(isA(SimpleMessageGroup.class)); } - private void verifyLocks(CorrelatingMessageHandler handler, int lockCount) { + private void verifyLocks(AggregatingMessageHandler handler, int lockCount) { assertEquals(lockCount, ((Map) ReflectionTestUtils.getField(handler, "locks")).size()); } @@ -110,6 +110,8 @@ public class CorrelatingMessageHandlerTests { when(correlationStrategy.getCorrelationKey(isA(Message.class))).thenReturn(correlationKey); + handler.setExpireGroupsUponCompletion(true); + handler.handleMessage(message1); try { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/MethodInvokingMessageGroupProcessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/MethodInvokingMessageGroupProcessorTests.java index 9d5b9ff1a6..98e7c62bbe 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/MethodInvokingMessageGroupProcessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/MethodInvokingMessageGroupProcessorTests.java @@ -479,7 +479,7 @@ public class MethodInvokingMessageGroupProcessorTests { proxyFactory.setProxyTargetClass(false); testBean = (GreetingService) proxyFactory.getProxy(); MethodInvokingMessageGroupProcessor aggregator = new MethodInvokingMessageGroupProcessor(testBean); - CorrelatingMessageHandler handler = new CorrelatingMessageHandler(aggregator); + AggregatingMessageHandler handler = new AggregatingMessageHandler(aggregator); handler.setReleaseStrategy(new MessageCountReleaseStrategy()); handler.setOutputChannel(output); EventDrivenConsumer endpoint = new EventDrivenConsumer(input, handler); @@ -498,7 +498,7 @@ public class MethodInvokingMessageGroupProcessorTests { proxyFactory.setProxyTargetClass(true); testBean = (GreetingService) proxyFactory.getProxy(); MethodInvokingMessageGroupProcessor aggregator = new MethodInvokingMessageGroupProcessor(testBean); - CorrelatingMessageHandler handler = new CorrelatingMessageHandler(aggregator); + AggregatingMessageHandler handler = new AggregatingMessageHandler(aggregator); handler.setReleaseStrategy(new MessageCountReleaseStrategy()); handler.setOutputChannel(output); EventDrivenConsumer endpoint = new EventDrivenConsumer(input, handler); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/ResequencerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/ResequencerTests.java index 4352bb9bbd..c35de895f5 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/ResequencerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/ResequencerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2011 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. @@ -16,6 +16,11 @@ package org.springframework.integration.aggregator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; + import org.junit.Before; import org.junit.Test; import org.springframework.integration.Message; @@ -25,23 +30,23 @@ import org.springframework.integration.store.MessageGroupStore; import org.springframework.integration.store.SimpleMessageStore; import org.springframework.integration.support.MessageBuilder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; +import static org.hamcrest.Matchers.is; -import static org.junit.Assert.*; -import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; /** * @author Marius Bogoevici * @author Alex Peters * @author Dave Syer * @author Iwein Fuld + * @author Oleg Zhurakousky */ public class ResequencerTests { - private CorrelatingMessageHandler resequencer; + private ResequencingMessageHandler resequencer; private ResequencingMessageGroupProcessor processor = new ResequencingMessageGroupProcessor(); @@ -49,7 +54,7 @@ public class ResequencerTests { @Before public void configureResequencer() { - this.resequencer = new CorrelatingMessageHandler(processor, store, null, null); + this.resequencer = new ResequencingMessageHandler(processor, store, null, null); } @Test @@ -71,6 +76,59 @@ public class ResequencerTests { assertNotNull(reply3); assertThat( reply3.getHeaders().getSequenceNumber(), is(3)); } + + @Test + public void testBasicResequencingA() throws InterruptedException { + SequenceSizeReleaseStrategy releaseStrategy = new SequenceSizeReleaseStrategy(); + releaseStrategy.setReleasePartialSequences(true); + this.resequencer = new ResequencingMessageHandler(processor, store, null, releaseStrategy); + + QueueChannel replyChannel = new QueueChannel(); + Message message1 = createMessage("123", "ABC", 3, 1, replyChannel); + Message message3 = createMessage("789", "ABC", 3, 3, replyChannel); + + this.resequencer.handleMessage(message3); + assertNull(replyChannel.receive(0)); + this.resequencer.handleMessage(message1); + assertNotNull(replyChannel.receive(0)); + assertNull(replyChannel.receive(0)); + } + + @Test + public void testBasicUnboundedResequencing() throws InterruptedException { + SequenceSizeReleaseStrategy releaseStrategy = new SequenceSizeReleaseStrategy(); + releaseStrategy.setReleasePartialSequences(true); + this.resequencer = new ResequencingMessageHandler(processor, store, null, releaseStrategy); + QueueChannel replyChannel = new QueueChannel(); + this.resequencer.setCorrelationStrategy(new CorrelationStrategy() { + public Object getCorrelationKey(Message message) { + return "A"; + } + }); + //Message message0 = MessageBuilder.withPayload("0").setSequenceNumber(0).build(); + Message message1 = MessageBuilder.withPayload("1").setSequenceNumber(1).setReplyChannel(replyChannel).build(); + Message message2 = MessageBuilder.withPayload("2").setSequenceNumber(2).setReplyChannel(replyChannel).build(); + Message message3 = MessageBuilder.withPayload("3").setSequenceNumber(3).setReplyChannel(replyChannel).build(); + Message message4 = MessageBuilder.withPayload("4").setSequenceNumber(4).setReplyChannel(replyChannel).build(); + Message message5 = MessageBuilder.withPayload("5").setSequenceNumber(5).setReplyChannel(replyChannel).build(); + + this.resequencer.handleMessage(message3); + assertNull(replyChannel.receive(0)); + this.resequencer.handleMessage(message1); + assertNotNull(replyChannel.receive(0)); + + this.resequencer.handleMessage(message2); + + assertNotNull(replyChannel.receive(0)); + assertNotNull(replyChannel.receive(0)); + assertNull(replyChannel.receive(0)); + + this.resequencer.handleMessage(message5); + assertNull(replyChannel.receive(0)); + this.resequencer.handleMessage(message4); + assertNotNull(replyChannel.receive(0)); + } + @Test public void testBasicResequencingWithCustomComparator() throws InterruptedException { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorExpressionIntegrationTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorExpressionIntegrationTests-context.xml index 0b5bfb0d5c..e2d9db4134 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorExpressionIntegrationTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorExpressionIntegrationTests-context.xml @@ -5,7 +5,7 @@ xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd - http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd"> + http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-2.1.xsd"> diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorIntegrationTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorIntegrationTests-context.xml index 276e0413f4..f46817b751 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorIntegrationTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorIntegrationTests-context.xml @@ -5,7 +5,7 @@ xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd - http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd"> + http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-2.1.xsd"> @@ -20,8 +20,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorIntegrationTests.java index d434287a41..4c1eca9ccd 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorIntegrationTests.java @@ -17,6 +17,8 @@ package org.springframework.integration.aggregator.integration; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import java.util.HashMap; import java.util.List; @@ -45,10 +47,22 @@ public class AggregatorIntegrationTests { @Autowired @Qualifier("input") private MessageChannel input; + + @Autowired + @Qualifier("expiringAggregatorInput") + private MessageChannel expiringAggregatorInput; + + @Autowired + @Qualifier("nonExpiringAggregatorInput") + private MessageChannel nonExpiringAggregatorInput; @Autowired @Qualifier("output") private PollableChannel output; + + @Autowired + @Qualifier("discard") + private PollableChannel discard; @Test//(timeout=5000) public void testVanillaAggregation() throws Exception { @@ -58,6 +72,49 @@ public class AggregatorIntegrationTests { } assertEquals(0 + 1 + 2 + 3 + 4, output.receive().getPayload()); } + + @Test + public void testNonExpiringAggregator() throws Exception { + for (int i = 0; i < 5; i++) { + Map headers = stubHeaders(i, 5, 1); + nonExpiringAggregatorInput.send(new GenericMessage(i, headers)); + } + assertNotNull(output.receive(0)); + + assertNull(discard.receive(0)); + + for (int i = 5; i < 10; i++) { + Map headers = stubHeaders(i, 5, 1); + nonExpiringAggregatorInput.send(new GenericMessage(i, headers)); + } + assertNull(output.receive(0)); + + assertNotNull(discard.receive(0)); + assertNotNull(discard.receive(0)); + assertNotNull(discard.receive(0)); + assertNotNull(discard.receive(0)); + assertNotNull(discard.receive(0)); + } + + @Test + public void testExpiringAggregator() throws Exception { + for (int i = 0; i < 5; i++) { + Map headers = stubHeaders(i, 5, 1); + expiringAggregatorInput.send(new GenericMessage(i, headers)); + } + assertNotNull(output.receive(0)); + + assertNull(discard.receive(0)); + + for (int i = 5; i < 10; i++) { + Map headers = stubHeaders(i, 5, 1); + expiringAggregatorInput.send(new GenericMessage(i, headers)); + } + assertNotNull(output.receive(0)); + + assertNull(discard.receive(0)); + + } // configured in context associated with this test public static class SummingAggregator { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorSupportedUseCasesTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorSupportedUseCasesTests.java new file mode 100644 index 0000000000..9119ae48b4 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorSupportedUseCasesTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2011 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.integration; + +import java.util.List; + +import org.junit.Test; +import org.springframework.integration.aggregator.AggregatingMessageHandler; +import org.springframework.integration.aggregator.DefaultAggregatingMessageGroupProcessor; +import org.springframework.integration.aggregator.ReleaseStrategy; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.store.MessageGroup; +import org.springframework.integration.store.MessageGroupStore; +import org.springframework.integration.store.SimpleMessageStore; +import org.springframework.integration.support.MessageBuilder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * @author Oleg Zhurakousky + * + */ +public class AggregatorSupportedUseCasesTests { + + private MessageGroupStore store = new SimpleMessageStore(100); + + private DefaultAggregatingMessageGroupProcessor processor = new DefaultAggregatingMessageGroupProcessor(); + + private AggregatingMessageHandler defaultHandler = new AggregatingMessageHandler(processor, store); + + @Test + public void waitForAllDefaultReleaseStrategyWithLateArrivals(){ + QueueChannel outputChannel = new QueueChannel(); + QueueChannel discardChannel = new QueueChannel(); + defaultHandler.setOutputChannel(outputChannel); + defaultHandler.setDiscardChannel(discardChannel); + defaultHandler.setKeepReleasedMessages(false); + + for (int i = 0; i < 5; i++) { + defaultHandler.handleMessage(MessageBuilder.withPayload(i).setSequenceSize(5).setCorrelationId("A").setSequenceNumber(i).build()); + } + assertEquals(5, ((List)outputChannel.receive(0).getPayload()).size()); + assertNull(discardChannel.receive(0)); + assertEquals(0, store.getMessageGroup("A").getUnmarked().size()); + assertEquals(0, store.getMessageGroup("A").getMarked().size()); + + // send another message with the same correlation id and see it in the discard channel + defaultHandler.handleMessage(MessageBuilder.withPayload("foo").setSequenceSize(5).setCorrelationId("A").setSequenceNumber(3).build()); + assertNotNull(discardChannel.receive(0)); + + // set 'expireGroupsUponCompletion' to 'true' and the messages should start accumulating again + defaultHandler.setExpireGroupsUponCompletion(true); + defaultHandler.handleMessage(MessageBuilder.withPayload("foo").setSequenceSize(5).setCorrelationId("A").setSequenceNumber(3).build()); + assertNull(discardChannel.receive(0)); + assertEquals(1, store.getMessageGroup("A").getUnmarked().size()); + } + + @Test + public void waitForAllCustomReleaseStrategyWithLateArrivals(){ + QueueChannel outputChannel = new QueueChannel(); + QueueChannel discardChannel = new QueueChannel(); + defaultHandler.setOutputChannel(outputChannel); + defaultHandler.setDiscardChannel(discardChannel); + defaultHandler.setReleaseStrategy(new SampleSizeReleaseStrategy()); + defaultHandler.setKeepReleasedMessages(false); + + for (int i = 0; i < 5; i++) { + defaultHandler.handleMessage(MessageBuilder.withPayload(i).setCorrelationId("A").build()); + } + assertEquals(5, ((List)outputChannel.receive(0).getPayload()).size()); + assertNull(discardChannel.receive(0)); + assertEquals(0, store.getMessageGroup("A").getUnmarked().size()); + assertEquals(0, store.getMessageGroup("A").getMarked().size()); + + // send another message with the same correlation id and see it in the discard channel + defaultHandler.handleMessage(MessageBuilder.withPayload("foo").setCorrelationId("A").build()); + assertNotNull(discardChannel.receive(0)); + + // set 'expireGroupsUponCompletion' to 'true' and the messages should start accumulating again + defaultHandler.setExpireGroupsUponCompletion(true); + defaultHandler.handleMessage(MessageBuilder.withPayload("foo").setCorrelationId("A").build()); + assertNull(discardChannel.receive(0)); + assertEquals(1, store.getMessageGroup("A").getUnmarked().size()); + } + + @Test + public void firstBest(){ + QueueChannel outputChannel = new QueueChannel(); + QueueChannel discardChannel = new QueueChannel(); + defaultHandler.setOutputChannel(outputChannel); + defaultHandler.setDiscardChannel(discardChannel); + defaultHandler.setReleaseStrategy(new FirstBestReleaseStrategy()); + + for (int i = 0; i < 5; i++) { + defaultHandler.handleMessage(MessageBuilder.withPayload(i).setCorrelationId("A").build()); + } + assertEquals(1, ((List)outputChannel.receive(0).getPayload()).size()); + assertNotNull(discardChannel.receive(0)); + assertNotNull(discardChannel.receive(0)); + assertNotNull(discardChannel.receive(0)); + assertNotNull(discardChannel.receive(0)); + } + + @Test + public void batchingWithoutLeftovers(){ + QueueChannel outputChannel = new QueueChannel(); + QueueChannel discardChannel = new QueueChannel(); + defaultHandler.setOutputChannel(outputChannel); + defaultHandler.setDiscardChannel(discardChannel); + defaultHandler.setReleaseStrategy(new SampleSizeReleaseStrategy()); + defaultHandler.setExpireGroupsUponCompletion(true); + + for (int i = 0; i < 10; i++) { + defaultHandler.handleMessage(MessageBuilder.withPayload(i).setCorrelationId("A").build()); + } + assertEquals(5, ((List)outputChannel.receive(0).getPayload()).size()); + assertEquals(5, ((List)outputChannel.receive(0).getPayload()).size()); + assertNull(discardChannel.receive(0)); + } + + @Test + public void batchingWithLeftovers(){ + QueueChannel outputChannel = new QueueChannel(); + QueueChannel discardChannel = new QueueChannel(); + defaultHandler.setOutputChannel(outputChannel); + defaultHandler.setDiscardChannel(discardChannel); + defaultHandler.setReleaseStrategy(new SampleSizeReleaseStrategy()); + defaultHandler.setExpireGroupsUponCompletion(true); + + for (int i = 0; i < 12; i++) { + defaultHandler.handleMessage(MessageBuilder.withPayload(i).setCorrelationId("A").build()); + } + assertEquals(5, ((List)outputChannel.receive(0).getPayload()).size()); + assertEquals(5, ((List)outputChannel.receive(0).getPayload()).size()); + assertNull(discardChannel.receive(0)); + assertEquals(2, store.getMessageGroup("A").getUnmarked().size()); + } + + private class SampleSizeReleaseStrategy implements ReleaseStrategy { + + public boolean canRelease(MessageGroup group) { + return group.getUnmarked().size() == 5; + } + + } + + private class FirstBestReleaseStrategy implements ReleaseStrategy { + + public boolean canRelease(MessageGroup group) { + return true; + } + + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AnnotationAggregatorTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AnnotationAggregatorTests-context.xml index 571d4d4d19..5de52eb809 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AnnotationAggregatorTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AnnotationAggregatorTests-context.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/integration - http://www.springframework.org/schema/integration/spring-integration.xsd"> + http://www.springframework.org/schema/integration/spring-integration-2.1.xsd"> diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/DefaultMessageAggregatorIntegrationTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/DefaultMessageAggregatorIntegrationTests-context.xml index 89d502edfc..5a82111a10 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/DefaultMessageAggregatorIntegrationTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/DefaultMessageAggregatorIntegrationTests-context.xml @@ -5,7 +5,7 @@ xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd - http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd"> + http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-2.1.xsd"> diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/MethodInvokingAggregatorReturningMessageTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/MethodInvokingAggregatorReturningMessageTests-context.xml index 9c0452da3d..aceb0b8afb 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/MethodInvokingAggregatorReturningMessageTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/MethodInvokingAggregatorReturningMessageTests-context.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/integration - http://www.springframework.org/schema/integration/spring-integration.xsd"> + http://www.springframework.org/schema/integration/spring-integration-2.1.xsd"> diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerExpressionIntegrationTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerExpressionIntegrationTests-context.xml deleted file mode 100644 index c65634174a..0000000000 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerExpressionIntegrationTests-context.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerExpressionIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerExpressionIntegrationTests.java deleted file mode 100644 index e1a08bb3ff..0000000000 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerExpressionIntegrationTests.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2002-2008 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.integration; - -import static org.junit.Assert.assertEquals; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.integration.Message; -import org.springframework.integration.MessageChannel; -import org.springframework.integration.MessageHeaders; -import org.springframework.integration.core.PollableChannel; -import org.springframework.integration.message.GenericMessage; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -/** - * @author Iwein Fuld - * @author Alex Peters - * @author Oleg Zhurakousky - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class ResequencerExpressionIntegrationTests { - - @Autowired - @Qualifier("input") - private MessageChannel input; - - @Autowired - @Qualifier("output") - private PollableChannel output; - - @Test//(timeout=5000) - public void testVanillaAggregation() throws Exception { - List> messages = new ArrayList>(); - for (int i = 0; i < 5; i++) { - Map headers = stubHeaders(i, 5, 1); - messages.add(new GenericMessage(i, headers)); - } - input.send(messages.get(2)); - input.send(messages.get(1)); - input.send(messages.get(0)); - assertEquals(0, output.receive().getPayload()); - assertEquals(1, output.receive().getPayload()); - assertEquals(2, output.receive().getPayload()); - } - - private Map stubHeaders(int sequenceNumber, int sequenceSize, int correllationId) { - Map headers = new HashMap(); - headers.put(MessageHeaders.SEQUENCE_NUMBER, sequenceNumber); - headers.put(MessageHeaders.SEQUENCE_SIZE, sequenceSize); - headers.put("foo", correllationId); - return headers; - } - -} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerIntegrationTest-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerIntegrationTest-context.xml new file mode 100644 index 0000000000..15ec2456c9 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerIntegrationTest-context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerIntegrationTest.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerIntegrationTest.java new file mode 100644 index 0000000000..adea088d12 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerIntegrationTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2011 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.integration; + +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.integration.Message; +import org.springframework.integration.MessageChannel; +import org.springframework.integration.aggregator.ResequencingMessageHandler; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.endpoint.EventDrivenConsumer; +import org.springframework.integration.store.MessageGroupStore; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.test.util.TestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +/** + * @author Oleg Zhurakousky + */ +public class ResequencerIntegrationTest { + + @Test + public void validateUnboundedResequencerLight(){ + ApplicationContext context = new ClassPathXmlApplicationContext("ResequencerIntegrationTest-context.xml", ResequencerIntegrationTest.class); + MessageChannel inputChannel = context .getBean("resequencerLightInput", MessageChannel.class); + QueueChannel outputChannel = context .getBean("outputChannel", QueueChannel.class); + EventDrivenConsumer edc = context.getBean("resequencerLight", EventDrivenConsumer.class); + ResequencingMessageHandler handler = TestUtils.getPropertyValue(edc, "handler", ResequencingMessageHandler.class); + MessageGroupStore store = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class); + + Message message1 = MessageBuilder.withPayload("1").setCorrelationId("A").setSequenceNumber(1).build(); + Message message2 = MessageBuilder.withPayload("2").setCorrelationId("A").setSequenceNumber(2).build(); + Message message3 = MessageBuilder.withPayload("3").setCorrelationId("A").setSequenceNumber(3).build(); + Message message4 = MessageBuilder.withPayload("4").setCorrelationId("A").setSequenceNumber(4).build(); + Message message5 = MessageBuilder.withPayload("5").setCorrelationId("A").setSequenceNumber(5).build(); + Message message6 = MessageBuilder.withPayload("6").setCorrelationId("A").setSequenceNumber(6).build(); + + inputChannel.send(message3); + assertNull(outputChannel.receive(0)); + + inputChannel.send(message1); + message1 = outputChannel.receive(0); + assertNotNull(message1); + assertEquals((Integer)1, message1.getHeaders().getSequenceNumber()); + + inputChannel.send(message2); + message2 = outputChannel.receive(0); + message3 = outputChannel.receive(0); + assertNotNull(message2); + assertNotNull(message3); + assertEquals((Integer)2, message2.getHeaders().getSequenceNumber()); + assertEquals((Integer)3, message3.getHeaders().getSequenceNumber()); + + inputChannel.send(message5); + assertNull(outputChannel.receive(0)); + + inputChannel.send(message6); + assertNull(outputChannel.receive(0)); + + inputChannel.send(message4); + message4 = outputChannel.receive(0); + message5 = outputChannel.receive(0); + message6 = outputChannel.receive(0); + assertNotNull(message4); + assertNotNull(message5); + assertNotNull(message6); + assertEquals((Integer)4, message4.getHeaders().getSequenceNumber()); + assertEquals((Integer)5, message5.getHeaders().getSequenceNumber()); + assertEquals((Integer)6, message6.getHeaders().getSequenceNumber()); + + + assertEquals(0, store.getMessageGroup("A").getUnmarked().size()); + assertEquals(0, store.getMessageGroup("A").getMarked().size()); + } + + @Test + public void validateUnboundedResequencerDeep(){ + ApplicationContext context = new ClassPathXmlApplicationContext("ResequencerIntegrationTest-context.xml", ResequencerIntegrationTest.class); + MessageChannel inputChannel = context .getBean("resequencerDeepInput", MessageChannel.class); + QueueChannel outputChannel = context .getBean("outputChannel", QueueChannel.class); + EventDrivenConsumer edc = context.getBean("resequencerDeep", EventDrivenConsumer.class); + ResequencingMessageHandler handler = TestUtils.getPropertyValue(edc, "handler", ResequencingMessageHandler.class); + MessageGroupStore store = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class); + + Message message1 = MessageBuilder.withPayload("1").setCorrelationId("A").setSequenceNumber(1).build(); + Message message2 = MessageBuilder.withPayload("2").setCorrelationId("A").setSequenceNumber(2).build(); + Message message3 = MessageBuilder.withPayload("3").setCorrelationId("A").setSequenceNumber(3).build(); + + inputChannel.send(message3); + assertNull(outputChannel.receive(0)); + inputChannel.send(message1); + assertNotNull(outputChannel.receive(0)); + inputChannel.send(message2); + assertNotNull(outputChannel.receive(0)); + assertNotNull(outputChannel.receive(0)); + assertEquals(0, store.getMessageGroup("A").getUnmarked().size()); + assertEquals(3, store.getMessageGroup("A").getMarked().size()); + } +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests.java index f88586dd24..c01ee4502f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests.java @@ -16,11 +16,6 @@ package org.springframework.integration.config; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -38,7 +33,7 @@ import org.springframework.integration.MessageChannel; import org.springframework.integration.MessageDeliveryException; import org.springframework.integration.MessageHandlingException; import org.springframework.integration.MessageRejectedException; -import org.springframework.integration.aggregator.CorrelatingMessageHandler; +import org.springframework.integration.aggregator.AggregatingMessageHandler; import org.springframework.integration.aggregator.CorrelationStrategy; import org.springframework.integration.aggregator.MethodInvokingReleaseStrategy; import org.springframework.integration.aggregator.ReleaseStrategy; @@ -49,6 +44,12 @@ import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.test.util.TestUtils; +import static org.hamcrest.CoreMatchers.is; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + /** * @author Marius Bogoevici * @author Mark Fisher @@ -111,7 +112,7 @@ public class AggregatorParserTests { MessageChannel outputChannel = (MessageChannel) context.getBean("outputChannel"); MessageChannel discardChannel = (MessageChannel) context.getBean("discardChannel"); Object consumer = new DirectFieldAccessor(endpoint).getPropertyValue("handler"); - assertThat(consumer, is(CorrelatingMessageHandler.class)); + assertThat(consumer, is(AggregatingMessageHandler.class)); DirectFieldAccessor accessor = new DirectFieldAccessor(consumer); Map map = (Map) new DirectFieldAccessor(new DirectFieldAccessor(new DirectFieldAccessor(accessor .getPropertyValue("outputProcessor")).getPropertyValue("processor")).getPropertyValue("delegate")) diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/ResequencerParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/ResequencerParserTests.java index e55632d68b..cfa9a3dd09 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/ResequencerParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/ResequencerParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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 @@ -13,21 +13,27 @@ package org.springframework.integration.config; +import java.util.Comparator; + import org.junit.Before; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.integration.Message; import org.springframework.integration.MessageChannel; -import org.springframework.integration.aggregator.*; +import org.springframework.integration.aggregator.CorrelationStrategy; +import org.springframework.integration.aggregator.MethodInvokingCorrelationStrategy; +import org.springframework.integration.aggregator.ResequencingMessageGroupProcessor; +import org.springframework.integration.aggregator.ResequencingMessageHandler; import org.springframework.integration.channel.NullChannel; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.test.util.TestUtils; -import java.util.Comparator; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; -import static org.junit.Assert.*; import static org.springframework.integration.test.util.TestUtils.getPropertyValue; /** @@ -47,8 +53,8 @@ public class ResequencerParserTests { @Test public void testDefaultResequencerProperties() { EventDrivenConsumer endpoint = (EventDrivenConsumer) context.getBean("defaultResequencer"); - CorrelatingMessageHandler resequencer = TestUtils.getPropertyValue(endpoint, "handler", - CorrelatingMessageHandler.class); + ResequencingMessageHandler resequencer = TestUtils.getPropertyValue(endpoint, "handler", + ResequencingMessageHandler.class); assertNull(getPropertyValue(resequencer, "outputChannel")); assertTrue(getPropertyValue(resequencer, "discardChannel") instanceof NullChannel); assertEquals("The ResequencerEndpoint is not set with the appropriate timeout value", 1000l, getPropertyValue( @@ -65,8 +71,8 @@ public class ResequencerParserTests { EventDrivenConsumer endpoint = (EventDrivenConsumer) context.getBean("completelyDefinedResequencer"); MessageChannel outputChannel = (MessageChannel) context.getBean("outputChannel"); MessageChannel discardChannel = (MessageChannel) context.getBean("discardChannel"); - CorrelatingMessageHandler resequencer = TestUtils.getPropertyValue(endpoint, "handler", - CorrelatingMessageHandler.class); + ResequencingMessageHandler resequencer = TestUtils.getPropertyValue(endpoint, "handler", + ResequencingMessageHandler.class); assertEquals("The ResequencerEndpoint is not injected with the appropriate output channel", outputChannel, getPropertyValue(resequencer, "outputChannel")); assertEquals("The ResequencerEndpoint is not injected with the appropriate discard channel", discardChannel, @@ -84,8 +90,8 @@ public class ResequencerParserTests { public void testCorrelationStrategyRefOnly() throws Exception { EventDrivenConsumer endpoint = (EventDrivenConsumer) context .getBean("resequencerWithCorrelationStrategyRefOnly"); - CorrelatingMessageHandler resequencer = TestUtils.getPropertyValue(endpoint, "handler", - CorrelatingMessageHandler.class); + ResequencingMessageHandler resequencer = TestUtils.getPropertyValue(endpoint, "handler", + ResequencingMessageHandler.class); assertEquals("The ResequencerEndpoint is not configured with the appropriate CorrelationStrategy", context .getBean("testCorrelationStrategy"), getPropertyValue(resequencer, "correlationStrategy")); } @@ -93,8 +99,8 @@ public class ResequencerParserTests { @Test public void shouldSetReleasePartialSequencesFlag(){ EventDrivenConsumer endpoint = (EventDrivenConsumer) context.getBean("completelyDefinedResequencer"); - CorrelatingMessageHandler resequencer = TestUtils.getPropertyValue(endpoint, "handler", - CorrelatingMessageHandler.class); + ResequencingMessageHandler resequencer = TestUtils.getPropertyValue(endpoint, "handler", + ResequencingMessageHandler.class); assertEquals("The ResequencerEndpoint is not configured with the appropriate 'release partial sequences' flag", true, getPropertyValue(getPropertyValue(resequencer, "releaseStrategy"), "releasePartialSequences")); } @@ -103,8 +109,8 @@ public class ResequencerParserTests { public void testCorrelationStrategyRefAndMethod() throws Exception { EventDrivenConsumer endpoint = (EventDrivenConsumer) context .getBean("resequencerWithCorrelationStrategyRefAndMethod"); - CorrelatingMessageHandler resequencer = TestUtils.getPropertyValue(endpoint, "handler", - CorrelatingMessageHandler.class); + ResequencingMessageHandler resequencer = TestUtils.getPropertyValue(endpoint, "handler", + ResequencingMessageHandler.class); Object correlationStrategy = getPropertyValue(resequencer, "correlationStrategy"); assertEquals("The ResequencerEndpoint is not configured with a CorrelationStrategy adapter", MethodInvokingCorrelationStrategy.class, correlationStrategy.getClass()); @@ -115,8 +121,8 @@ public class ResequencerParserTests { @Test public void testComparator() throws Exception { EventDrivenConsumer endpoint = (EventDrivenConsumer) context.getBean("resequencerWithComparator"); - CorrelatingMessageHandler handler = TestUtils.getPropertyValue(endpoint, "handler", - CorrelatingMessageHandler.class); + ResequencingMessageHandler handler = TestUtils.getPropertyValue(endpoint, "handler", + ResequencingMessageHandler.class); ResequencingMessageGroupProcessor resequencer = TestUtils.getPropertyValue(handler, "outputProcessor", ResequencingMessageGroupProcessor.class); Object comparator = getPropertyValue(resequencer, "comparator"); @@ -124,16 +130,6 @@ public class ResequencerParserTests { .getClass()); } - @Test - public void testReleaseStrategy() throws Exception { - EventDrivenConsumer endpoint = (EventDrivenConsumer) context.getBean("resequencerWithReleaseStrategy"); - CorrelatingMessageHandler handler = TestUtils.getPropertyValue(endpoint, "handler", - CorrelatingMessageHandler.class); - Object releaseStrategy = getPropertyValue(handler, "releaseStrategy"); - assertEquals("The Resequencer is not configured with an adapter", MethodInvokingReleaseStrategy.class, releaseStrategy - .getClass()); - } - @SuppressWarnings("unused") private static Message createMessage(T payload, Object correlationId, int sequenceSize, int sequenceNumber, MessageChannel outputChannel) { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/AggregatorAnnotationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/AggregatorAnnotationTests.java index e7ce5538e7..bc7b2f92b5 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/AggregatorAnnotationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/AggregatorAnnotationTests.java @@ -16,12 +16,6 @@ package org.springframework.integration.config.annotation; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.springframework.integration.test.util.TestUtils.getPropertyValue; - import java.lang.reflect.Method; import java.util.Map; @@ -30,9 +24,9 @@ import org.junit.Test; import org.springframework.beans.DirectFieldAccessor; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.integration.aggregator.MethodInvokingReleaseStrategy; -import org.springframework.integration.aggregator.CorrelatingMessageHandler; +import org.springframework.integration.aggregator.AggregatingMessageHandler; import org.springframework.integration.aggregator.MethodInvokingCorrelationStrategy; +import org.springframework.integration.aggregator.MethodInvokingReleaseStrategy; import org.springframework.integration.aggregator.SequenceSizeReleaseStrategy; import org.springframework.integration.channel.NullChannel; import org.springframework.integration.core.MessageHandler; @@ -41,6 +35,13 @@ import org.springframework.integration.support.channel.BeanFactoryChannelResolve import org.springframework.integration.support.channel.ChannelResolver; import org.springframework.integration.test.util.TestUtils; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import static org.springframework.integration.test.util.TestUtils.getPropertyValue; + /** * @author Marius Bogoevici * @author Mark Fisher @@ -56,7 +57,7 @@ public class AggregatorAnnotationTests { assertTrue(getPropertyValue(aggregator, "releaseStrategy") instanceof SequenceSizeReleaseStrategy); assertNull(getPropertyValue(aggregator, "outputChannel")); assertTrue(getPropertyValue(aggregator, "discardChannel") instanceof NullChannel); - assertEquals(CorrelatingMessageHandler.DEFAULT_SEND_TIMEOUT, getPropertyValue(aggregator, + assertEquals(AggregatingMessageHandler.DEFAULT_SEND_TIMEOUT, getPropertyValue(aggregator, "messagingTemplate.sendTimeout")); assertEquals(false, getPropertyValue(aggregator, "sendPartialResultOnExpiry")); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/resequencerParserTests.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/resequencerParserTests.xml index b4b90ebc66..ecbddfc421 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/resequencerParserTests.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/resequencerParserTests.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/integration - http://www.springframework.org/schema/integration/spring-integration.xsd"> + http://www.springframework.org/schema/integration/spring-integration-2.1.xsd"> @@ -50,10 +50,10 @@ input-channel="inputChannel5" comparator="testComparator"/> - + + + + @@ -64,9 +64,9 @@ - - - + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PNamespaceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PNamespaceTests.java index bf55c69029..2577034a95 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PNamespaceTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PNamespaceTests.java @@ -16,21 +16,20 @@ package org.springframework.integration.config.xml; -import static org.junit.Assert.assertEquals; - import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.integration.aggregator.CorrelatingMessageHandler; +import org.springframework.integration.aggregator.AggregatingMessageHandler; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.test.util.TestUtils; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import static org.junit.Assert.assertEquals; + /** * Validates the "p:namespace" is working for inner "bean" definition within SI components. * @@ -92,7 +91,7 @@ public class PNamespaceTests { @Test public void testPNamespaceChain() { List handlers = (List) TestUtils.getPropertyValue(sampleChain, "handler.handlers"); - CorrelatingMessageHandler handler = (CorrelatingMessageHandler) handlers.get(0); + AggregatingMessageHandler handler = (AggregatingMessageHandler) handlers.get(0); SampleAggregator aggregator = (SampleAggregator) TestUtils.getPropertyValue(handler, "outputProcessor.processor.delegate.targetObject"); assertEquals("Bill", aggregator.getName()); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/store/MessageStoreTests.java b/spring-integration-core/src/test/java/org/springframework/integration/store/MessageStoreTests.java index 1577a81fc0..fd6c8e2dc1 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/store/MessageStoreTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/store/MessageStoreTests.java @@ -87,7 +87,6 @@ public class MessageStoreTests { private boolean removed = false; - @Override public Iterator iterator() { return Arrays.asList(testMessages).iterator(); } @@ -118,6 +117,15 @@ public class MessageStoreTests { } } + public void setLastReleasedSequenceNumberForGroup(Object groupId, int sequenceNumber) { + throw new UnsupportedOperationException(); + } + + public void completeGroup(Object groupId) { + + throw new UnsupportedOperationException(); + } + } } diff --git a/spring-integration-gemfire/pom.xml b/spring-integration-gemfire/pom.xml index 794ede60f1..e016a91269 100644 --- a/spring-integration-gemfire/pom.xml +++ b/spring-integration-gemfire/pom.xml @@ -92,16 +92,10 @@ - - org.springframework.integration - spring-integration-test - 2.1.0.BUILD-SNAPSHOT - test - org.springframework.data.gemfire spring-gemfire - 1.1.0.M2 + 1.1.0.M3 compile @@ -118,6 +112,18 @@ + + org.springframework.integration + spring-integration-test + 2.1.0.BUILD-SNAPSHOT + test + + + org.springframework.integration + spring-integration-core + 2.1.0.BUILD-SNAPSHOT + compile + org.springframework spring-test @@ -144,9 +150,9 @@ org.springframework.integration - spring-integration-core + spring-integration-stream 2.1.0.BUILD-SNAPSHOT - compile + test org.easymock @@ -160,12 +166,6 @@ 2.3 test - - org.springframework.integration - spring-integration-stream - 2.1.0.BUILD-SNAPSHOT - test - org.springframework spring-context diff --git a/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStore.java b/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStore.java deleted file mode 100644 index 856ff1e5da..0000000000 --- a/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStore.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2002-2011 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.gemfire.store; - -import org.springframework.integration.Message; - -import com.gemstone.gemfire.cache.Region; - -/** - * Provides GemFire specific support as a backing key-value based {@link org.springframework.integration.store.MessageGroupStore}. - * Currently, this support is limited to explicitly depending on GemFire {@link com.gemstone.gemfire.cache.Region}s, but - * might conceptually also support optimized key traversal (using a {@link com.gemstone.gemfire.cache.query.Query}, for example). - * - * @author Josh Long - * @since 2.1 - * @see {@link org.springframework.integration.gemfire.store.KeyValueMessageGroupStore} - */ -public class GemfireMessageGroupStore extends KeyValueMessageGroupStore { - - public GemfireMessageGroupStore( - Region groupIdToMessageGroup, - Region> marked, - Region> unmarked ) { - super(groupIdToMessageGroup, marked, unmarked); - } - -} diff --git a/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/GemfireMessageStore.java b/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/GemfireMessageStore.java index 650bbb6ebd..9bdc9a7e2e 100644 --- a/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/GemfireMessageStore.java +++ b/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/GemfireMessageStore.java @@ -16,44 +16,97 @@ package org.springframework.integration.gemfire.store; -import java.util.UUID; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; -import org.springframework.integration.Message; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.gemfire.RegionAttributesFactoryBean; +import org.springframework.data.gemfire.RegionFactoryBean; +import org.springframework.integration.store.AbstractKeyValueMessageStore; +import org.springframework.integration.store.MessageGroupStore; import org.springframework.integration.store.MessageStore; -import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.util.Assert; +import org.springframework.util.PatternMatchUtils; +import com.gemstone.gemfire.cache.Cache; import com.gemstone.gemfire.cache.Region; /** + * Gemfire implementation of the key/value style {@link MessageStore} and {@link MessageGroupStore} + * * @author Mark Fisher + * @author Oleg Zhurakousky * @since 2.1 */ -public class GemfireMessageStore implements MessageStore { +public class GemfireMessageStore extends AbstractKeyValueMessageStore implements InitializingBean { - private final Region> region; + private volatile Region messageStoreRegion; - public GemfireMessageStore(Region> region) { - Assert.notNull(region, "region must not be null"); - this.region = region; + private final Cache cache; + + private volatile boolean ignoreJta = true; + + + public GemfireMessageStore(Cache cache) { + Assert.notNull(cache, "'cache' must not be null"); + this.cache = cache; } - public Message getMessage(UUID id) { - return this.region.get(id); + + public void setIgnoreJta(boolean ignoreJta) { + this.ignoreJta = ignoreJta; } - public Message addMessage(Message message) { - this.region.put(message.getHeaders().getId(), message); - return message; + @SuppressWarnings("unchecked") + public void afterPropertiesSet() { + try { + RegionAttributesFactoryBean attributesFactoryBean = new RegionAttributesFactoryBean(); + attributesFactoryBean.setIgnoreJTA(this.ignoreJta); + attributesFactoryBean.afterPropertiesSet(); + RegionFactoryBean messageRegionFactoryBean = new RegionFactoryBean(); + messageRegionFactoryBean.setBeanName("messageStoreRegion"); + messageRegionFactoryBean.setAttributes(attributesFactoryBean.getObject()); + messageRegionFactoryBean.setCache(cache); + messageRegionFactoryBean.afterPropertiesSet(); + this.messageStoreRegion = messageRegionFactoryBean.getObject(); + } + catch (Exception e) { + throw new IllegalArgumentException("Failed to initialize Gemfire Region", e); + } } - public Message removeMessage(UUID id) { - return this.region.remove(id); + @Override + protected Object doRetrieve(Object id) { + Assert.notNull(id, "'id' must not be null"); + return this.messageStoreRegion.get(id); } - @ManagedAttribute - public long getMessageCount() { - return this.region.size(); + @Override + protected void doStore(Object id, Object objectToStore) { + Assert.notNull(id, "'id' must not be null"); + Assert.notNull(objectToStore, "'objectToStore' must not be null"); + this.messageStoreRegion.put(id, objectToStore); + } + + @Override + protected Object doRemove(Object id) { + Assert.notNull(id, "'id' must not be null"); + return this.messageStoreRegion.remove(id); + } + + @Override + protected Collection doListKeys(String keyPattern) { + Assert.hasText(keyPattern, "'keyPattern' must not be empty"); + Collection keys = this.messageStoreRegion.keySet(); + List keyList = new ArrayList(); + for (Object key : keys) { + String keyValue = key.toString(); + if (PatternMatchUtils.simpleMatch(keyPattern, keyValue)){ + keyList.add(keyValue); + } + } + return keyList; } } diff --git a/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/KeyValueMessageGroup.java b/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/KeyValueMessageGroup.java deleted file mode 100644 index 97ded86221..0000000000 --- a/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/KeyValueMessageGroup.java +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Copyright 2002-2011 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.gemfire.store; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentMap; - -import org.springframework.integration.Message; -import org.springframework.integration.store.MessageGroup; - -/** - * A {@link org.springframework.integration.store.MessageGroup} that manipulates keys and values to provide persistence. - * Responsible for managing one group's messages as a {@link org.springframework.integration.store.MessageGroup}. - * - * @author Josh Long - * @since 2.1 - */ -@SuppressWarnings("serial") -public class KeyValueMessageGroup implements MessageGroup, Serializable { - - /** - * this should not be persisted. it's passed in through {@link KeyValueMessageGroupStore}, which has the reference to the {@link java.util.concurrent.ConcurrentMap} instance that should be set here - */ - private transient Map> marked; - - /** - * this should not be persisted. it's passed in through {@link KeyValueMessageGroupStore}, which has the reference to the {@link java.util.concurrent.ConcurrentMap} instance that should be set here - */ - private transient Map> unmarked; - - /** - * the #groupId is the unique ID to associate this aggregation of {@link org.springframework.integration.Message}s - */ - private Object groupId; - - /** - * passed in through the {@link org.springframework.integration.store.MessageGroupStore} - */ - private long timestamp; - - - /** - * default javabean ctor (so that this object plays well as a {@link java.io.Serializable} object) - */ - public KeyValueMessageGroup() { - } - - public KeyValueMessageGroup(Object groupId) { - this(groupId, System.currentTimeMillis(), null, null); - } - - public KeyValueMessageGroup(Object groupId, long timestamp, - ConcurrentMap> marked, - ConcurrentMap> unmarked) { - this.groupId = groupId; - this.timestamp = timestamp; - this.marked = marked; - this.unmarked = unmarked; - } - - public KeyValueMessageGroup(Object groupId, - ConcurrentMap> marked, - ConcurrentMap> unmarked) { - this(groupId, System.currentTimeMillis(), marked, unmarked); - } - - - @Override - public int hashCode() { - return groupId.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof KeyValueMessageGroup) { - Object otherGroupId = ((KeyValueMessageGroup) obj).getGroupId(); - return getGroupId().equals(otherGroupId); - } - return false; - } - - public void setUnmarked(Map> unmarked) { - this.unmarked = unmarked; - } - - public void setMarked( Map> marked) { - this.marked = marked; - } - - /** - * @return the timestamp (milliseconds since epoch) associated with the creation of this group - */ - public long getTimestamp() { - return timestamp; - } - - /** - * Query if the message can be added. - */ - public boolean canAdd(Message message) { - return !isMember(message); - } - - /** - * Add this {@link org.springframework.integration.Message} to the - * {@link org.springframework.integration.store.MessageGroup}, delegating in this case to the {@link #unmarked} field - * - * @param message the {@link org.springframework.integration.Message} you are adding to the {@link java.util.Map} - */ - public void add(Message message) { - if (isMember(message)) { - return; - } - - String unmarkedKey = this.unmarkedKey(message); - this.unmarked.put(unmarkedKey, (Message) message); - } - - /** - * the only reason we differentiate the keys is so that conceptually you could use the same {@link java.util.Map} instance for both marked and unmarked messages. - * - * This method simply differentiates the key, building on {@link #baseKey(org.springframework.integration.Message)}'s return value - * - * @param msg the {@link org.springframework.integration.Message} from which the key should be generated. - * @return a String to be used as a key - */ - protected String markedKey(Message msg) { - return baseKey(msg) + "-m"; - } - - /** - * the only reason we differentiate the keys is so that conceptually you could use the same {@link java.util.Map} instance for both marked and unmarked messages. - * - * This method simply differentiates the key, building on {@link #baseKey(org.springframework.integration.Message)}'s return value - * - * @param msg the {@link org.springframework.integration.Message} from which the key should be generated. - * @return a String to be used as a key - */ - protected String unmarkedKey(Message msg) { - return baseKey(msg) + "-u"; - } - - /** - * Removes this {@link org.springframework.integration.Message} from this {@link org.springframework.integration.store.MessageGroup}'s memory - * - * @param message the message to remove - */ - public void remove(Message message) { - if (unmarked.containsValue(message)) { - unmarked.remove(unmarkedKey(message)); - } - - if (marked.containsValue(message)) { - marked.remove(markedKey(message)); - } - } - - /** - * the groupKey is based on the groupID and it sits at the beginning of all the keys for this {@link org.springframework.integration.store.MessageGroup}s keys - * - * @return a string based on {@link #getGroupId()} - */ - protected String groupKey() { - return (getGroupId()).toString(); - } - - protected String baseKey(Message msg) { - String groupKey = groupKey(); - UUID id = msg.getHeaders().getId(); - Integer sn = msg.getHeaders().getSequenceNumber(); - Integer ss = msg.getHeaders().getSequenceSize(); - - return String.format("%s-%s-%s-%s", groupKey, id.toString(), - sn.toString(), ss.toString()); - } - - public Collection> getUnmarked() { - return getMessagesForMessageGroup(this.unmarked); - } - - /** - * this method will be used to discover all the messages for a given group in a {@link com.gemstone.gemfire.cache.Region} - * - * @param region the region from which we're hoping to discover these {@link org.springframework.integration.Message}s - * @return a collection of messages - */ - protected Collection> getMessagesForMessageGroup( - Map> region) { - try { - String groupMsgKey = groupKey(); - Collection> msgs = new ArrayList>(); - - for (String k : region.keySet()) { - if (k.startsWith(groupMsgKey)) { - msgs.add(region.get(k)); - } - } - - return msgs; - } catch (Throwable th) { - throw new RuntimeException(th); - } - } - - public Collection> getMarked() { - return getMessagesForMessageGroup(this.marked); - } - - /** - * @return the key that links these messages together - */ - public Object getGroupId() { - return groupId; - } - - /** - * @return true if the group is complete (i.e. no more messages are expected to be added) - */ - public boolean isComplete() { - if (size() == 0) { - return true; - } - - int sequenceSize = getSequenceSize(); - - return (sequenceSize > 0) && (sequenceSize == size()); - } - - public int getSequenceSize() { - if (size() == 0) { - return 0; - } - - return getOne().getHeaders().getSequenceSize(); - } - - /** - * Mark the given message in this group. If the message is not part of this group then this call has no effect. - * - * @param messageToMark the message that should be marked - */ - public void mark(Message messageToMark) { - if (this.unmarked.containsValue(messageToMark)) { - this.unmarked.remove(baseKey(messageToMark)); - } - - this.marked.put(baseKey(messageToMark), messageToMark); - } - - public void markAll() { - for (Message msg : getUnmarked()) - mark(msg); - } - - /** - * @return the total number of messages (marked and unmarked) in this group - */ - public int size() { - return getMarked().size() + getUnmarked().size(); - } - - /** - * @return a single message from the group - */ - public Message getOne() { - if (!this.unmarked.isEmpty()) { - String aKey = this.unmarked.keySet().iterator().next(); - - return this.unmarked.get(aKey); - } - - return 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. - * - * @param message the message to test for candidacy - * - * @return whether or not the message is a member of the group - * - */ - protected boolean isMember(Message message) { - if (size() == 0) { - return false; - } - - Integer messageSequenceNumber = message.getHeaders().getSequenceNumber(); - - if ((messageSequenceNumber != null) && (messageSequenceNumber > 0)) { - Integer messageSequenceSize = message.getHeaders().getSequenceSize(); - - if (!messageSequenceSize.equals(getSequenceSize())) { - return true; - } else { - if (containsSequenceNumber(getUnmarked(), messageSequenceNumber) || - containsSequenceNumber(getUnmarked(), - messageSequenceNumber)) { - return true; - } - } - } - - return false; - } - - protected boolean containsSequenceNumber(Collection> messages, - Integer messageSequenceNumber) { - for (Message member : messages) { - Integer memberSequenceNumber = member.getHeaders() - .getSequenceNumber(); - - if (messageSequenceNumber.equals(memberSequenceNumber)) { - return true; - } - } - - return false; - } -} diff --git a/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/KeyValueMessageGroupStore.java b/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/KeyValueMessageGroupStore.java deleted file mode 100644 index 2fd6ce34ce..0000000000 --- a/spring-integration-gemfire/src/main/java/org/springframework/integration/gemfire/store/KeyValueMessageGroupStore.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2002-2011 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.gemfire.store; - -import org.springframework.integration.Message; -import org.springframework.integration.store.AbstractMessageGroupStore; -import org.springframework.integration.store.MessageGroup; -import org.springframework.util.Assert; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; - -/** - * Provides an implementation of {@link org.springframework.integration.store.MessageGroupStore} that delegates to a backend Gemfire instance. - * Gemfire holds keys and values. This class provides a strategy to hold objects. - * - * @author Josh Long - * @since 2.1 - */ -public class KeyValueMessageGroupStore extends AbstractMessageGroupStore { - - /** - * Required {@link com.gemstone.gemfire.cache.Region} to managed the association of groups => {@link KeyValueMessageGroup} - */ - protected Map groupIdToMessageGroup; - - /** - * Required {@link com.gemstone.gemfire.cache.Region} to manage the #unmarked data - */ - protected Map> unmarked; - - /** - * Required {@link com.gemstone.gemfire.cache.Region} to manage the #marked data - */ - protected Map> marked; - - - /** - * Create a KeyValueMessageGroupStore with two backing regions to handle the state management. - * - * @param groupIdToMessageGroup the region to associate - * @param marked the collection that will hold which messages are marked (delivered) - * @param unmarked the collection that holds which messages are unmarked (not yet delivered) - */ - public KeyValueMessageGroupStore(Map groupIdToMessageGroup, Map> marked, Map> unmarked) { - this.marked = marked; - this.unmarked = unmarked; - this.groupIdToMessageGroup = groupIdToMessageGroup; - } - - - public MessageGroup getMessageGroup(Object groupId) { - Assert.notNull(groupId, "'groupId' must not be null"); - return this.getMessageGroupInternal(groupId); - } - - public MessageGroup addMessageToGroup(Object groupId, Message message) { - KeyValueMessageGroup group = getMessageGroupInternal(groupId); - group.add(message); - return group; - } - - public MessageGroup markMessageGroup(MessageGroup group) { - Object groupId = group.getGroupId(); - KeyValueMessageGroup internal = getMessageGroupInternal(groupId); - internal.markAll(); - return internal; - } - - public void removeMessageGroup(Object groupId) { - groupIdToMessageGroup.remove(groupId); - } - - public MessageGroup removeMessageFromGroup(Object key, Message messageToRemove) { - KeyValueMessageGroup group = getMessageGroupInternal(key); - group.remove(messageToRemove); - return group; - } - - public MessageGroup markMessageFromGroup(Object key, Message messageToMark) { - KeyValueMessageGroup group = getMessageGroupInternal(key); - group.mark(messageToMark); - return group; - } - - @Override - public Iterator iterator() { - return new HashSet(groupIdToMessageGroup.values()).iterator(); - } - - protected KeyValueMessageGroup ensureMessageGroupHasReferencesToRegions(KeyValueMessageGroup keyValueMessageGroup) { - if (keyValueMessageGroup == null) { - return null; - } - keyValueMessageGroup.setMarked(this.marked); - keyValueMessageGroup.setUnmarked(this.unmarked); - return keyValueMessageGroup; - } - - protected KeyValueMessageGroup getMessageGroupInternal(Object groupId) { - if (!groupIdToMessageGroup.containsKey(groupId)) { - groupIdToMessageGroup.put(groupId, new KeyValueMessageGroup(groupId)); - } - return ensureMessageGroupHasReferencesToRegions(groupIdToMessageGroup.get( groupId)); - } - -} \ No newline at end of file diff --git a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireGroupStoreTests.java b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireGroupStoreTests.java new file mode 100644 index 0000000000..1997edab18 --- /dev/null +++ b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireGroupStoreTests.java @@ -0,0 +1,358 @@ +/* + * Copyright 2007-2011 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.gemfire.store; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import junit.framework.AssertionFailedError; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.data.gemfire.CacheFactoryBean; +import org.springframework.integration.Message; +import org.springframework.integration.MessageChannel; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.message.GenericMessage; +import org.springframework.integration.store.MessageGroup; +import org.springframework.integration.store.SimpleMessageGroup; +import org.springframework.integration.support.MessageBuilder; + +import com.gemstone.gemfire.cache.Cache; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Oleg Zhurakousky + * + */ +public class GemfireGroupStoreTests { + + private Cache cache; + + @Test + public void testNonExistingEmptyMessageGroup() throws Exception{ + GemfireMessageStore store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + MessageGroup messageGroup = store.getMessageGroup(1); + assertNotNull(messageGroup); + assertTrue(messageGroup instanceof SimpleMessageGroup); + assertEquals(0, messageGroup.size()); + } + + @Test + public void testMessageGroupWithAddedMessage() throws Exception{ + GemfireMessageStore store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + MessageGroup messageGroup = store.getMessageGroup(1); + Message message = new GenericMessage("Hello"); + messageGroup = store.addMessageToGroup(1, message); + assertEquals(1, messageGroup.size()); + + // make sure the store is properly rebuild from Gemfire + store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + + messageGroup = store.getMessageGroup(1); + assertEquals(1, messageGroup.size()); + } + + @Test + public void testRemoveMessageGroup() throws Exception{ + GemfireMessageStore store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + MessageGroup messageGroup = store.getMessageGroup(1); + Message message = new GenericMessage("Hello"); + messageGroup = store.addMessageToGroup(messageGroup.getGroupId(), message); + assertEquals(1, messageGroup.size()); + + store.removeMessageGroup(1); + MessageGroup messageGroupA = store.getMessageGroup(1); + assertNotSame(messageGroup, messageGroupA); + assertEquals(0, messageGroupA.getMarked().size()); + assertEquals(0, messageGroupA.getUnmarked().size()); + assertEquals(0, messageGroupA.size()); + + // make sure the store is properly rebuild from Gemfire + store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + + messageGroup = store.getMessageGroup(1); + + assertEquals(0, messageGroup.getMarked().size()); + assertEquals(0, messageGroup.getUnmarked().size()); + assertEquals(0, messageGroup.size()); + } + + @Test + public void testRemoveMessageFromTheGroup() throws Exception{ + GemfireMessageStore store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + MessageGroup messageGroup = store.getMessageGroup(1); + Message message = new GenericMessage("2"); + store.addMessageToGroup(messageGroup.getGroupId(), new GenericMessage("1")); + store.addMessageToGroup(messageGroup.getGroupId(), message); + messageGroup = store.addMessageToGroup(messageGroup.getGroupId(), new GenericMessage("3")); + assertEquals(3, messageGroup.size()); + + messageGroup = store.removeMessageFromGroup(1, message); + assertEquals(2, messageGroup.size()); + + // make sure the store is properly rebuild from Gemfire + store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + + messageGroup = store.getMessageGroup(1); + assertEquals(2, messageGroup.size()); + + } + + @Test + public void testMarkAllMessagesInMessageGroup() throws Exception{ + GemfireMessageStore store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + MessageGroup messageGroup = store.getMessageGroup(1); + store.addMessageToGroup(messageGroup.getGroupId(), new GenericMessage("1")); + store.addMessageToGroup(messageGroup.getGroupId(), new GenericMessage("2")); + messageGroup = store.addMessageToGroup(messageGroup.getGroupId(), new GenericMessage("3")); + + assertEquals(3, messageGroup.getUnmarked().size()); + assertEquals(0, messageGroup.getMarked().size()); + messageGroup = store.markMessageGroup(messageGroup); + + assertEquals(0, messageGroup.getUnmarked().size()); + assertEquals(3, messageGroup.getMarked().size()); + + // make sure the store is properly rebuild from Gemfire + store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + + messageGroup = store.getMessageGroup(1); + assertEquals(0, messageGroup.getUnmarked().size()); + assertEquals(3, messageGroup.getMarked().size()); + } + + @Test + public void testRemoveNonExistingMessageFromTheGroup() throws Exception{ + GemfireMessageStore store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + MessageGroup messageGroup = store.getMessageGroup(1); + store.addMessageToGroup(messageGroup.getGroupId(), new GenericMessage("1")); + store.removeMessageFromGroup(1, new GenericMessage("2")); + } + + @Test + public void testRemoveNonExistingMessageFromNonExistingTheGroup() throws Exception{ + GemfireMessageStore store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + store.removeMessageFromGroup(1, new GenericMessage("2")); + } + + @Test + public void testMarkMessageInMessageGroup() throws Exception{ + GemfireMessageStore store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + MessageGroup messageGroup = store.getMessageGroup(1); + Message messageToMark = new GenericMessage("1"); + store.addMessageToGroup(messageGroup.getGroupId(), messageToMark); + store.addMessageToGroup(messageGroup.getGroupId(), new GenericMessage("2")); + messageGroup = store.addMessageToGroup(messageGroup.getGroupId(), new GenericMessage("3")); + + assertEquals(3, messageGroup.getUnmarked().size()); + assertEquals(0, messageGroup.getMarked().size()); + messageGroup = store.markMessageFromGroup(1, messageToMark); + assertEquals(2, messageGroup.getUnmarked().size()); + assertEquals(1, messageGroup.getMarked().size()); + + // make sure the store is properly rebuild from Gemfire + store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + + messageGroup = store.getMessageGroup(1); + assertEquals(2, messageGroup.getUnmarked().size()); + assertEquals(1, messageGroup.getMarked().size()); + } + + @Test + public void testCompleteMessageGroup() throws Exception{ + GemfireMessageStore store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + MessageGroup messageGroup = store.getMessageGroup(1); + Message messageToMark = new GenericMessage("1"); + store.addMessageToGroup(messageGroup.getGroupId(), messageToMark); + store.completeGroup(messageGroup.getGroupId()); + messageGroup = store.getMessageGroup(1); + assertTrue(messageGroup.isComplete()); + } + + @Test + public void testLastReleasedSequenceNumber() throws Exception{ + GemfireMessageStore store = new GemfireMessageStore(this.cache); + store.afterPropertiesSet(); + MessageGroup messageGroup = store.getMessageGroup(1); + Message messageToMark = new GenericMessage("1"); + store.addMessageToGroup(messageGroup.getGroupId(), messageToMark); + store.setLastReleasedSequenceNumberForGroup(messageGroup.getGroupId(), 5); + messageGroup = store.getMessageGroup(1); + assertEquals(5, messageGroup.getLastReleasedMessageSequenceNumber()); + } + + @Test + public void testMultipleInstancesOfGroupStore() throws Exception{ + GemfireMessageStore store1 = new GemfireMessageStore(this.cache); + store1.afterPropertiesSet(); + + GemfireMessageStore store2 = new GemfireMessageStore(this.cache); + store2.afterPropertiesSet(); + + Message message = new GenericMessage("1"); + store1.addMessageToGroup(1, message); + MessageGroup messageGroup = store2.addMessageToGroup(1, new GenericMessage("2")); + + assertEquals(2, messageGroup.getUnmarked().size()); + assertEquals(0, messageGroup.getMarked().size()); + + GemfireMessageStore store3 = new GemfireMessageStore(this.cache); + store3.afterPropertiesSet(); + + messageGroup = store3.markMessageFromGroup(1, message); + + assertEquals(1, messageGroup.getUnmarked().size()); + assertEquals(1, messageGroup.getMarked().size()); + } + + @Test + public void testIteratorOfMessageGroups() throws Exception{ + GemfireMessageStore store1 = new GemfireMessageStore(this.cache); + store1.afterPropertiesSet(); + GemfireMessageStore store2 = new GemfireMessageStore(this.cache); + store2.afterPropertiesSet(); + + store1.addMessageToGroup(1, new GenericMessage("1")); + store2.addMessageToGroup(2, new GenericMessage("2")); + store1.addMessageToGroup(3, new GenericMessage("3")); + store2.addMessageToGroup(3, new GenericMessage("3A")); + + Iterator messageGroups = store1.iterator(); + int counter = 0; + while (messageGroups.hasNext()) { + messageGroups.next(); + counter++; + } + assertEquals(3, counter); + + store2.removeMessageGroup(3); + + messageGroups = store1.iterator(); + counter = 0; + while (messageGroups.hasNext()) { + messageGroups.next(); + counter++; + } + assertEquals(2, counter); + } + + @Test + @Ignore + public void testConcurrentModifications() throws Exception{ + + final GemfireMessageStore store1 = new GemfireMessageStore(this.cache); + store1.afterPropertiesSet(); + final GemfireMessageStore store2 = new GemfireMessageStore(this.cache); + store2.afterPropertiesSet(); + + final Message message = new GenericMessage("1"); + + ExecutorService executor = null; + + final List failures = new ArrayList(); + + for (int i = 0; i < 100; i++) { + executor = Executors.newCachedThreadPool(); + + executor.execute(new Runnable() { + public void run() { + MessageGroup group = store1.addMessageToGroup(1, message); + if (group.getUnmarked().size() != 1){ + failures.add("ADD"); + throw new AssertionFailedError("Failed on ADD"); + } + } + }); + executor.execute(new Runnable() { + public void run() { + MessageGroup group = store2.removeMessageFromGroup(1, message); + if (group.getUnmarked().size() != 0){ + failures.add("REMOVE"); + throw new AssertionFailedError("Failed on Remove"); + } + } + }); + + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + store2.removeMessageFromGroup(1, message); // ensures that if ADD thread executed after REMOVE, the store is empty for the next cycle + } + assertTrue(failures.size() == 0); + } + + @Test + public void testWithAggregatorWithShutdown(){ + + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("gemfire-aggregator-config.xml", this.getClass()); + MessageChannel input = context.getBean("inputChannel", MessageChannel.class); + QueueChannel output = context.getBean("outputChannel", QueueChannel.class); + + Message m1 = MessageBuilder.withPayload("1").setSequenceNumber(1).setSequenceSize(3).setCorrelationId(1).build(); + Message m2 = MessageBuilder.withPayload("2").setSequenceNumber(2).setSequenceSize(3).setCorrelationId(1).build(); + input.send(m1); + assertNull(output.receive(1000)); + input.send(m2); + assertNull(output.receive(1000)); + + context = new ClassPathXmlApplicationContext("gemfire-aggregator-config-a.xml", this.getClass()); + MessageChannel inputA = context.getBean("inputChannel", MessageChannel.class); + QueueChannel outputA = context.getBean("outputChannel", QueueChannel.class); + + Message m3 = MessageBuilder.withPayload("3").setSequenceNumber(3).setSequenceSize(3).setCorrelationId(1).build(); + inputA.send(m3); + assertNotNull(outputA.receive(1000)); + } + + @Before + public void init() throws Exception{ + CacheFactoryBean cacheFactoryBean = new CacheFactoryBean(); + cacheFactoryBean.afterPropertiesSet(); + this.cache = (Cache)cacheFactoryBean.getObject(); + } + + @After + public void cleanup(){ + this.cache.close(); + } + +} diff --git a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStoreTestConfiguration.java b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStoreTestConfiguration.java deleted file mode 100644 index f0a6f7ed2b..0000000000 --- a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStoreTestConfiguration.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2002-2011 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.gemfire.store; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.SmartLifecycle; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.gemfire.CacheFactoryBean; -import org.springframework.data.gemfire.RegionFactoryBean; -import org.springframework.integration.Message; -import org.springframework.integration.MessageChannel; -import org.springframework.integration.aggregator.CorrelationStrategy; -import org.springframework.integration.aggregator.HeaderAttributeCorrelationStrategy; -import org.springframework.integration.aggregator.ReleaseStrategy; -import org.springframework.integration.aggregator.SequenceSizeReleaseStrategy; -import org.springframework.integration.annotation.ServiceActivator; -import org.springframework.integration.core.MessagingTemplate; -import org.springframework.integration.gemfire.store.KeyValueMessageGroup; -import org.springframework.integration.gemfire.store.KeyValueMessageGroupStore; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.util.Assert; - -import com.gemstone.gemfire.cache.Cache; -import com.gemstone.gemfire.cache.Region; - -/** - * Our aggregator needs a - * {@link org.springframework.integration.gemfire.store.KeyValueMessageGroupStore} - * . This handles configuration of the ancillary objects. - * - * @author Josh Long - * @since 2.1 - */ -@Configuration -public class GemfireMessageGroupStoreTestConfiguration { - - public static List LIST_OF_STRINGS = Arrays.asList("1,2,3,4,5".split(",")); - - static private Log log = LogFactory.getLog(GemfireMessageGroupStoreTestConfiguration.class); - - @Value("${correlation-header}") - private String correlationHeader; - - @Bean - public Cache cache() throws Throwable { - CacheFactoryBean cacheFactoryBean = new CacheFactoryBean(); - cacheFactoryBean.afterPropertiesSet(); - return (Cache)cacheFactoryBean.getObject(); - } - - @Bean - public Region messageGroupRegion() throws Throwable { - RegionFactoryBean regionFactoryBean = new RegionFactoryBean(); - regionFactoryBean.setName("messageGroupRegion"); - regionFactoryBean.setCache(cache()); - regionFactoryBean.afterPropertiesSet(); - return regionFactoryBean.getObject(); - } - - @Bean - public Region> unmarkedRegion() throws Throwable { - RegionFactoryBean> regionFactoryBean = new RegionFactoryBean>(); - regionFactoryBean.setName("unmarkedRegion"); - regionFactoryBean.setCache(cache()); - regionFactoryBean.afterPropertiesSet(); - return regionFactoryBean.getObject(); - } - - @Bean - public Region> markedRegion() throws Throwable { - RegionFactoryBean> regionFactoryBean = new RegionFactoryBean>(); - regionFactoryBean.setName("markedRegion"); - regionFactoryBean.setCache(cache()); - regionFactoryBean.afterPropertiesSet(); - return regionFactoryBean.getObject(); - } - - @Bean(name = "messageGroupStoreActivator") - public FakeMessageConsumer serviceActivator() { - return new FakeMessageConsumer(); - } - - @Bean - public ReleaseStrategy releaseStrategy() { - return new SequenceSizeReleaseStrategy(false); - } - - @Bean - public CorrelationStrategy correlationStrategy() { - return new HeaderAttributeCorrelationStrategy(this.correlationHeader); - } - - @Bean - public KeyValueMessageGroupStore gemfireMessageGroupStore() throws Throwable { - return new KeyValueMessageGroupStore(messageGroupRegion(), markedRegion(), unmarkedRegion()); - } - - @Bean - public FakeMessageProducer producer() { - return new FakeMessageProducer(); - } - - static public class FakeMessageConsumer { - - private List> batches = new ArrayList>(); - - public List> getBatches() { - return this.batches; - } - - @ServiceActivator - public void activateAsMessagesArriveInBatches(Message> msg) throws Throwable { - Collection payloads = msg.getPayload(); - batches.add(payloads); - - if (log.isDebugEnabled()) { - log.debug(payloads); - } - - } - - } - - static public class FakeMessageProducer implements InitializingBean, SmartLifecycle { - public boolean isAutoStartup() { - return false; - } - - public void stop(Runnable callback) { - stop(); - callback.run(); - } - - public int getPhase() { - return 0; - } - - @Autowired - @Qualifier("i") - private MessageChannel messageChannel; - - private MessagingTemplate messagingTemplate = new MessagingTemplate(); - - private volatile boolean running = false; - - @Value("${correlation-header}") - private String correlationHeader; - - public void sendManyMessages(int correlationValue, Collection lines) throws Throwable { - Assert.notNull(lines, "the collection must be non-null"); - Assert.notEmpty(lines, "the collection must not be empty"); - int ctr = 0; - int size = lines.size(); - for (String l : lines) { - Message msg = MessageBuilder.withPayload(l).setCorrelationId(this.correlationHeader) - .setHeader(this.correlationHeader, correlationValue).setSequenceNumber(++ctr) - .setSequenceSize(size).build(); - this.messagingTemplate.send(msg); - } - } - - public void afterPropertiesSet() throws Exception { - this.messagingTemplate.setDefaultChannel(this.messageChannel); - } - - public void start() { - running = true; - for (int i = 0; i < 10; i++) { - try { - sendManyMessages(i, LIST_OF_STRINGS); - } - catch (Throwable throwable) { - throw new RuntimeException(throwable); - } - } - - } - - public void stop() { - running = false; - } - - public boolean isRunning() { - return running; - } - - } -} \ No newline at end of file diff --git a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStoreTests-context.xml b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStoreTests-context.xml deleted file mode 100644 index 466eb0878d..0000000000 --- a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStoreTests-context.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStoreTests.java b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStoreTests.java deleted file mode 100644 index 4899af3f97..0000000000 --- a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageGroupStoreTests.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2002-2011 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.gemfire.store; - -import static org.junit.Assert.assertEquals; - -import java.util.Collection; -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -/** - * Tests the Gemfire - * {@link org.springframework.integration.store.MessageGroupStore} - * implementation, - * {@link org.springframework.integration.gemfire.store.GemfireMessageGroupStore} - * . - *

- * It tests the {@link org.springframework.integration.store.MessageGroupStore} - * by sending 10 batches of letters (all of the same width), and then counting - * on the other end that indeed all 10 batches arrived and that all letters - * expected are there. * - * - * @author Josh Long - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class GemfireMessageGroupStoreTests { - - @Autowired - private GemfireMessageGroupStoreTestConfiguration.FakeMessageConsumer consumer; - @Autowired - private GemfireMessageGroupStoreTestConfiguration.FakeMessageProducer producer; - - private List letters = GemfireMessageGroupStoreTestConfiguration.LIST_OF_STRINGS; - - private int maxSize = 10; - - @Test - public void testGemfireMessageGroupStore() throws Exception { - producer.afterPropertiesSet(); - producer.start(); - List> batches = consumer.getBatches(); - assertEquals(maxSize, batches.size()); - for (Collection collection : batches) { - Assert.assertTrue(letters.size() == collection.size()); - for (String c : this.letters) { - Assert.assertTrue(collection.contains(c)); - } - for (Object o : collection) { - Assert.assertTrue(o instanceof String); - } - } - producer.stop(); - } -} \ No newline at end of file diff --git a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageStoreTests.java b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageStoreTests.java index 254a9d6850..2245c8f2af 100644 --- a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageStoreTests.java +++ b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/GemfireMessageStoreTests.java @@ -16,20 +16,14 @@ package org.springframework.integration.gemfire.store; -import static org.junit.Assert.assertEquals; - -import java.util.UUID; - import org.junit.Test; - import org.springframework.data.gemfire.CacheFactoryBean; -import org.springframework.data.gemfire.RegionFactoryBean; import org.springframework.integration.Message; -import org.springframework.integration.store.MessageStore; import org.springframework.integration.support.MessageBuilder; import com.gemstone.gemfire.cache.Cache; -import com.gemstone.gemfire.cache.Region; + +import static org.junit.Assert.assertEquals; /** * @author Mark Fisher @@ -42,12 +36,9 @@ public class GemfireMessageStoreTests { CacheFactoryBean cacheFactoryBean = new CacheFactoryBean(); cacheFactoryBean.afterPropertiesSet(); Cache cache = (Cache)cacheFactoryBean.getObject(); - RegionFactoryBean> regionFactoryBean = new RegionFactoryBean>(); - regionFactoryBean.setName("test.addAndGetMessage"); - regionFactoryBean.setCache(cache); - regionFactoryBean.afterPropertiesSet(); - Region> region = regionFactoryBean.getObject(); - MessageStore store = new GemfireMessageStore(region); + GemfireMessageStore store = new GemfireMessageStore(cache); + store.afterPropertiesSet(); + Message message = MessageBuilder.withPayload("test").build(); store.addMessage(message); Message retrieved = store.getMessage(message.getHeaders().getId()); diff --git a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/gemfire-aggregator-config-a.xml b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/gemfire-aggregator-config-a.xml new file mode 100644 index 0000000000..a7f1e46ebe --- /dev/null +++ b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/gemfire-aggregator-config-a.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/gemfire-aggregator-config.xml b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/gemfire-aggregator-config.xml new file mode 100644 index 0000000000..311f4db787 --- /dev/null +++ b/spring-integration-gemfire/src/test/java/org/springframework/integration/gemfire/store/gemfire-aggregator-config.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageStore.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageStore.java index 5e6db1dc1f..5c2549858e 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageStore.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcMessageStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2011 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 @@ -59,6 +59,7 @@ import org.springframework.util.StringUtils; * target database type. * * @author Dave Syer + * @author Oleg Zhurakousky * @since 2.0 */ @ManagedResource @@ -80,7 +81,7 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa private static final String CREATE_MESSAGE = "INSERT into %PREFIX%MESSAGE(MESSAGE_ID, REGION, CREATED_DATE, MESSAGE_BYTES)" + " values (?, ?, ?, ?)"; - private static final String LIST_MESSAGES_BY_GROUP_KEY = "SELECT MESSAGE_ID, CREATED_DATE, GROUP_KEY, MESSAGE_BYTES, MARKED from %PREFIX%MESSAGE_GROUP where GROUP_KEY=? and REGION=? order by CREATED_DATE"; + private static final String LIST_MESSAGES_BY_GROUP_KEY = "SELECT MESSAGE_ID, CREATED_DATE, GROUP_KEY, MESSAGE_BYTES, MARKED, COMPLETE, LAST_RELEASED_SEQUENCE from %PREFIX%MESSAGE_GROUP where GROUP_KEY=? and REGION=? order by CREATED_DATE"; private static final String COUNT_ALL_GROUPS = "SELECT COUNT(GROUP_KEY) from %PREFIX%MESSAGE_GROUP where REGION=?"; @@ -91,13 +92,17 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa private static final String MARK_MESSAGES_IN_GROUP = "UPDATE %PREFIX%MESSAGE_GROUP set UPDATED_DATE=?, MARKED=1 where MARKED=0 and GROUP_KEY=? and REGION=?"; private static final String MARK_MESSAGE_IN_GROUP = "UPDATE %PREFIX%MESSAGE_GROUP set UPDATED_DATE=?, MARKED=1 where MESSAGE_ID=? and MARKED=0 and GROUP_KEY=? and REGION=?"; + + private static final String COMPLETE_GROUP = "UPDATE %PREFIX%MESSAGE_GROUP set UPDATED_DATE=?, COMPLETE=1 where GROUP_KEY=? and REGION=?"; + + private static final String UPDATE_LAST_RELEASED_SEQUENCE = "UPDATE %PREFIX%MESSAGE_GROUP set UPDATED_DATE=?, LAST_RELEASED_SEQUENCE=? where GROUP_KEY=? and REGION=?"; private static final String REMOVE_MESSAGE_FROM_GROUP = "DELETE from %PREFIX%MESSAGE_GROUP where GROUP_KEY=? and REGION=? and MESSAGE_ID=?"; private static final String DELETE_MESSAGE_GROUP = "DELETE from %PREFIX%MESSAGE_GROUP where GROUP_KEY=? and REGION=?"; - private static final String CREATE_MESSAGE_IN_GROUP = "INSERT into %PREFIX%MESSAGE_GROUP(MESSAGE_ID, REGION, CREATED_DATE, GROUP_KEY, MARKED, MESSAGE_BYTES)" - + " values (?, ?, ?, ?, 0, ?)"; + private static final String CREATE_MESSAGE_IN_GROUP = "INSERT into %PREFIX%MESSAGE_GROUP(MESSAGE_ID, REGION, CREATED_DATE, GROUP_KEY, MARKED, COMPLETE, LAST_RELEASED_SEQUENCE, MESSAGE_BYTES)" + + " values (?, ?, ?, ?, 0, 0, 0, ?)"; private static final String LIST_GROUP_KEYS = "SELECT distinct GROUP_KEY as CREATED from %PREFIX%MESSAGE_GROUP where REGION=?"; @@ -332,6 +337,9 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa final List> marked = new ArrayList>(); final List> unmarked = new ArrayList>(); final AtomicReference date = new AtomicReference(); + final AtomicReference completeFlag = new AtomicReference(); + final AtomicReference lastReleasedSequenceRef = new AtomicReference(); + jdbcTemplate.query(getQuery(LIST_MESSAGES_BY_GROUP_KEY), new Object[] { key, region }, new RowCallbackHandler() { int count = 0; @@ -346,6 +354,10 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa unmarked.add(message); } date.set(rs.getTimestamp("CREATED_DATE")); + + completeFlag.set(rs.getInt("COMPLETE") > 0); + + lastReleasedSequenceRef.set(rs.getInt("LAST_RELEASED_SEQUENCE")); } }); if (marked.isEmpty() && unmarked.isEmpty()) { @@ -353,7 +365,13 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa } Assert.state(date.get() != null, "Could not locate created date for groupId=" + groupId); long timestamp = date.get().getTime(); - return new SimpleMessageGroup(unmarked, marked, groupId, timestamp); + boolean complete = completeFlag.get().booleanValue(); + SimpleMessageGroup messageGroup = new SimpleMessageGroup(unmarked, marked, groupId, timestamp, complete); + int lastReleasedSequenceNumber = lastReleasedSequenceRef.get(); + if (lastReleasedSequenceNumber > 0){ + messageGroup.setLastReleasedMessageSequenceNumber(lastReleasedSequenceNumber); + } + return messageGroup; } public MessageGroup markMessageGroup(MessageGroup group) { @@ -421,10 +439,38 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa ps.setString(2, region); } }); - + } + + public void completeGroup(Object groupId) { + final long updatedDate = System.currentTimeMillis(); + final String groupKey = getKey(groupId); + + jdbcTemplate.update(getQuery(COMPLETE_GROUP), new PreparedStatementSetter() { + public void setValues(PreparedStatement ps) throws SQLException { + logger.debug("Completing MessageGroup: " + groupKey); + ps.setTimestamp(1, new Timestamp(updatedDate)); + ps.setString(2, groupKey); + ps.setString(3, region); + } + }); + } + + public void setLastReleasedSequenceNumberForGroup(Object groupId, final int sequenceNumber) { + Assert.notNull(groupId, "'groupId' must not be null"); + final long updatedDate = System.currentTimeMillis(); + final String groupKey = getKey(groupId); + + jdbcTemplate.update(getQuery(UPDATE_LAST_RELEASED_SEQUENCE), new PreparedStatementSetter() { + public void setValues(PreparedStatement ps) throws SQLException { + logger.debug("Updating the sequence number of the last released Message in the MessageGroup: " + groupKey); + ps.setTimestamp(1, new Timestamp(updatedDate)); + ps.setInt(2, sequenceNumber); + ps.setString(3, groupKey); + ps.setString(4, region); + } + }); } - @Override public Iterator iterator() { final Iterator iterator = jdbcTemplate.query(getQuery(LIST_GROUP_KEYS), new Object[] { region }, @@ -465,5 +511,4 @@ public class JdbcMessageStore extends AbstractMessageGroupStore implements Messa return message; } } - } diff --git a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-db2.sql b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-db2.sql index 1893dc9a6b..0aa02f9f3c 100644 --- a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-db2.sql +++ b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-db2.sql @@ -12,6 +12,8 @@ CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL, REGION VARCHAR(100), MARKED BIGINT, + COMPLETE BIGINT, + LAST_RELEASED_SEQUENCE BIGINT, CREATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP DEFAULT NULL, MESSAGE_BYTES BLOB, diff --git a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-derby.sql b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-derby.sql index 1893dc9a6b..0aa02f9f3c 100644 --- a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-derby.sql +++ b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-derby.sql @@ -12,6 +12,8 @@ CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL, REGION VARCHAR(100), MARKED BIGINT, + COMPLETE BIGINT, + LAST_RELEASED_SEQUENCE BIGINT, CREATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP DEFAULT NULL, MESSAGE_BYTES BLOB, diff --git a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-h2.sql b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-h2.sql index 1d48022272..046a436c95 100644 --- a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-h2.sql +++ b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-h2.sql @@ -12,6 +12,8 @@ CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL, REGION VARCHAR(100), MARKED BIGINT, + COMPLETE BIGINT, + LAST_RELEASED_SEQUENCE BIGINT, CREATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP DEFAULT NULL, MESSAGE_BYTES LONGVARBINARY, diff --git a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-hsqldb.sql b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-hsqldb.sql index 1d48022272..046a436c95 100644 --- a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-hsqldb.sql +++ b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-hsqldb.sql @@ -12,6 +12,8 @@ CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL, REGION VARCHAR(100), MARKED BIGINT, + COMPLETE BIGINT, + LAST_RELEASED_SEQUENCE BIGINT, CREATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP DEFAULT NULL, MESSAGE_BYTES LONGVARBINARY, diff --git a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-mysql.sql b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-mysql.sql index d43468164a..f9e3062ccd 100644 --- a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-mysql.sql +++ b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-mysql.sql @@ -12,6 +12,8 @@ CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL, REGION VARCHAR(100), MARKED BIGINT, + COMPLETE BIGINT, + LAST_RELEASED_SEQUENCE BIGINT, CREATED_DATE DATETIME NOT NULL, UPDATED_DATE DATETIME DEFAULT NULL, MESSAGE_BYTES BLOB, diff --git a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-oracle10g.sql b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-oracle10g.sql index b549ce9004..6937ed165e 100644 --- a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-oracle10g.sql +++ b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-oracle10g.sql @@ -12,6 +12,8 @@ CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL, REGION VARCHAR2(100), MARKED NUMBER(19,0), + COMPLETE NUMBER(19,0), + LAST_RELEASED_SEQUENCE NUMBER(19,0), CREATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP DEFAULT NULL, MESSAGE_BYTES BLOB, diff --git a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-postgresql.sql b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-postgresql.sql index 7e7654c24a..3e7917e848 100644 --- a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-postgresql.sql +++ b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-postgresql.sql @@ -12,6 +12,8 @@ CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL, REGION VARCHAR(100), MARKED BIGINT, + COMPLETE BIGINT, + LAST_RELEASED_SEQUENCE BIGINT, CREATED_DATE TIMESTAMP NOT NULL, UPDATED_DATE TIMESTAMP DEFAULT NULL, MESSAGE_BYTES BYTEA, diff --git a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-sqlserver.sql b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-sqlserver.sql index 1c2fccb438..73b3e8195c 100644 --- a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-sqlserver.sql +++ b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-sqlserver.sql @@ -12,6 +12,8 @@ CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL, REGION VARCHAR(100), MARKED BIGINT, + COMPLETE BIGINT, + LAST_RELEASED_SEQUENCE BIGINT, CREATED_DATE DATETIME NOT NULL, UPDATED_DATE DATETIME DEFAULT NULL, MESSAGE_BYTES IMAGE, diff --git a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-sybase.sql b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-sybase.sql index 1d00ea955e..4d12b8ed39 100644 --- a/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-sybase.sql +++ b/spring-integration-jdbc/src/main/resources/org/springframework/integration/jdbc/schema-sybase.sql @@ -12,6 +12,8 @@ CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL, REGION VARCHAR(100), MARKED BIGINT, + COMPLETE BIGINT, + LAST_RELEASED_SEQUENCE BIGINT, CREATED_DATE DATETIME NOT NULL, UPDATED_DATE DATETIME DEFAULT NULL, MESSAGE_BYTES IMAGE, diff --git a/spring-integration-jdbc/src/main/sql/schema.sql.vpp b/spring-integration-jdbc/src/main/sql/schema.sql.vpp index c8abfd212f..0ffb04b69d 100644 --- a/spring-integration-jdbc/src/main/sql/schema.sql.vpp +++ b/spring-integration-jdbc/src/main/sql/schema.sql.vpp @@ -12,6 +12,8 @@ CREATE TABLE INT_MESSAGE_GROUP ( GROUP_KEY CHAR(36) NOT NULL, REGION ${VARCHAR}(100), MARKED ${BIGINT}, + COMPLETE ${BIGINT}, + LAST_RELEASED_SEQUENCE ${BIGINT}, CREATED_DATE ${TIMESTAMP} NOT NULL, UPDATED_DATE ${TIMESTAMP} DEFAULT NULL, MESSAGE_BYTES ${BLOB}, diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageStoreTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageStoreTests.java index dbea45e4a8..dfe040585b 100644 --- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageStoreTests.java +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/JdbcMessageStoreTests.java @@ -199,6 +199,29 @@ public class JdbcMessageStoreTests { MessageGroup group = messageStore.getMessageGroup(groupId); assertEquals(0, group.size()); } + + @Test + @Transactional + public void testCompleteMessageGroup() throws Exception { + String groupId = "X"; + Message message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build(); + messageStore.addMessageToGroup(groupId, message); + messageStore.completeGroup(groupId); + MessageGroup group = messageStore.getMessageGroup(groupId); + assertTrue(group.isComplete()); + assertEquals(1, group.size()); + } + + @Test + @Transactional + public void testUpdateLastReleasedSequence() throws Exception { + String groupId = "X"; + Message message = MessageBuilder.withPayload("foo").setCorrelationId(groupId).build(); + messageStore.addMessageToGroup(groupId, message); + messageStore.setLastReleasedSequenceNumberForGroup(groupId, 5); + MessageGroup group = messageStore.getMessageGroup(groupId); + assertEquals(5, group.getLastReleasedMessageSequenceNumber()); + } @Test @Transactional diff --git a/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/MongoDbMessageStore.java b/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/MongoDbMessageStore.java index 58048bfea1..8327d8eb0e 100644 --- a/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/MongoDbMessageStore.java +++ b/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/MongoDbMessageStore.java @@ -68,6 +68,12 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me private final static String GROUP_ID_KEY = "_groupId"; private final static String MARKED_KEY = "_marked"; + + private final static String GROUP_COMPLETE_KEY = "_group_complete"; + + private final static String LAST_RELEASED_SEQUENCE_NUMBER = "_last_released_sequence"; + + private final static String GROUP_TIMESTAMP_KEY = "_group_timestamp"; private final static String PAYLOAD_TYPE_KEY = "_payloadType"; @@ -105,7 +111,7 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me public Message addMessage(Message message) { Assert.notNull(message, "'message' must not be null"); - this.template.insert(new MessageWrapper(message, null, false), this.collectionName); + this.template.insert(new MessageWrapper(message), this.collectionName); return message; } @@ -131,6 +137,16 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me List messageWrappers = this.template.find(whereGroupIdIs(groupId), MessageWrapper.class, this.collectionName); List> unmarkedMessages = new ArrayList>(); List> markedMessages = new ArrayList>(); + long timestamp = 0; + int lastReleasedSequenceNumber = 0; + boolean completeGroup = false; + if (messageWrappers.size() > 0){ + MessageWrapper messageWrapper = messageWrappers.get(0); + timestamp = messageWrapper.getGroupTimestamp(); + completeGroup = messageWrapper.isCompletedGroup(); + lastReleasedSequenceNumber = messageWrapper.getLastReleasedSequenceNumber(); + } + for (MessageWrapper messageWrapper : messageWrappers) { if (messageWrapper.isMarked()) { markedMessages.add(messageWrapper.getMessage()); @@ -139,13 +155,24 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me unmarkedMessages.add(messageWrapper.getMessage()); } } - return new SimpleMessageGroup(unmarkedMessages, markedMessages, groupId, System.currentTimeMillis()); + SimpleMessageGroup messageGroup = new SimpleMessageGroup(unmarkedMessages, markedMessages, groupId, timestamp, completeGroup); + if (lastReleasedSequenceNumber > 0){ + messageGroup.setLastReleasedMessageSequenceNumber(lastReleasedSequenceNumber); + } + return messageGroup; } public MessageGroup addMessageToGroup(Object groupId, Message message) { Assert.notNull(groupId, "'groupId' must not be null"); Assert.notNull(message, "'message' must not be null"); - MessageWrapper wrapper = new MessageWrapper(message, groupId, false); + MessageGroup messageGroup = this.getMessageGroup(groupId); + + MessageWrapper wrapper = new MessageWrapper(message); + wrapper.setGroupId(groupId); + wrapper.setGroupTimestamp(messageGroup.getTimestamp()); + wrapper.setCompletedGroup(messageGroup.isComplete()); + wrapper.setLastReleasedSequenceNumber(messageGroup.getLastReleasedMessageSequenceNumber()); + this.template.insert(wrapper, this.collectionName); return this.getMessageGroup(groupId); } @@ -181,7 +208,6 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me } } - @Override public Iterator iterator() { List groupedMessages = this.template.find(whereGroupIdExists(), MessageWrapper.class, this.collectionName); Map messageGroups = new HashMap(); @@ -193,7 +219,18 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me } return messageGroups.values().iterator(); } + + public void completeGroup(Object groupId) { + Update update = Update.update(GROUP_COMPLETE_KEY, true); + Query q = whereGroupIdIs(groupId); + this.template.updateFirst(q, update, this.collectionName); + } + public void setLastReleasedSequenceNumberForGroup(Object groupId, int sequenceNumber) { + Update update = Update.update(LAST_RELEASED_SEQUENCE_NUMBER, sequenceNumber); + Query q = whereGroupIdIs(groupId); + this.template.updateFirst(q, update, this.collectionName); + } /* * Common Queries @@ -236,11 +273,17 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me Message message = null; Object groupId = null; boolean marked = false; + boolean groupComplete = false; + long groupTimestamp = 0; + int lastReleasedSequenceNumber = 0; if (source instanceof MessageWrapper) { MessageWrapper wrapper = (MessageWrapper) source; message = wrapper.getMessage(); groupId = wrapper.getGroupId(); marked = wrapper.isMarked(); + groupComplete = wrapper.isCompletedGroup(); + lastReleasedSequenceNumber = wrapper.getLastReleasedSequenceNumber(); + groupTimestamp = wrapper.getGroupTimestamp(); } else { Class sourceType = (source != null) ? source.getClass() : null; @@ -249,6 +292,9 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me target.put(PAYLOAD_TYPE_KEY, message.getPayload().getClass().getName()); if (groupId != null) { target.put(GROUP_ID_KEY, groupId); + target.put(GROUP_COMPLETE_KEY, groupComplete); + target.put(LAST_RELEASED_SEQUENCE_NUMBER, lastReleasedSequenceNumber); + target.put(GROUP_TIMESTAMP_KEY, groupTimestamp); } if (marked) { target.put(MARKED_KEY, marked); @@ -280,7 +326,26 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me // using reflection to set ID and TIMESTAMP since they are immutable through MessageHeaders innerMap.put(MessageHeaders.ID, UUID.fromString((String) headers.get(MessageHeaders.ID))); innerMap.put(MessageHeaders.TIMESTAMP, headers.get(MessageHeaders.TIMESTAMP)); - MessageWrapper wrapper = new MessageWrapper(message, source.get(GROUP_ID_KEY), source.get(MARKED_KEY) != null); + Long groupTimestamp = (Long)source.get(GROUP_TIMESTAMP_KEY); + Integer lastReleasedSequenceNumber = (Integer)source.get(LAST_RELEASED_SEQUENCE_NUMBER); + Boolean completeGroup = (Boolean)source.get(GROUP_COMPLETE_KEY); + + MessageWrapper wrapper = new MessageWrapper(message); + + if (source.containsField(GROUP_ID_KEY)){ + wrapper.setGroupId(source.get(GROUP_ID_KEY)); + } + if (groupTimestamp != null){ + wrapper.setGroupTimestamp(groupTimestamp); + } + if (lastReleasedSequenceNumber != null){ + wrapper.setLastReleasedSequenceNumber(lastReleasedSequenceNumber); + } + + wrapper.setMarked(source.get(MARKED_KEY) != null); + + wrapper.setCompletedGroup(completeGroup.booleanValue()); + return (S) wrapper; } return null; @@ -307,16 +372,32 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me */ private static final class MessageWrapper { - private final Object groupId; + private volatile Object groupId; - private final boolean marked; + private volatile boolean marked; private final Message message; + + private volatile long groupTimestamp; + + private volatile int lastReleasedSequenceNumber; - public MessageWrapper(Message message, Object groupId, boolean marked) { - this.marked = marked; + private volatile boolean completedGroup; + + public MessageWrapper(Message message) { this.message = message; - this.groupId = groupId; + } + + public int getLastReleasedSequenceNumber() { + return lastReleasedSequenceNumber; + } + + public long getGroupTimestamp() { + return groupTimestamp; + } + + public boolean isCompletedGroup() { + return completedGroup; } public Object getGroupId() { @@ -330,6 +411,25 @@ public class MongoDbMessageStore extends AbstractMessageGroupStore implements Me public Message getMessage() { return message; } - } + + public void setGroupId(Object groupId) { + this.groupId = groupId; + } + public void setMarked(boolean marked) { + this.marked = marked; + } + + public void setGroupTimestamp(long groupTimestamp) { + this.groupTimestamp = groupTimestamp; + } + + public void setLastReleasedSequenceNumber(int lastReleasedSequenceNumber) { + this.lastReleasedSequenceNumber = lastReleasedSequenceNumber; + } + + public void setCompletedGroup(boolean completedGroup) { + this.completedGroup = completedGroup; + } + } } diff --git a/spring-integration-mongodb/src/test/java/org/springframework/integration/mongodb/store/MongoDbMessageGroupStoreTests.java b/spring-integration-mongodb/src/test/java/org/springframework/integration/mongodb/store/MongoDbMessageGroupStoreTests.java index e407761afe..99786f5965 100644 --- a/spring-integration-mongodb/src/test/java/org/springframework/integration/mongodb/store/MongoDbMessageGroupStoreTests.java +++ b/spring-integration-mongodb/src/test/java/org/springframework/integration/mongodb/store/MongoDbMessageGroupStoreTests.java @@ -15,15 +15,8 @@ */ package org.springframework.integration.mongodb.store; -import java.util.ArrayList; import java.util.Iterator; -import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import junit.framework.AssertionFailedError; import org.junit.Test; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -128,6 +121,34 @@ public class MongoDbMessageGroupStoreTests extends MongoDbAvailableTests { } + @Test + @MongoDbAvailable + public void testCompleteMessageGroup() throws Exception{ + MongoDbFactory mongoDbFactory = this.prepareMongoFactory(); + MongoDbMessageStore store = new MongoDbMessageStore(mongoDbFactory); + + MessageGroup messageGroup = store.getMessageGroup(1); + Message message = new GenericMessage("Hello"); + store.addMessageToGroup(messageGroup.getGroupId(), message); + store.completeGroup(messageGroup.getGroupId()); + messageGroup = store.getMessageGroup(1); + assertTrue(messageGroup.isComplete()); + } + + @Test + @MongoDbAvailable + public void testLastReleasedSequenceNumber() throws Exception{ + MongoDbFactory mongoDbFactory = this.prepareMongoFactory(); + MongoDbMessageStore store = new MongoDbMessageStore(mongoDbFactory); + + MessageGroup messageGroup = store.getMessageGroup(1); + Message message = new GenericMessage("Hello"); + store.addMessageToGroup(messageGroup.getGroupId(), message); + store.setLastReleasedSequenceNumberForGroup(messageGroup.getGroupId(), 5); + messageGroup = store.getMessageGroup(1); + assertEquals(5, messageGroup.getLastReleasedMessageSequenceNumber()); + } + @Test @MongoDbAvailable public void testRemoveMessageFromTheGroup() throws Exception{ diff --git a/spring-integration-redis/si-redis.conf b/spring-integration-redis/si-redis.conf index c14c96d78b..be34f59102 100644 --- a/spring-integration-redis/si-redis.conf +++ b/spring-integration-redis/si-redis.conf @@ -1,5 +1,5 @@ # minimal config -daemonize yes +#daemonize yes bind 127.0.0.1 loglevel notice port 7379 \ No newline at end of file diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/store/RedisMessageStore.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/store/RedisMessageStore.java index a91b9d5866..9e373f2c91 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/store/RedisMessageStore.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/store/RedisMessageStore.java @@ -16,278 +16,82 @@ package org.springframework.integration.redis.store; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; import java.util.Set; -import java.util.UUID; -import org.springframework.dao.DataAccessException; -import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.BoundListOperations; -import org.springframework.data.redis.core.BoundSetOperations; import org.springframework.data.redis.core.BoundValueOperations; -import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.springframework.integration.Message; -import org.springframework.integration.store.AbstractMessageGroupStore; -import org.springframework.integration.store.MessageGroup; +import org.springframework.integration.store.AbstractKeyValueMessageStore; import org.springframework.integration.store.MessageGroupStore; import org.springframework.integration.store.MessageStore; -import org.springframework.integration.store.MessageStoreException; -import org.springframework.integration.store.SimpleMessageGroup; -import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.util.Assert; /** - * An implementation of both the {@link MessageStore} and {@link MessageGroupStore} - * strategies that relies upon Redis for persistence. + * Redis implementation of the key/value style {@link MessageStore} and {@link MessageGroupStore} * * @author Oleg Zhurakousky * @since 2.1 */ -public class RedisMessageStore extends AbstractMessageGroupStore implements MessageStore { +public class RedisMessageStore extends AbstractKeyValueMessageStore { - private static final String MESSAGE_GROUPS_KEY = "MESSAGE_GROUPS"; - - private static final String MARKED_PREFIX = "MARKED_"; - - private static final String UNMARKED_PREFIX = "UNMARKED_"; - - - private final RedisTemplate redisTemplate; - + private final RedisTemplate redisTemplate; public RedisMessageStore(RedisConnectionFactory connectionFactory) { - this.redisTemplate = new RedisTemplate(); + this.redisTemplate = new RedisTemplate(); this.redisTemplate.setConnectionFactory(connectionFactory); this.redisTemplate.setKeySerializer(new StringRedisSerializer()); this.redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); } - public void setValueSerializer(RedisSerializer valueSerializer) { Assert.notNull(valueSerializer, "'valueSerializer' must not be null"); this.redisTemplate.setValueSerializer(valueSerializer); } - - public Message getMessage(final UUID id) { + + @Override + protected Object doRetrieve(Object id){ Assert.notNull(id, "'id' must not be null"); - if (this.redisTemplate.hasKey(id.toString())) { - BoundValueOperations ops = redisTemplate.boundValueOps(id.toString()); - Object result = ops.get(); - Assert.isInstanceOf(Message.class, result, "Return value is not an instace of Message"); - return (Message) result; - } - return null; + BoundValueOperations ops = redisTemplate.boundValueOps(id); + return ops.get(); } - @SuppressWarnings("unchecked") - public Message addMessage(Message message) { - Assert.notNull(message, "'message' must not be null"); - BoundValueOperations ops = redisTemplate.boundValueOps(message.getHeaders().getId().toString()); - try { - ops.set(message); - } - catch (SerializationException e) { - throw new MessageStoreException(message, "If relying on the default RedisSerializer (JdkSerializationRedisSerializer) " + - "the Message must be Serializable. Either make it Serializable or provide your own implementation of " + - "RedisSerializer via 'setValueSerializer(..)'", e); - } - Object result = ops.get(); - Assert.isInstanceOf(Message.class, result, "Return value is not an instace of Message"); - return (Message) result; - } - - public Message removeMessage(UUID id) { - Assert.notNull(id, "'id' must not be null"); - Message message = this.getMessage(id); - if (message != null) { - this.redisTemplate.delete(id.toString()); - } - return message; - } - - @ManagedAttribute - public long getMessageCount() { - return redisTemplate.execute(new RedisCallback() { - public Long doInRedis(RedisConnection connection) throws DataAccessException { - return connection.dbSize(); - } - }); - } - - - // MESSAGE GROUP methods - - /** - * Will create a new instance of SimpleMessageGroup initializing it with - * data collected from the Redis Message Store. - */ - public MessageGroup getMessageGroup(Object groupId) { - Assert.notNull(groupId, "'groupId' must not be null"); - long timestamp = System.currentTimeMillis(); - Collection> unmarkedMessages = this.buildMessageList(this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId)); - Collection> markedMessages = this.buildMessageList(this.redisTemplate.boundListOps(MARKED_PREFIX + groupId)); - this.doCreateMessageGroupIfNecessary(groupId); - return new SimpleMessageGroup(unmarkedMessages, markedMessages, groupId, timestamp); - } - - /** - * Add a Message to the group with the provided group ID. - */ - public MessageGroup addMessageToGroup(Object groupId, Message message) { - Assert.notNull(groupId, "'groupId' must not be null"); - Assert.notNull(message, "'message' must not be null"); - synchronized (groupId) { - this.doAddMessageToGroup(message, groupId); - this.addMessage(message); - return this.getMessageGroup(groupId); - } - } - - /** - * Mark all messages in the provided group. - */ - public MessageGroup markMessageGroup(MessageGroup group) { - Assert.notNull(group, "'group' must not be null"); - Object groupId = group.getGroupId(); - synchronized (groupId) { - this.doMarkMessageGroup(groupId); - return this.getMessageGroup(groupId); - } - } - - /** - * Remove a Message from the group with the provided group ID. - */ - public MessageGroup removeMessageFromGroup(Object groupId, Message messageToRemove) { - Assert.notNull(groupId, "'groupId' must not be null"); - Assert.notNull(messageToRemove, "'messageToRemove' must not be null"); - UUID messageId = messageToRemove.getHeaders().getId(); - synchronized (groupId) { - this.doRemoveMessageFromGroup(groupId, messageId); - this.removeMessage(messageId); - return this.getMessageGroup(groupId); - } - } - - /** - * Mark the given Message within the group corresponding to the provided group ID. - */ - public MessageGroup markMessageFromGroup(Object groupId, Message messageToMark) { - Assert.notNull(groupId, "'groupId' must not be null"); - Assert.notNull(messageToMark, "'messageToMark' must not be null"); - String messageIdAsString = messageToMark.getHeaders().getId().toString(); - synchronized (groupId) { - this.doMarkMessageFromGroup(messageIdAsString, groupId); - return this.getMessageGroup(groupId); - } - } - - /** - * Remove the MessageGroup with the provided group ID. - */ - public void removeMessageGroup(Object groupId) { - Assert.notNull(groupId, "'groupId' must not be null"); - synchronized (groupId) { - this.doRemoveMessageGroup(groupId); - } - } @Override - public Iterator iterator() { - BoundSetOperations mGroupsOps = this.redisTemplate.boundSetOps(MESSAGE_GROUPS_KEY); - Set messageGroupIds = mGroupsOps.members(); - List messageGroups = new ArrayList(); - for (Object messageGroupId : messageGroupIds) { - messageGroups.add(this.getMessageGroup(messageGroupId)); + protected void doStore(Object id, Object objectToStore) { + Assert.notNull(id, "'id' must not be null"); + Assert.notNull(objectToStore, "'objectToStore' must not be null"); + BoundValueOperations ops = redisTemplate.boundValueOps(id); + try { + ops.set(objectToStore); } - return messageGroups.iterator(); - } - - private Collection> buildMessageList(BoundListOperations messageGroupOps) { - List> messages = new LinkedList>(); - if (messageGroupOps.size() == 0) { - return Collections.emptyList(); - } - List messageIds = messageGroupOps.range(0, messageGroupOps.size() - 1); - for (Object messageId : messageIds) { - Message message = this.getMessage(UUID.fromString(messageId.toString())); - if (message != null) { - messages.add((Message) message); - } - } - return messages; - } - - /* candidates for future abstract methods */ - - private void doCreateMessageGroupIfNecessary(Object groupId) { - BoundSetOperations messageGroupsOps = this.redisTemplate.boundSetOps(MESSAGE_GROUPS_KEY); - if (!messageGroupsOps.members().contains(groupId)) { - messageGroupsOps.add(groupId); + catch (SerializationException e) { + throw new IllegalArgumentException("If relying on the default RedisSerializer (JdkSerializationRedisSerializer) " + + "the Object must be Serializable. Either make it Serializable or provide your own implementation of " + + "RedisSerializer via 'setValueSerializer(..)'", e); } } - private void doAddMessageToGroup(Message message, Object groupId) { - String messageId = message.getHeaders().getId().toString(); - BoundListOperations unmarkedOps = this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId); - unmarkedOps.rightPush(messageId); + + @Override + protected Object doRemove(Object id) { + Assert.notNull(id, "'id' must not be null"); + Object removedObject = this.doRetrieve(id); + if (removedObject != null){ + redisTemplate.delete(id); + } + return removedObject; } - private void doMarkMessageGroup(Object groupId) { - BoundListOperations unmarkedOps = this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId); - unmarkedOps.rename(MARKED_PREFIX + groupId); - } - private void doRemoveMessageFromGroup(Object groupId, UUID messageId) { - BoundListOperations unmarkedOps = this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId); - BoundListOperations markedOps = this.redisTemplate.boundListOps(MARKED_PREFIX + groupId); - unmarkedOps.remove(0, messageId.toString()); - markedOps.remove(0, messageId.toString()); + @Override + protected Collection doListKeys(String keyPattern) { + Assert.hasText(keyPattern, "'keyPattern' must not be empty"); + Set keys = redisTemplate.keys(keyPattern); + return keys; } - - private void doMarkMessageFromGroup(String messageIdAsString, Object groupId) { - BoundListOperations unmarkedOps = this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId); - if (unmarkedOps.size() > 0) { - List messageIds = unmarkedOps.range(0, unmarkedOps.size() - 1); - int objectIndex = messageIds.indexOf(messageIdAsString); - if (objectIndex > -1) { - BoundListOperations markedOps = this.redisTemplate.boundListOps(MARKED_PREFIX + groupId); - markedOps.rightPush(messageIdAsString); - unmarkedOps.remove(0, messageIdAsString); - } - } - } - - private void doRemoveMessageGroup(Object groupId) { - BoundListOperations unmarkedOps = this.redisTemplate.boundListOps(UNMARKED_PREFIX + groupId); - if (unmarkedOps.size() > 0) { - List messageIds = unmarkedOps.range(0, unmarkedOps.size() - 1); - for (Object messageId : messageIds) { - this.removeMessage(UUID.fromString(messageId.toString())); - } - this.redisTemplate.delete(UNMARKED_PREFIX + groupId); - } - BoundListOperations markedOps = this.redisTemplate.boundListOps(MARKED_PREFIX + groupId); - if (markedOps.size() > 0) { - List messageIds = markedOps.range(0, markedOps.size() - 1); - for (Object messageId : messageIds) { - this.removeMessage(UUID.fromString(messageId.toString())); - } - this.redisTemplate.delete(MARKED_PREFIX + groupId); - } - BoundSetOperations messageGroupsOps = this.redisTemplate.boundSetOps(MESSAGE_GROUPS_KEY); - messageGroupsOps.remove(groupId); - } - } diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java index 2faaf2850a..cb1a25e0d4 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java @@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit; import junit.framework.AssertionFailedError; +import org.junit.Ignore; import org.junit.Test; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; @@ -107,6 +108,34 @@ public class RedisMessageGroupStoreTests extends RedisAvailableTests { assertEquals(0, messageGroup.size()); } + @Test + @RedisAvailable + public void testCompleteMessageGroup() throws Exception{ + JedisConnectionFactory jcf = this.getConnectionFactoryForTest(); + RedisMessageStore store = new RedisMessageStore(jcf); + + MessageGroup messageGroup = store.getMessageGroup(1); + Message message = new GenericMessage("Hello"); + messageGroup = store.addMessageToGroup(messageGroup.getGroupId(), message); + store.completeGroup(messageGroup.getGroupId()); + messageGroup = store.getMessageGroup(1); + assertTrue(messageGroup.isComplete()); + } + + @Test + @RedisAvailable + public void testLastReleasedSequenceNumber() throws Exception{ + JedisConnectionFactory jcf = this.getConnectionFactoryForTest(); + RedisMessageStore store = new RedisMessageStore(jcf); + + MessageGroup messageGroup = store.getMessageGroup(1); + Message message = new GenericMessage("Hello"); + messageGroup = store.addMessageToGroup(messageGroup.getGroupId(), message); + store.setLastReleasedSequenceNumberForGroup(messageGroup.getGroupId(), 5); + messageGroup = store.getMessageGroup(1); + assertEquals(5, messageGroup.getLastReleasedMessageSequenceNumber()); + } + @Test @RedisAvailable public void testRemoveMessageFromTheGroup() throws Exception{ @@ -128,7 +157,24 @@ public class RedisMessageGroupStoreTests extends RedisAvailableTests { messageGroup = store.getMessageGroup(1); assertEquals(2, messageGroup.size()); + } + @Test + @RedisAvailable + public void testRemoveNonExistingMessageFromTheGroup() throws Exception{ + JedisConnectionFactory jcf = this.getConnectionFactoryForTest(); + RedisMessageStore store = new RedisMessageStore(jcf); + MessageGroup messageGroup = store.getMessageGroup(1); + store.addMessageToGroup(messageGroup.getGroupId(), new GenericMessage("1")); + store.removeMessageFromGroup(1, new GenericMessage("2")); + } + + @Test + @RedisAvailable + public void testRemoveNonExistingMessageFromNonExistingTheGroup() throws Exception{ + JedisConnectionFactory jcf = this.getConnectionFactoryForTest(); + RedisMessageStore store = new RedisMessageStore(jcf); + store.removeMessageFromGroup(1, new GenericMessage("2")); } @Test @@ -183,6 +229,8 @@ public class RedisMessageGroupStoreTests extends RedisAvailableTests { assertEquals(1, messageGroup.getMarked().size()); } + + @Test @RedisAvailable public void testMultipleInstancesOfGroupStore() throws Exception{ @@ -239,7 +287,7 @@ public class RedisMessageGroupStoreTests extends RedisAvailableTests { } @Test - @RedisAvailable + @RedisAvailable @Ignore public void testConcurrentModifications() throws Exception{ JedisConnectionFactory jcf = this.getConnectionFactoryForTest(); final RedisMessageStore store1 = new RedisMessageStore(jcf); diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageStoreTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageStoreTests.java index d74ab0e569..418ddcc77f 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageStoreTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageStoreTests.java @@ -24,7 +24,6 @@ import org.springframework.integration.Message; import org.springframework.integration.message.GenericMessage; import org.springframework.integration.redis.rules.RedisAvailable; import org.springframework.integration.redis.rules.RedisAvailableTests; -import org.springframework.integration.store.MessageStoreException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -80,7 +79,7 @@ public class RedisMessageStoreTests extends RedisAvailableTests { assertEquals("Barak Obama", storedMessage.getPayload().getName()); } - @Test(expected=MessageStoreException.class) + @Test(expected=IllegalArgumentException.class) @RedisAvailable public void testAddNonSerializableObjectMessage(){ JedisConnectionFactory jcf = this.getConnectionFactoryForTest();