/**
* GRANITE DATA SERVICES
* Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S.
*
* This file is part of the Granite Data Services Platform.
*
* Granite Data Services is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* Granite Data Services is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA, or see <http://www.gnu.org/licenses/>.
*/
package org.granite.gravity.gae;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.granite.gravity.Channel;
import org.granite.gravity.Subscription;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import flex.messaging.messages.AsyncMessage;
/**
* Adapted from Greg Wilkins code (Jetty).
*
* @author William DRAI
*/
public class GAETopic {
private final GAETopicId id;
private final GAEServiceAdapter serviceAdapter;
private static final String TOPIC_PREFIX = "org.granite.gravity.gae.topic.";
private static MemcacheService gaeCache = MemcacheServiceFactory.getMemcacheService();
private ConcurrentMap<String, GAETopic> children = new ConcurrentHashMap<String, GAETopic>();
private GAETopic wild;
private GAETopic wildWild;
public GAETopic(String topicId, GAEServiceAdapter serviceAdapter) {
this.id = new GAETopicId(topicId);
this.serviceAdapter = serviceAdapter;
}
public String getId() {
return id.toString();
}
public GAETopicId getTopicId() {
return id;
}
public GAETopic getChild(GAETopicId topicId) {
String next = topicId.getSegment(id.depth());
if (next == null)
return null;
GAETopic topic = children.get(next);
if (topic == null || topic.getTopicId().depth() == topicId.depth()) {
return topic;
}
return topic.getChild(topicId);
}
public void addChild(GAETopic topic) {
GAETopicId child = topic.getTopicId();
if (!id.isParentOf(child))
throw new IllegalArgumentException(id + " not parent of " + child);
String next = child.getSegment(id.depth());
if ((child.depth() - id.depth()) == 1) {
// add the topic to this topics
GAETopic old = children.putIfAbsent(next, topic);
if (old != null)
throw new IllegalArgumentException("Already Exists");
if (GAETopicId.WILD.equals(next))
wild = topic;
else if (GAETopicId.WILDWILD.equals(next))
wildWild = topic;
}
else {
GAETopic branch = serviceAdapter.getTopic((id.depth() == 0 ? "/" : (id.toString() + "/")) + next, true);
branch.addChild(topic);
}
}
private void removeExpiredSubscriptions(Map<String, Subscription> subscriptions) {
List<Object> channelIds = new ArrayList<Object>(subscriptions.size());
for (Subscription sub : subscriptions.values())
channelIds.add(GAEGravity.CHANNEL_PREFIX + sub.getChannel().getId());
Map<Object, Object> channels = gaeCache.getAll(channelIds);
// Remove expired channel subscriptions
for (Iterator<Map.Entry<String, Subscription>> ime = subscriptions.entrySet().iterator(); ime.hasNext(); ) {
Map.Entry<String, Subscription> me = ime.next();
if (!channels.containsKey(GAEGravity.CHANNEL_PREFIX + me.getValue().getChannel().getId()))
ime.remove();
}
}
public void subscribe(Channel channel, String destination, String subscriptionId, String selector, boolean noLocal) {
// How to handle cluster synchronization ???
synchronized (this) {
Subscription subscription = channel.addSubscription(destination, getId(), subscriptionId, noLocal);
subscription.setSelector(selector);
// Handle synchronization issues ???
@SuppressWarnings("unchecked")
Map<String, Subscription> subscriptions = (Map<String, Subscription>)gaeCache.get(TOPIC_PREFIX + getId());
if (subscriptions == null)
subscriptions = new HashMap<String, Subscription>();
else
removeExpiredSubscriptions(subscriptions);
subscriptions.put(subscriptionId, subscription);
gaeCache.put(TOPIC_PREFIX + getId(), subscriptions);
}
}
public void unsubscribe(Channel channel, String subscriptionId) {
// How to handle cluster synchronization ???
synchronized(this) {
@SuppressWarnings("unchecked")
Map<String, Subscription> subscriptions = (Map<String, Subscription>)gaeCache.get(TOPIC_PREFIX + getId());
if (subscriptions != null) {
subscriptions.remove(subscriptionId);
removeExpiredSubscriptions(subscriptions);
}
gaeCache.put(TOPIC_PREFIX + getId(), subscriptions);
channel.removeSubscription(subscriptionId);
}
}
public void publish(GAETopicId to, Channel fromChannel, AsyncMessage msg) {
int tail = to.depth()-id.depth();
switch(tail) {
case 0:
@SuppressWarnings("unchecked")
Map<String, Subscription> subscriptions = (Map<String, Subscription>)gaeCache.get(TOPIC_PREFIX + getId());
if (subscriptions != null) {
for (Subscription subscription : subscriptions.values()) {
AsyncMessage m = msg.clone();
subscription.deliver(fromChannel, m);
}
}
break;
case 1:
if (wild != null) {
@SuppressWarnings("unchecked")
Map<String, Subscription> subs = (Map<String, Subscription>)gaeCache.get(TOPIC_PREFIX + wild.getId());
for (Subscription subscription : subs.values()) {
AsyncMessage m = msg.clone();
subscription.deliver(fromChannel, m);
}
}
default: {
if (wildWild != null) {
@SuppressWarnings("unchecked")
Map<String, Subscription> subs = (Map<String, Subscription>)gaeCache.get(TOPIC_PREFIX + wildWild.getId());
for (Subscription subscription : subs.values()) {
AsyncMessage m = msg.clone();
subscription.deliver(fromChannel, m);
}
}
String next = to.getSegment(id.depth());
GAETopic topic = children.get(next);
if (topic != null)
topic.publish(to, fromChannel, msg);
}
}
}
@Override
public String toString() {
return id.toString() + " {" + children.values() + "}";
}
}