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    }