summaryrefslogtreecommitdiffhomepage
path: root/src/java/nginx/unit/websocket/pojo
diff options
context:
space:
mode:
authorMax Romanov <max.romanov@nginx.com>2019-09-05 15:27:32 +0300
committerMax Romanov <max.romanov@nginx.com>2019-09-05 15:27:32 +0300
commit2b8cab1e2478547398ad9c2fe68e025c180cac54 (patch)
treed317fcf9ee52f0f8967116f531784ae533b0ae5a /src/java/nginx/unit/websocket/pojo
parent3e23afb0d205e503f6cc7d852e34d07da9a5b7f7 (diff)
downloadunit-2b8cab1e2478547398ad9c2fe68e025c180cac54.tar.gz
unit-2b8cab1e2478547398ad9c2fe68e025c180cac54.tar.bz2
Java: introducing websocket support.
Diffstat (limited to 'src/java/nginx/unit/websocket/pojo')
-rw-r--r--src/java/nginx/unit/websocket/pojo/Constants.java32
-rw-r--r--src/java/nginx/unit/websocket/pojo/LocalStrings.properties40
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoEndpointBase.java156
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoEndpointClient.java47
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoEndpointServer.java66
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerBase.java122
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBase.java77
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBinary.java36
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialText.java35
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBase.java94
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBinary.java131
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholePong.java48
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeText.java136
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoMethodMapping.java731
-rw-r--r--src/java/nginx/unit/websocket/pojo/PojoPathParam.java47
-rw-r--r--src/java/nginx/unit/websocket/pojo/package-info.java21
16 files changed, 1819 insertions, 0 deletions
diff --git a/src/java/nginx/unit/websocket/pojo/Constants.java b/src/java/nginx/unit/websocket/pojo/Constants.java
new file mode 100644
index 00000000..93cdecc7
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/Constants.java
@@ -0,0 +1,32 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+/**
+ * Internal implementation constants.
+ */
+public class Constants {
+
+ public static final String POJO_PATH_PARAM_KEY =
+ "nginx.unit.websocket.pojo.PojoEndpoint.pathParams";
+ public static final String POJO_METHOD_MAPPING_KEY =
+ "nginx.unit.websocket.pojo.PojoEndpoint.methodMapping";
+
+ private Constants() {
+ // Hide default constructor
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/LocalStrings.properties b/src/java/nginx/unit/websocket/pojo/LocalStrings.properties
new file mode 100644
index 00000000..00ab7e6b
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/LocalStrings.properties
@@ -0,0 +1,40 @@
+# 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.
+
+pojoEndpointBase.closeSessionFail=Failed to close WebSocket session during error handling
+pojoEndpointBase.onCloseFail=Failed to call onClose method of POJO end point for POJO of type [{0}]
+pojoEndpointBase.onError=No error handling configured for [{0}] and the following error occurred
+pojoEndpointBase.onErrorFail=Failed to call onError method of POJO end point for POJO of type [{0}]
+pojoEndpointBase.onOpenFail=Failed to call onOpen method of POJO end point for POJO of type [{0}]
+pojoEndpointServer.getPojoInstanceFail=Failed to create instance of POJO of type [{0}]
+pojoMethodMapping.decodePathParamFail=Failed to decode path parameter value [{0}] to expected type [{1}]
+pojoMethodMapping.duplicateAnnotation=Duplicate annotations [{0}] present on class [{1}]
+pojoMethodMapping.duplicateLastParam=Multiple boolean (last) parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.duplicateMessageParam=Multiple message parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.duplicatePongMessageParam=Multiple PongMessage parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.duplicateSessionParam=Multiple session parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.invalidDecoder=The specified decoder of type [{0}] could not be instantiated
+pojoMethodMapping.invalidPathParamType=Parameters annotated with @PathParam may only be Strings, Java primitives or a boxed version thereof
+pojoMethodMapping.methodNotPublic=The annotated method [{0}] is not public
+pojoMethodMapping.noPayload=No payload parameter present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.onErrorNoThrowable=No Throwable parameter was present on the method [{0}] of class [{1}] that was annotated with OnError
+pojoMethodMapping.paramWithoutAnnotation=A parameter of type [{0}] was found on method[{1}] of class [{2}] that did not have a @PathParam annotation
+pojoMethodMapping.partialInputStream=Invalid InputStream and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.partialObject=Invalid Object and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.partialPong=Invalid PongMessage and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.partialReader=Invalid Reader and boolean parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMethodMapping.pongWithPayload=Invalid PongMessage and Message parameters present on the method [{0}] of class [{1}] that was annotated with OnMessage
+pojoMessageHandlerWhole.decodeIoFail=IO error while decoding message
+pojoMessageHandlerWhole.maxBufferSize=The maximum supported message size for this implementation is Integer.MAX_VALUE
diff --git a/src/java/nginx/unit/websocket/pojo/PojoEndpointBase.java b/src/java/nginx/unit/websocket/pojo/PojoEndpointBase.java
new file mode 100644
index 00000000..be679a35
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoEndpointBase.java
@@ -0,0 +1,156 @@
+/*
+ * 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 nginx.unit.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.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * 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 final Log log = LogFactory.getLog(PojoEndpointBase.class); // must not be static
+ private static final StringManager sm = StringManager.getManager(PojoEndpointBase.class);
+
+ 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();
+
+ // Add message handlers before calling onOpen since that may trigger a
+ // message which in turn could trigger a response and/or close the
+ // session
+ for (MessageHandler mh : methodMapping.getMessageHandlers(pojo,
+ pathParameters, session, config)) {
+ session.addMessageHandler(mh);
+ }
+
+ if (methodMapping.getOnOpen() != null) {
+ try {
+ methodMapping.getOnOpen().invoke(pojo,
+ methodMapping.getOnOpenArgs(
+ pathParameters, session, config));
+
+ } catch (IllegalAccessException e) {
+ // Reflection related problems
+ log.error(sm.getString(
+ "pojoEndpointBase.onOpenFail",
+ pojo.getClass().getName()), e);
+ handleOnOpenOrCloseError(session, e);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ handleOnOpenOrCloseError(session, cause);
+ } catch (Throwable t) {
+ handleOnOpenOrCloseError(session, t);
+ }
+ }
+ }
+
+
+ private void handleOnOpenOrCloseError(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) {
+ log.warn(sm.getString("pojoEndpointBase.closeSessionFail"), 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) {
+ log.error(sm.getString("pojoEndpointBase.onCloseFail",
+ pojo.getClass().getName()), t);
+ handleOnOpenOrCloseError(session, 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) {
+ log.error(sm.getString("pojoEndpointBase.onError",
+ pojo.getClass().getName()), throwable);
+ } else {
+ try {
+ methodMapping.getOnError().invoke(
+ pojo,
+ methodMapping.getOnErrorArgs(pathParameters, session,
+ throwable));
+ } catch (Throwable t) {
+ ExceptionUtils.handleThrowable(t);
+ log.error(sm.getString("pojoEndpointBase.onErrorFail",
+ 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;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoEndpointClient.java b/src/java/nginx/unit/websocket/pojo/PojoEndpointClient.java
new file mode 100644
index 00000000..6e569487
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoEndpointClient.java
@@ -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 nginx.unit.websocket.pojo;
+
+import java.util.Collections;
+import java.util.List;
+
+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,
+ List<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);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoEndpointServer.java b/src/java/nginx/unit/websocket/pojo/PojoEndpointServer.java
new file mode 100644
index 00000000..499f8274
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoEndpointServer.java
@@ -0,0 +1,66 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+import java.util.Map;
+
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * 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 {
+
+ private static final StringManager sm =
+ StringManager.getManager(PojoEndpointServer.class);
+
+ @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 new IllegalArgumentException(sm.getString(
+ "pojoEndpointServer.getPojoInstanceFail",
+ sec.getEndpointClass().getName()), e);
+ }
+ setPojo(pojo);
+
+ @SuppressWarnings("unchecked")
+ Map<String,String> pathParameters =
+ (Map<String, String>) sec.getUserProperties().get(
+ Constants.POJO_PATH_PARAM_KEY);
+ setPathParameters(pathParameters);
+
+ PojoMethodMapping methodMapping =
+ (PojoMethodMapping) sec.getUserProperties().get(
+ Constants.POJO_METHOD_MAPPING_KEY);
+ setMethodMapping(methodMapping);
+
+ doOnOpen(session, endpointConfig);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerBase.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerBase.java
new file mode 100644
index 00000000..b72d719a
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerBase.java
@@ -0,0 +1,122 @@
+/*
+ * 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 nginx.unit.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.util.ExceptionUtils;
+import nginx.unit.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;
+ // TODO: The method should already be accessible here but the following
+ // code seems to be necessary in some as yet not fully understood cases.
+ try {
+ this.method.setAccessible(true);
+ } catch (Exception e) {
+ // It is better to make sure the method is accessible, but
+ // ignore exceptions and hope for the best
+ }
+ 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 | EncodeException ioe) {
+ throw new IllegalStateException(ioe);
+ }
+ }
+
+
+ /**
+ * 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;
+ }
+
+
+ protected final void handlePojoMethodException(Throwable t) {
+ t = ExceptionUtils.unwrapInvocationTargetException(t);
+ ExceptionUtils.handleThrowable(t);
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ } else {
+ throw new RuntimeException(t.getMessage(), t);
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBase.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBase.java
new file mode 100644
index 00000000..d6f37724
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBase.java
@@ -0,0 +1,77 @@
+/*
+ * 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 nginx.unit.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 nginx.unit.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 = null;
+ try {
+ result = method.invoke(pojo, parameters);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ handlePojoMethodException(e);
+ }
+ processResult(result);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBinary.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBinary.java
new file mode 100644
index 00000000..1d334017
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialBinary.java
@@ -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 nginx.unit.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);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialText.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialText.java
new file mode 100644
index 00000000..8f7c1a0d
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerPartialText.java
@@ -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 nginx.unit.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);
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBase.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBase.java
new file mode 100644
index 00000000..23333eb7
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBase.java
@@ -0,0 +1,94 @@
+/*
+ * 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 nginx.unit.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 nginx.unit.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 = null;
+ try {
+ result = method.invoke(pojo, parameters);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ handlePojoMethodException(e);
+ }
+ processResult(result);
+ }
+
+ protected Object convert(T message) {
+ return message;
+ }
+
+
+ protected abstract Object decode(T message) throws DecodeException;
+ protected abstract void onClose();
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBinary.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBinary.java
new file mode 100644
index 00000000..07ff0648
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeBinary.java
@@ -0,0 +1,131 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+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;
+
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * ByteBuffer specific concrete implementation for handling whole messages.
+ */
+public class PojoMessageHandlerWholeBinary
+ extends PojoMessageHandlerWholeBase<ByteBuffer> {
+
+ private static final StringManager sm =
+ StringManager.getManager(PojoMessageHandlerWholeBinary.class);
+
+ private final List<Decoder> decoders = new ArrayList<>();
+
+ 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);
+
+ // Update binary text size handled by session
+ if (maxMessageSize > -1 && maxMessageSize > session.getMaxBinaryMessageBufferSize()) {
+ if (maxMessageSize > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMessageHandlerWhole.maxBufferSize"));
+ }
+ session.setMaxBinaryMessageBufferSize((int) maxMessageSize);
+ }
+
+ try {
+ if (decoderClazzes != null) {
+ for (Class<? extends Decoder> decoderClazz : decoderClazzes) {
+ if (Binary.class.isAssignableFrom(decoderClazz)) {
+ Binary<?> decoder = (Binary<?>) decoderClazz.getConstructor().newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else if (BinaryStream.class.isAssignableFrom(
+ decoderClazz)) {
+ BinaryStream<?> decoder = (BinaryStream<?>)
+ decoderClazz.getConstructor().newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else {
+ // Text decoder - ignore it
+ }
+ }
+ }
+ } catch (ReflectiveOperationException 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, sm.getString(
+ "pojoMessageHandlerWhole.decodeIoFail"), 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();
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholePong.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholePong.java
new file mode 100644
index 00000000..bdedd7de
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholePong.java
@@ -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 nginx.unit.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
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeText.java b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeText.java
new file mode 100644
index 00000000..59007349
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMessageHandlerWholeText.java
@@ -0,0 +1,136 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+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.util.res.StringManager;
+import nginx.unit.websocket.Util;
+
+
+/**
+ * Text specific concrete implementation for handling whole messages.
+ */
+public class PojoMessageHandlerWholeText
+ extends PojoMessageHandlerWholeBase<String> {
+
+ private static final StringManager sm =
+ StringManager.getManager(PojoMessageHandlerWholeText.class);
+
+ private final List<Decoder> decoders = new ArrayList<>();
+ 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);
+
+ // Update max text size handled by session
+ if (maxMessageSize > -1 && maxMessageSize > session.getMaxTextMessageBufferSize()) {
+ if (maxMessageSize > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMessageHandlerWhole.maxBufferSize"));
+ }
+ session.setMaxTextMessageBufferSize((int) 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.getConstructor().newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else if (TextStream.class.isAssignableFrom(
+ decoderClazz)) {
+ TextStream<?> decoder =
+ (TextStream<?>) decoderClazz.getConstructor().newInstance();
+ decoder.init(config);
+ decoders.add(decoder);
+ } else {
+ // Binary decoder - ignore it
+ }
+ }
+ }
+ } catch (ReflectiveOperationException 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, sm.getString(
+ "pojoMessageHandlerWhole.decodeIoFail"), ioe);
+ }
+ }
+ }
+ return null;
+ }
+
+
+ @Override
+ protected Object convert(String message) {
+ return new StringReader(message);
+ }
+
+
+ @Override
+ protected void onClose() {
+ for (Decoder decoder : decoders) {
+ decoder.destroy();
+ }
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoMethodMapping.java b/src/java/nginx/unit/websocket/pojo/PojoMethodMapping.java
new file mode 100644
index 00000000..2385b5c7
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoMethodMapping.java
@@ -0,0 +1,731 @@
+/*
+ * 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 nginx.unit.websocket.pojo;
+
+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.ArrayList;
+import java.util.Arrays;
+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.util.res.StringManager;
+import nginx.unit.websocket.DecoderEntry;
+import nginx.unit.websocket.Util;
+import nginx.unit.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 static final StringManager sm =
+ StringManager.getManager(PojoMethodMapping.class);
+
+ 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 List<MessageHandlerInfo> onMessage = new ArrayList<>();
+ private final String wsPath;
+
+
+ public PojoMethodMapping(Class<?> clazzPojo,
+ List<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;
+ Method[] clazzPojoMethods = null;
+ Class<?> currentClazz = clazzPojo;
+ while (!currentClazz.equals(Object.class)) {
+ Method[] currentClazzMethods = currentClazz.getDeclaredMethods();
+ if (currentClazz == clazzPojo) {
+ clazzPojoMethods = currentClazzMethods;
+ }
+ for (Method method : currentClazzMethods) {
+ if (method.getAnnotation(OnOpen.class) != null) {
+ checkPublic(method);
+ if (open == null) {
+ open = method;
+ } else {
+ if (currentClazz == clazzPojo ||
+ !isMethodOverride(open, method)) {
+ // Duplicate annotation
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.duplicateAnnotation",
+ OnOpen.class, currentClazz));
+ }
+ }
+ } else if (method.getAnnotation(OnClose.class) != null) {
+ checkPublic(method);
+ if (close == null) {
+ close = method;
+ } else {
+ if (currentClazz == clazzPojo ||
+ !isMethodOverride(close, method)) {
+ // Duplicate annotation
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.duplicateAnnotation",
+ OnClose.class, currentClazz));
+ }
+ }
+ } else if (method.getAnnotation(OnError.class) != null) {
+ checkPublic(method);
+ if (error == null) {
+ error = method;
+ } else {
+ if (currentClazz == clazzPojo ||
+ !isMethodOverride(error, method)) {
+ // Duplicate annotation
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.duplicateAnnotation",
+ OnError.class, currentClazz));
+ }
+ }
+ } else if (method.getAnnotation(OnMessage.class) != null) {
+ checkPublic(method);
+ MessageHandlerInfo messageHandler = new MessageHandlerInfo(method, decoders);
+ boolean found = false;
+ for (MessageHandlerInfo otherMessageHandler : onMessage) {
+ if (messageHandler.targetsSameWebSocketMessageType(otherMessageHandler)) {
+ found = true;
+ if (currentClazz == clazzPojo ||
+ !isMethodOverride(messageHandler.m, otherMessageHandler.m)) {
+ // Duplicate annotation
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.duplicateAnnotation",
+ OnMessage.class, currentClazz));
+ }
+ }
+ }
+ if (!found) {
+ onMessage.add(messageHandler);
+ }
+ } else {
+ // Method not annotated
+ }
+ }
+ currentClazz = currentClazz.getSuperclass();
+ }
+ // If the methods are not on clazzPojo and they are overridden
+ // by a non annotated method in clazzPojo, they should be ignored
+ if (open != null && open.getDeclaringClass() != clazzPojo) {
+ if (isOverridenWithoutAnnotation(clazzPojoMethods, open, OnOpen.class)) {
+ open = null;
+ }
+ }
+ if (close != null && close.getDeclaringClass() != clazzPojo) {
+ if (isOverridenWithoutAnnotation(clazzPojoMethods, close, OnClose.class)) {
+ close = null;
+ }
+ }
+ if (error != null && error.getDeclaringClass() != clazzPojo) {
+ if (isOverridenWithoutAnnotation(clazzPojoMethods, error, OnError.class)) {
+ error = null;
+ }
+ }
+ List<MessageHandlerInfo> overriddenOnMessage = new ArrayList<>();
+ for (MessageHandlerInfo messageHandler : onMessage) {
+ if (messageHandler.m.getDeclaringClass() != clazzPojo
+ && isOverridenWithoutAnnotation(clazzPojoMethods, messageHandler.m, OnMessage.class)) {
+ overriddenOnMessage.add(messageHandler);
+ }
+ }
+ for (MessageHandlerInfo messageHandler : overriddenOnMessage) {
+ onMessage.remove(messageHandler);
+ }
+ 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(sm.getString(
+ "pojoMethodMapping.methodNotPublic", m.getName()));
+ }
+ }
+
+
+ private boolean isMethodOverride(Method method1, Method method2) {
+ return method1.getName().equals(method2.getName())
+ && method1.getReturnType().equals(method2.getReturnType())
+ && Arrays.equals(method1.getParameterTypes(), method2.getParameterTypes());
+ }
+
+
+ private boolean isOverridenWithoutAnnotation(Method[] methods,
+ Method superclazzMethod, Class<? extends Annotation> annotation) {
+ for (Method method : methods) {
+ if (isMethodOverride(method, superclazzMethod)
+ && (method.getAnnotation(annotation) == null)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ 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 boolean hasMessageHandlers() {
+ return !onMessage.isEmpty();
+ }
+
+
+ public Set<MessageHandler> getMessageHandlers(Object pojo,
+ Map<String,String> pathParameters, Session session,
+ EndpointConfig config) {
+ Set<MessageHandler> result = new HashSet<>();
+ 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(sm.getString(
+ "pojoMethodMapping.invalidPathParamType"),
+ iae);
+ }
+ result[i] = new PojoPathParam(type,
+ ((PathParam) paramAnnotation).value());
+ break;
+ }
+ }
+ // Parameters without annotations are not permitted
+ if (result[i] == null) {
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.paramWithoutAnnotation",
+ type, m.getName(), m.getClass().getName()));
+ }
+ }
+ }
+ if (methodType == MethodType.ON_ERROR && !foundThrowable) {
+ throw new DeploymentException(sm.getString(
+ "pojoMethodMapping.onErrorNoThrowable",
+ 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, sm.getString(
+ "pojoMethodMapping.decodePathParamFail",
+ 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 Class<?> primitiveType = null;
+ private Map<Integer,PojoPathParam> indexPathParams = new HashMap<>();
+ 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 new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (Reader.class.isAssignableFrom(types[i])) {
+ if (indexReader == -1) {
+ indexReader = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (boolean.class == types[i]) {
+ if (indexBoolean == -1) {
+ indexBoolean = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateLastParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (ByteBuffer.class.isAssignableFrom(types[i])) {
+ if (indexByteBuffer == -1) {
+ indexByteBuffer = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (byte[].class == types[i]) {
+ if (indexByteArray == -1) {
+ indexByteArray = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (InputStream.class.isAssignableFrom(types[i])) {
+ if (indexInputStream == -1) {
+ indexInputStream = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (Util.isPrimitive(types[i])) {
+ if (indexPrimitive == -1) {
+ indexPrimitive = i;
+ primitiveType = types[i];
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (Session.class.isAssignableFrom(types[i])) {
+ if (indexSession == -1) {
+ indexSession = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateSessionParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else if (PongMessage.class.isAssignableFrom(types[i])) {
+ if (indexPong == -1) {
+ indexPong = i;
+ } else {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicatePongMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ } else {
+ if (decoderMatch != null && decoderMatch.hasMatches()) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ 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 new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexString;
+ }
+ }
+ if (indexReader != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexReader;
+ }
+ }
+ if (indexByteArray != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexByteArray;
+ }
+ }
+ if (indexByteBuffer != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexByteBuffer;
+ }
+ }
+ if (indexInputStream != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexInputStream;
+ }
+ }
+ if (indexPrimitive != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.duplicateMessageParam",
+ m.getName(), m.getDeclaringClass().getName()));
+ } else {
+ indexPayload = indexPrimitive;
+ }
+ }
+ if (indexPong != -1) {
+ if (indexPayload != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.pongWithPayload",
+ 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;
+ primitiveType = Boolean.TYPE;
+ indexBoolean = -1;
+ }
+ if (indexPayload == -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.noPayload",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ if (indexPong != -1 && indexBoolean != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.partialPong",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ if(indexReader != -1 && indexBoolean != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.partialReader",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ if(indexInputStream != -1 && indexBoolean != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.partialInputStream",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+ if (decoderMatch != null && decoderMatch.hasMatches() &&
+ indexBoolean != -1) {
+ throw new IllegalArgumentException(sm.getString(
+ "pojoMethodMapping.partialObject",
+ m.getName(), m.getDeclaringClass().getName()));
+ }
+
+ maxMessageSize = m.getAnnotation(OnMessage.class).maxMessageSize();
+ }
+
+
+ public boolean targetsSameWebSocketMessageType(MessageHandlerInfo otherHandler) {
+ if (otherHandler == null) {
+ return false;
+ }
+ if (indexByteArray >= 0 && otherHandler.indexByteArray >= 0) {
+ return true;
+ }
+ if (indexByteBuffer >= 0 && otherHandler.indexByteBuffer >= 0) {
+ return true;
+ }
+ if (indexInputStream >= 0 && otherHandler.indexInputStream >= 0) {
+ return true;
+ }
+ if (indexPong >= 0 && otherHandler.indexPong >= 0) {
+ return true;
+ }
+ if (indexPrimitive >= 0 && otherHandler.indexPrimitive >= 0
+ && primitiveType == otherHandler.primitiveType) {
+ return true;
+ }
+ if (indexReader >= 0 && otherHandler.indexReader >= 0) {
+ return true;
+ }
+ if (indexString >= 0 && otherHandler.indexString >= 0) {
+ return true;
+ }
+ if (decoderMatch != null && otherHandler.decoderMatch != null
+ && decoderMatch.getTarget().equals(otherHandler.decoderMatch.getTarget())) {
+ return true;
+ }
+ return false;
+ }
+
+
+ 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,
+ sm.getString(
+ "pojoMethodMapping.decodePathParamFail",
+ valueString, pathParam.getType()), e);
+ params = new Object[] { de };
+ break;
+ }
+ params[entry.getKey().intValue()] = value;
+ }
+
+ Set<MessageHandler> results = new HashSet<>(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 enum MethodType {
+ ON_OPEN,
+ ON_CLOSE,
+ ON_ERROR
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/PojoPathParam.java b/src/java/nginx/unit/websocket/pojo/PojoPathParam.java
new file mode 100644
index 00000000..859b6d68
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/PojoPathParam.java
@@ -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 nginx.unit.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;
+ }
+}
diff --git a/src/java/nginx/unit/websocket/pojo/package-info.java b/src/java/nginx/unit/websocket/pojo/package-info.java
new file mode 100644
index 00000000..39cf80c8
--- /dev/null
+++ b/src/java/nginx/unit/websocket/pojo/package-info.java
@@ -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 nginx.unit.websocket.pojo;