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}