Author: remy.maucherat(a)jboss.com
Date: 2013-09-20 12:27:05 -0400 (Fri, 20 Sep 2013)
New Revision: 2261
Added:
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/AbstractServletInputStream.java
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/AbstractServletOutputStream.java
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/HttpUpgradeHandler.java
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/ReadListener.java
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/WebConnection.java
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/WriteListener.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/ExceptionUtils.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/buf/Utf8Encoder.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/BinaryDecoder.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/BinaryEncoder.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/Decoder.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/DecoderException.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/Encoder.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/EncoderException.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/Base64.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/BaseNCodec.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/StringUtils.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/package.html
branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/
branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/Constants.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/TaskQueue.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/TaskThread.java
branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/BackgroundProcess.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/BackgroundProcessManager.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/Constants.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/DecoderEntry.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/FutureToSendHandler.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/MessageHandlerResult.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/MessageHandlerResultType.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/ReadBufferOverflowException.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/SendHandlerToCompletionHandler.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/Util.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WrappedMessageHandler.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsContainerProvider.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsFrameBase.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsFrameClient.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsHandshakeResponse.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsIOException.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsPongMessage.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointAsync.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointBase.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointBasic.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointImplClient.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsSession.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/Constants.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointClient.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointServer.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerBase.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBase.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBinary.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialText.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBase.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBinary.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholePong.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoPathParam.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/package-info.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/Constants.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/DefaultServerEndpointConfigurator.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/UpgradeUtil.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/UriTemplate.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsContextListener.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsFilter.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsFrameServer.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsHandshakeRequest.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsMappingResult.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsRemoteEndpointImplServer.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsSci.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsServerContainer.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsSessionListener.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsWriteTimeout.java
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/package-info.java
branches/7.4.x/src/main/java/org/jboss/web/WebsocketsLogger.java
branches/7.4.x/src/main/java/org/jboss/web/WebsocketsMessages.java
Removed:
branches/7.4.x/src/main/java/org/apache/tomcat/util/buf/UTF8Decoder.java
Modified:
branches/7.4.x/src/main/java/org/apache/catalina/connector/Connector.java
branches/7.4.x/src/main/java/org/apache/catalina/connector/CoyoteInputStream.java
branches/7.4.x/src/main/java/org/apache/catalina/connector/CoyoteOutputStream.java
branches/7.4.x/src/main/java/org/apache/catalina/connector/HttpEventImpl.java
branches/7.4.x/src/main/java/org/apache/catalina/connector/InputBuffer.java
branches/7.4.x/src/main/java/org/apache/catalina/connector/OutputBuffer.java
branches/7.4.x/src/main/java/org/apache/catalina/connector/Request.java
branches/7.4.x/src/main/java/org/apache/catalina/connector/RequestFacade.java
branches/7.4.x/src/main/java/org/apache/catalina/connector/Response.java
branches/7.4.x/src/main/java/org/apache/catalina/connector/ResponseFacade.java
branches/7.4.x/src/main/java/org/apache/catalina/core/StandardWrapperValve.java
branches/7.4.x/src/main/java/org/apache/coyote/ActionCode.java
branches/7.4.x/src/main/java/org/apache/coyote/http11/Http11AprProcessor.java
branches/7.4.x/src/main/java/org/apache/coyote/http11/Http11NioProcessor.java
branches/7.4.x/src/main/java/org/jboss/web/CatalinaMessages.java
branches/7.4.x/src/main/java/org/jboss/web/CoyoteMessages.java
Log:
- Add Servlet 3.1 IO API from Tomcat 7.
- Port Websockets 1.0 from Tomcat 7.
- No Websockets 1.0 testing yet (but no apparent regression for the usual Servlet
functionality).
Modified: branches/7.4.x/src/main/java/org/apache/catalina/connector/Connector.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/catalina/connector/Connector.java 2013-09-18
14:42:28 UTC (rev 2260)
+++ branches/7.4.x/src/main/java/org/apache/catalina/connector/Connector.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -588,7 +588,7 @@
}
} else {
if ("HTTP/1.1".equals(protocol) ||
"http".equals(protocol)) {
- /*try {
+ try {
Class.forName("java.nio.channels.CompletionHandler");
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11NioProtocol");
@@ -597,9 +597,7 @@
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11Protocol");
CatalinaLogger.CONNECTOR_LOGGER.usingJavaIoConnector();
- }*/
- setProtocolHandlerClassName
- ("org.apache.coyote.http11.Http11Protocol");
+ }
} else if ("AJP/1.3".equals(protocol) ||
"ajp".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.ajp.AjpProtocol");
Modified:
branches/7.4.x/src/main/java/org/apache/catalina/connector/CoyoteInputStream.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/catalina/connector/CoyoteInputStream.java 2013-09-18
14:42:28 UTC (rev 2260)
+++
branches/7.4.x/src/main/java/org/apache/catalina/connector/CoyoteInputStream.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -23,9 +23,9 @@
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
-import javax.servlet.ServletInputStream;
-
import org.apache.catalina.security.SecurityUtil;
+import org.apache.coyote.http11.upgrade.AbstractServletInputStream;
+import org.apache.coyote.http11.upgrade.servlet31.ReadListener;
/**
* This class handles reading bytes.
@@ -34,7 +34,7 @@
* @author Jean-Francois Arcand
*/
public class CoyoteInputStream
- extends ServletInputStream {
+ extends AbstractServletInputStream {
// ----------------------------------------------------- Instance Variables
@@ -231,4 +231,18 @@
}
}
+ public boolean isFinished() {
+ return ib.isEof();
+ }
+
+ public boolean isReady() {
+ return (ib.available() > 0);
+ }
+
+
+ @Override
+ public void setReadListener(ReadListener readListener) {
+ ib.setReadListener(readListener);
+ }
+
}
Modified:
branches/7.4.x/src/main/java/org/apache/catalina/connector/CoyoteOutputStream.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/catalina/connector/CoyoteOutputStream.java 2013-09-18
14:42:28 UTC (rev 2260)
+++
branches/7.4.x/src/main/java/org/apache/catalina/connector/CoyoteOutputStream.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -20,7 +20,8 @@
import java.io.IOException;
-import javax.servlet.ServletOutputStream;
+import org.apache.coyote.http11.upgrade.AbstractServletOutputStream;
+import org.apache.coyote.http11.upgrade.servlet31.WriteListener;
/**
* Coyote implementation of the servlet output stream.
@@ -29,7 +30,7 @@
* @author Remy Maucherat
*/
public class CoyoteOutputStream
- extends ServletOutputStream {
+ extends AbstractServletOutputStream {
// ----------------------------------------------------- Instance Variables
@@ -105,5 +106,14 @@
}
+ public boolean isReady() {
+ return (ob.lastWrite() > 0);
+ }
+
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ ob.setWriteListener(writeListener);
+ }
+
}
-
Modified: branches/7.4.x/src/main/java/org/apache/catalina/connector/HttpEventImpl.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/catalina/connector/HttpEventImpl.java 2013-09-18
14:42:28 UTC (rev 2260)
+++
branches/7.4.x/src/main/java/org/apache/catalina/connector/HttpEventImpl.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -23,9 +23,12 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.apache.coyote.http11.upgrade.AbstractServletInputStream;
+import org.apache.coyote.http11.upgrade.AbstractServletOutputStream;
+import org.apache.coyote.http11.upgrade.servlet31.WebConnection;
import org.jboss.servlet.http.HttpEvent;
-public class HttpEventImpl implements HttpEvent {
+public class HttpEventImpl implements HttpEvent, WebConnection {
public HttpEventImpl(Request request, Response response) {
this.request = request;
@@ -105,6 +108,14 @@
request.suspend();
}
+ public AbstractServletInputStream getInputStream() throws IOException {
+ return (AbstractServletInputStream) request.getInputStream();
+ }
+
+ public AbstractServletOutputStream getOutputStream() throws IOException {
+ return (AbstractServletOutputStream) response.getOutputStream();
+ }
+
public String toString() {
StringBuilder buf = new StringBuilder("HttpEventImpl[");
buf.append(super.toString());
Modified: branches/7.4.x/src/main/java/org/apache/catalina/connector/InputBuffer.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/catalina/connector/InputBuffer.java 2013-09-18
14:42:28 UTC (rev 2260)
+++ branches/7.4.x/src/main/java/org/apache/catalina/connector/InputBuffer.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -30,6 +30,7 @@
import org.apache.catalina.security.SecurityUtil;
import org.apache.coyote.ActionCode;
import org.apache.coyote.Request;
+import org.apache.coyote.http11.upgrade.servlet31.ReadListener;
import org.apache.tomcat.util.buf.B2CConverter;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.CharChunk;
@@ -143,6 +144,12 @@
private int size = -1;
+ /**
+ * Read listener.
+ */
+ private ReadListener readListener = null;
+
+
// ----------------------------------------------------------- Constructors
@@ -229,7 +236,8 @@
gotEnc = false;
enc = null;
-
+ readListener = null;
+
}
@@ -575,4 +583,22 @@
}
+ public ReadListener getReadListener() {
+ return readListener;
+ }
+
+ public void setReadListener(ReadListener readListener) {
+ if (this.readListener != null) {
+ throw MESSAGES.readListenerAlreadySet();
+ }
+ if (readListener == null) {
+ throw MESSAGES.nullListener();
+ }
+ if (!request.isEventMode()) {
+ throw MESSAGES.cannotSetListenerWithoutUpgradeOrAsync();
+ }
+ this.readListener = readListener;
+ coyoteRequest.action(ActionCode.ACTION_EVENT_READ_BEGIN, null);
+ }
+
}
Modified: branches/7.4.x/src/main/java/org/apache/catalina/connector/OutputBuffer.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/catalina/connector/OutputBuffer.java 2013-09-18
14:42:28 UTC (rev 2260)
+++
branches/7.4.x/src/main/java/org/apache/catalina/connector/OutputBuffer.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -18,6 +18,8 @@
package org.apache.catalina.connector;
+import static org.jboss.web.CatalinaMessages.MESSAGES;
+
import java.io.IOException;
import java.io.Writer;
import java.security.AccessController;
@@ -28,6 +30,7 @@
import org.apache.coyote.ActionCode;
import org.apache.coyote.Response;
+import org.apache.coyote.http11.upgrade.servlet31.WriteListener;
import org.apache.catalina.Globals;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.C2BConverter;
@@ -143,20 +146,32 @@
/**
+ * Associated request.
+ */
+ private org.apache.catalina.connector.Response response;
+
+
+ /**
* Suspended flag. All output bytes will be swallowed if this is true.
*/
private boolean suspended = false;
+ /**
+ * Write listener.
+ */
+ private WriteListener writeListener = null;
+
+
// ----------------------------------------------------------- Constructors
/**
* Default constructor. Allocate the buffer with the default buffer size.
*/
- public OutputBuffer() {
+ public OutputBuffer(org.apache.catalina.connector.Response response) {
- this(DEFAULT_BUFFER_SIZE);
+ this(response, DEFAULT_BUFFER_SIZE);
}
@@ -166,8 +181,9 @@
*
* @param size Buffer size to use
*/
- public OutputBuffer(int size) {
+ public OutputBuffer(org.apache.catalina.connector.Response response, int size) {
+ this.response = response;
bb = new ByteChunk(size);
bb.setLimit(size);
bb.setByteOutputChannel(this);
@@ -257,6 +273,7 @@
gotEnc = false;
enc = null;
+ writeListener = null;
}
@@ -662,5 +679,22 @@
return bb.getLimit();
}
+ public WriteListener getWriteListener() {
+ return writeListener;
+ }
+ public void setWriteListener(WriteListener writeListener) {
+ if (this.writeListener != null) {
+ throw MESSAGES.writeListenerAlreadySet();
+ }
+ if (writeListener == null) {
+ throw MESSAGES.nullListener();
+ }
+ if (!response.getRequest().isEventMode()) {
+ throw MESSAGES.cannotSetListenerWithoutUpgradeOrAsync();
+ }
+ this.writeListener = writeListener;
+ coyoteResponse.action(ActionCode.ACTION_EVENT_WRITE_BEGIN, null);
+ }
+
}
Modified: branches/7.4.x/src/main/java/org/apache/catalina/connector/Request.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/catalina/connector/Request.java 2013-09-18
14:42:28 UTC (rev 2260)
+++ branches/7.4.x/src/main/java/org/apache/catalina/connector/Request.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -83,6 +83,8 @@
import org.apache.catalina.util.ParameterMap;
import org.apache.catalina.util.StringParser;
import org.apache.coyote.ActionCode;
+import org.apache.coyote.http11.upgrade.servlet31.HttpUpgradeHandler;
+import org.apache.coyote.http11.upgrade.servlet31.ReadListener;
import org.apache.tomcat.util.buf.B2CConverter;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.EncodingToCharset;
@@ -487,6 +489,12 @@
protected LinkedList<AsyncListener> asyncListenerInstances = new
LinkedList<AsyncListener>();
+ /**
+ * Upgrade handler.
+ */
+ protected HttpUpgradeHandler upgradeHandler = null;
+
+
// --------------------------------------------------------- Public Methods
@@ -518,7 +526,8 @@
event.clear();
event = null;
}
-
+ upgradeHandler = null;
+
sslAttributes = false;
asyncContext = null;
asyncTimeout = -1;
@@ -3295,6 +3304,38 @@
}
+ public long getContentLengthLong() {
+ return (coyoteRequest.getContentLengthLong());
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T extends HttpUpgradeHandler> T upgrade(Class<T>
upgradeHandlerClass)
+ throws IOException {
+ T ugradeHandler = null;
+ Throwable upgradeError = null;
+ try {
+ ugradeHandler = (T)
context.getInstanceManager().newInstance(upgradeHandlerClass);
+ } catch (Throwable t) {
+ upgradeError = t;
+ }
+ if (ugradeHandler == null) {
+ throw new IOException(MESSAGES.upgradeError(), upgradeError);
+ }
+ response.sendUpgrade();
+ eventMode = true;
+ ugradeHandler.init(getEvent());
+ this.upgradeHandler = ugradeHandler;
+ return ugradeHandler;
+ }
+
+ public HttpUpgradeHandler getUpgradeHandler() {
+ return upgradeHandler;
+ }
+
+ public ReadListener getReadListener() {
+ return inputBuffer.getReadListener();
+ }
+
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("Current Servlet stack for thread
").append(Thread.currentThread().getName());
Modified: branches/7.4.x/src/main/java/org/apache/catalina/connector/RequestFacade.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/catalina/connector/RequestFacade.java 2013-09-18
14:42:28 UTC (rev 2260)
+++
branches/7.4.x/src/main/java/org/apache/catalina/connector/RequestFacade.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -46,6 +46,7 @@
import org.apache.catalina.Globals;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.security.SecurityUtil;
+import org.apache.coyote.http11.upgrade.servlet31.HttpUpgradeHandler;
/**
* Facade class that wraps a Coyote request object.
@@ -1044,4 +1045,21 @@
return request.hasSendfile();
}
+ public long getContentLengthLong() {
+ if (request == null) {
+ throw MESSAGES.nullRequestFacade();
+ }
+
+ return request.getContentLengthLong();
+ }
+
+ public <T extends HttpUpgradeHandler> T upgrade(Class<T> upgradeHandler)
+ throws IOException {
+ if (request == null) {
+ throw MESSAGES.nullRequestFacade();
+ }
+
+ return request.upgrade(upgradeHandler);
+ }
+
}
Modified: branches/7.4.x/src/main/java/org/apache/catalina/connector/Response.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/catalina/connector/Response.java 2013-09-18
14:42:28 UTC (rev 2260)
+++ branches/7.4.x/src/main/java/org/apache/catalina/connector/Response.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -50,6 +50,7 @@
import org.apache.catalina.util.CharsetMapper;
import org.apache.catalina.util.DateTool;
import org.apache.coyote.ActionCode;
+import org.apache.coyote.http11.upgrade.servlet31.WriteListener;
import org.apache.naming.resources.CacheEntry;
import org.apache.naming.resources.ProxyDirContext;
import org.apache.tomcat.util.buf.CharChunk;
@@ -125,9 +126,9 @@
this.connector = connector;
if("AJP/1.3".equals(connector.getProtocol())) {
// default size to size of one ajp-packet
- outputBuffer = new OutputBuffer(8184);
+ outputBuffer = new OutputBuffer(this, 8184);
} else {
- outputBuffer = new OutputBuffer();
+ outputBuffer = new OutputBuffer(this);
}
outputStream = new CoyoteOutputStream(outputBuffer);
writer = new CoyoteWriter(outputBuffer);
@@ -1339,9 +1340,6 @@
if (!connector.hasIoEvents())
throw MESSAGES.cannotUpgradeWithoutEvents();
- if (!request.isEventMode() || request.getAsyncContext() != null)
- throw MESSAGES.cannotUpgradeWithoutEventServlet();
-
// Ignore any call from an included servlet
if (included)
return;
@@ -1360,9 +1358,6 @@
if (!connector.hasIoEvents())
throw MESSAGES.cannotUpgradeWithoutEvents();
- if (!request.isEventMode() || request.getAsyncContext() != null)
- throw MESSAGES.cannotUpgradeWithoutEventServlet();
-
// Ignore any call from an included servlet
if (included)
return;
@@ -1549,7 +1544,25 @@
}
+ public void setContentLengthLong(long length) {
+ if (isCommitted())
+ return;
+ // Ignore any call from an included servlet
+ if (included)
+ return;
+
+ if (usingWriter)
+ return;
+
+ coyoteResponse.setContentLength(length);
+
+ }
+
+ public WriteListener getWriteListener() {
+ return outputBuffer.getWriteListener();
+ }
+
// ------------------------------------------------------ Protected Methods
Modified: branches/7.4.x/src/main/java/org/apache/catalina/connector/ResponseFacade.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/catalina/connector/ResponseFacade.java 2013-09-18
14:42:28 UTC (rev 2260)
+++
branches/7.4.x/src/main/java/org/apache/catalina/connector/ResponseFacade.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -605,4 +605,12 @@
return response.getStatus();
}
+ public void setContentLengthLong(long contentLength) {
+ if (response == null) {
+ throw MESSAGES.nullResponseFacade();
+ }
+
+ response.setContentLengthLong(contentLength);
+ }
+
}
Modified: branches/7.4.x/src/main/java/org/apache/catalina/core/StandardWrapperValve.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/catalina/core/StandardWrapperValve.java 2013-09-18
14:42:28 UTC (rev 2260)
+++
branches/7.4.x/src/main/java/org/apache/catalina/core/StandardWrapperValve.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -509,8 +509,8 @@
if (event.getType() == EventType.END || event.getType() == EventType.ERROR
|| event.getType() == EventType.TIMEOUT) {
// Invoke the listeners with onComplete or onTimeout
- boolean timeout = (event.getType() == EventType.TIMEOUT) ? true : false;
- boolean error = (event.getType() == EventType.ERROR) ? true : false;
+ boolean timeout = (event.getType() == EventType.TIMEOUT);
+ boolean error = (event.getType() == EventType.ERROR);
Iterator<AsyncListenerRegistration> asyncListenerRegistrations =
asyncContext.getAsyncListeners().values().iterator();
while (asyncListenerRegistrations.hasNext()) {
@@ -535,12 +535,71 @@
exception(request, response, e);
}
}
+ boolean end = (event.getType() == EventType.END) || error;
if (timeout && request.isEventMode() &&
asyncContext.getPath() == null) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ end = true;
}
+ try {
+ // Call error notifications for IO listeners
+ if (error) {
+ Throwable throwable = asyncContext.getError();
+ if (throwable == null) {
+ throwable = new Exception();
+ }
+ if (request.getReadListener() != null) {
+ request.getReadListener().onError(throwable);
+ }
+ if (response.getWriteListener() != null) {
+ response.getWriteListener().onError(throwable);
+ }
+ }
+ } catch (Throwable e) {
+
container.getLogger().error(MESSAGES.ioListenerError(getContainer().getName()), e);
+ exception(request, response, e);
+ }
+ if (end && (request.getUpgradeHandler() != null)) {
+ try {
+ // FIXME: Examine if need to call elsewhere
+ request.getUpgradeHandler().destroy();
+ } catch (Throwable e) {
+
container.getLogger().error(MESSAGES.upgradeHandlerDestroyError(getContainer().getName()),
e);
+ exception(request, response, e);
+ }
+ }
if (error && request.isEventMode() &&
asyncContext.getPath() == null) {
exception(request, response, asyncContext.getError());
}
+ } else if (event.getType() == EventType.READ) {
+ // Read notification
+ if (request.getReadListener() != null) {
+ try {
+ request.getReadListener().onDataAvailable();
+ } catch (Throwable e) {
+
container.getLogger().error(MESSAGES.ioListenerError(getContainer().getName()), e);
+ exception(request, response, e);
+ }
+ }
+ } else if (event.getType() == EventType.EOF) {
+ // End of stream notification
+ if (request.getReadListener() != null) {
+ try {
+ request.getReadListener().onAllDataRead();
+ } catch (Throwable e) {
+
container.getLogger().error(MESSAGES.ioListenerError(getContainer().getName()), e);
+ exception(request, response, e);
+ }
+ }
+ } else if (event.getType() == EventType.WRITE) {
+ // Write notification
+ if (response.getWriteListener() != null) {
+ try {
+ response.getWriteListener().onWritePossible();
+ } catch (Throwable e) {
+
container.getLogger().error(MESSAGES.ioListenerError(getContainer().getName()), e);
+ exception(request, response, e);
+ }
+ }
} else if (asyncContext.getRunnable() != null) {
// Execute the runnable
try {
Modified: branches/7.4.x/src/main/java/org/apache/coyote/ActionCode.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/coyote/ActionCode.java 2013-09-18 14:42:28 UTC
(rev 2260)
+++ branches/7.4.x/src/main/java/org/apache/coyote/ActionCode.java 2013-09-20 16:27:05 UTC
(rev 2261)
@@ -181,6 +181,16 @@
*/
public static final ActionCode ACTION_EVENT_WAKEUP = new ActionCode(29);
+ /**
+ * Ask for read callbacks
+ */
+ public static final ActionCode ACTION_EVENT_READ_BEGIN = new ActionCode(30);
+
+ /**
+ * Ask for a write callbacks
+ */
+ public static final ActionCode ACTION_EVENT_WRITE_BEGIN = new ActionCode(31);
+
// ----------------------------------------------------------- Constructors
int code;
Modified: branches/7.4.x/src/main/java/org/apache/coyote/http11/Http11AprProcessor.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/coyote/http11/Http11AprProcessor.java 2013-09-18
14:42:28 UTC (rev 2260)
+++
branches/7.4.x/src/main/java/org/apache/coyote/http11/Http11AprProcessor.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -1288,6 +1288,17 @@
writeNotification = true;
} else if (actionCode == ActionCode.ACTION_EVENT_TIMEOUT) {
timeout = ((Integer) param).intValue();
+ } else if (actionCode == ActionCode.ACTION_EVENT_READ_BEGIN) {
+ Socket.timeoutSet(socket, 0);
+ inputBuffer.setNonBlocking(true);
+ readNotifications = true;
+ } else if (actionCode == ActionCode.ACTION_EVENT_WRITE_BEGIN) {
+ Socket.timeoutSet(socket, 0);
+ outputBuffer.setNonBlocking(true);
+ if (!eventProcessing && !writeNotification) {
+ endpoint.getEventPoller().add(socket, timeout, false, true, false,
true);
+ }
+ writeNotification = true;
} else if (actionCode == ActionCode.UPGRADE) {
// Switch to raw bytes mode
inputBuffer.removeActiveFilters();
Modified: branches/7.4.x/src/main/java/org/apache/coyote/http11/Http11NioProcessor.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/coyote/http11/Http11NioProcessor.java 2013-09-18
14:42:28 UTC (rev 2260)
+++
branches/7.4.x/src/main/java/org/apache/coyote/http11/Http11NioProcessor.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -855,6 +855,12 @@
} else if (actionCode == ActionCode.ACTION_EVENT_TIMEOUT) {
// Timeout event
timeoutEvent(param);
+ } else if (actionCode == ActionCode.ACTION_EVENT_READ_BEGIN) {
+ inputBuffer.setNonBlocking(true);
+ readNotifications = true;
+ } else if (actionCode == ActionCode.ACTION_EVENT_WRITE_BEGIN) {
+ outputBuffer.setNonBlocking(true);
+ writeEvent(param);
} else if (actionCode == ActionCode.UPGRADE) {
// Switch to raw bytes mode
inputBuffer.removeActiveFilters();
Added:
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/AbstractServletInputStream.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/AbstractServletInputStream.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/AbstractServletInputStream.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote.http11.upgrade;
+
+import javax.servlet.ServletInputStream;
+
+import org.apache.coyote.http11.upgrade.servlet31.ReadListener;
+
+/**
+ * Implements the new Servlet 3.1 methods for {@link ServletInputStream}.
+ */
+public abstract class AbstractServletInputStream extends ServletInputStream {
+
+ /**
+ * New Servlet 3.1 method.
+ */
+ public abstract boolean isFinished();
+
+ /**
+ * New Servlet 3.1 method.
+ */
+ public abstract boolean isReady();
+
+ /**
+ * New Servlet 3.1 method.
+ */
+ public abstract void setReadListener(ReadListener listener);
+
+}
Added:
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/AbstractServletOutputStream.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/AbstractServletOutputStream.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/AbstractServletOutputStream.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote.http11.upgrade;
+
+import javax.servlet.ServletOutputStream;
+
+import org.apache.coyote.http11.upgrade.servlet31.WriteListener;
+
+/**
+ * Implements the new Servlet 3.1 methods for {@link ServletOutputStream}.
+ */
+public abstract class AbstractServletOutputStream extends ServletOutputStream {
+
+ /**
+ * New Servlet 3.1 method.
+ */
+ public abstract boolean isReady();
+
+ /**
+ * New Servlet 3.1 method.
+ */
+ public abstract void setWriteListener(WriteListener listener);
+
+}
Added:
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/HttpUpgradeHandler.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/HttpUpgradeHandler.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/HttpUpgradeHandler.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote.http11.upgrade.servlet31;
+
+/**
+ * Interface between the HTTP upgrade process and the new protocol.
+ */
+public interface HttpUpgradeHandler {
+
+ /**
+ * This method is called once the request/response pair where the upgrade
+ * is initiated has completed processing and is the point where control of
+ * the connection passes from the container to the
+ * {@link HttpUpgradeHandler}.
+ *
+ * @param connection The connection that has been upgraded
+ */
+ void init(WebConnection connection);
+
+ /**
+ * This method is called after the upgraded connection has been closed.
+ */
+ void destroy();
+}
Added:
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/ReadListener.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/ReadListener.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/ReadListener.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote.http11.upgrade.servlet31;
+
+import java.io.IOException;
+
+/**
+ * Receives notification of read events when using non-blocking IO.
+ */
+public interface ReadListener extends java.util.EventListener{
+
+ /**
+ * Invoked when data is available to read. The container will invoke this
+ * method the first time for a request as soon as there is data to read.
+ * Subsequent invocations will only occur if a call to {@link
+ * org.apache.coyote.http11.upgrade.AbstractServletInputStream#isReady()}
+ * has returned false and data has subsequently become available to read.
+ *
+ * @throws IOException
+ */
+ public abstract void onDataAvailable() throws IOException;
+
+ /**
+ * Invoked when the request body has been fully read.
+ *
+ * @throws IOException
+ */
+ public abstract void onAllDataRead() throws IOException;
+
+ /**
+ * Invoked if an error occurs while reading the request body.
+ *
+ * @param throwable The exception that occurred
+ */
+ public abstract void onError(java.lang.Throwable throwable);
+}
Added:
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/WebConnection.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/WebConnection.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/WebConnection.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote.http11.upgrade.servlet31;
+
+import java.io.IOException;
+
+import org.apache.coyote.http11.upgrade.AbstractServletInputStream;
+import org.apache.coyote.http11.upgrade.AbstractServletOutputStream;
+
+/**
+ * The interface used by a {@link HttpUpgradeHandler} to interact with an upgraded
+ * HTTP connection.
+ */
+public interface WebConnection {
+
+ /**
+ * Provides access to the {@link AbstractServletInputStream} for reading
+ * data from the client.
+ */
+ AbstractServletInputStream getInputStream() throws IOException;
+
+ /**
+ * Provides access to the {@link AbstractServletOutputStream} for writing
+ * data to the client.
+ */
+ AbstractServletOutputStream getOutputStream() throws IOException;
+
+ /**
+ * The Servlet 3.1 interface extends AutoCloseable but that is not available
+ * in Java 6 so this is the single method from that interface.
+ */
+ void close() throws Exception;
+}
\ No newline at end of file
Added:
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/WriteListener.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/WriteListener.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/coyote/http11/upgrade/servlet31/WriteListener.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote.http11.upgrade.servlet31;
+
+import java.io.IOException;
+
+/**
+ * Receives notification of write events when using non-blocking IO.
+ */
+public interface WriteListener extends java.util.EventListener{
+
+ /**
+ * Invoked when it it possible to write data without blocking. The container
+ * will invoke this method the first time for a request as soon as data can
+ * be written. Subsequent invocations will only occur if a call to {@link
+ * org.apache.coyote.http11.upgrade.AbstractServletOutputStream#isReady()}
+ * has returned false and it has since become possible to write data.
+ *
+ * @throws IOException
+ */
+ public void onWritePossible() throws IOException;
+
+ /**
+ * Invoked if an error occurs while writing the response.
+ *
+ * @param throwable
+ */
+ public void onError(java.lang.Throwable throwable);
+}
\ No newline at end of file
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/ExceptionUtils.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/ExceptionUtils.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/util/ExceptionUtils.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util;
+
+import java.lang.reflect.InvocationTargetException;
+
+
+/**
+ * Utilities for handling Throwables and Exceptions.
+ */
+public class ExceptionUtils {
+
+ /**
+ * Checks whether the supplied Throwable is one that needs to be
+ * rethrown and swallows all others.
+ * @param t the Throwable to check
+ */
+ public static void handleThrowable(Throwable t) {
+ if (t instanceof ThreadDeath) {
+ throw (ThreadDeath) t;
+ }
+ if (t instanceof VirtualMachineError) {
+ throw (VirtualMachineError) t;
+ }
+ // All other instances of Throwable will be silently swallowed
+ }
+
+ /**
+ * Checks whether the supplied Throwable is an instance of
+ * <code>InvocationTargetException</code> and returns the throwable that
is
+ * wrapped by it, if there is any.
+ *
+ * @param t the Throwable to check
+ * @return <code>t</code> or <code>t.getCause()</code>
+ */
+ public static Throwable unwrapInvocationTargetException(Throwable t) {
+ if (t instanceof InvocationTargetException && t.getCause() != null) {
+ return t.getCause();
+ }
+ return t;
+ }
+}
Deleted: branches/7.4.x/src/main/java/org/apache/tomcat/util/buf/UTF8Decoder.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/buf/UTF8Decoder.java 2013-09-18
14:42:28 UTC (rev 2260)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/util/buf/UTF8Decoder.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -1,126 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *
http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.tomcat.util.buf;
-
-import static org.jboss.web.CoyoteMessages.MESSAGES;
-
-import java.io.IOException;
-
-/**
- * Moved from ByteChunk - code to convert from UTF8 bytes to chars.
- * Not used in the current tomcat3.3 : the performance gain is not very
- * big if the String is created, only if we avoid that and work only
- * on char[]. Until than, it's better to be safe. ( I tested this code
- * with 2 and 3 bytes chars, and it works fine in xerces )
- *
- * Cut from xerces' UTF8Reader.copyMultiByteCharData()
- *
- * @author Costin Manolache
- * @author ( Xml-Xerces )
- */
-public final class UTF8Decoder extends B2CConverter {
-
-
- // may have state !!
-
- public UTF8Decoder()
- throws IOException {
- super("UTF-8");
- }
-
- public void recycle() {
- }
-
- public void convert(ByteChunk mb, CharChunk cb )
- throws IOException {
- int bytesOff=mb.getOffset();
- int bytesLen=mb.getLength();
- byte bytes[]=mb.getBytes();
-
- int j=bytesOff;
- int end=j+bytesLen;
-
- while( j< end ) {
- int b0=0xff & bytes[j];
-
- if( (b0 & 0x80) == 0 ) {
- cb.append((char)b0);
- j++;
- continue;
- }
-
- // 2 byte ?
- if( j++ >= end ) {
- // ok, just ignore - we could throw exception
- throw new IOException(MESSAGES.utf8DecodingEof());
- }
- int b1=0xff & bytes[j];
-
- // ok, let's the fun begin - we're handling UTF8
- if ((0xe0 & b0) == 0xc0) { // 110yyyyy 10xxxxxx (0x80 to 0x7ff)
- int ch = ((0x1f & b0)<<6) + (0x3f & b1);
-
- cb.append((char)ch);
- j++;
- continue;
- }
-
- if( j++ >= end )
- return ;
- int b2=0xff & bytes[j];
-
- if( (b0 & 0xf0 ) == 0xe0 ) {
- if ((b0 == 0xED && b1 >= 0xA0) ||
- (b0 == 0xEF && b1 == 0xBF && b2 >= 0xBE)) {
- throw new IOException(MESSAGES.utf8DecodingFailure(b0, b1, b2));
- }
-
- int ch = ((0x0f & b0)<<12) + ((0x3f & b1)<<6) + (0x3f
& b2);
- cb.append((char)ch);
- j++;
- continue;
- }
-
- if( j++ >= end )
- return ;
- int b3=0xff & bytes[j];
-
- if (( 0xf8 & b0 ) == 0xf0 ) {
- if (b0 > 0xF4 || (b0 == 0xF4 && b1 >= 0x90)) {
- throw new IOException(MESSAGES.utf8DecodingFailure(b0, b1, b2, b3));
- }
- int ch = ((0x0f & b0)<<18) + ((0x3f & b1)<<12) +
- ((0x3f & b2)<<6) + (0x3f & b3);
-
- if (ch < 0x10000) {
- cb.append( (char)ch );
- } else {
- cb.append((char)(((ch-0x00010000)>>10)+
- 0xd800));
- cb.append((char)(((ch-0x00010000)&0x3ff)+
- 0xdc00));
- }
- j++;
- continue;
- } else {
- throw new IOException(MESSAGES.utf8DecodingFailure(b0, b1, b2, b3));
- }
- }
- }
-
-}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/buf/Utf8Encoder.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/buf/Utf8Encoder.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/util/buf/Utf8Encoder.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.buf;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+
+/**
+ * Encodes characters as bytes using UTF-8. Extracted from Apache Harmony with
+ * some minor bug fixes applied.
+ */
+public class Utf8Encoder extends CharsetEncoder {
+
+ public Utf8Encoder() {
+ super(EncodingToCharset.UTF_8, 1.1f, 4.0f);
+ }
+
+ @Override
+ protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) {
+ if (in.hasArray() && out.hasArray()) {
+ return encodeHasArray(in, out);
+ }
+ return encodeNotHasArray(in, out);
+ }
+
+ private CoderResult encodeHasArray(CharBuffer in, ByteBuffer out) {
+ int outRemaining = out.remaining();
+ int pos = in.position();
+ int limit = in.limit();
+ byte[] bArr;
+ char[] cArr;
+ int x = pos;
+ bArr = out.array();
+ cArr = in.array();
+ int outPos = out.position();
+ int rem = in.remaining();
+ for (x = pos; x < pos + rem; x++) {
+ int jchar = (cArr[x] & 0xFFFF);
+
+ if (jchar <= 0x7F) {
+ if (outRemaining < 1) {
+ in.position(x);
+ out.position(outPos);
+ return CoderResult.OVERFLOW;
+ }
+ bArr[outPos++] = (byte) (jchar & 0xFF);
+ outRemaining--;
+ } else if (jchar <= 0x7FF) {
+
+ if (outRemaining < 2) {
+ in.position(x);
+ out.position(outPos);
+ return CoderResult.OVERFLOW;
+ }
+ bArr[outPos++] = (byte) (0xC0 + ((jchar >> 6) & 0x1F));
+ bArr[outPos++] = (byte) (0x80 + (jchar & 0x3F));
+ outRemaining -= 2;
+
+ } else if (jchar >= 0xD800 && jchar <= 0xDFFF) {
+
+ // in has to have one byte more.
+ if (limit <= x + 1) {
+ in.position(x);
+ out.position(outPos);
+ return CoderResult.UNDERFLOW;
+ }
+
+ if (outRemaining < 4) {
+ in.position(x);
+ out.position(outPos);
+ return CoderResult.OVERFLOW;
+ }
+
+ // The surrogate pair starts with a low-surrogate.
+ if (jchar >= 0xDC00) {
+ in.position(x);
+ out.position(outPos);
+ return CoderResult.malformedForLength(1);
+ }
+
+ int jchar2 = cArr[x + 1] & 0xFFFF;
+
+ // The surrogate pair ends with a high-surrogate.
+ if (jchar2 < 0xDC00) {
+ in.position(x);
+ out.position(outPos);
+ return CoderResult.malformedForLength(1);
+ }
+
+ // Note, the Unicode scalar value n is defined
+ // as follows:
+ // n = (jchar-0xD800)*0x400+(jchar2-0xDC00)+0x10000
+ // Where jchar is a high-surrogate,
+ // jchar2 is a low-surrogate.
+ int n = (jchar << 10) + jchar2 + 0xFCA02400;
+
+ bArr[outPos++] = (byte) (0xF0 + ((n >> 18) & 0x07));
+ bArr[outPos++] = (byte) (0x80 + ((n >> 12) & 0x3F));
+ bArr[outPos++] = (byte) (0x80 + ((n >> 6) & 0x3F));
+ bArr[outPos++] = (byte) (0x80 + (n & 0x3F));
+ outRemaining -= 4;
+ x++;
+
+ } else {
+
+ if (outRemaining < 3) {
+ in.position(x);
+ out.position(outPos);
+ return CoderResult.OVERFLOW;
+ }
+ bArr[outPos++] = (byte) (0xE0 + ((jchar >> 12) & 0x0F));
+ bArr[outPos++] = (byte) (0x80 + ((jchar >> 6) & 0x3F));
+ bArr[outPos++] = (byte) (0x80 + (jchar & 0x3F));
+ outRemaining -= 3;
+ }
+ if (outRemaining == 0) {
+ in.position(x + 1);
+ out.position(outPos);
+ // If both input and output are exhausted, return UNDERFLOW
+ if (x + 1 == limit) {
+ return CoderResult.UNDERFLOW;
+ } else {
+ return CoderResult.OVERFLOW;
+ }
+ }
+
+ }
+ if (rem != 0) {
+ in.position(x);
+ out.position(outPos);
+ }
+ return CoderResult.UNDERFLOW;
+ }
+
+ private CoderResult encodeNotHasArray(CharBuffer in, ByteBuffer out) {
+ int outRemaining = out.remaining();
+ int pos = in.position();
+ int limit = in.limit();
+ try {
+ while (pos < limit) {
+ if (outRemaining == 0) {
+ return CoderResult.OVERFLOW;
+ }
+
+ int jchar = (in.get() & 0xFFFF);
+
+ if (jchar <= 0x7F) {
+
+ if (outRemaining < 1) {
+ return CoderResult.OVERFLOW;
+ }
+ out.put((byte) jchar);
+ outRemaining--;
+
+ } else if (jchar <= 0x7FF) {
+
+ if (outRemaining < 2) {
+ return CoderResult.OVERFLOW;
+ }
+ out.put((byte) (0xC0 + ((jchar >> 6) & 0x1F)));
+ out.put((byte) (0x80 + (jchar & 0x3F)));
+ outRemaining -= 2;
+
+ } else if (jchar >= 0xD800 && jchar <= 0xDFFF) {
+
+ // in has to have one byte more.
+ if (limit <= pos + 1) {
+ return CoderResult.UNDERFLOW;
+ }
+
+ if (outRemaining < 4) {
+ return CoderResult.OVERFLOW;
+ }
+
+ // The surrogate pair starts with a low-surrogate.
+ if (jchar >= 0xDC00) {
+ return CoderResult.malformedForLength(1);
+ }
+
+ int jchar2 = (in.get() & 0xFFFF);
+
+ // The surrogate pair ends with a high-surrogate.
+ if (jchar2 < 0xDC00) {
+ return CoderResult.malformedForLength(1);
+ }
+
+ // Note, the Unicode scalar value n is defined
+ // as follows:
+ // n = (jchar-0xD800)*0x400+(jchar2-0xDC00)+0x10000
+ // Where jchar is a high-surrogate,
+ // jchar2 is a low-surrogate.
+ int n = (jchar << 10) + jchar2 + 0xFCA02400;
+
+ out.put((byte) (0xF0 + ((n >> 18) & 0x07)));
+ out.put((byte) (0x80 + ((n >> 12) & 0x3F)));
+ out.put((byte) (0x80 + ((n >> 6) & 0x3F)));
+ out.put((byte) (0x80 + (n & 0x3F)));
+ outRemaining -= 4;
+ pos++;
+
+ } else {
+
+ if (outRemaining < 3) {
+ return CoderResult.OVERFLOW;
+ }
+ out.put((byte) (0xE0 + ((jchar >> 12) & 0x0F)));
+ out.put((byte) (0x80 + ((jchar >> 6) & 0x3F)));
+ out.put((byte) (0x80 + (jchar & 0x3F)));
+ outRemaining -= 3;
+ }
+ pos++;
+ }
+ } finally {
+ in.position(pos);
+ }
+ return CoderResult.UNDERFLOW;
+ }
+}
\ No newline at end of file
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/BinaryDecoder.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/BinaryDecoder.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/BinaryDecoder.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.codec;
+
+/**
+ * Defines common decoding methods for byte array decoders.
+ *
+ * @version $Id: BinaryDecoder.java 1459218 2013-03-21 10:31:50Z markt $
+ */
+public interface BinaryDecoder extends Decoder {
+
+ /**
+ * Decodes a byte array and returns the results as a byte array.
+ *
+ * @param source
+ * A byte array which has been encoded with the appropriate encoder
+ * @return a byte array that contains decoded content
+ * @throws DecoderException
+ * A decoder exception is thrown if a Decoder encounters a failure
condition during the decode process.
+ */
+ byte[] decode(byte[] source) throws DecoderException;
+}
+
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/BinaryEncoder.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/BinaryEncoder.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/BinaryEncoder.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.codec;
+
+/**
+ * Defines common encoding methods for byte array encoders.
+ *
+ * @version $Id: BinaryEncoder.java 1459218 2013-03-21 10:31:50Z markt $
+ */
+public interface BinaryEncoder extends Encoder {
+
+ /**
+ * Encodes a byte array and return the encoded data as a byte array.
+ *
+ * @param source
+ * Data to be encoded
+ * @return A byte array containing the encoded data
+ * @throws EncoderException
+ * thrown if the Encoder encounters a failure condition during the
encoding process.
+ */
+ byte[] encode(byte[] source) throws EncoderException;
+}
+
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/Decoder.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/Decoder.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/Decoder.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.codec;
+
+/**
+ * Provides the highest level of abstraction for Decoders.
+ * <p>
+ * This is the sister interface of {@link Encoder}. All Decoders implement this common
generic interface.
+ * Allows a user to pass a generic Object to any Decoder implementation in the codec
package.
+ * <p>
+ * One of the two interfaces at the center of the codec package.
+ *
+ * @version $Id: Decoder.java 1459218 2013-03-21 10:31:50Z markt $
+ */
+public interface Decoder {
+
+ /**
+ * Decodes an "encoded" Object and returns a "decoded" Object.
Note that the implementation of this interface will
+ * try to cast the Object parameter to the specific type expected by a particular
Decoder implementation. If a
+ * {@link ClassCastException} occurs this decode method will throw a
DecoderException.
+ *
+ * @param source
+ * the object to decode
+ * @return a 'decoded" object
+ * @throws DecoderException
+ * a decoder exception can be thrown for any number of reasons. Some good
candidates are that the
+ * parameter passed to this method is null, a param cannot be cast to the
appropriate type for a
+ * specific encoder.
+ */
+ Object decode(Object source) throws DecoderException;
+}
+
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/DecoderException.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/DecoderException.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/DecoderException.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.codec;
+
+/**
+ * Thrown when there is a failure condition during the decoding process. This exception
is thrown when a {@link Decoder}
+ * encounters a decoding specific exception such as invalid data, or characters outside
of the expected range.
+ *
+ * @version $Id: DecoderException.java 1459218 2013-03-21 10:31:50Z markt $
+ */
+public class DecoderException extends Exception {
+
+ /**
+ * Declares the Serial Version Uid.
+ *
+ * @see <a
href="http://c2.com/cgi/wiki?AlwaysDeclareSerialVersionUid">... Declare
Serial Version Uid</a>
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with {@code null} as its detail message. The cause is
not initialized, and may
+ * subsequently be initialized by a call to {@link #initCause}.
+ *
+ * @since 1.4
+ */
+ public DecoderException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message. The cause is not
initialized, and may subsequently
+ * be initialized by a call to {@link #initCause}.
+ *
+ * @param message
+ * The detail message which is saved for later retrieval by the {@link
#getMessage()} method.
+ */
+ public DecoderException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ * <p>
+ * Note that the detail message associated with <code>cause</code> is not
automatically incorporated into this
+ * exception's detail message.
+ *
+ * @param message
+ * The detail message which is saved for later retrieval by the {@link
#getMessage()} method.
+ * @param cause
+ * The cause which is saved for later retrieval by the {@link #getCause()}
method. A {@code null}
+ * value is permitted, and indicates that the cause is nonexistent or
unknown.
+ * @since 1.4
+ */
+ public DecoderException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause and a detail message of
<code>(cause==null ?
+ * null : cause.toString())</code> (which typically contains the class and
detail message of <code>cause</code>).
+ * This constructor is useful for exceptions that are little more than wrappers for
other throwables.
+ *
+ * @param cause
+ * The cause which is saved for later retrieval by the {@link #getCause()}
method. A {@code null}
+ * value is permitted, and indicates that the cause is nonexistent or
unknown.
+ * @since 1.4
+ */
+ public DecoderException(final Throwable cause) {
+ super(cause);
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/Encoder.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/Encoder.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/Encoder.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.codec;
+
+/**
+ * Provides the highest level of abstraction for Encoders.
+ * <p>
+ * This is the sister interface of {@link Decoder}. Every implementation of Encoder
provides this
+ * common generic interface which allows a user to pass a generic Object to any Encoder
implementation
+ * in the codec package.
+ *
+ * @version $Id: Encoder.java 1459218 2013-03-21 10:31:50Z markt $
+ */
+public interface Encoder {
+
+ /**
+ * Encodes an "Object" and returns the encoded content as an Object. The
Objects here may just be
+ * <code>byte[]</code> or <code>String</code>s depending on
the implementation used.
+ *
+ * @param source
+ * An object to encode
+ * @return An "encoded" Object
+ * @throws EncoderException
+ * An encoder exception is thrown if the encoder experiences a failure
condition during the encoding
+ * process.
+ */
+ Object encode(Object source) throws EncoderException;
+}
+
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/EncoderException.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/EncoderException.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/EncoderException.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.codec;
+
+/**
+ * Thrown when there is a failure condition during the encoding process. This exception
is thrown when an
+ * {@link Encoder} encounters a encoding specific exception such as invalid data,
inability to calculate a checksum,
+ * characters outside of the expected range.
+ *
+ * @version $Id: EncoderException.java 1459218 2013-03-21 10:31:50Z markt $
+ */
+public class EncoderException extends Exception {
+
+ /**
+ * Declares the Serial Version Uid.
+ *
+ * @see <a
href="http://c2.com/cgi/wiki?AlwaysDeclareSerialVersionUid">... Declare
Serial Version Uid</a>
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with {@code null} as its detail message. The cause is
not initialized, and may
+ * subsequently be initialized by a call to {@link #initCause}.
+ *
+ * @since 1.4
+ */
+ public EncoderException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message. The cause is not
initialized, and may subsequently
+ * be initialized by a call to {@link #initCause}.
+ *
+ * @param message
+ * a useful message relating to the encoder specific error.
+ */
+ public EncoderException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ *
+ * <p>
+ * Note that the detail message associated with <code>cause</code> is not
automatically incorporated into this
+ * exception's detail message.
+ * </p>
+ *
+ * @param message
+ * The detail message which is saved for later retrieval by the {@link
#getMessage()} method.
+ * @param cause
+ * The cause which is saved for later retrieval by the {@link #getCause()}
method. A {@code null}
+ * value is permitted, and indicates that the cause is nonexistent or
unknown.
+ * @since 1.4
+ */
+ public EncoderException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause and a detail message of
<code>(cause==null ?
+ * null : cause.toString())</code> (which typically contains the class and
detail message of <code>cause</code>).
+ * This constructor is useful for exceptions that are little more than wrappers for
other throwables.
+ *
+ * @param cause
+ * The cause which is saved for later retrieval by the {@link #getCause()}
method. A {@code null}
+ * value is permitted, and indicates that the cause is nonexistent or
unknown.
+ * @since 1.4
+ */
+ public EncoderException(final Throwable cause) {
+ super(cause);
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/Base64.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/Base64.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/Base64.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,779 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.codec.binary;
+
+import java.math.BigInteger;
+
+/**
+ * Provides Base64 encoding and decoding as defined by <a
href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>.
+ *
+ * <p>
+ * This class implements section <cite>6.8. Base64
Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
+ * Internet Mail Extensions (MIME) Part One: Format of Internet Message
Bodies</cite> by Freed and Borenstein.
+ * </p>
+ * <p>
+ * The class can be parameterized in the following manner with various constructors:
+ * <ul>
+ * <li>URL-safe mode: Default off.</li>
+ * <li>Line length: Default 76. Line length that aren't multiples of 4 will
still essentially end up being multiples of
+ * 4 in the encoded data.
+ * <li>Line separator: Default is CRLF ("\r\n")</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Since this class operates directly on byte streams, and not character streams, it is
hard-coded to only
+ * encode/decode character encodings which are compatible with the lower 127 ASCII chart
(ISO-8859-1, Windows-1252,
+ * UTF-8, etc).
+ * </p>
+ * <p>
+ * This class is thread-safe.
+ * </p>
+ *
+ * @see <a
href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
+ * @since 1.0
+ * @version $Id: Base64.java 1459346 2013-03-21 15:05:54Z markt $
+ */
+public class Base64 extends BaseNCodec {
+
+ /**
+ * BASE32 characters are 6 bits in length.
+ * They are formed by taking a block of 3 octets to form a 24-bit string,
+ * which is converted into 4 BASE64 characters.
+ */
+ private static final int BITS_PER_ENCODED_BYTE = 6;
+ private static final int BYTES_PER_UNENCODED_BLOCK = 3;
+ private static final int BYTES_PER_ENCODED_BLOCK = 4;
+
+ /**
+ * Chunk separator per RFC 2045 section 2.1.
+ *
+ * <p>
+ * N.B. The next major release may break compatibility and make this field private.
+ * </p>
+ *
+ * @see <a
href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045
section 2.1</a>
+ */
+ static final byte[] CHUNK_SEPARATOR = {'\r', '\n'};
+
+ /**
+ * This array is a lookup table that translates 6-bit positive integer index values
into their "Base64 Alphabet"
+ * equivalents as specified in Table 1 of RFC 2045.
+ *
+ * Thanks to "commons" project in
ws.apache.org for this code.
+ *
http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ */
+ private static final byte[] STANDARD_ENCODE_TABLE = {
+ 'A', 'B', 'C', 'D', 'E', 'F',
'G', 'H', 'I', 'J', 'K', 'L',
'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y',
'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l',
'm',
+ 'n', 'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x', 'y',
'z',
+ '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', '+', '/'
+ };
+
+ /**
+ * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and /
+ * changed to - and _ to make the encoded Base64 results more URL-SAFE.
+ * This table is only used when the Base64's mode is set to URL-SAFE.
+ */
+ private static final byte[] URL_SAFE_ENCODE_TABLE = {
+ 'A', 'B', 'C', 'D', 'E', 'F',
'G', 'H', 'I', 'J', 'K', 'L',
'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y',
'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l',
'm',
+ 'n', 'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x', 'y',
'z',
+ '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', '-', '_'
+ };
+
+ /**
+ * This array is a lookup table that translates Unicode characters drawn from the
"Base64 Alphabet" (as specified
+ * in Table 1 of RFC 2045) into their 6-bit positive integer equivalents. Characters
that are not in the Base64
+ * alphabet but fall within the bounds of the array are translated to -1.
+ *
+ * Note: '+' and '-' both decode to 62. '/' and '_'
both decode to 63. This means decoder seamlessly handles both
+ * URL_SAFE and STANDARD base64. (The encoder, on the other hand, needs to know ahead
of time what to emit).
+ *
+ * Thanks to "commons" project in
ws.apache.org for this code.
+ *
http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ */
+ private static final byte[] DECODE_TABLE = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54,
+ 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
+ 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
+ 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
+ 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
+ };
+
+ /**
+ * Base64 uses 6-bit fields.
+ */
+ /** Mask used to extract 6 bits, used when encoding */
+ private static final int MASK_6BITS = 0x3f;
+
+ // The static final fields above are used for the original static byte[] methods on
Base64.
+ // The private member fields below are used with the new streaming approach, which
requires
+ // some state be preserved between calls of encode() and decode().
+
+ /**
+ * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE above
remains static because it is able
+ * to decode both STANDARD and URL_SAFE streams, but the encodeTable must be a member
variable so we can switch
+ * between the two modes.
+ */
+ private final byte[] encodeTable;
+
+ // Only one decode table currently; keep for consistency with Base32 code
+ private final byte[] decodeTable = DECODE_TABLE;
+
+ /**
+ * Line separator for encoding. Not used when decoding. Only used if lineLength >
0.
+ */
+ private final byte[] lineSeparator;
+
+ /**
+ * Convenience variable to help us determine when our buffer is going to run out of
room and needs resizing.
+ * <code>decodeSize = 3 + lineSeparator.length;</code>
+ */
+ private final int decodeSize;
+
+ /**
+ * Convenience variable to help us determine when our buffer is going to run out of
room and needs resizing.
+ * <code>encodeSize = 4 + lineSeparator.length;</code>
+ */
+ private final int encodeSize;
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe
mode.
+ * <p>
+ * When encoding the line length is 0 (no chunking), and the encoding table is
STANDARD_ENCODE_TABLE.
+ * </p>
+ *
+ * <p>
+ * When decoding all variants are supported.
+ * </p>
+ */
+ public Base64() {
+ this(0);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in the given
URL-safe mode.
+ * <p>
+ * When encoding the line length is 76, the line separator is CRLF, and the encoding
table is
+ * STANDARD_ENCODE_TABLE.
+ * </p>
+ *
+ * <p>
+ * When decoding all variants are supported.
+ * </p>
+ *
+ * @param urlSafe
+ * if {@code true}, URL-safe encoding is used. In most cases this should
be set to {@code false}.
+ * @since 1.4
+ */
+ public Base64(final boolean urlSafe) {
+ this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe
mode.
+ * <p>
+ * When encoding the line length is given in the constructor, the line separator is
CRLF, and the encoding table is
+ * STANDARD_ENCODE_TABLE.
+ * </p>
+ * <p>
+ * Line lengths that aren't multiples of 4 will still essentially end up being
multiples of 4 in the encoded data.
+ * </p>
+ * <p>
+ * When decoding all variants are supported.
+ * </p>
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length (rounded
down to nearest multiple of
+ * 4). If lineLength <= 0, then the output will not be divided into
lines (chunks). Ignored when
+ * decoding.
+ * @since 1.4
+ */
+ public Base64(final int lineLength) {
+ this(lineLength, CHUNK_SEPARATOR);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe
mode.
+ * <p>
+ * When encoding the line length and line separator are given in the constructor, and
the encoding table is
+ * STANDARD_ENCODE_TABLE.
+ * </p>
+ * <p>
+ * Line lengths that aren't multiples of 4 will still essentially end up being
multiples of 4 in the encoded data.
+ * </p>
+ * <p>
+ * When decoding all variants are supported.
+ * </p>
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length (rounded
down to nearest multiple of
+ * 4). If lineLength <= 0, then the output will not be divided into
lines (chunks). Ignored when
+ * decoding.
+ * @param lineSeparator
+ * Each line of encoded data will end with this sequence of bytes.
+ * @throws IllegalArgumentException
+ * Thrown when the provided lineSeparator included some base64
characters.
+ * @since 1.4
+ */
+ public Base64(final int lineLength, final byte[] lineSeparator) {
+ this(lineLength, lineSeparator, false);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe
mode.
+ * <p>
+ * When encoding the line length and line separator are given in the constructor, and
the encoding table is
+ * STANDARD_ENCODE_TABLE.
+ * </p>
+ * <p>
+ * Line lengths that aren't multiples of 4 will still essentially end up being
multiples of 4 in the encoded data.
+ * </p>
+ * <p>
+ * When decoding all variants are supported.
+ * </p>
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length (rounded
down to nearest multiple of
+ * 4). If lineLength <= 0, then the output will not be divided into
lines (chunks). Ignored when
+ * decoding.
+ * @param lineSeparator
+ * Each line of encoded data will end with this sequence of bytes.
+ * @param urlSafe
+ * Instead of emitting '+' and '/' we emit '-' and
'_' respectively. urlSafe is only applied to encode
+ * operations. Decoding seamlessly handles both modes.
+ * <b>Note: no padding is added when using the URL-safe
alphabet.</b>
+ * @throws IllegalArgumentException
+ * The provided lineSeparator included some base64 characters. That's
not going to work!
+ * @since 1.4
+ */
+ public Base64(final int lineLength, final byte[] lineSeparator, final boolean
urlSafe) {
+ super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK,
+ lineLength,
+ lineSeparator == null ? 0 : lineSeparator.length);
+ // TODO could be simplified if there is no requirement to reject invalid line sep
when length <=0
+ // @see test case Base64Test.testConstructors()
+ if (lineSeparator != null) {
+ if (containsAlphabetOrPad(lineSeparator)) {
+ final String sep = StringUtils.newStringUtf8(lineSeparator);
+ throw new IllegalArgumentException("lineSeparator must not contain
base64 characters: [" + sep + "]");
+ }
+ if (lineLength > 0){ // null line-sep forces no chunking rather than
throwing IAE
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length;
+ this.lineSeparator = new byte[lineSeparator.length];
+ System.arraycopy(lineSeparator, 0, this.lineSeparator, 0,
lineSeparator.length);
+ } else {
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK;
+ this.lineSeparator = null;
+ }
+ } else {
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK;
+ this.lineSeparator = null;
+ }
+ this.decodeSize = this.encodeSize - 1;
+ this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE;
+ }
+
+ /**
+ * Returns our current encode mode. True if we're URL-SAFE, false otherwise.
+ *
+ * @return true if we're in URL-SAFE mode, false otherwise.
+ * @since 1.4
+ */
+ public boolean isUrlSafe() {
+ return this.encodeTable == URL_SAFE_ENCODE_TABLE;
+ }
+
+ /**
+ * <p>
+ * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be
called at least twice: once with
+ * the data to encode, and once with inAvail set to "-1" to alert encoder
that EOF has been reached, to flush last
+ * remaining bytes (if not multiple of 3).
+ * </p>
+ * <p><b>Note: no padding is added when encoding using the URL-safe
alphabet.</b></p>
+ * <p>
+ * Thanks to "commons" project in
ws.apache.org for the bitwise operations,
and general approach.
+ *
http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ * </p>
+ *
+ * @param in
+ * byte[] array of binary data to base64 encode.
+ * @param inPos
+ * Position to start reading data from.
+ * @param inAvail
+ * Amount of bytes available from input for encoding.
+ * @param context
+ * the context to be used
+ */
+ @Override
+ void encode(final byte[] in, int inPos, final int inAvail, final Context context) {
+ if (context.eof) {
+ return;
+ }
+ // inAvail < 0 is how we're informed of EOF in the underlying data
we're
+ // encoding.
+ if (inAvail < 0) {
+ context.eof = true;
+ if (0 == context.modulus && lineLength == 0) {
+ return; // no leftovers to process and not using chunking
+ }
+ final byte[] buffer = ensureBufferSize(encodeSize, context);
+ final int savedPos = context.pos;
+ switch (context.modulus) { // 0-2
+ case 0 : // nothing to do here
+ break;
+ case 1 : // 8 bits = 6 + 2
+ // top 6 bits:
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 2)
& MASK_6BITS];
+ // remaining 2:
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 4)
& MASK_6BITS];
+ // URL-SAFE skips the padding to further reduce size.
+ if (encodeTable == STANDARD_ENCODE_TABLE) {
+ buffer[context.pos++] = PAD;
+ buffer[context.pos++] = PAD;
+ }
+ break;
+
+ case 2 : // 16 bits = 6 + 6 + 4
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >>
10) & MASK_6BITS];
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 4)
& MASK_6BITS];
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 2)
& MASK_6BITS];
+ // URL-SAFE skips the padding to further reduce size.
+ if (encodeTable == STANDARD_ENCODE_TABLE) {
+ buffer[context.pos++] = PAD;
+ }
+ break;
+ default:
+ throw new IllegalStateException("Impossible modulus
"+context.modulus);
+ }
+ context.currentLinePos += context.pos - savedPos; // keep track of current
line position
+ // if currentPos == 0 we are at the start of a line, so don't add CRLF
+ if (lineLength > 0 && context.currentLinePos > 0) {
+ System.arraycopy(lineSeparator, 0, buffer, context.pos,
lineSeparator.length);
+ context.pos += lineSeparator.length;
+ }
+ } else {
+ for (int i = 0; i < inAvail; i++) {
+ final byte[] buffer = ensureBufferSize(encodeSize, context);
+ context.modulus = (context.modulus+1) % BYTES_PER_UNENCODED_BLOCK;
+ int b = in[inPos++];
+ if (b < 0) {
+ b += 256;
+ }
+ context.ibitWorkArea = (context.ibitWorkArea << 8) + b; //
BITS_PER_BYTE
+ if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >>
18) & MASK_6BITS];
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >>
12) & MASK_6BITS];
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 6)
& MASK_6BITS];
+ buffer[context.pos++] = encodeTable[context.ibitWorkArea &
MASK_6BITS];
+ context.currentLinePos += BYTES_PER_ENCODED_BLOCK;
+ if (lineLength > 0 && lineLength <=
context.currentLinePos) {
+ System.arraycopy(lineSeparator, 0, buffer, context.pos,
lineSeparator.length);
+ context.pos += lineSeparator.length;
+ context.currentLinePos = 0;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * <p>
+ * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be
called at least twice: once
+ * with the data to decode, and once with inAvail set to "-1" to alert
decoder that EOF has been reached. The "-1"
+ * call is not necessary when decoding, but it doesn't hurt, either.
+ * </p>
+ * <p>
+ * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is
handled, since CR and LF are
+ * silently ignored, but has implications for other bytes, too. This method
subscribes to the garbage-in,
+ * garbage-out philosophy: it will not check the provided data for validity.
+ * </p>
+ * <p>
+ * Thanks to "commons" project in
ws.apache.org for the bitwise operations,
and general approach.
+ *
http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ * </p>
+ *
+ * @param in
+ * byte[] array of ascii data to base64 decode.
+ * @param inPos
+ * Position to start reading data from.
+ * @param inAvail
+ * Amount of bytes available from input for encoding.
+ * @param context
+ * the context to be used
+ */
+ @Override
+ void decode(final byte[] in, int inPos, final int inAvail, final Context context) {
+ if (context.eof) {
+ return;
+ }
+ if (inAvail < 0) {
+ context.eof = true;
+ }
+ for (int i = 0; i < inAvail; i++) {
+ final byte[] buffer = ensureBufferSize(decodeSize, context);
+ final byte b = in[inPos++];
+ if (b == PAD) {
+ // We're done.
+ context.eof = true;
+ break;
+ } else {
+ if (b >= 0 && b < DECODE_TABLE.length) {
+ final int result = DECODE_TABLE[b];
+ if (result >= 0) {
+ context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK;
+ context.ibitWorkArea = (context.ibitWorkArea <<
BITS_PER_ENCODED_BYTE) + result;
+ if (context.modulus == 0) {
+ buffer[context.pos++] = (byte) ((context.ibitWorkArea
>> 16) & MASK_8BITS);
+ buffer[context.pos++] = (byte) ((context.ibitWorkArea
>> 8) & MASK_8BITS);
+ buffer[context.pos++] = (byte) (context.ibitWorkArea &
MASK_8BITS);
+ }
+ }
+ }
+ }
+ }
+
+ // Two forms of EOF as far as base64 decoder is concerned: actual
+ // EOF (-1) and first time '=' character is encountered in stream.
+ // This approach makes the '=' padding characters completely optional.
+ if (context.eof && context.modulus != 0) {
+ final byte[] buffer = ensureBufferSize(decodeSize, context);
+
+ // We have some spare bits remaining
+ // Output all whole multiples of 8 bits and ignore the rest
+ switch (context.modulus) {
+// case 0 : // impossible, as excluded above
+ case 1 : // 6 bits - ignore entirely
+ // TODO not currently tested; perhaps it is impossible?
+ break;
+ case 2 : // 12 bits = 8 + 4
+ context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the
extra 4 bits
+ buffer[context.pos++] = (byte) ((context.ibitWorkArea) &
MASK_8BITS);
+ break;
+ case 3 : // 18 bits = 8 + 8 + 2
+ context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2
bits
+ buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8)
& MASK_8BITS);
+ buffer[context.pos++] = (byte) ((context.ibitWorkArea) &
MASK_8BITS);
+ break;
+ default:
+ throw new IllegalStateException("Impossible modulus
"+context.modulus);
+ }
+ }
+ }
+
+ /**
+ * Tests a given byte array to see if it contains only valid characters within the
Base64 alphabet. Currently the
+ * method treats whitespace as valid.
+ *
+ * @param arrayOctet
+ * byte array to test
+ * @return {@code true} if all bytes are valid characters in the Base64 alphabet or
if the byte array is empty;
+ * {@code false}, otherwise
+ * @deprecated 1.5 Use {@link #isBase64(byte[])}, will be removed in 2.0.
+ */
+ @Deprecated
+ public static boolean isArrayByteBase64(final byte[] arrayOctet) {
+ return isBase64(arrayOctet);
+ }
+
+ /**
+ * Returns whether or not the <code>octet</code> is in the base 64
alphabet.
+ *
+ * @param octet
+ * The value to test
+ * @return {@code true} if the value is defined in the the base 64 alphabet, {@code
false} otherwise.
+ * @since 1.4
+ */
+ public static boolean isBase64(final byte octet) {
+ return octet == PAD_DEFAULT || (octet >= 0 && octet <
DECODE_TABLE.length && DECODE_TABLE[octet] != -1);
+ }
+
+ /**
+ * Tests a given String to see if it contains only valid characters within the Base64
alphabet. Currently the
+ * method treats whitespace as valid.
+ *
+ * @param base64
+ * String to test
+ * @return {@code true} if all characters in the String are valid characters in the
Base64 alphabet or if
+ * the String is empty; {@code false}, otherwise
+ * @since 1.5
+ */
+ public static boolean isBase64(final String base64) {
+ return isBase64(StringUtils.getBytesUtf8(base64));
+ }
+
+ /**
+ * Tests a given byte array to see if it contains only valid characters within the
Base64 alphabet. Currently the
+ * method treats whitespace as valid.
+ *
+ * @param arrayOctet
+ * byte array to test
+ * @return {@code true} if all bytes are valid characters in the Base64 alphabet or
if the byte array is empty;
+ * {@code false}, otherwise
+ * @since 1.5
+ */
+ public static boolean isBase64(final byte[] arrayOctet) {
+ for (int i = 0; i < arrayOctet.length; i++) {
+ if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm but does not chunk the output.
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return byte[] containing Base64 characters in their UTF-8 representation.
+ */
+ public static byte[] encodeBase64(final byte[] binaryData) {
+ return encodeBase64(binaryData, false);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm but does not chunk the output.
+ *
+ * NOTE: We changed the behaviour of this method from multi-line chunking
(commons-codec-1.4) to
+ * single-line non-chunking (commons-codec-1.5).
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return String containing Base64 characters.
+ * @since 1.4 (NOTE: 1.4 chunked the output, whereas 1.5 does not).
+ */
+ public static String encodeBase64String(final byte[] binaryData) {
+ return StringUtils.newStringUtf8(encodeBase64(binaryData, false));
+ }
+
+ /**
+ * Encodes binary data using a URL-safe variation of the base64 algorithm but does
not chunk the output. The
+ * url-safe variation emits - and _ instead of + and / characters.
+ * <b>Note: no padding is added.</b>
+ * @param binaryData
+ * binary data to encode
+ * @return byte[] containing Base64 characters in their UTF-8 representation.
+ * @since 1.4
+ */
+ public static byte[] encodeBase64URLSafe(final byte[] binaryData) {
+ return encodeBase64(binaryData, false, true);
+ }
+
+ /**
+ * Encodes binary data using a URL-safe variation of the base64 algorithm but does
not chunk the output. The
+ * url-safe variation emits - and _ instead of + and / characters.
+ * <b>Note: no padding is added.</b>
+ * @param binaryData
+ * binary data to encode
+ * @return String containing Base64 characters
+ * @since 1.4
+ */
+ public static String encodeBase64URLSafeString(final byte[] binaryData) {
+ return StringUtils.newStringUtf8(encodeBase64(binaryData, false, true));
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm and chunks the encoded output into
76 character blocks
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return Base64 characters chunked in 76 character blocks
+ */
+ public static byte[] encodeBase64Chunked(final byte[] binaryData) {
+ return encodeBase64(binaryData, true);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm, optionally chunking the output
into 76 character blocks.
+ *
+ * @param binaryData
+ * Array containing binary data to encode.
+ * @param isChunked
+ * if {@code true} this encoder will chunk the base64 output into 76
character blocks
+ * @return Base64-encoded data.
+ * @throws IllegalArgumentException
+ * Thrown when the input array needs an output array bigger than {@link
Integer#MAX_VALUE}
+ */
+ public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked)
{
+ return encodeBase64(binaryData, isChunked, false);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm, optionally chunking the output
into 76 character blocks.
+ *
+ * @param binaryData
+ * Array containing binary data to encode.
+ * @param isChunked
+ * if {@code true} this encoder will chunk the base64 output into 76
character blocks
+ * @param urlSafe
+ * if {@code true} this encoder will emit - and _ instead of the usual +
and / characters.
+ * <b>Note: no padding is added when encoding using the URL-safe
alphabet.</b>
+ * @return Base64-encoded data.
+ * @throws IllegalArgumentException
+ * Thrown when the input array needs an output array bigger than {@link
Integer#MAX_VALUE}
+ * @since 1.4
+ */
+ public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked,
final boolean urlSafe) {
+ return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm, optionally chunking the output
into 76 character blocks.
+ *
+ * @param binaryData
+ * Array containing binary data to encode.
+ * @param isChunked
+ * if {@code true} this encoder will chunk the base64 output into 76
character blocks
+ * @param urlSafe
+ * if {@code true} this encoder will emit - and _ instead of the usual +
and / characters.
+ * <b>Note: no padding is added when encoding using the URL-safe
alphabet.</b>
+ * @param maxResultSize
+ * The maximum result size to accept.
+ * @return Base64-encoded data.
+ * @throws IllegalArgumentException
+ * Thrown when the input array needs an output array bigger than
maxResultSize
+ * @since 1.4
+ */
+ public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked,
+ final boolean urlSafe, final int maxResultSize) {
+ if (binaryData == null || binaryData.length == 0) {
+ return binaryData;
+ }
+
+ // Create this so can use the super-class method
+ // Also ensures that the same roundings are performed by the ctor and the code
+ final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0,
CHUNK_SEPARATOR, urlSafe);
+ final long len = b64.getEncodedLength(binaryData);
+ if (len > maxResultSize) {
+ throw new IllegalArgumentException("Input array too big, the output
array would be bigger (" +
+ len +
+ ") than the specified maximum size of " +
+ maxResultSize);
+ }
+
+ return b64.encode(binaryData);
+ }
+
+ /**
+ * Decodes a Base64 String into octets
+ *
+ * @param base64String
+ * String containing Base64 data
+ * @return Array containing decoded data.
+ * @since 1.4
+ */
+ public static byte[] decodeBase64(final String base64String) {
+ return new Base64().decode(base64String);
+ }
+
+ /**
+ * Decodes Base64 data into octets
+ *
+ * @param base64Data
+ * Byte array containing Base64 data
+ * @return Array containing decoded data.
+ */
+ public static byte[] decodeBase64(final byte[] base64Data) {
+ return decodeBase64(base64Data, 0, base64Data.length);
+ }
+
+ public static byte[] decodeBase64(
+ final byte[] base64Data, final int off, final int len) {
+ return new Base64().decode(base64Data, off, len);
+ }
+
+ // Implementation of the Encoder Interface
+
+ // Implementation of integer encoding used for crypto
+ /**
+ * Decodes a byte64-encoded integer according to crypto standards such as W3C's
XML-Signature
+ *
+ * @param pArray
+ * a byte array containing base64 character data
+ * @return A BigInteger
+ * @since 1.4
+ */
+ public static BigInteger decodeInteger(final byte[] pArray) {
+ return new BigInteger(1, decodeBase64(pArray));
+ }
+
+ /**
+ * Encodes to a byte64-encoded integer according to crypto standards such as
W3C's XML-Signature
+ *
+ * @param bigInt
+ * a BigInteger
+ * @return A byte array containing base64 character data
+ * @throws NullPointerException
+ * if null is passed in
+ * @since 1.4
+ */
+ public static byte[] encodeInteger(final BigInteger bigInt) {
+ if (bigInt == null) {
+ throw new NullPointerException("encodeInteger called with null
parameter");
+ }
+ return encodeBase64(toIntegerBytes(bigInt), false);
+ }
+
+ /**
+ * Returns a byte-array representation of a <code>BigInteger</code>
without sign bit.
+ *
+ * @param bigInt
+ * <code>BigInteger</code> to be converted
+ * @return a byte array representation of the BigInteger parameter
+ */
+ static byte[] toIntegerBytes(final BigInteger bigInt) {
+ int bitlen = bigInt.bitLength();
+ // round bitlen
+ bitlen = ((bitlen + 7) >> 3) << 3;
+ final byte[] bigBytes = bigInt.toByteArray();
+
+ if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) ==
(bitlen / 8))) {
+ return bigBytes;
+ }
+ // set up params for copying everything but sign bit
+ int startSrc = 0;
+ int len = bigBytes.length;
+
+ // if bigInt is exactly byte-aligned, just skip signbit in copy
+ if ((bigInt.bitLength() % 8) == 0) {
+ startSrc = 1;
+ len--;
+ }
+ final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec
+ final byte[] resizedBytes = new byte[bitlen / 8];
+ System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);
+ return resizedBytes;
+ }
+
+ /**
+ * Returns whether or not the <code>octet</code> is in the Base64
alphabet.
+ *
+ * @param octet
+ * The value to test
+ * @return {@code true} if the value is defined in the the Base64 alphabet {@code
false} otherwise.
+ */
+ @Override
+ protected boolean isInAlphabet(final byte octet) {
+ return octet >= 0 && octet < decodeTable.length &&
decodeTable[octet] != -1;
+ }
+
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/BaseNCodec.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/BaseNCodec.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/BaseNCodec.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,503 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.codec.binary;
+
+import org.apache.tomcat.util.codec.BinaryDecoder;
+import org.apache.tomcat.util.codec.BinaryEncoder;
+import org.apache.tomcat.util.codec.DecoderException;
+import org.apache.tomcat.util.codec.EncoderException;
+
+/**
+ * Abstract superclass for Base-N encoders and decoders.
+ *
+ * <p>
+ * This class is thread-safe.
+ * </p>
+ *
+ * @version $Id: BaseNCodec.java 1459390 2013-03-21 16:38:39Z markt $
+ */
+public abstract class BaseNCodec implements BinaryEncoder, BinaryDecoder {
+
+ /**
+ * Holds thread context so classes can be thread-safe.
+ *
+ * This class is not itself thread-safe; each thread must allocate its own copy.
+ *
+ * @since 1.7
+ */
+ static class Context {
+
+ /**
+ * Place holder for the bytes we're dealing with for our based logic.
+ * Bitwise operations store and extract the encoding or decoding from this
variable.
+ */
+ int ibitWorkArea;
+
+ /**
+ * Place holder for the bytes we're dealing with for our based logic.
+ * Bitwise operations store and extract the encoding or decoding from this
variable.
+ */
+ long lbitWorkArea;
+
+ /**
+ * Buffer for streaming.
+ */
+ byte[] buffer;
+
+ /**
+ * Position where next character should be written in the buffer.
+ */
+ int pos;
+
+ /**
+ * Position where next character should be read from the buffer.
+ */
+ int readPos;
+
+ /**
+ * Boolean flag to indicate the EOF has been reached. Once EOF has been reached,
this object becomes useless,
+ * and must be thrown away.
+ */
+ boolean eof;
+
+ /**
+ * Variable tracks how many characters have been written to the current line.
Only used when encoding. We use
+ * it to make sure each encoded line never goes beyond lineLength (if lineLength
> 0).
+ */
+ int currentLinePos;
+
+ /**
+ * Writes to the buffer only occur after every 3/5 reads when encoding, and every
4/8 reads when decoding. This
+ * variable helps track that.
+ */
+ int modulus;
+
+ Context() {
+ }
+
+ /**
+ * Returns a String useful for debugging (especially within a debugger.)
+ *
+ * @return a String useful for debugging.
+ */
+ @SuppressWarnings("boxing") // OK to ignore boxing here
+ @Override
+ public String toString() {
+ return String.format("%s[buffer=%s, currentLinePos=%s, eof=%s,
ibitWorkArea=%s, lbitWorkArea=%s, " +
+ "modulus=%s, pos=%s, readPos=%s]",
this.getClass().getSimpleName(), buffer, currentLinePos, eof,
+ ibitWorkArea, lbitWorkArea, modulus, pos, readPos);
+ }
+ }
+
+ /**
+ * EOF
+ *
+ * @since 1.7
+ */
+ static final int EOF = -1;
+
+ /**
+ * MIME chunk size per RFC 2045 section 6.8.
+ *
+ * <p>
+ * The {@value} character limit does not count the trailing CRLF, but counts all
other characters, including any
+ * equal signs.
+ * </p>
+ *
+ * @see <a
href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045
section 6.8</a>
+ */
+ public static final int MIME_CHUNK_SIZE = 76;
+
+ /**
+ * PEM chunk size per RFC 1421 section 4.3.2.4.
+ *
+ * <p>
+ * The {@value} character limit does not count the trailing CRLF, but counts all
other characters, including any
+ * equal signs.
+ * </p>
+ *
+ * @see <a
href="http://tools.ietf.org/html/rfc1421">RFC 1421 section
4.3.2.4</a>
+ */
+ public static final int PEM_CHUNK_SIZE = 64;
+
+ private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2;
+
+ /**
+ * Defines the default buffer size - currently {@value}
+ * - must be large enough for at least one encoded block+separator
+ */
+ private static final int DEFAULT_BUFFER_SIZE = 128;
+
+ /** Mask used to extract 8 bits, used in decoding bytes */
+ protected static final int MASK_8BITS = 0xff;
+
+ /**
+ * Byte used to pad output.
+ */
+ protected static final byte PAD_DEFAULT = '='; // Allow static access to
default
+
+ protected final byte PAD = PAD_DEFAULT; // instance variable just in case it needs to
vary later
+
+ /** Number of bytes in each full block of unencoded data, e.g. 4 for Base64 and 5 for
Base32 */
+ private final int unencodedBlockSize;
+
+ /** Number of bytes in each full block of encoded data, e.g. 3 for Base64 and 8 for
Base32 */
+ private final int encodedBlockSize;
+
+ /**
+ * Chunksize for encoding. Not used when decoding.
+ * A value of zero or less implies no chunking of the encoded data.
+ * Rounded down to nearest multiple of encodedBlockSize.
+ */
+ protected final int lineLength;
+
+ /**
+ * Size of chunk separator. Not used unless {@link #lineLength} > 0.
+ */
+ private final int chunkSeparatorLength;
+
+ /**
+ * Note <code>lineLength</code> is rounded down to the nearest multiple
of {@link #encodedBlockSize}
+ * If <code>chunkSeparatorLength</code> is zero, then chunking is
disabled.
+ * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3)
+ * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4)
+ * @param lineLength if > 0, use chunking with a length
<code>lineLength</code>
+ * @param chunkSeparatorLength the chunk separator length, if relevant
+ */
+ protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize,
+ final int lineLength, final int chunkSeparatorLength) {
+ this.unencodedBlockSize = unencodedBlockSize;
+ this.encodedBlockSize = encodedBlockSize;
+ final boolean useChunking = lineLength > 0 && chunkSeparatorLength
> 0;
+ this.lineLength = useChunking ? (lineLength / encodedBlockSize) *
encodedBlockSize : 0;
+ this.chunkSeparatorLength = chunkSeparatorLength;
+ }
+
+ /**
+ * Returns true if this object has buffered data for reading.
+ *
+ * @param context the context to be used
+ * @return true if there is data still available for reading.
+ */
+ boolean hasData(final Context context) { // package protected for access from I/O
streams
+ return context.buffer != null;
+ }
+
+ /**
+ * Returns the amount of buffered data available for reading.
+ *
+ * @param context the context to be used
+ * @return The amount of buffered data available for reading.
+ */
+ int available(final Context context) { // package protected for access from I/O
streams
+ return context.buffer != null ? context.pos - context.readPos : 0;
+ }
+
+ /**
+ * Get the default buffer size. Can be overridden.
+ *
+ * @return {@link #DEFAULT_BUFFER_SIZE}
+ */
+ protected int getDefaultBufferSize() {
+ return DEFAULT_BUFFER_SIZE;
+ }
+
+ /**
+ * Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}.
+ * @param context the context to be used
+ */
+ private byte[] resizeBuffer(final Context context) {
+ if (context.buffer == null) {
+ context.buffer = new byte[getDefaultBufferSize()];
+ context.pos = 0;
+ context.readPos = 0;
+ } else {
+ final byte[] b = new byte[context.buffer.length *
DEFAULT_BUFFER_RESIZE_FACTOR];
+ System.arraycopy(context.buffer, 0, b, 0, context.buffer.length);
+ context.buffer = b;
+ }
+ return context.buffer;
+ }
+
+ /**
+ * Ensure that the buffer has room for <code>size</code> bytes
+ *
+ * @param size minimum spare space required
+ * @param context the context to be used
+ */
+ protected byte[] ensureBufferSize(final int size, final Context context){
+ if ((context.buffer == null) || (context.buffer.length < context.pos +
size)){
+ return resizeBuffer(context);
+ }
+ return context.buffer;
+ }
+
+ /**
+ * Extracts buffered data into the provided byte[] array, starting at position bPos,
up to a maximum of bAvail
+ * bytes. Returns how many bytes were actually extracted.
+ * <p>
+ * Package protected for access from I/O streams.
+ *
+ * @param b
+ * byte[] array to extract the buffered data into.
+ * @param bPos
+ * position in byte[] array to start extraction at.
+ * @param bAvail
+ * amount of bytes we're allowed to extract. We may extract fewer (if
fewer are available).
+ * @param context
+ * the context to be used
+ * @return The number of bytes successfully extracted into the provided byte[]
array.
+ */
+ int readResults(final byte[] b, final int bPos, final int bAvail, final Context
context) {
+ if (context.buffer != null) {
+ final int len = Math.min(available(context), bAvail);
+ System.arraycopy(context.buffer, context.readPos, b, bPos, len);
+ context.readPos += len;
+ if (context.readPos >= context.pos) {
+ context.buffer = null; // so hasData() will return false, and this method
can return -1
+ }
+ return len;
+ }
+ return context.eof ? EOF : 0;
+ }
+
+ /**
+ * Checks if a byte value is whitespace or not.
+ * Whitespace is taken to mean: space, tab, CR, LF
+ * @param byteToCheck
+ * the byte to check
+ * @return true if byte is whitespace, false otherwise
+ */
+ protected static boolean isWhiteSpace(final byte byteToCheck) {
+ switch (byteToCheck) {
+ case ' ' :
+ case '\n' :
+ case '\r' :
+ case '\t' :
+ return true;
+ default :
+ return false;
+ }
+ }
+
+ /**
+ * Encodes an Object using the Base-N algorithm. This method is provided in order to
satisfy the requirements of
+ * the Encoder interface, and will throw an EncoderException if the supplied object
is not of type byte[].
+ *
+ * @param obj
+ * Object to encode
+ * @return An object (of type byte[]) containing the Base-N encoded data which
corresponds to the byte[] supplied.
+ * @throws EncoderException
+ * if the parameter supplied is not of type byte[]
+ */
+ @Override
+ public Object encode(final Object obj) throws EncoderException {
+ if (!(obj instanceof byte[])) {
+ throw new EncoderException("Parameter supplied to Base-N encode is not a
byte[]");
+ }
+ return encode((byte[]) obj);
+ }
+
+ /**
+ * Encodes a byte[] containing binary data, into a String containing characters in
the Base-N alphabet.
+ * Uses UTF8 encoding.
+ *
+ * @param pArray
+ * a byte array containing binary data
+ * @return A String containing only Base-N character data
+ */
+ public String encodeToString(final byte[] pArray) {
+ return StringUtils.newStringUtf8(encode(pArray));
+ }
+
+ /**
+ * Encodes a byte[] containing binary data, into a String containing characters in
the appropriate alphabet.
+ * Uses UTF8 encoding.
+ *
+ * @param pArray a byte array containing binary data
+ * @return String containing only character data in the appropriate alphabet.
+ */
+ public String encodeAsString(final byte[] pArray){
+ return StringUtils.newStringUtf8(encode(pArray));
+ }
+
+ /**
+ * Decodes an Object using the Base-N algorithm. This method is provided in order to
satisfy the requirements of
+ * the Decoder interface, and will throw a DecoderException if the supplied object is
not of type byte[] or String.
+ *
+ * @param obj
+ * Object to decode
+ * @return An object (of type byte[]) containing the binary data which corresponds to
the byte[] or String
+ * supplied.
+ * @throws DecoderException
+ * if the parameter supplied is not of type byte[]
+ */
+ @Override
+ public Object decode(final Object obj) throws DecoderException {
+ if (obj instanceof byte[]) {
+ return decode((byte[]) obj);
+ } else if (obj instanceof String) {
+ return decode((String) obj);
+ } else {
+ throw new DecoderException("Parameter supplied to Base-N decode is not a
byte[] or a String");
+ }
+ }
+
+ /**
+ * Decodes a String containing characters in the Base-N alphabet.
+ *
+ * @param pArray
+ * A String containing Base-N character data
+ * @return a byte array containing binary data
+ */
+ public byte[] decode(final String pArray) {
+ return decode(StringUtils.getBytesUtf8(pArray));
+ }
+
+ /**
+ * Decodes a byte[] containing characters in the Base-N alphabet.
+ *
+ * @param pArray
+ * A byte array containing Base-N character data
+ * @return a byte array containing binary data
+ */
+ @Override
+ public byte[] decode(final byte[] pArray) {
+ return decode(pArray, 0, pArray.length);
+ }
+
+ public byte[] decode(final byte[] pArray, final int off, final int len) {
+ if (pArray == null || len == 0) {
+ return new byte[0];
+ }
+ final Context context = new Context();
+ decode(pArray, off, len, context);
+ decode(pArray, off, EOF, context); // Notify decoder of EOF.
+ final byte[] result = new byte[context.pos];
+ readResults(result, 0, result.length, context);
+ return result;
+ }
+
+ /**
+ * Encodes a byte[] containing binary data, into a byte[] containing characters in
the alphabet.
+ *
+ * @param pArray
+ * a byte array containing binary data
+ * @return A byte array containing only the basen alphabetic character data
+ */
+ @Override
+ public byte[] encode(final byte[] pArray) {
+ if (pArray == null || pArray.length == 0) {
+ return pArray;
+ }
+ final Context context = new Context();
+ encode(pArray, 0, pArray.length, context);
+ encode(pArray, 0, EOF, context); // Notify encoder of EOF.
+ final byte[] buf = new byte[context.pos - context.readPos];
+ readResults(buf, 0, buf.length, context);
+ return buf;
+ }
+
+ // package protected for access from I/O streams
+ abstract void encode(byte[] pArray, int i, int length, Context context);
+
+ // package protected for access from I/O streams
+ abstract void decode(byte[] pArray, int i, int length, Context context);
+
+ /**
+ * Returns whether or not the <code>octet</code> is in the current
alphabet.
+ * Does not allow whitespace or pad.
+ *
+ * @param value The value to test
+ *
+ * @return {@code true} if the value is defined in the current alphabet, {@code
false} otherwise.
+ */
+ protected abstract boolean isInAlphabet(byte value);
+
+ /**
+ * Tests a given byte array to see if it contains only valid characters within the
alphabet.
+ * The method optionally treats whitespace and pad as valid.
+ *
+ * @param arrayOctet byte array to test
+ * @param allowWSPad if {@code true}, then whitespace and PAD are also allowed
+ *
+ * @return {@code true} if all bytes are valid characters in the alphabet or if the
byte array is empty;
+ * {@code false}, otherwise
+ */
+ public boolean isInAlphabet(final byte[] arrayOctet, final boolean allowWSPad) {
+ for (int i = 0; i < arrayOctet.length; i++) {
+ if (!isInAlphabet(arrayOctet[i]) &&
+ (!allowWSPad || (arrayOctet[i] != PAD) &&
!isWhiteSpace(arrayOctet[i]))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Tests a given String to see if it contains only valid characters within the
alphabet.
+ * The method treats whitespace and PAD as valid.
+ *
+ * @param basen String to test
+ * @return {@code true} if all characters in the String are valid characters in the
alphabet or if
+ * the String is empty; {@code false}, otherwise
+ * @see #isInAlphabet(byte[], boolean)
+ */
+ public boolean isInAlphabet(final String basen) {
+ return isInAlphabet(StringUtils.getBytesUtf8(basen), true);
+ }
+
+ /**
+ * Tests a given byte array to see if it contains any characters within the alphabet
or PAD.
+ *
+ * Intended for use in checking line-ending arrays
+ *
+ * @param arrayOctet
+ * byte array to test
+ * @return {@code true} if any byte is a valid character in the alphabet or PAD;
{@code false} otherwise
+ */
+ protected boolean containsAlphabetOrPad(final byte[] arrayOctet) {
+ if (arrayOctet == null) {
+ return false;
+ }
+ for (final byte element : arrayOctet) {
+ if (PAD == element || isInAlphabet(element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Calculates the amount of space needed to encode the supplied array.
+ *
+ * @param pArray byte[] array which will later be encoded
+ *
+ * @return amount of space needed to encoded the supplied array.
+ * Returns a long since a max-len array will require > Integer.MAX_VALUE
+ */
+ public long getEncodedLength(final byte[] pArray) {
+ // Calculate non-chunked size - rounded up to allow for padding
+ // cast to long is needed to avoid possibility of overflow
+ long len = ((pArray.length + unencodedBlockSize-1) / unencodedBlockSize) *
(long) encodedBlockSize;
+ if (lineLength > 0) { // We're using chunking
+ // Round up to nearest multiple
+ len += ((len + lineLength-1) / lineLength) * chunkSeparatorLength;
+ }
+ return len;
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/StringUtils.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/StringUtils.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/StringUtils.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.codec.binary;
+
+import java.nio.charset.Charset;
+
+import org.apache.tomcat.util.buf.EncodingToCharset;
+
+/**
+ * Converts String to and from bytes using the encodings required by the Java
specification. These encodings are
+ * specified in <a
href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/...
+ * Standard charsets</a>.
+ *
+ * <p>This class is immutable and thread-safe.</p>
+ *
+ * @see <a
href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/...
charsets</a>
+ * @version $Id: StringUtils.java 1459218 2013-03-21 10:31:50Z markt $
+ * @since 1.4
+ */
+public class StringUtils {
+
+ /**
+ * Calls {@link String#getBytes(Charset)}
+ *
+ * @param string
+ * The string to encode (if null, return null).
+ * @param charset
+ * The {@link Charset} to encode the {@code String}
+ * @return the encoded bytes
+ */
+ private static byte[] getBytes(final String string, final Charset charset) {
+ if (string == null) {
+ return null;
+ }
+ return string.getBytes(charset);
+ }
+
+ /**
+ * Encodes the given string into a sequence of bytes using the UTF-8 charset, storing
the result into a new byte
+ * array.
+ *
+ * @param string
+ * the String to encode, may be {@code null}
+ * @return encoded bytes, or {@code null} if the input string was {@code null}
+ * @see <a
href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/...
charsets</a>
+ */
+ public static byte[] getBytesUtf8(final String string) {
+ return getBytes(string, EncodingToCharset.UTF_8);
+ }
+
+ /**
+ * Constructs a new <code>String</code> by decoding the specified array
of bytes using the given charset.
+ *
+ * @param bytes
+ * The bytes to be decoded into characters
+ * @param charset
+ * The {@link Charset} to encode the {@code String}
+ * @return A new <code>String</code> decoded from the specified array of
bytes using the given charset,
+ * or {@code null} if the input byte array was {@code null}.
+ */
+ private static String newString(final byte[] bytes, final Charset charset) {
+ return bytes == null ? null : new String(bytes, charset);
+ }
+
+ /**
+ * Constructs a new <code>String</code> by decoding the specified array
of bytes using the UTF-8 charset.
+ *
+ * @param bytes
+ * The bytes to be decoded into characters
+ * @return A new <code>String</code> decoded from the specified array of
bytes using the UTF-8 charset,
+ * or {@code null} if the input byte array was {@code null}.
+ */
+ public static String newStringUtf8(final byte[] bytes) {
+ return newString(bytes, EncodingToCharset.UTF_8);
+ }
+
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/package.html
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/package.html
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/util/codec/binary/package.html 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,21 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+
http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<html>
+ <body>
+ Base64, Base32, Binary, and Hexadecimal String encoding and decoding.
+ </body>
+</html>
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/Constants.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/Constants.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/Constants.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.threads;
+
+/**
+ * Static constants for this package.
+ */
+public final class Constants {
+
+ public static final String Package = "org.apache.tomcat.util.threads";
+
+ public static final long DEFAULT_THREAD_RENEWAL_DELAY = 1000L;
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/TaskQueue.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/TaskQueue.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/TaskQueue.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.threads;
+
+import java.util.Collection;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * As task queue specifically designed to run with a thread pool executor.
+ * The task queue is optimised to properly utilize threads within
+ * a thread pool executor. If you use a normal queue, the executor will spawn threads
+ * when there are idle threads and you wont be able to force items unto the queue itself
+ * @author fhanik
+ *
+ */
+public class TaskQueue extends LinkedBlockingQueue<Runnable> {
+
+ private static final long serialVersionUID = 1L;
+
+ private ThreadPoolExecutor parent = null;
+
+ // no need to be volatile, the one times when we change and read it occur in
+ // a single thread (the one that did stop a context and fired listeners)
+ private Integer forcedRemainingCapacity = null;
+
+ public TaskQueue() {
+ super();
+ }
+
+ public TaskQueue(int capacity) {
+ super(capacity);
+ }
+
+ public TaskQueue(Collection<? extends Runnable> c) {
+ super(c);
+ }
+
+ public void setParent(ThreadPoolExecutor tp) {
+ parent = tp;
+ }
+
+ public boolean force(Runnable o) {
+ if ( parent.isShutdown() ) throw new RejectedExecutionException("Executor
not running, can't force a command into the queue");
+ return super.offer(o); //forces the item onto the queue, to be used if the task
is rejected
+ }
+
+ public boolean force(Runnable o, long timeout, TimeUnit unit) throws
InterruptedException {
+ if ( parent.isShutdown() ) throw new RejectedExecutionException("Executor
not running, can't force a command into the queue");
+ return super.offer(o,timeout,unit); //forces the item onto the queue, to be used
if the task is rejected
+ }
+
+ @Override
+ public boolean offer(Runnable o) {
+ //we can't do any checks
+ if (parent==null) return super.offer(o);
+ //we are maxed out on threads, simply queue the object
+ if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
+ //we have idle threads, just add it to the queue
+ if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o);
+ //if we have less threads than maximum force creation of a new thread
+ if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
+ //if we reached here, we need to add it to the queue
+ return super.offer(o);
+ }
+
+
+ @Override
+ public Runnable poll(long timeout, TimeUnit unit)
+ throws InterruptedException {
+ Runnable runnable = super.poll(timeout, unit);
+ if (runnable == null && parent != null) {
+ // the poll timed out, it gives an opportunity to stop the current
+ // thread if needed to avoid memory leaks.
+ parent.stopCurrentThreadIfNeeded();
+ }
+ return runnable;
+ }
+
+ @Override
+ public Runnable take() throws InterruptedException {
+ if (parent != null && parent.currentThreadShouldBeStopped()) {
+ return poll(parent.getKeepAliveTime(TimeUnit.MILLISECONDS),
+ TimeUnit.MILLISECONDS);
+ // yes, this may return null (in case of timeout) which normally
+ // does not occur with take()
+ // but the ThreadPoolExecutor implementation allows this
+ }
+ return super.take();
+ }
+
+ @Override
+ public int remainingCapacity() {
+ if (forcedRemainingCapacity != null) {
+ // ThreadPoolExecutor.setCorePoolSize checks that
+ // remainingCapacity==0 to allow to interrupt idle threads
+ // I don't see why, but this hack allows to conform to this
+ // "requirement"
+ return forcedRemainingCapacity.intValue();
+ }
+ return super.remainingCapacity();
+ }
+
+ public void setForcedRemainingCapacity(Integer forcedRemainingCapacity) {
+ this.forcedRemainingCapacity = forcedRemainingCapacity;
+ }
+
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/TaskThread.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/TaskThread.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/TaskThread.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.threads;
+
+/**
+ * A Thread implementation that records the time at which it was created.
+ *
+ */
+public class TaskThread extends Thread {
+
+ private final long creationTime;
+
+ public TaskThread(ThreadGroup group, Runnable target, String name) {
+ super(group, target, name);
+ this.creationTime = System.currentTimeMillis();
+ }
+
+ public TaskThread(ThreadGroup group, Runnable target, String name,
+ long stackSize) {
+ super(group, target, name, stackSize);
+ this.creationTime = System.currentTimeMillis();
+ }
+
+ /**
+ * @return the time (in ms) at which this thread was created
+ */
+ public final long getCreationTime() {
+ return creationTime;
+ }
+
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.threads;
+
+import static org.jboss.web.CoyoteMessages.MESSAGES;
+
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.jboss.web.CoyoteLogger;
+
+/**
+ * Same as a java.util.concurrent.ThreadPoolExecutor but implements a much more
efficient
+ * {@link #getSubmittedCount()} method, to be used to properly handle the work queue.
+ * If a RejectedExecutionHandler is not specified a default one will be configured
+ * and that one will always throw a RejectedExecutionException
+ * @author fhanik
+ *
+ */
+public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
+
+ /**
+ * The number of tasks submitted but not yet finished. This includes tasks
+ * in the queue and tasks that have been handed to a worker thread but the
+ * latter did not start executing the task yet.
+ * This number is always greater or equal to {@link #getActiveCount()}.
+ */
+ private final AtomicInteger submittedCount = new AtomicInteger(0);
+ private final AtomicLong lastContextStoppedTime = new AtomicLong(0L);
+
+ /**
+ * Most recent time in ms when a thread decided to kill itself to avoid
+ * potential memory leaks. Useful to throttle the rate of renewals of
+ * threads.
+ */
+ private final AtomicLong lastTimeThreadKilledItself = new AtomicLong(0L);
+
+ /**
+ * Delay in ms between 2 threads being renewed. If negative, do not renew threads.
+ */
+ private long threadRenewalDelay = Constants.DEFAULT_THREAD_RENEWAL_DELAY;
+
+ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
{
+ super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
+ }
+
+ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
+ RejectedExecutionHandler handler) {
+ super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, handler);
+ }
+
+ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
+ super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, new RejectHandler());
+ }
+
+ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
+ super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new
RejectHandler());
+ }
+
+ public long getThreadRenewalDelay() {
+ return threadRenewalDelay;
+ }
+
+ public void setThreadRenewalDelay(long threadRenewalDelay) {
+ this.threadRenewalDelay = threadRenewalDelay;
+ }
+
+ @Override
+ protected void afterExecute(Runnable r, Throwable t) {
+ submittedCount.decrementAndGet();
+
+ if (t == null) {
+ stopCurrentThreadIfNeeded();
+ }
+ }
+
+ /**
+ * If the current thread was started before the last time when a context was
+ * stopped, an exception is thrown so that the current thread is stopped.
+ */
+ protected void stopCurrentThreadIfNeeded() {
+ if (currentThreadShouldBeStopped()) {
+ long lastTime = lastTimeThreadKilledItself.longValue();
+ if (lastTime + threadRenewalDelay < System.currentTimeMillis()) {
+ if (lastTimeThreadKilledItself.compareAndSet(lastTime,
+ System.currentTimeMillis() + 1)) {
+ // OK, it's really time to dispose of this thread
+
+ final String msg =
MESSAGES.threadStopped(Thread.currentThread().getName());
+
+ Thread.currentThread().setUncaughtExceptionHandler(
+ new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread t,
+ Throwable e) {
+ // yes, swallow the exception
+ CoyoteLogger.UTIL_LOGGER.debug(msg);
+ }
+ });
+ throw new RuntimeException(msg);
+ }
+ }
+ }
+ }
+
+ protected boolean currentThreadShouldBeStopped() {
+ if (threadRenewalDelay >= 0
+ && Thread.currentThread() instanceof TaskThread) {
+ TaskThread currentTaskThread = (TaskThread) Thread.currentThread();
+ if (currentTaskThread.getCreationTime() <
+ this.lastContextStoppedTime.longValue()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public int getSubmittedCount() {
+ return submittedCount.get();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void execute(Runnable command) {
+ execute(command,0,TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Executes the given command at some time in the future. The command
+ * may execute in a new thread, in a pooled thread, or in the calling
+ * thread, at the discretion of the <tt>Executor</tt> implementation.
+ * If no threads are available, it will be added to the work queue.
+ * If the work queue is full, the system will wait for the specified
+ * time and it throw a RejectedExecutionException if the queue is still
+ * full after that.
+ *
+ * @param command the runnable task
+ * @throws RejectedExecutionException if this task cannot be
+ * accepted for execution - the queue is full
+ * @throws NullPointerException if command or unit is null
+ */
+ public void execute(Runnable command, long timeout, TimeUnit unit) {
+ submittedCount.incrementAndGet();
+ try {
+ super.execute(command);
+ } catch (RejectedExecutionException rx) {
+ if (super.getQueue() instanceof TaskQueue) {
+ final TaskQueue queue = (TaskQueue)super.getQueue();
+ try {
+ if (!queue.force(command, timeout, unit)) {
+ submittedCount.decrementAndGet();
+ throw new RejectedExecutionException("Queue capacity is
full.");
+ }
+ } catch (InterruptedException x) {
+ submittedCount.decrementAndGet();
+ Thread.interrupted();
+ throw new RejectedExecutionException(x);
+ }
+ } else {
+ submittedCount.decrementAndGet();
+ throw rx;
+ }
+
+ }
+ }
+
+ public void contextStopping() {
+ this.lastContextStoppedTime.set(System.currentTimeMillis());
+
+ // save the current pool parameters to restore them later
+ int savedCorePoolSize = this.getCorePoolSize();
+ TaskQueue taskQueue =
+ getQueue() instanceof TaskQueue ? (TaskQueue) getQueue() : null;
+ if (taskQueue != null) {
+ // note by slaurent : quite oddly threadPoolExecutor.setCorePoolSize
+ // checks that queue.remainingCapacity()==0. I did not understand
+ // why, but to get the intended effect of waking up idle threads, I
+ // temporarily fake this condition.
+ taskQueue.setForcedRemainingCapacity(Integer.valueOf(0));
+ }
+
+ // setCorePoolSize(0) wakes idle threads
+ this.setCorePoolSize(0);
+
+ // wait a little so that idle threads wake and poll the queue again,
+ // this time always with a timeout (queue.poll() instead of
+ // queue.take())
+ // even if we did not wait enough, TaskQueue.take() takes care of timing
+ // out, so that we are sure that all threads of the pool are renewed in
+ // a limited time, something like
+ // (threadKeepAlive + longest request time)
+ try {
+ Thread.sleep(200L);
+ } catch (InterruptedException e) {
+ // yes, ignore
+ }
+
+ if (taskQueue != null) {
+ // ok, restore the state of the queue and pool
+ taskQueue.setForcedRemainingCapacity(null);
+ }
+ this.setCorePoolSize(savedCorePoolSize);
+ }
+
+ private static class RejectHandler implements RejectedExecutionHandler {
+ @Override
+ public void rejectedExecution(Runnable r,
+ java.util.concurrent.ThreadPoolExecutor executor) {
+ throw new RejectedExecutionException();
+ }
+
+ }
+
+
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapper.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.nio.ByteBuffer;
+import java.nio.channels.CompletionHandler;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLException;
+
+/**
+ * This is a wrapper for a {@link java.nio.channels.AsynchronousSocketChannel}
+ * that limits the methods available thereby simplifying the process of
+ * implementing SSL/TLS support since there are fewer methods to intercept.
+ */
+public interface AsyncChannelWrapper {
+
+ Future<Integer> read(ByteBuffer dst);
+
+ <B,A extends B> void read(ByteBuffer dst, A attachment,
+ CompletionHandler<Integer,B> handler);
+
+ Future<Integer> write(ByteBuffer src);
+
+ <B,A extends B> void write(ByteBuffer[] srcs, int offset, int length,
+ long timeout, TimeUnit unit, A attachment,
+ CompletionHandler<Long,B> handler);
+
+ void close();
+
+ Future<Void> handshake() throws SSLException;
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapperNonSecure.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.channels.CompletionHandler;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Generally, just passes calls straight to the wrapped
+ * {@link AsynchronousSocketChannel}. In some cases exceptions may be swallowed
+ * to save them being swallowed by the calling code.
+ */
+public class AsyncChannelWrapperNonSecure implements AsyncChannelWrapper {
+
+ private static final Future<Void> NOOP_FUTURE = new NoOpFuture();
+
+ private final AsynchronousSocketChannel socketChannel;
+
+ public AsyncChannelWrapperNonSecure(
+ AsynchronousSocketChannel socketChannel) {
+ this.socketChannel = socketChannel;
+ }
+
+ @Override
+ public Future<Integer> read(ByteBuffer dst) {
+ return socketChannel.read(dst);
+ }
+
+ @Override
+ public <B,A extends B> void read(ByteBuffer dst, A attachment,
+ CompletionHandler<Integer,B> handler) {
+ socketChannel.read(dst, attachment, handler);
+ }
+
+ @Override
+ public Future<Integer> write(ByteBuffer src) {
+ return socketChannel.write(src);
+ }
+
+ @Override
+ public <B,A extends B> void write(ByteBuffer[] srcs, int offset, int length,
+ long timeout, TimeUnit unit, A attachment,
+ CompletionHandler<Long,B> handler) {
+ socketChannel.write(
+ srcs, offset, length, timeout, unit, attachment, handler);
+ }
+
+ @Override
+ public void close() {
+ try {
+ socketChannel.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+
+ @Override
+ public Future<Void> handshake() {
+ return NOOP_FUTURE;
+ }
+
+
+ private static final class NoOpFuture implements Future<Void> {
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
+
+ @Override
+ public Void get() throws InterruptedException, ExecutionException {
+ return null;
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException,
+ TimeoutException {
+ return null;
+ }
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/AsyncChannelWrapperSecure.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,541 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.channels.CompletionHandler;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.SSLException;
+
+import org.jboss.web.WebsocketsLogger;
+
+/**
+ * Wraps the {@link AsynchronousSocketChannel} with SSL/TLS. This needs a lot
+ * more testing before it can be considered robust.
+ */
+public class AsyncChannelWrapperSecure implements AsyncChannelWrapper {
+
+ private static final ByteBuffer DUMMY = ByteBuffer.allocate(8192);
+ private final AsynchronousSocketChannel socketChannel;
+ private final SSLEngine sslEngine;
+ private final ByteBuffer socketReadBuffer;
+ private final ByteBuffer socketWriteBuffer;
+ // One thread for read, one for write
+ private final ExecutorService executor = Executors.newFixedThreadPool(2);
+ private AtomicBoolean writing = new AtomicBoolean(false);
+ private AtomicBoolean reading = new AtomicBoolean(false);
+
+ public AsyncChannelWrapperSecure(AsynchronousSocketChannel socketChannel,
+ SSLEngine sslEngine) {
+ this.socketChannel = socketChannel;
+ this.sslEngine = sslEngine;
+
+ int socketBufferSize = sslEngine.getSession().getPacketBufferSize();
+ socketReadBuffer = ByteBuffer.allocateDirect(socketBufferSize);
+ socketWriteBuffer = ByteBuffer.allocateDirect(socketBufferSize);
+ }
+
+ @Override
+ public Future<Integer> read(ByteBuffer dst) {
+ WrapperFuture<Integer,Void> future = new WrapperFuture<Integer,
Void>();
+
+ if (!reading.compareAndSet(false, true)) {
+ throw MESSAGES.invalidConcurrentRead();
+ }
+
+ ReadTask readTask = new ReadTask(dst, future);
+
+ executor.execute(readTask);
+
+ return future;
+ }
+
+ @Override
+ public <B,A extends B> void read(ByteBuffer dst, A attachment,
+ CompletionHandler<Integer,B> handler) {
+
+ WrapperFuture<Integer,B> future =
+ new WrapperFuture<Integer, B>(handler, attachment);
+
+ if (!reading.compareAndSet(false, true)) {
+ throw MESSAGES.invalidConcurrentRead();
+ }
+
+ ReadTask readTask = new ReadTask(dst, future);
+
+ executor.execute(readTask);
+ }
+
+ @Override
+ public Future<Integer> write(ByteBuffer src) {
+
+ WrapperFuture<Long,Void> inner = new WrapperFuture<Long, Void>();
+
+ if (!writing.compareAndSet(false, true)) {
+ throw MESSAGES.invalidConcurrentWrite();
+ }
+
+ WriteTask writeTask =
+ new WriteTask(new ByteBuffer[] {src}, 0, 1, inner);
+
+ executor.execute(writeTask);
+
+ Future<Integer> future = new LongToIntegerFuture(inner);
+ return future;
+ }
+
+ @Override
+ public <B,A extends B> void write(ByteBuffer[] srcs, int offset, int length,
+ long timeout, TimeUnit unit, A attachment,
+ CompletionHandler<Long,B> handler) {
+
+ WrapperFuture<Long,B> future =
+ new WrapperFuture<Long, B>(handler, attachment);
+
+ if (!writing.compareAndSet(false, true)) {
+ throw MESSAGES.invalidConcurrentWrite();
+ }
+
+ WriteTask writeTask = new WriteTask(srcs, offset, length, future);
+
+ executor.execute(writeTask);
+ }
+
+ @Override
+ public void close() {
+ try {
+ socketChannel.close();
+ } catch (IOException e) {
+ WebsocketsLogger.ROOT_LOGGER.errorClose();
+ }
+ }
+
+ @Override
+ public Future<Void> handshake() throws SSLException {
+
+ WrapperFuture<Void,Void> wFuture = new WrapperFuture<Void, Void>();
+
+ Thread t = new WebSocketSslHandshakeThread(wFuture);
+ t.start();
+
+ return wFuture;
+ }
+
+
+ private class WriteTask implements Runnable {
+
+ private final ByteBuffer[] srcs;
+ private final int offset;
+ private final int length;
+ private final WrapperFuture<Long,?> future;
+
+ public WriteTask(ByteBuffer[] srcs, int offset, int length,
+ WrapperFuture<Long,?> future) {
+ this.srcs = srcs;
+ this.future = future;
+ this.offset = offset;
+ this.length = length;
+ }
+
+ @Override
+ public void run() {
+ long written = 0;
+
+ try {
+ for (int i = offset; i < offset + length; i++) {
+ ByteBuffer src = srcs[i];
+ while (src.hasRemaining()) {
+ socketWriteBuffer.clear();
+
+ // Encrypt the data
+ SSLEngineResult r = sslEngine.wrap(src, socketWriteBuffer);
+ written += r.bytesConsumed();
+ Status s = r.getStatus();
+
+ if (s == Status.OK || s == Status.BUFFER_OVERFLOW) {
+ // Need to write out the bytes and may need to read from
+ // the source again to empty it
+ } else {
+ // Status.BUFFER_UNDERFLOW - only happens on unwrap
+ // Status.CLOSED - unexpected
+ throw MESSAGES.unexpectedStatusAfterWrap();
+ }
+
+ // Check for tasks
+ if (r.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+ Runnable runnable = sslEngine.getDelegatedTask();
+ while (runnable != null) {
+ runnable.run();
+ runnable = sslEngine.getDelegatedTask();
+ }
+ }
+
+ socketWriteBuffer.flip();
+
+ // Do the write
+ int toWrite = r.bytesProduced();
+ while (toWrite > 0) {
+ Future<Integer> f =
+ socketChannel.write(socketWriteBuffer);
+ Integer socketWrite = f.get();
+ toWrite -= socketWrite.intValue();
+ }
+ }
+ }
+
+
+ if (writing.compareAndSet(true, false)) {
+ future.complete(Long.valueOf(written));
+ } else {
+ future.fail(MESSAGES.invalidWriteState());
+ }
+ } catch (Exception e) {
+ future.fail(e);
+ }
+ }
+ }
+
+
+ private class ReadTask implements Runnable {
+
+ private final ByteBuffer dest;
+ private final WrapperFuture<Integer,?> future;
+
+ public ReadTask(ByteBuffer dest, WrapperFuture<Integer,?> future) {
+ this.dest = dest;
+ this.future = future;
+ }
+
+ @Override
+ public void run() {
+ int read = 0;
+
+ boolean forceRead = false;
+
+ try {
+ while (read == 0) {
+ socketReadBuffer.compact();
+
+ if (forceRead) {
+ Future<Integer> f =
+ socketChannel.read(socketReadBuffer);
+ Integer socketRead = f.get();
+ if (socketRead.intValue() == -1) {
+ throw new EOFException(MESSAGES.unexpectedEndOfStream());
+ }
+ }
+
+ socketReadBuffer.flip();
+
+ if (socketReadBuffer.hasRemaining()) {
+ // Decrypt the data in the buffer
+ SSLEngineResult r =
+ sslEngine.unwrap(socketReadBuffer, dest);
+ read += r.bytesProduced();
+ Status s = r.getStatus();
+
+ if (s == Status.OK) {
+ // Bytes available for reading and there may be
+ // sufficient data in the socketReadBuffer to
+ // support further reads without reading from the
+ // socket
+ } else if (s == Status.BUFFER_UNDERFLOW) {
+ // There is partial data in the socketReadBuffer
+ if (read == 0) {
+ // Need more data before the partial data can be
+ // processed and some output generated
+ forceRead = true;
+ }
+ // else return the data we have and deal with the
+ // partial data on the next read
+ } else if (s == Status.BUFFER_OVERFLOW) {
+ // Not enough space in the destination buffer to
+ // store all of the data. We could use a bytes read
+ // value of -bufferSizeRequired to signal the new
+ // buffer size required but an explicit exception is
+ // clearer.
+ if (reading.compareAndSet(true, false)) {
+ throw new ReadBufferOverflowException(sslEngine.
+ getSession().getApplicationBufferSize());
+ } else {
+ future.fail(MESSAGES.invalidReadState());
+ }
+ } else {
+ // Status.CLOSED - unexpected
+ throw MESSAGES.unexpectedStatusAfterUnwrap();
+ }
+
+ // Check for tasks
+ if (r.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+ Runnable runnable = sslEngine.getDelegatedTask();
+ while (runnable != null) {
+ runnable.run();
+ runnable = sslEngine.getDelegatedTask();
+ }
+ }
+ } else {
+ forceRead = true;
+ }
+ }
+
+
+ if (reading.compareAndSet(true, false)) {
+ future.complete(Integer.valueOf(read));
+ } else {
+ future.fail(MESSAGES.invalidReadState());
+ }
+ } catch (Exception e) {
+ future.fail(e);
+ }
+ }
+ }
+
+
+ private class WebSocketSslHandshakeThread extends Thread {
+
+ private final WrapperFuture<Void,Void> hFuture;
+
+ private HandshakeStatus handshakeStatus;
+ private Status resultStatus;
+
+ public WebSocketSslHandshakeThread(WrapperFuture<Void,Void> hFuture) {
+ this.hFuture = hFuture;
+ }
+
+ @Override
+ public void run() {
+ try {
+ sslEngine.beginHandshake();
+ // So the first compact does the right thing
+ socketReadBuffer.position(socketReadBuffer.limit());
+
+ handshakeStatus = sslEngine.getHandshakeStatus();
+ resultStatus = Status.OK;
+
+ boolean handshaking = true;
+
+ while(handshaking) {
+ switch (handshakeStatus) {
+ case NEED_WRAP: {
+ socketWriteBuffer.clear();
+ SSLEngineResult r =
+ sslEngine.wrap(DUMMY, socketWriteBuffer);
+ checkResult(r, true);
+ socketWriteBuffer.flip();
+ Future<Integer> fWrite =
+ socketChannel.write(socketWriteBuffer);
+ fWrite.get();
+ break;
+ }
+ case NEED_UNWRAP: {
+ socketReadBuffer.compact();
+ if (socketReadBuffer.position() == 0 ||
+ resultStatus == Status.BUFFER_UNDERFLOW) {
+ Future<Integer> fRead =
+ socketChannel.read(socketReadBuffer);
+ fRead.get();
+ }
+ socketReadBuffer.flip();
+ SSLEngineResult r =
+ sslEngine.unwrap(socketReadBuffer, DUMMY);
+ checkResult(r, false);
+ break;
+ }
+ case NEED_TASK: {
+ Runnable r = null;
+ while ((r = sslEngine.getDelegatedTask()) != null) {
+ r.run();
+ }
+ handshakeStatus = sslEngine.getHandshakeStatus();
+ break;
+ }
+ case FINISHED: {
+ handshaking = false;
+ break;
+ }
+ default: {
+ throw new SSLException("TODO");
+ }
+ }
+ }
+ } catch (SSLException e) {
+ hFuture.fail(e);
+ } catch (InterruptedException e) {
+ hFuture.fail(e);
+ } catch (ExecutionException e) {
+ hFuture.fail(e);
+ }
+
+ hFuture.complete(null);
+ }
+
+ private void checkResult(SSLEngineResult result, boolean wrap)
+ throws SSLException {
+
+ handshakeStatus = result.getHandshakeStatus();
+ resultStatus = result.getStatus();
+
+ if (resultStatus != Status.OK &&
+ (wrap || resultStatus != Status.BUFFER_UNDERFLOW)) {
+ throw new SSLException("TODO");
+ }
+ if (wrap && result.bytesConsumed() != 0) {
+ throw new SSLException("TODO");
+ }
+ if (!wrap && result.bytesProduced() != 0) {
+ throw new SSLException("TODO");
+ }
+ }
+ }
+
+
+ private static class WrapperFuture<T,A> implements Future<T> {
+
+ private final CompletionHandler<T,A> handler;
+ private final A attachment;
+
+ private volatile T result = null;
+ private volatile Throwable throwable = null;
+ private CountDownLatch completionLatch = new CountDownLatch(1);
+
+ public WrapperFuture() {
+ this(null, null);
+ }
+
+ public WrapperFuture(CompletionHandler<T,A> handler, A attachment) {
+ this.handler = handler;
+ this.attachment = attachment;
+ }
+
+ public void complete(T result) {
+ this.result = result;
+ completionLatch.countDown();
+ if (handler != null) {
+ handler.completed(result, attachment);
+ }
+ }
+
+ public void fail(Throwable t) {
+ throwable = t;
+ completionLatch.countDown();
+ if (handler != null) {
+ handler.failed(throwable, attachment);
+ }
+ }
+
+ @Override
+ public final boolean cancel(boolean mayInterruptIfRunning) {
+ // Could support cancellation by closing the connection
+ return false;
+ }
+
+ @Override
+ public final boolean isCancelled() {
+ // Could support cancellation by closing the connection
+ return false;
+ }
+
+ @Override
+ public final boolean isDone() {
+ return completionLatch.getCount() > 0;
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ completionLatch.await();
+ if (throwable != null) {
+ throw new ExecutionException(throwable);
+ }
+ return result;
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException,
+ TimeoutException {
+ boolean latchResult = completionLatch.await(timeout, unit);
+ if (latchResult == false) {
+ throw new TimeoutException();
+ }
+ if (throwable != null) {
+ throw new ExecutionException(throwable);
+ }
+ return result;
+ }
+ }
+
+ private static final class LongToIntegerFuture implements Future<Integer> {
+
+ private final Future<Long> wrapped;
+
+ public LongToIntegerFuture(Future<Long> wrapped) {
+ this.wrapped = wrapped;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return wrapped.cancel(mayInterruptIfRunning);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return wrapped.isCancelled();
+ }
+
+ @Override
+ public boolean isDone() {
+ return wrapped.isDone();
+ }
+
+ @Override
+ public Integer get() throws InterruptedException, ExecutionException {
+ Long result = wrapped.get();
+ if (result.longValue() > Integer.MAX_VALUE) {
+ throw new ExecutionException(MESSAGES.notAnInteger(result), null);
+ }
+ return new Integer(result.intValue());
+ }
+
+ @Override
+ public Integer get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException,
+ TimeoutException {
+ Long result = wrapped.get(timeout, unit);
+ if (result.longValue() > Integer.MAX_VALUE) {
+ throw new ExecutionException(MESSAGES.notAnInteger(result), null);
+ }
+ return new Integer(result.intValue());
+ }
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/BackgroundProcess.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/BackgroundProcess.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/BackgroundProcess.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+public interface BackgroundProcess {
+
+ void backgroundProcess();
+
+ void setProcessPeriod(int period);
+
+ int getProcessPeriod();
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/BackgroundProcessManager.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/BackgroundProcessManager.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/BackgroundProcessManager.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.tomcat.util.ExceptionUtils;
+import org.jboss.web.WebsocketsLogger;
+
+/**
+ * Provides a background processing mechanism that triggers roughly once a
+ * second. The class maintains a thread that only runs when there is at least
+ * one instance of {@link BackgroundProcess} registered.
+ */
+public class BackgroundProcessManager {
+
+ private static final BackgroundProcessManager instance;
+
+
+ static {
+ instance = new BackgroundProcessManager();
+ }
+
+
+ public static BackgroundProcessManager getInstance() {
+ return instance;
+ }
+
+ private final Set<BackgroundProcess> processes = new
HashSet<BackgroundProcess>();
+ private final Object processesLock = new Object();
+ private WsBackgroundThread wsBackgroundThread = null;
+
+ private BackgroundProcessManager() {
+ // Hide default constructor
+ }
+
+
+ public void register(BackgroundProcess process) {
+ synchronized (processesLock) {
+ if (processes.size() == 0) {
+ wsBackgroundThread = new WsBackgroundThread(this);
+ wsBackgroundThread.setContextClassLoader(
+ this.getClass().getClassLoader());
+ wsBackgroundThread.setDaemon(true);
+ wsBackgroundThread.start();
+ }
+ processes.add(process);
+ }
+ }
+
+
+ public void unregister(BackgroundProcess process) {
+ synchronized (processesLock) {
+ processes.remove(process);
+ if (wsBackgroundThread != null && processes.size() == 0) {
+ wsBackgroundThread.halt();
+ wsBackgroundThread = null;
+ }
+ }
+ }
+
+
+ private void process() {
+ Set<BackgroundProcess> currentProcesses = new
HashSet<BackgroundProcess>();
+ synchronized (processesLock) {
+ currentProcesses.addAll(processes);
+ }
+ for (BackgroundProcess process : currentProcesses) {
+ try {
+ process.backgroundProcess();
+ } catch (Throwable t) {
+ ExceptionUtils.handleThrowable(t);
+ WebsocketsLogger.ROOT_LOGGER.backgroundProcessFailed(t);
+ }
+ }
+ }
+
+
+ private static class WsBackgroundThread extends Thread {
+
+ private final BackgroundProcessManager manager;
+ private volatile boolean running = true;
+
+ public WsBackgroundThread(BackgroundProcessManager manager) {
+ setName("WebSocket background processing");
+ this.manager = manager;
+ }
+
+ @Override
+ public void run() {
+ while (running) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ manager.process();
+ }
+ }
+
+ public void halt() {
+ setName("WebSocket background processing - stopping");
+ running = false;
+ }
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/Constants.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/Constants.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/websocket/Constants.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.util.Locale;
+
+/**
+ * Internal implementation constants.
+ */
+public class Constants {
+
+ protected static final String PACKAGE_NAME =
+ Constants.class.getPackage().getName();
+ // OP Codes
+ public static final byte OPCODE_CONTINUATION = 0x00;
+ public static final byte OPCODE_TEXT = 0x01;
+ public static final byte OPCODE_BINARY = 0x02;
+ public static final byte OPCODE_CLOSE = 0x08;
+ public static final byte OPCODE_PING = 0x09;
+ public static final byte OPCODE_PONG = 0x0A;
+
+ // Internal OP Codes
+ // RFC 6455 limits OP Codes to 4 bits so these should never clash
+ // Always set bit 4 so these will be treated as control codes
+ static final byte INTERNAL_OPCODE_FLUSH = 0x18;
+
+ // Buffers
+ static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
+
+ // Client connection
+ public static final String HOST_HEADER_NAME = "Host";
+ public static final String UPGRADE_HEADER_NAME = "Upgrade";
+ public static final String UPGRADE_HEADER_VALUE = "websocket";
+ public static final String CONNECTION_HEADER_NAME = "Connection";
+ public static final String CONNECTION_HEADER_VALUE = "upgrade";
+ public static final String WS_VERSION_HEADER_NAME =
"Sec-WebSocket-Version";
+ public static final String WS_VERSION_HEADER_VALUE = "13";
+ public static final String WS_KEY_HEADER_NAME = "Sec-WebSocket-Key";
+ public static final String WS_PROTOCOL_HEADER_NAME =
+ "Sec-WebSocket-Protocol";
+ public static final String WS_PROTOCOL_HEADER_NAME_LOWER =
+ WS_PROTOCOL_HEADER_NAME.toLowerCase(Locale.ENGLISH);
+ public static final String WS_EXTENSIONS_HEADER_NAME =
+ "Sec-WebSocket-Extensions";
+
+ public static final boolean STRICT_SPEC_COMPLIANCE =
+ Boolean.getBoolean(
+ "org.apache.tomcat.websocket.STRICT_SPEC_COMPLIANCE");
+
+ private Constants() {
+ // Hide default constructor
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/DecoderEntry.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/DecoderEntry.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/websocket/DecoderEntry.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import javax.websocket.Decoder;
+
+public class DecoderEntry {
+
+ private final Class<?> clazz;
+ private final Class<? extends Decoder> decoderClazz;
+
+ public DecoderEntry(Class<?> clazz,
+ Class<? extends Decoder> decoderClazz) {
+ this.clazz = clazz;
+ this.decoderClazz = decoderClazz;
+ }
+
+ public Class<?> getClazz() {
+ return clazz;
+ }
+
+ public Class<? extends Decoder> getDecoderClazz() {
+ return decoderClazz;
+ }
+}
\ No newline at end of file
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/FutureToSendHandler.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/FutureToSendHandler.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/FutureToSendHandler.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.websocket.SendHandler;
+import javax.websocket.SendResult;
+
+/**
+ * Converts a Future to a SendHandler.
+ */
+class FutureToSendHandler implements Future<Void>, SendHandler {
+
+ private final CountDownLatch latch = new CountDownLatch(1);
+ private final WsSession wsSession;
+ private volatile SendResult result = null;
+
+ public FutureToSendHandler(WsSession wsSession) {
+ this.wsSession = wsSession;
+ }
+
+
+ // --------------------------------------------------------- SendHandler
+
+ @Override
+ public void onResult(SendResult result) {
+
+ this.result = result;
+ latch.countDown();
+ }
+
+
+ // -------------------------------------------------------------- Future
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ // Cancelling the task is not supported
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ // Cancelling the task is not supported
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return latch.getCount() == 0;
+ }
+
+ @Override
+ public Void get() throws InterruptedException,
+ ExecutionException {
+ try {
+ wsSession.registerFuture(this);
+ latch.await();
+ } finally {
+ wsSession.unregisterFuture(this);
+ }
+ if (result.getException() != null) {
+ throw new ExecutionException(result.getException());
+ }
+ return null;
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException,
+ TimeoutException {
+ boolean retval = false;
+ try {
+ wsSession.registerFuture(this);
+ retval = latch.await(timeout, unit);
+ } finally {
+ wsSession.unregisterFuture(this);
+
+ }
+ if (retval == false) {
+ throw new TimeoutException();
+ }
+ if (result.getException() != null) {
+ throw new ExecutionException(result.getException());
+ }
+ return null;
+ }
+}
+
+
+
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/MessageHandlerResult.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/MessageHandlerResult.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/MessageHandlerResult.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import javax.websocket.MessageHandler;
+
+public class MessageHandlerResult {
+
+ private final MessageHandler handler;
+ private final MessageHandlerResultType type;
+
+
+ public MessageHandlerResult(MessageHandler handler,
+ MessageHandlerResultType type) {
+ this.handler = handler;
+ this.type = type;
+ }
+
+
+ public MessageHandler getHandler() {
+ return handler;
+ }
+
+
+ public MessageHandlerResultType getType() {
+ return type;
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/MessageHandlerResultType.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/MessageHandlerResultType.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/MessageHandlerResultType.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+public enum MessageHandlerResultType {
+ BINARY,
+ TEXT,
+ PONG
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/ReadBufferOverflowException.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/ReadBufferOverflowException.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/ReadBufferOverflowException.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.io.IOException;
+
+public class ReadBufferOverflowException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final int minBufferSize;
+
+ public ReadBufferOverflowException(int minBufferSize) {
+ this.minBufferSize = minBufferSize;
+ }
+
+ public int getMinBufferSize() {
+ return minBufferSize;
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/SendHandlerToCompletionHandler.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/SendHandlerToCompletionHandler.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/SendHandlerToCompletionHandler.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.nio.channels.CompletionHandler;
+
+import javax.websocket.SendHandler;
+import javax.websocket.SendResult;
+
+public class SendHandlerToCompletionHandler
+ implements CompletionHandler<Long,Void> {
+
+ private SendHandler handler;
+
+ public SendHandlerToCompletionHandler(SendHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public void completed(Long result, Void attachment) {
+ handler.onResult(new SendResult());
+ }
+
+ @Override
+ public void failed(Throwable exc, Void attachment) {
+ handler.onResult(new SendResult(exc));
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/Util.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/Util.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/websocket/Util.java 2013-09-20 16:27:05
UTC (rev 2261)
@@ -0,0 +1,473 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import javax.websocket.CloseReason.CloseCode;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.Decoder;
+import javax.websocket.Decoder.Binary;
+import javax.websocket.Decoder.BinaryStream;
+import javax.websocket.Decoder.Text;
+import javax.websocket.Decoder.TextStream;
+import javax.websocket.DeploymentException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.PongMessage;
+
+import org.apache.tomcat.websocket.pojo.PojoMessageHandlerWholeBinary;
+import org.apache.tomcat.websocket.pojo.PojoMessageHandlerWholeText;
+
+/**
+ * Utility class for internal use only within the
+ * {@link org.apache.tomcat.websocket} package.
+ */
+public class Util {
+
+ private static final Queue<SecureRandom> randoms =
+ new ConcurrentLinkedQueue<SecureRandom>();
+
+ private Util() {
+ // Hide default constructor
+ }
+
+
+ static boolean isControl(byte opCode) {
+ return (opCode & 0x08) > 0;
+ }
+
+
+ static boolean isText(byte opCode) {
+ return opCode == Constants.OPCODE_TEXT;
+ }
+
+
+ static CloseCode getCloseCode(int code) {
+ if (code > 2999 && code < 5000) {
+ return CloseCodes.NORMAL_CLOSURE;
+ }
+ switch (code) {
+ case 1000:
+ return CloseCodes.NORMAL_CLOSURE;
+ case 1001:
+ return CloseCodes.GOING_AWAY;
+ case 1002:
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1003:
+ return CloseCodes.CANNOT_ACCEPT;
+ case 1004:
+ // Should not be used in a close frame
+ // return CloseCodes.RESERVED;
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1005:
+ // Should not be used in a close frame
+ // return CloseCodes.NO_STATUS_CODE;
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1006:
+ // Should not be used in a close frame
+ // return CloseCodes.CLOSED_ABNORMALLY;
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1007:
+ return CloseCodes.NOT_CONSISTENT;
+ case 1008:
+ return CloseCodes.VIOLATED_POLICY;
+ case 1009:
+ return CloseCodes.TOO_BIG;
+ case 1010:
+ return CloseCodes.NO_EXTENSION;
+ case 1011:
+ return CloseCodes.UNEXPECTED_CONDITION;
+ case 1012:
+ // Not in RFC6455
+ // return CloseCodes.SERVICE_RESTART;
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1013:
+ // Not in RFC6455
+ // return CloseCodes.TRY_AGAIN_LATER;
+ return CloseCodes.PROTOCOL_ERROR;
+ case 1015:
+ // Should not be used in a close frame
+ // return CloseCodes.TLS_HANDSHAKE_FAILURE;
+ return CloseCodes.PROTOCOL_ERROR;
+ default:
+ return CloseCodes.PROTOCOL_ERROR;
+ }
+ }
+
+
+ static byte[] generateMask() {
+ // SecureRandom is not thread-safe so need to make sure only one thread
+ // uses it at a time. In theory, the pool could grow to the same size
+ // as the number of request processing threads. In reality it will be
+ // a lot smaller.
+
+ // Get a SecureRandom from the pool
+ SecureRandom sr = randoms.poll();
+
+ // If one isn't available, generate a new one
+ if (sr == null) {
+ try {
+ sr = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ // Fall back to platform default
+ sr = new SecureRandom();
+ }
+ }
+
+ // Generate the mask
+ byte[] result = new byte[4];
+ sr.nextBytes(result);
+
+ // Put the SecureRandom back in the poll
+ randoms.add(sr);
+
+ return result;
+ }
+
+
+ static Class<?> getMessageType(MessageHandler listener) {
+ return (Class<?>) Util.getGenericType(MessageHandler.class,
+ listener.getClass());
+ }
+
+
+ public static Class<?> getDecoderType(Class<? extends Decoder> Decoder)
{
+ return (Class<?>) Util.getGenericType(Decoder.class, Decoder);
+ }
+
+
+ static Class<?> getEncoderType(Class<? extends Encoder> encoder) {
+ return (Class<?>) Util.getGenericType(Encoder.class, encoder);
+ }
+
+
+ private static <T> Object getGenericType(Class<T> type,
+ Class<? extends T> clazz) {
+
+ // Look to see if this class implements the generic MessageHandler<>
+ // interface
+
+ // Get all the interfaces
+ Type[] interfaces = clazz.getGenericInterfaces();
+ for (Type iface : interfaces) {
+ // Only need to check interfaces that use generics
+ if (iface instanceof ParameterizedType) {
+ ParameterizedType pi = (ParameterizedType) iface;
+ // Look for the MessageHandler<> interface
+ if (pi.getRawType() instanceof Class) {
+ if (type.isAssignableFrom((Class<?>) pi.getRawType())) {
+ return getTypeParameter(
+ clazz, pi.getActualTypeArguments()[0]);
+ }
+ }
+ }
+ }
+
+ // Interface not found on this class. Look at the superclass.
+ @SuppressWarnings("unchecked")
+ Class<? extends T> superClazz =
+ (Class<? extends T>) clazz.getSuperclass();
+
+ Object result = getGenericType(type, superClazz);
+ if (result instanceof Class<?>) {
+ // Superclass implements interface and defines explicit type for
+ // MessageHandler<>
+ return result;
+ } else if (result instanceof Integer) {
+ // Superclass implements interface and defines unknown type for
+ // MessageHandler<>
+ // Map that unknown type to the generic types defined in this class
+ ParameterizedType superClassType =
+ (ParameterizedType) clazz.getGenericSuperclass();
+ return getTypeParameter(clazz,
+ superClassType.getActualTypeArguments()[
+ ((Integer) result).intValue()]);
+ } else {
+ // Error will be logged further up the call stack
+ return null;
+ }
+ }
+
+
+ /*
+ * For a generic parameter, return either the Class used or if the type
+ * is unknown, the index for the type in definition of the class
+ */
+ private static Object getTypeParameter(Class<?> clazz, Type argType) {
+ if (argType instanceof Class<?>) {
+ return argType;
+ } else {
+ TypeVariable<?>[] tvs = clazz.getTypeParameters();
+ for (int i = 0; i < tvs.length; i++) {
+ if (tvs[i].equals(argType)) {
+ return Integer.valueOf(i);
+ }
+ }
+ return null;
+ }
+ }
+
+
+ public static boolean isPrimitive(Class<?> clazz) {
+ if (clazz.isPrimitive()) {
+ return true;
+ } else if(clazz.equals(Boolean.class) ||
+ clazz.equals(Byte.class) ||
+ clazz.equals(Character.class) ||
+ clazz.equals(Double.class) ||
+ clazz.equals(Float.class) ||
+ clazz.equals(Integer.class) ||
+ clazz.equals(Long.class) ||
+ clazz.equals(Short.class)) {
+ return true;
+ }
+ return false;
+ }
+
+
+ public static Object coerceToType(Class<?> type, String value) {
+ if (type.equals(String.class)) {
+ return value;
+ } else if (type.equals(boolean.class) || type.equals(Boolean.class)) {
+ return Boolean.valueOf(value);
+ } else if (type.equals(byte.class) || type.equals(Byte.class)) {
+ return Byte.valueOf(value);
+ } else if (value.length() == 1 &&
+ (type.equals(char.class) || type.equals(Character.class))) {
+ return Character.valueOf(value.charAt(0));
+ } else if (type.equals(double.class) || type.equals(Double.class)) {
+ return Double.valueOf(value);
+ } else if (type.equals(float.class) || type.equals(Float.class)) {
+ return Float.valueOf(value);
+ } else if (type.equals(int.class) || type.equals(Integer.class)) {
+ return Integer.valueOf(value);
+ } else if (type.equals(long.class) || type.equals(Long.class)) {
+ return Long.valueOf(value);
+ } else if (type.equals(short.class) || type.equals(Short.class)) {
+ return Short.valueOf(value);
+ } else {
+ throw MESSAGES.invalidType(value, type.getName());
+ }
+ }
+
+
+ public static List<DecoderEntry> getDecoders(
+ Class<? extends Decoder>[] decoderClazzes)
+ throws DeploymentException{
+
+ List<DecoderEntry> result = new ArrayList<DecoderEntry>();
+ for (Class<? extends Decoder> decoderClazz : decoderClazzes) {
+ // Need to instantiate decoder to ensure it is valid and that
+ // deployment can be failed if it is not
+ @SuppressWarnings("unused")
+ Decoder instance;
+ try {
+ instance = decoderClazz.newInstance();
+ } catch (InstantiationException e) {
+ throw new
DeploymentException(MESSAGES.cannotInstatiateDecoder(decoderClazz.getName()), e);
+ } catch (IllegalAccessException e) {
+ throw new
DeploymentException(MESSAGES.cannotInstatiateDecoder(decoderClazz.getName()), e);
+ }
+ DecoderEntry entry = new DecoderEntry(
+ Util.getDecoderType(decoderClazz), decoderClazz);
+ result.add(entry);
+ }
+
+ return result;
+ }
+
+
+
+ public static Set<MessageHandlerResult> getMessageHandlers(
+ MessageHandler listener, EndpointConfig endpointConfig) {
+
+ Class<?> target = Util.getMessageType(listener);
+
+ // Will never be more than 2 types
+ Set<MessageHandlerResult> results = new
HashSet<MessageHandlerResult>(2);
+
+ // Simple cases - handlers already accepts one of the types expected by
+ // the frame handling code
+ if (String.class.isAssignableFrom(target)) {
+ MessageHandlerResult result =
+ new MessageHandlerResult(listener,
+ MessageHandlerResultType.TEXT);
+ results.add(result);
+ } else if (ByteBuffer.class.isAssignableFrom(target)) {
+ MessageHandlerResult result =
+ new MessageHandlerResult(listener,
+ MessageHandlerResultType.BINARY);
+ results.add(result);
+ } else if (PongMessage.class.isAssignableFrom(target)) {
+ MessageHandlerResult result =
+ new MessageHandlerResult(listener,
+ MessageHandlerResultType.PONG);
+ results.add(result);
+ // Relatively simple cases - handler needs wrapping but no decoder to
+ // convert it to one of the types expected by the frame handling code
+ } else if (byte[].class.isAssignableFrom(target)) {
+ MessageHandlerResult result = new MessageHandlerResult(
+ new PojoMessageHandlerWholeBinary(listener,
+ getOnMessageMethod(listener), null,
+ endpointConfig, null, new Object[1], 0, true, -1,
+ false, -1),
+ MessageHandlerResultType.BINARY);
+ results.add(result);
+ } else if (InputStream.class.isAssignableFrom(target)) {
+ MessageHandlerResult result = new MessageHandlerResult(
+ new PojoMessageHandlerWholeBinary(listener,
+ getOnMessageMethod(listener), null,
+ endpointConfig, null, new Object[1], 0, true, -1,
+ true, -1),
+ MessageHandlerResultType.BINARY);
+ results.add(result);
+ } else if (Reader.class.isAssignableFrom(target)) {
+ MessageHandlerResult result = new MessageHandlerResult(
+ new PojoMessageHandlerWholeText(listener,
+ getOnMessageMethod(listener), null,
+ endpointConfig, null, new Object[1], 0, true, -1,
+ -1),
+ MessageHandlerResultType.TEXT);
+ results.add(result);
+ } else {
+ // More complex case - listener that requires a decoder
+ DecoderMatch decoderMatch;
+ try {
+ List<Class<? extends Decoder>> decoders =
+ endpointConfig.getDecoders();
+ @SuppressWarnings("unchecked")
+ List<DecoderEntry> decoderEntries = getDecoders(
+ decoders.toArray(new Class[decoders.size()]));
+ decoderMatch = new DecoderMatch(target, decoderEntries);
+ } catch (DeploymentException e) {
+ throw new IllegalArgumentException(e);
+ }
+ Method m = getOnMessageMethod(listener);
+ if (decoderMatch.getBinaryDecoders().size() > 0) {
+ MessageHandlerResult result = new MessageHandlerResult(
+ new PojoMessageHandlerWholeBinary(listener, m, null,
+ endpointConfig,
+ decoderMatch.getBinaryDecoders(), new Object[1],
+ 0, false, -1, false, -1),
+ MessageHandlerResultType.BINARY);
+ results.add(result);
+ }
+ if (decoderMatch.getTextDecoders().size() > 0) {
+ MessageHandlerResult result = new MessageHandlerResult(
+ new PojoMessageHandlerWholeText(listener, m, null,
+ endpointConfig,
+ decoderMatch.getTextDecoders(), new Object[1],
+ 0, false, -1, -1),
+ MessageHandlerResultType.TEXT);
+ results.add(result);
+ }
+ }
+
+ if (results.size() == 0) {
+ throw MESSAGES.unknownHandler(listener, target);
+ }
+
+ return results;
+ }
+
+
+ private static Method getOnMessageMethod(MessageHandler listener) {
+ try {
+ return listener.getClass().getMethod("onMessage", Object.class);
+ } catch (NoSuchMethodException e) {
+ throw MESSAGES.invalidMessageHandler(e);
+ } catch ( SecurityException e) {
+ throw MESSAGES.invalidMessageHandler(e);
+ }
+ }
+
+ public static class DecoderMatch {
+
+ private final List<Class<? extends Decoder>> textDecoders =
+ new ArrayList<Class<? extends Decoder>>();
+ private final List<Class<? extends Decoder>> binaryDecoders =
+ new ArrayList<Class<? extends Decoder>>();
+
+
+ public DecoderMatch(Class<?> target, List<DecoderEntry>
decoderEntries) {
+ for (DecoderEntry decoderEntry : decoderEntries) {
+ if (decoderEntry.getClazz().isAssignableFrom(target)) {
+ if (Binary.class.isAssignableFrom(
+ decoderEntry.getDecoderClazz())) {
+ binaryDecoders.add(decoderEntry.getDecoderClazz());
+ // willDecode() method means this decoder may or may not
+ // decode a message so need to carry on checking for
+ // other matches
+ } else if (BinaryStream.class.isAssignableFrom(
+ decoderEntry.getDecoderClazz())) {
+ binaryDecoders.add(decoderEntry.getDecoderClazz());
+ // Stream decoders have to process the message so no
+ // more decoders can be matched
+ break;
+ } else if (Text.class.isAssignableFrom(
+ decoderEntry.getDecoderClazz())) {
+ textDecoders.add(decoderEntry.getDecoderClazz());
+ // willDecode() method means this decoder may or may not
+ // decode a message so need to carry on checking for
+ // other matches
+ } else if (TextStream.class.isAssignableFrom(
+ decoderEntry.getDecoderClazz())) {
+ textDecoders.add(decoderEntry.getDecoderClazz());
+ // Stream decoders have to process the message so no
+ // more decoders can be matched
+ break;
+ } else {
+ throw
MESSAGES.unknownDecoderType(decoderEntry.getDecoderClazz().getName());
+ }
+ }
+ }
+ }
+
+
+ public List<Class<? extends Decoder>> getTextDecoders() {
+ return textDecoders;
+ }
+
+
+ public List<Class<? extends Decoder>> getBinaryDecoders() {
+ return binaryDecoders;
+ }
+
+
+ public boolean hasMatches() {
+ return (textDecoders.size() > 0) || (binaryDecoders.size() > 0);
+ }
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WrappedMessageHandler.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WrappedMessageHandler.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WrappedMessageHandler.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import javax.websocket.MessageHandler;
+
+public interface WrappedMessageHandler {
+ long getMaxMessageSize();
+
+ MessageHandler getWrappedHandler();
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsContainerProvider.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsContainerProvider.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsContainerProvider.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import javax.websocket.ContainerProvider;
+import javax.websocket.WebSocketContainer;
+
+public class WsContainerProvider extends ContainerProvider {
+
+ @Override
+ protected WebSocketContainer getContainer() {
+ return new WsWebSocketContainer();
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsFrameBase.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsFrameBase.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsFrameBase.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,669 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.MessageHandler;
+import javax.websocket.PongMessage;
+
+import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.buf.Utf8Decoder;
+
+/**
+ * Takes the ServletInputStream, processes the WebSocket frames it contains and
+ * extracts the messages. WebSocket Pings received will be responded to
+ * automatically without any action required by the application.
+ */
+public abstract class WsFrameBase {
+
+ // Connection level attributes
+ protected final WsSession wsSession;
+ protected final byte[] inputBuffer;
+
+ // Attributes for control messages
+ // Control messages can appear in the middle of other messages so need
+ // separate attributes
+ private final ByteBuffer controlBufferBinary = ByteBuffer.allocate(125);
+ private final CharBuffer controlBufferText = CharBuffer.allocate(125);
+
+ // Attributes of the current message
+ private final CharsetDecoder utf8DecoderControl = new Utf8Decoder().
+ onMalformedInput(CodingErrorAction.REPORT).
+ onUnmappableCharacter(CodingErrorAction.REPORT);
+ private final CharsetDecoder utf8DecoderMessage = new Utf8Decoder().
+ onMalformedInput(CodingErrorAction.REPORT).
+ onUnmappableCharacter(CodingErrorAction.REPORT);
+ private boolean continuationExpected = false;
+ private boolean textMessage = false;
+ private ByteBuffer messageBufferBinary;
+ private CharBuffer messageBufferText;
+ // Cache the message handler in force when the message starts so it is used
+ // consistently for the entire message
+ private MessageHandler binaryMsgHandler = null;
+ private MessageHandler textMsgHandler = null;
+
+ // Attributes of the current frame
+ private boolean fin = false;
+ private int rsv = 0;
+ private byte opCode = 0;
+ private final byte[] mask = new byte[4];
+ private int maskIndex = 0;
+ private long payloadLength = 0;
+ private long payloadWritten = 0;
+
+ // Attributes tracking state
+ private State state = State.NEW_FRAME;
+ private volatile boolean open = true;
+ private int readPos = 0;
+ protected int writePos = 0;
+
+ public WsFrameBase(WsSession wsSession) {
+
+ inputBuffer = new byte[Constants.DEFAULT_BUFFER_SIZE];
+ messageBufferBinary =
+ ByteBuffer.allocate(wsSession.getMaxBinaryMessageBufferSize());
+ messageBufferText =
+ CharBuffer.allocate(wsSession.getMaxTextMessageBufferSize());
+ this.wsSession = wsSession;
+ }
+
+
+ protected void processInputBuffer() throws IOException {
+ while (true) {
+ wsSession.updateLastActive();
+
+ if (state == State.NEW_FRAME) {
+ if (!processInitialHeader()) {
+ break;
+ }
+ // If a close frame has been received, no further data should
+ // have seen
+ if (!open) {
+ throw new IOException(MESSAGES.receivedFrameAfterClose());
+ }
+ }
+ if (state == State.PARTIAL_HEADER) {
+ if (!processRemainingHeader()) {
+ break;
+ }
+ }
+ if (state == State.DATA) {
+ if (!processData()) {
+ break;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * @return <code>true</code> if sufficient data was present to process
all
+ * of the initial header
+ */
+ private boolean processInitialHeader() throws IOException {
+ // Need at least two bytes of data to do this
+ if (writePos - readPos < 2) {
+ return false;
+ }
+ int b = inputBuffer[readPos++];
+ fin = (b & 0x80) > 0;
+ rsv = (b & 0x70) >>> 4;
+ if (rsv != 0) {
+ // Note extensions may use rsv bits but currently no extensions are
+ // supported
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.unsupportedReservedBitsSet(Integer.valueOf(rsv))));
+ }
+ opCode = (byte) (b & 0x0F);
+ if (Util.isControl(opCode)) {
+ if (!fin) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.invalidFragmentedControlFrame()));
+ }
+ if (opCode != Constants.OPCODE_PING &&
+ opCode != Constants.OPCODE_PONG &&
+ opCode != Constants.OPCODE_CLOSE) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.invalidFrameOpcode(opCode)));
+ }
+ } else {
+ if (continuationExpected) {
+ if (opCode != Constants.OPCODE_CONTINUATION) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.noContinuationFrame()));
+ }
+ } else {
+ try {
+ if (opCode == Constants.OPCODE_BINARY) {
+ // New binary message
+ textMessage = false;
+ int size = wsSession.getMaxBinaryMessageBufferSize();
+ if (size != messageBufferBinary.capacity()) {
+ messageBufferBinary = ByteBuffer.allocate(size);
+ }
+ binaryMsgHandler = wsSession.getBinaryMessageHandler();
+ textMsgHandler = null;
+ } else if (opCode == Constants.OPCODE_TEXT) {
+ // New text message
+ textMessage = true;
+ int size = wsSession.getMaxTextMessageBufferSize();
+ if (size != messageBufferText.capacity()) {
+ messageBufferText = CharBuffer.allocate(size);
+ }
+ binaryMsgHandler = null;
+ textMsgHandler = wsSession.getTextMessageHandler();
+ } else {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.invalidFrameOpcode(opCode)));
+ }
+ } catch (IllegalStateException ise) {
+ // Thrown if the session is already closed
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.sessionClosed()));
+ }
+ }
+ continuationExpected = !fin;
+ }
+ b = inputBuffer[readPos++];
+ // Client data must be masked
+ if ((b & 0x80) == 0 && isMasked()) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.frameWithoutMask()));
+ }
+ payloadLength = b & 0x7F;
+ state = State.PARTIAL_HEADER;
+ return true;
+ }
+
+
+ protected abstract boolean isMasked();
+
+
+ /**
+ * @return <code>true</code> if sufficient data was present to complete
the
+ * processing of the header
+ */
+ private boolean processRemainingHeader() throws IOException {
+ // Ignore the 2 bytes already read. 4 for the mask
+ int headerLength;
+ if (isMasked()) {
+ headerLength = 4;
+ } else {
+ headerLength = 0;
+ }
+ // Add additional bytes depending on length
+ if (payloadLength == 126) {
+ headerLength += 2;
+ } else if (payloadLength == 127) {
+ headerLength += 8;
+ }
+ if (writePos - readPos < headerLength) {
+ return false;
+ }
+ // Calculate new payload length if necessary
+ if (payloadLength == 126) {
+ payloadLength = byteArrayToLong(inputBuffer, readPos, 2);
+ readPos += 2;
+ } else if (payloadLength == 127) {
+ payloadLength = byteArrayToLong(inputBuffer, readPos, 8);
+ readPos += 8;
+ }
+ if (Util.isControl(opCode)) {
+ if (payloadLength > 125) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+
MESSAGES.controlFramePayloadTooLarge(Long.valueOf(payloadLength))));
+ }
+ if (!fin) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.controlFrameWithoutFin()));
+ }
+ }
+ if (isMasked()) {
+ System.arraycopy(inputBuffer, readPos, mask, 0, 4);
+ readPos += 4;
+ }
+ state = State.DATA;
+ return true;
+ }
+
+
+ private boolean processData() throws IOException {
+ checkRoomPayload();
+ if (Util.isControl(opCode)) {
+ return processDataControl();
+ } else if (textMessage) {
+ return processDataText();
+ } else {
+ return processDataBinary();
+ }
+ }
+
+
+ private boolean processDataControl() throws IOException {
+ if (!appendPayloadToMessage(controlBufferBinary)) {
+ return false;
+ }
+ controlBufferBinary.flip();
+ if (opCode == Constants.OPCODE_CLOSE) {
+ open = false;
+ String reason = null;
+ int code = CloseCodes.NORMAL_CLOSURE.getCode();
+ if (controlBufferBinary.remaining() == 1) {
+ controlBufferBinary.clear();
+ // Payload must be zero or greater than 2
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.invalidOneByteClose()));
+ }
+ if (controlBufferBinary.remaining() > 1) {
+ code = controlBufferBinary.getShort();
+ if (controlBufferBinary.remaining() > 0) {
+ CoderResult cr = utf8DecoderControl.decode(
+ controlBufferBinary, controlBufferText, true);
+ if (cr.isError()) {
+ controlBufferBinary.clear();
+ controlBufferText.clear();
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.invalidUtf8Close()));
+ }
+ // There will be no overflow as the output buffer is big
+ // enough. There will be no underflow as all the data is
+ // passed to the decoder in a single call.
+ controlBufferText.flip();
+ reason = controlBufferText.toString();
+ }
+ }
+ wsSession.onClose(new CloseReason(Util.getCloseCode(code), reason));
+ } else if (opCode == Constants.OPCODE_PING) {
+ if (wsSession.isOpen()) {
+ wsSession.getBasicRemote().sendPong(controlBufferBinary);
+ }
+ } else if (opCode == Constants.OPCODE_PONG) {
+ MessageHandler.Whole<PongMessage> mhPong =
+ wsSession.getPongMessageHandler();
+ if (mhPong != null) {
+ try {
+ mhPong.onMessage(new WsPongMessage(controlBufferBinary));
+ } catch (Throwable t) {
+ ExceptionUtils.handleThrowable(t);
+ wsSession.getLocal().onError(wsSession, t);
+ } finally {
+ controlBufferBinary.clear();
+ }
+ }
+ } else {
+ // Should have caught this earlier but just in case...
+ controlBufferBinary.clear();
+ throw new WsIOException(new CloseReason(
+ CloseCodes.PROTOCOL_ERROR,
+ MESSAGES.invalidFrameOpcode(Integer.valueOf(opCode))));
+ }
+ controlBufferBinary.clear();
+ newFrame();
+ return true;
+ }
+
+
+ @SuppressWarnings("unchecked")
+ private void sendMessageText(boolean last) throws WsIOException {
+ if (textMsgHandler != null) {
+ if (textMsgHandler instanceof WrappedMessageHandler) {
+ long maxMessageSize =
+ ((WrappedMessageHandler) textMsgHandler).getMaxMessageSize();
+ if (maxMessageSize > -1 &&
+ messageBufferText.remaining() > maxMessageSize) {
+ throw new WsIOException(new CloseReason(CloseCodes.TOO_BIG,
+
MESSAGES.messageTooLarge(Long.valueOf(messageBufferText.remaining()),
+ Long.valueOf(maxMessageSize))));
+ }
+ }
+
+ try {
+ if (textMsgHandler instanceof MessageHandler.Partial<?>) {
+ ((MessageHandler.Partial<String>) textMsgHandler).onMessage(
+ messageBufferText.toString(), last);
+ } else {
+ // Caller ensures last == true if this branch is used
+ ((MessageHandler.Whole<String>) textMsgHandler).onMessage(
+ messageBufferText.toString());
+ }
+ } catch (Throwable t) {
+ ExceptionUtils.handleThrowable(t);
+ wsSession.getLocal().onError(wsSession, t);
+ } finally {
+ messageBufferText.clear();
+ }
+ }
+ }
+
+
+ private boolean processDataText() throws IOException {
+ // Copy the available data to the buffer
+ while (!appendPayloadToMessage(messageBufferBinary)) {
+ // Frame not complete - we ran out of something
+ // Convert bytes to UTF-8
+ messageBufferBinary.flip();
+ while (true) {
+ CoderResult cr = utf8DecoderMessage.decode(
+ messageBufferBinary, messageBufferText, false);
+ if (cr.isError()) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.NOT_CONSISTENT,
+ MESSAGES.invalidUtf8()));
+ } else if (cr.isOverflow()) {
+ // Ran out of space in text buffer - flush it
+ if (usePartial()) {
+ messageBufferText.flip();
+ sendMessageText(false);
+ messageBufferText.clear();
+ } else {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.TOO_BIG,
+ MESSAGES.textMessageTooLarge()));
+ }
+ } else if (cr.isUnderflow()) {
+ // Need more input
+ // Compact what we have to create as much space as possible
+ messageBufferBinary.compact();
+
+ // What did we run out of?
+ if (readPos == writePos) {
+ // Ran out of input data - get some more
+ return false;
+ } else {
+ // Ran out of message buffer - exit inner loop and
+ // refill
+ break;
+ }
+ }
+ }
+ }
+
+ messageBufferBinary.flip();
+ boolean last = false;
+ // Frame is fully received
+ // Convert bytes to UTF-8
+ while (true) {
+ CoderResult cr = utf8DecoderMessage.decode(messageBufferBinary,
+ messageBufferText, last);
+ if (cr.isError()) {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.NOT_CONSISTENT,
+ MESSAGES.invalidUtf8()));
+ } else if (cr.isOverflow()) {
+ // Ran out of space in text buffer - flush it
+ if (usePartial()) {
+ messageBufferText.flip();
+ sendMessageText(false);
+ messageBufferText.clear();
+ } else {
+ throw new WsIOException(new CloseReason(
+ CloseCodes.TOO_BIG,
+ MESSAGES.textMessageTooLarge()));
+ }
+ } else if (cr.isUnderflow() & !last) {
+ // End of frame and possible message as well.
+
+ if (continuationExpected) {
+ // If partial messages are supported, send what we have
+ // managed to decode
+ if (usePartial()) {
+ messageBufferText.flip();
+ sendMessageText(false);
+ messageBufferText.clear();
+ }
+ messageBufferBinary.compact();
+ newFrame();
+ // Process next frame
+ return true;
+ } else {
+ // Make sure coder has flushed all output
+ last = true;
+ }
+ } else {
+ // End of message
+ messageBufferText.flip();
+ sendMessageText(true);
+
+ newMessage();
+ return true;
+ }
+ }
+ }
+
+
+ private boolean processDataBinary() throws IOException {
+ // Copy the available data to the buffer
+ while (!appendPayloadToMessage(messageBufferBinary)) {
+ // Frame not complete - what did we run out of?
+ if (readPos == writePos) {
+ // Ran out of input data - get some more
+ return false;
+ } else {
+ // Ran out of message buffer - flush it
+ if (!usePartial()) {
+ CloseReason cr = new CloseReason(CloseCodes.TOO_BIG,
+
MESSAGES.bufferTooSmall(Integer.valueOf(messageBufferBinary.capacity()),
+ Long.valueOf(payloadLength)));
+ throw new WsIOException(cr);
+ }
+ messageBufferBinary.flip();
+ ByteBuffer copy =
+ ByteBuffer.allocate(messageBufferBinary.limit());
+ copy.put(messageBufferBinary);
+ copy.flip();
+ sendMessageBinary(copy, false);
+ messageBufferBinary.clear();
+ }
+ }
+
+ // Frame is fully received
+ // Send the message if either:
+ // - partial messages are supported
+ // - the message is complete
+ if (usePartial() || !continuationExpected) {
+ messageBufferBinary.flip();
+ ByteBuffer copy =
+ ByteBuffer.allocate(messageBufferBinary.limit());
+ copy.put(messageBufferBinary);
+ copy.flip();
+ sendMessageBinary(copy, !continuationExpected);
+ messageBufferBinary.clear();
+ }
+
+ if (continuationExpected) {
+ // More data for this message expected, start a new frame
+ newFrame();
+ } else {
+ // Message is complete, start a new message
+ newMessage();
+ }
+
+ return true;
+ }
+
+
+ @SuppressWarnings("unchecked")
+ private void sendMessageBinary(ByteBuffer msg, boolean last)
+ throws WsIOException {
+ if (binaryMsgHandler != null) {
+ if (binaryMsgHandler instanceof WrappedMessageHandler) {
+ long maxMessageSize =
+ ((WrappedMessageHandler) binaryMsgHandler).getMaxMessageSize();
+ if (maxMessageSize > -1 && msg.remaining() >
maxMessageSize) {
+ throw new WsIOException(new CloseReason(CloseCodes.TOO_BIG,
+ MESSAGES.messageTooLarge(
+ Long.valueOf(msg.remaining()),
+ Long.valueOf(maxMessageSize))));
+ }
+ }
+ try {
+ if (binaryMsgHandler instanceof MessageHandler.Partial<?>) {
+ ((MessageHandler.Partial<ByteBuffer>)
binaryMsgHandler).onMessage(msg, last);
+ } else {
+ // Caller ensures last == true if this branch is used
+ ((MessageHandler.Whole<ByteBuffer>)
binaryMsgHandler).onMessage(msg);
+ }
+ } catch(Throwable t) {
+ ExceptionUtils.handleThrowable(t);
+ wsSession.getLocal().onError(wsSession, t);
+ }
+ }
+ }
+
+
+ private void newMessage() {
+ messageBufferBinary.clear();
+ messageBufferText.clear();
+ utf8DecoderMessage.reset();
+ continuationExpected = false;
+ newFrame();
+ }
+
+
+ private void newFrame() {
+ if (readPos == writePos) {
+ readPos = 0;
+ writePos = 0;
+ }
+
+ maskIndex = 0;
+ payloadWritten = 0;
+ state = State.NEW_FRAME;
+
+ // These get reset in processInitialHeader()
+ // fin, rsv, opCode, payloadLength, mask
+
+ checkRoomHeaders();
+ }
+
+
+ private void checkRoomHeaders() {
+ // Is the start of the current frame too near the end of the input
+ // buffer?
+ if (inputBuffer.length - readPos < 131) {
+ // Limit based on a control frame with a full payload
+ makeRoom();
+ }
+ }
+
+
+ private void checkRoomPayload() {
+ if (inputBuffer.length - readPos - payloadLength + payloadWritten < 0) {
+ makeRoom();
+ }
+ }
+
+
+ private void makeRoom() {
+ System.arraycopy(inputBuffer, readPos, inputBuffer, 0,
+ writePos - readPos);
+ writePos = writePos - readPos;
+ readPos = 0;
+ }
+
+
+ private boolean usePartial() {
+ if (Util.isControl(opCode)) {
+ return false;
+ } else if (textMessage) {
+ if (textMsgHandler != null) {
+ return textMsgHandler instanceof MessageHandler.Partial<?>;
+ }
+ return false;
+ } else {
+ // Must be binary
+ if (binaryMsgHandler != null) {
+ return binaryMsgHandler instanceof MessageHandler.Partial<?>;
+ }
+ return false;
+ }
+ }
+
+
+ private boolean appendPayloadToMessage(ByteBuffer dest) {
+ if (isMasked()) {
+ while (payloadWritten < payloadLength && readPos < writePos
&&
+ dest.hasRemaining()) {
+ byte b = (byte) ((inputBuffer[readPos] ^ mask[maskIndex]) & 0xFF);
+ maskIndex++;
+ if (maskIndex == 4) {
+ maskIndex = 0;
+ }
+ readPos++;
+ payloadWritten++;
+ dest.put(b);
+ }
+ return (payloadWritten == payloadLength);
+ } else {
+ long toWrite = Math.min(
+ payloadLength - payloadWritten, writePos - readPos);
+ toWrite = Math.min(toWrite, dest.remaining());
+
+ dest.put(inputBuffer, readPos, (int) toWrite);
+ readPos += toWrite;
+ payloadWritten += toWrite;
+ return (payloadWritten == payloadLength);
+
+ }
+ }
+
+
+ protected static long byteArrayToLong(byte[] b, int start, int len)
+ throws IOException {
+ if (len > 8) {
+ throw new IOException(MESSAGES.invalidLong(Long.valueOf(len)));
+ }
+ int shift = 0;
+ long result = 0;
+ for (int i = start + len - 1; i >= start; i--) {
+ result = result + ((b[i] & 0xFF) << shift);
+ shift += 8;
+ }
+ return result;
+ }
+
+
+ protected boolean isOpen() {
+ return open;
+ }
+
+
+ private static enum State {
+ NEW_FRAME, PARTIAL_HEADER, DATA
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsFrameClient.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsFrameClient.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsFrameClient.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.CompletionHandler;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+
+public class WsFrameClient extends WsFrameBase {
+
+ private final AsyncChannelWrapper channel;
+ private final CompletionHandler<Integer,Void> handler;
+ // Not final as it may need to be re-sized
+ private ByteBuffer response;
+
+ public WsFrameClient(ByteBuffer response, AsyncChannelWrapper channel,
+ WsSession wsSession) {
+ super(wsSession);
+ this.response = response;
+ this.channel = channel;
+ this.handler = new WsFrameClientCompletionHandler();
+
+ try {
+ processSocketRead();
+ } catch (IOException e) {
+ close(e);
+ }
+ }
+
+
+ private void processSocketRead() throws IOException {
+
+ while (response.hasRemaining()) {
+ int remaining = response.remaining();
+
+ int toCopy = Math.min(remaining, inputBuffer.length - writePos);
+
+ // Copy remaining bytes read in HTTP phase to input buffer used by
+ // frame processing
+ response.get(inputBuffer, writePos, toCopy);
+ writePos += toCopy;
+
+ // Process the data we have
+ processInputBuffer();
+ }
+ response.clear();
+
+ // Get some more data
+ if (isOpen()) {
+ channel.read(response, null, handler);
+ }
+ }
+
+
+ private final void close(Throwable t) {
+ CloseReason cr;
+ if (t instanceof WsIOException) {
+ cr = ((WsIOException) t).getCloseReason();
+ } else {
+ cr = new CloseReason(
+ CloseCodes.CLOSED_ABNORMALLY, t.getMessage());
+ }
+
+ try {
+ wsSession.close(cr);
+ } catch (IOException ignore) {
+ // Ignore
+ }
+ }
+
+
+ @Override
+ protected boolean isMasked() {
+ // Data is from the server so it is not masked
+ return false;
+ }
+
+
+ private class WsFrameClientCompletionHandler
+ implements CompletionHandler<Integer,Void> {
+
+ @Override
+ public void completed(Integer result, Void attachment) {
+ response.flip();
+ try {
+ processSocketRead();
+ } catch (IOException e) {
+ close(e);
+ }
+ }
+
+ @Override
+ public void failed(Throwable exc, Void attachment) {
+ if (exc instanceof ReadBufferOverflowException) {
+ // response will be empty if this exception is thrown
+ response = ByteBuffer.allocate(
+ ((ReadBufferOverflowException) exc).getMinBufferSize());
+ response.flip();
+ try {
+ processSocketRead();
+ } catch (IOException e) {
+ close(e);
+ }
+ } else {
+ close(exc);
+ }
+ }
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsHandshakeResponse.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsHandshakeResponse.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsHandshakeResponse.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.HandshakeResponse;
+
+/**
+ * Represents the response to a WebSocket handshake.
+ */
+public class WsHandshakeResponse implements HandshakeResponse {
+
+ private final Map<String,List<String>> headers;
+
+
+ public WsHandshakeResponse() {
+ this(new HashMap<String,List<String>>());
+ }
+
+
+ public WsHandshakeResponse(Map<String,List<String>> headers) {
+ this.headers = headers;
+ }
+
+
+ @Override
+ public Map<String,List<String>> getHeaders() {
+ return headers;
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsIOException.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsIOException.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsIOException.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.io.IOException;
+
+import javax.websocket.CloseReason;
+
+/**
+ * Allows the WebSocket implementation to throw an {@link IOException} that
+ * includes a {@link CloseReason} specific to the error that can be passed back
+ * to the client.
+ */
+public class WsIOException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final CloseReason closeReason;
+
+ public WsIOException(CloseReason closeReason) {
+ this.closeReason = closeReason;
+ }
+
+ public CloseReason getCloseReason() {
+ return closeReason;
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsPongMessage.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsPongMessage.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsPongMessage.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.PongMessage;
+
+public class WsPongMessage implements PongMessage {
+
+ private final ByteBuffer applicationData;
+
+
+ public WsPongMessage(ByteBuffer applicationData) {
+ byte[] dst = new byte[applicationData.limit()];
+ applicationData.get(dst);
+ this.applicationData = ByteBuffer.wrap(dst);
+ }
+
+
+ @Override
+ public ByteBuffer getApplicationData() {
+ return applicationData;
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointAsync.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointAsync.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointAsync.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.Future;
+
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.SendHandler;
+
+public class WsRemoteEndpointAsync extends WsRemoteEndpointBase
+ implements RemoteEndpoint.Async {
+
+ WsRemoteEndpointAsync(WsRemoteEndpointImplBase base) {
+ super(base);
+ }
+
+
+ @Override
+ public long getSendTimeout() {
+ return base.getSendTimeout();
+ }
+
+
+ @Override
+ public void setSendTimeout(long timeout) {
+ base.setSendTimeout(timeout);
+ }
+
+
+ @Override
+ public void sendText(String text, SendHandler completion) {
+ base.sendStringByCompletion(text, completion);
+ }
+
+
+ @Override
+ public Future<Void> sendText(String text) {
+ return base.sendStringByFuture(text);
+ }
+
+
+ @Override
+ public Future<Void> sendBinary(ByteBuffer data) {
+ return base.sendBytesByFuture(data);
+ }
+
+
+ @Override
+ public void sendBinary(ByteBuffer data, SendHandler completion) {
+ base.sendBytesByCompletion(data, completion);
+ }
+
+
+ @Override
+ public Future<Void> sendObject(Object obj) {
+ return base.sendObjectByFuture(obj);
+ }
+
+
+ @Override
+ public void sendObject(Object obj, SendHandler completion) {
+ base.sendObjectByCompletion(obj, completion);
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointBase.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointBase.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointBase.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import javax.websocket.RemoteEndpoint;
+
+public abstract class WsRemoteEndpointBase implements RemoteEndpoint {
+
+ protected final WsRemoteEndpointImplBase base;
+
+
+ WsRemoteEndpointBase(WsRemoteEndpointImplBase base) {
+ this.base = base;
+ }
+
+
+ @Override
+ public final void setBatchingAllowed(boolean batchingAllowed) throws IOException {
+ base.setBatchingAllowed(batchingAllowed);
+ }
+
+
+ @Override
+ public final boolean getBatchingAllowed() {
+ return base.getBatchingAllowed();
+ }
+
+
+ @Override
+ public final void flushBatch() throws IOException {
+ base.flushBatch();
+ }
+
+
+ @Override
+ public final void sendPing(ByteBuffer applicationData) throws IOException,
+ IllegalArgumentException {
+ base.sendPing(applicationData);
+ }
+
+
+ @Override
+ public final void sendPong(ByteBuffer applicationData) throws IOException,
+ IllegalArgumentException {
+ base.sendPong(applicationData);
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointBasic.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointBasic.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointBasic.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+
+import javax.websocket.EncodeException;
+import javax.websocket.RemoteEndpoint;
+
+public class WsRemoteEndpointBasic extends WsRemoteEndpointBase
+ implements RemoteEndpoint.Basic {
+
+ WsRemoteEndpointBasic(WsRemoteEndpointImplBase base) {
+ super(base);
+ }
+
+
+ @Override
+ public void sendText(String text) throws IOException {
+ base.sendString(text);
+ }
+
+
+ @Override
+ public void sendBinary(ByteBuffer data) throws IOException {
+ base.sendBytes(data);
+ }
+
+
+ @Override
+ public void sendText(String fragment, boolean isLast) throws IOException {
+ base.sendPartialString(fragment, isLast);
+ }
+
+
+ @Override
+ public void sendBinary(ByteBuffer partialByte, boolean isLast)
+ throws IOException {
+ base.sendPartialBytes(partialByte, isLast);
+ }
+
+
+ @Override
+ public OutputStream getSendStream() throws IOException {
+ return base.getSendStream();
+ }
+
+
+ @Override
+ public Writer getSendWriter() throws IOException {
+ return base.getSendWriter();
+ }
+
+
+ @Override
+ public void sendObject(Object o) throws IOException, EncodeException {
+ base.sendObject(o);
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,932 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.websocket.DeploymentException;
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.SendHandler;
+import javax.websocket.SendResult;
+
+import org.apache.tomcat.util.buf.Utf8Encoder;
+import org.jboss.web.WebsocketsLogger;
+
+public abstract class WsRemoteEndpointImplBase implements RemoteEndpoint {
+
+ // Milliseconds so this is 20 seconds
+ private static final long DEFAULT_BLOCKING_SEND_TIMEOUT = 20 * 1000;
+
+ public static final String BLOCKING_SEND_TIMEOUT_PROPERTY =
+ "org.apache.tomcat.websocket.BLOCKING_SEND_TIMEOUT";
+
+ private boolean messagePartInProgress = false;
+ private final Queue<MessagePart> messagePartQueue = new
ArrayDeque<MessagePart>();
+ private final Object messagePartLock = new Object();
+ private boolean dataMessageInProgress = false;
+
+ // State
+ private boolean closed = false;
+ private boolean fragmented = false;
+ private boolean nextFragmented = false;
+ private boolean text = false;
+ private boolean nextText = false;
+
+ // Max size of WebSocket header is 14 bytes
+ private final ByteBuffer headerBuffer = ByteBuffer.allocate(14);
+ private final ByteBuffer outputBuffer = ByteBuffer.allocate(8192);
+ private final CharsetEncoder encoder = new Utf8Encoder();
+ private final ByteBuffer encoderBuffer = ByteBuffer.allocate(8192);
+ private final AtomicBoolean batchingAllowed = new AtomicBoolean(false);
+ private volatile long sendTimeout = -1;
+ private WsSession wsSession;
+ private List<EncoderEntry> encoderEntries = new
ArrayList<EncoderEntry>();
+
+ public long getSendTimeout() {
+ return sendTimeout;
+ }
+
+
+ public void setSendTimeout(long timeout) {
+ this.sendTimeout = timeout;
+ }
+
+
+ @Override
+ public void setBatchingAllowed(boolean batchingAllowed) throws IOException {
+ boolean oldValue = this.batchingAllowed.getAndSet(batchingAllowed);
+
+ if (oldValue && !batchingAllowed) {
+ flushBatch();
+ }
+ }
+
+
+ @Override
+ public boolean getBatchingAllowed() {
+ return batchingAllowed.get();
+ }
+
+
+ @Override
+ public void flushBatch() throws IOException {
+ startMessageBlock(Constants.INTERNAL_OPCODE_FLUSH, null, true);
+ }
+
+
+ public void sendBytes(ByteBuffer data) throws IOException {
+ startMessageBlock(Constants.OPCODE_BINARY, data, true);
+ }
+
+
+ public Future<Void> sendBytesByFuture(ByteBuffer data) {
+ FutureToSendHandler f2sh = new FutureToSendHandler(wsSession);
+ sendBytesByCompletion(data, f2sh);
+ return f2sh;
+ }
+
+
+ public void sendBytesByCompletion(ByteBuffer data, SendHandler handler) {
+ startMessage(Constants.OPCODE_BINARY, data, true, handler);
+ }
+
+
+ public void sendPartialBytes(ByteBuffer partialByte, boolean last)
+ throws IOException {
+ startMessageBlock(Constants.OPCODE_BINARY, partialByte, last);
+ }
+
+
+ @Override
+ public void sendPing(ByteBuffer applicationData) throws IOException,
+ IllegalArgumentException {
+ startMessageBlock(Constants.OPCODE_PING, applicationData, true);
+ }
+
+
+ @Override
+ public void sendPong(ByteBuffer applicationData) throws IOException,
+ IllegalArgumentException {
+ startMessageBlock(Constants.OPCODE_PONG, applicationData, true);
+ }
+
+
+ public void sendString(String text) throws IOException {
+ sendPartialString(CharBuffer.wrap(text), true);
+ }
+
+
+ public Future<Void> sendStringByFuture(String text) {
+ FutureToSendHandler f2sh = new FutureToSendHandler(wsSession);
+ sendStringByCompletion(text, f2sh);
+ return f2sh;
+ }
+
+
+ public void sendStringByCompletion(String text, SendHandler handler) {
+ TextMessageSendHandler tmsh = new TextMessageSendHandler(handler,
+ CharBuffer.wrap(text), true, encoder, encoderBuffer, this);
+ tmsh.write();
+ }
+
+
+ public void sendPartialString(String fragment, boolean isLast)
+ throws IOException {
+ sendPartialString(CharBuffer.wrap(fragment), isLast);
+ }
+
+
+ public OutputStream getSendStream() {
+ return new WsOutputStream(this);
+ }
+
+
+ public Writer getSendWriter() {
+ return new WsWriter(this);
+ }
+
+
+ void sendPartialString(CharBuffer part, boolean last) throws IOException {
+ try {
+ // Get the timeout before we send the message. The message may
+ // trigger a session close and depending on timing the client
+ // session may close before we can read the timeout.
+ long timeout = getBlockingSendTimeout();
+ FutureToSendHandler f2sh = new FutureToSendHandler(wsSession);
+ TextMessageSendHandler tmsh = new TextMessageSendHandler(f2sh, part,
+ last, encoder, encoderBuffer, this);
+ tmsh.write();
+ if (timeout == -1) {
+ f2sh.get();
+ } else {
+ f2sh.get(timeout, TimeUnit.MILLISECONDS);
+ }
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ } catch (ExecutionException e) {
+ throw new IOException(e);
+ } catch (TimeoutException e) {
+ throw new IOException(e);
+ }
+ }
+
+
+ void startMessageBlock(byte opCode, ByteBuffer payload, boolean last)
+ throws IOException {
+ // Get the timeout before we send the message. The message may
+ // trigger a session close and depending on timing the client
+ // session may close before we can read the timeout.
+ long timeout = getBlockingSendTimeout();
+ FutureToSendHandler f2sh = new FutureToSendHandler(wsSession);
+ startMessage(opCode, payload, last, f2sh);
+ try {
+ if (timeout == -1) {
+ f2sh.get();
+ } else {
+ f2sh.get(timeout, TimeUnit.MILLISECONDS);
+ }
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ } catch (ExecutionException e) {
+ throw new IOException(e);
+ } catch (TimeoutException e) {
+ throw new IOException(e);
+ }
+ }
+
+
+ void startMessage(byte opCode, ByteBuffer payload, boolean last,
+ SendHandler handler) {
+
+ wsSession.updateLastActive();
+
+ MessagePart mp = new MessagePart(opCode, payload, last, handler, this);
+
+ synchronized (messagePartLock) {
+ if (Constants.OPCODE_CLOSE == mp.getOpCode()) {
+ try {
+ setBatchingAllowed(false);
+ } catch (IOException e) {
+ WebsocketsLogger.ROOT_LOGGER.flushOnCloseFailed(e);
+ }
+ }
+ if (messagePartInProgress) {
+ if (!Util.isControl(opCode)) {
+ if (dataMessageInProgress) {
+ throw MESSAGES.messageInProgress();
+ } else {
+ dataMessageInProgress = true;
+ }
+ }
+ messagePartQueue.add(mp);
+ } else {
+ messagePartInProgress = true;
+ writeMessagePart(mp);
+ }
+ }
+ }
+
+
+ void endMessage(SendHandler handler, SendResult result,
+ boolean dataMessage) {
+ synchronized (messagePartLock) {
+
+ fragmented = nextFragmented;
+ text = nextText;
+
+ if (dataMessage) {
+ dataMessageInProgress = false;
+ }
+ MessagePart mpNext = messagePartQueue.poll();
+ if (mpNext == null) {
+ messagePartInProgress = false;
+ } else {
+ writeMessagePart(mpNext);
+ }
+ }
+
+ wsSession.updateLastActive();
+
+ handler.onResult(result);
+ }
+
+
+ void writeMessagePart(MessagePart mp) {
+
+ if (closed) {
+ throw MESSAGES.messageSessionClosed();
+ }
+
+ if (Constants.INTERNAL_OPCODE_FLUSH == mp.getOpCode()) {
+ nextFragmented = fragmented;
+ nextText = text;
+ doWrite(mp.getHandler(), outputBuffer);
+ return;
+ }
+
+ // Control messages may be sent in the middle of fragmented message
+ // so they have no effect on the fragmented or text flags
+ boolean first;
+ if (Util.isControl(mp.getOpCode())) {
+ nextFragmented = fragmented;
+ nextText = text;
+ if (mp.getOpCode() == Constants.OPCODE_CLOSE) {
+ closed = true;
+ }
+ first = true;
+ } else {
+ boolean isText = Util.isText(mp.getOpCode());
+
+ if (fragmented) {
+ // Currently fragmented
+ if (text != isText) {
+ throw MESSAGES.messageFragmentTypeChange();
+ }
+ nextText = text;
+ nextFragmented = !mp.isLast();
+ first = false;
+ } else {
+ // Wasn't fragmented. Might be now
+ if (mp.isLast()) {
+ nextFragmented = false;
+ } else {
+ nextFragmented = true;
+ nextText = isText;
+ }
+ first = true;
+ }
+ }
+
+ byte[] mask;
+
+ if (isMasked()) {
+ mask = Util.generateMask();
+ } else {
+ mask = null;
+ }
+
+ headerBuffer.clear();
+ writeHeader(headerBuffer, mp.getOpCode(), mp.getPayload(), first,
+ mp.isLast(), isMasked(), mask);
+ headerBuffer.flip();
+
+ if (getBatchingAllowed() || isMasked()) {
+ // Need to write via output buffer
+ OutputBufferSendHandler obsh = new OutputBufferSendHandler(
+ mp.getHandler(), headerBuffer, mp.getPayload(), mask,
+ outputBuffer, !getBatchingAllowed(), this);
+ obsh.write();
+ } else {
+ // Can write directly
+ doWrite(mp.getHandler(), headerBuffer, mp.getPayload());
+ }
+
+ }
+
+
+ private long getBlockingSendTimeout() {
+ Object obj = wsSession.getUserProperties().get(
+ BLOCKING_SEND_TIMEOUT_PROPERTY);
+ Long userTimeout = null;
+ if (obj instanceof Long) {
+ userTimeout = (Long) obj;
+ }
+ if (userTimeout == null) {
+ return DEFAULT_BLOCKING_SEND_TIMEOUT;
+ } else {
+ return userTimeout.longValue();
+ }
+ }
+
+
+ private static class MessagePart {
+ private final byte opCode;
+ private final ByteBuffer payload;
+ private final boolean last;
+ private final SendHandler handler;
+
+ public MessagePart(byte opCode, ByteBuffer payload, boolean last,
+ SendHandler handler, WsRemoteEndpointImplBase endpoint) {
+ this.opCode = opCode;
+ this.payload = payload;
+ this.last = last;
+ this.handler = new EndMessageHandler(
+ endpoint, handler, !Util.isControl(opCode));
+ }
+
+
+ public byte getOpCode() {
+ return opCode;
+ }
+
+
+ public ByteBuffer getPayload() {
+ return payload;
+ }
+
+
+ public boolean isLast() {
+ return last;
+ }
+
+
+ public SendHandler getHandler() {
+ return handler;
+ }
+ }
+
+
+ /**
+ * Wraps the user provided handler so that the end point is notified when
+ * the message is complete.
+ */
+ private static class EndMessageHandler implements SendHandler {
+
+ private final WsRemoteEndpointImplBase endpoint;
+ private final SendHandler handler;
+ private final boolean dataMessage;
+
+ public EndMessageHandler(WsRemoteEndpointImplBase endpoint,
+ SendHandler handler, boolean dataMessage) {
+ this.endpoint = endpoint;
+ this.handler = handler;
+ this.dataMessage = dataMessage;
+ }
+
+
+ @Override
+ public void onResult(SendResult result) {
+ endpoint.endMessage(handler, result, dataMessage);
+ }
+ }
+
+
+ public void sendObject(Object obj) throws IOException {
+ Future<Void> f = sendObjectByFuture(obj);
+ try {
+ f.get();
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ } catch (ExecutionException e) {
+ throw new IOException(e);
+ }
+ }
+
+ public Future<Void> sendObjectByFuture(Object obj) {
+ FutureToSendHandler f2sh = new FutureToSendHandler(wsSession);
+ sendObjectByCompletion(obj, f2sh);
+ return f2sh;
+ }
+
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void sendObjectByCompletion(Object obj, SendHandler completion) {
+
+ if (Util.isPrimitive(obj.getClass())) {
+ String msg = obj.toString();
+ sendStringByCompletion(msg, completion);
+ return;
+ }
+
+ Encoder encoder = findEncoder(obj);
+
+ try {
+ if (encoder instanceof Encoder.Text) {
+ String msg = ((Encoder.Text) encoder).encode(obj);
+ sendStringByCompletion(msg, completion);
+ } else if (encoder instanceof Encoder.TextStream) {
+ Writer w = null;
+ try {
+ w = getSendWriter();
+ ((Encoder.TextStream) encoder).encode(obj, w);
+ } finally {
+ if (w != null) {
+ try {
+ w.close();
+ } catch (IOException ioe) {
+ // Ignore
+ }
+ }
+ }
+ completion.onResult(new SendResult());
+ } else if (encoder instanceof Encoder.Binary) {
+ ByteBuffer msg = ((Encoder.Binary) encoder).encode(obj);
+ sendBytesByCompletion(msg, completion);
+ } else if (encoder instanceof Encoder.BinaryStream) {
+ OutputStream os = null;
+ try {
+ os = getSendStream();
+ ((Encoder.BinaryStream) encoder).encode(obj, os);
+ } finally {
+ if (os != null) {
+ try {
+ os.close();
+ } catch (IOException ioe) {
+ // Ignore
+ }
+ }
+ }
+ completion.onResult(new SendResult());
+ } else {
+ throw new EncodeException(obj,
MESSAGES.noEncoderForClass(obj.getClass().getName()));
+ }
+ } catch (EncodeException e) {
+ SendResult sr = new SendResult(e);
+ completion.onResult(sr);
+ } catch (IOException e) {
+ SendResult sr = new SendResult(e);
+ completion.onResult(sr);
+ }
+ }
+
+
+ protected void setSession(WsSession wsSession) {
+ this.wsSession = wsSession;
+ }
+
+
+ protected void setEncoders(EndpointConfig endpointConfig)
+ throws DeploymentException {
+ encoderEntries.clear();
+ for (Class<? extends Encoder> encoderClazz :
+ endpointConfig.getEncoders()) {
+ Encoder instance;
+ try {
+ instance = encoderClazz.newInstance();
+ instance.init(endpointConfig);
+ } catch (InstantiationException e) {
+ throw new DeploymentException(
+ MESSAGES.cannotInstatiateEncoder(encoderClazz.getName()), e);
+ } catch (IllegalAccessException e) {
+ throw new DeploymentException(
+ MESSAGES.cannotInstatiateEncoder(encoderClazz.getName()), e);
+ }
+ EncoderEntry entry = new EncoderEntry(
+ Util.getEncoderType(encoderClazz), instance);
+ encoderEntries.add(entry);
+ }
+ }
+
+
+ private Encoder findEncoder(Object obj) {
+ for (EncoderEntry entry : encoderEntries) {
+ if (entry.getClazz().isAssignableFrom(obj.getClass())) {
+ return entry.getEncoder();
+ }
+ }
+ return null;
+ }
+
+
+ public final void close() {
+ for (EncoderEntry entry : encoderEntries) {
+ entry.getEncoder().destroy();
+ }
+ doClose();
+ }
+
+
+ protected abstract void doWrite(SendHandler handler, ByteBuffer... data);
+ protected abstract boolean isMasked();
+ protected abstract void doClose();
+
+ private static void writeHeader(ByteBuffer headerBuffer, byte opCode,
+ ByteBuffer payload, boolean first, boolean last, boolean masked,
+ byte[] mask) {
+
+ byte b = 0;
+
+ if (last) {
+ // Set the fin bit
+ b = -128;
+ }
+
+ if (first) {
+ // This is the first fragment of this message
+ b = (byte) (b + opCode);
+ }
+ // If not the first fragment, it is a continuation with opCode of zero
+
+ headerBuffer.put(b);
+
+ if (masked) {
+ b = (byte) 0x80;
+ } else {
+ b = 0;
+ }
+
+ // Next write the mask && length length
+ if (payload.limit() < 126) {
+ headerBuffer.put((byte) (payload.limit() | b));
+ } else if (payload.limit() < 65536) {
+ headerBuffer.put((byte) (126 | b));
+ headerBuffer.put((byte) (payload.limit() >>> 8));
+ headerBuffer.put((byte) (payload.limit() & 0xFF));
+ } else {
+ // Will never be more than 2^31-1
+ headerBuffer.put((byte) (127 | b));
+ headerBuffer.put((byte) 0);
+ headerBuffer.put((byte) 0);
+ headerBuffer.put((byte) 0);
+ headerBuffer.put((byte) 0);
+ headerBuffer.put((byte) (payload.limit() >>> 24));
+ headerBuffer.put((byte) (payload.limit() >>> 16));
+ headerBuffer.put((byte) (payload.limit() >>> 8));
+ headerBuffer.put((byte) (payload.limit() & 0xFF));
+ }
+ if (masked) {
+ headerBuffer.put(mask[0]);
+ headerBuffer.put(mask[1]);
+ headerBuffer.put(mask[2]);
+ headerBuffer.put(mask[3]);
+ }
+ }
+
+
+ private static class TextMessageSendHandler implements SendHandler {
+
+ private final SendHandler handler;
+ private final CharBuffer message;
+ private final boolean isLast;
+ private final CharsetEncoder encoder;
+ private final ByteBuffer buffer;
+ private final WsRemoteEndpointImplBase endpoint;
+ private volatile boolean isDone = false;
+
+ public TextMessageSendHandler(SendHandler handler, CharBuffer message,
+ boolean isLast, CharsetEncoder encoder,
+ ByteBuffer encoderBuffer, WsRemoteEndpointImplBase endpoint) {
+ this.handler = handler;
+ this.message = message;
+ this.isLast = isLast;
+ this.encoder = encoder.reset();
+ this.buffer = encoderBuffer;
+ this.endpoint = endpoint;
+ }
+
+ public void write() {
+ buffer.clear();
+ CoderResult cr = encoder.encode(message, buffer, true);
+ if (cr.isError()) {
+ throw new IllegalArgumentException(cr.toString());
+ }
+ isDone = !cr.isOverflow();
+ buffer.flip();
+ endpoint.startMessage(Constants.OPCODE_TEXT, buffer,
+ isDone && isLast, this);
+ }
+
+ @Override
+ public void onResult(SendResult result) {
+ if (isDone || !result.isOK()) {
+ handler.onResult(result);
+ } else {
+ write();
+ }
+ }
+ }
+
+
+ /**
+ * Used to write data to the output buffer, flushing the buffer if it fills
+ * up.
+ */
+ private static class OutputBufferSendHandler implements SendHandler {
+
+ private final SendHandler handler;
+ private final ByteBuffer headerBuffer;
+ private final ByteBuffer payload;
+ private final byte[] mask;
+ private final ByteBuffer outputBuffer;
+ private final boolean flushRequired;
+ private final WsRemoteEndpointImplBase endpoint;
+ private int maskIndex = 0;
+
+ public OutputBufferSendHandler(SendHandler completion,
+ ByteBuffer headerBuffer, ByteBuffer payload, byte[] mask,
+ ByteBuffer outputBuffer, boolean flushRequired,
+ WsRemoteEndpointImplBase endpoint) {
+ this.handler = completion;
+ this.headerBuffer = headerBuffer;
+ this.payload = payload;
+ this.mask = mask;
+ this.outputBuffer = outputBuffer;
+ this.flushRequired = flushRequired;
+ this.endpoint = endpoint;
+ }
+
+ public void write() {
+ // Write the header
+ while (headerBuffer.hasRemaining() && outputBuffer.hasRemaining()) {
+ outputBuffer.put(headerBuffer.get());
+ }
+ if (headerBuffer.hasRemaining()) {
+ // Still more headers to write, need to flush
+ outputBuffer.flip();
+ endpoint.doWrite(this, outputBuffer);
+ return;
+ }
+
+ // Write the payload
+ int payloadLeft = payload.remaining();
+ int payloadLimit = payload.limit();
+ int outputSpace = outputBuffer.remaining();
+ int toWrite = payloadLeft;
+
+ if (payloadLeft > outputSpace) {
+ toWrite = outputSpace;
+ // Temporarily reduce the limit
+ payload.limit(payload.position() + toWrite);
+ }
+
+ if (mask == null) {
+ // Use a bulk copy
+ outputBuffer.put(payload);
+ } else {
+ for (int i = 0; i < toWrite; i++) {
+ outputBuffer.put(
+ (byte) (payload.get() ^ (mask[maskIndex++] & 0xFF)));
+ if (maskIndex > 3) {
+ maskIndex = 0;
+ }
+ }
+ }
+
+ if (payloadLeft > outputSpace) {
+ // Restore the original limit
+ payload.limit(payloadLimit);
+ // Still more headers to write, need to flush
+ outputBuffer.flip();
+ endpoint.doWrite(this, outputBuffer);
+ return;
+ }
+
+ if (flushRequired) {
+ outputBuffer.flip();
+ if (outputBuffer.remaining() == 0) {
+ handler.onResult(new SendResult());
+ } else {
+ endpoint.doWrite(this, outputBuffer);
+ }
+ } else {
+ handler.onResult(new SendResult());
+ }
+ }
+
+ // ------------------------------------------------- SendHandler methods
+ @Override
+ public void onResult(SendResult result) {
+ if (result.isOK()) {
+ if (outputBuffer.hasRemaining()) {
+ endpoint.doWrite(this, outputBuffer);
+ } else {
+ outputBuffer.clear();
+ write();
+ }
+ } else {
+ handler.onResult(result);
+ }
+ }
+ }
+
+ private static class WsOutputStream extends OutputStream {
+
+ private final WsRemoteEndpointImplBase endpoint;
+ private final ByteBuffer buffer = ByteBuffer.allocate(8192);
+ private final Object closeLock = new Object();
+ private volatile boolean closed = false;
+
+ public WsOutputStream(WsRemoteEndpointImplBase endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ if (closed) {
+ throw MESSAGES.closedOutputStream();
+ }
+
+ if (buffer.remaining() == 0) {
+ flush();
+ }
+ buffer.put((byte) b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ if (closed) {
+ throw MESSAGES.closedOutputStream();
+ }
+ if (len == 0) {
+ return;
+ }
+ if ((off < 0) || (off > b.length) || (len < 0) ||
+ ((off + len) > b.length) || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (buffer.remaining() == 0) {
+ flush();
+ }
+ int remaining = buffer.remaining();
+ int written = 0;
+
+ while (remaining < len - written) {
+ buffer.put(b, off + written, remaining);
+ written += remaining;
+ flush();
+ remaining = buffer.remaining();
+ }
+ buffer.put(b, off + written, len - written);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (closed) {
+ throw MESSAGES.closedOutputStream();
+ }
+
+ doWrite(false);
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (closeLock) {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ }
+
+ doWrite(true);
+ }
+
+ private void doWrite(boolean last) throws IOException {
+ buffer.flip();
+ endpoint.sendPartialBytes(buffer, last);
+ buffer.clear();
+ }
+ }
+
+
+ private static class WsWriter extends Writer {
+
+ private final WsRemoteEndpointImplBase endpoint;
+ private final CharBuffer buffer = CharBuffer.allocate(8192);
+ private final Object closeLock = new Object();
+ private volatile boolean closed = false;
+
+ public WsWriter(WsRemoteEndpointImplBase endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ @Override
+ public void write(char[] cbuf, int off, int len) throws IOException {
+ if (closed) {
+ throw MESSAGES.closedWriter();
+ }
+ if (len == 0) {
+ return;
+ }
+ if ((off < 0) || (off > cbuf.length) || (len < 0) ||
+ ((off + len) > cbuf.length) || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (buffer.remaining() == 0) {
+ flush();
+ }
+ int remaining = buffer.remaining();
+ int written = 0;
+
+ while (remaining < len - written) {
+ buffer.put(cbuf, off + written, remaining);
+ written += remaining;
+ flush();
+ remaining = buffer.remaining();
+ }
+ buffer.put(cbuf, off + written, len - written);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (closed) {
+ throw MESSAGES.closedWriter();
+ }
+
+ doWrite(false);
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (closeLock) {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ }
+
+ doWrite(true);
+ }
+
+ private void doWrite(boolean last) throws IOException {
+ buffer.flip();
+ endpoint.sendPartialString(buffer, last);
+ buffer.clear();
+ }
+ }
+
+
+ private static class EncoderEntry {
+
+ private final Class<?> clazz;
+ private final Encoder encoder;
+
+ public EncoderEntry(Class<?> clazz, Encoder encoder) {
+ this.clazz = clazz;
+ this.encoder = encoder;
+ }
+
+ public Class<?> getClazz() {
+ return clazz;
+ }
+
+ public Encoder getEncoder() {
+ return encoder;
+ }
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointImplClient.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointImplClient.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsRemoteEndpointImplClient.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeUnit;
+
+import javax.websocket.SendHandler;
+
+public class WsRemoteEndpointImplClient extends WsRemoteEndpointImplBase {
+
+ private final AsyncChannelWrapper channel;
+
+ public WsRemoteEndpointImplClient(AsyncChannelWrapper channel) {
+ this.channel = channel;
+ }
+
+
+ @Override
+ protected boolean isMasked() {
+ return true;
+ }
+
+
+ @Override
+ protected void doWrite(SendHandler handler, ByteBuffer... data) {
+ long timeout = getSendTimeout();
+ if (timeout < 1) {
+ timeout = Long.MAX_VALUE;
+
+ }
+ SendHandlerToCompletionHandler sh2ch =
+ new SendHandlerToCompletionHandler(handler);
+ channel.write(data, 0, data.length, timeout, TimeUnit.MILLISECONDS,
+ null, sh2ch);
+ }
+
+ @Override
+ protected void doClose() {
+ channel.close();
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsSession.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsSession.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsSession.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,632 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Extension;
+import javax.websocket.MessageHandler;
+import javax.websocket.PongMessage;
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.SendResult;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+import org.jboss.web.WebsocketsLogger;
+
+public class WsSession implements Session {
+
+ // An ellipsis is a single character that looks like three periods in a row
+ // and is used to indicate a continuation.
+ private static final byte[] ELLIPSIS_BYTES =
+ "\u2026".getBytes(StandardCharsets.UTF_8);
+ // An ellipsis is three bytes in UTF-8
+ private static final int ELLIPSIS_BYTES_LEN = ELLIPSIS_BYTES.length;
+
+ private static AtomicLong ids = new AtomicLong(0);
+
+ private final Endpoint localEndpoint;
+ private final WsRemoteEndpointImplBase wsRemoteEndpoint;
+ private final RemoteEndpoint.Async remoteEndpointAsync;
+ private final RemoteEndpoint.Basic remoteEndpointBasic;
+ private final ClassLoader applicationClassLoader;
+ private final WsWebSocketContainer webSocketContainer;
+ private final URI requestUri;
+ private final Map<String,List<String>> requestParameterMap;
+ private final String queryString;
+ private final Principal userPrincipal;
+ private final EndpointConfig endpointConfig;
+
+ private final String subProtocol;
+ private final Map<String,String> pathParameters;
+ private final boolean secure;
+ private final String httpSessionId;
+ private final String id;
+
+ // Expected to handle message types of <String> only
+ private MessageHandler textMessageHandler = null;
+ // Expected to handle message types of <ByteBuffer> only
+ private MessageHandler binaryMessageHandler = null;
+ private MessageHandler.Whole<PongMessage> pongMessageHandler = null;
+ private volatile State state = State.OPEN;
+ private final Object stateLock = new Object();
+ private final Map<String,Object> userProperties = new
ConcurrentHashMap<String, Object>();
+ private volatile int maxBinaryMessageBufferSize =
+ Constants.DEFAULT_BUFFER_SIZE;
+ private volatile int maxTextMessageBufferSize =
+ Constants.DEFAULT_BUFFER_SIZE;
+ private volatile long maxIdleTimeout = 0;
+ private volatile long lastActive = System.currentTimeMillis();
+ private Map<FutureToSendHandler,FutureToSendHandler> futures =
+ new ConcurrentHashMap<FutureToSendHandler,FutureToSendHandler>();
+
+ /**
+ * Creates a new WebSocket session for communication between the two
+ * provided end points. The result of {@link Thread#getContextClassLoader()}
+ * at the time this constructor is called will be used when calling
+ * {@link Endpoint#onClose(Session, CloseReason)}.
+ *
+ * @param localEndpoint
+ * @param wsRemoteEndpoint
+ * @throws DeploymentException
+ */
+ public WsSession(Endpoint localEndpoint,
+ WsRemoteEndpointImplBase wsRemoteEndpoint,
+ WsWebSocketContainer wsWebSocketContainer,
+ URI requestUri, Map<String,List<String>> requestParameterMap,
+ String queryString, Principal userPrincipal, String httpSessionId,
+ String subProtocol, Map<String,String> pathParameters,
+ boolean secure, EndpointConfig endpointConfig)
+ throws DeploymentException {
+ this.localEndpoint = localEndpoint;
+ this.wsRemoteEndpoint = wsRemoteEndpoint;
+ this.wsRemoteEndpoint.setSession(this);
+ this.remoteEndpointAsync = new WsRemoteEndpointAsync(wsRemoteEndpoint);
+ this.remoteEndpointBasic = new WsRemoteEndpointBasic(wsRemoteEndpoint);
+ this.webSocketContainer = wsWebSocketContainer;
+ applicationClassLoader = Thread.currentThread().getContextClassLoader();
+ wsRemoteEndpoint.setSendTimeout(
+ wsWebSocketContainer.getDefaultAsyncSendTimeout());
+ this.maxBinaryMessageBufferSize =
+ webSocketContainer.getDefaultMaxBinaryMessageBufferSize();
+ this.maxTextMessageBufferSize =
+ webSocketContainer.getDefaultMaxTextMessageBufferSize();
+ this.maxIdleTimeout =
+ webSocketContainer.getDefaultMaxSessionIdleTimeout();
+ this.requestUri = requestUri;
+ if (requestParameterMap == null) {
+ this.requestParameterMap = Collections.emptyMap();
+ } else {
+ this.requestParameterMap = requestParameterMap;
+ }
+ this.queryString = queryString;
+ this.userPrincipal = userPrincipal;
+ this.httpSessionId = httpSessionId;
+ if (subProtocol == null) {
+ this.subProtocol = "";
+ } else {
+ this.subProtocol = subProtocol;
+ }
+ this.pathParameters = pathParameters;
+ this.secure = secure;
+ this.wsRemoteEndpoint.setEncoders(endpointConfig);
+ this.endpointConfig = endpointConfig;
+
+ this.userProperties.putAll(endpointConfig.getUserProperties());
+ this.id = Long.toHexString(ids.getAndIncrement());
+ }
+
+
+ @Override
+ public WebSocketContainer getContainer() {
+ checkState();
+ return webSocketContainer;
+ }
+
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void addMessageHandler(MessageHandler listener) {
+
+ checkState();
+
+ // Message handlers that require decoders may map to text messages,
+ // binary messages, both or neither.
+
+ // The frame processing code expects binary message handlers to
+ // accept ByteBuffer
+
+ // Use the POJO message handler wrappers as they are designed to wrap
+ // arbitrary objects with MessageHandlers and can wrap MessageHandlers
+ // just as easily.
+
+ Set<MessageHandlerResult> mhResults =
+ Util.getMessageHandlers(listener, endpointConfig);
+
+ for (MessageHandlerResult mhResult : mhResults) {
+ switch (mhResult.getType()) {
+ case TEXT: {
+ if (textMessageHandler != null) {
+ throw MESSAGES.duplicateHandlerText();
+ }
+ textMessageHandler = mhResult.getHandler();
+ break;
+ }
+ case BINARY: {
+ if (binaryMessageHandler != null) {
+ throw MESSAGES.duplicateHandlerBinary();
+ }
+ binaryMessageHandler = mhResult.getHandler();
+ break;
+ }
+ case PONG: {
+ if (pongMessageHandler != null) {
+ throw MESSAGES.duplicateHandlerPong();
+ }
+ MessageHandler handler = mhResult.getHandler();
+ if (handler instanceof MessageHandler.Whole<?>) {
+ pongMessageHandler =
+ (MessageHandler.Whole<PongMessage>) handler;
+ } else {
+ throw MESSAGES.invalidHandlerPong();
+ }
+
+ break;
+ }
+ default: {
+ throw MESSAGES.invalidMessageHandler(listener,
+ mhResult.getType());
+ }
+ }
+ }
+ }
+
+
+ @Override
+ public Set<MessageHandler> getMessageHandlers() {
+ checkState();
+ Set<MessageHandler> result = new HashSet<MessageHandler>();
+ if (binaryMessageHandler != null) {
+ result.add(binaryMessageHandler);
+ }
+ if (textMessageHandler != null) {
+ result.add(textMessageHandler);
+ }
+ if (pongMessageHandler != null) {
+ result.add(pongMessageHandler);
+ }
+ return result;
+ }
+
+
+ @Override
+ public void removeMessageHandler(MessageHandler listener) {
+ checkState();
+ if (listener == null) {
+ return;
+ }
+
+ MessageHandler wrapped = null;
+
+ if (listener instanceof WrappedMessageHandler) {
+ wrapped = ((WrappedMessageHandler) listener).getWrappedHandler();
+ }
+
+ if (wrapped == null) {
+ wrapped = listener;
+ }
+
+ boolean removed = false;
+ if (wrapped.equals(textMessageHandler) ||
+ listener.equals(textMessageHandler)) {
+ textMessageHandler = null;
+ removed = true;
+ }
+
+ if (listener.equals(binaryMessageHandler) ||
+ listener.equals(binaryMessageHandler)) {
+ binaryMessageHandler = null;
+ removed = true;
+ }
+
+ if (listener.equals(pongMessageHandler) ||
+ listener.equals(pongMessageHandler)) {
+ pongMessageHandler = null;
+ removed = true;
+ }
+
+ if (!removed) {
+ // ISE for now. Could swallow this silently / log this if the ISE
+ // becomes a problem
+ throw MESSAGES.cannotRemoveHandler(listener);
+ }
+ }
+
+
+ @Override
+ public String getProtocolVersion() {
+ checkState();
+ return Constants.WS_VERSION_HEADER_VALUE;
+ }
+
+
+ @Override
+ public String getNegotiatedSubprotocol() {
+ checkState();
+ return subProtocol;
+ }
+
+
+ @Override
+ public List<Extension> getNegotiatedExtensions() {
+ checkState();
+ return Collections.emptyList();
+ }
+
+
+ @Override
+ public boolean isSecure() {
+ checkState();
+ return secure;
+ }
+
+
+ @Override
+ public boolean isOpen() {
+ return state == State.OPEN;
+ }
+
+
+ @Override
+ public long getMaxIdleTimeout() {
+ checkState();
+ return maxIdleTimeout;
+ }
+
+
+ @Override
+ public void setMaxIdleTimeout(long timeout) {
+ checkState();
+ this.maxIdleTimeout = timeout;
+ }
+
+
+ @Override
+ public void setMaxBinaryMessageBufferSize(int max) {
+ checkState();
+ this.maxBinaryMessageBufferSize = max;
+ }
+
+
+ @Override
+ public int getMaxBinaryMessageBufferSize() {
+ checkState();
+ return maxBinaryMessageBufferSize;
+ }
+
+
+ @Override
+ public void setMaxTextMessageBufferSize(int max) {
+ checkState();
+ this.maxTextMessageBufferSize = max;
+ }
+
+
+ @Override
+ public int getMaxTextMessageBufferSize() {
+ checkState();
+ return maxTextMessageBufferSize;
+ }
+
+
+ @Override
+ public Set<Session> getOpenSessions() {
+ checkState();
+ return webSocketContainer.getOpenSessions(localEndpoint.getClass());
+ }
+
+
+ @Override
+ public RemoteEndpoint.Async getAsyncRemote() {
+ checkState();
+ return remoteEndpointAsync;
+ }
+
+
+ @Override
+ public RemoteEndpoint.Basic getBasicRemote() {
+ checkState();
+ return remoteEndpointBasic;
+ }
+
+
+ @Override
+ public void close() throws IOException {
+ close(new CloseReason(CloseCodes.NORMAL_CLOSURE, ""));
+ }
+
+
+ @Override
+ public void close(CloseReason closeReason) throws IOException {
+ doClose(closeReason, closeReason);
+ }
+
+
+ /**
+ * WebSocket 1.0. Section 2.1.5.
+ * Need internal close method as spec requires that the local endpoint
+ * receives a 1006 on timeout.
+ */
+ private void doClose(CloseReason closeReasonMessage,
+ CloseReason closeReasonLocal) {
+ // Double-checked locking. OK because state is volatile
+ if (state != State.OPEN) {
+ return;
+ }
+
+ synchronized (stateLock) {
+ if (state != State.OPEN) {
+ return;
+ }
+
+ state = State.CLOSING;
+
+ sendCloseMessage(closeReasonMessage);
+ fireEndpointOnClose(closeReasonLocal);
+
+ state = State.CLOSED;
+ }
+
+ IOException ioe = new IOException(MESSAGES.messageFailed());
+ SendResult sr = new SendResult(ioe);
+ for (FutureToSendHandler f2sh : futures.keySet()) {
+ f2sh.onResult(sr);
+ }
+ }
+
+
+ /**
+ * Called when a close message is received. Should only ever happen once.
+ * Also called after a protocol error when the ProtocolHandler needs to
+ * force the closing of the connection.
+ */
+ public void onClose(CloseReason closeReason) {
+
+ synchronized (stateLock) {
+ if (state == State.OPEN) {
+ sendCloseMessage(closeReason);
+ fireEndpointOnClose(closeReason);
+ state = State.CLOSED;
+ }
+
+ // Close the socket
+ wsRemoteEndpoint.close();
+ }
+ }
+
+
+ private void fireEndpointOnClose(CloseReason closeReason) {
+
+ // Fire the onClose event
+ Thread t = Thread.currentThread();
+ ClassLoader cl = t.getContextClassLoader();
+ t.setContextClassLoader(applicationClassLoader);
+ try {
+ localEndpoint.onClose(this, closeReason);
+ } finally {
+ t.setContextClassLoader(cl);
+ }
+ }
+
+
+ private void sendCloseMessage(CloseReason closeReason) {
+ // 125 is maximum size for the payload of a control message
+ ByteBuffer msg = ByteBuffer.allocate(125);
+ msg.putShort((short) closeReason.getCloseCode().getCode());
+
+ String reason = closeReason.getReasonPhrase();
+ if (reason != null && reason.length() > 0) {
+ appendCloseReasonWithTruncation(msg, reason);
+ }
+ msg.flip();
+ try {
+ wsRemoteEndpoint.startMessageBlock(
+ Constants.OPCODE_CLOSE, msg, true);
+ } catch (IOException ioe) {
+ // Failed to send close message. Close the socket and let the caller
+ // deal with the Exception
+ WebsocketsLogger.ROOT_LOGGER.closeMessageFail(ioe);
+ wsRemoteEndpoint.close();
+ localEndpoint.onError(this, ioe);
+ } finally {
+ webSocketContainer.unregisterSession(localEndpoint, this);
+ }
+ }
+
+
+ /**
+ * Use protected so unit tests can access this method directly.
+ */
+ protected static void appendCloseReasonWithTruncation(ByteBuffer msg,
+ String reason) {
+ // Once the close code has been added there are a maximum of 123 bytes
+ // left for the reason phrase. If it is truncated then care needs to be
+ // taken to ensure the bytes are not truncated in the middle of a
+ // multi-byte UTF-8 character.
+ byte[] reasonBytes = reason.getBytes(StandardCharsets.UTF_8);
+
+ if (reasonBytes.length <= 123) {
+ // No need to truncate
+ msg.put(reasonBytes);
+ } else {
+ // Need to truncate
+ int remaining = 123 - ELLIPSIS_BYTES_LEN;
+ int pos = 0;
+ byte[] bytesNext = reason.substring(pos, pos + 1).getBytes(
+ StandardCharsets.UTF_8);
+ while (remaining >= bytesNext.length) {
+ msg.put(bytesNext);
+ remaining -= bytesNext.length;
+ pos++;
+ bytesNext = reason.substring(pos, pos + 1).getBytes(
+ StandardCharsets.UTF_8);
+ }
+ msg.put(ELLIPSIS_BYTES);
+ }
+ }
+
+
+ /**
+ * Make the session aware of a {@link FutureToSendHandler} that will need to
+ * be forcibly closed if the session closes before the
+ * {@link FutureToSendHandler} completes.
+ */
+ protected void registerFuture(FutureToSendHandler f2sh) {
+ futures.put(f2sh, f2sh);
+ }
+
+
+ /**
+ * Remove a {@link FutureToSendHandler} from the set of tracked instances.
+ */
+ protected void unregisterFuture(FutureToSendHandler f2sh) {
+ futures.remove(f2sh);
+ }
+
+
+ @Override
+ public URI getRequestURI() {
+ checkState();
+ return requestUri;
+ }
+
+
+ @Override
+ public Map<String,List<String>> getRequestParameterMap() {
+ checkState();
+ return requestParameterMap;
+ }
+
+
+ @Override
+ public String getQueryString() {
+ checkState();
+ return queryString;
+ }
+
+
+ @Override
+ public Principal getUserPrincipal() {
+ checkState();
+ return userPrincipal;
+ }
+
+
+ @Override
+ public Map<String,String> getPathParameters() {
+ checkState();
+ return pathParameters;
+ }
+
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+
+ @Override
+ public Map<String,Object> getUserProperties() {
+ checkState();
+ return userProperties;
+ }
+
+
+ public Endpoint getLocal() {
+ return localEndpoint;
+ }
+
+
+ public String getHttpSessionId() {
+ return httpSessionId;
+ }
+
+
+ protected MessageHandler getTextMessageHandler() {
+ return textMessageHandler;
+ }
+
+
+ protected MessageHandler getBinaryMessageHandler() {
+ return binaryMessageHandler;
+ }
+
+
+ protected MessageHandler.Whole<PongMessage> getPongMessageHandler() {
+ return pongMessageHandler;
+ }
+
+
+ protected void updateLastActive() {
+ lastActive = System.currentTimeMillis();
+ }
+
+
+ protected void checkExpiration() {
+ long timeout = maxIdleTimeout;
+ if (timeout < 1) {
+ return;
+ }
+
+ if (System.currentTimeMillis() - lastActive > timeout) {
+ String msg = MESSAGES.sessionTimeout();
+ doClose(new CloseReason(CloseCodes.GOING_AWAY, msg),
+ new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg));
+ }
+ }
+
+
+ private void checkState() {
+ if (state == State.CLOSED) {
+ throw MESSAGES.sessionAlreadyClosed();
+ }
+ }
+
+ private static enum State {
+ OPEN,
+ CLOSING,
+ CLOSED
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/WsWebSocketContainer.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,821 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousChannelGroup;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.TrustManagerFactory;
+import javax.websocket.ClientEndpoint;
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.Extension;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+import org.apache.tomcat.util.codec.binary.Base64;
+import org.apache.tomcat.util.threads.ThreadPoolExecutor;
+import org.apache.tomcat.websocket.pojo.PojoEndpointClient;
+import org.jboss.web.WebsocketsLogger;
+
+public class WsWebSocketContainer
+ implements WebSocketContainer, BackgroundProcess {
+
+ /**
+ * Property name to set to configure the value that is passed to
+ * {@link SSLEngine#setEnabledProtocols(String[])}. The value should be a
+ * comma separated string.
+ */
+ public static final String SSL_PROTOCOLS_PROPERTY =
+ "org.apache.tomcat.websocket.SSL_PROTOCOLS";
+ public static final String SSL_TRUSTSTORE_PROPERTY =
+ "org.apache.tomcat.websocket.SSL_TRUSTSTORE";
+ public static final String SSL_TRUSTSTORE_PWD_PROPERTY =
+ "org.apache.tomcat.websocket.SSL_TRUSTSTORE_PWD";
+ public static final String SSL_TRUSTSTORE_PWD_DEFAULT = "changeit";
+
+ private static final Random random = new Random();
+ private static final byte[] crlf = new byte[] {13, 10};
+ private static final AsynchronousChannelGroup asynchronousChannelGroup;
+
+ static {
+ AsynchronousChannelGroup result = null;
+
+ // Need to do this with the right thread context class loader else the
+ // first web app to call this will trigger a leak
+ ClassLoader original = Thread.currentThread().getContextClassLoader();
+
+ try {
+ Thread.currentThread().setContextClassLoader(
+ AsyncIOThreadFactory.class.getClassLoader());
+
+ // These are the same settings as the default
+ // AsynchronousChannelGroup
+ int initialSize = Runtime.getRuntime().availableProcessors();
+ ExecutorService executorService = new ThreadPoolExecutor(
+ 0,
+ Integer.MAX_VALUE,
+ Long.MAX_VALUE, TimeUnit.MILLISECONDS,
+ new SynchronousQueue<Runnable>(),
+ new AsyncIOThreadFactory());
+
+ try {
+ result = AsynchronousChannelGroup.withCachedThreadPool(
+ executorService, initialSize);
+ } catch (IOException e) {
+ // No good reason for this to happen.
+ throw MESSAGES.asyncGroupFail();
+ }
+ } finally {
+ Thread.currentThread().setContextClassLoader(original);
+ }
+
+ asynchronousChannelGroup = result;
+ }
+
+ private final Map<Class<?>, Set<WsSession>> endpointSessionMap =
+ new HashMap<Class<?>, Set<WsSession>>();
+ private final Map<WsSession,WsSession> sessions = new
ConcurrentHashMap<WsSession, WsSession>();
+ private final Object endPointSessionMapLock = new Object();
+
+ private long defaultAsyncTimeout = -1;
+ private int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ private int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ private volatile long defaultMaxSessionIdleTimeout = 0;
+ private int backgroundProcessCount = 0;
+ private int processPeriod = 10;
+
+
+ @Override
+ public Session connectToServer(Object pojo, URI path)
+ throws DeploymentException {
+
+ ClientEndpoint annotation =
+ pojo.getClass().getAnnotation(ClientEndpoint.class);
+ if (annotation == null) {
+ throw new DeploymentException(
+
MESSAGES.missingClientEndpointAnnotation(pojo.getClass().getName()));
+ }
+
+ Endpoint ep = new PojoEndpointClient(pojo, annotation.decoders());
+
+ Class<? extends ClientEndpointConfig.Configurator> configuratorClazz =
+ pojo.getClass().getAnnotation(
+ ClientEndpoint.class).configurator();
+
+ ClientEndpointConfig.Configurator configurator = null;
+ if (!ClientEndpointConfig.Configurator.class.equals(
+ configuratorClazz)) {
+ try {
+ configurator = configuratorClazz.newInstance();
+ } catch (InstantiationException e) {
+ throw new DeploymentException(MESSAGES.defaultConfiguratorFailed(), e);
+ } catch (IllegalAccessException e) {
+ throw new DeploymentException(MESSAGES.defaultConfiguratorFailed(), e);
+ }
+ }
+
+ ClientEndpointConfig config = ClientEndpointConfig.Builder.create().
+ configurator(configurator).
+ decoders(Arrays.asList(annotation.decoders())).
+ encoders(Arrays.asList(annotation.encoders())).
+ build();
+ return connectToServer(ep, config, path);
+ }
+
+
+ @Override
+ public Session connectToServer(Class<?> annotatedEndpointClass, URI path)
+ throws DeploymentException {
+
+ Object pojo;
+ try {
+ pojo = annotatedEndpointClass.newInstance();
+ } catch (InstantiationException e) {
+ throw new
DeploymentException(MESSAGES.endpointCreateFailed(annotatedEndpointClass.getName()), e);
+ } catch (IllegalAccessException e) {
+ throw new
DeploymentException(MESSAGES.endpointCreateFailed(annotatedEndpointClass.getName()), e);
+ }
+
+ return connectToServer(pojo, path);
+ }
+
+
+ @Override
+ public Session connectToServer(Class<? extends Endpoint> clazz,
+ ClientEndpointConfig clientEndpointConfiguration, URI path)
+ throws DeploymentException {
+
+ Endpoint endpoint;
+ try {
+ endpoint = clazz.newInstance();
+ } catch (InstantiationException e) {
+ throw new DeploymentException(MESSAGES.endpointCreateFailed(clazz.getName()),
e);
+ } catch (IllegalAccessException e) {
+ throw new DeploymentException(MESSAGES.endpointCreateFailed(clazz.getName()),
e);
+ }
+
+ return connectToServer(endpoint, clientEndpointConfiguration, path);
+ }
+
+
+ @Override
+ public Session connectToServer(Endpoint endpoint,
+ ClientEndpointConfig clientEndpointConfiguration, URI path)
+ throws DeploymentException {
+
+ boolean secure = false;
+
+ String scheme = path.getScheme();
+ if (!("ws".equalsIgnoreCase(scheme) ||
+ "wss".equalsIgnoreCase(scheme))) {
+ throw new DeploymentException(MESSAGES.pathWrongScheme(scheme));
+ }
+ String host = path.getHost();
+ if (host == null) {
+ throw new DeploymentException(MESSAGES.pathNoHost());
+ }
+ int port = path.getPort();
+ Map<String,List<String>> reqHeaders = createRequestHeaders(host,
port,
+ clientEndpointConfiguration.getPreferredSubprotocols(),
+ clientEndpointConfiguration.getExtensions());
+ clientEndpointConfiguration.getConfigurator().
+ beforeRequest(reqHeaders);
+
+ ByteBuffer request = createRequest(path, reqHeaders);
+
+ SocketAddress sa;
+ if (port == -1) {
+ if ("ws".equalsIgnoreCase(scheme)) {
+ sa = new InetSocketAddress(host, 80);
+ } else if ("wss".equalsIgnoreCase(scheme)) {
+ sa = new InetSocketAddress(host, 443);
+ secure = true;
+ } else {
+ throw new DeploymentException(MESSAGES.invalidScheme(scheme));
+ }
+ } else {
+ if ("wss".equalsIgnoreCase(scheme)) {
+ secure = true;
+ }
+ sa = new InetSocketAddress(host, port);
+ }
+
+ AsynchronousSocketChannel socketChannel;
+ try {
+ socketChannel =
+ AsynchronousSocketChannel.open(asynchronousChannelGroup);
+ } catch (IOException ioe) {
+ throw new DeploymentException(MESSAGES.connectionFailed(), ioe);
+ }
+
+ Future<Void> fConnect = socketChannel.connect(sa);
+
+ AsyncChannelWrapper channel;
+ if (secure) {
+ SSLEngine sslEngine = createSSLEngine(
+ clientEndpointConfiguration.getUserProperties());
+ channel = new AsyncChannelWrapperSecure(socketChannel, sslEngine);
+ } else {
+ channel = new AsyncChannelWrapperNonSecure(socketChannel);
+ }
+
+ ByteBuffer response;
+ String subProtocol;
+ try {
+ fConnect.get();
+
+ Future<Void> fHandshake = channel.handshake();
+ fHandshake.get();
+
+ int toWrite = request.limit();
+
+ Future<Integer> fWrite = channel.write(request);
+ Integer thisWrite = fWrite.get();
+ toWrite -= thisWrite.intValue();
+
+ while (toWrite > 0) {
+ fWrite = channel.write(request);
+ thisWrite = fWrite.get();
+ toWrite -= thisWrite.intValue();
+ }
+ // Same size as the WsFrame input buffer
+ response = ByteBuffer.allocate(maxBinaryMessageBufferSize);
+
+ HandshakeResponse handshakeResponse =
+ processResponse(response, channel);
+ clientEndpointConfiguration.getConfigurator().
+ afterResponse(handshakeResponse);
+
+ // Sub-protocol
+ // Header names are always stored in lower case
+ List<String> values = handshakeResponse.getHeaders().get(
+ Constants.WS_PROTOCOL_HEADER_NAME_LOWER);
+ if (values == null || values.size() == 0) {
+ subProtocol = null;
+ } else if (values.size() == 1) {
+ subProtocol = values.get(0);
+ } else {
+ throw new DeploymentException(MESSAGES.invalidProtocolHeader());
+ }
+ } catch (ExecutionException e) {
+ throw new DeploymentException(MESSAGES.httpRequestFailed(), e);
+ } catch (InterruptedException e) {
+ throw new DeploymentException(MESSAGES.httpRequestFailed(), e);
+ } catch (SSLException e) {
+ throw new DeploymentException(MESSAGES.httpRequestFailed(), e);
+ } catch (EOFException e) {
+ throw new DeploymentException(MESSAGES.httpRequestFailed(), e);
+ }
+
+ // Switch to WebSocket
+ WsRemoteEndpointImplClient wsRemoteEndpointClient =
+ new WsRemoteEndpointImplClient(channel);
+
+
+ WsSession wsSession = new WsSession(endpoint, wsRemoteEndpointClient,
+ this, null, null, null, null, null, subProtocol,
+ Collections.<String, String> emptyMap(), false,
+ clientEndpointConfiguration);
+ endpoint.onOpen(wsSession, clientEndpointConfiguration);
+ registerSession(endpoint, wsSession);
+
+ // Object creation will trigger input processing
+ @SuppressWarnings("unused")
+ WsFrameClient wsFrameClient = new WsFrameClient(response, channel,
+ wsSession);
+
+ return wsSession;
+ }
+
+
+ protected void registerSession(Endpoint endpoint, WsSession wsSession) {
+
+ Class<?> endpointClazz = endpoint.getClass();
+
+ if (!wsSession.isOpen()) {
+ // The session was closed during onOpen. No need to register it.
+ return;
+ }
+ synchronized (endPointSessionMapLock) {
+ if (endpointSessionMap.size() == 0) {
+ BackgroundProcessManager.getInstance().register(this);
+ }
+ Set<WsSession> wsSessions = endpointSessionMap.get(endpointClazz);
+ if (wsSessions == null) {
+ wsSessions = new HashSet<WsSession>();
+ endpointSessionMap.put(endpointClazz, wsSessions);
+ }
+ wsSessions.add(wsSession);
+ }
+ sessions.put(wsSession, wsSession);
+ }
+
+
+ protected void unregisterSession(Endpoint endpoint, WsSession wsSession) {
+
+ Class<?> endpointClazz = endpoint.getClass();
+
+ synchronized (endPointSessionMapLock) {
+ Set<WsSession> wsSessions = endpointSessionMap.get(endpointClazz);
+ if (wsSessions != null) {
+ wsSessions.remove(wsSession);
+ if (wsSessions.size() == 0) {
+ endpointSessionMap.remove(endpointClazz);
+ }
+ }
+ if (endpointSessionMap.size() == 0) {
+ BackgroundProcessManager.getInstance().unregister(this);
+ }
+ }
+ sessions.remove(wsSession);
+ }
+
+
+ Set<Session> getOpenSessions(Class<?> endpoint) {
+ HashSet<Session> result = new HashSet<Session>();
+ synchronized (endPointSessionMapLock) {
+ Set<WsSession> sessions = endpointSessionMap.get(endpoint);
+ if (sessions != null) {
+ result.addAll(sessions);
+ }
+ }
+ return result;
+ }
+
+ private Map<String,List<String>> createRequestHeaders(String host,
+ int port, List<String> subProtocols, List<Extension> extensions)
{
+
+ Map<String,List<String>> headers = new HashMap<String,
List<String>>();
+
+ // Host header
+ List<String> hostValues = new ArrayList<String>(1);
+ if (port == -1) {
+ hostValues.add(host);
+ } else {
+ hostValues.add(host + ':' + port);
+ }
+
+ headers.put(Constants.HOST_HEADER_NAME, hostValues);
+
+ // Upgrade header
+ List<String> upgradeValues = new ArrayList<String>(1);
+ upgradeValues.add(Constants.UPGRADE_HEADER_VALUE);
+ headers.put(Constants.UPGRADE_HEADER_NAME, upgradeValues);
+
+ // Connection header
+ List<String> connectionValues = new ArrayList<String>(1);
+ connectionValues.add(Constants.CONNECTION_HEADER_VALUE);
+ headers.put(Constants.CONNECTION_HEADER_NAME, connectionValues);
+
+ // WebSocket version header
+ List<String> wsVersionValues = new ArrayList<String>(1);
+ wsVersionValues.add(Constants.WS_VERSION_HEADER_VALUE);
+ headers.put(Constants.WS_VERSION_HEADER_NAME, wsVersionValues);
+
+ // WebSocket key
+ List<String> wsKeyValues = new ArrayList<String>(1);
+ wsKeyValues.add(generateWsKeyValue());
+ headers.put(Constants.WS_KEY_HEADER_NAME, wsKeyValues);
+
+ // WebSocket sub-protocols
+ if (subProtocols != null && subProtocols.size() > 0) {
+ headers.put(Constants.WS_PROTOCOL_HEADER_NAME, subProtocols);
+ }
+
+ // WebSocket extensions
+ if (extensions != null && extensions.size() > 0) {
+ headers.put(Constants.WS_EXTENSIONS_HEADER_NAME,
+ generateExtensionHeaders(extensions));
+ }
+
+ return headers;
+ }
+
+
+ private List<String> generateExtensionHeaders(List<Extension> extensions)
{
+ List<String> result = new ArrayList<String>(extensions.size());
+ for (Extension extension : extensions) {
+ StringBuilder header = new StringBuilder();
+ header.append(extension.getName());
+ for (Extension.Parameter param : extension.getParameters()) {
+ header.append(';');
+ header.append(param.getName());
+ String value = param.getValue();
+ if (value != null && value.length() > 0) {
+ header.append('=');
+ header.append(value);
+ }
+ }
+ }
+ return result;
+ }
+
+
+ private String generateWsKeyValue() {
+ byte[] keyBytes = new byte[16];
+ random.nextBytes(keyBytes);
+ return Base64.encodeBase64String(keyBytes);
+ }
+
+
+ private ByteBuffer createRequest(URI uri,
+ Map<String,List<String>> reqHeaders) {
+ ByteBuffer result = ByteBuffer.allocate(4 * 1024);
+
+ // Request line
+ result.put("GET ".getBytes(StandardCharsets.ISO_8859_1));
+ result.put(uri.getRawPath().getBytes(StandardCharsets.ISO_8859_1));
+ String query = uri.getRawQuery();
+ if (query != null) {
+ result.put((byte) '?');
+ result.put(query.getBytes(StandardCharsets.ISO_8859_1));
+ }
+ result.put(" HTTP/1.1\r\n".getBytes(StandardCharsets.ISO_8859_1));
+
+ // Headers
+ Iterator<Entry<String,List<String>>> iter =
+ reqHeaders.entrySet().iterator();
+ while (iter.hasNext()) {
+ Entry<String,List<String>> entry = iter.next();
+ addHeader(result, entry.getKey(), entry.getValue());
+ }
+
+ // Terminating CRLF
+ result.put(crlf);
+
+ result.flip();
+
+ return result;
+ }
+
+
+ private void addHeader(ByteBuffer result, String key, List<String> values) {
+ StringBuilder sb = new StringBuilder();
+
+ Iterator<String> iter = values.iterator();
+ if (!iter.hasNext()) {
+ return;
+ }
+ sb.append(iter.next());
+ while (iter.hasNext()) {
+ sb.append(',');
+ sb.append(iter.next());
+ }
+
+ result.put(key.getBytes(StandardCharsets.ISO_8859_1));
+ result.put(": ".getBytes(StandardCharsets.ISO_8859_1));
+ result.put(sb.toString().getBytes(StandardCharsets.ISO_8859_1));
+ result.put(crlf);
+ }
+
+
+ /**
+ * Process response, blocking until HTTP response has been fully received.
+ * @throws ExecutionException
+ * @throws InterruptedException
+ * @throws DeploymentException
+ */
+ private HandshakeResponse processResponse(ByteBuffer response,
+ AsyncChannelWrapper channel) throws InterruptedException,
+ ExecutionException, DeploymentException, EOFException {
+
+ Map<String,List<String>> headers = new HashMap<String,
List<String>>();
+
+ boolean readStatus = false;
+ boolean readHeaders = false;
+ String line = null;
+ while (!readHeaders) {
+ // Blocking read
+ Future<Integer> read = channel.read(response);
+ Integer bytesRead = read.get();
+ if (bytesRead.intValue() == -1) {
+ throw new EOFException();
+ }
+ response.flip();
+ while (response.hasRemaining() && !readHeaders) {
+ if (line == null) {
+ line = readLine(response);
+ } else {
+ line += readLine(response);
+ }
+ if ("\r\n".equals(line)) {
+ readHeaders = true;
+ } else if (line.endsWith("\r\n")) {
+ if (readStatus) {
+ parseHeaders(line, headers);
+ } else {
+ parseStatus(line);
+ readStatus = true;
+ }
+ line = null;
+ }
+ }
+ }
+
+ return new WsHandshakeResponse(headers);
+ }
+
+
+ private void parseStatus(String line) throws DeploymentException {
+ // This client only understands HTTP 1.1
+ // RFC2616 is case specific
+ if (!line.startsWith("HTTP/1.1 101")) {
+ throw new DeploymentException(MESSAGES.invalidHttpStatus(line));
+ }
+ }
+
+
+ private void parseHeaders(String line, Map<String,List<String>> headers)
{
+ // Treat headers as single values by default.
+
+ int index = line.indexOf(':');
+ if (index == -1) {
+ WebsocketsLogger.ROOT_LOGGER.invalidHttpHeader(line);
+ return;
+ }
+ // Header names are case insensitive so always use lower case
+ String headerName = line.substring(0, index).trim().toLowerCase();
+ // TODO handle known multi-value headers
+ String headerValue = line.substring(index + 1).trim();
+
+ List<String> values = headers.get(headerName);
+ if (values == null) {
+ values = new ArrayList<String>(1);
+ headers.put(headerName, values);
+ }
+ values.add(headerValue);
+ }
+
+
+ private String readLine(ByteBuffer response) {
+ // All ISO-8859-1
+ StringBuilder sb = new StringBuilder();
+
+ char c = 0;
+ while (response.hasRemaining()) {
+ c = (char) response.get();
+ sb.append(c);
+ if (c == 10) {
+ break;
+ }
+ }
+
+ return sb.toString();
+ }
+
+
+ private SSLEngine createSSLEngine(Map<String,Object> userProperties)
+ throws DeploymentException {
+
+ try {
+ // Create the SSL Context
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+
+ // Trust store
+ String sslTrustStoreValue =
+ (String) userProperties.get(SSL_TRUSTSTORE_PROPERTY);
+ if (sslTrustStoreValue != null) {
+ String sslTrustStorePwdValue = (String) userProperties.get(
+ SSL_TRUSTSTORE_PWD_PROPERTY);
+ if (sslTrustStorePwdValue == null) {
+ sslTrustStorePwdValue = SSL_TRUSTSTORE_PWD_DEFAULT;
+ }
+
+ File keyStoreFile = new File(sslTrustStoreValue);
+ KeyStore ks = KeyStore.getInstance("JKS");
+ InputStream is = null;
+ try {
+ is = new FileInputStream(keyStoreFile);
+ ks.load(is, sslTrustStorePwdValue.toCharArray());
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException ioe) {
+ // Ignore
+ }
+ }
+ }
+
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(ks);
+
+ sslContext.init(null, tmf.getTrustManagers(), null);
+ } else {
+ sslContext.init(null, null, null);
+ }
+
+ SSLEngine engine = sslContext.createSSLEngine();
+
+ String sslProtocolsValue =
+ (String) userProperties.get(SSL_PROTOCOLS_PROPERTY);
+ if (sslProtocolsValue != null) {
+ engine.setEnabledProtocols(sslProtocolsValue.split(","));
+ }
+
+ engine.setUseClientMode(true);
+
+ return engine;
+ } catch (Exception e) {
+ throw new DeploymentException(MESSAGES.sslEngineFail(), e);
+ }
+ }
+
+
+ @Override
+ public long getDefaultMaxSessionIdleTimeout() {
+ return defaultMaxSessionIdleTimeout;
+ }
+
+
+ @Override
+ public void setDefaultMaxSessionIdleTimeout(long timeout) {
+ this.defaultMaxSessionIdleTimeout = timeout;
+ }
+
+
+ @Override
+ public int getDefaultMaxBinaryMessageBufferSize() {
+ return maxBinaryMessageBufferSize;
+ }
+
+
+ @Override
+ public void setDefaultMaxBinaryMessageBufferSize(int max) {
+ maxBinaryMessageBufferSize = max;
+ }
+
+
+ @Override
+ public int getDefaultMaxTextMessageBufferSize() {
+ return maxTextMessageBufferSize;
+ }
+
+
+ @Override
+ public void setDefaultMaxTextMessageBufferSize(int max) {
+ maxTextMessageBufferSize = max;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * Currently, this implementation does not support any extensions.
+ */
+ @Override
+ public Set<Extension> getInstalledExtensions() {
+ return Collections.emptySet();
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default value for this implementation is -1.
+ */
+ @Override
+ public long getDefaultAsyncSendTimeout() {
+ return defaultAsyncTimeout;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default value for this implementation is -1.
+ */
+ @Override
+ public void setAsyncSendTimeout(long timeout) {
+ this.defaultAsyncTimeout = timeout;
+ }
+
+
+ /**
+ * Cleans up the resources still in use by WebSocket sessions created from
+ * this container. This includes closing sessions and cancelling
+ * {@link Future}s associated with blocking read/writes.
+ */
+ public void destroy() {
+ CloseReason cr = new CloseReason(
+ CloseCodes.GOING_AWAY, MESSAGES.webappStopping());
+
+ for (WsSession session : sessions.keySet()) {
+ try {
+ session.close(cr);
+ } catch (IOException ioe) {
+ WebsocketsLogger.ROOT_LOGGER.sessionCloseFailed(session.getId(), ioe);
+ }
+ }
+ }
+
+
+ // ----------------------------------------------- BackgroundProcess methods
+
+ @Override
+ public void backgroundProcess() {
+ // This method gets called once a second.
+ backgroundProcessCount ++;
+
+ if (backgroundProcessCount >= processPeriod) {
+ backgroundProcessCount = 0;
+
+ for (WsSession wsSession : sessions.keySet()) {
+ wsSession.checkExpiration();
+ }
+ }
+
+ }
+
+
+ @Override
+ public void setProcessPeriod(int period) {
+ this.processPeriod = period;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default value is 10 which means session expirations are processed
+ * every 10 seconds.
+ */
+ @Override
+ public int getProcessPeriod() {
+ return processPeriod;
+ }
+
+
+ /**
+ * Create threads for AsyncIO that have the right context class loader to
+ * prevent memory leaks.
+ */
+ private static class AsyncIOThreadFactory implements ThreadFactory {
+
+ private AtomicInteger count = new AtomicInteger(0);
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setName("WebSocketClient-AsyncIO-" + count.incrementAndGet());
+ t.setContextClassLoader(this.getClass().getClassLoader());
+ t.setDaemon(true);
+ return t;
+ }
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/Constants.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/Constants.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/Constants.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+/**
+ * Internal implementation constants.
+ */
+public class Constants {
+
+ protected static final String PACKAGE_NAME =
+ Constants.class.getPackage().getName();
+
+ private Constants() {
+ // Hide default constructor
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+import java.util.Set;
+
+import javax.websocket.CloseReason;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+
+import org.apache.tomcat.util.ExceptionUtils;
+import org.jboss.web.WebsocketsLogger;
+
+/**
+ * Base implementation (client and server have different concrete
+ * implementations) of the wrapper that converts a POJO instance into a
+ * WebSocket endpoint instance.
+ */
+public abstract class PojoEndpointBase extends Endpoint {
+
+ private Object pojo;
+ private Map<String,String> pathParameters;
+ private PojoMethodMapping methodMapping;
+
+
+ protected final void doOnOpen(Session session, EndpointConfig config) {
+ PojoMethodMapping methodMapping = getMethodMapping();
+ Object pojo = getPojo();
+ Map<String,String> pathParameters = getPathParameters();
+
+ if (methodMapping.getOnOpen() != null) {
+ try {
+ methodMapping.getOnOpen().invoke(pojo,
+ methodMapping.getOnOpenArgs(
+ pathParameters, session, config));
+
+ } catch (IllegalAccessException e) {
+ // Reflection related problems
+ WebsocketsLogger.ROOT_LOGGER.onOpenFailed(pojo.getClass().getName(), e);
+ handleOnOpenError(session, e);
+ return;
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ handleOnOpenError(session, cause);
+ return;
+ } catch (Throwable t) {
+ handleOnOpenError(session, t);
+ return;
+ }
+ }
+
+ for (MessageHandler mh : methodMapping.getMessageHandlers(pojo,
+ pathParameters, session, config)) {
+ session.addMessageHandler(mh);
+ }
+ }
+
+
+ private void handleOnOpenError(Session session, Throwable t) {
+ // If really fatal - re-throw
+ ExceptionUtils.handleThrowable(t);
+
+ // Trigger the error handler and close the session
+ onError(session, t);
+ try {
+ session.close();
+ } catch (IOException ioe) {
+ WebsocketsLogger.ROOT_LOGGER.closeSessionFailed(ioe);
+ }
+ }
+
+ @Override
+ public final void onClose(Session session, CloseReason closeReason) {
+
+ if (methodMapping.getOnClose() != null) {
+ try {
+ methodMapping.getOnClose().invoke(pojo,
+ methodMapping.getOnCloseArgs(pathParameters, session,
closeReason));
+ } catch (Throwable t) {
+ ExceptionUtils.handleThrowable(t);
+ WebsocketsLogger.ROOT_LOGGER.onCloseFailed(pojo.getClass().getName(),
t);
+ }
+ }
+
+ // Trigger the destroy method for any associated decoders
+ Set<MessageHandler> messageHandlers = session.getMessageHandlers();
+ for (MessageHandler messageHandler : messageHandlers) {
+ if (messageHandler instanceof PojoMessageHandlerWholeBase<?>) {
+ ((PojoMessageHandlerWholeBase<?>) messageHandler).onClose();
+ }
+ }
+ }
+
+
+ @Override
+ public final void onError(Session session, Throwable throwable) {
+
+ if (methodMapping.getOnError() == null) {
+ WebsocketsLogger.ROOT_LOGGER.noOnError(pojo.getClass().getName(),
throwable);
+ } else {
+ try {
+ methodMapping.getOnError().invoke(
+ pojo,
+ methodMapping.getOnErrorArgs(pathParameters, session,
+ throwable));
+ } catch (Throwable t) {
+ ExceptionUtils.handleThrowable(t);
+ WebsocketsLogger.ROOT_LOGGER.onErrorFailed(pojo.getClass().getName(),
t);
+ }
+ }
+ }
+
+ protected Object getPojo() { return pojo; }
+ protected void setPojo(Object pojo) { this.pojo = pojo; }
+
+
+ protected Map<String,String> getPathParameters() { return pathParameters; }
+ protected void setPathParameters(Map<String,String> pathParameters) {
+ this.pathParameters = pathParameters;
+ }
+
+
+ protected PojoMethodMapping getMethodMapping() { return methodMapping; }
+ protected void setMethodMapping(PojoMethodMapping methodMapping) {
+ this.methodMapping = methodMapping;
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointClient.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointClient.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointClient.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import java.util.Collections;
+
+import javax.websocket.Decoder;
+import javax.websocket.DeploymentException;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+
+
+/**
+ * Wrapper class for instances of POJOs annotated with
+ * {@link javax.websocket.ClientEndpoint} so they appear as standard
+ * {@link javax.websocket.Endpoint} instances.
+ */
+public class PojoEndpointClient extends PojoEndpointBase {
+
+ public PojoEndpointClient(Object pojo,
+ Class<? extends Decoder>[] decoders) throws DeploymentException {
+ setPojo(pojo);
+ setMethodMapping(
+ new PojoMethodMapping(pojo.getClass(), decoders, null));
+ setPathParameters(Collections.<String, String> emptyMap());
+ }
+
+ @Override
+ public void onOpen(Session session, EndpointConfig config) {
+ doOnOpen(session, config);
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointServer.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointServer.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoEndpointServer.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.util.Map;
+
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpointConfig;
+
+/**
+ * Wrapper class for instances of POJOs annotated with
+ * {@link javax.websocket.server.ServerEndpoint} so they appear as standard
+ * {@link javax.websocket.Endpoint} instances.
+ */
+public class PojoEndpointServer extends PojoEndpointBase {
+
+ public static final String POJO_PATH_PARAM_KEY =
+ "org.apache.tomcat.websocket.pojo.PojoEndpoint.pathParams";
+ public static final String POJO_METHOD_MAPPING_KEY =
+ "org.apache.tomcat.websocket.pojo.PojoEndpoint.methodMapping";
+
+
+ @Override
+ public void onOpen(Session session, EndpointConfig endpointConfig) {
+
+ ServerEndpointConfig sec = (ServerEndpointConfig) endpointConfig;
+
+ Object pojo;
+ try {
+ pojo = sec.getConfigurator().getEndpointInstance(
+ sec.getEndpointClass());
+ } catch (InstantiationException e) {
+ throw MESSAGES.pojoInstanceFailed(sec.getEndpointClass().getName(), e);
+ }
+ setPojo(pojo);
+
+ @SuppressWarnings("unchecked")
+ Map<String,String> pathParameters =
+ (Map<String, String>) sec.getUserProperties().get(
+ POJO_PATH_PARAM_KEY);
+ setPathParameters(pathParameters);
+
+ PojoMethodMapping methodMapping =
+ (PojoMethodMapping) sec.getUserProperties().get(
+ POJO_METHOD_MAPPING_KEY);
+ setMethodMapping(methodMapping);
+
+ doOnOpen(session, endpointConfig);
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerBase.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerBase.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerBase.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+import javax.websocket.EncodeException;
+import javax.websocket.MessageHandler;
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.Session;
+
+import org.apache.tomcat.websocket.WrappedMessageHandler;
+
+/**
+ * Common implementation code for the POJO message handlers.
+ *
+ * @param <T> The type of message to handle
+ */
+public abstract class PojoMessageHandlerBase<T>
+ implements WrappedMessageHandler {
+
+ protected final Object pojo;
+ protected final Method method;
+ protected final Session session;
+ protected final Object[] params;
+ protected final int indexPayload;
+ protected final boolean convert;
+ protected final int indexSession;
+ protected final long maxMessageSize;
+
+ public PojoMessageHandlerBase(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload, boolean convert,
+ int indexSession, long maxMessageSize) {
+ this.pojo = pojo;
+ this.method = method;
+ this.session = session;
+ this.params = params;
+ this.indexPayload = indexPayload;
+ this.convert = convert;
+ this.indexSession = indexSession;
+ this.maxMessageSize = maxMessageSize;
+ }
+
+
+ protected final void processResult(Object result) {
+ if (result == null) {
+ return;
+ }
+
+ RemoteEndpoint.Basic remoteEndpoint = session.getBasicRemote();
+ try {
+ if (result instanceof String) {
+ remoteEndpoint.sendText((String) result);
+ } else if (result instanceof ByteBuffer) {
+ remoteEndpoint.sendBinary((ByteBuffer) result);
+ } else if (result instanceof byte[]) {
+ remoteEndpoint.sendBinary(ByteBuffer.wrap((byte[]) result));
+ } else {
+ remoteEndpoint.sendObject(result);
+ }
+ } catch (IOException ioe) {
+ throw new IllegalStateException(ioe);
+ } catch (EncodeException ee) {
+ throw new IllegalStateException(ee);
+ }
+ }
+
+
+ /**
+ * Expose the POJO if it is a message handler so the Session is able to
+ * match requests to remove handlers if the original handler has been
+ * wrapped.
+ */
+ @Override
+ public final MessageHandler getWrappedHandler() {
+ if (pojo instanceof MessageHandler) {
+ return (MessageHandler) pojo;
+ } else {
+ return null;
+ }
+ }
+
+
+ @Override
+ public final long getMaxMessageSize() {
+ return maxMessageSize;
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBase.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBase.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBase.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+import javax.websocket.DecodeException;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+
+import org.apache.tomcat.websocket.WsSession;
+
+/**
+ * Common implementation code for the POJO partial message handlers. All
+ * the real work is done in this class and in the superclass.
+ *
+ * @param <T> The type of message to handle
+ */
+public abstract class PojoMessageHandlerPartialBase<T>
+ extends PojoMessageHandlerBase<T> implements
MessageHandler.Partial<T> {
+
+ private final int indexBoolean;
+
+ public PojoMessageHandlerPartialBase(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload,
+ boolean convert, int indexBoolean, int indexSession,
+ long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert,
+ indexSession, maxMessageSize);
+ this.indexBoolean = indexBoolean;
+ }
+
+
+ @Override
+ public final void onMessage(T message, boolean last) {
+ if (params.length == 1 && params[0] instanceof DecodeException) {
+ ((WsSession) session).getLocal().onError(session,
+ (DecodeException) params[0]);
+ return;
+ }
+ Object[] parameters = params.clone();
+ if (indexBoolean != -1) {
+ parameters[indexBoolean] = Boolean.valueOf(last);
+ }
+ if (indexSession != -1) {
+ parameters[indexSession] = session;
+ }
+ if (convert) {
+ parameters[indexPayload] = ((ByteBuffer) message).array();
+ } else {
+ parameters[indexPayload] = message;
+ }
+ Object result;
+ try {
+ result = method.invoke(pojo, parameters);
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(e);
+ } catch (InvocationTargetException e) {
+ throw new IllegalArgumentException(e);
+ }
+ processResult(result);
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBinary.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBinary.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialBinary.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+import javax.websocket.Session;
+
+/**
+ * ByteBuffer specific concrete implementation for handling partial messages.
+ */
+public class PojoMessageHandlerPartialBinary
+ extends PojoMessageHandlerPartialBase<ByteBuffer>{
+
+ public PojoMessageHandlerPartialBinary(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload, boolean convert,
+ int indexBoolean, int indexSession, long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert, indexBoolean,
+ indexSession, maxMessageSize);
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialText.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialText.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerPartialText.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import java.lang.reflect.Method;
+
+import javax.websocket.Session;
+
+/**
+ * Text specific concrete implementation for handling partial messages.
+ */
+public class PojoMessageHandlerPartialText
+ extends PojoMessageHandlerPartialBase<String>{
+
+ public PojoMessageHandlerPartialText(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload, boolean convert,
+ int indexBoolean, int indexSession, long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert, indexBoolean,
+ indexSession, maxMessageSize);
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBase.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBase.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBase.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import javax.websocket.DecodeException;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+
+import org.apache.tomcat.websocket.WsSession;
+
+/**
+ * Common implementation code for the POJO whole message handlers. All the real
+ * work is done in this class and in the superclass.
+ *
+ * @param <T> The type of message to handle
+ */
+public abstract class PojoMessageHandlerWholeBase<T>
+ extends PojoMessageHandlerBase<T> implements MessageHandler.Whole<T>
{
+
+ public PojoMessageHandlerWholeBase(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload,
+ boolean convert, int indexSession, long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert,
+ indexSession, maxMessageSize);
+ }
+
+
+ @Override
+ public final void onMessage(T message) {
+
+ if (params.length == 1 && params[0] instanceof DecodeException) {
+ ((WsSession) session).getLocal().onError(session,
+ (DecodeException) params[0]);
+ return;
+ }
+
+ // Can this message be decoded?
+ Object payload;
+ try {
+ payload = decode(message);
+ } catch (DecodeException de) {
+ ((WsSession) session).getLocal().onError(session, de);
+ return;
+ }
+
+ if (payload == null) {
+ // Not decoded. Convert if required.
+ if (convert) {
+ payload = convert(message);
+ } else {
+ payload = message;
+ }
+ }
+
+ Object[] parameters = params.clone();
+ if (indexSession != -1) {
+ parameters[indexSession] = session;
+ }
+ parameters[indexPayload] = payload;
+
+ Object result;
+ try {
+ result = method.invoke(pojo, parameters);
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(e);
+ } catch (InvocationTargetException e) {
+ throw new IllegalArgumentException(e);
+ }
+ processResult(result);
+ }
+
+ protected Object convert(T message) {
+ return message;
+ }
+
+
+ protected abstract Object decode(T message) throws DecodeException;
+ protected abstract void onClose();
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBinary.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBinary.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeBinary.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.Decoder.Binary;
+import javax.websocket.Decoder.BinaryStream;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+
+/**
+ * ByteBuffer specific concrete implementation for handling whole messages.
+ */
+public class PojoMessageHandlerWholeBinary
+ extends PojoMessageHandlerWholeBase<ByteBuffer> {
+
+ private final List<Decoder> decoders = new ArrayList<Decoder>();
+
+ private final boolean isForInputStream;
+
+ public PojoMessageHandlerWholeBinary(Object pojo, Method method,
+ Session session, EndpointConfig config,
+ List<Class<? extends Decoder>> decoderClazzes, Object[] params,
+ int indexPayload, boolean convert, int indexSession,
+ boolean isForInputStream, long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert,
+ indexSession, maxMessageSize);
+ try {
+ if (decoderClazzes != null) {
+ for (Class<? extends Decoder> decoderClazz : decoderClazzes) {
+ if (Binary.class.isAssignableFrom(decoderClazz)) {
+ Binary<?> decoder =
+ (Binary<?>) decoderClazz.newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else if (BinaryStream.class.isAssignableFrom(
+ decoderClazz)) {
+ BinaryStream<?> decoder =
+ (BinaryStream<?>) decoderClazz.newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else {
+ // Text decoder - ignore it
+ }
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(e);
+ } catch (InstantiationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ this.isForInputStream = isForInputStream;
+ }
+
+
+ @Override
+ protected Object decode(ByteBuffer message) throws DecodeException {
+ for (Decoder decoder : decoders) {
+ if (decoder instanceof Binary) {
+ if (((Binary<?>) decoder).willDecode(message)) {
+ return ((Binary<?>) decoder).decode(message);
+ }
+ } else {
+ byte[] array = new byte[message.limit() - message.position()];
+ message.get(array);
+ ByteArrayInputStream bais = new ByteArrayInputStream(array);
+ try {
+ return ((BinaryStream<?>) decoder).decode(bais);
+ } catch (IOException ioe) {
+ throw new DecodeException(message, MESSAGES.errorDecodingMessage(),
ioe);
+ }
+ }
+ }
+ return null;
+ }
+
+
+ @Override
+ protected Object convert(ByteBuffer message) {
+ byte[] array = new byte[message.remaining()];
+ message.get(array);
+ if (isForInputStream) {
+ return new ByteArrayInputStream(array);
+ } else {
+ return array;
+ }
+ }
+
+
+ @Override
+ protected void onClose() {
+ for (Decoder decoder : decoders) {
+ decoder.destroy();
+ }
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholePong.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholePong.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholePong.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import java.lang.reflect.Method;
+
+import javax.websocket.PongMessage;
+import javax.websocket.Session;
+
+/**
+ * PongMessage specific concrete implementation for handling whole messages.
+ */
+public class PojoMessageHandlerWholePong
+ extends PojoMessageHandlerWholeBase<PongMessage> {
+
+ public PojoMessageHandlerWholePong(Object pojo, Method method,
+ Session session, Object[] params, int indexPayload, boolean convert,
+ int indexSession) {
+ super(pojo, method, session, params, indexPayload, convert,
+ indexSession, -1);
+ }
+
+ @Override
+ protected Object decode(PongMessage message) {
+ // Never decoded
+ return null;
+ }
+
+
+ @Override
+ protected void onClose() {
+ // NO-OP
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMessageHandlerWholeText.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.Decoder.Text;
+import javax.websocket.Decoder.TextStream;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+
+import org.apache.tomcat.websocket.Util;
+
+
+/**
+ * Text specific concrete implementation for handling whole messages.
+ */
+public class PojoMessageHandlerWholeText
+ extends PojoMessageHandlerWholeBase<String> {
+
+ private final List<Decoder> decoders = new ArrayList<Decoder>();
+ private final Class<?> primitiveType;
+
+ public PojoMessageHandlerWholeText(Object pojo, Method method,
+ Session session, EndpointConfig config,
+ List<Class<? extends Decoder>> decoderClazzes, Object[] params,
+ int indexPayload, boolean convert, int indexSession,
+ long maxMessageSize) {
+ super(pojo, method, session, params, indexPayload, convert,
+ indexSession, maxMessageSize);
+
+ // Check for primitives
+ Class<?> type = method.getParameterTypes()[indexPayload];
+ if (Util.isPrimitive(type)) {
+ primitiveType = type;
+ return;
+ } else {
+ primitiveType = null;
+ }
+
+ try {
+ if (decoderClazzes != null) {
+ for (Class<? extends Decoder> decoderClazz : decoderClazzes) {
+ if (Text.class.isAssignableFrom(decoderClazz)) {
+ Text<?> decoder = (Text<?>)
decoderClazz.newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else if (TextStream.class.isAssignableFrom(
+ decoderClazz)) {
+ TextStream<?> decoder =
+ (TextStream<?>) decoderClazz.newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else {
+ // Binary decoder - ignore it
+ }
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(e);
+ } catch (InstantiationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+
+ @Override
+ protected Object decode(String message) throws DecodeException {
+ // Handle primitives
+ if (primitiveType != null) {
+ return Util.coerceToType(primitiveType, message);
+ }
+ // Handle full decoders
+ for (Decoder decoder : decoders) {
+ if (decoder instanceof Text) {
+ if (((Text<?>) decoder).willDecode(message)) {
+ return ((Text<?>) decoder).decode(message);
+ }
+ } else {
+ StringReader r = new StringReader(message);
+ try {
+ return ((TextStream<?>) decoder).decode(r);
+ } catch (IOException ioe) {
+ throw new DecodeException(message, MESSAGES.errorDecodingMessage(),
ioe);
+ }
+ }
+ }
+ return null;
+ }
+
+
+ @Override
+ protected Object convert(String message) {
+ return new StringReader(message);
+ }
+
+
+ @Override
+ protected void onClose() {
+ for (Decoder decoder : decoders) {
+ decoder.destroy();
+ }
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,544 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.websocket.CloseReason;
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.DeploymentException;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.PongMessage;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+
+import org.apache.tomcat.websocket.DecoderEntry;
+import org.apache.tomcat.websocket.Util;
+import org.apache.tomcat.websocket.Util.DecoderMatch;
+
+/**
+ * For a POJO class annotated with
+ * {@link javax.websocket.server.ServerEndpoint}, an instance of this class
+ * creates and caches the method handler, method information and parameter
+ * information for the onXXX calls.
+ */
+public class PojoMethodMapping {
+
+ private final Method onOpen;
+ private final Method onClose;
+ private final Method onError;
+ private final PojoPathParam[] onOpenParams;
+ private final PojoPathParam[] onCloseParams;
+ private final PojoPathParam[] onErrorParams;
+ private final Set<MessageHandlerInfo> onMessage = new
HashSet<MessageHandlerInfo>();
+ private final String wsPath;
+
+
+ public PojoMethodMapping(Class<?> clazzPojo,
+ Class<? extends Decoder>[] decoderClazzes, String wsPath)
+ throws DeploymentException {
+
+ this.wsPath = wsPath;
+
+ List<DecoderEntry> decoders = Util.getDecoders(decoderClazzes);
+ Method open = null;
+ Method close = null;
+ Method error = null;
+ for (Method method : clazzPojo.getDeclaredMethods()) {
+ if (method.getAnnotation(OnOpen.class) != null) {
+ checkPublic(method);
+ if (open == null) {
+ open = method;
+ } else {
+ // Duplicate annotation
+ throw new
DeploymentException(MESSAGES.duplicateAnnotations(OnOpen.class, clazzPojo));
+ }
+ } else if (method.getAnnotation(OnClose.class) != null) {
+ checkPublic(method);
+ if (close == null) {
+ close = method;
+ } else {
+ // Duplicate annotation
+ throw new
DeploymentException(MESSAGES.duplicateAnnotations(OnClose.class, clazzPojo));
+ }
+ } else if (method.getAnnotation(OnError.class) != null) {
+ checkPublic(method);
+ if (error == null) {
+ error = method;
+ } else {
+ // Duplicate annotation
+ throw new
DeploymentException(MESSAGES.duplicateAnnotations(OnError.class, clazzPojo));
+ }
+ } else if (method.getAnnotation(OnMessage.class) != null) {
+ checkPublic(method);
+ onMessage.add(new MessageHandlerInfo(method, decoders));
+ } else {
+ // Method not annotated
+ }
+ }
+ this.onOpen = open;
+ this.onClose = close;
+ this.onError = error;
+ onOpenParams = getPathParams(onOpen, MethodType.ON_OPEN);
+ onCloseParams = getPathParams(onClose, MethodType.ON_CLOSE);
+ onErrorParams = getPathParams(onError, MethodType.ON_ERROR);
+ }
+
+
+ private void checkPublic(Method m) throws DeploymentException {
+ if (!Modifier.isPublic(m.getModifiers())) {
+ throw new DeploymentException(MESSAGES.methodNotPublic(m.getName()));
+ }
+ }
+
+
+ public String getWsPath() {
+ return wsPath;
+ }
+
+
+ public Method getOnOpen() {
+ return onOpen;
+ }
+
+
+ public Object[] getOnOpenArgs(Map<String,String> pathParameters,
+ Session session, EndpointConfig config) throws DecodeException {
+ return buildArgs(onOpenParams, pathParameters, session, config, null,
+ null);
+ }
+
+
+ public Method getOnClose() {
+ return onClose;
+ }
+
+
+ public Object[] getOnCloseArgs(Map<String,String> pathParameters,
+ Session session, CloseReason closeReason) throws DecodeException {
+ return buildArgs(onCloseParams, pathParameters, session, null, null,
+ closeReason);
+ }
+
+
+ public Method getOnError() {
+ return onError;
+ }
+
+
+ public Object[] getOnErrorArgs(Map<String,String> pathParameters,
+ Session session, Throwable throwable) throws DecodeException {
+ return buildArgs(onErrorParams, pathParameters, session, null,
+ throwable, null);
+ }
+
+
+ public Set<MessageHandler> getMessageHandlers(Object pojo,
+ Map<String,String> pathParameters, Session session,
+ EndpointConfig config) {
+ Set<MessageHandler> result = new HashSet<MessageHandler>();
+ for (MessageHandlerInfo messageMethod : onMessage) {
+ result.addAll(messageMethod.getMessageHandlers(pojo, pathParameters,
+ session, config));
+ }
+ return result;
+ }
+
+
+ private static PojoPathParam[] getPathParams(Method m,
+ MethodType methodType) throws DeploymentException {
+ if (m == null) {
+ return new PojoPathParam[0];
+ }
+ boolean foundThrowable = false;
+ Class<?>[] types = m.getParameterTypes();
+ Annotation[][] paramsAnnotations = m.getParameterAnnotations();
+ PojoPathParam[] result = new PojoPathParam[types.length];
+ for (int i = 0; i < types.length; i++) {
+ Class<?> type = types[i];
+ if (type.equals(Session.class)) {
+ result[i] = new PojoPathParam(type, null);
+ } else if (methodType == MethodType.ON_OPEN &&
+ type.equals(EndpointConfig.class)) {
+ result[i] = new PojoPathParam(type, null);
+ } else if (methodType == MethodType.ON_ERROR
+ && type.equals(Throwable.class)) {
+ foundThrowable = true;
+ result[i] = new PojoPathParam(type, null);
+ } else if (methodType == MethodType.ON_CLOSE &&
+ type.equals(CloseReason.class)) {
+ result[i] = new PojoPathParam(type, null);
+ } else {
+ Annotation[] paramAnnotations = paramsAnnotations[i];
+ for (Annotation paramAnnotation : paramAnnotations) {
+ if (paramAnnotation.annotationType().equals(
+ PathParam.class)) {
+ // Check that the type is valid. "0" coerces to every
+ // valid type
+ try {
+ Util.coerceToType(type, "0");
+ } catch (IllegalArgumentException iae) {
+ throw new
DeploymentException(MESSAGES.invalidPathParamType(),
+ iae);
+ }
+ result[i] = new PojoPathParam(type,
+ ((PathParam) paramAnnotation).value());
+ break;
+ }
+ }
+ // Parameters without annotations are not permitted
+ if (result[i] == null) {
+ throw new
DeploymentException(MESSAGES.pathParamWithoutAnnotation(type, m.getName(),
m.getClass().getName()));
+ }
+ }
+ }
+ if (methodType == MethodType.ON_ERROR && !foundThrowable) {
+ throw new DeploymentException(MESSAGES.onErrorWithoutThrowable(m.getName(),
m.getDeclaringClass().getName()));
+ }
+ return result;
+ }
+
+
+ private static Object[] buildArgs(PojoPathParam[] pathParams,
+ Map<String,String> pathParameters, Session session,
+ EndpointConfig config, Throwable throwable, CloseReason closeReason)
+ throws DecodeException {
+ Object[] result = new Object[pathParams.length];
+ for (int i = 0; i < pathParams.length; i++) {
+ Class<?> type = pathParams[i].getType();
+ if (type.equals(Session.class)) {
+ result[i] = session;
+ } else if (type.equals(EndpointConfig.class)) {
+ result[i] = config;
+ } else if (type.equals(Throwable.class)) {
+ result[i] = throwable;
+ } else if (type.equals(CloseReason.class)) {
+ result[i] = closeReason;
+ } else {
+ String name = pathParams[i].getName();
+ String value = pathParameters.get(name);
+ try {
+ result[i] = Util.coerceToType(type, value);
+ } catch (Exception e) {
+ throw new DecodeException(value,
MESSAGES.errorDecodingPathParam(value, type), e);
+ }
+ }
+ }
+ return result;
+ }
+
+
+ private static class MessageHandlerInfo {
+
+ private final Method m;
+ private int indexString = -1;
+ private int indexByteArray = -1;
+ private int indexByteBuffer = -1;
+ private int indexPong = -1;
+ private int indexBoolean = -1;
+ private int indexSession = -1;
+ private int indexInputStream = -1;
+ private int indexReader = -1;
+ private int indexPrimitive = -1;
+ private Map<Integer,PojoPathParam> indexPathParams = new
HashMap<Integer, PojoPathParam>();
+ private int indexPayload = -1;
+ private DecoderMatch decoderMatch = null;
+ private long maxMessageSize = -1;
+
+ public MessageHandlerInfo(Method m, List<DecoderEntry> decoderEntries) {
+ this.m = m;
+
+ Class<?>[] types = m.getParameterTypes();
+ Annotation[][] paramsAnnotations = m.getParameterAnnotations();
+
+ for (int i = 0; i < types.length; i++) {
+ boolean paramFound = false;
+ Annotation[] paramAnnotations = paramsAnnotations[i];
+ for (Annotation paramAnnotation : paramAnnotations) {
+ if (paramAnnotation.annotationType().equals(
+ PathParam.class)) {
+ indexPathParams.put(
+ Integer.valueOf(i), new PojoPathParam(types[i],
+ ((PathParam) paramAnnotation).value()));
+ paramFound = true;
+ break;
+ }
+ }
+ if (paramFound) {
+ continue;
+ }
+ if (String.class.isAssignableFrom(types[i])) {
+ if (indexString == -1) {
+ indexString = i;
+ } else {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ }
+ } else if (Reader.class.isAssignableFrom(types[i])) {
+ if (indexReader == -1) {
+ indexReader = i;
+ } else {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ }
+ } else if (boolean.class == types[i]) {
+ if (indexBoolean == -1) {
+ indexBoolean = i;
+ } else {
+ throw MESSAGES.duplicateLastMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ }
+ } else if (ByteBuffer.class.isAssignableFrom(types[i])) {
+ if (indexByteBuffer == -1) {
+ indexByteBuffer = i;
+ } else {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ }
+ } else if (byte[].class == types[i]) {
+ if (indexByteArray == -1) {
+ indexByteArray = i;
+ } else {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ }
+ } else if (InputStream.class.isAssignableFrom(types[i])) {
+ if (indexInputStream == -1) {
+ indexInputStream = i;
+ } else {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ }
+ } else if (Util.isPrimitive(types[i])) {
+ if (indexPrimitive == -1) {
+ indexPrimitive = i;
+ } else {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ }
+ } else if (Session.class.isAssignableFrom(types[i])) {
+ if (indexSession == -1) {
+ indexSession = i;
+ } else {
+ throw MESSAGES.duplicateSessionParameter(m.getName(),
m.getDeclaringClass().getName());
+ }
+ } else if (PongMessage.class.isAssignableFrom(types[i])) {
+ if (indexPong == -1) {
+ indexPong = i;
+ } else {
+ throw MESSAGES.duplicatePongMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ }
+ } else {
+ if (decoderMatch != null && decoderMatch.hasMatches()) {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ }
+ decoderMatch = new DecoderMatch(types[i], decoderEntries);
+
+ if (decoderMatch.hasMatches()) {
+ indexPayload = i;
+ }
+ }
+ }
+
+ // Additional checks required
+ if (indexString != -1) {
+ if (indexPayload != -1) {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ } else {
+ indexPayload = indexString;
+ }
+ }
+ if (indexReader != -1) {
+ if (indexPayload != -1) {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ } else {
+ indexPayload = indexReader;
+ }
+ }
+ if (indexByteArray != -1) {
+ if (indexPayload != -1) {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ } else {
+ indexPayload = indexByteArray;
+ }
+ }
+ if (indexByteBuffer != -1) {
+ if (indexPayload != -1) {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ } else {
+ indexPayload = indexByteBuffer;
+ }
+ }
+ if (indexInputStream != -1) {
+ if (indexPayload != -1) {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ } else {
+ indexPayload = indexInputStream;
+ }
+ }
+ if (indexPrimitive != -1) {
+ if (indexPayload != -1) {
+ throw MESSAGES.duplicateMessageParameter(m.getName(),
m.getDeclaringClass().getName());
+ } else {
+ indexPayload = indexPrimitive;
+ }
+ }
+ if (indexPong != -1) {
+ if (indexPayload != -1) {
+ throw MESSAGES.invalidPongWithPayload(m.getName(),
m.getDeclaringClass().getName());
+ } else {
+ indexPayload = indexPong;
+ }
+ }
+ if (indexPayload == -1 && indexPrimitive == -1 &&
+ indexBoolean != -1) {
+ // The boolean we found is a payload, not a last flag
+ indexPayload = indexBoolean;
+ indexPrimitive = indexBoolean;
+ indexBoolean = -1;
+ }
+ if (indexPayload == -1) {
+ throw MESSAGES.missingPayload(m.getName(),
m.getDeclaringClass().getName());
+ }
+ if (indexPong != -1 && indexBoolean != -1) {
+ throw MESSAGES.partialPong(m.getName(),
m.getDeclaringClass().getName());
+ }
+ if(indexReader != -1 && indexBoolean != -1) {
+ throw MESSAGES.partialReader(m.getName(),
m.getDeclaringClass().getName());
+ }
+ if(indexInputStream != -1 && indexBoolean != -1) {
+ throw MESSAGES.partialInputStream(m.getName(),
m.getDeclaringClass().getName());
+ }
+ if (decoderMatch != null && decoderMatch.hasMatches() &&
+ indexBoolean != -1) {
+ throw MESSAGES.partialObject(m.getName(),
m.getDeclaringClass().getName());
+ }
+
+ maxMessageSize = m.getAnnotation(OnMessage.class).maxMessageSize();
+ }
+
+
+ public Set<MessageHandler> getMessageHandlers(Object pojo,
+ Map<String,String> pathParameters, Session session,
+ EndpointConfig config) {
+ Object[] params = new Object[m.getParameterTypes().length];
+
+ for (Map.Entry<Integer,PojoPathParam> entry :
+ indexPathParams.entrySet()) {
+ PojoPathParam pathParam = entry.getValue();
+ String valueString = pathParameters.get(pathParam.getName());
+ Object value = null;
+ try {
+ value = Util.coerceToType(pathParam.getType(), valueString);
+ } catch (Exception e) {
+ DecodeException de = new DecodeException(valueString,
+ MESSAGES.errorDecodingPathParam(valueString,
pathParam.getType()), e);
+ params = new Object[] { de };
+ }
+ params[entry.getKey().intValue()] = value;
+ }
+
+ Set<MessageHandler> results = new HashSet<MessageHandler>(2);
+ if (indexBoolean == -1) {
+ // Basic
+ if (indexString != -1 || indexPrimitive != -1) {
+ MessageHandler mh = new PojoMessageHandlerWholeText(pojo, m,
+ session, config, null, params, indexPayload, false,
+ indexSession, maxMessageSize);
+ results.add(mh);
+ } else if (indexReader != -1) {
+ MessageHandler mh = new PojoMessageHandlerWholeText(pojo, m,
+ session, config, null, params, indexReader, true,
+ indexSession, maxMessageSize);
+ results.add(mh);
+ } else if (indexByteArray != -1) {
+ MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo,
+ m, session, config, null, params, indexByteArray,
+ true, indexSession, false, maxMessageSize);
+ results.add(mh);
+ } else if (indexByteBuffer != -1) {
+ MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo,
+ m, session, config, null, params, indexByteBuffer,
+ false, indexSession, false, maxMessageSize);
+ results.add(mh);
+ } else if (indexInputStream != -1) {
+ MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo,
+ m, session, config, null, params, indexInputStream,
+ true, indexSession, true, maxMessageSize);
+ results.add(mh);
+ } else if (decoderMatch != null && decoderMatch.hasMatches()) {
+ if (decoderMatch.getBinaryDecoders().size() > 0) {
+ MessageHandler mh = new PojoMessageHandlerWholeBinary(
+ pojo, m, session, config,
+ decoderMatch.getBinaryDecoders(), params,
+ indexPayload, true, indexSession, true,
+ maxMessageSize);
+ results.add(mh);
+ }
+ if (decoderMatch.getTextDecoders().size() > 0) {
+ MessageHandler mh = new PojoMessageHandlerWholeText(
+ pojo, m, session, config,
+ decoderMatch.getTextDecoders(), params,
+ indexPayload, true, indexSession, maxMessageSize);
+ results.add(mh);
+ }
+ } else {
+ MessageHandler mh = new PojoMessageHandlerWholePong(pojo, m,
+ session, params, indexPong, false, indexSession);
+ results.add(mh);
+ }
+ } else {
+ // ASync
+ if (indexString != -1) {
+ MessageHandler mh = new PojoMessageHandlerPartialText(pojo,
+ m, session, params, indexString, false,
+ indexBoolean, indexSession, maxMessageSize);
+ results.add(mh);
+ } else if (indexByteArray != -1) {
+ MessageHandler mh = new PojoMessageHandlerPartialBinary(
+ pojo, m, session, params, indexByteArray, true,
+ indexBoolean, indexSession, maxMessageSize);
+ results.add(mh);
+ } else {
+ MessageHandler mh = new PojoMessageHandlerPartialBinary(
+ pojo, m, session, params, indexByteBuffer, false,
+ indexBoolean, indexSession, maxMessageSize);
+ results.add(mh);
+ }
+ }
+ return results;
+ }
+ }
+
+
+ private static enum MethodType {
+ ON_OPEN,
+ ON_CLOSE,
+ ON_ERROR
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoPathParam.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoPathParam.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/PojoPathParam.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.pojo;
+
+/**
+ * Stores the parameter type and name for a parameter that needs to be passed to
+ * an onXxx method of {@link javax.websocket.Endpoint}. The name is only present
+ * for parameters annotated with
+ * {@link javax.websocket.server.PathParam}. For the
+ * {@link javax.websocket.Session} and {@link java.lang.Throwable} parameters,
+ * {@link #getName()} will always return <code>null</code>.
+ */
+public class PojoPathParam {
+
+ private final Class<?> type;
+ private final String name;
+
+
+ public PojoPathParam(Class<?> type, String name) {
+ this.type = type;
+ this.name = name;
+ }
+
+
+ public Class<?> getType() {
+ return type;
+ }
+
+
+ public String getName() {
+ return name;
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/package-info.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/package-info.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/pojo/package-info.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * This package provides the necessary plumbing to convert an annotated POJO
+ * into a WebSocket {@link javax.websocket.Endpoint}.
+ */
+package org.apache.tomcat.websocket.pojo;
\ No newline at end of file
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/Constants.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/Constants.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/Constants.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+/**
+ * Internal implementation constants.
+ */
+public class Constants {
+
+ protected static final String PACKAGE_NAME =
+ Constants.class.getPackage().getName();
+
+ public static final String BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM =
+ "org.apache.tomcat.websocket.binaryBufferSize";
+ public static final String TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM =
+ "org.apache.tomcat.websocket.textBufferSize";
+ public static final String ENFORCE_NO_ADD_AFTER_HANDSHAKE_CONTEXT_INIT_PARAM =
+ "org.apache.tomcat.websocket.noAddAfterHandshake";
+
+ public static final String SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE =
+ "javax.websocket.server.ServerContainer";
+
+
+ private Constants() {
+ // Hide default constructor
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/DefaultServerEndpointConfigurator.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/DefaultServerEndpointConfigurator.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/DefaultServerEndpointConfigurator.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.websocket.Extension;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpointConfig;
+
+public class DefaultServerEndpointConfigurator
+ extends ServerEndpointConfig.Configurator {
+
+ @Override
+ public <T> T getEndpointInstance(Class<T> clazz)
+ throws InstantiationException {
+ try {
+ return clazz.newInstance();
+ } catch (IllegalAccessException e) {
+ InstantiationException ie = new InstantiationException();
+ ie.initCause(e);
+ throw ie;
+ }
+ }
+
+
+ @Override
+ public String getNegotiatedSubprotocol(List<String> supported,
+ List<String> requested) {
+
+ for (String request : requested) {
+ if (supported.contains(request)) {
+ return request;
+ }
+ }
+ return "";
+ }
+
+
+ @Override
+ public List<Extension> getNegotiatedExtensions(List<Extension>
installed,
+ List<Extension> requested) {
+
+ List<Extension> result = new ArrayList<Extension>();
+ for (Extension request : requested) {
+ if (installed.contains(request)) {
+ result.add(request);
+ }
+ }
+ return result;
+ }
+
+
+ @Override
+ public boolean checkOrigin(String originHeaderValue) {
+ return true;
+ }
+
+ @Override
+ public void modifyHandshake(ServerEndpointConfig sec,
+ HandshakeRequest request, HandshakeResponse response) {
+ // NO-OP
+ }
+
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/UpgradeUtil.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/UpgradeUtil.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/UpgradeUtil.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletRequestWrapper;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.websocket.Endpoint;
+import javax.websocket.Extension;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.apache.catalina.connector.RequestFacade;
+import org.apache.tomcat.util.codec.binary.Base64;
+import org.apache.tomcat.websocket.Constants;
+import org.apache.tomcat.websocket.WsHandshakeResponse;
+import org.apache.tomcat.websocket.pojo.PojoEndpointServer;
+
+public class UpgradeUtil {
+
+ private static final byte[] WS_ACCEPT =
+ "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".getBytes(
+ StandardCharsets.ISO_8859_1);
+ private static final Queue<MessageDigest> sha1Helpers =
+ new ConcurrentLinkedQueue<MessageDigest>();
+
+ private UpgradeUtil() {
+ // Utility class. Hide default constructor.
+ }
+
+ /**
+ * Checks to see if this is an HTTP request that includes a valid upgrade
+ * request to web socket.
+ * <p>
+ * Note: RFC 2616 does not limit HTTP upgrade to GET requests but the Java
+ * WebSocket spec 1.0, section 8.2 implies such a limitation and RFC
+ * 6455 section 4.1 requires that a WebSocket Upgrade uses GET.
+ */
+ public static boolean isWebSocketUpgrageRequest(ServletRequest request,
+ ServletResponse response) {
+
+ return ((request instanceof HttpServletRequest) &&
+ (response instanceof HttpServletResponse) &&
+ headerContainsToken((HttpServletRequest) request,
+ Constants.UPGRADE_HEADER_NAME,
+ Constants.UPGRADE_HEADER_VALUE) &&
+ "GET".equals(((HttpServletRequest) request).getMethod()));
+ }
+
+
+ public static void doUpgrade(WsServerContainer sc, HttpServletRequest req,
+ HttpServletResponse resp, ServerEndpointConfig sec,
+ Map<String,String> pathParams)
+ throws ServletException, IOException {
+
+ // Validate the rest of the headers and reject the request if that
+ // validation fails
+ String key;
+ String subProtocol = null;
+ List<Extension> extensions = Collections.emptyList();
+ if (!headerContainsToken(req, Constants.CONNECTION_HEADER_NAME,
+ Constants.CONNECTION_HEADER_VALUE)) {
+ resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+ if (!headerContainsToken(req, Constants.WS_VERSION_HEADER_NAME,
+ Constants.WS_VERSION_HEADER_VALUE)) {
+ resp.setStatus(426);
+ resp.setHeader(Constants.WS_VERSION_HEADER_NAME,
+ Constants.WS_VERSION_HEADER_VALUE);
+ return;
+ }
+ key = req.getHeader(Constants.WS_KEY_HEADER_NAME);
+ if (key == null) {
+ resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+
+ // Origin check
+ String origin = req.getHeader("Origin");
+ if (!sec.getConfigurator().checkOrigin(origin)) {
+ resp.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ // Sub-protocols
+ List<String> subProtocols = getTokensFromHeader(req,
+ "Sec-WebSocket-Protocol");
+ if (!subProtocols.isEmpty()) {
+ subProtocol = sec.getConfigurator().
+ getNegotiatedSubprotocol(
+ sec.getSubprotocols(), subProtocols);
+ }
+
+ // Extensions
+ // Currently no extensions are supported by this implementation
+
+ // If we got this far, all is good. Accept the connection.
+ resp.setHeader(Constants.UPGRADE_HEADER_NAME,
+ Constants.UPGRADE_HEADER_VALUE);
+ resp.setHeader(Constants.CONNECTION_HEADER_NAME,
+ Constants.CONNECTION_HEADER_VALUE);
+ resp.setHeader(HandshakeResponse.SEC_WEBSOCKET_ACCEPT,
+ getWebSocketAccept(key));
+ if (subProtocol != null) {
+ resp.setHeader("Sec-WebSocket-Protocol", subProtocol);
+ }
+ if (!extensions.isEmpty()) {
+ StringBuilder sb = new StringBuilder();
+ Iterator<Extension> iter = extensions.iterator();
+ // There must be at least one
+ sb.append(iter.next());
+ while (iter.hasNext()) {
+ sb.append(',');
+ sb.append(iter.next().getName());
+ }
+ resp.setHeader("Sec-WebSocket-Extensions", sb.toString());
+ }
+ Endpoint ep;
+ try {
+ Class<?> clazz = sec.getEndpointClass();
+ if (Endpoint.class.isAssignableFrom(clazz)) {
+ ep = (Endpoint) sec.getConfigurator().getEndpointInstance(
+ clazz);
+ } else {
+ ep = new PojoEndpointServer();
+ }
+ } catch (InstantiationException e) {
+ throw new ServletException(e);
+ }
+
+ WsHandshakeRequest wsRequest = new WsHandshakeRequest(req);
+ WsHandshakeResponse wsResponse = new WsHandshakeResponse();
+ sec.getConfigurator().modifyHandshake(sec, wsRequest, wsResponse);
+ wsRequest.finished();
+
+ // Add any additional headers
+ for (Entry<String,List<String>> entry :
+ wsResponse.getHeaders().entrySet()) {
+ for (String headerValue: entry.getValue()) {
+ resp.addHeader(entry.getKey(), headerValue);
+ }
+ }
+
+ // Small hack until the Servlet API provides a way to do this.
+ ServletRequest inner = req;
+ // Unwrap the request
+ while (inner instanceof ServletRequestWrapper) {
+ inner = ((ServletRequestWrapper) inner).getRequest();
+ }
+ if (inner instanceof RequestFacade) {
+ WsHttpUpgradeHandler wsHandler =
+ ((RequestFacade) req).upgrade(WsHttpUpgradeHandler.class);
+ wsHandler.preInit(ep, sec, sc, wsRequest, subProtocol,
+ pathParams, req.isSecure());
+ } else {
+ throw new ServletException("Upgrade failed");
+ }
+ }
+
+
+ /*
+ * This only works for tokens. Quoted strings need more sophisticated
+ * parsing.
+ */
+ private static boolean headerContainsToken(HttpServletRequest req,
+ String headerName, String target) {
+ Enumeration<String> headers = req.getHeaders(headerName);
+ while (headers.hasMoreElements()) {
+ String header = headers.nextElement();
+ String[] tokens = header.split(",");
+ for (String token : tokens) {
+ if (target.equalsIgnoreCase(token.trim())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+
+ /*
+ * This only works for tokens. Quoted strings need more sophisticated
+ * parsing.
+ */
+ private static List<String> getTokensFromHeader(HttpServletRequest req,
+ String headerName) {
+ List<String> result = new ArrayList<String>();
+ Enumeration<String> headers = req.getHeaders(headerName);
+ while (headers.hasMoreElements()) {
+ String header = headers.nextElement();
+ String[] tokens = header.split(",");
+ for (String token : tokens) {
+ result.add(token.trim());
+ }
+ }
+ return result;
+ }
+
+
+ private static String getWebSocketAccept(String key) throws ServletException {
+ MessageDigest sha1Helper = sha1Helpers.poll();
+ if (sha1Helper == null) {
+ try {
+ sha1Helper = MessageDigest.getInstance("SHA1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new ServletException(e);
+ }
+ }
+ sha1Helper.reset();
+ sha1Helper.update(key.getBytes(StandardCharsets.ISO_8859_1));
+ String result = Base64.encodeBase64String(sha1Helper.digest(WS_ACCEPT));
+ sha1Helpers.add(sha1Helper);
+ return result;
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/UriTemplate.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/UriTemplate.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/UriTemplate.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.websocket.DeploymentException;
+
+/**
+ * Extracts path parameters from URIs used to create web socket connections
+ * using the URI template defined for the associated Endpoint.
+ */
+public class UriTemplate {
+
+ private final String normalized;
+ private final List<Segment> segments = new ArrayList<Segment>();
+ private final boolean hasParameters;
+
+
+ public UriTemplate(String path) throws DeploymentException {
+
+ if (path == null || path.length() ==0 || !path.startsWith("/")) {
+ throw new DeploymentException(MESSAGES.invalidPath(path));
+ }
+
+ StringBuilder normalized = new StringBuilder(path.length());
+ Set<String> paramNames = new HashSet<String>();
+
+ // Include empty segments.
+ String[] segments = path.split("/", -1);
+ int paramCount = 0;
+ int segmentCount = 0;
+
+ for (int i = 0; i < segments.length; i++) {
+ String segment = segments[i];
+ if (segment.length() == 0) {
+ if (i == 0 || (i == segments.length - 1 && paramCount == 0)) {
+ // Ignore the first empty segment as the path must always
+ // start with '/'
+ // Ending with a '/' is also OK for instances used for
+ // matches but not for parameterised templates.
+ continue;
+ } else {
+ // As per EG discussion, all other empty segments are
+ // invalid
+ throw MESSAGES.invalidEmptySegment(path);
+ }
+ }
+ normalized.append('/');
+ int index = -1;
+ if (segment.startsWith("{") &&
segment.endsWith("}")) {
+ index = segmentCount;
+ segment = segment.substring(1, segment.length() - 1);
+ normalized.append('{');
+ normalized.append(paramCount++);
+ normalized.append('}');
+ if (!paramNames.add(segment)) {
+ throw MESSAGES.duplicateParameter(segment);
+ }
+ } else {
+ if (segment.contains("{") || segment.contains("}"))
{
+ throw MESSAGES.invalidPathSegment(segment, path);
+ }
+ normalized.append(segment);
+ }
+ this.segments.add(new Segment(index, segment));
+ segmentCount++;
+ }
+
+ this.normalized = normalized.toString();
+ this.hasParameters = paramCount > 0;
+ }
+
+
+ public Map<String,String> match(UriTemplate candidate) {
+
+ Map<String,String> result = new HashMap<String, String>();
+
+ // Should not happen but for safety
+ if (candidate.getSegmentCount() != getSegmentCount()) {
+ return null;
+ }
+
+ Iterator<Segment> candidateSegments =
+ candidate.getSegments().iterator();
+ Iterator<Segment> targetSegments = segments.iterator();
+
+ while (candidateSegments.hasNext()) {
+ Segment candidateSegment = candidateSegments.next();
+ Segment targetSegment = targetSegments.next();
+
+ if (targetSegment.getParameterIndex() == -1) {
+ // Not a parameter - values must match
+ if (!targetSegment.getValue().equals(
+ candidateSegment.getValue())) {
+ // Not a match. Stop here
+ return null;
+ }
+ } else {
+ // Parameter
+ result.put(targetSegment.getValue(),
+ candidateSegment.getValue());
+ }
+ }
+
+ return result;
+ }
+
+
+ public boolean hasParameters() {
+ return hasParameters;
+ }
+
+
+ public int getSegmentCount() {
+ return segments.size();
+ }
+
+
+ public String getNormalizedPath() {
+ return normalized;
+ }
+
+
+ private List<Segment> getSegments() {
+ return segments;
+ }
+
+
+ private static class Segment {
+ private final int parameterIndex;
+ private final String value;
+
+ public Segment(int parameterIndex, String value) {
+ this.parameterIndex = parameterIndex;
+ this.value = value;
+ }
+
+
+ public int getParameterIndex() {
+ return parameterIndex;
+ }
+
+
+ public String getValue() {
+ return value;
+ }
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsContextListener.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsContextListener.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsContextListener.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+/**
+ * In normal usage, this {@link ServletContextListener} does not need to be
+ * explicitly configured as the {@link WsSci} performs all the necessary
+ * bootstrap and installs this listener in the {@link ServletContext}. If the
+ * {@link WsSci} is disabled, this listener must be added manually to every
+ * {@link ServletContext} that uses WebSocket to bootstrap the
+ * {@link WsServerContainer} correctly.
+ */
+public class WsContextListener implements ServletContextListener {
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+ ServletContext sc = sce.getServletContext();
+ // Don't trigger WebSocket initialization if a WebSocket Server
+ // Container is already present
+ if (sc.getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE) ==
null) {
+ WsSci.init(sce.getServletContext(), false);
+ }
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce) {
+ ServletContext sc = sce.getServletContext();
+ Object obj =
sc.getAttribute(Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE);
+ if (obj instanceof WsServerContainer) {
+ ((WsServerContainer) obj).destroy();
+ }
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsFilter.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsFilter.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsFilter.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Handles the initial HTTP connection for WebSocket connections.
+ */
+public class WsFilter implements Filter {
+
+ private WsServerContainer sc;
+
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ sc = (WsServerContainer) filterConfig.getServletContext().getAttribute(
+ Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE);
+ }
+
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+
+ // This filter only needs to handle WebSocket upgrade requests
+ if (!UpgradeUtil.isWebSocketUpgrageRequest(request, response)) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ // HTTP request with an upgrade header for WebSocket present
+ HttpServletRequest req = (HttpServletRequest) request;
+ HttpServletResponse resp = (HttpServletResponse) response;
+
+ // Check to see if this WebSocket implementation has a matching mapping
+ String path;
+ String pathInfo = req.getPathInfo();
+ if (pathInfo == null) {
+ path = req.getServletPath();
+ } else {
+ path = req.getServletPath() + pathInfo;
+ }
+ WsMappingResult mappingResult = sc.findMapping(path);
+
+ if (mappingResult == null) {
+ // No endpoint registered for the requested path. Let the
+ // application handle it (it might redirect or forward for example)
+ chain.doFilter(request, response);
+ return;
+ }
+
+ UpgradeUtil.doUpgrade(sc, req, resp, mappingResult.getConfig(),
+ mappingResult.getPathParams());
+ }
+
+
+ @Override
+ public void destroy() {
+ // NO-OP
+ }
+
+
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsFrameServer.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsFrameServer.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsFrameServer.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+import org.apache.coyote.http11.upgrade.AbstractServletInputStream;
+import org.apache.tomcat.websocket.WsFrameBase;
+import org.apache.tomcat.websocket.WsSession;
+
+public class WsFrameServer extends WsFrameBase {
+
+ private final AbstractServletInputStream sis;
+ private final Object connectionReadLock = new Object();
+
+
+ public WsFrameServer(AbstractServletInputStream sis, WsSession wsSession) {
+ super(wsSession);
+ this.sis = sis;
+ }
+
+
+ /**
+ * Called when there is data in the ServletInputStream to process.
+ */
+ public void onDataAvailable() throws IOException {
+ synchronized (connectionReadLock) {
+ while (isOpen() && sis.isReady()) {
+ // Fill up the input buffer with as much data as we can
+ int read = sis.read(
+ inputBuffer, writePos, inputBuffer.length - writePos);
+ if (read == 0) {
+ return;
+ }
+ if (read == -1) {
+ throw new EOFException();
+ }
+ writePos += read;
+ processInputBuffer();
+ }
+ }
+ }
+
+
+ @Override
+ protected boolean isMasked() {
+ // Data is from the client so it should be masked
+ return true;
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsHandshakeRequest.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsHandshakeRequest.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsHandshakeRequest.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.websocket.server.HandshakeRequest;
+
+/**
+ * Represents the request that this session was opened under.
+ */
+public class WsHandshakeRequest implements HandshakeRequest {
+
+ private final URI requestUri;
+ private final Map<String,List<String>> parameterMap;
+ private final String queryString;
+ private final Principal userPrincipal;
+ private final Map<String,List<String>> headers;
+ private final Object httpSession;
+
+ private volatile HttpServletRequest request;
+
+
+ public WsHandshakeRequest(HttpServletRequest request) {
+
+ this.request = request;
+
+ queryString = request.getQueryString();
+ userPrincipal = request.getUserPrincipal();
+ httpSession = request.getSession(false);
+
+ // URI
+ StringBuilder sb = new StringBuilder(request.getRequestURI());
+ if (queryString != null) {
+ sb.append("?");
+ sb.append(queryString);
+ }
+ try {
+ requestUri = new URI(sb.toString());
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+
+ // ParameterMap
+ Map<String,String[]> originalParameters = request.getParameterMap();
+ Map<String,List<String>> newParameters =
+ new HashMap<String,
List<String>>(originalParameters.size());
+ for (Entry<String,String[]> entry : originalParameters.entrySet()) {
+ newParameters.put(entry.getKey(),
+ Collections.unmodifiableList(
+ Arrays.asList(entry.getValue())));
+ }
+ parameterMap = Collections.unmodifiableMap(newParameters);
+
+ // Headers
+ Map<String,List<String>> newHeaders = new HashMap<String,
List<String>>();
+
+ Enumeration<String> headerNames = request.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String headerName = headerNames.nextElement();
+
+ newHeaders.put(headerName, Collections.unmodifiableList(
+ Collections.list(request.getHeaders(headerName))));
+ }
+
+ headers = Collections.unmodifiableMap(newHeaders);
+ }
+
+ @Override
+ public URI getRequestURI() {
+ return requestUri;
+ }
+
+ @Override
+ public Map<String,List<String>> getParameterMap() {
+ return parameterMap;
+ }
+
+ @Override
+ public String getQueryString() {
+ return queryString;
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return userPrincipal;
+ }
+
+ @Override
+ public Map<String,List<String>> getHeaders() {
+ return headers;
+ }
+
+ @Override
+ public boolean isUserInRole(String role) {
+ if (request == null) {
+ throw new IllegalStateException();
+ }
+
+ return request.isUserInRole(role);
+ }
+
+ @Override
+ public Object getHttpSession() {
+ return httpSession;
+ }
+
+ /**
+ * Called when the HandshakeRequest is no longer required. Since an instance
+ * of this class retains a reference to the current HttpServletRequest that
+ * reference needs to be cleared as the HttpServletRequest may be reused.
+ *
+ * There is no reason for instances of this class to be accessed once the
+ * handshake has been completed.
+ */
+ void finished() {
+ request = null;
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsHttpUpgradeHandler.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Map;
+
+import javax.servlet.http.HttpSession;
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+
+import org.apache.coyote.http11.upgrade.AbstractServletInputStream;
+import org.apache.coyote.http11.upgrade.AbstractServletOutputStream;
+import org.apache.coyote.http11.upgrade.servlet31.HttpUpgradeHandler;
+import org.apache.coyote.http11.upgrade.servlet31.ReadListener;
+import org.apache.coyote.http11.upgrade.servlet31.WebConnection;
+import org.apache.coyote.http11.upgrade.servlet31.WriteListener;
+import org.apache.tomcat.websocket.WsIOException;
+import org.apache.tomcat.websocket.WsSession;
+import org.jboss.web.WebsocketsLogger;
+
+/**
+ * Servlet 3.1 HTTP upgrade handler for WebSocket connections.
+ */
+public class WsHttpUpgradeHandler implements HttpUpgradeHandler {
+
+ private final ClassLoader applicationClassLoader;
+
+ private Endpoint ep;
+ private EndpointConfig endpointConfig;
+ private WsServerContainer webSocketContainer;
+ private WsHandshakeRequest handshakeRequest;
+ private String subProtocol;
+ private Map<String,String> pathParameters;
+ private boolean secure;
+ private WebConnection connection;
+
+ private WsSession wsSession;
+
+
+ public WsHttpUpgradeHandler() {
+ applicationClassLoader = Thread.currentThread().getContextClassLoader();
+ }
+
+
+ public void preInit(Endpoint ep, EndpointConfig endpointConfig,
+ WsServerContainer wsc, WsHandshakeRequest handshakeRequest,
+ String subProtocol, Map<String,String> pathParameters,
+ boolean secure) {
+ this.ep = ep;
+ this.endpointConfig = endpointConfig;
+ this.webSocketContainer = wsc;
+ this.handshakeRequest = handshakeRequest;
+ this.subProtocol = subProtocol;
+ this.pathParameters = pathParameters;
+ this.secure = secure;
+ }
+
+
+ @Override
+ public void init(WebConnection connection) {
+ if (ep == null) {
+ throw MESSAGES.noPreInit();
+ }
+
+ this.connection = connection;
+
+ AbstractServletInputStream sis;
+ AbstractServletOutputStream sos;
+ try {
+ sis = connection.getInputStream();
+ sos = connection.getOutputStream();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+
+ String httpSessionId = null;
+ Object session = handshakeRequest.getHttpSession();
+ if (session != null ) {
+ httpSessionId = ((HttpSession) session).getId();
+ }
+
+ // Need to call onOpen using the web application's class loader
+ // Create the frame using the application's class loader so it can pick
+ // up application specific config from the ServerContainerImpl
+ Thread t = Thread.currentThread();
+ ClassLoader cl = t.getContextClassLoader();
+ t.setContextClassLoader(applicationClassLoader);
+ try {
+ WsRemoteEndpointImplServer wsRemoteEndpointServer =
+ new WsRemoteEndpointImplServer(sos, webSocketContainer);
+ wsSession = new WsSession(ep, wsRemoteEndpointServer,
+ webSocketContainer, handshakeRequest.getRequestURI(),
+ handshakeRequest.getParameterMap(),
+ handshakeRequest.getQueryString(),
+ handshakeRequest.getUserPrincipal(), httpSessionId,
+ subProtocol, pathParameters, secure, endpointConfig);
+ WsFrameServer wsFrame = new WsFrameServer(
+ sis,
+ wsSession);
+ sos.setWriteListener(
+ new WsWriteListener(this, wsRemoteEndpointServer));
+ ep.onOpen(wsSession, endpointConfig);
+ webSocketContainer.registerSession(ep, wsSession);
+ sis.setReadListener(new WsReadListener(this, wsFrame));
+ } catch (DeploymentException e) {
+ throw new IllegalArgumentException(e);
+ } finally {
+ t.setContextClassLoader(cl);
+ }
+ }
+
+
+ @Override
+ public void destroy() {
+ try {
+ connection.close();
+ } catch (Exception e) {
+ WebsocketsLogger.ROOT_LOGGER.destroyFailed(e);
+ }
+ }
+
+
+ private void onError(Throwable throwable) {
+ // Need to call onError using the web application's class loader
+ Thread t = Thread.currentThread();
+ ClassLoader cl = t.getContextClassLoader();
+ t.setContextClassLoader(applicationClassLoader);
+ try {
+ ep.onError(wsSession, throwable);
+ } finally {
+ t.setContextClassLoader(cl);
+ }
+ }
+
+
+ private void close(CloseReason cr) {
+ /*
+ * Any call to this method is a result of a problem reading from the
+ * client. At this point that state of the connection is unknown.
+ * Attempt to send a close frame to the client and then close the socket
+ * immediately. There is no point in waiting for a close frame from the
+ * client because there is no guarantee that we can recover from
+ * whatever messed up state the client put the connection into.
+ */
+ wsSession.onClose(cr);
+ }
+
+
+ private static class WsReadListener implements ReadListener {
+
+ private final WsHttpUpgradeHandler wsProtocolHandler;
+ private final WsFrameServer wsFrame;
+
+
+ private WsReadListener(WsHttpUpgradeHandler wsProtocolHandler,
+ WsFrameServer wsFrame) {
+ this.wsProtocolHandler = wsProtocolHandler;
+ this.wsFrame = wsFrame;
+ }
+
+
+ @Override
+ public void onDataAvailable() {
+ try {
+ wsFrame.onDataAvailable();
+ } catch (WsIOException ws) {
+ wsProtocolHandler.close(ws.getCloseReason());
+ } catch (EOFException eof) {
+ CloseReason cr = new CloseReason(
+ CloseCodes.CLOSED_ABNORMALLY, eof.getMessage());
+ wsProtocolHandler.close(cr);
+ } catch (IOException ioe) {
+ onError(ioe);
+ }
+ }
+
+
+ @Override
+ public void onAllDataRead() {
+ // Will never happen with WebSocket
+ throw new IllegalStateException();
+ }
+
+
+ @Override
+ public void onError(Throwable throwable) {
+ wsProtocolHandler.onError(throwable);
+ }
+ }
+
+
+ private static class WsWriteListener implements WriteListener {
+
+ private final WsHttpUpgradeHandler wsProtocolHandler;
+ private final WsRemoteEndpointImplServer wsRemoteEndpointServer;
+
+ private WsWriteListener(WsHttpUpgradeHandler wsProtocolHandler,
+ WsRemoteEndpointImplServer wsRemoteEndpointServer) {
+ this.wsProtocolHandler = wsProtocolHandler;
+ this.wsRemoteEndpointServer = wsRemoteEndpointServer;
+ }
+
+
+ @Override
+ public void onWritePossible() {
+ wsRemoteEndpointServer.onWritePossible();
+ }
+
+
+ @Override
+ public void onError(Throwable throwable) {
+ wsProtocolHandler.onError(throwable);
+ wsRemoteEndpointServer.close();
+ }
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsMappingResult.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsMappingResult.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsMappingResult.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.util.Map;
+
+import javax.websocket.server.ServerEndpointConfig;
+
+class WsMappingResult {
+
+ private final ServerEndpointConfig config;
+ private final Map<String,String> pathParams;
+
+
+ WsMappingResult(ServerEndpointConfig config,
+ Map<String,String> pathParams) {
+ this.config = config;
+ this.pathParams = pathParams;
+ }
+
+
+ ServerEndpointConfig getConfig() {
+ return config;
+ }
+
+
+ Map<String,String> getPathParams() {
+ return pathParams;
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsRemoteEndpointImplServer.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsRemoteEndpointImplServer.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsRemoteEndpointImplServer.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.nio.ByteBuffer;
+
+import javax.websocket.SendHandler;
+import javax.websocket.SendResult;
+
+import org.apache.coyote.http11.upgrade.AbstractServletOutputStream;
+import org.apache.tomcat.websocket.WsRemoteEndpointImplBase;
+import org.jboss.web.WebsocketsLogger;
+
+/**
+ * This is the server side {@link javax.websocket.RemoteEndpoint} implementation
+ * - i.e. what the server uses to send data to the client. Communication is over
+ * a {@link javax.servlet.ServletOutputStream}.
+ */
+public class WsRemoteEndpointImplServer extends WsRemoteEndpointImplBase {
+
+ private final AbstractServletOutputStream sos;
+ private final WsWriteTimeout wsWriteTimeout;
+ private volatile SendHandler handler = null;
+ private volatile ByteBuffer[] buffers = null;
+
+ private volatile long timeoutExpiry = -1;
+ private volatile boolean close;
+
+
+ public WsRemoteEndpointImplServer(AbstractServletOutputStream sos,
+ WsServerContainer serverContainer) {
+ this.sos = sos;
+ this.wsWriteTimeout = serverContainer.getTimeout();
+ }
+
+
+ @Override
+ protected final boolean isMasked() {
+ return false;
+ }
+
+
+ @Override
+ protected void doWrite(SendHandler handler, ByteBuffer... buffers) {
+ this.handler = handler;
+ this.buffers = buffers;
+ onWritePossible();
+ }
+
+
+ public void onWritePossible() {
+ boolean complete = true;
+ try {
+ // If this is false there will be a call back when it is true
+ while (sos.isReady()) {
+ complete = true;
+ for (ByteBuffer buffer : buffers) {
+ if (buffer.hasRemaining()) {
+ complete = false;
+ sos.write(buffer.array(), buffer.arrayOffset(),
+ buffer.limit());
+ buffer.position(buffer.limit());
+ break;
+ }
+ }
+ if (complete) {
+ wsWriteTimeout.unregister(this);
+ if (close) {
+ close();
+ }
+ // Setting the result marks this (partial) message as
+ // complete which means the next one may be sent which
+ // could update the value of the handler. Therefore, keep a
+ // local copy before signalling the end of the (partial)
+ // message.
+ clearHandler(null);
+ break;
+ }
+ }
+
+ } catch (IOException ioe) {
+ wsWriteTimeout.unregister(this);
+ close();
+ clearHandler(ioe);
+ }
+ if (!complete) {
+ // Async write is in progress
+
+ long timeout = getSendTimeout();
+ if (timeout > 0) {
+ // Register with timeout thread
+ timeoutExpiry = timeout + System.currentTimeMillis();
+ wsWriteTimeout.register(this);
+ }
+ }
+ }
+
+
+ @Override
+ protected void doClose() {
+ if (handler != null) {
+ clearHandler(new EOFException());
+ }
+ try {
+ sos.close();
+ } catch (IOException e) {
+ WebsocketsLogger.ROOT_LOGGER.closeFailed(e);
+ }
+ wsWriteTimeout.unregister(this);
+ }
+
+
+ protected long getTimeoutExpiry() {
+ return timeoutExpiry;
+ }
+
+
+ protected void onTimeout() {
+ if (handler != null) {
+ clearHandler(new SocketTimeoutException());
+ }
+ close();
+ }
+
+
+ private void clearHandler(Throwable t) {
+ SendHandler sh = handler;
+ handler = null;
+ if (sh != null) {
+ if (t == null) {
+ sh.onResult(new SendResult());
+ } else {
+ sh.onResult(new SendResult(t));
+ }
+ }
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsSci.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsSci.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsSci.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.lang.reflect.Modifier;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.servlet.ServletContainerInitializer;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.HandlesTypes;
+import javax.websocket.ContainerProvider;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerApplicationConfig;
+import javax.websocket.server.ServerEndpoint;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.jboss.web.WebsocketsLogger;
+
+/**
+ * Registers an interest in any class that is annotated with
+ * {@link ServerEndpoint} so that Endpoint can be published via the WebSocket
+ * server.
+ */
+(a)HandlesTypes({ServerEndpoint.class, ServerApplicationConfig.class,
+ Endpoint.class})
+public class WsSci implements ServletContainerInitializer {
+
+ private static boolean logMessageWritten = false;
+
+ @Override
+ public void onStartup(Set<Class<?>> clazzes, ServletContext ctx)
+ throws ServletException {
+
+ if (!isJava7OrLater()) {
+ // The WebSocket implementation requires Java 7 so don't initialise
+ // it if Java 7 is not available.
+ if (!logMessageWritten) {
+ logMessageWritten = true;
+ WebsocketsLogger.ROOT_LOGGER.noWebsocketsSupport();
+ }
+ return;
+ }
+
+ WsServerContainer sc = init(ctx, true);
+
+ if (clazzes == null || clazzes.size() == 0) {
+ return;
+ }
+
+ // Group the discovered classes by type
+ Set<ServerApplicationConfig> serverApplicationConfigs = new
HashSet<ServerApplicationConfig>();
+ Set<Class<? extends Endpoint>> scannedEndpointClazzes = new
HashSet<Class<? extends Endpoint>>();
+ Set<Class<?>> scannedPojoEndpoints = new
HashSet<Class<?>>();
+
+ try {
+ // wsPackage is "javax.websocket."
+ String wsPackage = ContainerProvider.class.getName();
+ wsPackage = wsPackage.substring(0, wsPackage.lastIndexOf('.') + 1);
+ for (Class<?> clazz : clazzes) {
+ int modifiers = clazz.getModifiers();
+ if (!Modifier.isPublic(modifiers) ||
+ Modifier.isAbstract(modifiers)) {
+ // Non-public or abstract - skip it.
+ continue;
+ }
+ // Protect against scanning the WebSocket API JARs
+ if (clazz.getName().startsWith(wsPackage)) {
+ continue;
+ }
+ if (ServerApplicationConfig.class.isAssignableFrom(clazz)) {
+ serverApplicationConfigs.add(
+ (ServerApplicationConfig) clazz.newInstance());
+ }
+ if (Endpoint.class.isAssignableFrom(clazz)) {
+ @SuppressWarnings("unchecked")
+ Class<? extends Endpoint> endpoint =
+ (Class<? extends Endpoint>) clazz;
+ scannedEndpointClazzes.add(endpoint);
+ }
+ if (clazz.isAnnotationPresent(ServerEndpoint.class)) {
+ scannedPojoEndpoints.add(clazz);
+ }
+ }
+ } catch (InstantiationException e) {
+ throw new ServletException(e);
+ } catch (IllegalAccessException e) {
+ throw new ServletException(e);
+ }
+
+ // Filter the results
+ Set<ServerEndpointConfig> filteredEndpointConfigs = new
HashSet<ServerEndpointConfig>();
+ Set<Class<?>> filteredPojoEndpoints = new
HashSet<Class<?>>();
+
+ if (serverApplicationConfigs.isEmpty()) {
+ filteredPojoEndpoints.addAll(scannedPojoEndpoints);
+ } else {
+ for (ServerApplicationConfig config : serverApplicationConfigs) {
+ Set<ServerEndpointConfig> configFilteredEndpoints =
+ config.getEndpointConfigs(scannedEndpointClazzes);
+ if (configFilteredEndpoints != null) {
+ filteredEndpointConfigs.addAll(configFilteredEndpoints);
+ }
+ Set<Class<?>> configFilteredPojos =
+ config.getAnnotatedEndpointClasses(
+ scannedPojoEndpoints);
+ if (configFilteredPojos != null) {
+ filteredPojoEndpoints.addAll(configFilteredPojos);
+ }
+ }
+ }
+
+ try {
+ // Deploy endpoints
+ for (ServerEndpointConfig config : filteredEndpointConfigs) {
+ sc.addEndpoint(config);
+ }
+ // Deploy POJOs
+ for (Class<?> clazz : filteredPojoEndpoints) {
+ sc.addEndpoint(clazz);
+ }
+ } catch (DeploymentException e) {
+ throw new ServletException(e);
+ }
+ }
+
+
+ static WsServerContainer init(ServletContext servletContext,
+ boolean initBySciMechanism) {
+
+ WsServerContainer sc = new WsServerContainer(servletContext);
+
+ servletContext.setAttribute(
+ Constants.SERVER_CONTAINER_SERVLET_CONTEXT_ATTRIBUTE, sc);
+
+ servletContext.addListener(new WsSessionListener(sc));
+ // Can't register the ContextListener again if the ContextListener is
+ // calling this method
+ if (initBySciMechanism) {
+ servletContext.addListener(new WsContextListener());
+ }
+
+ return sc;
+ }
+
+
+ private static boolean isJava7OrLater() {
+ try {
+ Class.forName("java.nio.channels.AsynchronousSocketChannel");
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ return true;
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsServerContainer.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsServerContainer.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsServerContainer.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,439 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import static org.jboss.web.WebsocketsMessages.MESSAGES;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.servlet.DispatcherType;
+import javax.servlet.FilterRegistration;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.DeploymentException;
+import javax.websocket.Encoder;
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerContainer;
+import javax.websocket.server.ServerEndpoint;
+import javax.websocket.server.ServerEndpointConfig;
+import javax.websocket.server.ServerEndpointConfig.Configurator;
+
+import org.apache.tomcat.websocket.WsSession;
+import org.apache.tomcat.websocket.WsWebSocketContainer;
+import org.apache.tomcat.websocket.pojo.PojoEndpointServer;
+import org.apache.tomcat.websocket.pojo.PojoMethodMapping;
+
+/**
+ * Provides a per class loader (i.e. per web application) instance of a
+ * ServerContainer. Web application wide defaults may be configured by setting
+ * the following servlet context initialisation parameters to the desired
+ * values.
+ * <ul>
+ * <li>{@link Constants#BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM}</li>
+ * <li>{@link Constants#TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM}</li>
+ * </ul>
+ */
+public class WsServerContainer extends WsWebSocketContainer
+ implements ServerContainer {
+
+ private static final CloseReason AUTHENTICATED_HTTP_SESSION_CLOSED =
+ new CloseReason(CloseCodes.VIOLATED_POLICY,
+ "This connection was established under an authenticated "
+
+ "HTTP session that has ended.");
+
+ private final WsWriteTimeout wsWriteTimeout = new WsWriteTimeout();
+
+ private final ServletContext servletContext;
+ private final Map<String,ServerEndpointConfig> configExactMatchMap =
+ new ConcurrentHashMap<String, ServerEndpointConfig>();
+ private final ConcurrentHashMap<Integer,SortedSet<TemplatePathMatch>>
+ configTemplateMatchMap = new ConcurrentHashMap<Integer,
SortedSet<TemplatePathMatch>>();
+ private volatile boolean enforceNoAddAfterHandshake =
+ org.apache.tomcat.websocket.Constants.STRICT_SPEC_COMPLIANCE;
+ private volatile boolean addAllowed = true;
+ private final ConcurrentHashMap<String,Set<WsSession>>
authenticatedSessions =
+ new ConcurrentHashMap<String, Set<WsSession>>();
+
+ WsServerContainer(ServletContext servletContext) {
+
+ this.servletContext = servletContext;
+
+ // Configure servlet context wide defaults
+ String value = servletContext.getInitParameter(
+ Constants.BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM);
+ if (value != null) {
+ setDefaultMaxBinaryMessageBufferSize(Integer.parseInt(value));
+ }
+
+ value = servletContext.getInitParameter(
+ Constants.TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM);
+ if (value != null) {
+ setDefaultMaxTextMessageBufferSize(Integer.parseInt(value));
+ }
+
+ value = servletContext.getInitParameter(
+ Constants.ENFORCE_NO_ADD_AFTER_HANDSHAKE_CONTEXT_INIT_PARAM);
+ if (value != null) {
+ setEnforceNoAddAfterHandshake(Boolean.parseBoolean(value));
+ }
+
+ FilterRegistration.Dynamic fr = servletContext.addFilter(
+ WsFilter.class.getName(), new WsFilter());
+ fr.setAsyncSupported(true);
+
+ EnumSet<DispatcherType> types = EnumSet.of(DispatcherType.REQUEST,
+ DispatcherType.FORWARD);
+
+ fr.addMappingForUrlPatterns(types, true, "/*");
+ }
+
+
+ /**
+ * Published the provided endpoint implementation at the specified path with
+ * the specified configuration. {@link #WsServerContainer(ServletContext)}
+ * must be called before calling this method.
+ *
+ * @param sec The configuration to use when creating endpoint instances
+ * @throws DeploymentException
+ */
+ @Override
+ public void addEndpoint(ServerEndpointConfig sec)
+ throws DeploymentException {
+
+ if (enforceNoAddAfterHandshake && !addAllowed) {
+ throw new DeploymentException(MESSAGES.addNotAllowed());
+ }
+
+ if (servletContext == null) {
+ throw new DeploymentException(MESSAGES.missingServletContext());
+ }
+ String path = sec.getPath();
+
+ UriTemplate uriTemplate = new UriTemplate(path);
+ if (uriTemplate.hasParameters()) {
+ Integer key = Integer.valueOf(uriTemplate.getSegmentCount());
+ SortedSet<TemplatePathMatch> templateMatches =
+ configTemplateMatchMap.get(key);
+ if (templateMatches == null) {
+ // Ensure that if concurrent threads execute this block they
+ // both end up using the same TreeSet instance
+ templateMatches = new TreeSet<TemplatePathMatch>(
+ TemplatePathMatchComparator.getInstance());
+ configTemplateMatchMap.putIfAbsent(key, templateMatches);
+ templateMatches = configTemplateMatchMap.get(key);
+ }
+ if (!templateMatches.add(new TemplatePathMatch(sec, uriTemplate))) {
+ // Duplicate uriTemplate;
+ throw new DeploymentException(MESSAGES.duplicatePaths(path));
+ }
+ } else {
+ // Exact match
+ ServerEndpointConfig old = configExactMatchMap.put(path, sec);
+ if (old != null) {
+ // Duplicate path mappings
+ throw new DeploymentException(MESSAGES.duplicatePaths(path));
+ }
+ }
+ }
+
+
+ /**
+ * Provides the equivalent of {@link #addEndpoint(ServerEndpointConfig)}
+ * for publishing plain old java objects (POJOs) that have been annotated as
+ * WebSocket endpoints.
+ *
+ * @param pojo The annotated POJO
+ */
+ @Override
+ public void addEndpoint(Class<?> pojo) throws DeploymentException {
+
+ ServerEndpoint annotation = pojo.getAnnotation(ServerEndpoint.class);
+ if (annotation == null) {
+ throw new DeploymentException(MESSAGES.cannotDeployPojo(pojo.getName()));
+ }
+ String path = annotation.value();
+
+ // Validate encoders
+ validateEncoders(annotation.encoders());
+
+ // Method mapping
+ PojoMethodMapping methodMapping = new PojoMethodMapping(pojo,
+ annotation.decoders(), path);
+
+ // ServerEndpointConfig
+ ServerEndpointConfig sec;
+ Class<? extends Configurator> configuratorClazz =
+ annotation.configurator();
+ Configurator configurator = null;
+ if (!configuratorClazz.equals(Configurator.class)) {
+ try {
+ configurator = annotation.configurator().newInstance();
+ } catch (InstantiationException e) {
+ throw new
DeploymentException(MESSAGES.configuratorFailed(annotation.configurator().getName(),
+ pojo.getClass().getName()), e);
+ } catch (IllegalAccessException e) {
+ throw new
DeploymentException(MESSAGES.configuratorFailed(annotation.configurator().getName(),
+ pojo.getClass().getName()), e);
+ }
+ }
+ sec = ServerEndpointConfig.Builder.create(pojo, path).
+ decoders(Arrays.asList(annotation.decoders())).
+ encoders(Arrays.asList(annotation.encoders())).
+ subprotocols(Arrays.asList(annotation.subprotocols())).
+ configurator(configurator).
+ build();
+ sec.getUserProperties().put(
+ PojoEndpointServer.POJO_METHOD_MAPPING_KEY,
+ methodMapping);
+
+ addEndpoint(sec);
+ }
+
+
+ public void doUpgrade(HttpServletRequest request,
+ HttpServletResponse response, ServerEndpointConfig sec,
+ Map<String,String> pathParams)
+ throws ServletException, IOException {
+ UpgradeUtil.doUpgrade(this, request, response, sec, pathParams);
+ }
+
+
+ public WsMappingResult findMapping(String path) {
+
+ // Prevent registering additional endpoints once the first attempt has
+ // been made to use one
+ if (addAllowed) {
+ addAllowed = false;
+ }
+
+ // Check an exact match. Simple case as there are no templates.
+ ServerEndpointConfig sec = configExactMatchMap.get(path);
+ if (sec != null) {
+ return new WsMappingResult(sec,
+ Collections.<String, String> emptyMap());
+ }
+
+ // No exact match. Need to look for template matches.
+ UriTemplate pathUriTemplate = null;
+ try {
+ pathUriTemplate = new UriTemplate(path);
+ } catch (DeploymentException e) {
+ // Path is not valid so can't be matched to a WebSocketEndpoint
+ return null;
+ }
+
+ // Number of segments has to match
+ Integer key = Integer.valueOf(pathUriTemplate.getSegmentCount());
+ SortedSet<TemplatePathMatch> templateMatches =
+ configTemplateMatchMap.get(key);
+
+ if (templateMatches == null) {
+ // No templates with an equal number of segments so there will be
+ // no matches
+ return null;
+ }
+
+ // List is in alphabetical order of normalised templates.
+ // Correct match is the first one that matches.
+ Map<String,String> pathParams = null;
+ for (TemplatePathMatch templateMatch : templateMatches) {
+ pathParams = templateMatch.getUriTemplate().match(pathUriTemplate);
+ if (pathParams != null) {
+ sec = templateMatch.getConfig();
+ break;
+ }
+ }
+
+ if (sec == null) {
+ // No match
+ return null;
+ }
+
+ if (!PojoEndpointServer.class.isAssignableFrom(sec.getEndpointClass())) {
+ // Need to make path params available to POJO
+ sec.getUserProperties().put(
+ PojoEndpointServer.POJO_PATH_PARAM_KEY,
+ pathParams);
+ }
+
+ return new WsMappingResult(sec, pathParams);
+ }
+
+
+
+ public boolean isEnforceNoAddAfterHandshake() {
+ return enforceNoAddAfterHandshake;
+ }
+
+
+ public void setEnforceNoAddAfterHandshake(
+ boolean enforceNoAddAfterHandshake) {
+ this.enforceNoAddAfterHandshake = enforceNoAddAfterHandshake;
+ }
+
+
+ protected WsWriteTimeout getTimeout() {
+ return wsWriteTimeout;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * Overridden to make it visible to other classes in this package.
+ */
+ @Override
+ protected void registerSession(Endpoint endpoint, WsSession wsSession) {
+ super.registerSession(endpoint, wsSession);
+ if (wsSession.isOpen() &&
+ wsSession.getUserPrincipal() != null &&
+ wsSession.getHttpSessionId() != null) {
+ registerAuthenticatedSession(wsSession,
+ wsSession.getHttpSessionId());
+ }
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * Overridden to make it visible to other classes in this package.
+ */
+ @Override
+ protected void unregisterSession(Endpoint endpoint, WsSession wsSession) {
+ if (wsSession.getUserPrincipal() != null &&
+ wsSession.getHttpSessionId() != null) {
+ unregisterAuthenticatedSession(wsSession,
+ wsSession.getHttpSessionId());
+ }
+ super.unregisterSession(endpoint, wsSession);
+ }
+
+
+ private void registerAuthenticatedSession(WsSession wsSession,
+ String httpSessionId) {
+ Set<WsSession> wsSessions = authenticatedSessions.get(httpSessionId);
+ if (wsSessions == null) {
+ wsSessions = Collections.newSetFromMap(
+ new ConcurrentHashMap<WsSession,Boolean>());
+ authenticatedSessions.putIfAbsent(httpSessionId, wsSessions);
+ wsSessions = authenticatedSessions.get(httpSessionId);
+ }
+ wsSessions.add(wsSession);
+ }
+
+
+ private void unregisterAuthenticatedSession(WsSession wsSession,
+ String httpSessionId) {
+ Set<WsSession> wsSessions = authenticatedSessions.get(httpSessionId);
+ wsSessions.remove(wsSession);
+ }
+
+
+ public void closeAuthenticatedSession(String httpSessionId) {
+ Set<WsSession> wsSessions = authenticatedSessions.remove(httpSessionId);
+
+ if (wsSessions != null && !wsSessions.isEmpty()) {
+ for (WsSession wsSession : wsSessions) {
+ try {
+ wsSession.close(AUTHENTICATED_HTTP_SESSION_CLOSED);
+ } catch (IOException e) {
+ // Any IOExceptions during close will have been caught and the
+ // onError method called.
+ }
+ }
+ }
+ }
+
+ private static void validateEncoders(Class<? extends Encoder>[] encoders)
+ throws DeploymentException {
+
+ for (Class<? extends Encoder> encoder : encoders) {
+ // Need to instantiate decoder to ensure it is valid and that
+ // deployment can be failed if it is not
+ @SuppressWarnings("unused")
+ Encoder instance;
+ try {
+ encoder.newInstance();
+ } catch(InstantiationException e) {
+ throw new
DeploymentException(MESSAGES.cannotInstatiateEncoder(encoder.getName()), e);
+ } catch (IllegalAccessException e) {
+ throw new
DeploymentException(MESSAGES.cannotInstatiateEncoder(encoder.getName()), e);
+ }
+ }
+ }
+
+ private static class TemplatePathMatch {
+ private final ServerEndpointConfig config;
+ private final UriTemplate uriTemplate;
+
+ public TemplatePathMatch(ServerEndpointConfig config,
+ UriTemplate uriTemplate) {
+ this.config = config;
+ this.uriTemplate = uriTemplate;
+ }
+
+
+ public ServerEndpointConfig getConfig() {
+ return config;
+ }
+
+
+ public UriTemplate getUriTemplate() {
+ return uriTemplate;
+ }
+ }
+
+
+ /**
+ * This Comparator implementation is thread-safe so only create a single
+ * instance.
+ */
+ private static class TemplatePathMatchComparator
+ implements Comparator<TemplatePathMatch> {
+
+ private static final TemplatePathMatchComparator INSTANCE =
+ new TemplatePathMatchComparator();
+
+ public static TemplatePathMatchComparator getInstance() {
+ return INSTANCE;
+ }
+
+ private TemplatePathMatchComparator() {
+ // Hide default constructor
+ }
+
+ @Override
+ public int compare(TemplatePathMatch tpm1, TemplatePathMatch tpm2) {
+ return tpm1.getUriTemplate().getNormalizedPath().compareTo(
+ tpm2.getUriTemplate().getNormalizedPath());
+ }
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsSessionListener.java
===================================================================
---
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsSessionListener.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsSessionListener.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
+
+public class WsSessionListener implements HttpSessionListener{
+
+ private final WsServerContainer wsServerContainer;
+
+
+ public WsSessionListener(WsServerContainer wsServerContainer) {
+ this.wsServerContainer = wsServerContainer;
+ }
+
+
+ @Override
+ public void sessionCreated(HttpSessionEvent se) {
+ // NO-OP
+ }
+
+
+ @Override
+ public void sessionDestroyed(HttpSessionEvent se) {
+ wsServerContainer.closeAuthenticatedSession(se.getSession().getId());
+ }
+}
Added:
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsWriteTimeout.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsWriteTimeout.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/WsWriteTimeout.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.websocket.server;
+
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.tomcat.websocket.BackgroundProcess;
+import org.apache.tomcat.websocket.BackgroundProcessManager;
+
+/**
+ * Provides timeouts for asynchronous web socket writes. On the server side we
+ * only have access to {@link javax.servlet.ServletOutputStream} and
+ * {@link javax.servlet.ServletInputStream} so there is no way to set a timeout
+ * for writes to the client.
+ */
+public class WsWriteTimeout implements BackgroundProcess {
+
+ private final Set<WsRemoteEndpointImplServer> endpoints =
+ new ConcurrentSkipListSet<WsRemoteEndpointImplServer>(new
EndpointComparator());
+ private final AtomicInteger count = new AtomicInteger(0);
+ private int backgroundProcessCount = 0;
+ private volatile int processPeriod = 1;
+
+ @Override
+ public void backgroundProcess() {
+ // This method gets called once a second.
+ backgroundProcessCount ++;
+
+ if (backgroundProcessCount >= processPeriod) {
+ backgroundProcessCount = 0;
+
+ long now = System.currentTimeMillis();
+ Iterator<WsRemoteEndpointImplServer> iter = endpoints.iterator();
+ while (iter.hasNext()) {
+ WsRemoteEndpointImplServer endpoint = iter.next();
+ if (endpoint.getTimeoutExpiry() < now) {
+ endpoint.onTimeout();
+ } else {
+ // Endpoints are ordered by timeout expiry so if this point
+ // is reached there is no need to check the remaining
+ // endpoints
+ break;
+ }
+ }
+ }
+ }
+
+
+ @Override
+ public void setProcessPeriod(int period) {
+ this.processPeriod = period;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default value is 1 which means asynchronous write timeouts are
+ * processed every 1 second.
+ */
+ @Override
+ public int getProcessPeriod() {
+ return processPeriod;
+ }
+
+
+ public void register(WsRemoteEndpointImplServer endpoint) {
+ boolean result = endpoints.add(endpoint);
+ if (result) {
+ int newCount = count.incrementAndGet();
+ if (newCount == 1) {
+ BackgroundProcessManager.getInstance().register(this);
+ }
+ }
+ }
+
+
+ public void unregister(WsRemoteEndpointImplServer endpoint) {
+ boolean result = endpoints.remove(endpoint);
+ if (result) {
+ int newCount = count.decrementAndGet();
+ if (newCount == 0) {
+ BackgroundProcessManager.getInstance().unregister(this);
+ }
+ }
+ }
+
+
+ /**
+ * Note: this comparator imposes orderings that are inconsistent with equals
+ */
+ private static class EndpointComparator implements
+ Comparator<WsRemoteEndpointImplServer> {
+
+ @Override
+ public int compare(WsRemoteEndpointImplServer o1,
+ WsRemoteEndpointImplServer o2) {
+
+ long t1 = o1.getTimeoutExpiry();
+ long t2 = o2.getTimeoutExpiry();
+
+ if (t1 < t2) {
+ return -1;
+ } else if (t1 == t2) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+ }
+}
Added: branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/package-info.java
===================================================================
--- branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/package-info.java
(rev 0)
+++
branches/7.4.x/src/main/java/org/apache/tomcat/websocket/server/package-info.java 2013-09-20
16:27:05 UTC (rev 2261)
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Server-side specific implementation classes. These are in a separate package
+ * to make packaging a pure client JAR simpler.
+ */
+package org.apache.tomcat.websocket.server;
\ No newline at end of file
Modified: branches/7.4.x/src/main/java/org/jboss/web/CatalinaMessages.java
===================================================================
--- branches/7.4.x/src/main/java/org/jboss/web/CatalinaMessages.java 2013-09-18 14:42:28
UTC (rev 2260)
+++ branches/7.4.x/src/main/java/org/jboss/web/CatalinaMessages.java 2013-09-20 16:27:05
UTC (rev 2261)
@@ -141,9 +141,6 @@
@Message(id = 34, value = "Cannot upgrade from HTTP/1.1 without IO
events")
IllegalStateException cannotUpgradeWithoutEvents();
- @Message(id = 35, value = "Cannot upgrade from HTTP/1.1 is not using an
HttpEventServlet")
- IllegalStateException cannotUpgradeWithoutEventServlet();
-
@Message(id = 36, value = "Cannot call sendFile() after the response has been
committed")
IllegalStateException cannotSendFile();
@@ -1002,4 +999,25 @@
@Message(id = 369, value = "The WebSocket connection has been closed")
String outboundClosed();
+ @Message(id = 370, value = "Read listener already set")
+ IllegalStateException readListenerAlreadySet();
+
+ @Message(id = 371, value = "Write listener already set")
+ IllegalStateException writeListenerAlreadySet();
+
+ @Message(id = 372, value = "Upgrade error")
+ String upgradeError();
+
+ @Message(id = 373, value = "Upgrade destroy processing for servlet %s threw
exception")
+ String upgradeHandlerDestroyError(String servletName);
+
+ @Message(id = 374, value = "IO listener processing for servlet %s threw
exception")
+ String ioListenerError(String servletName);
+
+ @Message(id = 375, value = "Null read or write listener")
+ NullPointerException nullListener();
+
+ @Message(id = 376, value = "Cannot use a read or write listener without
upgrading or starting async")
+ IllegalStateException cannotSetListenerWithoutUpgradeOrAsync();
+
}
Modified: branches/7.4.x/src/main/java/org/jboss/web/CoyoteMessages.java
===================================================================
--- branches/7.4.x/src/main/java/org/jboss/web/CoyoteMessages.java 2013-09-18 14:42:28 UTC
(rev 2260)
+++ branches/7.4.x/src/main/java/org/jboss/web/CoyoteMessages.java 2013-09-20 16:27:05 UTC
(rev 2261)
@@ -277,4 +277,7 @@
@Message(id = 2079, value = "Unexpected data read during handshake")
String sslHandshakeData();
+ @Message(id = 2080, value = "Thread [%s] stoppe to avoid potential leak")
+ String threadStopped(String threadName);
+
}
Added: branches/7.4.x/src/main/java/org/jboss/web/WebsocketsLogger.java
===================================================================
--- branches/7.4.x/src/main/java/org/jboss/web/WebsocketsLogger.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/jboss/web/WebsocketsLogger.java 2013-09-20 16:27:05
UTC (rev 2261)
@@ -0,0 +1,101 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jboss.web;
+
+import static org.jboss.logging.Logger.Level.DEBUG;
+import static org.jboss.logging.Logger.Level.ERROR;
+import static org.jboss.logging.Logger.Level.INFO;
+import static org.jboss.logging.Logger.Level.WARN;
+
+import org.jboss.logging.BasicLogger;
+import org.jboss.logging.Cause;
+import org.jboss.logging.LogMessage;
+import org.jboss.logging.Logger;
+import org.jboss.logging.Message;
+import org.jboss.logging.MessageLogger;
+
+/**
+ * Logging IDs 8800-9000
+ * @author Remy Maucherat
+ */
+@MessageLogger(projectCode = "JBWEB")
+public interface WebsocketsLogger extends BasicLogger {
+
+ /**
+ * A logger with the category of the package name.
+ */
+ WebsocketsLogger ROOT_LOGGER = Logger.getMessageLogger(WebsocketsLogger.class,
"org.apache.tomcat.websocket");
+
+ @LogMessage(level = INFO)
+ @Message(id = 8800, value = "Failed to close channel cleanly")
+ void errorClose();
+
+ @LogMessage(level = ERROR)
+ @Message(id = 8801, value = "A background process failed")
+ void backgroundProcessFailed(@Cause Throwable t);
+
+ @LogMessage(level = WARN)
+ @Message(id = 8802, value = "Flushing batched messages before closing the
session failed")
+ void flushOnCloseFailed(@Cause Throwable t);
+
+ @LogMessage(level = DEBUG)
+ @Message(id = 8803, value = "Failed to send close message to remote
endpoint")
+ void closeMessageFail(@Cause Throwable t);
+
+ @LogMessage(level = WARN)
+ @Message(id = 8804, value = "Unable to parse HTTP header as no colon is present
to delimit header name and header value in [%s]. The header has been skipped.")
+ void invalidHttpHeader(String header);
+
+ @LogMessage(level = DEBUG)
+ @Message(id = 8805, value = "Session with ID [%s] did not close cleanly")
+ void sessionCloseFailed(String id, @Cause Throwable t);
+
+ @LogMessage(level = ERROR)
+ @Message(id = 8806, value = "Failed to call onOpen method of POJO end point for
POJO of type [%s]")
+ void onOpenFailed(String className, @Cause Throwable t);
+
+ @LogMessage(level = WARN)
+ @Message(id = 8807, value = "Failed to close WebSocket session during error
handling")
+ void closeSessionFailed(@Cause Throwable t);
+
+ @LogMessage(level = ERROR)
+ @Message(id = 8808, value = "Failed to call onClose method of POJO end point for
POJO of type [%s]")
+ void onCloseFailed(String className, @Cause Throwable t);
+
+ @LogMessage(level = ERROR)
+ @Message(id = 8809, value = "No error handling configured for [%s] and the
following error occurred")
+ void noOnError(String className, @Cause Throwable t);
+
+ @LogMessage(level = ERROR)
+ @Message(id = 8810, value = "Failed to call onError method of POJO end point for
POJO of type [%s]")
+ void onErrorFailed(String className, @Cause Throwable t);
+
+ @LogMessage(level = ERROR)
+ @Message(id = 8811, value = "Failed to close WebConnection while destroying the
WebSocket HttpUpgradeHandler")
+ void destroyFailed(@Cause Throwable t);
+
+ @LogMessage(level = INFO)
+ @Message(id = 8812, value = "Failed to close the ServletOutputStream connection
cleanly")
+ void closeFailed(@Cause Throwable t);
+
+ @LogMessage(level = INFO)
+ @Message(id = 8813, value = "WebSocket support is not available when running on
Java 6")
+ void noWebsocketsSupport();
+
+}
Added: branches/7.4.x/src/main/java/org/jboss/web/WebsocketsMessages.java
===================================================================
--- branches/7.4.x/src/main/java/org/jboss/web/WebsocketsMessages.java
(rev 0)
+++ branches/7.4.x/src/main/java/org/jboss/web/WebsocketsMessages.java 2013-09-20 16:27:05
UTC (rev 2261)
@@ -0,0 +1,296 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jboss.web;
+
+import org.jboss.logging.Cause;
+import org.jboss.logging.Message;
+import org.jboss.logging.MessageBundle;
+import org.jboss.logging.Messages;
+
+/**
+ * Logging IDs 8500-8850
+ * @author Remy Maucherat
+ */
+@MessageBundle(projectCode = "JBWEB")
+public interface WebsocketsMessages {
+
+ /**
+ * The messages
+ */
+ WebsocketsMessages MESSAGES = Messages.getBundle(WebsocketsMessages.class);
+
+ @Message(id = 8500, value = "Concurrent read operations are not
permitted")
+ IllegalStateException invalidConcurrentRead();
+
+ @Message(id = 8501, value = "Concurrent write operations are not
permitted")
+ IllegalStateException invalidConcurrentWrite();
+
+ @Message(id = 8502, value = "Unexpected Status of SSLEngineResult after a wrap()
operation")
+ IllegalStateException unexpectedStatusAfterWrap();
+
+ @Message(id = 8503, value = "Flag that indicates a write is in progress was
found to be false (it should have been true) when trying to complete a write
operation")
+ IllegalStateException invalidWriteState();
+
+ @Message(id = 8504, value = "Unexpected end of stream")
+ String unexpectedEndOfStream();
+
+ @Message(id = 8505, value = "Flag that indicates a read is in progress was found
to be false (it should have been true) when trying to complete a read operation")
+ IllegalStateException invalidReadState();
+
+ @Message(id = 8506, value = "Unexpected Status of SSLEngineResult after an
unwrap() operation")
+ IllegalStateException unexpectedStatusAfterUnwrap();
+
+ @Message(id = 8507, value = "The result [%s] is too big to be expressed as an
Integer")
+ String notAnInteger(long value);
+
+ @Message(id = 8508, value = "Unable to coerce value [%s] to type [%s]. That type
is not supported.")
+ IllegalArgumentException invalidType(String value, String type);
+
+ @Message(id = 8509, value = "The specified decoder of type [%s] could not be
instantiated")
+ String cannotInstatiateDecoder(String className);
+
+ @Message(id = 8510, value = "Unable to add the message handler [%s] as it was
for the unrecognised type [%s]")
+ IllegalArgumentException unknownHandler(Object listener, Object target);
+
+ @Message(id = 8511, value = "The message handler provided does not have an
onMessage(Object) method")
+ IllegalArgumentException invalidMessageHandler(@Cause Throwable t);
+
+ @Message(id = 8512, value = "The Decoder type [%s] is not recognized")
+ IllegalArgumentException unknownDecoderType(String className);
+
+ @Message(id = 8513, value = "New frame received after a close control
frame")
+ String receivedFrameAfterClose();
+
+ @Message(id = 8514, value = "The client frame set the reserved bits to [%s]
which was not supported by this endpoint")
+ String unsupportedReservedBitsSet(int bit);
+
+ @Message(id = 8515, value = "A fragmented control frame was received but control
frames may not be fragmented")
+ String invalidFragmentedControlFrame();
+
+ @Message(id = 8516, value = "A WebSocket frame was sent with an unrecognised
opCode of [%s]")
+ String invalidFrameOpcode(int code);
+
+ @Message(id = 8517, value = "A new message was started when a continuation frame
was expected")
+ String noContinuationFrame();
+
+ @Message(id = 8518, value = "The client data can not be processed because the
session has already been closed")
+ String sessionClosed();
+
+ @Message(id = 8519, value = "The client frame was not masked but all client
frames must be masked")
+ String frameWithoutMask();
+
+ @Message(id = 8520, value = "A control frame was sent with a payload of size
[%s] which is larger than the maximum permitted of 125 bytes")
+ String controlFramePayloadTooLarge(long size);
+
+ @Message(id = 8521, value = "A control frame was sent that did not have the fin
bit set. Control frames are not permitted to use continuation frames.")
+ String controlFrameWithoutFin();
+
+ @Message(id = 8522, value = "The client sent a close frame with a single byte
payload which is not valid")
+ String invalidOneByteClose();
+
+ @Message(id = 8523, value = "A WebSocket close frame was received with a close
reason that contained invalid UTF-8 byte sequences")
+ String invalidUtf8Close();
+
+ @Message(id = 8524, value = "The message was [%s] bytes long but the
MessageHandler has a limit of [%s] bytes")
+ String messageTooLarge(long size, long limit);
+
+ @Message(id = 8525, value = "A WebSocket text frame was received that could not
be decoded to UTF-8 because it contained invalid byte sequences")
+ String invalidUtf8();
+
+ @Message(id = 8526, value = "The decoded text message was too big for the output
buffer and the endpoint does not support partial messages")
+ String textMessageTooLarge();
+
+ @Message(id = 8527, value = "No async message support and buffer too small.
Buffer size: [%s], Message size: [%s]")
+ String bufferTooSmall(int capacity, long payload);
+
+ @Message(id = 8528, value = "Too many bytes ([%s]) were provided to be converted
into a long")
+ String invalidLong(long length);
+
+ @Message(id = 8529, value = "Message will not be sent because the WebSocket
session is currently sending another message")
+ IllegalStateException messageInProgress();
+
+ @Message(id = 8530, value = "Message will not be sent because the WebSocket
session has been closed")
+ IllegalStateException messageSessionClosed();
+
+ @Message(id = 8531, value = "When sending a fragmented message, all fragments
bust be of the same type")
+ IllegalStateException messageFragmentTypeChange();
+
+ @Message(id = 8532, value = "No encoder specified for object of class
[%s]")
+ String noEncoderForClass(String className);
+
+ @Message(id = 8533, value = "The specified encoder of type [%s] could not be
instantiated")
+ String cannotInstatiateEncoder(String className);
+
+ @Message(id = 8534, value = "This method may not be called as the OutputStream
has been closed")
+ IllegalStateException closedOutputStream();
+
+ @Message(id = 8535, value = "This method may not be called as the Writer has
been closed")
+ IllegalStateException closedWriter();
+
+ @Message(id = 8536, value = "A text message handler has already been
configured")
+ IllegalStateException duplicateHandlerText();
+
+ @Message(id = 8537, value = "A binary message handler has already been
configured")
+ IllegalStateException duplicateHandlerBinary();
+
+ @Message(id = 8538, value = "A pong message handler has already been
configured")
+ IllegalStateException duplicateHandlerPong();
+
+ @Message(id = 8539, value = "A pong message handler must implement
MessageHandler.Basic")
+ IllegalStateException invalidHandlerPong();
+
+ @Message(id = 8540, value = "Unable to add the message handler [%s] as it was
wrapped as the unrecognised type [%s]")
+ IllegalArgumentException invalidMessageHandler(Object listener, Object type);
+
+ @Message(id = 8541, value = "Unable to remove the handler [%s] as it was not
registered with this session")
+ IllegalStateException cannotRemoveHandler(Object listener);
+
+ @Message(id = 8542, value = "Unable to write the complete message as the
WebSocket connection has been closed")
+ String messageFailed();
+
+ @Message(id = 8543, value = "The WebSocket session timeout expired")
+ String sessionTimeout();
+
+ @Message(id = 8544, value = "The WebSocket session has been closed and no method
(apart from close()) may be called on a closed session")
+ IllegalStateException sessionAlreadyClosed();
+
+ @Message(id = 8545, value = "Unable to create dedicated AsynchronousChannelGroup
for WebSocket clients which is required to prevent memory leaks in complex class loader
environments like J2EE containers")
+ IllegalStateException asyncGroupFail();
+
+ @Message(id = 8546, value = "Cannot use POJO class [%s] as it is not annotated
with @ClientEndpoint")
+ String missingClientEndpointAnnotation(String className);
+
+ @Message(id = 8547, value = "Failed to create the default configurator")
+ String defaultConfiguratorFailed();
+
+ @Message(id = 8548, value = "Failed to create a local endpoint of type
[%s]")
+ String endpointCreateFailed(String className);
+
+ @Message(id = 8549, value = "The scheme [%s] is not supported")
+ String pathWrongScheme(String scheme);
+
+ @Message(id = 8550, value = "No host was specified in URI")
+ String pathNoHost();
+
+ @Message(id = 8551, value = "The requested scheme, [%s], is not supported. The
supported schemes are ws and wss")
+ String invalidScheme(String scheme);
+
+ @Message(id = 8552, value = "Unable to open a connection to the server")
+ String connectionFailed();
+
+ @Message(id = 8553, value = "The HTTP request to initiate the WebSocket
connection failed")
+ String httpRequestFailed();
+
+ @Message(id = 8554, value = "Invalid websockets protocol header")
+ String invalidProtocolHeader();
+
+ @Message(id = 8555, value = "The HTTP response from the server [%s] did not
permit the HTTP upgrade to WebSocket")
+ String invalidHttpStatus(String line);
+
+ @Message(id = 8556, value = "Unable to create SSLEngine to support SSL/TLS
connections")
+ String sslEngineFail();
+
+ @Message(id = 8557, value = "The web application is stopping")
+ String webappStopping();
+
+ @Message(id = 8558, value = "Failed to create instance of POJO of type
[%s]")
+ IllegalArgumentException pojoInstanceFailed(String className, @Cause Throwable t);
+
+ @Message(id = 8559, value = "IO error while decoding message")
+ String errorDecodingMessage();
+
+ @Message(id = 8560, value = "Duplicate annotations [%s] present on class
[%s]")
+ String duplicateAnnotations(Class<?> annotation, Class<?> clazz);
+
+ @Message(id = 8561, value = "The annotated method [%s] is not public")
+ String methodNotPublic(String method);
+
+ @Message(id = 8562, value = "Parameters annotated with @PathParam may only be
Strings, Java primitives or a boxed version thereof")
+ String invalidPathParamType();
+
+ @Message(id = 8563, value = "A parameter of type [%s] was found on method[%s] of
class [%s] that did not have a @PathParam annotation")
+ String pathParamWithoutAnnotation(Class<?> clazz, String method, String
className);
+
+ @Message(id = 8564, value = "No Throwable parameter was present on the method
[%s] of class [%s] that was annotated with OnError")
+ String onErrorWithoutThrowable(String method, String className);
+
+ @Message(id = 8565, value = "Failed to decode path parameter value [%s] to
expected type [%s]")
+ String errorDecodingPathParam(String value, Class<?> clazz);
+
+ @Message(id = 8566, value = "Multiple message parameters present on the method
[%s] of class [%s] that was annotated with OnMessage")
+ IllegalArgumentException duplicateMessageParameter(String method, String className);
+
+ @Message(id = 8567, value = "Multiple boolean (last) parameters present on the
method [%s] of class [%s] that was annotated with OnMessage")
+ IllegalArgumentException duplicateLastMessageParameter(String method, String
className);
+
+ @Message(id = 8568, value = "Multiple session parameters present on the method
[%s] of class [%s] that was annotated with OnMessage")
+ IllegalArgumentException duplicateSessionParameter(String method, String className);
+
+ @Message(id = 8569, value = "Multiple PongMessage parameters present on the
method [%s] of class [%s] that was annotated with OnMessage")
+ IllegalArgumentException duplicatePongMessageParameter(String method, String
className);
+
+ @Message(id = 8570, value = "Invalid PongMessgae and Message parameters present
on the method [%s] of class [%s] that was annotated with OnMessage")
+ IllegalArgumentException invalidPongWithPayload(String method, String className);
+
+ @Message(id = 8571, value = "No payload parameter present on the method [%s] of
class [%s] that was annotated with OnMessage")
+ IllegalArgumentException missingPayload(String method, String className);
+
+ @Message(id = 8572, value = "Invalid PongMesssge and boolean parameters present
on the method [%s] of class [%s] that was annotated with OnMessage")
+ IllegalArgumentException partialPong(String method, String className);
+
+ @Message(id = 8573, value = "Invalid Reader and boolean parameters present on
the method [%s] of class [%s] that was annotated with OnMessage")
+ IllegalArgumentException partialReader(String method, String className);
+
+ @Message(id = 8574, value = "Invalid InputStream and boolean parameters present
on the method [%s] of class [%s] that was annotated with OnMessage")
+ IllegalArgumentException partialInputStream(String method, String className);
+
+ @Message(id = 8575, value = "Invalid Object and boolean parameters present on
the method [%s] of class [%s] that was annotated with OnMessage")
+ IllegalArgumentException partialObject(String method, String className);
+
+ @Message(id = 8576, value = "The path [%s] is not valid.")
+ String invalidPath(String path);
+
+ @Message(id = 8577, value = "The path [%s] contains one or more empty segments
which are is not permitted")
+ IllegalArgumentException invalidEmptySegment(String path);
+
+ @Message(id = 8578, value = "The parameter [%s] appears more than once in the
path which is not permitted")
+ IllegalArgumentException duplicateParameter(String path);
+
+ @Message(id = 8579, value = "The segment [%s] is not valid in the provided path
[%s]")
+ IllegalArgumentException invalidPathSegment(String segment, String path);
+
+ @Message(id = 8580, value = "The preInit() method must be called to configure
the WebSocket HttpUpgradeHandler before the container calls init(). Usually, this means
the Servlet that created the WsHttpUpgradeHandler instance should also call
preInit()")
+ IllegalStateException noPreInit();
+
+ @Message(id = 8581, value = "No further Endpoints may be registered once an
attempt has been made to use one of the previously registered endpoints")
+ String addNotAllowed();
+
+ @Message(id = 8582, value = "No ServletContext was specified")
+ String missingServletContext();
+
+ @Message(id = 8583, value = "Multiple Endpoints may not be deployed to using the
same path [%s]")
+ String duplicatePaths(String path);
+
+ @Message(id = 8584, value = "Cannot deploy POJO class [%s] as it is not
annotated with @ServerEndpoint")
+ String cannotDeployPojo(String className);
+
+ @Message(id = 8585, value = "Failed to create configurator of type [%s] for POJO
of type [%s]")
+ String configuratorFailed(String configurator, String className);
+
+}