/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2008-2010 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package com.sun.grizzly.messagesbus; import java.io.IOException; import java.io.UnsupportedEncodingException; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.sun.grizzly.comet.CometContext; import com.sun.grizzly.comet.CometEngine; import com.sun.grizzly.comet.CometHandler; import com.sun.grizzly.comet.NotificationHandler; import java.util.logging.Level; import java.util.logging.Logger; /** * Servlet implementation of the Grizzly Comet Protocol (GCP). The GCP protocol * is a very basic protocol that can be used by browser to share data, using the * comet technique, between serveral clients without having to poll for it. * * The protocol is very simple. First, a client must subscribe to a topic: * <p><pre><code> * http://host:port/contextPath? * subscribe=[topic name]&cometTechnique=[polling|log-polling|http-streaming]&message[text] * * Mandatory: subscribe and cometTechnique. * </code></pre></p> * When issuing the URL above, the connection will be automatically suspended * based on the <strong>cometTechnique</strong> specified. To share data * between application, a browser just need to send the following request: * <p><pre><code> * http://host:port/contextPath?publish=[topic name]&message[text] * </code></pre></p> * The Servlet can be used as it is or extended to add extra features like * filtering messages, security, login, etc. * * To use this Servlet, just add in your web.xml: * * <p><pre><code> <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:j2ee="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.4" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <description>Grizzly Messages Bus Servlet</description> <display-name>Grizzly Messages Bus Servlet</display-name> <servlet> <description>MessagesBus</description> <servlet-name>MessagesBus</servlet-name> <servlet-class>com.sun.grizzly.messagesbus.MessagesBus</servlet-class> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>MessagesBus</servlet-name> <url-pattern>/mb</url-pattern> </servlet-mapping> <session-config> <session-timeout>25</session-timeout> </session-config> </web-app> * </code></pre></p> * * By default, a connection will be timed out after a {@link #expirationDelay}. * The default is 30 seconds, but this can be configured by adding, in web.xml: * <p><pre><code> <init-param> <param-name>expirationDelay</param-name> <param-value>60000</param-value> </init-param> * </code></pre></p> * * The Grizzly Comet {@link NotificationHandler} can also be replaced by adding: * <p><pre><code> <init-param> <param-name>notificationHandler</param-name> <param-value>com.foo.bar.ScriptFilterNotificationHandler</param-value> </init-param> * </code></pre></p> * * A request can also be automatically suspended when the request URI only * contains the <strong>publish</strong> GCP action. * <p><pre><code> <init-param> <param-name>suspendOnTheFly</param-name> <param-value>true</param-value> </init-param> * </code></pre></p> * * * @author Jeanfrancois Arcand */ public class MessagesBus extends HttpServlet { /** * The Comet technique supported. */ public enum CometType { POLLING, LONG_POLLING, HTTP_STREAMING } // 30 seconds by default. private long expirationDelay = 30 * 1000; /** * The specified {@link NotificationHandler}. When not specified, * * */ private NotificationHandler notificationHandler = null; /** * Default Logger. */ private static final Logger logger = Logger.getAnonymousLogger(); /** * <tt>true</tt> if a connection can be automatically suspended * when the request only contains the GCP message <strong>publish</strong> * Default is <tt>true</tt> */ private boolean suspendOnTheFly = true; /** * {@inheritDoc} */ @Override public void init(ServletConfig config) throws ServletException { String ed = config.getInitParameter("expirationDelay"); if (ed != null) { expirationDelay = Long.parseLong(ed); } String nh = config.getInitParameter("notificationHandler"); if (ed != null) { notificationHandler = (NotificationHandler)loadClass(nh); } String stf = config.getInitParameter("suspendOnTheFly"); if (stf != null) { suspendOnTheFly = Boolean.valueOf(stf); } } /** * {@inheritDoc} */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { doPost(req, res); } /** * Basic implementation of the Grizzly Comet Protocol (GCP). * @param req The request * @param res The response * @throws javax.servlet.ServletException * @throws java.io.IOException */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { CometType cometType = suspend(req, res); res.setContentType("text/html"); // The connection has been suspended. if (cometType != null && cometType != CometType.POLLING) { res.flushBuffer(); return; } push(req,res); } /** * Inspect the request, looking for the GCP parameters <strong>publish</strong> * and <strong>message</strong>. If those parameters are included, the * message will be pushed to all subscriber of the publish's value. * * @param req The http request * @param res The http response. * @throws java.io.IOException */ protected synchronized void push(HttpServletRequest req, HttpServletResponse res) throws IOException { String message = req.getParameter("message"); String topic = req.getParameter("publish"); // Nothing to send if (message == null || topic == null) { return; } CometContext context = CometEngine.getEngine().getCometContext(topic); if (context == null && suspendOnTheFly) { context = createCometContext(topic); CometType cometType = CometType.LONG_POLLING; CometHandler cometHandler = new MessagesBusCometHandler(context, cometType); cometHandler.attach(res); context.addCometHandler(cometHandler); } else if (!suspendOnTheFly) { if (logger.isLoggable(Level.INFO)) { logger.info("Cannot create message topic:" + topic + " on the fly. You first have to " + "subscribe"); } return; } context.notify(message); } /** * Suspend the connection if the request contains the GCP * <strong>suscribe</strong> and <strong>cometTechnique</strong> action. * @param req * @param res * @return {@link CometType} used to suspend the connection. * @throws java.io.UnsupportedEncodingException * @throws java.io.IOException */ protected synchronized CometType suspend(HttpServletRequest req, HttpServletResponse res) throws UnsupportedEncodingException, IOException { String cometTechnique = req.getParameter("cometTechnique"); String topic = req.getParameter("subscribe"); String message = req.getParameter("message"); // This is not a request for suspending the connection. if (cometTechnique == null || topic == null) { return CometType.POLLING; } if (logger.isLoggable(Level.FINE)) { logger.fine("CometTechnique: " + cometTechnique + " topic:" + topic); } if (cometTechnique.equals("polling")) { return CometType.POLLING; } CometContext context = CometEngine.getEngine().getCometContext(topic); if (context == null) { context = createCometContext(topic); } CometType cometType = null; if (cometTechnique.equals("long-polling")) { cometType = CometType.LONG_POLLING; } else if (cometTechnique.equals("http-streaming")) { cometType = CometType.HTTP_STREAMING; } CometHandler cometHandler = new MessagesBusCometHandler(context, cometType); cometHandler.attach(res); if (message != null){ context.notify(message); // Echo res.getWriter().write(message); } context.addCometHandler(cometHandler); return cometType; } /** * Create a Grizzly Comet {@link CometContext} * @param topic The name of the {@link CometContext} * @return a cached or newly created {@link CometContext} */ protected final CometContext createCometContext(String topic){ CometContext context = CometEngine.getEngine().register(topic); if (notificationHandler != null){ context.setNotificationHandler(notificationHandler); } context.setExpirationDelay(expirationDelay); context.setBlockingNotification(true); return context; } /** * Util to load classes using reflection. */ private static Object loadClass(String clazzName) { Class className = null; try { className = Class.forName(clazzName, true, Thread.currentThread().getContextClassLoader()); return className.newInstance(); } catch (Throwable t) { logger.log(Level.SEVERE,"Invalid NotificationHandler",t); } return null; } /** * Return the time a connection can stay idle. * @return */ public long getExpirationDelay() { return expirationDelay; } /** * Set the maximum idle time a connection can stay suspended. * @param expirationDelay */ public void setExpirationDelay(long expirationDelay) { this.expirationDelay = expirationDelay; } }