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    }