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