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
021package org.granite.gravity.gae;
022
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028import java.util.concurrent.ConcurrentHashMap;
029import java.util.concurrent.ConcurrentMap;
030
031import org.granite.gravity.Channel;
032import org.granite.gravity.Subscription;
033
034import com.google.appengine.api.memcache.MemcacheService;
035import com.google.appengine.api.memcache.MemcacheServiceFactory;
036
037import flex.messaging.messages.AsyncMessage;
038
039/**
040 * Adapted from Greg Wilkins code (Jetty).
041 * 
042 * @author William DRAI
043 */
044public 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}