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 receiver;
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.receiver = 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            log.debug("Channel %s queue message %s for client %s", getId(), message.getMessageId(), message.getClientId());
218                        receivedQueue.add(message);
219                }
220                finally {
221                        receivedQueueLock.unlock();
222                }
223
224                if (hasAsyncHttpContext())
225                        receiver.queue(gravity);
226        }
227        
228        public boolean hasReceivedMessage() {
229                receivedQueueLock.lock();
230                try {
231                        return !receivedQueue.isEmpty();
232                }
233                finally {
234                        receivedQueueLock.unlock();
235                }
236        }
237
238        public boolean runReceive() {
239                return runReceived(null);
240        }
241        
242        public ObjectOutput newSerializer(GraniteContext context, OutputStream os) {
243                return context.getGraniteConfig().newAMF3Serializer(os);
244        }
245        
246        public String getSerializerContentType() {
247                return ContentType.AMF.mimeType();
248        }
249        
250        protected void createUdpReceiver(UdpReceiverFactory factory, AsyncHttpContext asyncHttpContext) {
251                OutputStream os = null;
252                try {
253                        Message connectMessage = asyncHttpContext.getConnectMessage();
254
255                        if (udpReceiver == null || udpReceiver.isClosed())
256                                udpReceiver = factory.newReceiver(this, asyncHttpContext.getRequest(), connectMessage);
257                        
258                AsyncMessage reply = udpReceiver.acknowledge(connectMessage);
259        
260                HttpServletRequest request = asyncHttpContext.getRequest();
261                        HttpServletResponse response = asyncHttpContext.getResponse();
262                        
263                GraniteContext context = HttpGraniteContext.createThreadIntance(
264                    gravity.getGraniteConfig(), gravity.getServicesConfig(),
265                    null, request, response
266                );
267                ((AMFContextImpl)context.getAMFContext()).setCurrentAmf3Message(asyncHttpContext.getConnectMessage());
268        
269                response.setStatus(HttpServletResponse.SC_OK);
270                response.setContentType(getSerializerContentType());
271                response.setDateHeader("Expire", 0L);
272                response.setHeader("Cache-Control", "no-store");
273                
274                os = response.getOutputStream();
275                
276                ObjectOutput serializer = newSerializer(context, os);
277                
278                serializer.writeObject(new AsyncMessage[] { reply });
279                
280                os.flush();
281                response.flushBuffer();
282                }
283                catch (IOException e) {
284                        log.error(e, "Could not send UDP connect acknowledgement to channel: %s", this);
285                }
286                finally {
287                        try {
288                                GraniteContext.release();
289                        }
290                        catch (Exception e) {
291                                // should never happen...
292                        }
293                        
294                        // Close output stream.
295                        try {
296                                if (os != null) {
297                                        try {
298                                                os.close();
299                                        }
300                                        catch (IOException e) {
301                                                log.warn(e, "Could not close output stream (ignored)");
302                                        }
303                                }
304                        }
305                        finally {
306                                releaseAsyncHttpContext(asyncHttpContext);
307                        }
308                }
309        }
310        
311        public boolean runReceived(AsyncHttpContext asyncHttpContext) {
312                
313                Gravity gravity = getGravity();
314                
315                if (asyncHttpContext != null && gravity.hasUdpReceiverFactory()) {
316                        UdpReceiverFactory factory = gravity.getUdpReceiverFactory();
317                        
318                        if (factory.isUdpConnectRequest(asyncHttpContext.getConnectMessage())) {
319                                createUdpReceiver(factory, asyncHttpContext);
320                                return true;
321                        }
322                        
323                        if (udpReceiver != null) {
324                                if (!udpReceiver.isClosed())
325                                        udpReceiver.close(false);
326                                udpReceiver = null;
327                        }
328                }
329
330                boolean httpAsParam = (asyncHttpContext != null);
331                LinkedList<AsyncMessage> messages = null;
332                OutputStream os = null;
333
334                try {
335                        receivedQueueLock.lock();
336                        try {
337                                // Do we have any pending messages? 
338                                if (receivedQueue.isEmpty())
339                                        return false;
340                                
341                                // Do we have a valid http context?
342                                if (asyncHttpContext == null) {
343                                        asyncHttpContext = acquireAsyncHttpContext();
344                                        if (asyncHttpContext == null)
345                                                return false;
346                                }
347                                
348                                // Both conditions are ok, get all pending messages.
349                                messages = receivedQueue;
350                                receivedQueue = new LinkedList<AsyncMessage>();
351                        }
352                        finally {
353                                receivedQueueLock.unlock();
354                        }
355                        
356                        HttpServletRequest request = asyncHttpContext.getRequest();
357                        HttpServletResponse response = asyncHttpContext.getResponse();
358                        
359                        // Set response messages correlation ids to connect request message id.
360                        String correlationId = asyncHttpContext.getConnectMessage().getMessageId();
361                        AsyncMessage[] messagesArray = new AsyncMessage[messages.size()];
362                        int i = 0;
363                        for (AsyncMessage message : messages) {
364                                message.setCorrelationId(correlationId);
365                                messagesArray[i++] = message;
366                        }
367
368                        // Setup serialization context (thread local)
369                GraniteContext context = HttpGraniteContext.createThreadIntance(
370                    gravity.getGraniteConfig(), gravity.getServicesConfig(),
371                    null, request, response
372                );
373                ((AMFContextImpl)context.getAMFContext()).setCurrentAmf3Message(asyncHttpContext.getConnectMessage());
374        
375                // Write messages to response output stream.
376
377                response.setStatus(HttpServletResponse.SC_OK);
378                response.setContentType(getSerializerContentType());
379                response.setDateHeader("Expire", 0L);
380                response.setHeader("Cache-Control", "no-store");
381                
382                os = response.getOutputStream();
383                ObjectOutput serializer = newSerializer(context, os);
384                
385                log.debug("<< [MESSAGES for channel=%s] %s", this, messagesArray);
386                
387                serializer.writeObject(messagesArray);
388                
389                os.flush();
390                response.flushBuffer();
391                
392                return true; // Messages were delivered, http context isn't valid anymore.
393                }
394                catch (IOException e) {
395                        log.warn(e, "Could not send messages to channel: %s (retrying later)", getId());
396                        
397                        GravityConfig gravityConfig = getGravity().getGravityConfig();
398                        if (gravityConfig.isRetryOnError()) {
399                                receivedQueueLock.lock();
400                                try {
401                                        if (receivedQueue.size() + messages.size() > gravityConfig.getMaxMessagesQueuedPerChannel()) {
402                                                log.warn(
403                                                        "Channel %s has reached its maximum queue capacity %s (throwing %s messages)",
404                                                        getId(),
405                                                        gravityConfig.getMaxMessagesQueuedPerChannel(),
406                                                        messages.size()
407                                                );
408                                        }
409                                        else
410                                                receivedQueue.addAll(0, messages);
411                                }
412                                finally {
413                                        receivedQueueLock.unlock();
414                                }
415                        }
416                        
417                        return true; // Messages weren't delivered, but http context isn't valid anymore.
418                }
419                finally {               
420                        // Cleanup serialization context (thread local)
421                        try {
422                                GraniteContext.release();
423                        }
424                        catch (Exception e) {
425                                // should never happen...
426                        }
427                        
428                        // Close output stream.
429                        try {
430                                if (os != null) {
431                                        try {
432                                                os.close();
433                                        }
434                                        catch (IOException e) {
435                                                log.warn(e, "Could not close output stream (ignored)");
436                                        }
437                                }
438                        }
439                        finally {
440                                // Cleanup http context (only if this method wasn't explicitly called with a non null
441                                // AsyncHttpContext from the servlet).
442                                if (!httpAsParam)
443                                        releaseAsyncHttpContext(asyncHttpContext);
444                        }
445                }
446        }
447
448    public void destroy() {
449        destroy(false);
450    }
451    
452    public void destroy(boolean timeout) {
453        try {
454                Gravity gravity = getGravity();
455                        gravity.cancel(publisher);
456                        gravity.cancel(receiver);
457        
458                subscriptions.clear();
459        }
460        finally {
461                        if (udpReceiver != null) {
462                                if (!udpReceiver.isClosed())
463                                        udpReceiver.close(timeout);
464                                udpReceiver = null;
465                        }
466        }
467        }
468    
469    ///////////////////////////////////////////////////////////////////////////
470    // Protected utilities.
471        
472        protected boolean queueReceiver() {
473                if (hasReceivedMessage()) {
474                        receiver.queue(getGravity());
475                        return true;
476                }
477                return false;
478        }       
479    
480    ///////////////////////////////////////////////////////////////////////////
481    // Object overwritten methods.
482
483        @Override
484    public boolean equals(Object obj) {
485        return (obj instanceof Channel && id.equals(((Channel)obj).getId()));
486    }
487
488    @Override
489    public int hashCode() {
490        return id.hashCode();
491    }
492
493        @Override
494    public String toString() {
495        return getClass().getName() + " {id=" + id + ", subscriptions=" + subscriptions.values() + "}";
496    }
497}