/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * Granite Data Services is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * Granite Data Services is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA, or see <http://www.gnu.org/licenses/>. */ package org.granite.gravity; import java.io.IOException; import java.io.OutputStream; import java.net.SocketException; import java.security.Principal; import java.util.Collection; import java.util.LinkedList; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.granite.context.AMFContextImpl; import org.granite.context.GraniteContext; import org.granite.gravity.udp.UdpReceiver; import org.granite.gravity.udp.UdpReceiverFactory; import org.granite.logging.Logger; import org.granite.messaging.webapp.HttpGraniteContext; import org.granite.util.ContentType; import flex.messaging.messages.AsyncMessage; import flex.messaging.messages.Message; /** * @author Franck WOLFF */ public abstract class AbstractChannel implements Channel { /////////////////////////////////////////////////////////////////////////// // Fields. private static final Logger log = Logger.getLogger(AbstractChannel.class); protected final String id; protected final String sessionId; protected final String clientType; protected final GravityInternal gravity; protected final ChannelFactory<? extends Channel> factory; // protected final ServletConfig servletConfig; protected final ConcurrentMap<String, Subscription> subscriptions = new ConcurrentHashMap<String, Subscription>(); protected Principal userPrincipal; protected LinkedList<AsyncPublishedMessage> publishedQueue = new LinkedList<AsyncPublishedMessage>(); protected final Lock publishedQueueLock = new ReentrantLock(); protected LinkedList<AsyncMessage> receivedQueue = new LinkedList<AsyncMessage>(); protected final Lock receivedQueueLock = new ReentrantLock(); protected final AsyncPublisher publisher; protected final AsyncReceiver receiver; protected UdpReceiver udpReceiver = null; /////////////////////////////////////////////////////////////////////////// // Constructor. protected AbstractChannel(GravityInternal gravity, String id, ChannelFactory<? extends Channel> factory, String clientType) { if (id == null) throw new NullPointerException("id cannot be null"); this.id = id; GraniteContext graniteContext = GraniteContext.getCurrentInstance(); this.clientType = clientType; this.sessionId = graniteContext != null ? graniteContext.getSessionId() : null; this.gravity = gravity; this.factory = factory; this.publisher = new AsyncPublisher(this); this.receiver = new AsyncReceiver(this); } /////////////////////////////////////////////////////////////////////////// // Abstract protected method. protected abstract boolean hasAsyncHttpContext(); protected abstract AsyncHttpContext acquireAsyncHttpContext(); protected abstract void releaseAsyncHttpContext(AsyncHttpContext context); /////////////////////////////////////////////////////////////////////////// // Channel interface implementation. public String getId() { return id; } public String getClientType() { return clientType; } public ChannelFactory<? extends Channel> getFactory() { return factory; } public GravityInternal getGravity() { return gravity; } public Subscription addSubscription(String destination, String subTopicId, String subscriptionId, boolean noLocal) { Subscription subscription = new Subscription(this, destination, subTopicId, subscriptionId, noLocal); Subscription present = subscriptions.putIfAbsent(subscriptionId, subscription); return (present != null ? present : subscription); } public Collection<Subscription> getSubscriptions() { return subscriptions.values(); } public Subscription removeSubscription(String subscriptionId) { return subscriptions.remove(subscriptionId); } public boolean isConnected() { return true; } public boolean isAuthenticated() { return userPrincipal != null; } public Principal getUserPrincipal() { return userPrincipal; } public void setUserPrincipal(Principal principal) { this.userPrincipal = principal; gravity.notifyAuthenticated(this, principal); } public void publish(AsyncPublishedMessage message) throws MessagePublishingException { if (message == null) throw new NullPointerException("message cannot be null"); publishedQueueLock.lock(); try { publishedQueue.add(message); } finally { publishedQueueLock.unlock(); } publisher.queue(getGravity()); } public boolean hasPublishedMessage() { publishedQueueLock.lock(); try { return !publishedQueue.isEmpty(); } finally { publishedQueueLock.unlock(); } } public boolean runPublish() { LinkedList<AsyncPublishedMessage> publishedCopy = null; publishedQueueLock.lock(); try { if (publishedQueue.isEmpty()) return false; publishedCopy = publishedQueue; publishedQueue = new LinkedList<AsyncPublishedMessage>(); } finally { publishedQueueLock.unlock(); } for (AsyncPublishedMessage message : publishedCopy) { try { message.publish(this); } catch (Exception e) { log.error(e, "Error while trying to publish message: %s", message); } } return true; } public void receive(AsyncMessage message) throws MessageReceivingException { if (message == null) throw new NullPointerException("message cannot be null"); GravityInternal gravity = getGravity(); if (udpReceiver != null) { if (udpReceiver.isClosed()) return; try { udpReceiver.receive(message); } catch (MessageReceivingException e) { if (e.getCause() instanceof SocketException) { log.debug(e, "Closing unreachable UDP channel %s", getId()); udpReceiver.close(false); } else log.error(e, "Cannot access UDP channel %s", getId()); } return; } receivedQueueLock.lock(); try { if (receivedQueue.size() + 1 > gravity.getGravityConfig().getMaxMessagesQueuedPerChannel()) throw new MessageReceivingException(message, "Could not queue message (channel's queue is full) for channel: " + this); log.debug("Channel %s queue message %s for client %s", getId(), message.getMessageId(), message.getClientId()); receivedQueue.add(message); } finally { receivedQueueLock.unlock(); } if (hasAsyncHttpContext()) receiver.queue(gravity); } public boolean hasReceivedMessage() { receivedQueueLock.lock(); try { return !receivedQueue.isEmpty(); } finally { receivedQueueLock.unlock(); } } public boolean runReceive() { return runReceived(null); } protected void createUdpReceiver(UdpReceiverFactory factory, AsyncHttpContext asyncHttpContext) { OutputStream os = null; try { Message connectMessage = asyncHttpContext.getConnectMessage(); if (udpReceiver == null || udpReceiver.isClosed()) udpReceiver = factory.newReceiver(this, asyncHttpContext.getRequest(), connectMessage); AsyncMessage reply = udpReceiver.acknowledge(connectMessage); HttpServletRequest request = asyncHttpContext.getRequest(); HttpServletResponse response = asyncHttpContext.getResponse(); GraniteContext context = HttpGraniteContext.createThreadIntance( gravity.getGraniteConfig(), gravity.getServicesConfig(), null, request, response ); ((AMFContextImpl)context.getAMFContext()).setCurrentAmf3Message(asyncHttpContext.getConnectMessage()); GravityServletUtil.serialize(gravity, response, new AsyncMessage[] { reply }, ContentType.forMimeType(request.getContentType())); } catch (ServletException e) { log.error(e, "Could not send UDP connect acknowledgement to channel: %s", this); } catch (IOException e) { log.error(e, "Could not send UDP connect acknowledgement to channel: %s", this); } finally { try { GraniteContext.release(); } catch (Exception e) { // should never happen... } // Close output stream. try { if (os != null) { try { os.close(); } catch (IOException e) { log.warn(e, "Could not close output stream (ignored)"); } } } finally { releaseAsyncHttpContext(asyncHttpContext); } } } public boolean runReceived(AsyncHttpContext asyncHttpContext) { GravityInternal gravity = getGravity(); if (asyncHttpContext != null && gravity.hasUdpReceiverFactory()) { UdpReceiverFactory factory = gravity.getUdpReceiverFactory(); if (factory.isUdpConnectRequest(asyncHttpContext.getConnectMessage())) { createUdpReceiver(factory, asyncHttpContext); return true; } if (udpReceiver != null) { if (!udpReceiver.isClosed()) udpReceiver.close(false); udpReceiver = null; } } boolean httpAsParam = (asyncHttpContext != null); LinkedList<AsyncMessage> messages = null; OutputStream os = null; try { receivedQueueLock.lock(); try { // Do we have any pending messages? if (receivedQueue.isEmpty()) return false; // Do we have a valid http context? if (asyncHttpContext == null) { asyncHttpContext = acquireAsyncHttpContext(); if (asyncHttpContext == null) return false; } // Both conditions are ok, get all pending messages. messages = receivedQueue; receivedQueue = new LinkedList<AsyncMessage>(); } finally { receivedQueueLock.unlock(); } HttpServletRequest request = asyncHttpContext.getRequest(); HttpServletResponse response = asyncHttpContext.getResponse(); // Set response messages correlation ids to connect request message id. String correlationId = asyncHttpContext.getConnectMessage().getMessageId(); AsyncMessage[] messagesArray = new AsyncMessage[messages.size()]; int i = 0; for (AsyncMessage message : messages) { message.setCorrelationId(correlationId); messagesArray[i++] = message; } // Setup serialization context (thread local) GraniteContext context = HttpGraniteContext.createThreadIntance( gravity.getGraniteConfig(), gravity.getServicesConfig(), null, request, response ); ((AMFContextImpl)context.getAMFContext()).setCurrentAmf3Message(asyncHttpContext.getConnectMessage()); // Write messages to response output stream. GravityServletUtil.serialize(gravity, response, messagesArray, ContentType.forMimeType(request.getContentType())); return true; // Messages were delivered, http context isn't valid anymore. } catch (ServletException e) { log.error(e, "Configuration error for channel: %s", getId()); return true; } catch (IOException e) { log.warn(e, "Could not send messages to channel: %s (retrying later)", getId()); GravityConfig gravityConfig = getGravity().getGravityConfig(); if (gravityConfig.isRetryOnError()) { receivedQueueLock.lock(); try { if (receivedQueue.size() + messages.size() > gravityConfig.getMaxMessagesQueuedPerChannel()) { log.warn( "Channel %s has reached its maximum queue capacity %s (throwing %s messages)", getId(), gravityConfig.getMaxMessagesQueuedPerChannel(), messages.size() ); } else receivedQueue.addAll(0, messages); } finally { receivedQueueLock.unlock(); } } return true; // Messages weren't delivered, but http context isn't valid anymore. } finally { // Cleanup serialization context (thread local) try { GraniteContext.release(); } catch (Exception e) { // should never happen... } // Close output stream. try { if (os != null) { try { os.close(); } catch (IOException e) { log.warn(e, "Could not close output stream (ignored)"); } } } finally { // Cleanup http context (only if this method wasn't explicitly called with a non null // AsyncHttpContext from the servlet). if (!httpAsParam) releaseAsyncHttpContext(asyncHttpContext); } } } public final void destroy() { destroy(false); } public void destroy(boolean timeout) { try { GravityInternal gravity = getGravity(); gravity.cancel(publisher); gravity.cancel(receiver); subscriptions.clear(); } finally { if (udpReceiver != null) { if (!udpReceiver.isClosed()) udpReceiver.close(timeout); udpReceiver = null; } } } /////////////////////////////////////////////////////////////////////////// // Protected utilities. protected boolean queueReceiver() { if (hasReceivedMessage()) { receiver.queue(getGravity()); return true; } return false; } /////////////////////////////////////////////////////////////////////////// // Object overwritten methods. @Override public boolean equals(Object obj) { return (obj instanceof Channel && id.equals(((Channel)obj).getId())); } @Override public int hashCode() { return id.hashCode(); } @Override public String toString() { return getClass().getName() + " {id=" + id + ", subscriptions=" + subscriptions.values() + "}"; } }