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
021 package org.granite.gravity;
022
023 import java.io.IOException;
024 import java.io.ObjectOutput;
025 import java.io.OutputStream;
026 import java.util.Collection;
027 import java.util.LinkedList;
028 import java.util.concurrent.ConcurrentHashMap;
029 import java.util.concurrent.ConcurrentMap;
030 import java.util.concurrent.locks.Lock;
031 import java.util.concurrent.locks.ReentrantLock;
032
033 import javax.servlet.ServletConfig;
034 import javax.servlet.ServletContext;
035 import javax.servlet.http.HttpServletRequest;
036 import javax.servlet.http.HttpServletResponse;
037
038 import org.granite.context.AMFContextImpl;
039 import org.granite.context.GraniteContext;
040 import org.granite.logging.Logger;
041 import org.granite.messaging.amf.AMF0Message;
042 import org.granite.messaging.webapp.HttpGraniteContext;
043
044 import flex.messaging.messages.AsyncMessage;
045
046 /**
047 * @author Franck WOLFF
048 */
049 public abstract class AbstractChannel implements Channel {
050
051 ///////////////////////////////////////////////////////////////////////////
052 // Fields.
053
054 private static final Logger log = Logger.getLogger(AbstractChannel.class);
055
056 protected final String id;
057 protected final ServletConfig servletConfig;
058
059 protected final ConcurrentMap<String, Subscription> subscriptions = new ConcurrentHashMap<String, Subscription>();
060
061 protected LinkedList<AsyncPublishedMessage> publishedQueue = new LinkedList<AsyncPublishedMessage>();
062 protected final Lock publishedQueueLock = new ReentrantLock();
063
064 protected LinkedList<AsyncMessage> receivedQueue = new LinkedList<AsyncMessage>();
065 protected final Lock receivedQueueLock = new ReentrantLock();
066
067 protected final AsyncPublisher publisher;
068 protected final AsyncReceiver receiver;
069
070 ///////////////////////////////////////////////////////////////////////////
071 // Constructor.
072
073 protected AbstractChannel(ServletConfig servletConfig, GravityConfig gravityConfig, String id) {
074 if (id == null)
075 throw new NullPointerException("id cannot be null");
076
077 this.id = id;
078 this.servletConfig = servletConfig;
079
080 this.publisher = new AsyncPublisher(this);
081 this.receiver = new AsyncReceiver(this);
082 }
083
084 ///////////////////////////////////////////////////////////////////////////
085 // Abstract protected method.
086
087 protected abstract boolean hasAsyncHttpContext();
088 protected abstract AsyncHttpContext acquireAsyncHttpContext();
089 protected abstract void releaseAsyncHttpContext(AsyncHttpContext context);
090
091 ///////////////////////////////////////////////////////////////////////////
092 // Channel interface implementation.
093
094 public String getId() {
095 return id;
096 }
097
098 public Gravity getGravity() {
099 return GravityManager.getGravity(getServletContext());
100 }
101
102 public Subscription addSubscription(String destination, String subTopicId, String subscriptionId, boolean noLocal) {
103 Subscription subscription = new Subscription(this, destination, subTopicId, subscriptionId, noLocal);
104 Subscription present = subscriptions.putIfAbsent(subscriptionId, subscription);
105 return (present != null ? present : subscription);
106 }
107
108 public Collection<Subscription> getSubscriptions() {
109 return subscriptions.values();
110 }
111
112 public Subscription removeSubscription(String subscriptionId) {
113 return subscriptions.remove(subscriptionId);
114 }
115
116 public void publish(AsyncPublishedMessage message) throws MessagePublishingException {
117 if (message == null)
118 throw new NullPointerException("message cannot be null");
119
120 publishedQueueLock.lock();
121 try {
122 publishedQueue.add(message);
123 }
124 finally {
125 publishedQueueLock.unlock();
126 }
127
128 publisher.queue(getGravity());
129 }
130
131 public boolean hasPublishedMessage() {
132 publishedQueueLock.lock();
133 try {
134 return !publishedQueue.isEmpty();
135 }
136 finally {
137 publishedQueueLock.unlock();
138 }
139 }
140
141 public boolean runPublish() {
142 LinkedList<AsyncPublishedMessage> publishedCopy = null;
143
144 publishedQueueLock.lock();
145 try {
146 if (publishedQueue.isEmpty())
147 return false;
148 publishedCopy = publishedQueue;
149 publishedQueue = new LinkedList<AsyncPublishedMessage>();
150 }
151 finally {
152 publishedQueueLock.unlock();
153 }
154
155 for (AsyncPublishedMessage message : publishedCopy) {
156 try {
157 message.publish(this);
158 }
159 catch (Exception e) {
160 log.error(e, "Error while trying to publish message: %s", message);
161 }
162 }
163
164 return true;
165 }
166
167 public void receive(AsyncMessage message) throws MessageReceivingException {
168 if (message == null)
169 throw new NullPointerException("message cannot be null");
170
171 Gravity gravity = getGravity();
172
173 receivedQueueLock.lock();
174 try {
175 if (receivedQueue.size() + 1 > gravity.getGravityConfig().getMaxMessagesQueuedPerChannel())
176 throw new MessageReceivingException(message, "Could not queue message (channel's queue is full) for channel: " + this);
177
178 receivedQueue.add(message);
179 }
180 finally {
181 receivedQueueLock.unlock();
182 }
183
184 if (hasAsyncHttpContext())
185 receiver.queue(gravity);
186 }
187
188 public boolean hasReceivedMessage() {
189 receivedQueueLock.lock();
190 try {
191 return !receivedQueue.isEmpty();
192 }
193 finally {
194 receivedQueueLock.unlock();
195 }
196 }
197
198 public boolean runReceive() {
199 return runReceived(null);
200 }
201
202 public boolean runReceived(AsyncHttpContext asyncHttpContext) {
203
204 boolean httpAsParam = (asyncHttpContext != null);
205 LinkedList<AsyncMessage> messages = null;
206 OutputStream os = null;
207
208 try {
209 receivedQueueLock.lock();
210 try {
211 // Do we have any pending messages?
212 if (receivedQueue.isEmpty())
213 return false;
214
215 // Do we have a valid http context?
216 if (asyncHttpContext == null) {
217 asyncHttpContext = acquireAsyncHttpContext();
218 if (asyncHttpContext == null)
219 return false;
220 }
221
222 // Both conditions are ok, get all pending messages.
223 messages = receivedQueue;
224 receivedQueue = new LinkedList<AsyncMessage>();
225 }
226 finally {
227 receivedQueueLock.unlock();
228 }
229
230 HttpServletRequest request = asyncHttpContext.getRequest();
231 HttpServletResponse response = asyncHttpContext.getResponse();
232
233 // Set response messages correlation ids to connect request message id.
234 String correlationId = asyncHttpContext.getConnectMessage().getMessageId();
235 AsyncMessage[] messagesArray = new AsyncMessage[messages.size()];
236 int i = 0;
237 for (AsyncMessage message : messages) {
238 message.setCorrelationId(correlationId);
239 messagesArray[i++] = message;
240 }
241
242 // Setup serialization context (thread local)
243 Gravity gravity = getGravity();
244 GraniteContext context = HttpGraniteContext.createThreadIntance(
245 gravity.getGraniteConfig(), gravity.getServicesConfig(),
246 null, request, response
247 );
248 ((AMFContextImpl)context.getAMFContext()).setCurrentAmf3Message(asyncHttpContext.getConnectMessage());
249
250 // Write messages to response output stream.
251
252 response.setStatus(HttpServletResponse.SC_OK);
253 response.setContentType(AMF0Message.CONTENT_TYPE);
254 response.setDateHeader("Expire", 0L);
255 response.setHeader("Cache-Control", "no-store");
256
257 os = response.getOutputStream();
258 ObjectOutput amf3Serializer = context.getGraniteConfig().newAMF3Serializer(os);
259
260 log.debug("<< [MESSAGES for channel=%s] %s", this, messagesArray);
261
262 amf3Serializer.writeObject(messagesArray);
263
264 os.flush();
265 response.flushBuffer();
266
267 return true; // Messages were delivered, http context isn't valid anymore.
268 }
269 catch (IOException e) {
270 log.warn(e, "Could not send messages to channel: %s (retrying later)", this);
271
272 GravityConfig gravityConfig = getGravity().getGravityConfig();
273 if (gravityConfig.isRetryOnError()) {
274 receivedQueueLock.lock();
275 try {
276 if (receivedQueue.size() + messages.size() > gravityConfig.getMaxMessagesQueuedPerChannel()) {
277 log.warn(
278 "Channel %s has reached its maximum queue capacity %s (throwing %s messages)",
279 this,
280 gravityConfig.getMaxMessagesQueuedPerChannel(),
281 messages.size()
282 );
283 }
284 else
285 receivedQueue.addAll(0, messages);
286 }
287 finally {
288 receivedQueueLock.unlock();
289 }
290 }
291
292 return true; // Messages weren't delivered, but http context isn't valid anymore.
293 }
294 finally {
295
296 // Cleanup serialization context (thread local)
297 try {
298 GraniteContext.release();
299 }
300 catch (Exception e) {
301 // should never happen...
302 }
303
304 // Close output stream.
305 try {
306 if (os != null) {
307 try {
308 os.close();
309 }
310 catch (IOException e) {
311 log.warn(e, "Could not close output stream (ignored)");
312 }
313 }
314 }
315 finally {
316 // Cleanup http context (only if this method wasn't explicitly called with a non null
317 // AsyncHttpContext from the servlet).
318 if (!httpAsParam)
319 releaseAsyncHttpContext(asyncHttpContext);
320 }
321 }
322 }
323
324 public void destroy() {
325 Gravity gravity = getGravity();
326 gravity.cancel(publisher);
327 gravity.cancel(receiver);
328
329 subscriptions.clear();
330 }
331
332 ///////////////////////////////////////////////////////////////////////////
333 // Protected utilities.
334
335 protected boolean queueReceiver() {
336 if (hasReceivedMessage()) {
337 receiver.queue(getGravity());
338 return true;
339 }
340 return false;
341 }
342
343 protected ServletConfig getServletConfig() {
344 return servletConfig;
345 }
346
347 protected ServletContext getServletContext() {
348 return servletConfig.getServletContext();
349 }
350
351 ///////////////////////////////////////////////////////////////////////////
352 // Object overwritten methods.
353
354 @Override
355 public boolean equals(Object obj) {
356 return (obj instanceof Channel && id.equals(((Channel)obj).getId()));
357 }
358
359 @Override
360 public int hashCode() {
361 return id.hashCode();
362 }
363
364 @Override
365 public String toString() {
366 return getClass().getName() + " {id=" + id + ", subscriptions=" + subscriptions.values() + "}";
367 }
368 }