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;
023
024import java.io.IOException;
025import java.io.ObjectOutput;
026import java.io.OutputStream;
027import java.net.SocketException;
028import java.util.Collection;
029import java.util.LinkedList;
030import java.util.concurrent.ConcurrentHashMap;
031import java.util.concurrent.ConcurrentMap;
032import java.util.concurrent.locks.Lock;
033import java.util.concurrent.locks.ReentrantLock;
034
035import javax.servlet.http.HttpServletRequest;
036import javax.servlet.http.HttpServletResponse;
037
038import org.granite.context.AMFContextImpl;
039import org.granite.context.GraniteContext;
040import org.granite.gravity.udp.UdpReceiver;
041import org.granite.gravity.udp.UdpReceiverFactory;
042import org.granite.logging.Logger;
043import org.granite.messaging.webapp.HttpGraniteContext;
044import org.granite.util.ContentType;
045
046import flex.messaging.messages.AsyncMessage;
047import flex.messaging.messages.Message;
048
049/**
050 * @author Franck WOLFF
051 */
052public 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}