001 /**
002 * GRANITE DATA SERVICES
003 * Copyright (C) 2006-2013 GRANITE DATA SERVICES S.A.S.
004 *
005 * This file is part of the Granite Data Services Platform.
006 *
007 * Granite Data Services is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * Granite Data Services is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
015 * General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
020 * USA, or see <http://www.gnu.org/licenses/>.
021 */
022 package org.granite.gravity.gae;
023
024 import java.util.ArrayList;
025 import java.util.HashMap;
026 import java.util.Iterator;
027 import java.util.List;
028 import java.util.Map;
029 import java.util.concurrent.ConcurrentHashMap;
030 import java.util.concurrent.ConcurrentMap;
031
032 import org.granite.gravity.Channel;
033 import org.granite.gravity.Subscription;
034
035 import com.google.appengine.api.memcache.MemcacheService;
036 import com.google.appengine.api.memcache.MemcacheServiceFactory;
037
038 import flex.messaging.messages.AsyncMessage;
039
040 /**
041 * Adapted from Greg Wilkins code (Jetty).
042 *
043 * @author William DRAI
044 */
045 public class GAETopic {
046
047 private final GAETopicId id;
048 private final GAEServiceAdapter serviceAdapter;
049
050 private static final String TOPIC_PREFIX = "org.granite.gravity.gae.topic.";
051
052 private static MemcacheService gaeCache = MemcacheServiceFactory.getMemcacheService();
053
054 private ConcurrentMap<String, GAETopic> children = new ConcurrentHashMap<String, GAETopic>();
055 private GAETopic wild;
056 private GAETopic wildWild;
057
058
059 public GAETopic(String topicId, GAEServiceAdapter serviceAdapter) {
060 this.id = new GAETopicId(topicId);
061 this.serviceAdapter = serviceAdapter;
062 }
063
064 public String getId() {
065 return id.toString();
066 }
067
068 public GAETopicId getTopicId() {
069 return id;
070 }
071
072 public GAETopic getChild(GAETopicId topicId) {
073 String next = topicId.getSegment(id.depth());
074 if (next == null)
075 return null;
076
077 GAETopic topic = children.get(next);
078
079 if (topic == null || topic.getTopicId().depth() == topicId.depth()) {
080 return topic;
081 }
082 return topic.getChild(topicId);
083 }
084
085 public void addChild(GAETopic topic) {
086 GAETopicId child = topic.getTopicId();
087 if (!id.isParentOf(child))
088 throw new IllegalArgumentException(id + " not parent of " + child);
089
090 String next = child.getSegment(id.depth());
091
092 if ((child.depth() - id.depth()) == 1) {
093 // add the topic to this topics
094 GAETopic old = children.putIfAbsent(next, topic);
095
096 if (old != null)
097 throw new IllegalArgumentException("Already Exists");
098
099 if (GAETopicId.WILD.equals(next))
100 wild = topic;
101 else if (GAETopicId.WILDWILD.equals(next))
102 wildWild = topic;
103 }
104 else {
105 GAETopic branch = serviceAdapter.getTopic((id.depth() == 0 ? "/" : (id.toString() + "/")) + next, true);
106 branch.addChild(topic);
107 }
108 }
109
110
111 private void removeExpiredSubscriptions(Map<String, Subscription> subscriptions) {
112 List<Object> channelIds = new ArrayList<Object>(subscriptions.size());
113 for (Subscription sub : subscriptions.values())
114 channelIds.add(GAEGravity.CHANNEL_PREFIX + sub.getChannel().getId());
115
116 Map<Object, Object> channels = gaeCache.getAll(channelIds);
117 // Remove expired channel subscriptions
118 for (Iterator<Map.Entry<String, Subscription>> ime = subscriptions.entrySet().iterator(); ime.hasNext(); ) {
119 Map.Entry<String, Subscription> me = ime.next();
120 if (!channels.containsKey(GAEGravity.CHANNEL_PREFIX + me.getValue().getChannel().getId()))
121 ime.remove();
122 }
123 }
124
125 public void subscribe(Channel channel, String destination, String subscriptionId, String selector, boolean noLocal) {
126 // How to handle cluster synchronization ???
127 synchronized (this) {
128 Subscription subscription = channel.addSubscription(destination, getId(), subscriptionId, noLocal);
129 subscription.setSelector(selector);
130
131 // Handle synchronization issues ???
132 @SuppressWarnings("unchecked")
133 Map<String, Subscription> subscriptions = (Map<String, Subscription>)gaeCache.get(TOPIC_PREFIX + getId());
134 if (subscriptions == null)
135 subscriptions = new HashMap<String, Subscription>();
136 else
137 removeExpiredSubscriptions(subscriptions);
138
139 subscriptions.put(subscriptionId, subscription);
140 gaeCache.put(TOPIC_PREFIX + getId(), subscriptions);
141 }
142 }
143
144 public void unsubscribe(Channel channel, String subscriptionId) {
145 // How to handle cluster synchronization ???
146 synchronized(this) {
147 @SuppressWarnings("unchecked")
148 Map<String, Subscription> subscriptions = (Map<String, Subscription>)gaeCache.get(TOPIC_PREFIX + getId());
149 if (subscriptions != null) {
150 subscriptions.remove(subscriptionId);
151 removeExpiredSubscriptions(subscriptions);
152 }
153 gaeCache.put(TOPIC_PREFIX + getId(), subscriptions);
154 channel.removeSubscription(subscriptionId);
155 }
156 }
157
158
159 public void publish(GAETopicId to, Channel fromChannel, AsyncMessage msg) {
160 int tail = to.depth()-id.depth();
161
162 switch(tail) {
163 case 0:
164 @SuppressWarnings("unchecked")
165 Map<String, Subscription> subscriptions = (Map<String, Subscription>)gaeCache.get(TOPIC_PREFIX + getId());
166 if (subscriptions != null) {
167 for (Subscription subscription : subscriptions.values()) {
168 AsyncMessage m = msg.clone();
169 subscription.deliver(fromChannel, m);
170 }
171 }
172
173 break;
174
175 case 1:
176 if (wild != null) {
177 @SuppressWarnings("unchecked")
178 Map<String, Subscription> subs = (Map<String, Subscription>)gaeCache.get(TOPIC_PREFIX + wild.getId());
179 for (Subscription subscription : subs.values()) {
180 AsyncMessage m = msg.clone();
181 subscription.deliver(fromChannel, m);
182 }
183 }
184
185 default: {
186 if (wildWild != null) {
187 @SuppressWarnings("unchecked")
188 Map<String, Subscription> subs = (Map<String, Subscription>)gaeCache.get(TOPIC_PREFIX + wildWild.getId());
189 for (Subscription subscription : subs.values()) {
190 AsyncMessage m = msg.clone();
191 subscription.deliver(fromChannel, m);
192 }
193 }
194 String next = to.getSegment(id.depth());
195 GAETopic topic = children.get(next);
196 if (topic != null)
197 topic.publish(to, fromChannel, msg);
198 }
199 }
200 }
201
202 @Override
203 public String toString() {
204 return id.toString() + " {" + children.values() + "}";
205 }
206 }