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    }