diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultMessage.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultMessage.java
index b053f4507..84d0a7d8d 100644
--- a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultMessage.java
+++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/connection/DefaultMessage.java
@@ -33,11 +33,11 @@ public class DefaultMessage implements Message {
@Override
public byte[] getChannel() {
- return channel;
+ return (channel != null ? channel.clone() : null);
}
@Override
public byte[] getPayload() {
- return payload;
+ return (payload != null ? payload.clone() : null);
}
}
diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/ChannelTopic.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/ChannelTopic.java
new file mode 100644
index 000000000..c76c2ad39
--- /dev/null
+++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/ChannelTopic.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 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.data.keyvalue.redis.listener;
+
+/**
+ * Topic describing a channel.
+ *
+ * @author Costin Leau
+ */
+public class ChannelTopic implements Topic {
+
+ private final String channelName;
+
+ /**
+ * Constructs a new ChannelTopic instance.
+ *
+ * @param name
+ */
+ public ChannelTopic(String name) {
+ this.channelName = name;
+ }
+
+ /**
+ * Returns the channel name.
+ *
+ * @return
+ */
+ public String getTopic() {
+ return channelName;
+ }
+}
diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/PatternTopic.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/PatternTopic.java
new file mode 100644
index 000000000..f5bbcc9e7
--- /dev/null
+++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/PatternTopic.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 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.data.keyvalue.redis.listener;
+
+/**
+ * Pattern topic (matching multiple channels).
+ *
+ * @author Costin Leau
+ */
+public class PatternTopic implements Topic {
+
+ private final String channelPattern;
+
+ public PatternTopic(String pattern) {
+ this.channelPattern = pattern;
+ }
+
+ public String getTopic() {
+ return channelPattern;
+ }
+}
diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/RedisListeningContainer.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/RedisListeningContainer.java
new file mode 100644
index 000000000..4edf416d5
--- /dev/null
+++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/RedisListeningContainer.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright 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.data.keyvalue.redis.listener;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.Executor;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.SmartLifecycle;
+import org.springframework.data.keyvalue.redis.connection.Message;
+import org.springframework.data.keyvalue.redis.connection.MessageListener;
+import org.springframework.data.keyvalue.redis.connection.RedisConnectionFactory;
+import org.springframework.data.keyvalue.redis.serializer.RedisSerializer;
+import org.springframework.data.keyvalue.redis.serializer.StringRedisSerializer;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * Container providing asynchronous behaviour for Redis message listeners.
+ * Handles the low level details of listening, converting and message dispatching.
+ *
+ * As oppose to the low level Redis (one connection per subscription), the container
+ * uses only one connection that is 'multiplexed' for all registered listeners,
+ * the message dispatch being done through the task executor.
+ *
+ *
+ * Note the container uses the connection only if at least one listener is configured.
+ *
+ * @author Costin Leau
+ */
+public class RedisListeningContainer implements InitializingBean, DisposableBean, BeanNameAware, SmartLifecycle {
+
+ private static final Log log = LogFactory.getLog(RedisListeningContainer.class);
+
+ private Executor connectionWorker;
+
+ private Executor taskExecutor;
+
+ private RedisConnectionFactory connectionFactory;
+
+ private String beanName;
+
+ private final Object monitor = new Object();
+ private volatile boolean running = false;
+ private volatile boolean initialized = false;
+
+
+ // lookup maps
+ // to avoid creation of hashes for each message, the maps use raw byte arrays (wrapped to respect the equals/hashcode contract)
+
+ // lookup map between patterns and listeners
+ private final Map> patternMapping = new ConcurrentHashMap>();
+ // lookup map between channels and listeners
+ private final Map> channelMapping = new ConcurrentHashMap>();
+
+ private final MessageListener multiplexer = new DispatchMessageListener();
+ private RedisSerializer serializer = new StringRedisSerializer();
+
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ //startListening();
+ initialized = true;
+ }
+
+ @Override
+ public void destroy() throws Exception {
+ initialized = false;
+
+ // stop listening
+ //stopListening();
+ }
+
+ @Override
+ public boolean isAutoStartup() {
+ return true;
+ }
+
+ @Override
+ public void stop(Runnable callback) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getPhase() {
+ // start the latest
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public boolean isRunning() {
+ return running;
+ }
+
+ @Override
+ public void start() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void stop() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns the connectionFactory.
+ *
+ * @return Returns the connectionFactory
+ */
+ public RedisConnectionFactory getConnectionFactory() {
+ return connectionFactory;
+ }
+
+ /**
+ * @param connectionFactory The connectionFactory to set.
+ */
+ public void setConnectionFactory(RedisConnectionFactory connectionFactory) {
+ this.connectionFactory = connectionFactory;
+ }
+
+ @Override
+ public void setBeanName(String name) {
+ this.beanName = name;
+ }
+
+
+ /**
+ * Sets the serializer for converting the raw channels and patterns into Strings.
+ * By default, {@link StringRedisSerializer} is used.
+ *
+ * @param serializer The serializer to set.
+ */
+ public void setSerializer(RedisSerializer serializer) {
+ this.serializer = serializer;
+ }
+
+ /**
+ * Attaches the given listeners (and their topics) to the container.
+ *
+ *
+ * Note: it's possible to call this method while the container is running forcing a reinitialization
+ * of the container. Note however that this might cause some messages to be lost (while the container
+ * reinitializes) - hence calling this method at runtime is considered advanced usage.
+ *
+ * @param listeners map of message listeners and their associated topics
+ */
+ public void setMessageListeners(Map extends MessageListener, Collection> listeners) {
+ initMapping(listeners);
+ }
+
+ /**
+ * Adds a message listener to the (potentially running) container. If the container is running,
+ * the listener starts receiving (matching) messages as soon as possible.
+ *
+ * @param listener message listener
+ * @param topics message listener topic
+ */
+ public void addMessageListener(MessageListener listener, Collection topics) {
+
+ }
+
+ private void initMapping(Map extends MessageListener, Collection> listeners) {
+ // stop the listener if currently running
+ if (isRunning()) {
+ stop();
+ }
+
+ patternMapping.clear();
+ channelMapping.clear();
+
+ if (!CollectionUtils.isEmpty(listeners)) {
+ for (Map.Entry extends MessageListener, Collection> entry : listeners.entrySet()) {
+ addListener(entry.getKey(), entry.getValue());
+ }
+ }
+
+ // resume activity
+ if (initialized) {
+ start();
+ }
+ }
+
+ private void addListener(MessageListener listener, Collection topics) {
+ for (Topic topic : topics) {
+
+ ArrayHolder holder = new ArrayHolder(serializer.serialize(topic.getTopic()));
+
+ if (topic instanceof ChannelTopic) {
+ Collection collection = channelMapping.get(holder);
+ if (collection == null) {
+ collection = new CopyOnWriteArraySet();
+ channelMapping.put(holder, collection);
+ }
+ collection.add(listener);
+ }
+
+ else if (topic instanceof PatternTopic) {
+ Collection collection = patternMapping.get(holder);
+ if (collection == null) {
+ collection = new CopyOnWriteArraySet();
+ patternMapping.put(holder, collection);
+ }
+ collection.add(listener);
+ }
+
+ else {
+ throw new IllegalArgumentException("Unknown topic type '" + topic.getClass() + "'");
+ }
+ }
+ }
+
+ /**
+ * Actual message dispatcher/multiplexer.
+ *
+ * @author Costin Leau
+ */
+ private class DispatchMessageListener implements MessageListener {
+
+ @Override
+ public void onMessage(Message message, byte[] pattern) {
+ // do channel matching first
+ byte[] channel = message.getChannel();
+
+ Collection ch = channelMapping.get(new ArrayHolder(channel));
+ Collection pt = null;
+
+ // followed by pattern matching
+ if (pattern != null && pattern.length > 0) {
+ pt = patternMapping.get(new ArrayHolder(pattern));
+ }
+
+ if (!CollectionUtils.isEmpty(ch)) {
+ dispatchChannels(ch, message);
+ }
+
+ if (!CollectionUtils.isEmpty(pt)) {
+ dispatchPatterns(pt, message, pattern);
+ }
+ }
+
+ private void dispatchChannels(Collection ch, final Message message) {
+ for (final MessageListener messageListener : ch) {
+ taskExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ messageListener.onMessage(message, null);
+ }
+ });
+ }
+ }
+
+ private void dispatchPatterns(Collection pt, final Message message, final byte[] pattern) {
+ for (final MessageListener messageListener : pt) {
+ taskExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ messageListener.onMessage(message, pattern.clone());
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Simple wrapper class used for wrapping arrays so they can be used as keys inside maps.
+ *
+ * @author Costin Leau
+ */
+ private class ArrayHolder {
+
+ private final byte[] array;
+ private final int hashCode;
+
+ ArrayHolder(byte[] array) {
+ this.array = array;
+ this.hashCode = Arrays.hashCode(array);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ArrayHolder) {
+ return Arrays.equals(array, ((ArrayHolder) obj).array);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/Topic.java b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/Topic.java
new file mode 100644
index 000000000..4c3c8380c
--- /dev/null
+++ b/spring-data-redis/src/main/java/org/springframework/data/keyvalue/redis/listener/Topic.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 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.data.keyvalue.redis.listener;
+
+/**
+ * Topic for a Redis message. Acts a high-level abstraction on top
+ * of Redis low-level channels or patterns.
+ *
+ * @author Costin Leau
+ */
+public interface Topic {
+
+ String getTopic();
+}