diff --git a/.gitignore b/.gitignore index bcae39a..49134a8 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ war build.properties target com.glines.socketio.sample.gwtchat.GWTChat +samples/chat-gwt/src/main/gwt-unitCache/ \ No newline at end of file diff --git a/README b/README index aee6566..ef49c8e 100644 --- a/README +++ b/README @@ -1 +1,28 @@ Java backend integration for Socket.IO library (http://socket.io/) + +To use in a jetty webapp, grant access in your jetty xml file to the jetty server class org.eclipse.jetty.server.HttpConnection + + + + + + + + -org.eclipse.jetty.server.HttpConnection + -org.eclipse.jetty.continuation. + -org.eclipse.jetty.jndi. + -org.eclipse.jetty.plus.jaas. + -org.eclipse.jetty.websocket. + -org.eclipse.jetty.servlet.DefaultServlet + org.eclipse.jetty. + + + + + +More info: + +mvn eclipse:eclipse # for import the project in eclipse +mvn install -Dlicense.skip=true # to install the jars in your repo + +Lauch from eclipse the Start* sample classes to test it. diff --git a/core/pom.xml b/core/pom.xml index f899f09..1ed748a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -7,7 +7,7 @@ com.glines.socketio socketio - 0.1-SNAPSHOT + 0.2.1 .. @@ -19,10 +19,15 @@ - org.testatoo.container - testatoo-container-jetty + org.mortbay.jetty + servlet-api provided + + log4j + log4j + true + diff --git a/core/src/main/java/com/glines/socketio/annotation/Handle.java b/core/src/main/java/com/glines/socketio/annotation/Handle.java new file mode 100644 index 0000000..48d1933 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/annotation/Handle.java @@ -0,0 +1,39 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.annotation; + +import com.glines.socketio.server.TransportType; + +import java.lang.annotation.*; + +/** + * @author Mathieu Carbou (mathieu.carbou@gmail.com) + */ +@Target(ElementType.TYPE) +@Inherited +@Retention(RetentionPolicy.RUNTIME) +public @interface Handle { + TransportType[] value() default {}; +} diff --git a/core/src/main/java/com/glines/socketio/client/common/SocketIOConnection.java b/core/src/main/java/com/glines/socketio/client/common/SocketIOConnection.java index a779f95..8cc11ef 100644 --- a/core/src/main/java/com/glines/socketio/client/common/SocketIOConnection.java +++ b/core/src/main/java/com/glines/socketio/client/common/SocketIOConnection.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -28,17 +30,11 @@ public interface SocketIOConnection { interface Factory { - SocketIOConnection create(SocketIOConnection.SocketIOConnectionListener listener, + SocketIOConnection create(SocketIOConnectionListener listener, String host, short port); } - interface SocketIOConnectionListener { - public abstract void onConnect(); - public abstract void onDisconnect(DisconnectReason reason, String errorMessage); - public abstract void onMessage(int messageType, String message); - } - - /** + /** * Initiate a connection attempt. If the connection succeeds, then the * {@link SocketIOConnectionListener#onConnect() onConnect} will be called. If the connection * attempt fails, then @@ -81,7 +77,7 @@ interface SocketIOConnectionListener { * * @param message * @throws IllegalStateException if the socket is not CONNECTED. - * @throws SocketIOMessageParserException if the message type parser encode() failed. + * @throws SocketIOException if the message type parser encode() failed. */ void sendMessage(int messageType, String message) throws SocketIOException; } diff --git a/core/src/main/java/com/glines/socketio/client/common/SocketIOConnectionListener.java b/core/src/main/java/com/glines/socketio/client/common/SocketIOConnectionListener.java new file mode 100644 index 0000000..4f72ba7 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/client/common/SocketIOConnectionListener.java @@ -0,0 +1,36 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.client.common; + +import com.glines.socketio.common.DisconnectReason; + +/** +* @author Mathieu Carbou (mathieu.carbou@gmail.com) +*/ +public interface SocketIOConnectionListener { + void onConnect(); + void onDisconnect(DisconnectReason reason, String errorMessage); + void onMessage(int messageType, String message); +} diff --git a/core/src/main/java/com/glines/socketio/common/ConnectionState.java b/core/src/main/java/com/glines/socketio/common/ConnectionState.java index 823a79f..301e224 100644 --- a/core/src/main/java/com/glines/socketio/common/ConnectionState.java +++ b/core/src/main/java/com/glines/socketio/common/ConnectionState.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights diff --git a/core/src/main/java/com/glines/socketio/common/DisconnectReason.java b/core/src/main/java/com/glines/socketio/common/DisconnectReason.java index eb70844..20de289 100644 --- a/core/src/main/java/com/glines/socketio/common/DisconnectReason.java +++ b/core/src/main/java/com/glines/socketio/common/DisconnectReason.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -30,7 +32,7 @@ public enum DisconnectReason { CLOSE_FAILED(4), // The connection dropped before an orderly close could complete. ERROR(5), // A GET or POST returned an error, or an internal error occurred. CLOSED_REMOTELY(6), // Remote end point initiated a close. - CLOSED(6); // Locally initiated close succeeded. + CLOSED(7); // Locally initiated close succeeded. private int value; private DisconnectReason(int v) { this.value = v; } diff --git a/core/src/main/java/com/glines/socketio/common/SocketIOException.java b/core/src/main/java/com/glines/socketio/common/SocketIOException.java index 220957f..694dcf9 100644 --- a/core/src/main/java/com/glines/socketio/common/SocketIOException.java +++ b/core/src/main/java/com/glines/socketio/common/SocketIOException.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights diff --git a/core/src/main/java/com/glines/socketio/server/AbstractTransport.java b/core/src/main/java/com/glines/socketio/server/AbstractTransport.java new file mode 100644 index 0000000..daa1ec4 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/AbstractTransport.java @@ -0,0 +1,75 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +import javax.servlet.ServletConfig; + +/** + * @author Mathieu Carbou + */ +public abstract class AbstractTransport implements Transport { + + private ServletConfig servletConfig; + private SocketIOConfig config; + private TransportHandlerProvider transportHandlerProvider; + + @Override + public void destroy() { + } + + @Override + public final void init(ServletConfig config) throws TransportInitializationException { + this.servletConfig = config; + this.config = new ServletBasedSocketIOConfig(servletConfig, getType().toString()); + init(); + } + + protected final ServletConfig getServletConfig() { + return servletConfig; + } + + protected final SocketIOConfig getConfig() { + return config; + } + + protected void init() throws TransportInitializationException { + } + + protected final TransportHandler newHandler(Class type, SocketIOSession session) { + TransportHandler handler = transportHandlerProvider.get(type, getType()); + handler.setSession(session); + return handler; + } + + @Override + public String toString() { + return getType().toString(); + } + + @Override + public final void setTransportHandlerProvider(TransportHandlerProvider transportHandlerProvider) { + this.transportHandlerProvider = transportHandlerProvider; + } +} diff --git a/core/src/main/java/com/glines/socketio/server/AbstractTransportHandler.java b/core/src/main/java/com/glines/socketio/server/AbstractTransportHandler.java new file mode 100644 index 0000000..0fe091d --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/AbstractTransportHandler.java @@ -0,0 +1,60 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +/** + * @author Mathieu Carbou + */ +public abstract class AbstractTransportHandler implements TransportHandler { + + private SocketIOConfig config; + private SocketIOSession session; + + @Override + public final void init(SocketIOConfig config) { + this.config = config; + init(); + } + + @Override + public void setSession(SocketIOSession session) { + this.session = session; + } + + protected final SocketIOConfig getConfig() { + return config; + } + + protected final SocketIOSession getSession() { + return session; + } + + protected void init() { + } + + @Override + public void disconnectWhenEmpty() { + } +} diff --git a/core/src/main/java/com/glines/socketio/server/AnnotationTransportHandlerProvider.java b/core/src/main/java/com/glines/socketio/server/AnnotationTransportHandlerProvider.java new file mode 100644 index 0000000..5f7bf31 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/AnnotationTransportHandlerProvider.java @@ -0,0 +1,108 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +import com.glines.socketio.annotation.Handle; +import com.glines.socketio.util.DefaultLoader; +import com.glines.socketio.util.ServiceClassLoader; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.EnumMap; +import java.util.Iterator; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Mathieu Carbou (mathieu.carbou@gmail.com) + */ +public final class AnnotationTransportHandlerProvider implements TransportHandlerProvider { + + private static final Logger LOGGER = Logger.getLogger(AnnotationTransportHandlerProvider.class.getName()); + + private final Map> handlerClasses = new EnumMap>(TransportType.class); + + @Override + public TransportHandler get(Class handlerType, TransportType transportType) { + Class handlerClass = handlerClasses.get(transportType); + if (handlerClass == null) + throw new IllegalArgumentException("No TransportHandler found for transport " + transportType); + if (!handlerType.isAssignableFrom(handlerClass)) + throw new IllegalArgumentException("TransportHandler " + handlerClass.getName() + " is not of required type " + handlerType.getName() + " for transport " + transportType); + return load(handlerClass); + } + + @Override + public boolean isSupported(TransportType type) { + return handlerClasses.containsKey(type); + } + + @Override + public void init() { + handlerClasses.clear(); + ServiceClassLoader serviceClassLoader = ServiceClassLoader.load( + TransportHandler.class, + new DefaultLoader(Thread.currentThread().getContextClassLoader())); + Iterator> it = serviceClassLoader.iterator(); + while (it.hasNext()) { + Class transportHandlerClass; + try { + transportHandlerClass = it.next(); + } catch (Throwable e) { + LOGGER.log(Level.INFO, "Unable to load transport hander class. Error: " + e.getMessage(), e); + continue; + } + // try to load it to see if it is available + if (load(transportHandlerClass) != null) { + Handle handle = transportHandlerClass.getAnnotation(Handle.class); + if (handle != null) { + for (TransportType type : handle.value()) { + handlerClasses.put(type, transportHandlerClass); + } + } + } + } + } + + private static TransportHandler load(Class c) { + try { + Constructor ctor = c.getConstructor(); + return ctor.newInstance(); + } catch (InvocationTargetException e) { + LOGGER.log(Level.INFO, "Unable to load transport handler class " + c.getName() + ". Error: " + e.getTargetException().getMessage(), e.getTargetException()); + } catch (Exception e) { + LOGGER.log(Level.INFO, "Unable to load transport handler class " + c.getName() + ". Error: " + e.getMessage(), e); + } + return null; + } + + @Override + public Map> listAll() { + return handlerClasses; + } + +} diff --git a/core/src/main/java/com/glines/socketio/server/ClasspathTransportDiscovery.java b/core/src/main/java/com/glines/socketio/server/ClasspathTransportDiscovery.java new file mode 100644 index 0000000..62113fc --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/ClasspathTransportDiscovery.java @@ -0,0 +1,72 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +import com.glines.socketio.util.DefaultLoader; +import com.glines.socketio.util.ServiceClassLoader; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Mathieu Carbou + */ +public final class ClasspathTransportDiscovery implements TransportDiscovery { + + private static final Logger LOGGER = Logger.getLogger(ClasspathTransportDiscovery.class.getName()); + + @Override + public Iterable discover() { + List transports = new LinkedList(); + // discover transports lazily + ServiceClassLoader serviceClassLoader = ServiceClassLoader.load( + Transport.class, + new DefaultLoader(Thread.currentThread().getContextClassLoader())); + Iterator> it = serviceClassLoader.iterator(); + while (it.hasNext()) { + Class transportClass; + try { + transportClass = it.next(); + } catch (Throwable e) { + LOGGER.log(Level.INFO, "Unable to load transport class: Error: " + e.getMessage(), e); + continue; + } + try { + Constructor ctor = transportClass.getConstructor(); + transports.add(ctor.newInstance()); + } catch (InvocationTargetException e) { + LOGGER.log(Level.INFO, "Unable to load transport class " + transportClass.getName() + ". Error: " + e.getTargetException().getMessage(), e.getTargetException()); + } catch (Throwable e) { + LOGGER.log(Level.INFO, "Unable to load transport class " + transportClass.getName() + ". Error: " + e.getMessage(), e); + } + } + return transports; + } +} diff --git a/core/src/main/java/com/glines/socketio/server/ConnectableTransportHandler.java b/core/src/main/java/com/glines/socketio/server/ConnectableTransportHandler.java new file mode 100644 index 0000000..14c1f94 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/ConnectableTransportHandler.java @@ -0,0 +1,39 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +import com.glines.socketio.server.transport.DataHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Mathieu Carbou (mathieu.carbou@gmail.com) + */ +public interface ConnectableTransportHandler { + void connect(HttpServletRequest request, HttpServletResponse response) throws IOException; + void setDataHandler(DataHandler dataHandler); +} diff --git a/core/src/main/java/com/glines/socketio/server/DefaultSession.java b/core/src/main/java/com/glines/socketio/server/DefaultSession.java new file mode 100644 index 0000000..9bd639f --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/DefaultSession.java @@ -0,0 +1,354 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +import com.glines.socketio.common.ConnectionState; +import com.glines.socketio.common.DisconnectReason; +import com.glines.socketio.common.SocketIOException; + +import java.util.Map; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Mathieu Carbou (mathieu.carbou@gmail.com) + */ +class DefaultSession implements SocketIOSession { + + private static final int SESSION_ID_LENGTH = 20; + private static final Logger LOGGER = Logger.getLogger(DefaultSession.class.getName()); + + private final SocketIOSessionManager socketIOSessionManager; + private final String sessionId; + private final AtomicLong messageId = new AtomicLong(0); + private final Map attributes = new ConcurrentHashMap(); + + private SocketIOInbound inbound; + private TransportHandler handler; + private ConnectionState state = ConnectionState.CONNECTING; + private long hbDelay; + private SessionTask hbDelayTask; + private long timeout; + private SessionTask timeoutTask; + private boolean timedout; + private String closeId; + + DefaultSession(SocketIOSessionManager socketIOSessionManager, SocketIOInbound inbound, String sessionId) { + this.socketIOSessionManager = socketIOSessionManager; + this.inbound = inbound; + this.sessionId = sessionId; + } + + @Override + public void setAttribute(String key, Object val) { + attributes.put(key, val); + } + + @Override + public Object getAttribute(String key) { + return attributes.get(key); + } + + @Override + public String getSessionId() { + return sessionId; + } + + @Override + public ConnectionState getConnectionState() { + return state; + } + + @Override + public SocketIOInbound getInbound() { + return inbound; + } + + @Override + public TransportHandler getTransportHandler() { + return handler; + } + + private void onTimeout() { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "Session[" + sessionId + "]: onTimeout"); + if (!timedout) { + timedout = true; + state = ConnectionState.CLOSED; + onDisconnect(DisconnectReason.TIMEOUT); + handler.abort(); + } + } + + @Override + public void startTimeoutTimer() { + clearTimeoutTimer(); + if (!timedout && timeout > 0) { + timeoutTask = scheduleTask(new Runnable() { + @Override + public void run() { + DefaultSession.this.onTimeout(); + } + }, timeout); + } + } + + @Override + public void clearTimeoutTimer() { + if (timeoutTask != null) { + timeoutTask.cancel(); + timeoutTask = null; + } + } + + private void sendHeartBeat() { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "Session[" + sessionId + "]: send heartbeat "); + try { + handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.HEARTBEAT, 0, "")); + } catch (SocketIOException e) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "handler.sendMessage failed: ", e); + handler.abort(); + } + startTimeoutTimer(); + } + + @Override + public void startHeartbeatTimer() { + clearHeartbeatTimer(); + if (!timedout && hbDelay > 0) { + hbDelayTask = scheduleTask(new Runnable() { + @Override + public void run() { + sendHeartBeat(); + } + }, hbDelay); + } + } + + @Override + public void clearHeartbeatTimer() { + if (hbDelayTask != null) { + hbDelayTask.cancel(); + hbDelayTask = null; + } + } + + @Override + public void setHeartbeat(long delay) { + hbDelay = delay; + } + + @Override + public long getHeartbeat() { + return hbDelay; + } + + @Override + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + @Override + public long getTimeout() { + return timeout; + } + + @Override + public void startClose() { + state = ConnectionState.CLOSING; + closeId = "server"; + try { + handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, closeId)); + } catch (SocketIOException e) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "handler.sendMessage failed: ", e); + handler.abort(); + } + } + + @Override + public void onMessage(SocketIOFrame message) { + switch (message.getFrameType()) { + case CONNECT: + onPing(message.getData()); + case HEARTBEAT: + // Ignore this message type as they are only intended to be from server to client. + startHeartbeatTimer(); + break; + case CLOSE: + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "Session[" + sessionId + "]: onClose: " + message.getData()); + onClose(message.getData()); + break; + case MESSAGE: + case JSON_MESSAGE: + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "Session[" + sessionId + "]: onMessage: " + message.getData()); + onMessage(message.getData()); + break; + default: + // Ignore unknown message types + break; + } + } + + @Override + public void onPing(String data) { + try { + handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.CONNECT, 0, data)); + } catch (SocketIOException e) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "handler.sendMessage failed: ", e); + handler.abort(); + } + } + + @Override + public void onPong(String data) { + clearTimeoutTimer(); + } + + @Override + public void onClose(String data) { + if (state == ConnectionState.CLOSING) { + if (closeId != null && closeId.equals(data)) { + state = ConnectionState.CLOSED; + onDisconnect(DisconnectReason.CLOSED); + handler.abort(); + } else { + try { + handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, data)); + } catch (SocketIOException e) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "handler.sendMessage failed: ", e); + handler.abort(); + } + } + } else { + state = ConnectionState.CLOSING; + try { + handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, data)); + handler.disconnectWhenEmpty(); + if ("client".equals(data)) + onDisconnect(DisconnectReason.CLOSED_REMOTELY); + } catch (SocketIOException e) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "handler.sendMessage failed: ", e); + handler.abort(); + } + } + } + + @Override + public SessionTask scheduleTask(Runnable task, long delay) { + final Future future = socketIOSessionManager.executor.schedule(task, delay, TimeUnit.MILLISECONDS); + return new SessionTask() { + @Override + public boolean cancel() { + return future.cancel(false); + } + }; + } + + @Override + public void onConnect(TransportHandler handler) { + if (handler == null) { + state = ConnectionState.CLOSED; + inbound = null; + socketIOSessionManager.socketIOSessions.remove(sessionId); + } else if (this.handler == null) { + this.handler = handler; + if (inbound == null) { + state = ConnectionState.CLOSED; + handler.abort(); + } else { + try { + state = ConnectionState.CONNECTED; + inbound.onConnect(handler); + startHeartbeatTimer(); + } catch (Throwable e) { + if (LOGGER.isLoggable(Level.WARNING)) + LOGGER.log(Level.WARNING, "Session[" + sessionId + "]: Exception thrown by SocketIOInbound.onConnect()", e); + state = ConnectionState.CLOSED; + handler.abort(); + } + } + } else { + handler.abort(); + } + } + + @Override + public void onMessage(String message) { + if (inbound != null) { + try { + inbound.onMessage(SocketIOFrame.TEXT_MESSAGE_TYPE, message); + } catch (Throwable e) { + if (LOGGER.isLoggable(Level.WARNING)) + LOGGER.log(Level.WARNING, "Session[" + sessionId + "]: Exception thrown by SocketIOInbound.onMessage()", e); + } + } + } + + @Override + public void onDisconnect(DisconnectReason reason) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "Session[" + sessionId + "]: onDisconnect: " + reason); + clearTimeoutTimer(); + clearHeartbeatTimer(); + if (inbound != null) { + state = ConnectionState.CLOSED; + try { + inbound.onDisconnect(reason, null); + } catch (Throwable e) { + if (LOGGER.isLoggable(Level.WARNING)) + LOGGER.log(Level.WARNING, "Session[" + sessionId + "]: Exception thrown by SocketIOInbound.onDisconnect()", e); + } + inbound = null; + } + } + + @Override + public void onShutdown() { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "Session[" + sessionId + "]: onShutdown"); + if (inbound != null) { + if (state == ConnectionState.CLOSING) { + if (closeId != null) { + onDisconnect(DisconnectReason.CLOSE_FAILED); + } else { + onDisconnect(DisconnectReason.CLOSED_REMOTELY); + } + } else { + onDisconnect(DisconnectReason.ERROR); + } + } + socketIOSessionManager.socketIOSessions.remove(sessionId); + } +} diff --git a/core/src/main/java/com/glines/socketio/server/ServletBasedSocketIOConfig.java b/core/src/main/java/com/glines/socketio/server/ServletBasedSocketIOConfig.java new file mode 100644 index 0000000..7672617 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/ServletBasedSocketIOConfig.java @@ -0,0 +1,144 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +import javax.servlet.ServletConfig; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Mathieu Carbou + */ +public final class ServletBasedSocketIOConfig implements SocketIOConfig { + + private static final Logger LOGGER = Logger.getLogger(ServletBasedSocketIOConfig.class.getName()); + private static final String KEY_TRANSPORT = SocketIOConfig.class.getName() + ".TRANSPORTS"; + + private final ServletConfig config; + private final String namespace; + + public ServletBasedSocketIOConfig(ServletConfig config, String namespace) { + this.namespace = namespace; + this.config = config; + } + + @Override + public long getHeartbeatDelay(long def) { + return getLong(PARAM_HEARTBEAT_DELAY, def); + } + + @Override + public long getHeartbeatTimeout(long def) { + return getLong(PARAM_HEARTBEAT_TIMEOUT, def); + } + + @Override + public long getTimeout(long def) { + return getLong(PARAM_TIMEOUT, def); + } + + @Override + public int getBufferSize() { + return getInt(PARAM_BUFFER_SIZE, DEFAULT_BUFFER_SIZE); + } + + @Override + public int getMaxIdle() { + return getInt(PARAM_MAX_IDLE, DEFAULT_MAX_IDLE); + } + + @Override + public void addTransport(Transport transport) { + getTransportMap().put(transport.getType(), transport); + } + + @Override + public Collection getTransports() { + return getTransportMap().values(); + } + + @Override + public Transport getTransport(TransportType type) { + return getTransportMap().get(type); + } + + @Override + public void removeTransport(TransportType type) { + getTransportMap().remove(type); + } + + @Override + public Transport getWebSocketTransport() { + return getTransport(TransportType.WEB_SOCKET); + } + + @Override + public int getInt(String param, int def) { + String v = getString(param); + return v == null ? def : Integer.parseInt(v); + } + + @Override + public long getLong(String param, long def) { + String v = getString(param); + return v == null ? def : Long.parseLong(v); + } + + @Override + public String getNamespace() { + return namespace; + } + + @Override + public String getString(String param) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("Getting InitParameter: " + namespace + "." + param); + String v = config.getInitParameter(namespace + "." + param); + if (v == null) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("Fallback to InitParameter: " + param); + v = config.getInitParameter(param); + } + return v; + } + + @Override + public String getString(String param, String def) { + String v = getString(param); + return v == null ? def : v; + } + + @SuppressWarnings({"unchecked"}) + private Map getTransportMap() { + Map transports = (Map) config.getServletContext().getAttribute(KEY_TRANSPORT); + if (transports == null) + config.getServletContext().setAttribute(KEY_TRANSPORT, transports = new ConcurrentHashMap()); + return transports; + } + +} diff --git a/core/src/main/java/com/glines/socketio/server/SessionManager.java b/core/src/main/java/com/glines/socketio/server/SessionManager.java new file mode 100644 index 0000000..52c4ebe --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/SessionManager.java @@ -0,0 +1,33 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +/** +* @author Mathieu Carbou (mathieu.carbou@gmail.com) +*/ +public interface SessionManager { + SocketIOSession createSession(SocketIOInbound inbound, String sessionId); + SocketIOSession getSession(String sessionId); +} diff --git a/core/src/main/java/com/glines/socketio/server/SocketIOClosedException.java b/core/src/main/java/com/glines/socketio/server/SocketIOClosedException.java index c8ae5d8..3e14dce 100644 --- a/core/src/main/java/com/glines/socketio/server/SocketIOClosedException.java +++ b/core/src/main/java/com/glines/socketio/server/SocketIOClosedException.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights diff --git a/core/src/main/java/com/glines/socketio/server/SocketIOConfig.java b/core/src/main/java/com/glines/socketio/server/SocketIOConfig.java new file mode 100644 index 0000000..b4e882b --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/SocketIOConfig.java @@ -0,0 +1,62 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +import java.util.Collection; + +/** + * @author Mathieu Carbou + */ +public interface SocketIOConfig { + + String PARAM_HEARTBEAT_DELAY = "heartbeat-delay"; + String PARAM_HEARTBEAT_TIMEOUT = "heartbeat-timeout"; + String PARAM_TIMEOUT = "timeout"; + + String PARAM_BUFFER_SIZE = "bufferSize"; + String PARAM_MAX_IDLE = "maxIdleTime"; + + int DEFAULT_BUFFER_SIZE = 8192; + int DEFAULT_MAX_IDLE = 300 * 1000; + + long getHeartbeatDelay(long def); + long getHeartbeatTimeout(long def); + long getTimeout(long def); + int getBufferSize(); + int getMaxIdle(); + + void addTransport(Transport transport); + Collection getTransports(); + Transport getTransport(TransportType type); + void removeTransport(TransportType type); + Transport getWebSocketTransport(); + + String getString(String key); + String getString(String key, String def); + int getInt(String key, int def); + long getLong(String key, long def); + + String getNamespace(); +} diff --git a/core/src/main/java/com/glines/socketio/server/SocketIOFrame.java b/core/src/main/java/com/glines/socketio/server/SocketIOFrame.java index c37fc59..7f5bd2e 100644 --- a/core/src/main/java/com/glines/socketio/server/SocketIOFrame.java +++ b/core/src/main/java/com/glines/socketio/server/SocketIOFrame.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -26,16 +28,20 @@ import java.util.List; public class SocketIOFrame { - public static final char SEPERATOR_CHAR = '~'; + public static final char SEPERATOR_CHAR = ':'; + public static final char MESSAGE_SEPARATOR = '�'; // char code 65535 + public enum FrameType { UNKNOWN(-1), CLOSE(0), - SESSION_ID(1), - HEARTBEAT_INTERVAL(2), - PING(3), - PONG(4), - DATA(0xE), - FRAGMENT(0xF); + CONNECT(1), + HEARTBEAT(2), + MESSAGE(3), + JSON_MESSAGE(4), + EVENT(5), + ACK(6), + ERROR(7), + NOOP(8); private int value; @@ -52,17 +58,21 @@ public static FrameType fromInt(int val) { case 0: return CLOSE; case 1: - return SESSION_ID; + return CONNECT; case 2: - return HEARTBEAT_INTERVAL; + return HEARTBEAT; case 3: - return PING; + return MESSAGE; case 4: - return PONG; - case 0xE: - return DATA; - case 0xF: - return FRAGMENT; + return JSON_MESSAGE; + case 5: + return EVENT; + case 6: + return ACK; + case 7: + return ERROR; + case 8: + return NOOP; default: return UNKNOWN; } @@ -85,61 +95,71 @@ private static boolean isHexDigit(String str, int start, int end) { public static List parse(String data) { List messages = new ArrayList(); - int idx = 0; // Parse the data and silently ignore any part that fails to parse properly. - while (data.length() > idx && data.charAt(idx) == SEPERATOR_CHAR) { - int start = idx + 1; - int end = data.indexOf(SEPERATOR_CHAR, start); + int messageEnd; + int start = 0; + int end = 0; + while (data.length() > start) { + if (data.charAt(start) == MESSAGE_SEPARATOR) { + start += 1; + end = data.indexOf(MESSAGE_SEPARATOR, start); + messageEnd = Integer.parseInt(data.substring(start, end)); + start = end + 1; + end = start + 1; + messageEnd += start; + } + else { + messageEnd = data.length(); + end = start + 1; + } - if (-1 == end || start == end || !isHexDigit(data, start, end)) { + if (!isHexDigit(data, start, end)) { break; } - int mtype = 0; - int ftype = Integer.parseInt(data.substring(start, start + 1), 16); + int ftype = Integer.parseInt(data.substring(start, end)); FrameType frameType = FrameType.fromInt(ftype); if (frameType == FrameType.UNKNOWN) { break; } - if (end - start > 1) { - mtype = Integer.parseInt(data.substring(start + 1, end), 16); + start = end + 1; + end = data.indexOf(SEPERATOR_CHAR, start); + + int messageId = 0; + if (end - start > 1) { + messageId = Integer.parseInt(data.substring(start + 1, end)); } start = end + 1; end = data.indexOf(SEPERATOR_CHAR, start); - if (-1 == end || start == end || !isHexDigit(data, start, end)) { - break; + String endpoint = ""; + if (end - start > 1) { + endpoint = data.substring(start + 1, end); } - - int size = Integer.parseInt(data.substring(start, end), 16); start = end + 1; - end = start + size; + end = messageEnd; - if (data.length() < end) { - break; - } - - messages.add(new SocketIOFrame(frameType, mtype, data.substring(start, end))); - idx = end; + messages.add(new SocketIOFrame(frameType, + frameType == FrameType.MESSAGE ? TEXT_MESSAGE_TYPE : JSON_MESSAGE_TYPE, + data.substring(start, end))); + start = end; } return messages; } - public static String encode(FrameType type, int messageType, String data) { + public static String encode(FrameType type, String data) { StringBuilder str = new StringBuilder(data.length() + 16); - str.append(SEPERATOR_CHAR); str.append(Integer.toHexString(type.value())); - if (messageType != TEXT_MESSAGE_TYPE) { - str.append(Integer.toHexString(messageType)); - } + str.append(SEPERATOR_CHAR); + //str.append("1"); // message id str.append(SEPERATOR_CHAR); - str.append(Integer.toHexString(data.length())); + //str.append(""); // endpoint str.append(SEPERATOR_CHAR); str.append(data); return str.toString(); @@ -168,6 +188,6 @@ public String getData() { } public String encode() { - return encode(frameType, messageType, data); + return encode(frameType, data); } } diff --git a/core/src/main/java/com/glines/socketio/server/SocketIOInbound.java b/core/src/main/java/com/glines/socketio/server/SocketIOInbound.java index dc2cfb6..62275ea 100644 --- a/core/src/main/java/com/glines/socketio/server/SocketIOInbound.java +++ b/core/src/main/java/com/glines/socketio/server/SocketIOInbound.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights diff --git a/core/src/main/java/com/glines/socketio/server/SocketIOOutbound.java b/core/src/main/java/com/glines/socketio/server/SocketIOOutbound.java index ef9725f..6ca17bc 100644 --- a/core/src/main/java/com/glines/socketio/server/SocketIOOutbound.java +++ b/core/src/main/java/com/glines/socketio/server/SocketIOOutbound.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights diff --git a/core/src/main/java/com/glines/socketio/server/SocketIOServlet.java b/core/src/main/java/com/glines/socketio/server/SocketIOServlet.java index 8cc4375..b4727c3 100644 --- a/core/src/main/java/com/glines/socketio/server/SocketIOServlet.java +++ b/core/src/main/java/com/glines/socketio/server/SocketIOServlet.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,119 +24,145 @@ */ package com.glines.socketio.server; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Map; +import com.glines.socketio.util.IO; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; -import org.eclipse.jetty.util.IO; +public abstract class SocketIOServlet extends HttpServlet { -import com.glines.socketio.server.transport.FlashSocketTransport; -import com.glines.socketio.server.transport.HTMLFileTransport; -import com.glines.socketio.server.transport.JSONPPollingTransport; -import com.glines.socketio.server.transport.WebSocketTransport; -import com.glines.socketio.server.transport.XHRMultipartTransport; -import com.glines.socketio.server.transport.XHRPollingTransport; + private static final Logger LOGGER = Logger.getLogger(SocketIOServlet.class.getName()); + private static final long serialVersionUID = 2L; -/** - */ -public abstract class SocketIOServlet extends HttpServlet { - public static final String BUFFER_SIZE_INIT_PARAM = "bufferSize"; - public static final String MAX_IDLE_TIME_INIT_PARAM = "maxIdleTime"; - public static final int BUFFER_SIZE_DEFAULT = 8192; - public static final int MAX_IDLE_TIME_DEFAULT = 300*1000; - private static final long serialVersionUID = 1L; - private SocketIOSessionManager sessionManager = null; - private Map transports = new HashMap(); - - @Override - public void init() throws ServletException { - super.init(); - String str = this.getInitParameter(BUFFER_SIZE_INIT_PARAM); - int bufferSize = str==null ? BUFFER_SIZE_DEFAULT : Integer.parseInt(str); - str = this.getInitParameter(MAX_IDLE_TIME_INIT_PARAM); - int maxIdleTime = str==null ? MAX_IDLE_TIME_DEFAULT : Integer.parseInt(str); - - sessionManager = new SocketIOSessionManager(); - WebSocketTransport websocketTransport = new WebSocketTransport(bufferSize, maxIdleTime); - FlashSocketTransport flashsocketTransport = new FlashSocketTransport(bufferSize, maxIdleTime); - HTMLFileTransport htmlFileTransport = new HTMLFileTransport(bufferSize, maxIdleTime); - XHRMultipartTransport xhrMultipartTransport = new XHRMultipartTransport(bufferSize, maxIdleTime); - XHRPollingTransport xhrPollingTransport = new XHRPollingTransport(bufferSize, maxIdleTime); - JSONPPollingTransport jsonpPollingTransport = new JSONPPollingTransport(bufferSize, maxIdleTime); - transports.put(websocketTransport.getName(), websocketTransport); - transports.put(flashsocketTransport.getName(), flashsocketTransport); - transports.put(htmlFileTransport.getName(), htmlFileTransport); - transports.put(xhrMultipartTransport.getName(), xhrMultipartTransport); - transports.put(xhrPollingTransport.getName(), xhrPollingTransport); - transports.put(jsonpPollingTransport.getName(), jsonpPollingTransport); - - for (Transport t: transports.values()) { - t.init(this.getServletConfig()); - } - } + private final SocketIOSessionManager sessionManager = new SocketIOSessionManager(); + private final TransportHandlerProvider transportHandlerProvider = new AnnotationTransportHandlerProvider(); - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - serve(req, resp); - } + private SocketIOConfig config; - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - serve(req, resp); - } + public final static String DEFAULT_HEARTBEAT_TIMEOUT = "defaultHeartbeatTimeout"; + public final static String DEFAULT_TIMEOUT = "defaultTimeout"; - private void serve(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + public final static String MAX_TEXT_MESSAGE_SIZE = "maxTextMessageSize"; - String path = request.getPathInfo(); - if (path == null || path.length() == 0 || "/".equals(path)) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing SocketIO transport"); - return; - } - if (path.startsWith("/")) path = path.substring(1); - String[] parts = path.split("/"); - - Transport transport = transports.get(parts[0]); - if (transport == null) { - if ("GET".equals(request.getMethod()) && "socket.io.js".equals(parts[0])) { - response.setContentType("text/javascript"); - InputStream is = this.getClass().getClassLoader().getResourceAsStream("com/glines/socketio/socket.io.js"); - OutputStream os = response.getOutputStream(); - IO.copy(is, os); - return; - } else { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unknown SocketIO transport"); - return; - } - } - - transport.handle(request, response, new Transport.InboundFactory() { - - @Override - public SocketIOInbound getInbound(HttpServletRequest request) { - return SocketIOServlet.this.doSocketIOConnect(request); - } - - }, sessionManager); + @Override + public void init() throws ServletException { + config = new ServletBasedSocketIOConfig(getServletConfig(), "socketio"); + + // lazy load available transport handlers + transportHandlerProvider.init(); + if (LOGGER.isLoggable(Level.INFO)) + LOGGER.log(Level.INFO, "Transport handlers loaded: " + transportHandlerProvider.listAll()); + + // lazily load available transports + TransportDiscovery transportDiscovery = new ClasspathTransportDiscovery(); + for (Transport transport : transportDiscovery.discover()) { + if (transportHandlerProvider.isSupported(transport.getType())) { + transport.setTransportHandlerProvider(transportHandlerProvider); + config.addTransport(transport); + } else { + LOGGER.log(Level.WARNING, "Transport " + transport.getType() + " ignored since not supported by any TransportHandler"); + } + } + // initialize them + for (Transport t : config.getTransports()) { + try { + t.init(getServletConfig()); + } catch (TransportInitializationException e) { + config.removeTransport(t.getType()); + LOGGER.log(Level.WARNING, "Transport " + t.getType() + " disabled. Initialization failed: " + e.getMessage()); + } + } + if (LOGGER.isLoggable(Level.INFO)) + LOGGER.log(Level.INFO, "Transports loaded: " + config.getTransports()); } @Override public void destroy() { - for (Transport t: transports.values()) { - t.destroy(); - } - super.destroy(); + for (Transport t : config.getTransports()) { + t.destroy(); + } + super.destroy(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + serve(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + serve(req, resp); } /** * Returns an instance of SocketIOInbound or null if the connection is to be denied. * The value of cookies and protocols may be null. */ - protected abstract SocketIOInbound doSocketIOConnect(HttpServletRequest request); + protected abstract SocketIOInbound doSocketIOConnect(HttpServletRequest request); + + private void serve(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + String path = request.getPathInfo(); + if (path == null || path.length() == 0 || "/".equals(path)) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing SocketIO transport"); + return; + } + if (path.startsWith("/")) path = path.substring(1); + String[] parts = path.split("/"); + + if ("GET".equals(request.getMethod()) && "socket.io.js".equals(parts[0])) { + response.setContentType("text/javascript"); + InputStream is = this.getClass().getClassLoader().getResourceAsStream("com/glines/socketio/socket.io.js"); + OutputStream os = response.getOutputStream(); + IO.copy(is, os); + return; + } else if ("GET".equals(request.getMethod()) && "WebSocketMain.swf".equals(parts[0])) { + response.setContentType("application/x-shockwave-flash"); + InputStream is = this.getClass().getClassLoader().getResourceAsStream("com/glines/socketio/WebSocketMain.swf"); + OutputStream os = response.getOutputStream(); + IO.copy(is, os); + return; + } else if (parts.length <= 2) { // handshake + OutputStream os = response.getOutputStream(); + + // Format: sessionId : heartbeat : timeout + String body = request.getSession().getId().toString() + + ":" + (config.getString(DEFAULT_HEARTBEAT_TIMEOUT) == null ? "15000" : config.getString(DEFAULT_HEARTBEAT_TIMEOUT)) + + ":" + (config.getString(DEFAULT_TIMEOUT) == null? "10000" : config.getString(DEFAULT_TIMEOUT)) + ":"; + + String transports = ""; // websocket,flashsocket,xhr-polling,jsonp-polling,htmlfile + for (Transport transport : config.getTransports()) { + if (!transports.isEmpty()) + transports += ","; + transports += transport.getType().toString(); + } + body += transports; + + os.write(body.getBytes()); + return; + } else { + Transport transport = config.getTransport(TransportType.from(parts[1])); + if (transport == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unknown SocketIO transport: " + parts[0]); + return; + } + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "Handling request from " + request.getRemoteHost() + ":" + request.getRemotePort() + " with transport: " + transport.getType()); + + transport.handle(request, response, new Transport.InboundFactory() { + @Override + public SocketIOInbound getInbound(HttpServletRequest request) { + return SocketIOServlet.this.doSocketIOConnect(request); + } + }, sessionManager); + } + } + } diff --git a/core/src/main/java/com/glines/socketio/server/SocketIOSession.java b/core/src/main/java/com/glines/socketio/server/SocketIOSession.java index d479368..39aa9db 100644 --- a/core/src/main/java/com/glines/socketio/server/SocketIOSession.java +++ b/core/src/main/java/com/glines/socketio/server/SocketIOSession.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,49 +24,27 @@ */ package com.glines.socketio.server; -import java.io.IOException; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import com.glines.socketio.common.ConnectionState; import com.glines.socketio.common.DisconnectReason; -import com.glines.socketio.common.SocketIOException; public interface SocketIOSession { - interface Factory { - SocketIOSession createSession(SocketIOInbound inbound); - SocketIOSession getSession(String sessionId); - } + void setAttribute(String key, Object val); + Object getAttribute(String key); - interface SessionTransportHandler extends SocketIOOutbound { - void handle(HttpServletRequest request, HttpServletResponse response, SocketIOSession session) throws IOException; - void sendMessage(SocketIOFrame message) throws SocketIOException; - void disconnectWhenEmpty(); - /** - * Cause connection and all activity to be aborted and all resources to be released. - * The handler is expected to call the session's onShutdown() when it is finished. - * The only session method that the handler can legally call after this is onShutdown(); - */ - void abort(); - } - - interface SessionTask { + interface SessionTask { /** * @return True if task was or was already canceled, false if the task is executing or has executed. */ boolean cancel(); } - - String generateRandomString(int length); - + String getSessionId(); ConnectionState getConnectionState(); SocketIOInbound getInbound(); - SessionTransportHandler getTransportHandler(); + TransportHandler getTransportHandler(); void setHeartbeat(long delay); long getHeartbeat(); @@ -98,7 +78,7 @@ interface SessionTask { /** * @param handler The handler or null if the connection failed. */ - void onConnect(SessionTransportHandler handler); + void onConnect(TransportHandler handler); /** * Pass message through to contained SocketIOInbound diff --git a/core/src/main/java/com/glines/socketio/server/SocketIOSessionManager.java b/core/src/main/java/com/glines/socketio/server/SocketIOSessionManager.java index e36a886..37ce4ce 100644 --- a/core/src/main/java/com/glines/socketio/server/SocketIOSessionManager.java +++ b/core/src/main/java/com/glines/socketio/server/SocketIOSessionManager.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,342 +24,25 @@ */ package com.glines.socketio.server; -import java.security.SecureRandom; -import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -import org.eclipse.jetty.util.log.Log; - -import com.glines.socketio.common.ConnectionState; -import com.glines.socketio.common.DisconnectReason; -import com.glines.socketio.common.SocketIOException; - -class SocketIOSessionManager implements SocketIOSession.Factory { - private static final char[] BASE64_ALPHABET = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" - .toCharArray(); - private static final int SESSION_ID_LENGTH = 20; - - private static Random random = new SecureRandom(); - private ConcurrentMap socketIOSessions = new ConcurrentHashMap(); - private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); - - private static String generateRandomString(int length) { - StringBuilder result = new StringBuilder(length); - byte[] bytes = new byte[length]; - random.nextBytes(bytes); - for (int i = 0; i < bytes.length; i++) { - result.append(BASE64_ALPHABET[bytes[i] & 0x3F]); - } - return result.toString(); - } - - private class SessionImpl implements SocketIOSession { - private final String sessionId; - private SocketIOInbound inbound; - private SessionTransportHandler handler = null; - private ConnectionState state = ConnectionState.CONNECTING; - private long hbDelay = 0; - private SessionTask hbDelayTask = null; - private long timeout = 0; - private SessionTask timeoutTask = null; - private boolean timedout = false; - private AtomicLong messageId = new AtomicLong(0); - private String closeId = null; - - SessionImpl(String sessionId, SocketIOInbound inbound) { - this.sessionId = sessionId; - this.inbound = inbound; - } - - @Override - public String generateRandomString(int length) { - return SocketIOSessionManager.generateRandomString(length); - } - - @Override - public String getSessionId() { - return sessionId; - } - - @Override - public ConnectionState getConnectionState() { - return state; - } - - @Override - public SocketIOInbound getInbound() { - return inbound; - } - - @Override - public SessionTransportHandler getTransportHandler() { - return handler; - } - - private void onTimeout() { - Log.debug("Session["+sessionId+"]: onTimeout"); - if (!timedout) { - timedout = true; - state = ConnectionState.CLOSED; - onDisconnect(DisconnectReason.TIMEOUT); - handler.abort(); - } - } - - @Override - public void startTimeoutTimer() { - clearTimeoutTimer(); - if (!timedout && timeout > 0) { - timeoutTask = scheduleTask(new Runnable() { - @Override - public void run() { - SessionImpl.this.onTimeout(); - } - }, timeout); - } - } - - @Override - public void clearTimeoutTimer() { - if (timeoutTask != null) { - timeoutTask.cancel(); - timeoutTask = null; - } - } - - private void sendPing() { - String data = "" + messageId.incrementAndGet(); - Log.debug("Session["+sessionId+"]: sendPing " + data); - try { - handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.PING, 0, data)); - } catch (SocketIOException e) { - Log.debug("handler.sendMessage failed: ", e); - handler.abort(); - } - startTimeoutTimer(); - } - - @Override - public void startHeartbeatTimer() { - clearHeartbeatTimer(); - if (!timedout && hbDelay > 0) { - hbDelayTask = scheduleTask(new Runnable() { - @Override - public void run() { - sendPing(); - } - }, hbDelay); - } - } - - @Override - public void clearHeartbeatTimer() { - if (hbDelayTask != null) { - hbDelayTask.cancel(); - hbDelayTask = null; - } - } - - @Override - public void setHeartbeat(long delay) { - hbDelay = delay; - } - - @Override - public long getHeartbeat() { - return hbDelay; - } - - @Override - public void setTimeout(long timeout) { - this.timeout = timeout; - } - - @Override - public long getTimeout() { - return timeout; - } - - @Override - public void startClose() { - state = ConnectionState.CLOSING; - closeId = "server"; - try { - handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, closeId)); - } catch (SocketIOException e) { - Log.debug("handler.sendMessage failed: ", e); - handler.abort(); - } - } - - @Override - public void onMessage(SocketIOFrame message) { - switch (message.getFrameType()) { - case SESSION_ID: - case HEARTBEAT_INTERVAL: - // Ignore these two messages types as they are only intended to be from server to client. - break; - case CLOSE: - Log.debug("Session["+sessionId+"]: onClose: " + message.getData()); - onClose(message.getData()); - break; - case PING: - Log.debug("Session["+sessionId+"]: onPing: " + message.getData()); - onPing(message.getData()); - break; - case PONG: - Log.debug("Session["+sessionId+"]: onPong: " + message.getData()); - onPong(message.getData()); - break; - case DATA: - Log.debug("Session["+sessionId+"]: onMessage: " + message.getData()); - onMessage(message.getData()); - break; - default: - // Ignore unknown message types - break; - } - } - - @Override - public void onPing(String data) { - try { - handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.PONG, 0, data)); - } catch (SocketIOException e) { - Log.debug("handler.sendMessage failed: ", e); - handler.abort(); - } - } - - @Override - public void onPong(String data) { - clearTimeoutTimer(); - } - - @Override - public void onClose(String data) { - if (state == ConnectionState.CLOSING) { - if (closeId != null && closeId.equals(data)) { - state = ConnectionState.CLOSED; - onDisconnect(DisconnectReason.CLOSED); - handler.abort(); - } else { - try { - handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, data)); - } catch (SocketIOException e) { - Log.debug("handler.sendMessage failed: ", e); - handler.abort(); - } - } - } else { - state = ConnectionState.CLOSING; - try { - handler.sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, data)); - handler.disconnectWhenEmpty(); - } catch (SocketIOException e) { - Log.debug("handler.sendMessage failed: ", e); - handler.abort(); - } - } - } - - @Override - public SessionTask scheduleTask(Runnable task, long delay) { - final Future future = executor.schedule(task, delay, TimeUnit.MILLISECONDS); - return new SessionTask() { - @Override - public boolean cancel() { - return future.cancel(false); - } - }; - } - - @Override - public void onConnect(SessionTransportHandler handler) { - if (handler == null) { - state = ConnectionState.CLOSED; - inbound = null; - socketIOSessions.remove(sessionId); - } else if (this.handler == null) { - this.handler = handler; - try { - state = ConnectionState.CONNECTED; - inbound.onConnect(handler); - } catch (Throwable e) { - Log.warn("Session["+sessionId+"]: Exception thrown by SocketIOInbound.onConnect()", e); - state = ConnectionState.CLOSED; - handler.abort(); - } - } else { - handler.abort(); - } - } - @Override - public void onMessage(String message) { - if (inbound != null) { - try { - inbound.onMessage(SocketIOFrame.TEXT_MESSAGE_TYPE, message); - } catch (Throwable e) { - Log.warn("Session["+sessionId+"]: Exception thrown by SocketIOInbound.onMessage()", e); - } - } - } +public final class SocketIOSessionManager implements SessionManager { - @Override - public void onDisconnect(DisconnectReason reason) { - Log.debug("Session["+sessionId+"]: onDisconnect: " + reason); - clearTimeoutTimer(); - clearHeartbeatTimer(); - if (inbound != null) { - state = ConnectionState.CLOSED; - try { - inbound.onDisconnect(reason, null); - } catch (Throwable e) { - Log.warn("Session["+sessionId+"]: Exception thrown by SocketIOInbound.onDisconnect()", e); - } - inbound = null; - } - } - - @Override - public void onShutdown() { - Log.debug("Session["+sessionId+"]: onShutdown"); - if (inbound != null) { - if (state == ConnectionState.CLOSING) { - if (closeId != null) { - onDisconnect(DisconnectReason.CLOSE_FAILED); - } else { - onDisconnect(DisconnectReason.CLOSED_REMOTELY); - } - } else { - onDisconnect(DisconnectReason.ERROR); - } - } - socketIOSessions.remove(sessionId); - } - } - - private String generateSessionId() { - return generateRandomString(SESSION_ID_LENGTH); - } + final ConcurrentMap socketIOSessions = new ConcurrentHashMap(); + final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); - @Override - public SocketIOSession createSession(SocketIOInbound inbound) { - SessionImpl impl = new SessionImpl(generateSessionId(), inbound); - socketIOSessions.put(impl.getSessionId(), impl); - return impl; - } + @Override + public SocketIOSession createSession(SocketIOInbound inbound, String sessionId) { + DefaultSession impl = new DefaultSession(this, inbound, sessionId); + socketIOSessions.put(impl.getSessionId(), impl); + return impl; + } - @Override - public SocketIOSession getSession(String sessionId) { - return socketIOSessions.get(sessionId); - } + @Override + public SocketIOSession getSession(String sessionId) { + return socketIOSessions.get(sessionId); + } } diff --git a/core/src/main/java/com/glines/socketio/server/Transport.java b/core/src/main/java/com/glines/socketio/server/Transport.java index 35a5d98..5d9ea07 100644 --- a/core/src/main/java/com/glines/socketio/server/Transport.java +++ b/core/src/main/java/com/glines/socketio/server/Transport.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,26 +24,30 @@ */ package com.glines.socketio.server; -import java.io.IOException; - import javax.servlet.ServletConfig; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; public interface Transport { - interface InboundFactory { - SocketIOInbound getInbound(HttpServletRequest request); - } - /** - * @return The name of the transport instance. - */ - String getName(); + interface InboundFactory { + SocketIOInbound getInbound(HttpServletRequest request); + } + + /** + * @return The names of the transport instance. + */ + TransportType getType(); + + void setTransportHandlerProvider(TransportHandlerProvider transportHandlerProvider); + + void init(ServletConfig config) throws TransportInitializationException; - void init(ServletConfig config); - - void destroy(); + void destroy(); - void handle(HttpServletRequest request, HttpServletResponse response, - Transport.InboundFactory inboundFactory, SocketIOSession.Factory sessionFactory) throws IOException; + void handle(HttpServletRequest request, + HttpServletResponse response, + Transport.InboundFactory inboundFactory, + SessionManager sessionFactory) throws IOException; } diff --git a/core/src/main/java/com/glines/socketio/server/TransportDiscovery.java b/core/src/main/java/com/glines/socketio/server/TransportDiscovery.java new file mode 100644 index 0000000..0dd13a6 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/TransportDiscovery.java @@ -0,0 +1,32 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +/** + * @author Mathieu Carbou + */ +public interface TransportDiscovery { + Iterable discover(); +} diff --git a/core/src/main/java/com/glines/socketio/server/TransportHandler.java b/core/src/main/java/com/glines/socketio/server/TransportHandler.java new file mode 100644 index 0000000..319b542 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/TransportHandler.java @@ -0,0 +1,52 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +import com.glines.socketio.common.SocketIOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** +* @author Mathieu Carbou +*/ +public interface TransportHandler extends SocketIOOutbound { + void init(SocketIOConfig config); + void setSession(SocketIOSession session); + + void handle(HttpServletRequest request, HttpServletResponse response, SocketIOSession session) throws IOException; + void sendMessage(SocketIOFrame message) throws SocketIOException; + void disconnectWhenEmpty(); + /** + * Cause connection and all activity to be aborted and all resources to be released. + * The handler is expected to call the session's onShutdown() when it is finished. + * The only session method that the handler can legally call after this is onShutdown(); + */ + void abort(); + + void onConnect(); + +} diff --git a/core/src/main/java/com/glines/socketio/server/TransportHandlerProvider.java b/core/src/main/java/com/glines/socketio/server/TransportHandlerProvider.java new file mode 100644 index 0000000..dbc0089 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/TransportHandlerProvider.java @@ -0,0 +1,40 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +import java.util.Collection; +import java.util.Map; + +/** + * @author Mathieu Carbou (mathieu.carbou@gmail.com) + */ +public interface TransportHandlerProvider { + TransportHandler get(Class handlerType, TransportType transportType); + boolean isSupported(TransportType type); + + Map> listAll(); + + void init(); +} diff --git a/core/src/main/java/com/glines/socketio/server/TransportInitializationException.java b/core/src/main/java/com/glines/socketio/server/TransportInitializationException.java new file mode 100644 index 0000000..56a545b --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/TransportInitializationException.java @@ -0,0 +1,45 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +/** + * @author Mathieu Carbou + */ +public class TransportInitializationException extends RuntimeException { + + private static final long serialVersionUID = -5971560873659475024L; + + public TransportInitializationException(Throwable cause) { + super(cause); + } + + public TransportInitializationException(String message) { + super(message); + } + + public TransportInitializationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/com/glines/socketio/server/TransportType.java b/core/src/main/java/com/glines/socketio/server/TransportType.java new file mode 100644 index 0000000..2f5dfb5 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/TransportType.java @@ -0,0 +1,58 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server; + +/** + * @author Mathieu Carbou (mathieu.carbou@gmail.com) + */ +public enum TransportType { + + WEB_SOCKET("websocket"), + FLASH_SOCKET("flashsocket"), + HTML_FILE("htmlfile"), + JSONP_POLLING("jsonp-polling"), + XHR_MULTIPART("xhr-multipart"), + XHR_POLLING("xhr-polling"), + UNKNOWN(""); + + private final String name; + + TransportType(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public static TransportType from(String name) { + for (TransportType type : values()) { + if (type.name.equals(name)) + return type; + } + return UNKNOWN; + } +} diff --git a/core/src/main/java/com/glines/socketio/server/transport/AbstractDataHandler.java b/core/src/main/java/com/glines/socketio/server/transport/AbstractDataHandler.java new file mode 100644 index 0000000..e7d5bcb --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/transport/AbstractDataHandler.java @@ -0,0 +1,49 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server.transport; + +import com.glines.socketio.server.SocketIOConfig; + +/** + * @author Mathieu Carbou + */ +abstract class AbstractDataHandler implements DataHandler { + + private SocketIOConfig config; + + @Override + public final void init(SocketIOConfig config) { + this.config = config; + init(); + } + + protected final SocketIOConfig getConfig() { + return config; + } + + protected void init() { + } + +} diff --git a/core/src/main/java/com/glines/socketio/server/transport/AbstractHttpTransport.java b/core/src/main/java/com/glines/socketio/server/transport/AbstractHttpTransport.java index 631e15b..ce6068c 100644 --- a/core/src/main/java/com/glines/socketio/server/transport/AbstractHttpTransport.java +++ b/core/src/main/java/com/glines/socketio/server/transport/AbstractHttpTransport.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,87 +24,79 @@ */ package com.glines.socketio.server.transport; -import java.io.IOException; +import com.glines.socketio.server.*; +import com.glines.socketio.util.Web; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - -import com.glines.socketio.server.SocketIOSession; -import com.glines.socketio.server.SocketIOSession.SessionTransportHandler; -import com.glines.socketio.server.Transport; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; public abstract class AbstractHttpTransport extends AbstractTransport { - /** - * This is a sane default based on the timeout values of various browsers. - */ - public static final long HTTP_REQUEST_TIMEOUT = 30*1000; - - /** - * The amount of time the session will wait before trying to send a ping. - * Using a value of half the HTTP_REQUEST_TIMEOUT should be good enough. - */ - public static final long HEARTBEAT_DELAY = HTTP_REQUEST_TIMEOUT/2; - - /** - * This specifies how long to wait for a pong (ping response). - */ - public static final long HEARTBEAT_TIMEOUT = 10*1000; - /** - * For non persistent connection transports, this is the amount of time to wait - * for messages before returning empty results. - */ - public static long REQUEST_TIMEOUT = 20*1000; - - protected static final String SESSION_KEY = "com.glines.socketio.server.AbstractHttpTransport.Session"; + private static final Logger LOGGER = Logger.getLogger(AbstractHttpTransport.class.getName()); + public static final String SESSION_KEY = AbstractHttpTransport.class.getName() + ".Session"; + + @Override + public final void handle(HttpServletRequest request, + HttpServletResponse response, + Transport.InboundFactory inboundFactory, + SessionManager sessionFactory) throws IOException { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("Handling request " + request.getRequestURI() + " by " + getClass().getName()); - public AbstractHttpTransport() { - } + SocketIOSession session = null; + String sessionId = Web.extractSessionId(request); + if (sessionId != null && sessionId.length() > 0) { + session = sessionFactory.getSession(sessionId); + } - protected abstract SocketIOSession connect( - HttpServletRequest request, - HttpServletResponse response, - Transport.InboundFactory inboundFactory, - SocketIOSession.Factory sessionFactory) throws IOException; + if (session != null) { + TransportHandler handler = session.getTransportHandler(); + if (handler != null) { + handler.handle(request, response, session); + } else { + session.onShutdown(); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } else { + if ("GET".equals(request.getMethod())) { + session = connect(request, response, inboundFactory, + sessionFactory, sessionId); + if (session == null) { + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + } + } else { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + } + } + } - @Override - public void handle(HttpServletRequest request, - HttpServletResponse response, - Transport.InboundFactory inboundFactory, - SocketIOSession.Factory sessionFactory) - throws IOException { + private SocketIOSession connect(HttpServletRequest request, + HttpServletResponse response, + InboundFactory inboundFactory, + SessionManager sessionFactory, + String sessionId) throws IOException { + SocketIOInbound inbound = inboundFactory.getInbound(request); + if (inbound != null) { + if (sessionId == null) + sessionId = request.getSession().getId().toString(); + SocketIOSession session = sessionFactory.createSession(inbound, sessionId); + // get and init data handler + DataHandler dataHandler = newDataHandler(session); + dataHandler.init(getConfig()); + // get and init transport handler + TransportHandler transportHandler = newHandler(ConnectableTransportHandler.class, session); + ConnectableTransportHandler connectableTransportHandler = ConnectableTransportHandler.class.cast(transportHandler); + connectableTransportHandler.setDataHandler(dataHandler); + transportHandler.init(getConfig()); + // connect transport to session + connectableTransportHandler.connect(request, response); + return session; + } + return null; + } - Object obj = request.getAttribute(SESSION_KEY); - SocketIOSession session = null; - String sessionId = null; - if (obj != null) { - session = (SocketIOSession)obj; - } else { - sessionId = extractSessionId(request); - if (sessionId != null && sessionId.length() > 0) { - session = sessionFactory.getSession(sessionId); - } - } - - if (session != null) { - SessionTransportHandler handler = session.getTransportHandler(); - if (handler != null) { - handler.handle(request, response, session); - } else { - session.onShutdown(); - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - } else if (sessionId != null && sessionId.length() > 0) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - } else { - if ("GET".equals(request.getMethod())) { - session = connect(request, response, inboundFactory, sessionFactory); - if (session == null) { - response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - } - } else { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - } - } - } + protected abstract DataHandler newDataHandler(SocketIOSession session); } diff --git a/core/src/main/java/com/glines/socketio/server/transport/ConnectionTimeoutPreventor.java b/core/src/main/java/com/glines/socketio/server/transport/ConnectionTimeoutPreventor.java deleted file mode 100644 index 6118e35..0000000 --- a/core/src/main/java/com/glines/socketio/server/transport/ConnectionTimeoutPreventor.java +++ /dev/null @@ -1,72 +0,0 @@ -/** - * The MIT License - * Copyright (c) 2010 Tad Glines - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.glines.socketio.server.transport; - -import org.eclipse.jetty.io.AsyncEndPoint; -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.io.nio.SelectChannelEndPoint; -import org.eclipse.jetty.server.HttpConnection; - -/** - * Jetty will close a connection even if there is continuous outbound data if the request's response - * is not completed within maxIdleTime milliseconds. This is not appropriate for a persistent connection - * SocketIO transport. IN order to prevent this, without disabling the maxIdleTime completely, - * this class is used to obtain a @{link IdleCheck} instance that can be used to reset the idle timeout. - */ -public class ConnectionTimeoutPreventor { - interface IdleCheck { - void activity(); - } - - /** - * This must be called within the context of an active HTTP request. - */ - public static IdleCheck newTimeoutPreventor() { - HttpConnection httpConnection = HttpConnection.getCurrentConnection(); - if (httpConnection != null) { - EndPoint endPoint = httpConnection.getEndPoint(); - if (endPoint instanceof AsyncEndPoint) { - ((AsyncEndPoint)endPoint).cancelIdle(); - } - if (endPoint instanceof SelectChannelEndPoint) { - final SelectChannelEndPoint scep = (SelectChannelEndPoint)endPoint; - scep.cancelIdle(); - return new IdleCheck() { - @Override - public void activity() { - scep.scheduleIdle(); - } - }; - } else { - return new IdleCheck() { - @Override - public void activity() { - // Do nothing - } - }; - } - } else { - return null; - } - } -} diff --git a/core/src/main/java/com/glines/socketio/server/transport/AbstractTransport.java b/core/src/main/java/com/glines/socketio/server/transport/DataHandler.java similarity index 64% rename from core/src/main/java/com/glines/socketio/server/transport/AbstractTransport.java rename to core/src/main/java/com/glines/socketio/server/transport/DataHandler.java index 2ade94e..f6a4e80 100644 --- a/core/src/main/java/com/glines/socketio/server/transport/AbstractTransport.java +++ b/core/src/main/java/com/glines/socketio/server/transport/DataHandler.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,31 +24,28 @@ */ package com.glines.socketio.server.transport; -import javax.servlet.ServletConfig; +import com.glines.socketio.server.SocketIOConfig; + +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Mathieu Carbou + */ +public interface DataHandler { + + void init(SocketIOConfig config); + + boolean isConnectionPersistent(); + + void onStartSend(HttpServletResponse response) throws IOException; + + void onWriteData(ServletResponse response, String data) throws IOException; + + void onFinishSend(ServletResponse response) throws IOException; + + void onConnect(HttpServletRequest request, HttpServletResponse response) throws IOException; -import com.glines.socketio.server.Transport; - -public abstract class AbstractTransport implements Transport { - protected String extractSessionId(HttpServletRequest request) { - String path = request.getPathInfo(); - if (path != null && path.length() > 0 && !"/".equals(path)) { - if (path.startsWith("/")) path = path.substring(1); - String[] parts = path.split("/"); - if (parts.length >= 2) { - return parts[1] == null ? null : (parts[1].length() == 0 ? null : parts[1]); - } - } - return null; - } - - @Override - public void init(ServletConfig config) { - - } - - @Override - public void destroy() { - - } } diff --git a/core/src/main/java/com/glines/socketio/server/transport/FlashSocketTransport.java b/core/src/main/java/com/glines/socketio/server/transport/FlashSocketTransport.java index f0352fe..167bf4b 100644 --- a/core/src/main/java/com/glines/socketio/server/transport/FlashSocketTransport.java +++ b/core/src/main/java/com/glines/socketio/server/transport/FlashSocketTransport.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,11 +24,12 @@ */ package com.glines.socketio.server.transport; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PrintWriter; +import com.glines.socketio.server.*; +import com.glines.socketio.util.IO; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.channels.ClosedChannelException; @@ -36,178 +39,182 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; - -import javax.servlet.ServletConfig; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.util.IO; - -import com.glines.socketio.server.SocketIOSession; -import com.glines.socketio.server.Transport; - -public class FlashSocketTransport extends WebSocketTransport { - public static final String TRANSPORT_NAME = "flashsocket"; - public static final String FLASHPOLICY_SERVER_HOST_KEY = "flashPolicyServerHost"; - public static final String FLASHPOLICY_SERVER_PORT_KEY = "flashPolicyServerPort"; - public static final String FLASHPOLICY_DOMAIN_KEY = "flashPolicyDomain"; - public static final String FLASHPOLICY_PORTS_KEY = "flashPolicyPorts"; - - private static final String FLASHFILE_NAME = "WebSocketMain.swf"; - private static final String FLASHFILE_PATH = TRANSPORT_NAME + "/" + FLASHFILE_NAME; - private ServerSocketChannel flashPolicyServer = null; - private ExecutorService executor = Executors.newCachedThreadPool(); - private Future policyAcceptorThread = null; - private String flashPolicyServerHost = null; - private short flashPolicyServerPort = 843; - private String flashPolicyDomain = null; - private String flashPolicyPorts = null; - - - public FlashSocketTransport(int bufferSize, int maxIdleTime) { - super(bufferSize, maxIdleTime); - } - - @Override - public String getName() { - return TRANSPORT_NAME; - } - - @Override - public void init(ServletConfig config) { - flashPolicyServerHost = config.getInitParameter(FLASHPOLICY_SERVER_HOST_KEY); - flashPolicyDomain = config.getInitParameter(FLASHPOLICY_DOMAIN_KEY); - flashPolicyPorts = config.getInitParameter(FLASHPOLICY_PORTS_KEY); - String port = config.getInitParameter(FLASHPOLICY_SERVER_PORT_KEY); - if (port != null) { - flashPolicyServerPort = Short.parseShort(port); - } - if (flashPolicyServerHost != null && flashPolicyDomain != null && flashPolicyPorts != null) { - try { - startFlashPolicyServer(); - } catch (IOException e) { - // Ignore - } - } - } - - @Override - public void destroy() { - stopFlashPolicyServer(); - } - - @Override - public void handle(HttpServletRequest request, - HttpServletResponse response, - Transport.InboundFactory inboundFactory, - SocketIOSession.Factory sessionFactory) - throws IOException { - - String path = request.getPathInfo(); - if (path == null || path.length() == 0 || "/".equals(path)) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid " + TRANSPORT_NAME + " transport request"); - return; - } - if (path.startsWith("/")) path = path.substring(1); - String[] parts = path.split("/"); - - if ("GET".equals(request.getMethod()) && TRANSPORT_NAME.equals(parts[0])) { - if (!FLASHFILE_PATH.equals(path)) { - super.handle(request, response, inboundFactory, sessionFactory); - } else { - response.setContentType("application/x-shockwave-flash"); - InputStream is = this.getClass().getClassLoader().getResourceAsStream("com/glines/socketio/" + FLASHFILE_NAME); - OutputStream os = response.getOutputStream(); - try { - IO.copy(is, os); - } catch (IOException e) { - // TODO: Do we care? - } - } - } else { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid " + TRANSPORT_NAME + " transport request"); - } - } - - /** - * Starts this server, binding to the previously passed SocketAddress. - */ - public void startFlashPolicyServer() throws IOException { - final String POLICY_FILE_REQUEST = ""; - flashPolicyServer = ServerSocketChannel.open(); - flashPolicyServer.socket().setReuseAddress(true); - flashPolicyServer.socket().bind(new InetSocketAddress(flashPolicyServerHost, flashPolicyServerPort)); - flashPolicyServer.configureBlocking(true); - - // Spawn a new server acceptor thread, which must accept incoming - // connections indefinitely - until a ClosedChannelException is thrown. - policyAcceptorThread = executor.submit(new Runnable() { - @Override - public void run() { - try { - while (true) { - final SocketChannel serverSocket = flashPolicyServer.accept(); - executor.submit(new Runnable() { - @Override - public void run() { - try { - serverSocket.configureBlocking(true); - Socket s = serverSocket.socket(); - StringBuilder request = new StringBuilder(); - InputStreamReader in = new InputStreamReader(s.getInputStream()); - int c; - while ((c = in.read()) != 0 && request.length() <= POLICY_FILE_REQUEST.length()) { - request.append((char)c); - } - if (request.toString().equalsIgnoreCase(POLICY_FILE_REQUEST) || - flashPolicyDomain != null && flashPolicyPorts != null) { - PrintWriter out = new PrintWriter(s.getOutputStream()); - out.println(""); - out.write(0); - out.flush(); - } - serverSocket.close(); - } catch (IOException e) { - // TODO: Add loging - } finally { - try { - serverSocket.close(); - } catch (IOException e) { - // Ignore error on close. - } - } - } - }); - } - } catch (ClosedChannelException e) { - return; - } catch (IOException e) { - throw new IllegalStateException("Server should not throw a misunderstood IOException", e); - } - } - }); - } - - private void stopFlashPolicyServer() { - if (flashPolicyServer != null) { - try { - flashPolicyServer.close(); - } catch (IOException e) { - // Ignore - } - } - if (policyAcceptorThread != null) { - try { - policyAcceptorThread.get(); - } catch (InterruptedException e) { - throw new IllegalStateException(); - } catch (ExecutionException e) { - throw new IllegalStateException("Server thread threw an exception", e.getCause()); - } - if (!policyAcceptorThread.isDone()) { - throw new IllegalStateException("Server acceptor thread has not stopped."); - } - } - } +import java.util.logging.Level; +import java.util.logging.Logger; + +public class FlashSocketTransport extends AbstractTransport { + + public static final String PARAM_FLASHPOLICY_DOMAIN = "flashPolicyDomain"; + public static final String PARAM_FLASHPOLICY_SERVER_HOST = "flashPolicyServerHost"; + public static final String PARAM_FLASHPOLICY_SERVER_PORT = "flashPolicyServerPort"; + public static final String PARAM_FLASHPOLICY_PORTS = "flashPolicyPorts"; + + private static final Logger LOGGER = Logger.getLogger(FlashSocketTransport.class.getName()); + private static final String FLASHFILE_NAME = "WebSocketMain.swf"; + private static final String FLASHFILE_PATH = TransportType.FLASH_SOCKET + "/" + FLASHFILE_NAME; + + private ServerSocketChannel flashPolicyServer; + private ExecutorService executor = Executors.newCachedThreadPool(); + private Future policyAcceptorThread; + + private int flashPolicyServerPort; + private String flashPolicyServerHost; + private String flashPolicyDomain; + private String flashPolicyPorts; + + private Transport delegate; + + @Override + public TransportType getType() { + return TransportType.FLASH_SOCKET; + } + + @Override + public void init() throws TransportInitializationException { + this.flashPolicyDomain = getConfig().getString(PARAM_FLASHPOLICY_DOMAIN); + this.flashPolicyPorts = getConfig().getString(PARAM_FLASHPOLICY_PORTS); + this.flashPolicyServerHost = getConfig().getString(PARAM_FLASHPOLICY_SERVER_HOST); + this.flashPolicyServerPort = getConfig().getInt(PARAM_FLASHPOLICY_SERVER_PORT, 843); + this.delegate = getConfig().getWebSocketTransport(); + + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine(getType() + " configuration:\n" + + " - flashPolicyDomain=" + flashPolicyDomain + "\n" + + " - flashPolicyPorts=" + flashPolicyPorts + "\n" + + " - flashPolicyServerHost=" + flashPolicyServerHost + "\n" + + " - flashPolicyServerPort=" + flashPolicyServerPort + "\n" + + " - websocket delegate=" + (delegate == null ? "" : delegate.getClass().getName())); + + if (delegate == null) + throw new TransportInitializationException("No WebSocket transport available for this transport: " + getClass().getName()); + + if (flashPolicyServerHost != null && flashPolicyDomain != null && flashPolicyPorts != null) { + try { + startFlashPolicyServer(); + } catch (IOException e) { + e.printStackTrace(); + // Ignore + } + } + } + + @Override + public void destroy() { + stopFlashPolicyServer(); + } + + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + Transport.InboundFactory inboundFactory, + SessionManager sessionFactory) throws IOException { + + String path = request.getPathInfo(); + if (path == null || path.length() == 0 || "/".equals(path)) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid " + TransportType.FLASH_SOCKET + " transport request"); + return; + } + if (path.startsWith("/")) path = path.substring(1); + String[] parts = path.split("/"); + if ("GET".equals(request.getMethod()) && "flashsocket".equals(parts[1])) { + if (!FLASHFILE_PATH.equals(path)) { + delegate.handle(request, response, inboundFactory, sessionFactory); + } else { + response.setContentType("application/x-shockwave-flash"); + InputStream is = this.getClass().getClassLoader().getResourceAsStream("com/glines/socketio/" + FLASHFILE_NAME); + OutputStream os = response.getOutputStream(); + try { + IO.copy(is, os); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Error writing " + FLASHFILE_NAME + ": " + e.getMessage(), e); + } + } + } else { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid " + TransportType.FLASH_SOCKET + " transport request"); + } + } + + /** + * Starts this server, binding to the previously passed SocketAddress. + */ + public void startFlashPolicyServer() throws IOException { + final String POLICY_FILE_REQUEST = ""; + flashPolicyServer = ServerSocketChannel.open(); + flashPolicyServer.socket().setReuseAddress(true); + flashPolicyServer.socket().bind(new InetSocketAddress(flashPolicyServerHost, flashPolicyServerPort)); + flashPolicyServer.configureBlocking(true); + + // Spawn a new server acceptor thread, which must accept incoming + // connections indefinitely - until a ClosedChannelException is thrown. + policyAcceptorThread = executor.submit(new Runnable() { + @Override + public void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + final SocketChannel serverSocket = flashPolicyServer.accept(); + executor.submit(new Runnable() { + @Override + public void run() { + try { + serverSocket.configureBlocking(true); + Socket s = serverSocket.socket(); + StringBuilder request = new StringBuilder(); + InputStreamReader in = new InputStreamReader(s.getInputStream()); + int c; + while ((c = in.read()) != 0 && request.length() <= POLICY_FILE_REQUEST.length()) { + request.append((char) c); + } + if (request.toString().equalsIgnoreCase(POLICY_FILE_REQUEST) || + flashPolicyDomain != null && flashPolicyPorts != null) { + PrintWriter out = new PrintWriter(s.getOutputStream()); + out.println(""); + out.write(0); + out.flush(); + } + serverSocket.close(); + } catch (IOException e) { + LOGGER.log(Level.FINE, "startFlashPolicyServer: " + e.getMessage(), e); + } finally { + try { + serverSocket.close(); + } catch (IOException e) { + // Ignore error on close. + } + } + } + }); + } + } catch (ClosedChannelException e) { + Thread.currentThread().interrupt(); + } catch (IOException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Server should not throw a misunderstood IOException", e); + } + } + }); + } + + private void stopFlashPolicyServer() { + if (flashPolicyServer != null) { + try { + flashPolicyServer.close(); + } catch (IOException e) { + // Ignore + } + } + if (policyAcceptorThread != null) { + try { + policyAcceptorThread.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(); + } catch (ExecutionException e) { + throw new IllegalStateException("Server thread threw an exception", e.getCause()); + } + if (!policyAcceptorThread.isDone()) { + throw new IllegalStateException("Server acceptor thread has not stopped."); + } + } + } } diff --git a/core/src/main/java/com/glines/socketio/server/transport/HTMLFileDataHandler.java b/core/src/main/java/com/glines/socketio/server/transport/HTMLFileDataHandler.java new file mode 100644 index 0000000..3666daf --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/transport/HTMLFileDataHandler.java @@ -0,0 +1,96 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server.transport; + +import com.glines.socketio.server.SocketIOFrame; +import com.glines.socketio.server.SocketIOSession; +import com.glines.socketio.util.JSON; + +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Mathieu Carbou + */ +final class HTMLFileDataHandler extends AbstractDataHandler { + + private static final Logger LOGGER = Logger.getLogger(HTMLFileDataHandler.class.getName()); + private static final long DEFAULT_HEARTBEAT_DELAY = 15 * 1000; + + private final SocketIOSession session; + + private long hearbeat; + + HTMLFileDataHandler(SocketIOSession session) { + this.session = session; + } + + @Override + protected void init() { + this.hearbeat = getConfig().getHeartbeatDelay(DEFAULT_HEARTBEAT_DELAY); + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine(getConfig().getNamespace() + " data handler configuration:\n" + + " - heartbeatDelay=" + hearbeat); + } + + @Override + public boolean isConnectionPersistent() { + return true; + } + + @Override + public void onStartSend(HttpServletResponse response) throws IOException { + response.setContentType("text/html"); + response.setHeader("Connection", "keep-alive"); + response.setHeader("Transfer-Encoding", "chunked"); + char[] spaces = new char[244]; + Arrays.fill(spaces, ' '); + ServletOutputStream os = response.getOutputStream(); + os.print("" + new String(spaces)); + response.flushBuffer(); + } + + @Override + public void onWriteData(ServletResponse response, String data) throws IOException { + response.getOutputStream().print(""); + response.flushBuffer(); + } + + @Override + public void onFinishSend(ServletResponse response) throws IOException { + } + + @Override + public void onConnect(HttpServletRequest request, HttpServletResponse response) throws IOException { + onStartSend(response); + onWriteData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.CONNECT, session.getSessionId())); + } +} diff --git a/core/src/main/java/com/glines/socketio/server/transport/HTMLFileTransport.java b/core/src/main/java/com/glines/socketio/server/transport/HTMLFileTransport.java index 5209374..dd0b4c7 100644 --- a/core/src/main/java/com/glines/socketio/server/transport/HTMLFileTransport.java +++ b/core/src/main/java/com/glines/socketio/server/transport/HTMLFileTransport.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,69 +24,17 @@ */ package com.glines.socketio.server.transport; -import java.io.IOException; -import java.util.Arrays; - -import javax.servlet.ServletOutputStream; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.util.ajax.JSON; - -import com.glines.socketio.server.SocketIOFrame; import com.glines.socketio.server.SocketIOSession; -import com.glines.socketio.server.transport.ConnectionTimeoutPreventor.IdleCheck; - -public class HTMLFileTransport extends XHRTransport { - public static final String TRANSPORT_NAME = "htmlfile"; - - private class HTMLFileSessionHelper extends XHRSessionHelper { - private final IdleCheck idleCheck; - - HTMLFileSessionHelper(SocketIOSession session, IdleCheck idleCheck) { - super(session, true); - this.idleCheck = idleCheck; - } - - protected void startSend(HttpServletResponse response) throws IOException { - response.setContentType("text/html"); - response.setHeader("Connection", "keep-alive"); - response.setHeader("Transfer-Encoding", "chunked"); - char[] spaces = new char[244]; - Arrays.fill(spaces, ' '); - ServletOutputStream os = response.getOutputStream(); - os.print("" + new String(spaces)); - response.flushBuffer(); - } - - protected void writeData(ServletResponse response, String data) throws IOException { - idleCheck.activity(); - response.getOutputStream().print(""); - response.flushBuffer(); - } - - protected void finishSend(ServletResponse response) throws IOException {}; - - protected void customConnect(HttpServletRequest request, - HttpServletResponse response) throws IOException { - startSend(response); - writeData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.SESSION_ID, 0, session.getSessionId())); - writeData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.HEARTBEAT_INTERVAL, 0, "" + HEARTBEAT_DELAY)); - } - } - - public HTMLFileTransport(int bufferSize, int maxIdleTime) { - super(bufferSize, maxIdleTime); - } - - @Override - public String getName() { - return TRANSPORT_NAME; - } - - protected XHRSessionHelper createHelper(SocketIOSession session) { - IdleCheck idleCheck = ConnectionTimeoutPreventor.newTimeoutPreventor(); - return new HTMLFileSessionHelper(session, idleCheck); - } +import com.glines.socketio.server.TransportType; + +public class HTMLFileTransport extends AbstractHttpTransport { + @Override + public TransportType getType() { + return TransportType.HTML_FILE; + } + + @Override + protected DataHandler newDataHandler(SocketIOSession session) { + return new HTMLFileDataHandler(session); + } } diff --git a/core/src/main/java/com/glines/socketio/server/transport/JSONPPollingDataHandler.java b/core/src/main/java/com/glines/socketio/server/transport/JSONPPollingDataHandler.java new file mode 100644 index 0000000..64adc9f --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/transport/JSONPPollingDataHandler.java @@ -0,0 +1,104 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server.transport; + +import com.glines.socketio.server.SocketIOFrame; +import com.glines.socketio.server.SocketIOSession; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Mathieu Carbou + */ +final class JSONPPollingDataHandler extends AbstractDataHandler { + + /** + * For non persistent connection transports, this is the amount of time to wait + * for messages before returning empty results. + */ + private static final long DEFAULT_TIMEOUT = 20 * 1000; + private static final String FRAME_ID = JSONPPollingDataHandler.class.getName() + "FRAME_ID"; + private static final Logger LOGGER = Logger.getLogger(JSONPPollingDataHandler.class.getName()); + + private final SocketIOSession session; + private long timeout; + + JSONPPollingDataHandler(SocketIOSession session) { + this.session = session; + } + + @Override + protected void init() { + this.timeout = getConfig().getTimeout(DEFAULT_TIMEOUT); + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine(getConfig().getNamespace() + " data handler configuration:\n" + + " - timeout=" + timeout); + } + + @Override + public boolean isConnectionPersistent() { + return false; + } + + @Override + public void onStartSend(HttpServletResponse response) throws IOException { + response.setContentType("text/javascript; charset=UTF-8"); + } + + @Override + public void onWriteData(ServletResponse response, String data) throws IOException { + response.getOutputStream().print("io.j[" + session.getAttribute(FRAME_ID) + "]('"); + response.getOutputStream().print(data); + response.getOutputStream().print("');"); + } + + @Override + public void onFinishSend(ServletResponse response) throws IOException { + response.flushBuffer(); + } + + @Override + public void onConnect(HttpServletRequest request, HttpServletResponse response) throws IOException { + String query = request.getQueryString(); + String[] parts = query.split("&"); + int frameId = 0; + for (String part : parts) { + if (part.startsWith("i=")) { + try { + frameId = Integer.parseInt(part.substring(2)); + break; + } catch (NumberFormatException e) { } + } + } + session.setAttribute(FRAME_ID, frameId); + onStartSend(response); + onWriteData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.CONNECT, session.getSessionId())); + } +} diff --git a/core/src/main/java/com/glines/socketio/server/transport/JSONPPollingTransport.java b/core/src/main/java/com/glines/socketio/server/transport/JSONPPollingTransport.java index 4af5591..47dedeb 100644 --- a/core/src/main/java/com/glines/socketio/server/transport/JSONPPollingTransport.java +++ b/core/src/main/java/com/glines/socketio/server/transport/JSONPPollingTransport.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,65 +24,17 @@ */ package com.glines.socketio.server.transport; -import java.io.IOException; - -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.glines.socketio.server.SocketIOFrame; import com.glines.socketio.server.SocketIOSession; - -public class JSONPPollingTransport extends XHRTransport { - public static final String TRANSPORT_NAME = "jsonp-polling"; - private long jsonpIndex = -1; - - protected class XHRPollingSessionHelper extends XHRSessionHelper { - - XHRPollingSessionHelper(SocketIOSession session) { - super(session, false); - } - - protected void startSend(HttpServletResponse response) throws IOException { - response.setContentType("text/javascript; charset=UTF-8"); - response.getOutputStream().print("io.JSONP["+ jsonpIndex +"]._('"); - } - - @Override - protected void writeData(ServletResponse response, String data) throws IOException { - response.getOutputStream().print(data); - } - - protected void finishSend(ServletResponse response) throws IOException { - response.getOutputStream().print("');"); - response.flushBuffer(); - } - - protected void customConnect(HttpServletRequest request, - HttpServletResponse response) throws IOException { - String path = request.getPathInfo(); - if (path.startsWith("/")) path = path.substring(1); - String[] parts = path.split("/"); - if (parts.length >= 4) { - jsonpIndex = Integer.parseInt(parts[3]); - } - startSend(response); - writeData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.SESSION_ID, 0, session.getSessionId())); - writeData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.HEARTBEAT_INTERVAL, 0, "" + REQUEST_TIMEOUT)); - } - } - - public JSONPPollingTransport(int bufferSize, int maxIdleTime) { - super(bufferSize, maxIdleTime); - } - - @Override - public String getName() { - return TRANSPORT_NAME; - } - - - protected XHRPollingSessionHelper createHelper(SocketIOSession session) { - return new XHRPollingSessionHelper(session); - } +import com.glines.socketio.server.TransportType; + +public class JSONPPollingTransport extends AbstractHttpTransport { + @Override + public TransportType getType() { + return TransportType.JSONP_POLLING; + } + + @Override + protected DataHandler newDataHandler(SocketIOSession session) { + return new JSONPPollingDataHandler(session); + } } diff --git a/core/src/main/java/com/glines/socketio/server/transport/WebSocketTransport.java b/core/src/main/java/com/glines/socketio/server/transport/WebSocketTransport.java deleted file mode 100644 index badc9f7..0000000 --- a/core/src/main/java/com/glines/socketio/server/transport/WebSocketTransport.java +++ /dev/null @@ -1,259 +0,0 @@ -/** - * The MIT License - * Copyright (c) 2010 Tad Glines - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.glines.socketio.server.transport; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.List; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.websocket.WebSocket; -import org.eclipse.jetty.websocket.WebSocketFactory; - -import com.glines.socketio.common.ConnectionState; -import com.glines.socketio.common.DisconnectReason; -import com.glines.socketio.common.SocketIOException; -import com.glines.socketio.server.SocketIOClosedException; -import com.glines.socketio.server.SocketIOInbound; -import com.glines.socketio.server.SocketIOFrame; -import com.glines.socketio.server.SocketIOSession; -import com.glines.socketio.server.Transport; - -public class WebSocketTransport extends AbstractTransport { - public static final String TRANSPORT_NAME = "websocket"; - public static final long CONNECTION_TIMEOUT = 10*1000; - private final WebSocketFactory wsFactory; - private final long maxIdleTime; - - private class SessionWrapper implements WebSocket, SocketIOSession.SessionTransportHandler { - private final SocketIOSession session; - private Outbound outbound = null; - private boolean initiated = false; - - SessionWrapper(SocketIOSession session) { - this.session = session; - session.setHeartbeat(maxIdleTime/2); - session.setTimeout(CONNECTION_TIMEOUT); - } - - /* - * (non-Javadoc) - * @see org.eclipse.jetty.websocket.WebSocket#onConnect(org.eclipse.jetty.websocket.WebSocket.Outbound) - */ - @Override - public void onConnect(final Outbound outbound) { - this.outbound = outbound; - } - - /* - * (non-Javadoc) - * @see org.eclipse.jetty.websocket.WebSocket#onDisconnect() - */ - @Override - public void onDisconnect() { - session.onShutdown(); - } - - /* - * (non-Javadoc) - * @see org.eclipse.jetty.websocket.WebSocket#onMessage(byte, java.lang.String) - */ - @Override - public void onMessage(byte frame, String message) { - session.startHeartbeatTimer(); - if (!initiated) { - if ("OPEN".equals(message)) { - try { - outbound.sendMessage(SocketIOFrame.encode(SocketIOFrame.FrameType.SESSION_ID, 0, session.getSessionId())); - outbound.sendMessage(SocketIOFrame.encode(SocketIOFrame.FrameType.HEARTBEAT_INTERVAL, 0, "" + session.getHeartbeat())); - session.onConnect(this); - initiated = true; - } catch (IOException e) { - outbound.disconnect(); - session.onShutdown(); - } - } else { - outbound.disconnect(); - session.onShutdown(); - } - } else { - List messages = SocketIOFrame.parse(message); - - for (SocketIOFrame msg: messages) { - session.onMessage(msg); - } - } - } - - /* - * (non-Javadoc) - * @see org.eclipse.jetty.websocket.WebSocket#onMessage(byte, byte[], int, int) - */ - @Override - public void onMessage(byte frame, byte[] data, int offset, int length) { - try - { - onMessage(frame,new String(data,offset,length,"UTF-8")); - } - catch(UnsupportedEncodingException e) - { - // Do nothing for now. - } - } - - @Override - public void onFragment(boolean more, byte opcode, byte[] data, int offset, int length) { - throw new UnsupportedOperationException(); - } - - /* - * (non-Javadoc) - * @see com.glines.socketio.SocketIOInbound.SocketIOOutbound#disconnect() - */ - @Override - public void disconnect() { - session.onDisconnect(DisconnectReason.DISCONNECT); - outbound.disconnect(); - } - - @Override - public void close() { - session.startClose(); - } - - @Override - public ConnectionState getConnectionState() { - return session.getConnectionState(); - } - - @Override - public void sendMessage(SocketIOFrame frame) throws SocketIOException { - if (outbound.isOpen()) { - Log.debug("Session["+session.getSessionId()+"]: sendMessage: [" + frame.getFrameType() + "]: " + frame.getData()); - try { - outbound.sendMessage(frame.encode()); - } catch (IOException e) { - outbound.disconnect(); - throw new SocketIOException(e); - } - } else { - throw new SocketIOClosedException(); - } - } - - - /* - * (non-Javadoc) - * @see com.glines.socketio.SocketIOInbound.SocketIOOutbound#sendMessage(java.lang.String) - */ - @Override - public void sendMessage(String message) throws SocketIOException { - sendMessage(SocketIOFrame.TEXT_MESSAGE_TYPE, message); - } - - @Override - public void sendMessage(int messageType, String message) - throws SocketIOException { - if (outbound.isOpen() && session.getConnectionState() == ConnectionState.CONNECTED) { - sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.DATA, messageType, message)); - } else { - throw new SocketIOClosedException(); - } - } - - /* - * (non-Javadoc) - * @see com.glines.socketio.SocketIOSession.SessionTransportHandler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, com.glines.socketio.SocketIOSession) - */ - @Override - public void handle(HttpServletRequest request, - HttpServletResponse response, SocketIOSession session) throws IOException { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unexpected request on upgraded WebSocket connection"); - return; - } - - @Override - public void disconnectWhenEmpty() { - } - - @Override - public void abort() { - outbound.disconnect(); - outbound = null; - session.onShutdown(); - } - } - - public WebSocketTransport(int bufferSize, int maxIdleTime) { - wsFactory = new WebSocketFactory(); - wsFactory.setBufferSize(bufferSize); - wsFactory.setMaxIdleTime(maxIdleTime); - this.maxIdleTime = maxIdleTime; - } - - @Override - public String getName() { - return TRANSPORT_NAME; - } - - @Override - public void handle(HttpServletRequest request, - HttpServletResponse response, - Transport.InboundFactory inboundFactory, - SocketIOSession.Factory sessionFactory) - throws IOException { - - String sessionId = extractSessionId(request); - - if ("GET".equals(request.getMethod()) && sessionId == null && "WebSocket".equals(request.getHeader("Upgrade"))) { - boolean hixie = request.getHeader("Sec-WebSocket-Key1") != null; - - String protocol=request.getHeader(hixie ? "Sec-WebSocket-Protocol" : "WebSocket-Protocol"); - if (protocol == null) - protocol=request.getHeader("Sec-WebSocket-Protocol"); - - String host=request.getHeader("Host"); - String origin=request.getHeader("Origin"); - if (origin == null) { - origin = host; - } - - SocketIOInbound inbound = inboundFactory.getInbound(request); - if (inbound == null) { - if (hixie) { - response.setHeader("Connection","close"); - } - response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - } else { - SocketIOSession session = sessionFactory.createSession(inbound); - SessionWrapper wrapper = new SessionWrapper(session); - wsFactory.upgrade(request,response,wrapper,origin,protocol); - } - } else { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid " + TRANSPORT_NAME + " transport request"); - } - } -} diff --git a/core/src/main/java/com/glines/socketio/server/transport/XHRMultipartDataHandler.java b/core/src/main/java/com/glines/socketio/server/transport/XHRMultipartDataHandler.java new file mode 100644 index 0000000..1352521 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/transport/XHRMultipartDataHandler.java @@ -0,0 +1,108 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server.transport; + +import com.glines.socketio.server.SocketIOFrame; +import com.glines.socketio.server.SocketIOSession; +import com.glines.socketio.util.Web; + +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Mathieu Carbou + */ +final class XHRMultipartDataHandler extends AbstractDataHandler { + + private static final Logger LOGGER = Logger.getLogger(XHRMultipartDataHandler.class.getName()); + private static final int MULTIPART_BOUNDARY_LENGTH = 20; + private static final long DEFAULT_HEARTBEAT_DELAY = 15 * 1000; + + private final SocketIOSession session; + + private long hearbeatDelay; + + private final String contentType; + private final String boundarySeperator; + + XHRMultipartDataHandler(SocketIOSession session) { + this.session = session; + String boundary = Web.generateRandomString(MULTIPART_BOUNDARY_LENGTH); + boundarySeperator = "--" + boundary; + contentType = "multipart/x-mixed-replace;boundary=\"" + boundary + "\""; + } + + @Override + protected void init() { + this.hearbeatDelay = getConfig().getHeartbeatDelay(DEFAULT_HEARTBEAT_DELAY); + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine(getConfig().getNamespace() + " data handler configuration:\n" + + " - heartbeatDelay=" + hearbeatDelay); + } + + @Override + public boolean isConnectionPersistent() { + return true; + } + + @Override + public void onStartSend(HttpServletResponse response) throws IOException { + response.setContentType(contentType); + response.setHeader("Connection", "keep-alive"); + ServletOutputStream os = response.getOutputStream(); + os.print(boundarySeperator); + response.flushBuffer(); + } + + @Override + public void onWriteData(ServletResponse response, String data) throws IOException { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "Session[" + session.getSessionId() + "]: writeData(START): " + data); + ServletOutputStream os = response.getOutputStream(); + os.println("Content-Type: text/plain; charset=utf-8"); + os.println(); + os.write(data.getBytes()); + os.println(); + os.println(boundarySeperator); + response.flushBuffer(); + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.log(Level.FINE, "Session[" + session.getSessionId() + "]: writeData(END): " + data); + } + + @Override + public void onFinishSend(ServletResponse response) throws IOException { + } + + @Override + public void onConnect(HttpServletRequest request, HttpServletResponse response) throws IOException { + onStartSend(response); + onWriteData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.CONNECT, session.getSessionId())); + } +} diff --git a/core/src/main/java/com/glines/socketio/server/transport/XHRMultipartTransport.java b/core/src/main/java/com/glines/socketio/server/transport/XHRMultipartTransport.java index fbf4809..354166c 100644 --- a/core/src/main/java/com/glines/socketio/server/transport/XHRMultipartTransport.java +++ b/core/src/main/java/com/glines/socketio/server/transport/XHRMultipartTransport.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,80 +24,17 @@ */ package com.glines.socketio.server.transport; -import java.io.IOException; -import java.util.Arrays; - -import javax.servlet.ServletOutputStream; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.util.log.Log; - -import com.glines.socketio.server.SocketIOFrame; import com.glines.socketio.server.SocketIOSession; -import com.glines.socketio.server.transport.ConnectionTimeoutPreventor.IdleCheck; - -public class XHRMultipartTransport extends XHRTransport { - public static final String TRANSPORT_NAME = "xhr-multipart"; - private static final int MULTIPART_BOUNDARY_LENGTH = 20; - - private class XHRMultipartSessionHelper extends XHRSessionHelper { - private final String contentType; - private final String boundary; - private final String boundarySeperator; - private final IdleCheck idleCheck; - - XHRMultipartSessionHelper(SocketIOSession session, IdleCheck idleCheck) { - super(session, true); - boundary = session.generateRandomString(MULTIPART_BOUNDARY_LENGTH); - boundarySeperator = "--" + boundary; - contentType = "multipart/x-mixed-replace;boundary=\""+boundary+"\""; - this.idleCheck = idleCheck; - } - - protected void startSend(HttpServletResponse response) throws IOException { - response.setContentType(contentType); - response.setHeader("Connection", "keep-alive"); - ServletOutputStream os = response.getOutputStream(); - os.print(boundarySeperator); - response.flushBuffer(); - } - - protected void writeData(ServletResponse response, String data) throws IOException { - idleCheck.activity(); - Log.debug("Session["+session.getSessionId()+"]: writeData(START): " + data); - ServletOutputStream os = response.getOutputStream(); - os.println("Content-Type: text/plain"); - os.println(); - os.println(data); - os.println(boundarySeperator); - response.flushBuffer(); - Log.debug("Session["+session.getSessionId()+"]: writeData(END): " + data); - } - - protected void finishSend(ServletResponse response) throws IOException { - }; - - protected void customConnect(HttpServletRequest request, - HttpServletResponse response) throws IOException { - startSend(response); - writeData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.SESSION_ID, 0, session.getSessionId())); - writeData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.HEARTBEAT_INTERVAL, 0, "" + HEARTBEAT_DELAY)); - } - } - - public XHRMultipartTransport(int bufferSize, int maxIdleTime) { - super(bufferSize, maxIdleTime); - } - - @Override - public String getName() { - return TRANSPORT_NAME; - } - - protected XHRSessionHelper createHelper(SocketIOSession session) { - IdleCheck idleCheck = ConnectionTimeoutPreventor.newTimeoutPreventor(); - return new XHRMultipartSessionHelper(session, idleCheck); - } +import com.glines.socketio.server.TransportType; + +public class XHRMultipartTransport extends AbstractHttpTransport { + @Override + public TransportType getType() { + return TransportType.XHR_MULTIPART; + } + + @Override + protected DataHandler newDataHandler(SocketIOSession session) { + return new XHRMultipartDataHandler(session); + } } diff --git a/core/src/main/java/com/glines/socketio/server/transport/XHRPollingDataHandler.java b/core/src/main/java/com/glines/socketio/server/transport/XHRPollingDataHandler.java new file mode 100644 index 0000000..a457dd9 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/server/transport/XHRPollingDataHandler.java @@ -0,0 +1,89 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.server.transport; + +import com.glines.socketio.server.SocketIOFrame; +import com.glines.socketio.server.SocketIOSession; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Mathieu Carbou + */ +final class XHRPollingDataHandler extends AbstractDataHandler { + + /** + * For non persistent connection transports, this is the amount of time to wait + * for messages before returning empty results. + */ + private static final long DEFAULT_TIMEOUT = 20 * 1000; + private static final Logger LOGGER = Logger.getLogger(XHRPollingDataHandler.class.getName()); + + private final SocketIOSession session; + private long timeout; + + XHRPollingDataHandler(SocketIOSession session) { + this.session = session; + } + + @Override + protected void init() { + this.timeout = getConfig().getTimeout(DEFAULT_TIMEOUT); + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine(getConfig().getNamespace() + " data handler configuration:\n" + + " - timeout=" + timeout); + } + + @Override + public boolean isConnectionPersistent() { + return false; + } + + @Override + public void onStartSend(HttpServletResponse response) throws IOException { + response.setContentType("text/plain; charset=UTF-8"); + } + + @Override + public void onWriteData(ServletResponse response, String data) throws IOException { + response.getOutputStream().print(data); + response.flushBuffer(); + } + + @Override + public void onFinishSend(ServletResponse response) throws IOException { + } + + @Override + public void onConnect(HttpServletRequest request, HttpServletResponse response) throws IOException { + onStartSend(response); + onWriteData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.CONNECT, session.getSessionId())); + } +} diff --git a/core/src/main/java/com/glines/socketio/server/transport/XHRPollingTransport.java b/core/src/main/java/com/glines/socketio/server/transport/XHRPollingTransport.java index b3e6c74..6021519 100644 --- a/core/src/main/java/com/glines/socketio/server/transport/XHRPollingTransport.java +++ b/core/src/main/java/com/glines/socketio/server/transport/XHRPollingTransport.java @@ -2,6 +2,8 @@ * The MIT License * Copyright (c) 2010 Tad Glines * + * Contributors: Ovea.com, Mycila.com + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -22,55 +24,17 @@ */ package com.glines.socketio.server.transport; -import java.io.IOException; - -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.glines.socketio.server.SocketIOFrame; import com.glines.socketio.server.SocketIOSession; - -public class XHRPollingTransport extends XHRTransport { - public static final String TRANSPORT_NAME = "xhr-polling"; - - protected class XHRPollingSessionHelper extends XHRSessionHelper { - - XHRPollingSessionHelper(SocketIOSession session) { - super(session, false); - } - - protected void startSend(HttpServletResponse response) throws IOException { - response.setContentType("text/plain; charset=UTF-8"); - } - - @Override - protected void writeData(ServletResponse response, String data) throws IOException { - response.getOutputStream().print(data); - response.flushBuffer(); - } - - protected void finishSend(ServletResponse response) throws IOException {}; - - protected void customConnect(HttpServletRequest request, - HttpServletResponse response) throws IOException { - startSend(response); - writeData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.SESSION_ID, 0, session.getSessionId())); - writeData(response, SocketIOFrame.encode(SocketIOFrame.FrameType.HEARTBEAT_INTERVAL, 0, "" + REQUEST_TIMEOUT)); - } - } - - public XHRPollingTransport(int bufferSize, int maxIdleTime) { - super(bufferSize, maxIdleTime); - } - - @Override - public String getName() { - return TRANSPORT_NAME; - } - - - protected XHRPollingSessionHelper createHelper(SocketIOSession session) { - return new XHRPollingSessionHelper(session); - } +import com.glines.socketio.server.TransportType; + +public class XHRPollingTransport extends AbstractHttpTransport { + @Override + public TransportType getType() { + return TransportType.XHR_POLLING; + } + + @Override + protected DataHandler newDataHandler(SocketIOSession session) { + return new XHRPollingDataHandler(session); + } } diff --git a/core/src/main/java/com/glines/socketio/server/transport/XHRTransport.java b/core/src/main/java/com/glines/socketio/server/transport/XHRTransport.java deleted file mode 100644 index 22e2960..0000000 --- a/core/src/main/java/com/glines/socketio/server/transport/XHRTransport.java +++ /dev/null @@ -1,402 +0,0 @@ -/** - * The MIT License - * Copyright (c) 2010 Tad Glines - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.glines.socketio.server.transport; - -import java.io.BufferedReader; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.continuation.Continuation; -import org.eclipse.jetty.continuation.ContinuationListener; -import org.eclipse.jetty.continuation.ContinuationSupport; -import org.eclipse.jetty.util.IO; -import org.eclipse.jetty.util.URIUtil; -import org.eclipse.jetty.util.log.Log; - -import com.glines.socketio.common.ConnectionState; -import com.glines.socketio.common.DisconnectReason; -import com.glines.socketio.common.SocketIOException; -import com.glines.socketio.server.SocketIOClosedException; -import com.glines.socketio.server.SocketIOInbound; -import com.glines.socketio.server.SocketIOFrame; -import com.glines.socketio.server.SocketIOSession; -import com.glines.socketio.server.SocketIOSession.SessionTransportHandler; -import com.glines.socketio.server.Transport; - -public abstract class XHRTransport extends AbstractHttpTransport { - public static final String CONTINUATION_KEY = - "com.glines.socketio.server.transport.XHRTransport.Continuation"; - private final int bufferSize; - private final int maxIdleTime; - - protected abstract class XHRSessionHelper - implements SessionTransportHandler, ContinuationListener { - protected final SocketIOSession session; - private final TransportBuffer buffer = new TransportBuffer(bufferSize); - private volatile boolean is_open = false; - private volatile Continuation continuation = null; - private final boolean isConnectionPersistant; - private boolean disconnectWhenEmpty = false; - - XHRSessionHelper(SocketIOSession session, boolean isConnectionPersistant) { - this.session = session; - this.isConnectionPersistant = isConnectionPersistant; - if (isConnectionPersistant) { - session.setHeartbeat(HEARTBEAT_DELAY); - session.setTimeout(HEARTBEAT_TIMEOUT); - } else { - session.setTimeout((HTTP_REQUEST_TIMEOUT-REQUEST_TIMEOUT)/2); - } - } - - protected abstract void startSend(HttpServletResponse response) throws IOException; - - protected abstract void writeData(ServletResponse response, String data) throws IOException; - - protected abstract void finishSend(ServletResponse response) throws IOException; - - @Override - public void disconnect() { - synchronized (this) { - session.onDisconnect(DisconnectReason.DISCONNECT); - abort(); - } - } - - @Override - public void close() { - synchronized (this) { - session.startClose(); - } - } - - @Override - public ConnectionState getConnectionState() { - return session.getConnectionState(); - } - - @Override - public void sendMessage(SocketIOFrame frame) - throws SocketIOException { - synchronized (this) { - Log.debug("Session["+session.getSessionId()+"]: " + - "sendMessage(frame): [" + frame.getFrameType() + "]: " + frame.getData()); - if (is_open) { - if (continuation != null) { - List messages = buffer.drainMessages(); - messages.add(frame.encode()); - StringBuilder data = new StringBuilder(); - for (String msg: messages) { - data.append(msg); - } - try { - writeData(continuation.getServletResponse(), data.toString()); - } catch (IOException e) { - throw new SocketIOException(e); - } - if (!isConnectionPersistant && !continuation.isInitial()) { - Continuation cont = continuation; - continuation = null; - cont.complete(); - } else { - session.startHeartbeatTimer(); - } - } else { - String data = frame.encode(); - if (buffer.putMessage(data, maxIdleTime) == false) { - session.onDisconnect(DisconnectReason.TIMEOUT); - abort(); - throw new SocketIOException(); - } - } - } else { - throw new SocketIOClosedException(); - } - } - } - - @Override - public void sendMessage(String message) throws SocketIOException { - Log.debug("Session["+session.getSessionId()+"]: " + - "sendMessage(String): " + message); - sendMessage(SocketIOFrame.TEXT_MESSAGE_TYPE, message); - } - - @Override - public void sendMessage(int messageType, String message) - throws SocketIOException { - synchronized (this) { - Log.debug("Session["+session.getSessionId()+"]: " + - "sendMessage(int, String): [" + messageType + "]: " + message); - if (is_open && session.getConnectionState() == ConnectionState.CONNECTED) { - sendMessage(new SocketIOFrame(SocketIOFrame.FrameType.DATA, messageType, message)); - } else { - throw new SocketIOClosedException(); - } - } - } - - @Override - public void handle(HttpServletRequest request, - HttpServletResponse response, SocketIOSession session) - throws IOException { - if ("GET".equals(request.getMethod())) { - synchronized (this) { - if (!is_open && buffer.isEmpty()) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - } else { - /* - */ - Continuation cont = (Continuation)request.getAttribute(CONTINUATION_KEY); - if (continuation != null || cont != null) { - if (continuation == cont) { - continuation = null; - finishSend(response); - } - if (cont != null) { - request.removeAttribute(CONTINUATION_KEY); - } - return; - } - if (!isConnectionPersistant) { - if (!buffer.isEmpty()) { - List messages = buffer.drainMessages(); - if (messages.size() > 0) { - StringBuilder data = new StringBuilder(); - for (String msg: messages) { - data.append(msg); - } - startSend(response); - writeData(response, data.toString()); - finishSend(response); - if (!disconnectWhenEmpty) { - session.startTimeoutTimer(); - } else { - abort(); - } - } - } else { - session.clearTimeoutTimer(); - request.setAttribute(SESSION_KEY, session); - response.setBufferSize(bufferSize); - continuation = ContinuationSupport.getContinuation(request); - continuation.addContinuationListener(this); - continuation.setTimeout(REQUEST_TIMEOUT); - continuation.suspend(response); - request.setAttribute(CONTINUATION_KEY, continuation); - startSend(response); - } - } else { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - } - } - } - } else if ("POST".equals(request.getMethod())) { - if (is_open) { - int size = request.getContentLength(); - BufferedReader reader = request.getReader(); - if (size == 0) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - } else { - String data = decodePostData(request.getContentType(), IO.toString(reader)); - if (data != null && data.length() > 0) { - List list = SocketIOFrame.parse(data); - synchronized (session) { - for (SocketIOFrame msg: list) { - session.onMessage(msg); - } - } - } - // Ensure that the disconnectWhenEmpty flag is obeyed in the case where - // it is set during a POST. - synchronized (this) { - if (disconnectWhenEmpty && buffer.isEmpty()) { - if (session.getConnectionState() == ConnectionState.CLOSING) { - session.onDisconnect(DisconnectReason.CLOSED); - } - abort(); - } - } - } - } - } else { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - } - - } - - protected String decodePostData(String contentType, String data) { - if (contentType.startsWith("application/x-www-form-urlencoded")) { - if (data.substring(0, 5).equals("data=")) { - return URIUtil.decodePath(data.substring(5)); - } else { - return ""; - } - } else if (contentType.startsWith("text/plain")) { - return data; - } else { - // TODO: Treat as text for now, maybe error in the future. - return data; - } - } - - @Override - public void onComplete(Continuation cont) { - if (continuation != null && cont == continuation) { - continuation = null; - if (isConnectionPersistant) { - is_open = false; - if (!disconnectWhenEmpty) { - session.onDisconnect(DisconnectReason.DISCONNECT); - } - abort(); - } else { - if (!is_open && buffer.isEmpty() && !disconnectWhenEmpty) { - session.onDisconnect(DisconnectReason.DISCONNECT); - abort(); - } else { - if (disconnectWhenEmpty) { - abort(); - } else { - session.startTimeoutTimer(); - } - } - } - } - } - - @Override - public void onTimeout(Continuation cont) { - if (continuation != null && cont == continuation) { - continuation = null; - if (isConnectionPersistant) { - is_open = false; - session.onDisconnect(DisconnectReason.TIMEOUT); - abort(); - } else { - if (!is_open && buffer.isEmpty()) { - session.onDisconnect(DisconnectReason.DISCONNECT); - abort(); - } else { - try { - finishSend(cont.getServletResponse()); - } catch (IOException e) { - session.onDisconnect(DisconnectReason.DISCONNECT); - abort(); - } - } - session.startTimeoutTimer(); - } - } - } - - protected abstract void customConnect(HttpServletRequest request, - HttpServletResponse response) throws IOException; - - public void connect(HttpServletRequest request, - HttpServletResponse response) throws IOException { - request.setAttribute(SESSION_KEY, session); - response.setBufferSize(bufferSize); - continuation = ContinuationSupport.getContinuation(request); - continuation.addContinuationListener(this); - if (isConnectionPersistant) { - continuation.setTimeout(0); - } - customConnect(request, response); - is_open = true; - session.onConnect(this); - finishSend(response); - if (continuation != null) { - if (isConnectionPersistant) { - request.setAttribute(CONTINUATION_KEY, continuation); - continuation.suspend(response); - } else { - continuation = null; - } - } - } - - @Override - public void disconnectWhenEmpty() { - disconnectWhenEmpty = true; - } - - @Override - public void abort() { - session.clearHeartbeatTimer(); - session.clearTimeoutTimer(); - is_open = false; - if (continuation != null) { - Continuation cont = continuation; - continuation = null; - if (cont.isSuspended()) { - cont.complete(); - } - } - buffer.setListener(new TransportBuffer.BufferListener() { - @Override - public boolean onMessages(List messages) { - return false; - } - - @Override - public boolean onMessage(String message) { - return false; - } - }); - buffer.clear(); - session.onShutdown(); - } - } - - public XHRTransport(int bufferSize, int maxIdleTime) { - this.bufferSize = bufferSize; - this.maxIdleTime = maxIdleTime; - } - - /** - * This method should only be called within the context of an active HTTP request. - */ - protected abstract XHRSessionHelper createHelper(SocketIOSession session); - - @Override - protected SocketIOSession connect(HttpServletRequest request, - HttpServletResponse response, Transport.InboundFactory inboundFactory, - com.glines.socketio.server.SocketIOSession.Factory sessionFactory) - throws IOException { - SocketIOInbound inbound = inboundFactory.getInbound(request); - if (inbound != null) { - SocketIOSession session = sessionFactory.createSession(inbound); - XHRSessionHelper handler = createHelper(session); - handler.connect(request, response); - return session; - } - return null; - } - -} diff --git a/core/src/main/java/com/glines/socketio/util/DefaultLoader.java b/core/src/main/java/com/glines/socketio/util/DefaultLoader.java new file mode 100644 index 0000000..b774f76 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/util/DefaultLoader.java @@ -0,0 +1,86 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.util; + +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Mathieu Carbou (mathieu.carbou@gmail.com) + */ +public class DefaultLoader implements Loader { + + private final ClassLoader classLoader; + + public DefaultLoader() { + this(getDefaultClassLoader()); + } + + public DefaultLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public Class loadClass(String className) { + try { + return classLoader.loadClass(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Unable to load class " + className, e); + } + } + + @Override + public URL getResource(String path) { + return classLoader.getResource(path); + } + + @Override + public List getResources(String path) { + List urls = new LinkedList(); + try { + Enumeration e = classLoader.getResources(path); + while (e.hasMoreElements()) + urls.add(e.nextElement()); + } catch (IOException ignored) { + } + return urls; + } + + private static ClassLoader getDefaultClassLoader() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } + catch (Throwable ignored) { + } + if (cl == null) + cl = DefaultLoader.class.getClassLoader(); + return cl; + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/glines/socketio/util/IO.java b/core/src/main/java/com/glines/socketio/util/IO.java new file mode 100644 index 0000000..cb18dd4 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/util/IO.java @@ -0,0 +1,65 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.util; + +import java.io.*; + +/** + * @author Mathieu Carbou + */ +public final class IO { + private IO() { + } + + public static void copy(InputStream is, OutputStream os) throws IOException { + byte[] buffer = new byte[64 * 1024]; + int len; + while ((len = is.read(buffer)) >= 0) + os.write(buffer, 0, len); + } + + public static void copy(Reader in, Writer out) throws IOException { + char[] buffer = new char[64 * 1024]; + int len; + while ((len = in.read(buffer)) >= 0) + out.write(buffer, 0, len); + } + + public static String toString(InputStream in) throws IOException { + return toString(in, null); + } + + public static String toString(Reader in) throws IOException { + StringWriter writer = new StringWriter(); + copy(in, writer); + return writer.toString(); + } + + public static String toString(InputStream in, String encoding) throws IOException { + InputStreamReader reader = encoding == null ? new InputStreamReader(in) : new InputStreamReader(in, encoding); + return toString(reader); + } + +} diff --git a/core/src/main/java/com/glines/socketio/util/JSON.java b/core/src/main/java/com/glines/socketio/util/JSON.java new file mode 100644 index 0000000..174f88c --- /dev/null +++ b/core/src/main/java/com/glines/socketio/util/JSON.java @@ -0,0 +1,1116 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.util; + +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Array; +import java.util.*; + +public final class JSON { + + private static final int BUFFER = 1024; + + private JSON() { + } + + public static String toString(Object object) { + StringBuilder buffer = new StringBuilder(BUFFER); + append(buffer, object); + return buffer.toString(); + } + + + public static String toString(Map object) { + StringBuilder buffer = new StringBuilder(BUFFER); + appendMap(buffer, object); + return buffer.toString(); + } + + + public static String toString(Object[] array) { + StringBuilder buffer = new StringBuilder(BUFFER); + appendArray(buffer, array); + return buffer.toString(); + } + + + /** + * @param s String containing JSON object or array. + * @return A Map, Object array or primitive array parsed from the JSON. + */ + public static Object parse(String s) { + return parse(new StringSource(s), false); + } + + + /** + * @param s String containing JSON object or array. + * @param stripOuterComment If true, an outer comment around the JSON is ignored. + * @return A Map, Object array or primitive array parsed from the JSON. + */ + public static Object parse(String s, boolean stripOuterComment) { + return parse(new StringSource(s), stripOuterComment); + } + + + /** + * @param in Reader containing JSON object or array. + * @return A Map, Object array or primitive array parsed from the JSON. + * @throws java.io.IOException - + */ + public static Object parse(Reader in) throws IOException { + return parse(new ReaderSource(in), false); + } + + + /** + * @param in Reader containing JSON object or array. + * @param stripOuterComment If true, an outer comment around the JSON is ignored. + * @return A Map, Object array or primitive array parsed from the JSON. + * @throws java.io.IOException - + */ + public static Object parse(Reader in, boolean stripOuterComment) throws IOException { + return parse(new ReaderSource(in), stripOuterComment); + } + + + /** + * Append object as JSON to string buffer. + * + * @param buffer the buffer to append to + * @param object the object to append + */ + public static void append(Appendable buffer, Object object) { + try { + if (object == null) + buffer.append("null"); + else if (object instanceof Convertible) + appendJSON(buffer, (Convertible) object); + else if (object instanceof Generator) + appendJSON(buffer, (Generator) object); + else if (object instanceof Map) + appendMap(buffer, (Map) object); + else if (object instanceof Collection) + appendArray(buffer, (Collection) object); + else if (object.getClass().isArray()) + appendArray(buffer, object); + else if (object instanceof Number) + appendNumber(buffer, (Number) object); + else if (object instanceof Boolean) + appendBoolean(buffer, (Boolean) object); + else if (object instanceof Character) + appendString(buffer, object.toString()); + else if (object instanceof String) + appendString(buffer, (String) object); + else { + appendString(buffer, object.toString()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public static void appendNull(Appendable buffer) { + try { + buffer.append("null"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public static void appendJSON(final Appendable buffer, Convertible converter) { + ConvertableOutput out = new ConvertableOutput(buffer); + converter.toJSON(out); + out.complete(); + } + + + public static void appendJSON(Appendable buffer, Generator generator) { + generator.addJSON(buffer); + } + + + public static void appendMap(Appendable buffer, Map map) { + try { + if (map == null) { + appendNull(buffer); + return; + } + + buffer.append('{'); + Iterator iter = map.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = (Map.Entry) iter.next(); + quote(buffer, entry.getKey().toString()); + buffer.append(':'); + append(buffer, entry.getValue()); + if (iter.hasNext()) + buffer.append(','); + } + + buffer.append('}'); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public static void appendArray(Appendable buffer, Collection collection) { + try { + if (collection == null) { + appendNull(buffer); + return; + } + + buffer.append('['); + Iterator iter = collection.iterator(); + boolean first = true; + while (iter.hasNext()) { + if (!first) + buffer.append(','); + + first = false; + append(buffer, iter.next()); + } + + buffer.append(']'); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public static void appendArray(Appendable buffer, Object array) { + try { + if (array == null) { + appendNull(buffer); + return; + } + + buffer.append('['); + int length = Array.getLength(array); + + for (int i = 0; i < length; i++) { + if (i != 0) + buffer.append(','); + append(buffer, Array.get(array, i)); + } + + buffer.append(']'); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public static void appendBoolean(Appendable buffer, Boolean b) { + try { + if (b == null) { + appendNull(buffer); + return; + } + buffer.append(b ? "true" : "false"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public static void appendNumber(Appendable buffer, Number number) { + try { + if (number == null) { + appendNull(buffer); + return; + } + buffer.append(String.valueOf(number)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public static void appendString(Appendable buffer, String string) { + if (string == null) { + appendNull(buffer); + return; + } + quote(buffer, string); + } + + // Parsing utilities + + + protected static String toString(char[] buffer, int offset, int length) { + return new String(buffer, offset, length); + } + + + protected static Map newMap() { + return new HashMap(); + } + + + protected static Object[] newArray(int size) { + return new Object[size]; + } + + protected static Object convertTo(Class type, Map map) { + if (type != null && Convertible.class.isAssignableFrom(type)) { + try { + Convertible conv = (Convertible) type.newInstance(); + conv.fromJSON(map); + return conv; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return map; + } + + + public static Object parse(Source source, boolean stripOuterComment) { + int comment_state = 0; // 0=no comment, 1="/", 2="/*", 3="/* *" -1="//" + if (!stripOuterComment) + return parse(source); + + int strip_state = 1; // 0=no strip, 1=wait for /*, 2= wait for */ + + Object o = null; + while (source.hasNext()) { + char c = source.peek(); + + // handle // or /* comment + if (comment_state == 1) { + switch (c) { + case '/': + comment_state = -1; + break; + case '*': + comment_state = 2; + if (strip_state == 1) { + comment_state = 0; + strip_state = 2; + } + } + } + // handle /* */ comment + else if (comment_state > 1) { + switch (c) { + case '*': + comment_state = 3; + break; + case '/': + if (comment_state == 3) { + comment_state = 0; + if (strip_state == 2) + return o; + } else + comment_state = 2; + break; + default: + comment_state = 2; + } + } + // handle // comment + else if (comment_state < 0) { + switch (c) { + case '\r': + case '\n': + comment_state = 0; + default: + break; + } + } + // handle unknown + else { + if (!Character.isWhitespace(c)) { + if (c == '/') + comment_state = 1; + else if (c == '*') + comment_state = 3; + else if (o == null) { + o = parse(source); + continue; + } + } + } + + source.next(); + } + + return o; + } + + + public static Object parse(Source source) { + int comment_state = 0; // 0=no comment, 1="/", 2="/*", 3="/* *" -1="//" + + while (source.hasNext()) { + char c = source.peek(); + + // handle // or /* comment + if (comment_state == 1) { + switch (c) { + case '/': + comment_state = -1; + break; + case '*': + comment_state = 2; + } + } + // handle /* */ comment + else if (comment_state > 1) { + switch (c) { + case '*': + comment_state = 3; + break; + case '/': + if (comment_state == 3) + comment_state = 0; + else + comment_state = 2; + break; + default: + comment_state = 2; + } + } + // handle // comment + else if (comment_state < 0) { + switch (c) { + case '\r': + case '\n': + comment_state = 0; + break; + default: + break; + } + } + // handle unknown + else { + switch (c) { + case '{': + return parseObject(source); + case '[': + return parseArray(source); + case '"': + return parseString(source); + case '-': + return parseNumber(source); + + case 'n': + complete("null", source); + return null; + case 't': + complete("true", source); + return Boolean.TRUE; + case 'f': + complete("false", source); + return Boolean.FALSE; + case 'u': + complete("undefined", source); + return null; + case 'N': + complete("NaN", source); + return null; + + case '/': + comment_state = 1; + break; + + default: + if (Character.isDigit(c)) + return parseNumber(source); + else if (Character.isWhitespace(c)) + break; + return handleUnknown(source, c); + } + } + source.next(); + } + + return null; + } + + + protected static Object handleUnknown(Source source, char c) { + throw new IllegalStateException("unknown char '" + c + "'(" + (int) c + ") in " + source); + } + + + private static Object parseObject(Source source) { + if (source.next() != '{') + throw new IllegalStateException(); + Map map = newMap(); + + char next = seekTo("\"}", source); + + while (source.hasNext()) { + if (next == '}') { + source.next(); + break; + } + + String name = parseString(source); + seekTo(':', source); + source.next(); + + Object value = parse(source); + map.put(name, value); + + seekTo(",}", source); + next = source.next(); + if (next == '}') + break; + else + next = seekTo("\"}", source); + } + + String classname = (String) map.get("class"); + if (classname != null) { + try { + Class c = Thread.currentThread().getContextClassLoader().loadClass(classname); + return convertTo(c, map); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + return map; + } + + + protected static Object parseArray(Source source) { + if (source.next() != '[') + throw new IllegalStateException(); + + int size = 0; + ArrayList list = null; + Object item = null; + boolean coma = true; + + while (source.hasNext()) { + char c = source.peek(); + switch (c) { + case ']': + source.next(); + switch (size) { + case 0: + return newArray(0); + case 1: + Object array = newArray(1); + Array.set(array, 0, item); + return array; + default: + return list.toArray(newArray(list.size())); + } + + case ',': + if (coma) + throw new IllegalStateException(); + coma = true; + source.next(); + break; + + default: + if (Character.isWhitespace(c)) + source.next(); + else { + coma = false; + if (size++ == 0) + item = parse(source); + else if (list == null) { + list = new ArrayList(); + list.add(item); + item = parse(source); + list.add(item); + item = null; + } else { + item = parse(source); + list.add(item); + item = null; + } + } + } + + } + + throw new IllegalStateException("unexpected end of array"); + } + + + protected static String parseString(Source source) { + if (source.next() != '"') + throw new IllegalStateException(); + + boolean escape = false; + + StringBuilder b = null; + final char[] scratch = source.scratchBuffer(); + + if (scratch != null) { + int i = 0; + while (source.hasNext()) { + if (i >= scratch.length) { + // we have filled the scratch buffer, so we must + // use the StringBuffer for a large string + b = new StringBuilder(scratch.length * 2); + b.append(scratch, 0, i); + break; + } + + char c = source.next(); + + if (escape) { + escape = false; + switch (c) { + case '"': + scratch[i++] = '"'; + break; + case '\\': + scratch[i++] = '\\'; + break; + case '/': + scratch[i++] = '/'; + break; + case 'b': + scratch[i++] = '\b'; + break; + case 'f': + scratch[i++] = '\f'; + break; + case 'n': + scratch[i++] = '\n'; + break; + case 'r': + scratch[i++] = '\r'; + break; + case 't': + scratch[i++] = '\t'; + break; + case 'u': + char uc = (char) ((convertHexDigit((byte) source.next()) << 12) + (convertHexDigit((byte) source.next()) << 8) + + (convertHexDigit((byte) source.next()) << 4) + (convertHexDigit((byte) source.next()))); + scratch[i++] = uc; + break; + default: + scratch[i++] = c; + } + } else if (c == '\\') { + escape = true; + } else if (c == '\"') { + // Return string that fits within scratch buffer + return toString(scratch, 0, i); + } else + scratch[i++] = c; + } + + // Missing end quote, but return string anyway ? + if (b == null) + return toString(scratch, 0, i); + } else + b = new StringBuilder(BUFFER); + + // parse large string into string buffer + final StringBuilder builder = b; + while (source.hasNext()) { + char c = source.next(); + + if (escape) { + escape = false; + switch (c) { + case '"': + builder.append('"'); + break; + case '\\': + builder.append('\\'); + break; + case '/': + builder.append('/'); + break; + case 'b': + builder.append('\b'); + break; + case 'f': + builder.append('\f'); + break; + case 'n': + builder.append('\n'); + break; + case 'r': + builder.append('\r'); + break; + case 't': + builder.append('\t'); + break; + case 'u': + char uc = (char) ((convertHexDigit((byte) source.next()) << 12) + (convertHexDigit((byte) source.next()) << 8) + + (convertHexDigit((byte) source.next()) << 4) + (convertHexDigit((byte) source.next()))); + builder.append(uc); + break; + default: + builder.append(c); + } + } else if (c == '\\') { + escape = true; + } else if (c == '\"') + break; + else + builder.append(c); + } + return builder.toString(); + } + + + public static Number parseNumber(Source source) { + boolean minus = false; + long number = 0; + StringBuilder buffer = null; + + longLoop: + while (source.hasNext()) { + char c = source.peek(); + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + number = number * 10 + (c - '0'); + source.next(); + break; + + case '-': + case '+': + if (number != 0) + throw new IllegalStateException("bad number"); + minus = true; + source.next(); + break; + + case '.': + case 'e': + case 'E': + buffer = new StringBuilder(16); + if (minus) + buffer.append('-'); + buffer.append(number); + buffer.append(c); + source.next(); + break longLoop; + + default: + break longLoop; + } + } + + if (buffer == null) + return minus ? -1 * number : number; + + doubleLoop: + while (source.hasNext()) { + char c = source.peek(); + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + case '.': + case '+': + case 'e': + case 'E': + buffer.append(c); + source.next(); + break; + + default: + break doubleLoop; + } + } + return new Double(buffer.toString()); + + } + + + protected static void seekTo(char seek, Source source) { + while (source.hasNext()) { + char c = source.peek(); + if (c == seek) + return; + + if (!Character.isWhitespace(c)) + throw new IllegalStateException("Unexpected '" + c + " while seeking '" + seek + "'"); + source.next(); + } + + throw new IllegalStateException("Expected '" + seek + "'"); + } + + + protected static char seekTo(String seek, Source source) { + while (source.hasNext()) { + char c = source.peek(); + if (seek.indexOf(c) >= 0) { + return c; + } + + if (!Character.isWhitespace(c)) + throw new IllegalStateException("Unexpected '" + c + "' while seeking one of '" + seek + "'"); + source.next(); + } + + throw new IllegalStateException("Expected one of '" + seek + "'"); + } + + + protected static void complete(String seek, Source source) { + int i = 0; + while (source.hasNext() && i < seek.length()) { + char c = source.next(); + if (c != seek.charAt(i++)) + throw new IllegalStateException("Unexpected '" + c + " while seeking \"" + seek + "\""); + } + + if (i < seek.length()) + throw new IllegalStateException("Expected \"" + seek + "\""); + } + + private static final class ConvertableOutput implements Output { + private final Appendable _buffer; + char c = '{'; + + private ConvertableOutput(Appendable buffer) { + _buffer = buffer; + } + + public void complete() { + try { + if (c == '{') + _buffer.append("{}"); + else if (c != 0) + _buffer.append("}"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void add(Object obj) { + if (c == 0) + throw new IllegalStateException(); + append(_buffer, obj); + c = 0; + } + + public void addClass(Class type) { + try { + if (c == 0) + throw new IllegalStateException(); + _buffer.append(c); + _buffer.append("\"class\":"); + append(_buffer, type.getName()); + c = ','; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void add(String name, Object value) { + try { + if (c == 0) + throw new IllegalStateException(); + _buffer.append(c); + quote(_buffer, name); + _buffer.append(':'); + append(_buffer, value); + c = ','; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void add(String name, double value) { + try { + if (c == 0) + throw new IllegalStateException(); + _buffer.append(c); + quote(_buffer, name); + _buffer.append(':'); + appendNumber(_buffer, value); + c = ','; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void add(String name, long value) { + try { + if (c == 0) + throw new IllegalStateException(); + _buffer.append(c); + quote(_buffer, name); + _buffer.append(':'); + appendNumber(_buffer, value); + c = ','; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void add(String name, boolean value) { + try { + if (c == 0) + throw new IllegalStateException(); + _buffer.append(c); + quote(_buffer, name); + _buffer.append(':'); + appendBoolean(_buffer, value ? Boolean.TRUE : Boolean.FALSE); + c = ','; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + + public static interface Source { + boolean hasNext(); + + char next(); + + char peek(); + + char[] scratchBuffer(); + } + + + public static class StringSource implements Source { + private final String string; + private int index; + private char[] scratch; + + public StringSource(String s) { + string = s; + } + + public boolean hasNext() { + if (index < string.length()) + return true; + scratch = null; + return false; + } + + public char next() { + return string.charAt(index++); + } + + public char peek() { + return string.charAt(index); + } + + @Override + public String toString() { + return string.substring(0, index) + "|||" + string.substring(index); + } + + public char[] scratchBuffer() { + if (scratch == null) + scratch = new char[string.length()]; + return scratch; + } + } + + + public static class ReaderSource implements Source { + private Reader _reader; + private int _next = -1; + private char[] scratch; + + public ReaderSource(Reader r) { + _reader = r; + } + + public boolean hasNext() { + getNext(); + if (_next < 0) { + scratch = null; + return false; + } + return true; + } + + public char next() { + getNext(); + char c = (char) _next; + _next = -1; + return c; + } + + public char peek() { + getNext(); + return (char) _next; + } + + private void getNext() { + if (_next < 0) { + try { + _next = _reader.read(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + public char[] scratchBuffer() { + if (scratch == null) + scratch = new char[1024]; + return scratch; + } + + } + + /** + * JSON Output class for use by {@link Convertible}. + */ + public static interface Output { + public void addClass(Class c); + + public void add(Object obj); + + public void add(String name, Object value); + + public void add(String name, double value); + + public void add(String name, long value); + + public void add(String name, boolean value); + } + + + /** + * JSON Convertible object. Object can implement this interface in a similar + * way to the {@link java.io.Externalizable} interface is used to allow classes to + * provide their own serialization mechanism. + *

+ * A JSON.Convertible object may be written to a JSONObject or initialized + * from a Map of field names to values. + *

+ * If the JSON is to be convertible back to an Object, then the method + * {@link Output#addClass(Class)} must be called from within toJSON() + */ + public static interface Convertible { + public void toJSON(Output out); + + public void fromJSON(Map object); + } + + + /** + * JSON Generator. A class that can add it's JSON representation directly to + * a StringBuffer. This is useful for object instances that are frequently + * converted and wish to avoid multiple Conversions + */ + public interface Generator { + public void addJSON(Appendable buffer); + } + + /** + * Quote a string into an Appendable. + * The characters ", \, \n, \r, \t, \f and \b are escaped + * + * @param buf The Appendable + * @param s The String to quote. + */ + private static void quote(Appendable buf, String s) { + try { + buf.append('"'); + + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '"': + buf.append("\\\""); + continue; + case '\\': + buf.append("\\\\"); + continue; + case '\n': + buf.append("\\n"); + continue; + case '\r': + buf.append("\\r"); + continue; + case '\t': + buf.append("\\t"); + continue; + case '\f': + buf.append("\\f"); + continue; + case '\b': + buf.append("\\b"); + continue; + + default: + if (c < 0x10) { + buf.append("\\u000"); + buf.append(Integer.toString(c, 16)); + } else if (c <= 0x1f) { + buf.append("\\u00"); + buf.append(Integer.toString(c, 16)); + } else + buf.append(c); + } + } + + buf.append('"'); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * @param b An ASCII encoded character 0-9 a-f A-F + * @return The byte value of the character 0-16. + */ + private static byte convertHexDigit(byte b) { + if ((b >= '0') && (b <= '9')) return (byte) (b - '0'); + if ((b >= 'a') && (b <= 'f')) return (byte) (b - 'a' + 10); + if ((b >= 'A') && (b <= 'F')) return (byte) (b - 'A' + 10); + return 0; + } + +} diff --git a/core/src/main/java/com/glines/socketio/util/JdkOverLog4j.java b/core/src/main/java/com/glines/socketio/util/JdkOverLog4j.java new file mode 100644 index 0000000..07f4e53 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/util/JdkOverLog4j.java @@ -0,0 +1,108 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.util; + +/** + * @author Mathieu Carbou (mathieu.carbou@gmail.com) + */ + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Handler; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; + +/** + * @author Mathieu Carbou + */ + +/** + * @author Mathieu Carbou + */ +public final class JdkOverLog4j extends Handler { + + private static final Map LEVELS_JDK_TO_LOG4J = new HashMap() { + private static final long serialVersionUID = 4627902405916164098L; + + { + put(java.util.logging.Level.OFF, Level.OFF); + put(java.util.logging.Level.SEVERE, Level.ERROR); + put(java.util.logging.Level.WARNING, Level.WARN); + put(java.util.logging.Level.INFO, Level.INFO); + put(java.util.logging.Level.CONFIG, Level.INFO); + put(java.util.logging.Level.FINE, Level.DEBUG); + put(java.util.logging.Level.FINER, Level.DEBUG); + put(java.util.logging.Level.FINEST, Level.DEBUG); + put(java.util.logging.Level.ALL, Level.ALL); + } + }; + + private static final Map LEVELS_LOG4J_TO_JDK = new HashMap() { + private static final long serialVersionUID = 1880156903738858787L; + + { + put(Level.OFF, java.util.logging.Level.OFF); + put(Level.FATAL, java.util.logging.Level.SEVERE); + put(Level.ERROR, java.util.logging.Level.SEVERE); + put(Level.WARN, java.util.logging.Level.WARNING); + put(Level.INFO, java.util.logging.Level.INFO); + put(Level.DEBUG, java.util.logging.Level.FINE); + put(Level.TRACE, java.util.logging.Level.FINE); + put(Level.ALL, java.util.logging.Level.ALL); + } + }; + + @Override + public void publish(LogRecord record) { + // normalize levels + Logger log4jLogger = Logger.getLogger(record.getLoggerName()); + Level log4jLevel = log4jLogger.getEffectiveLevel(); + java.util.logging.Logger jdkLogger = java.util.logging.Logger.getLogger(record.getLoggerName()); + java.util.logging.Level expectedJdkLevel = LEVELS_LOG4J_TO_JDK.get(log4jLevel); + if (expectedJdkLevel == null) + throw new AssertionError("Level not supported yet - have a bug !" + log4jLevel); + if (!expectedJdkLevel.equals(jdkLogger.getLevel())) { + jdkLogger.setLevel(expectedJdkLevel); + } + log4jLogger.log(record.getLoggerName(), LEVELS_JDK_TO_LOG4J.get(record.getLevel()), record.getMessage(), record.getThrown()); + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } + + public static void install() { + LogManager.getLogManager().reset(); + LogManager.getLogManager().getLogger("").addHandler(new JdkOverLog4j()); + LogManager.getLogManager().getLogger("").setLevel(java.util.logging.Level.ALL); + } +} diff --git a/core/src/main/java/com/glines/socketio/util/Loader.java b/core/src/main/java/com/glines/socketio/util/Loader.java new file mode 100644 index 0000000..03a63e8 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/util/Loader.java @@ -0,0 +1,39 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.util; + +import java.net.URL; +import java.util.List; + +/** + * @author Mathieu Carbou (mathieu.carbou@gmail.com) + */ +public interface Loader { + Class loadClass(String className); + + URL getResource(String path); + + List getResources(String path); +} \ No newline at end of file diff --git a/core/src/main/java/com/glines/socketio/util/ServiceClassLoader.java b/core/src/main/java/com/glines/socketio/util/ServiceClassLoader.java new file mode 100644 index 0000000..f7065df --- /dev/null +++ b/core/src/main/java/com/glines/socketio/util/ServiceClassLoader.java @@ -0,0 +1,207 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.ServiceConfigurationError; + +/** + * @author Mathieu Carbou + * @param The type of the service to be loaded by this loader + */ +public final class ServiceClassLoader implements Iterable> { + + private static final String PREFIX = "META-INF/services/"; + private final Class service; + private final Loader loader; + private LinkedHashMap> providers = new LinkedHashMap>(); + private LazyIterator lookupIterator; + + public void reload() { + providers.clear(); + lookupIterator = new LazyIterator(service, loader); + } + + private ServiceClassLoader(Class svc, Loader loader) { + this.service = svc; + this.loader = loader; + reload(); + } + + private static void fail(Class service, String msg, Throwable cause) throws ServiceConfigurationError { + throw new ServiceConfigurationError(service.getName() + ": " + msg, cause); + } + + private static void fail(Class service, String msg) throws ServiceConfigurationError { + throw new ServiceConfigurationError(service.getName() + ": " + msg); + } + + private static void fail(Class service, URL u, int line, String msg) throws ServiceConfigurationError { + fail(service, u + ":" + line + ": " + msg); + } + + private int parseLine(Class service, URL u, BufferedReader r, int lc, List names) throws IOException, ServiceConfigurationError { + String ln = r.readLine(); + if (ln == null) return -1; + int ci = ln.indexOf('#'); + if (ci >= 0) ln = ln.substring(0, ci); + ln = ln.trim(); + int n = ln.length(); + if (n != 0) { + if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) + fail(service, u, lc, "Illegal configuration-file syntax"); + int cp = ln.codePointAt(0); + if (!Character.isJavaIdentifierStart(cp)) + fail(service, u, lc, "Illegal provider-class name: " + ln); + for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { + cp = ln.codePointAt(i); + if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) + fail(service, u, lc, "Illegal provider-class name: " + ln); + } + if (!providers.containsKey(ln) && !names.contains(ln)) + names.add(ln); + } + return lc + 1; + } + + private Iterator parse(Class service, URL u) throws ServiceConfigurationError { + InputStream in = null; + BufferedReader r = null; + ArrayList names = new ArrayList(); + try { + in = u.openStream(); + r = new BufferedReader(new InputStreamReader(in, "utf-8")); + int lc = 1; + while ((lc = parseLine(service, u, r, lc, names)) >= 0) ; + } catch (IOException x) { + fail(service, "Error reading configuration file", x); + } finally { + try { + if (r != null) r.close(); + if (in != null) in.close(); + } catch (IOException y) { + fail(service, "Error closing configuration file", y); + } + } + return names.iterator(); + } + + private class LazyIterator implements Iterator> { + final Class service; + final Loader loader; + Iterator configs = null; + Iterator pending = null; + String nextName = null; + + private LazyIterator(Class service, Loader loader) { + this.service = service; + this.loader = loader; + } + + public boolean hasNext() { + if (nextName != null) { + return true; + } + if (configs == null) { + String fullName = PREFIX + service.getName(); + configs = loader.getResources(fullName).iterator(); + } + while ((pending == null) || !pending.hasNext()) { + if (!configs.hasNext()) { + return false; + } + pending = parse(service, configs.next()); + } + nextName = pending.next(); + return true; + } + + @SuppressWarnings({"unchecked"}) + public Class next() { + if (!hasNext()) + throw new NoSuchElementException(); + String cn = nextName; + nextName = null; + try { + Class p = (Class) loader.loadClass(cn); + providers.put(cn, p); + return p; + } catch (RuntimeException x) { + fail(service, + "Provider " + cn + " could not be instantiated: " + x, + x); + } + throw new Error(); // This cannot happen + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + } + + public Iterator> iterator() { + return new Iterator>() { + Iterator>> knownProviders = providers.entrySet().iterator(); + + public boolean hasNext() { + return knownProviders.hasNext() || lookupIterator.hasNext(); + } + + public Class next() { + if (knownProviders.hasNext()) + return knownProviders.next().getValue(); + return lookupIterator.next(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + public static ServiceClassLoader load(Class service, Loader loader) { + return new ServiceClassLoader(service, loader); + } + + public static ServiceClassLoader load(Class service) { + return new ServiceClassLoader(service, new DefaultLoader()); + } + + public String toString() { + return "ServiceClassLoader[" + service.getName() + "]"; + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/glines/socketio/util/URI.java b/core/src/main/java/com/glines/socketio/util/URI.java new file mode 100644 index 0000000..58da700 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/util/URI.java @@ -0,0 +1,122 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.util; + +import java.io.UnsupportedEncodingException; + +public final class URI { + + private URI() { + } + + public static String decodePath(String path) { + return decodePath(path, "UTF-8"); + } + + /* Decode a URI path. + * @param path The path the encode + * @param buf StringBuilder to encode path into + */ + public static String decodePath(String path, String charset) { + if (path == null) + return null; + char[] chars = null; + int n = 0; + byte[] bytes = null; + int b = 0; + + int len = path.length(); + + for (int i = 0; i < len; i++) { + char c = path.charAt(i); + + if (c == '%' && (i + 2) < len) { + if (chars == null) { + chars = new char[len]; + bytes = new byte[len]; + path.getChars(0, i, chars, 0); + } + bytes[b++] = (byte) (0xff & parseInt(path, i + 1, 2, 16)); + i += 2; + continue; + } else if (bytes == null) { + n++; + continue; + } + + if (b > 0) { + String s; + try { + s = new String(bytes, 0, b, charset); + } catch (UnsupportedEncodingException e) { + s = new String(bytes, 0, b); + } + s.getChars(0, s.length(), chars, n); + n += s.length(); + b = 0; + } + + chars[n++] = c; + } + + if (chars == null) + return path; + + if (b > 0) { + String s; + try { + s = new String(bytes, 0, b, charset); + } catch (UnsupportedEncodingException e) { + s = new String(bytes, 0, b); + } + s.getChars(0, s.length(), chars, n); + n += s.length(); + } + + return new String(chars, 0, n); + } + + private static int parseInt(String s, int offset, int length, int base) throws NumberFormatException { + int value = 0; + if (length < 0) + length = s.length() - offset; + for (int i = 0; i < length; i++) { + char c = s.charAt(offset + i); + int digit = c - '0'; + if (digit < 0 || digit >= base || digit >= 10) { + digit = 10 + c - 'A'; + if (digit < 10 || digit >= base) + digit = 10 + c - 'a'; + } + if (digit < 0 || digit >= base) + throw new NumberFormatException(s.substring(offset, offset + length)); + value = value * base + digit; + } + return value; + } +} + + + diff --git a/core/src/main/java/com/glines/socketio/util/Web.java b/core/src/main/java/com/glines/socketio/util/Web.java new file mode 100644 index 0000000..0dcf7a6 --- /dev/null +++ b/core/src/main/java/com/glines/socketio/util/Web.java @@ -0,0 +1,64 @@ +/** + * The MIT License + * Copyright (c) 2010 Tad Glines + * + * Contributors: Ovea.com, Mycila.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.glines.socketio.util; + +import javax.servlet.http.HttpServletRequest; +import java.security.SecureRandom; +import java.util.Random; + +/** + * @author Mathieu Carbou + */ +public final class Web { + + private static final char[] BASE64_ALPHABET ="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray(); + private static Random random = new SecureRandom(); + + private Web() { + } + + public static String extractSessionId(HttpServletRequest request) { + String path = request.getPathInfo(); + if (path != null && path.length() > 0 && !"/".equals(path)) { + if (path.startsWith("/")) path = path.substring(1); + String[] parts = path.split("/"); + if (parts.length >= 3) { + return parts[2] == null || parts[2].length() == 0 || parts[2].equals("null") ? null : parts[2]; + } + } + return null; + } + + public static String generateRandomString(int length) { + StringBuilder result = new StringBuilder(length); + byte[] bytes = new byte[length]; + random.nextBytes(bytes); + for (byte aByte : bytes) { + result.append(BASE64_ALPHABET[aByte & 0x3F]); + } + return result.toString(); + } + +} diff --git a/core/src/main/resources/META-INF/services/com.glines.socketio.server.Transport b/core/src/main/resources/META-INF/services/com.glines.socketio.server.Transport new file mode 100644 index 0000000..aa6896e --- /dev/null +++ b/core/src/main/resources/META-INF/services/com.glines.socketio.server.Transport @@ -0,0 +1,5 @@ +com.glines.socketio.server.transport.FlashSocketTransport +com.glines.socketio.server.transport.HTMLFileTransport +com.glines.socketio.server.transport.XHRMultipartTransport +com.glines.socketio.server.transport.XHRPollingTransport +com.glines.socketio.server.transport.JSONPPollingTransport diff --git a/core/src/main/resources/com/glines/socketio/WebSocketMain.swf b/core/src/main/resources/com/glines/socketio/WebSocketMain.swf index 5eab452..8174466 100644 Binary files a/core/src/main/resources/com/glines/socketio/WebSocketMain.swf and b/core/src/main/resources/com/glines/socketio/WebSocketMain.swf differ diff --git a/core/src/main/resources/com/glines/socketio/socket.io.js b/core/src/main/resources/com/glines/socketio/socket.io.js index ff26812..d01e8da 100644 --- a/core/src/main/resources/com/glines/socketio/socket.io.js +++ b/core/src/main/resources/com/glines/socketio/socket.io.js @@ -1,2184 +1,3871 @@ -/** Socket.IO 0.6 - Built with build.js */ +/*! Socket.IO.js build:0.9.11, development. Copyright(c) 2011 LearnBoost MIT Licensed */ + +var io = ('undefined' === typeof module ? {} : module.exports); +(function() { + /** - * Socket.IO client - * - * @author Guillermo Rauch - * @license The MIT license. - * @copyright Copyright (c) 2010 LearnBoost + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed */ -this.io = { - version: '0.6', - - setPath: function(path){ - if (window.console && console.error) console.error('io.setPath will be removed. Please set the variable WEB_SOCKET_SWF_LOCATION pointing to WebSocketMain.swf'); - this.path = /\/$/.test(path) ? path : path + '/'; - WEB_SOCKET_SWF_LOCATION = path + 'flashsocket/WebSocketMain.swf'; - } -}; +(function (exports, global) { -if ('jQuery' in this) jQuery.io = this.io; + /** + * IO namespace. + * + * @namespace + */ -if (typeof window != 'undefined'){ - //WEB_SOCKET_SWF_LOCATION = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//cdn.socket.io/' + this.io.version + '/WebSocketMain.swf'; - if (typeof WEB_SOCKET_SWF_LOCATION === 'undefined') - WEB_SOCKET_SWF_LOCATION = '/socket.io/flashsocket/WebSocketMain.swf'; -} -/** - * Socket.IO client - * - * @author Guillermo Rauch - * @license The MIT license. - * @copyright Copyright (c) 2010 LearnBoost - */ + var io = exports; -(function(){ - - var _pageLoaded = false; - - io.util = { - - ios: false, - - load: function(fn){ - if (document.readyState == 'complete' || _pageLoaded) return fn(); - if ('attachEvent' in window){ - window.attachEvent('onload', fn); - } else { - window.addEventListener('load', fn, false); - } - }, - - inherit: function(ctor, superCtor){ - // no support for `instanceof` for now - for (var i in superCtor.prototype){ - ctor.prototype[i] = superCtor.prototype[i]; - } - }, - - indexOf: function(arr, item, from){ - for (var l = arr.length, i = (from < 0) ? Math.max(0, l + from) : from || 0; i < l; i++){ - if (arr[i] === item) return i; - } - return -1; - }, - - isArray: function(obj){ - return Object.prototype.toString.call(obj) === '[object Array]'; - }, - - merge: function(target, additional){ - for (var i in additional) - if (additional.hasOwnProperty(i)) - target[i] = additional[i]; - } - - }; - - io.util.ios = /iphone|ipad/i.test(navigator.userAgent); - io.util.android = /android/i.test(navigator.userAgent); - io.util.opera = /opera/i.test(navigator.userAgent); - - io.util.load(function(){ - _pageLoaded = true; - }); + /** + * Socket.IO version + * + * @api public + */ -})(); -/** - * Socket.IO client - * - * @author Guillermo Rauch - * @license The MIT license. - * @copyright Copyright (c) 2010 LearnBoost - */ + io.version = '0.9.11'; -// abstract - -(function(){ - - var Frame = { - // - FRAME_CHAR: '~', - // Control Codes - CLOSE_CODE: 0, - SESSION_ID_CODE: 1, - TIMEOUT_CODE: 2, - PING_CODE: 3, - PONG_CODE: 4, - DATA_CODE: 0xE, - FRAGMENT_CODE: 0xF, - - // Core Message Types - TEXT_MESSAGE_TYPE: 0, - JSON_MESSAGE_TYPE: 1, - - // Methods - encode: function(ftype, mtype, data) { - if (!!mtype) { - return this.FRAME_CHAR + ftype.toString(16) + mtype.toString(16) - + this.FRAME_CHAR + data.length.toString(16) - + this.FRAME_CHAR + data; - } else { - return this.FRAME_CHAR + ftype.toString(16) - + this.FRAME_CHAR + data.length.toString(16) - + this.FRAME_CHAR + data; - } - }, - - decode: function(data) { - var frames = []; - var idx = 0; - var start = 0; - var end = 0; - var ftype = 0; - var mtype = 0; - var size = 0; - - // Parse the data and silently ignore any part that fails to parse properly. - while (data.length > idx && data.charAt(idx) == this.FRAME_CHAR) { - ftype = 0; - mtype = 0; - start = idx + 1; - end = data.indexOf(this.FRAME_CHAR, start); - - if (-1 == end || start == end || - !/[0-9A-Fa-f]+/.test(data.substring(start, end))) { - break; - } - - ftype = parseInt(data.substring(start, start+1), 16); - - if (end-start > 1) { - if (ftype == this.DATA_CODE || ftype == this.FRAGEMENT_CODE) { - mtype = parseInt(data.substring(start+1, end), 16); - } else { - break; - } - } - - start = end + 1; - end = data.indexOf(this.FRAME_CHAR, start); - - if (-1 == end || start == end || - !/[0-9A-Fa-f]+/.test(data.substring(start, end))) { - break; - } - - var size = parseInt(data.substring(start, end), 16); - - start = end + 1; - end = start + size; - - if (data.length < end) { - break; - } - - frames.push({ftype: ftype, mtype: mtype, data: data.substring(start, end)}); - idx = end; - } - return frames; - } - }; - - Transport = io.Transport = function(base, options){ - this.base = base; - this.options = { - timeout: 15000 // based on heartbeat interval default - }; - io.util.merge(this.options, options); - this.message_id = 0; - }; - - Transport.prototype.send = function(mtype, data){ - this.message_id++; - this.rawsend(Frame.encode(Frame.DATA_CODE, mtype, data)); - }; - - Transport.prototype.rawsend = function(){ - throw new Error('Missing send() implementation'); - }; - - Transport.prototype._destroy = function(){ - throw new Error('Missing _destroy() implementation'); - }; - - Transport.prototype.connect = function(){ - throw new Error('Missing connect() implementation'); - }; - - Transport.prototype.disconnect = function(){ - throw new Error('Missing disconnect() implementation'); - }; - - Transport.prototype.close = function() { - this.close_id = 'client'; - this.rawsend(Frame.encode(Frame.CLOSE_CODE, null, this.close_id)); - }; - - Transport.prototype._onData = function(data){ - this._setTimeout(); - var msgs = Frame.decode(data); - if (msgs && msgs.length){ - for (var i = 0, l = msgs.length; i < l; i++){ - this._onMessage(msgs[i]); - } - } - }; - - Transport.prototype._setTimeout = function(){ - var self = this; - if (this._timeout) clearTimeout(this._timeout); - this._timeout = setTimeout(function(){ - self._onTimeout(); - }, this.options.timeout); - }; - - Transport.prototype._onTimeout = function(){ - this._timedout = true; - if (!!this._interval) { - clearInterval(this._interval); - this._interval = null; - } - this.disconnect(); - }; - - Transport.prototype._onMessage = function(message){ - if (!this.sessionid){ - if (message.ftype == Frame.SESSION_ID_CODE) { - this.sessionid = message.data; - this._onConnect(); - } else { - this._onDisconnect(this.base.DR_ERROR, "First frame wasn't the sesion ID!"); - } - } else if (message.ftype == Frame.TIMEOUT_CODE) { - hg_interval = Number(message.data); - if (message.data == hg_interval) { - this.options.timeout = hg_interval*2; // Set timeout to twice the new heartbeat interval - this._setTimeout(); - } - } else if (message.ftype == Frame.CLOSE_CODE) { - this._onCloseFrame(message.data); - } else if (message.ftype == Frame.PING_CODE) { - this._onPingFrame(message.data); - } else if (message.ftype == Frame.DATA_CODE) { - this.base._onMessage(message.mtype, message.data); - } else { - // For now we'll ignore other frame types. - } - }, - - Transport.prototype._onPingFrame = function(data){ - this.rawsend(Frame.encode(Frame.PONG_CODE, null, data)); - }; - - Transport.prototype._onConnect = function(){ - this.base._onConnect(); - this._setTimeout(); - }; - - Transport.prototype._onCloseFrame = function(data){ - if (this.base.socketState == this.base.CLOSING) { - if (!!this.close_id && this.close_id == data) { - this.base.socketState = this.base.CLOSED; - this.disconnect(); - } else { - /* - * It's possible the server initiated a close at the same time we did and our - * initial close messages passed each other like ships in the night. So, to be nice - * we'll send an acknowledge of the server's close message. - */ - this.rawsend(Frame.encode(Frame.CLOSE_CODE, null, data)); - } - } else { - this.base.socketState = this.base.CLOSING; - this.disconnectWhenEmpty = true; - this.rawsend(Frame.encode(Frame.CLOSE_CODE, null, data)); - } - }; - - Transport.prototype._onDisconnect = function(reason, error){ - if (this._timeout) clearTimeout(this._timeout); - this.sessionid = null; - this.disconnectWhenEmpty = false; - if (this._timedout) { - reason = this.base.DR_TIMEOUT; - error = null; - } - this.base._onDisconnect(reason, error); - }; - - Transport.prototype._prepareUrl = function(){ - return (this.base.options.secure ? 'https' : 'http') - + '://' + this.base.host - + ':' + this.base.options.port - + '/' + this.base.options.resource - + '/' + this.type - + (this.sessionid ? ('/' + this.sessionid) : '/'); - }; + /** + * Protocol implemented. + * + * @api public + */ -})(); -/** - * Socket.IO client - * - * @author Guillermo Rauch - * @license The MIT license. - * @copyright Copyright (c) 2010 LearnBoost - */ + io.protocol = 1; -(function(){ - - var empty = new Function, - - XMLHttpRequestCORS = (function(){ - if (!('XMLHttpRequest' in window)) return false; - // CORS feature detection - var a = new XMLHttpRequest(); - return a.withCredentials != undefined; - })(), - - request = function(xdomain){ - if ('XDomainRequest' in window && xdomain) return new XDomainRequest(); - if ('XMLHttpRequest' in window && (!xdomain || XMLHttpRequestCORS)) return new XMLHttpRequest(); - if (!xdomain){ - try { - var a = new ActiveXObject('MSXML2.XMLHTTP'); - return a; - } catch(e){} - - try { - var b = new ActiveXObject('Microsoft.XMLHTTP'); - return b; - } catch(e){} - } - return false; - }, - - XHR = io.Transport.XHR = function(){ - io.Transport.apply(this, arguments); - this._sendBuffer = []; - }; - - io.util.inherit(XHR, io.Transport); - - XHR.prototype.connect = function(){ - this._get(); - return this; - }; - - XHR.prototype._checkSend = function(){ - if (!this._posting && this._sendBuffer.length){ - var data = ''; - for (var i = 0, l = this._sendBuffer.length; i < l; i++){ - data += this._sendBuffer[i]; - } - this._sendBuffer = []; - this._send(data); - } else if (!!this.disconnectWhenEmpty) { - this.disconnect(); - } - }; - - XHR.prototype.rawsend = function(data){ - if (io.util.isArray(data)){ - this._sendBuffer.push.apply(this._sendBuffer, data); - } else { - this._sendBuffer.push(data); - } - this._checkSend(); - return this; - }; - - XHR.prototype._send = function(data){ - var self = this; - this._posting = true; - this._sendXhr = this._request('send', 'POST'); - this._sendXhr.onreadystatechange = function(){ - var status; - if (self._sendXhr.readyState == 4){ - self._sendXhr.onreadystatechange = empty; - try { status = self._sendXhr.status; } catch(e){} - self._posting = false; - if (status == 200){ - self._checkSend(); - } else { - self._onDisconnect(self.base.DR_ERROR, "POST failed!"); - } - } - }; - this._sendXhr.send(data); - }; - - XHR.prototype.disconnect = function(){ - // send disconnection signal - this._onDisconnect(); - return this; - }; - - XHR.prototype._destroy = function(){ - if (this._xhr){ - this._xhr.onreadystatechange = this._xhr.onload = empty; - this._xhr.abort(); - this._xhr = null; - } - if (this._sendXhr){ - this._sendXhr.onreadystatechange = this._sendXhr.onload = empty; - this._sendXhr.abort(); - this._posting = false; - this._sendXhr = null; - } - this._sendBuffer = []; - }; - - XHR.prototype._onDisconnect = function(reason, error){ - this._destroy(); - io.Transport.prototype._onDisconnect.call(this, reason, error); - }; - - XHR.prototype._request = function(url, method, multipart){ - var req = request(this.base._isXDomain()); - if (multipart) req.multipart = true; - req.open(method || 'GET', this._prepareUrl() + (url ? '/' + url : '')); - if (method == 'POST' && 'setRequestHeader' in req){ - req.setRequestHeader('Content-type', 'text/plain; charset=utf-8'); - } - return req; - }; - - XHR.check = function(xdomain){ - try { - if (request(xdomain)) return true; - } catch(e){} - return false; - }; - - XHR.xdomainCheck = function(){ - return XHR.check(true); - }; - - XHR.request = request; - -})(); -/** - * Socket.IO client - * - * @author Guillermo Rauch - * @license The MIT license. - * @copyright Copyright (c) 2010 LearnBoost - */ + /** + * Available transports, these will be populated with the available transports + * + * @api public + */ -(function(){ - - var WS = io.Transport.websocket = function(){ - io.Transport.apply(this, arguments); - }; - - io.util.inherit(WS, io.Transport); - - WS.prototype.type = 'websocket'; - - WS.prototype.connect = function(){ - var self = this; - this.socket = new WebSocket(this._prepareUrl()); - this.socket.onmessage = function(ev){ self._onData(ev.data); }; - this.socket.onopen = function(ev){ self._onOpen(); }; - this.socket.onclose = function(ev){ self._onClose(); }; - return this; - }; - - WS.prototype.rawsend = function(data){ - if (this.socket) this.socket.send(data); - - /* - * This rigmarole is required because the WebSockets specification doesn't say what happens - * to buffered data when close() is called. It cannot be assumed that buffered data is - * transmitted before the connection is close. - */ - if (!!this.disconnectWhenEmpty && !this._interval) { - var self = this; - self._interval = setInterval(function() { - if (self.socket.bufferedAmount == 0) { - self.disconnect(); - clearInterval(self._interval); - } else if (!self.disconnectWhenEmpty || - self.socket.readyState == self.socket.CLOSED) { - clearInterval(self._interval); - } - }, 50); - } - return this; - }; - - WS.prototype.disconnect = function(){ - this.disconnectCalled = true; - if (this.socket) this.socket.close(); - return this; - }; - - WS.prototype._destroy = function(){ - if (this.socket) { - this.socket.onclose = null; - this.socket.onopen = null; - this.socket.onmessage = null; - this.socket.close(); - this.socket = null; - } - return this; - }; - - WS.prototype._onOpen = function(){ - // This is needed because the 7.1.6 version of jetty's WebSocket fails if messages are - // sent from inside WebSocket.onConnect() method. - this.socket.send('OPEN'); - return this; - }; - - WS.prototype._onClose = function(){ - if (!!this.disconnectCalled || this.base.socketState == this.base.CLOSED) { - this.disconnectCalled = false; - this._onDisconnect(); - } else { - this._onDisconnect(this.base.DR_ERROR, "WebSocket closed unexpectedly"); - } - return this; - }; - - WS.prototype._prepareUrl = function(){ - return (this.base.options.secure ? 'wss' : 'ws') - + '://' + this.base.host - + ':' + this.base.options.port - + '/' + this.base.options.resource - + '/' + this.type - + (this.sessionid ? ('/' + this.sessionid) : ''); - }; - - WS.check = function(){ - // we make sure WebSocket is not confounded with a previously loaded flash WebSocket - return 'WebSocket' in window && WebSocket.prototype && ( WebSocket.prototype.send && !!WebSocket.prototype.send.toString().match(/native/i)) && typeof WebSocket !== "undefined"; - }; - - WS.xdomainCheck = function(){ - return true; - }; - -})(); -/** - * Socket.IO client - * - * @author Guillermo Rauch - * @license The MIT license. - * @copyright Copyright (c) 2010 LearnBoost - */ + io.transports = []; -(function(){ - - var Flashsocket = io.Transport.flashsocket = function(){ - io.Transport.websocket.apply(this, arguments); - }; - - io.util.inherit(Flashsocket, io.Transport.websocket); - - Flashsocket.prototype.type = 'flashsocket'; - - Flashsocket.prototype.connect = function(){ - var self = this, args = arguments; - WebSocket.__addTask(function(){ - io.Transport.websocket.prototype.connect.apply(self, args); - }); - return this; - }; - - Flashsocket.prototype.send = function(){ - var self = this, args = arguments; - WebSocket.__addTask(function(){ - io.Transport.websocket.prototype.send.apply(self, args); - }); - return this; - }; - - Flashsocket.check = function(){ - if (typeof WebSocket == 'undefined' || !('__addTask' in WebSocket)) return false; - if (io.util.opera) return false; // opera is buggy with this transport - if ('navigator' in window && 'plugins' in navigator && navigator.plugins['Shockwave Flash']){ - return !!navigator.plugins['Shockwave Flash'].description; - } - if ('ActiveXObject' in window) { - try { - return !!new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); - } catch (e) {} - } - return false; - }; - - Flashsocket.xdomainCheck = function(){ - return true; - }; - -})(); -/** - * Socket.IO client - * - * @author Guillermo Rauch - * @license The MIT license. - * @copyright Copyright (c) 2010 LearnBoost - */ + /** + * Keep track of jsonp callbacks. + * + * @api private + */ -(function(){ - - var HTMLFile = io.Transport.htmlfile = function(){ - io.Transport.XHR.apply(this, arguments); - }; - - io.util.inherit(HTMLFile, io.Transport.XHR); - - HTMLFile.prototype.type = 'htmlfile'; - - HTMLFile.prototype._get = function(){ - var self = this; - this._open(); - window.attachEvent('onunload', function(){ self._destroy(); }); - }; - - HTMLFile.prototype._open = function(){ - this._doc = new ActiveXObject('htmlfile'); - this._doc.open(); - this._doc.write(''); - this._doc.parentWindow.s = this; - this._doc.close(); - - var _iframeC = this._doc.createElement('div'); - this._doc.body.appendChild(_iframeC); - this._iframe = this._doc.createElement('iframe'); - _iframeC.appendChild(this._iframe); - this._iframe.src = this._prepareUrl() + '/' + (+ new Date); - }; - - HTMLFile.prototype._ = function(data, doc){ - this._onData(data); - var script = doc.getElementsByTagName('script')[0]; - script.parentNode.removeChild(script); - }; - - HTMLFile.prototype._destroy = function(){ - if (this._iframe){ - this._iframe.src = 'about:blank'; - this._doc = null; - CollectGarbage(); - } - }; - - HTMLFile.prototype.disconnect = function(){ - this._destroy(); - return io.Transport.XHR.prototype.disconnect.call(this); - }; - - HTMLFile.check = function(){ - if ('ActiveXObject' in window){ - try { - var a = new ActiveXObject('htmlfile'); - return a && io.Transport.XHR.check(); - } catch(e){} - } - return false; - }; - - HTMLFile.xdomainCheck = function(){ - // we can probably do handling for sub-domains, we should test that it's cross domain but a subdomain here - return false; - }; - -})(); -/** - * Socket.IO client - * - * @author Guillermo Rauch - * @license The MIT license. - * @copyright Copyright (c) 2010 LearnBoost - */ + io.j = []; -(function(){ - - var XHRMultipart = io.Transport['xhr-multipart'] = function(){ - io.Transport.XHR.apply(this, arguments); - }; - - io.util.inherit(XHRMultipart, io.Transport.XHR); - - XHRMultipart.prototype.type = 'xhr-multipart'; - - XHRMultipart.prototype._get = function(){ - var self = this; - var lastReadyState = 4; - this._xhr = this._request('', 'GET', true); - this._xhr.onreadystatechange = function(){ - // Normally the readyState will progress from 1-4 (e.g. 1,2,3,4) for a normal part. - // But on disconnect, the readyState will go from 1 to 4 skipping 2 and 3. - // Thanks to Wilfred Nilsen (http://www.mail-archive.com/mozilla-xpcom@mozilla.org/msg04845.html) for discovering this. - // So, if the readyState skips a step and equals 4, then the connection has dropped. - if (self._xhr.readyState - lastReadyState > 1 && self._xhr.readyState == 4) { - self._onDisconnect(self.base.DR_ERROR, "XHR Connection dropped unexpectedly"); - } else { - lastReadyState = self._xhr.readyState; - if (self._xhr.readyState == 3) { - self._onData(self._xhr.responseText); - } - } - }; - this._xhr.send(); - }; - - XHRMultipart.check = function(){ - return 'XMLHttpRequest' in window && 'prototype' in XMLHttpRequest && 'multipart' in XMLHttpRequest.prototype; - }; - - XHRMultipart.xdomainCheck = function(){ - return true; - }; - -})(); -/** - * Socket.IO client - * - * @author Guillermo Rauch - * @license The MIT license. - * @copyright Copyright (c) 2010 LearnBoost - */ + /** + * Keep track of our io.Sockets + * + * @api private + */ + io.sockets = {}; -(function(){ - - var empty = new Function(), - - XHRPolling = io.Transport['xhr-polling'] = function(){ - io.Transport.XHR.apply(this, arguments); - }; - - io.util.inherit(XHRPolling, io.Transport.XHR); - - XHRPolling.prototype.type = 'xhr-polling'; - - XHRPolling.prototype.connect = function(){ - if (io.util.ios || io.util.android){ - var self = this; - io.util.load(function(){ - setTimeout(function(){ - io.Transport.XHR.prototype.connect.call(self); - }, 10); - }); - } else { - io.Transport.XHR.prototype.connect.call(this); - } - }; - - XHRPolling.prototype._get = function(){ - var self = this; - this._xhr = this._request(+ new Date, 'GET'); - if ('onload' in this._xhr){ - this._xhr.onload = function(){ - var status; - try { status = self._xhr.status; } catch(e){} - if (status == 200){ - self._onData(this.responseText); - self._get(); - } else { - self._onDisconnect(); - } - }; - this._xhr.onerror = function(){ - self._onDisconnect(); - }; - } else { - this._xhr.onreadystatechange = function(){ - var status; - if (self._xhr.readyState == 4){ - self._xhr.onreadystatechange = empty; - try { status = self._xhr.status; } catch(e){} - if (status == 200){ - self._onData(self._xhr.responseText); - self._get(); - } else { - self._onDisconnect(); - } - } - }; - } - this._xhr.send(); - }; - - XHRPolling.check = function(){ - return io.Transport.XHR.check(); - }; - - XHRPolling.xdomainCheck = function(){ - return io.Transport.XHR.xdomainCheck(); - }; -})(); -/** - * Socket.IO client - * - * @author Guillermo Rauch - * @license The MIT license. - * @copyright Copyright (c) 2010 LearnBoost - */ + /** + * Manages connections to hosts. + * + * @param {String} uri + * @Param {Boolean} force creation of new socket (defaults to false) + * @api public + */ -io.JSONP = []; + io.connect = function (host, details) { + var uri = io.util.parseUri(host) + , uuri + , socket; -JSONPPolling = io.Transport['jsonp-polling'] = function(){ - io.Transport.XHR.apply(this, arguments); - this._insertAt = document.getElementsByTagName('script')[0]; - this._index = io.JSONP.length; - io.JSONP.push(this); -}; + if (global && global.location) { + uri.protocol = uri.protocol || global.location.protocol.slice(0, -1); + uri.host = uri.host || (global.document + ? global.document.domain : global.location.hostname); + uri.port = uri.port || global.location.port; + } -io.util.inherit(JSONPPolling, io.Transport['xhr-polling']); - -JSONPPolling.prototype.type = 'jsonp-polling'; - -JSONPPolling.prototype._send = function(data){ - var self = this; - if (!('_form' in this)){ - var form = document.createElement('FORM'), - area = document.createElement('TEXTAREA'), - id = this._iframeId = 'socket_io_iframe_' + this._index, - iframe; - - form.style.position = 'absolute'; - form.style.top = '-1000px'; - form.style.left = '-1000px'; - form.target = id; - form.method = 'POST'; - form.action = this._prepareUrl() + '/' + (+new Date) + '/' + this._index; - area.name = 'data'; - form.appendChild(area); - this._insertAt.parentNode.insertBefore(form, this._insertAt); - document.body.appendChild(form); - - this._form = form; - this._area = area; - } - - function complete(){ - initIframe(); - self._posting = false; - self._checkSend(); - }; - - function initIframe(){ - if (self._iframe){ - self._form.removeChild(self._iframe); - } - - try { - // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) - iframe = document.createElement('