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}