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