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