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