|
|
|
@ -29,9 +29,9 @@ import java.util.ArrayList;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.function.Function;
|
|
|
|
|
import java.util.stream.Stream;
|
|
|
|
|
import javax.websocket.CloseReason;
|
|
|
|
|
import javax.websocket.Decoder;
|
|
|
|
|
import javax.websocket.Endpoint;
|
|
|
|
|
import javax.websocket.EndpointConfig;
|
|
|
|
|
import javax.websocket.OnClose;
|
|
|
|
|
import javax.websocket.OnError;
|
|
|
|
@ -51,17 +51,13 @@ import org.eclipse.jetty.websocket.util.InvalidSignatureException;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.InvalidWebSocketException;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.InvokerUtils;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.ReflectUtils;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.messages.ByteArrayMessageSink;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.messages.ByteBufferMessageSink;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.messages.InputStreamMessageSink;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.messages.MessageSink;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.messages.PartialByteArrayMessageSink;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.messages.PartialByteBufferMessageSink;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.messages.PartialStringMessageSink;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.messages.ReaderMessageSink;
|
|
|
|
|
import org.eclipse.jetty.websocket.util.messages.StringMessageSink;
|
|
|
|
|
|
|
|
|
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
|
|
|
|
import static org.eclipse.jetty.websocket.javax.common.JavaxWebSocketCallingArgs.getArgsFor;
|
|
|
|
|
|
|
|
|
|
public abstract class JavaxWebSocketFrameHandlerFactory
|
|
|
|
|
{
|
|
|
|
@ -154,6 +150,321 @@ public abstract class JavaxWebSocketFrameHandlerFactory
|
|
|
|
|
config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static MessageSink createMessageSink(JavaxWebSocketSession session, JavaxWebSocketMessageMetadata msgMetadata)
|
|
|
|
|
{
|
|
|
|
|
if (msgMetadata == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
MethodHandles.Lookup lookup = getServerMethodHandleLookup();
|
|
|
|
|
if (AbstractDecodedMessageSink.class.isAssignableFrom(msgMetadata.getSinkClass()))
|
|
|
|
|
{
|
|
|
|
|
MethodHandle ctorHandle = lookup.findConstructor(msgMetadata.getSinkClass(),
|
|
|
|
|
MethodType.methodType(void.class, CoreSession.class, MethodHandle.class, List.class));
|
|
|
|
|
List<RegisteredDecoder> registeredDecoders = msgMetadata.getRegisteredDecoders();
|
|
|
|
|
return (MessageSink)ctorHandle.invoke(session.getCoreSession(), msgMetadata.getMethodHandle(), registeredDecoders);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
MethodHandle ctorHandle = lookup.findConstructor(msgMetadata.getSinkClass(),
|
|
|
|
|
MethodType.methodType(void.class, CoreSession.class, MethodHandle.class));
|
|
|
|
|
return (MessageSink)ctorHandle.invoke(session.getCoreSession(), msgMetadata.getMethodHandle());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (NoSuchMethodException e)
|
|
|
|
|
{
|
|
|
|
|
throw new RuntimeException("Missing expected MessageSink constructor found at: " + msgMetadata.getSinkClass().getName(), e);
|
|
|
|
|
}
|
|
|
|
|
catch (IllegalAccessException | InstantiationException | InvocationTargetException e)
|
|
|
|
|
{
|
|
|
|
|
throw new RuntimeException("Unable to create MessageSink: " + msgMetadata.getSinkClass().getName(), e);
|
|
|
|
|
}
|
|
|
|
|
catch (RuntimeException e)
|
|
|
|
|
{
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
catch (Throwable t)
|
|
|
|
|
{
|
|
|
|
|
throw new RuntimeException(t);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static MethodHandle wrapNonVoidReturnType(MethodHandle handle, JavaxWebSocketSession session)
|
|
|
|
|
{
|
|
|
|
|
if (handle == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
if (handle.type().returnType() == Void.TYPE)
|
|
|
|
|
return handle;
|
|
|
|
|
|
|
|
|
|
// Technique from https://stackoverflow.com/questions/48505787/methodhandle-with-general-non-void-return-filter
|
|
|
|
|
|
|
|
|
|
// Change the return type of the to be Object so it will match exact with JavaxWebSocketSession.filterReturnType(Object)
|
|
|
|
|
handle = handle.asType(handle.type().changeReturnType(Object.class));
|
|
|
|
|
|
|
|
|
|
// Filter the method return type to a call to JavaxWebSocketSession.filterReturnType() bound to this session
|
|
|
|
|
handle = MethodHandles.filterReturnValue(handle, FILTER_RETURN_TYPE_METHOD.bindTo(session));
|
|
|
|
|
|
|
|
|
|
return handle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private MethodHandle toMethodHandle(MethodHandles.Lookup lookup, Method method)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return lookup.unreflect(method);
|
|
|
|
|
}
|
|
|
|
|
catch (IllegalAccessException e)
|
|
|
|
|
{
|
|
|
|
|
throw new RuntimeException("Unable to access method " + method, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected JavaxWebSocketFrameHandlerMetadata createEndpointMetadata(Class<? extends Endpoint> endpointClass, EndpointConfig endpointConfig)
|
|
|
|
|
{
|
|
|
|
|
JavaxWebSocketFrameHandlerMetadata metadata = new JavaxWebSocketFrameHandlerMetadata(endpointConfig);
|
|
|
|
|
MethodHandles.Lookup lookup = getApplicationMethodHandleLookup(endpointClass);
|
|
|
|
|
|
|
|
|
|
Method openMethod = ReflectUtils.findMethod(endpointClass, "onOpen", Session.class, EndpointConfig.class);
|
|
|
|
|
MethodHandle open = toMethodHandle(lookup, openMethod);
|
|
|
|
|
metadata.setOpenHandler(open, openMethod);
|
|
|
|
|
|
|
|
|
|
Method closeMethod = ReflectUtils.findMethod(endpointClass, "onClose", Session.class, CloseReason.class);
|
|
|
|
|
MethodHandle close = toMethodHandle(lookup, closeMethod);
|
|
|
|
|
metadata.setCloseHandler(close, closeMethod);
|
|
|
|
|
|
|
|
|
|
Method errorMethod = ReflectUtils.findMethod(endpointClass, "onError", Session.class, Throwable.class);
|
|
|
|
|
MethodHandle error = toMethodHandle(lookup, errorMethod);
|
|
|
|
|
metadata.setErrorHandler(error, errorMethod);
|
|
|
|
|
|
|
|
|
|
return metadata;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected JavaxWebSocketFrameHandlerMetadata discoverJavaxFrameHandlerMetadata(Class<?> endpointClass, JavaxWebSocketFrameHandlerMetadata metadata)
|
|
|
|
|
{
|
|
|
|
|
MethodHandles.Lookup lookup = getApplicationMethodHandleLookup(endpointClass);
|
|
|
|
|
Method onmethod;
|
|
|
|
|
|
|
|
|
|
// OnOpen [0..1]
|
|
|
|
|
onmethod = ReflectUtils.findAnnotatedMethod(endpointClass, OnOpen.class);
|
|
|
|
|
if (onmethod != null)
|
|
|
|
|
{
|
|
|
|
|
assertSignatureValid(endpointClass, onmethod, OnOpen.class);
|
|
|
|
|
final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class);
|
|
|
|
|
final InvokerUtils.Arg ENDPOINT_CONFIG = new InvokerUtils.Arg(EndpointConfig.class);
|
|
|
|
|
MethodHandle methodHandle = InvokerUtils
|
|
|
|
|
.mutatedInvoker(lookup, endpointClass, onmethod, paramIdentifier, metadata.getNamedTemplateVariables(), SESSION, ENDPOINT_CONFIG);
|
|
|
|
|
metadata.setOpenHandler(methodHandle, onmethod);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnClose [0..1]
|
|
|
|
|
onmethod = ReflectUtils.findAnnotatedMethod(endpointClass, OnClose.class);
|
|
|
|
|
if (onmethod != null)
|
|
|
|
|
{
|
|
|
|
|
assertSignatureValid(endpointClass, onmethod, OnClose.class);
|
|
|
|
|
final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class);
|
|
|
|
|
final InvokerUtils.Arg CLOSE_REASON = new InvokerUtils.Arg(CloseReason.class);
|
|
|
|
|
MethodHandle methodHandle = InvokerUtils
|
|
|
|
|
.mutatedInvoker(lookup, endpointClass, onmethod, paramIdentifier, metadata.getNamedTemplateVariables(), SESSION, CLOSE_REASON);
|
|
|
|
|
metadata.setCloseHandler(methodHandle, onmethod);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnError [0..1]
|
|
|
|
|
onmethod = ReflectUtils.findAnnotatedMethod(endpointClass, OnError.class);
|
|
|
|
|
if (onmethod != null)
|
|
|
|
|
{
|
|
|
|
|
assertSignatureValid(endpointClass, onmethod, OnError.class);
|
|
|
|
|
final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class);
|
|
|
|
|
final InvokerUtils.Arg CAUSE = new InvokerUtils.Arg(Throwable.class).required();
|
|
|
|
|
MethodHandle methodHandle = InvokerUtils
|
|
|
|
|
.mutatedInvoker(lookup, endpointClass, onmethod, paramIdentifier, metadata.getNamedTemplateVariables(), SESSION, CAUSE);
|
|
|
|
|
metadata.setErrorHandler(methodHandle, onmethod);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnMessage [0..2]
|
|
|
|
|
Method[] onMessages = ReflectUtils.findAnnotatedMethods(endpointClass, OnMessage.class);
|
|
|
|
|
if (onMessages != null && onMessages.length > 0)
|
|
|
|
|
{
|
|
|
|
|
for (Method onMsg : onMessages)
|
|
|
|
|
{
|
|
|
|
|
assertSignatureValid(endpointClass, onMsg, OnMessage.class);
|
|
|
|
|
OnMessage onMessageAnno = onMsg.getAnnotation(OnMessage.class);
|
|
|
|
|
|
|
|
|
|
long annotationMaxMessageSize = onMessageAnno.maxMessageSize();
|
|
|
|
|
if (annotationMaxMessageSize > Integer.MAX_VALUE)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidWebSocketException(String.format("Value too large: %s#%s - @OnMessage.maxMessageSize=%,d > Integer.MAX_VALUE",
|
|
|
|
|
endpointClass.getName(), onMsg.getName(), annotationMaxMessageSize));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create MessageMetadata and set annotated maxMessageSize if it is not the default value.
|
|
|
|
|
JavaxWebSocketMessageMetadata msgMetadata = new JavaxWebSocketMessageMetadata();
|
|
|
|
|
if (annotationMaxMessageSize != -1)
|
|
|
|
|
msgMetadata.setMaxMessageSize((int)annotationMaxMessageSize);
|
|
|
|
|
|
|
|
|
|
// Function to search for matching MethodHandle for the endpointClass given a signature.
|
|
|
|
|
Function<InvokerUtils.Arg[], MethodHandle> getMethodHandle = (signature) ->
|
|
|
|
|
InvokerUtils.optionalMutatedInvoker(lookup, endpointClass, onMsg, paramIdentifier, metadata.getNamedTemplateVariables(), signature);
|
|
|
|
|
|
|
|
|
|
// Try to match from available decoders (includes primitive types).
|
|
|
|
|
if (matchDecoders(onMsg, metadata, msgMetadata, getMethodHandle))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
// No decoders matched try partial signatures and pong signatures.
|
|
|
|
|
if (matchOnMessage(onMsg, metadata, msgMetadata, getMethodHandle))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
// Not a valid @OnMessage declaration signature.
|
|
|
|
|
throw InvalidSignatureException.build(endpointClass, OnMessage.class, onMsg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return metadata;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean matchOnMessage(Method onMsg, JavaxWebSocketFrameHandlerMetadata metadata, JavaxWebSocketMessageMetadata msgMetadata,
|
|
|
|
|
Function<InvokerUtils.Arg[], MethodHandle> getMethodHandle)
|
|
|
|
|
{
|
|
|
|
|
// Partial Text Message.
|
|
|
|
|
MethodHandle methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.textPartialCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(PartialStringMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setTextMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Partial ByteBuffer Binary Message.
|
|
|
|
|
methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.binaryPartialBufferCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(PartialByteBufferMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Partial byte[] Binary Message.
|
|
|
|
|
methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.binaryPartialArrayCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(PartialByteArrayMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pong Message.
|
|
|
|
|
MethodHandle pongHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.pongCallingArgs);
|
|
|
|
|
if (pongHandle != null)
|
|
|
|
|
{
|
|
|
|
|
metadata.setPongHandle(pongHandle, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean matchDecoders(Method onMsg, JavaxWebSocketFrameHandlerMetadata metadata, JavaxWebSocketMessageMetadata msgMetadata,
|
|
|
|
|
Function<InvokerUtils.Arg[], MethodHandle> getMethodHandle)
|
|
|
|
|
{
|
|
|
|
|
// Get the first decoder match.
|
|
|
|
|
RegisteredDecoder firstDecoder = metadata.getAvailableDecoders().stream()
|
|
|
|
|
.filter(registeredDecoder -> getMethodHandle.apply(getArgsFor(registeredDecoder.objectType)) != null)
|
|
|
|
|
.findFirst()
|
|
|
|
|
.orElse(null);
|
|
|
|
|
if (firstDecoder == null)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
// Assemble a list of matching decoders which implement the interface type of the first matching decoder found.
|
|
|
|
|
List<RegisteredDecoder> decoders = new ArrayList<>();
|
|
|
|
|
Class<? extends Decoder> interfaceType = firstDecoder.interfaceType;
|
|
|
|
|
metadata.getAvailableDecoders().stream()
|
|
|
|
|
.filter(decoder ->
|
|
|
|
|
{
|
|
|
|
|
InvokerUtils.Arg[] args = {new InvokerUtils.Arg(Session.class), new InvokerUtils.Arg(decoder.objectType).required()};
|
|
|
|
|
return decoder.interfaceType.equals(interfaceType) && (getMethodHandle.apply(args) != null);
|
|
|
|
|
}).forEach(decoders::add);
|
|
|
|
|
msgMetadata.setRegisteredDecoders(decoders);
|
|
|
|
|
|
|
|
|
|
// Get the general methodHandle which applies to all the decoders in the list.
|
|
|
|
|
Class<?> objectType = firstDecoder.objectType;
|
|
|
|
|
for (RegisteredDecoder decoder : decoders)
|
|
|
|
|
{
|
|
|
|
|
if (decoder.objectType.isAssignableFrom(objectType))
|
|
|
|
|
objectType = decoder.objectType;
|
|
|
|
|
};
|
|
|
|
|
InvokerUtils.Arg[] args = {new InvokerUtils.Arg(Session.class), new InvokerUtils.Arg(objectType).required()};
|
|
|
|
|
MethodHandle methodHandle = getMethodHandle.apply(args);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
|
|
|
|
|
// Set the sinkClass and then set the MessageMetadata on the FrameHandlerMetadata
|
|
|
|
|
if (interfaceType.equals(Decoder.Text.class))
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(DecodedTextMessageSink.class);
|
|
|
|
|
metadata.setTextMetadata(msgMetadata, onMsg);
|
|
|
|
|
}
|
|
|
|
|
else if (interfaceType.equals(Decoder.Binary.class))
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(DecodedBinaryMessageSink.class);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
}
|
|
|
|
|
else if (interfaceType.equals(Decoder.TextStream.class))
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(DecodedTextStreamMessageSink.class);
|
|
|
|
|
metadata.setTextMetadata(msgMetadata, onMsg);
|
|
|
|
|
}
|
|
|
|
|
else if (interfaceType.equals(Decoder.BinaryStream.class))
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(DecodedBinaryStreamMessageSink.class);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void assertSignatureValid(Class<?> endpointClass, Method method, Class<? extends Annotation> annotationClass)
|
|
|
|
|
{
|
|
|
|
|
// Test modifiers
|
|
|
|
|
int mods = method.getModifiers();
|
|
|
|
|
if (!Modifier.isPublic(mods))
|
|
|
|
|
{
|
|
|
|
|
StringBuilder err = new StringBuilder();
|
|
|
|
|
err.append("@").append(annotationClass.getSimpleName());
|
|
|
|
|
err.append(" method must be public: ");
|
|
|
|
|
ReflectUtils.append(err, endpointClass, method);
|
|
|
|
|
throw new InvalidSignatureException(err.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Modifier.isStatic(mods))
|
|
|
|
|
{
|
|
|
|
|
StringBuilder err = new StringBuilder();
|
|
|
|
|
err.append("@").append(annotationClass.getSimpleName());
|
|
|
|
|
err.append(" method must not be static: ");
|
|
|
|
|
ReflectUtils.append(err, endpointClass, method);
|
|
|
|
|
throw new InvalidSignatureException(err.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test return type
|
|
|
|
|
Class<?> returnType = method.getReturnType();
|
|
|
|
|
if ((returnType == Void.TYPE) || (returnType == Void.class))
|
|
|
|
|
{
|
|
|
|
|
// Void is 100% valid, always
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!OnMessage.class.isAssignableFrom(annotationClass))
|
|
|
|
|
{
|
|
|
|
|
StringBuilder err = new StringBuilder();
|
|
|
|
|
err.append("@").append(annotationClass.getSimpleName());
|
|
|
|
|
err.append(" return must be void: ");
|
|
|
|
|
ReflectUtils.append(err, endpointClass, method);
|
|
|
|
|
throw new InvalidSignatureException(err.toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bind the URI Template Variables to their provided values, converting to the type
|
|
|
|
|
* that the MethodHandle target has declared.
|
|
|
|
@ -260,382 +571,6 @@ public abstract class JavaxWebSocketFrameHandlerFactory
|
|
|
|
|
return retHandle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static MessageSink createMessageSink(JavaxWebSocketSession session, JavaxWebSocketMessageMetadata msgMetadata)
|
|
|
|
|
{
|
|
|
|
|
if (msgMetadata == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
MethodHandles.Lookup lookup = getServerMethodHandleLookup();
|
|
|
|
|
if (AbstractDecodedMessageSink.class.isAssignableFrom(msgMetadata.getSinkClass()))
|
|
|
|
|
{
|
|
|
|
|
MethodHandle ctorHandle = lookup.findConstructor(msgMetadata.getSinkClass(),
|
|
|
|
|
MethodType.methodType(void.class, CoreSession.class, MethodHandle.class, List.class));
|
|
|
|
|
List<RegisteredDecoder> registeredDecoders = msgMetadata.getRegisteredDecoders();
|
|
|
|
|
return (MessageSink)ctorHandle.invoke(session.getCoreSession(), msgMetadata.getMethodHandle(), registeredDecoders);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
MethodHandle ctorHandle = lookup.findConstructor(msgMetadata.getSinkClass(),
|
|
|
|
|
MethodType.methodType(void.class, CoreSession.class, MethodHandle.class));
|
|
|
|
|
return (MessageSink)ctorHandle.invoke(session.getCoreSession(), msgMetadata.getMethodHandle());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (NoSuchMethodException e)
|
|
|
|
|
{
|
|
|
|
|
throw new RuntimeException("Missing expected MessageSink constructor found at: " + msgMetadata.getSinkClass().getName(), e);
|
|
|
|
|
}
|
|
|
|
|
catch (IllegalAccessException | InstantiationException | InvocationTargetException e)
|
|
|
|
|
{
|
|
|
|
|
throw new RuntimeException("Unable to create MessageSink: " + msgMetadata.getSinkClass().getName(), e);
|
|
|
|
|
}
|
|
|
|
|
catch (RuntimeException e)
|
|
|
|
|
{
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
catch (Throwable t)
|
|
|
|
|
{
|
|
|
|
|
throw new RuntimeException(t);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static MethodHandle wrapNonVoidReturnType(MethodHandle handle, JavaxWebSocketSession session)
|
|
|
|
|
{
|
|
|
|
|
if (handle == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
if (handle.type().returnType() == Void.TYPE)
|
|
|
|
|
return handle;
|
|
|
|
|
|
|
|
|
|
// Technique from https://stackoverflow.com/questions/48505787/methodhandle-with-general-non-void-return-filter
|
|
|
|
|
|
|
|
|
|
// Change the return type of the to be Object so it will match exact with JavaxWebSocketSession.filterReturnType(Object)
|
|
|
|
|
handle = handle.asType(handle.type().changeReturnType(Object.class));
|
|
|
|
|
|
|
|
|
|
// Filter the method return type to a call to JavaxWebSocketSession.filterReturnType() bound to this session
|
|
|
|
|
handle = MethodHandles.filterReturnValue(handle, FILTER_RETURN_TYPE_METHOD.bindTo(session));
|
|
|
|
|
|
|
|
|
|
return handle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private MethodHandle toMethodHandle(MethodHandles.Lookup lookup, Method method)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return lookup.unreflect(method);
|
|
|
|
|
}
|
|
|
|
|
catch (IllegalAccessException e)
|
|
|
|
|
{
|
|
|
|
|
throw new RuntimeException("Unable to access method " + method, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected JavaxWebSocketFrameHandlerMetadata createEndpointMetadata(Class<? extends javax.websocket.Endpoint> endpointClass, EndpointConfig endpointConfig)
|
|
|
|
|
{
|
|
|
|
|
JavaxWebSocketFrameHandlerMetadata metadata = new JavaxWebSocketFrameHandlerMetadata(endpointConfig);
|
|
|
|
|
MethodHandles.Lookup lookup = getApplicationMethodHandleLookup(endpointClass);
|
|
|
|
|
|
|
|
|
|
Method openMethod = ReflectUtils.findMethod(endpointClass, "onOpen",
|
|
|
|
|
javax.websocket.Session.class, javax.websocket.EndpointConfig.class);
|
|
|
|
|
MethodHandle open = toMethodHandle(lookup, openMethod);
|
|
|
|
|
metadata.setOpenHandler(open, openMethod);
|
|
|
|
|
|
|
|
|
|
Method closeMethod = ReflectUtils.findMethod(endpointClass, "onClose",
|
|
|
|
|
javax.websocket.Session.class, javax.websocket.CloseReason.class);
|
|
|
|
|
MethodHandle close = toMethodHandle(lookup, closeMethod);
|
|
|
|
|
metadata.setCloseHandler(close, closeMethod);
|
|
|
|
|
|
|
|
|
|
Method errorMethod = ReflectUtils.findMethod(endpointClass, "onError",
|
|
|
|
|
javax.websocket.Session.class, Throwable.class);
|
|
|
|
|
MethodHandle error = toMethodHandle(lookup, errorMethod);
|
|
|
|
|
metadata.setErrorHandler(error, errorMethod);
|
|
|
|
|
|
|
|
|
|
return metadata;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected JavaxWebSocketFrameHandlerMetadata discoverJavaxFrameHandlerMetadata(Class<?> endpointClass, JavaxWebSocketFrameHandlerMetadata metadata)
|
|
|
|
|
{
|
|
|
|
|
MethodHandles.Lookup lookup = getApplicationMethodHandleLookup(endpointClass);
|
|
|
|
|
Method onmethod;
|
|
|
|
|
|
|
|
|
|
// OnOpen [0..1]
|
|
|
|
|
onmethod = ReflectUtils.findAnnotatedMethod(endpointClass, OnOpen.class);
|
|
|
|
|
if (onmethod != null)
|
|
|
|
|
{
|
|
|
|
|
assertSignatureValid(endpointClass, onmethod, OnOpen.class);
|
|
|
|
|
final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class);
|
|
|
|
|
final InvokerUtils.Arg ENDPOINT_CONFIG = new InvokerUtils.Arg(EndpointConfig.class);
|
|
|
|
|
MethodHandle methodHandle = InvokerUtils
|
|
|
|
|
.mutatedInvoker(lookup, endpointClass, onmethod, paramIdentifier, metadata.getNamedTemplateVariables(), SESSION, ENDPOINT_CONFIG);
|
|
|
|
|
metadata.setOpenHandler(methodHandle, onmethod);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnClose [0..1]
|
|
|
|
|
onmethod = ReflectUtils.findAnnotatedMethod(endpointClass, OnClose.class);
|
|
|
|
|
if (onmethod != null)
|
|
|
|
|
{
|
|
|
|
|
assertSignatureValid(endpointClass, onmethod, OnClose.class);
|
|
|
|
|
final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class);
|
|
|
|
|
final InvokerUtils.Arg CLOSE_REASON = new InvokerUtils.Arg(CloseReason.class);
|
|
|
|
|
MethodHandle methodHandle = InvokerUtils
|
|
|
|
|
.mutatedInvoker(lookup, endpointClass, onmethod, paramIdentifier, metadata.getNamedTemplateVariables(), SESSION, CLOSE_REASON);
|
|
|
|
|
metadata.setCloseHandler(methodHandle, onmethod);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnError [0..1]
|
|
|
|
|
onmethod = ReflectUtils.findAnnotatedMethod(endpointClass, OnError.class);
|
|
|
|
|
if (onmethod != null)
|
|
|
|
|
{
|
|
|
|
|
assertSignatureValid(endpointClass, onmethod, OnError.class);
|
|
|
|
|
final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class);
|
|
|
|
|
final InvokerUtils.Arg CAUSE = new InvokerUtils.Arg(Throwable.class).required();
|
|
|
|
|
MethodHandle methodHandle = InvokerUtils
|
|
|
|
|
.mutatedInvoker(lookup, endpointClass, onmethod, paramIdentifier, metadata.getNamedTemplateVariables(), SESSION, CAUSE);
|
|
|
|
|
metadata.setErrorHandler(methodHandle, onmethod);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OnMessage [0..2]
|
|
|
|
|
Method[] onMessages = ReflectUtils.findAnnotatedMethods(endpointClass, OnMessage.class);
|
|
|
|
|
if (onMessages != null && onMessages.length > 0)
|
|
|
|
|
{
|
|
|
|
|
for (Method onMsg : onMessages)
|
|
|
|
|
{
|
|
|
|
|
assertSignatureValid(endpointClass, onMsg, OnMessage.class);
|
|
|
|
|
OnMessage onMessageAnno = onMsg.getAnnotation(OnMessage.class);
|
|
|
|
|
|
|
|
|
|
long annotationMaxMessageSize = onMessageAnno.maxMessageSize();
|
|
|
|
|
if (annotationMaxMessageSize > Integer.MAX_VALUE)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidWebSocketException(String.format("Value too large: %s#%s - @OnMessage.maxMessageSize=%,d > Integer.MAX_VALUE",
|
|
|
|
|
endpointClass.getName(), onMsg.getName(), annotationMaxMessageSize));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create MessageMetadata and set annotated maxMessageSize if it is not the default value.
|
|
|
|
|
JavaxWebSocketMessageMetadata msgMetadata = new JavaxWebSocketMessageMetadata();
|
|
|
|
|
if (annotationMaxMessageSize != -1)
|
|
|
|
|
msgMetadata.setMaxMessageSize((int)annotationMaxMessageSize);
|
|
|
|
|
|
|
|
|
|
// Function to search for matching MethodHandle for the endpointClass given a signature.
|
|
|
|
|
Function<InvokerUtils.Arg[], MethodHandle> getMethodHandle = (signature) ->
|
|
|
|
|
InvokerUtils.optionalMutatedInvoker(lookup, endpointClass, onMsg, paramIdentifier, metadata.getNamedTemplateVariables(), signature);
|
|
|
|
|
|
|
|
|
|
// Try to match from available decoders.
|
|
|
|
|
if (matchDecoders(onMsg, metadata, msgMetadata, getMethodHandle))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
// No decoders matched try basic signatures to call onMessage directly.
|
|
|
|
|
if (matchOnMessage(onMsg, metadata, msgMetadata, getMethodHandle))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
// Not a valid @OnMessage declaration signature.
|
|
|
|
|
throw InvalidSignatureException.build(endpointClass, OnMessage.class, onMsg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return metadata;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean matchOnMessage(Method onMsg, JavaxWebSocketFrameHandlerMetadata metadata, JavaxWebSocketMessageMetadata msgMetadata,
|
|
|
|
|
Function<InvokerUtils.Arg[], MethodHandle> getMethodHandle)
|
|
|
|
|
{
|
|
|
|
|
// Whole Text Message.
|
|
|
|
|
MethodHandle methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.textCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(StringMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setTextMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Partial Text Message.
|
|
|
|
|
methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.textPartialCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(PartialStringMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setTextMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Whole ByteBuffer Binary Message.
|
|
|
|
|
methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.binaryBufferCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(ByteBufferMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Partial ByteBuffer Binary Message.
|
|
|
|
|
methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.binaryPartialBufferCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(PartialByteBufferMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Whole byte[] Binary Message.
|
|
|
|
|
methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.binaryArrayCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(ByteArrayMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Partial byte[] Binary Message.
|
|
|
|
|
methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.binaryPartialArrayCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(PartialByteArrayMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// InputStream Binary Message.
|
|
|
|
|
methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.inputStreamCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(InputStreamMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reader Text Message.
|
|
|
|
|
methodHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.readerCallingArgs);
|
|
|
|
|
if (methodHandle != null)
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(ReaderMessageSink.class);
|
|
|
|
|
msgMetadata.setMethodHandle(methodHandle);
|
|
|
|
|
metadata.setTextMetadata(msgMetadata, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pong Message.
|
|
|
|
|
MethodHandle pongHandle = getMethodHandle.apply(JavaxWebSocketCallingArgs.pongCallingArgs);
|
|
|
|
|
if (pongHandle != null)
|
|
|
|
|
{
|
|
|
|
|
metadata.setPongHandle(pongHandle, onMsg);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean matchDecoders(Method onMsg, JavaxWebSocketFrameHandlerMetadata metadata, JavaxWebSocketMessageMetadata msgMetadata,
|
|
|
|
|
Function<InvokerUtils.Arg[], MethodHandle> getMethodHandle)
|
|
|
|
|
{
|
|
|
|
|
// We need to get all the decoders which match not just the first.
|
|
|
|
|
Stream<RegisteredDecoder> matchedDecodersStream = metadata.getAvailableDecoders().stream().filter(registeredDecoder ->
|
|
|
|
|
{
|
|
|
|
|
InvokerUtils.Arg[] args = {new InvokerUtils.Arg(Session.class), new InvokerUtils.Arg(registeredDecoder.objectType).required()};
|
|
|
|
|
return getMethodHandle.apply(args) != null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Use the interface type of the first matched decoder.
|
|
|
|
|
RegisteredDecoder firstDecoder = matchedDecodersStream.findFirst().orElse(null);
|
|
|
|
|
if (firstDecoder == null)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: COMMENT
|
|
|
|
|
List<RegisteredDecoder> decoders = new ArrayList<>();
|
|
|
|
|
Class<? extends Decoder> interfaceType = firstDecoder.interfaceType;
|
|
|
|
|
metadata.getAvailableDecoders().stream()
|
|
|
|
|
.filter(registeredDecoder -> registeredDecoder.interfaceType.equals(interfaceType))
|
|
|
|
|
.forEach(decoders::add);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get the original argument type.
|
|
|
|
|
Class<?> type = firstDecoder.objectType;
|
|
|
|
|
for (Class<?> clazz : onMsg.getParameterTypes())
|
|
|
|
|
{
|
|
|
|
|
if (clazz.isAssignableFrom(firstDecoder.objectType))
|
|
|
|
|
type = clazz;
|
|
|
|
|
}
|
|
|
|
|
InvokerUtils.Arg[] generalArgs = {new InvokerUtils.Arg(Session.class), new InvokerUtils.Arg(type).required()};
|
|
|
|
|
MethodHandle generalMethodHandle = getMethodHandle.apply(generalArgs);
|
|
|
|
|
if (generalMethodHandle == null)
|
|
|
|
|
{
|
|
|
|
|
// TODO: warn or throw
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
msgMetadata.setRegisteredDecoders(decoders);
|
|
|
|
|
msgMetadata.setMethodHandle(generalMethodHandle);
|
|
|
|
|
|
|
|
|
|
if (interfaceType.equals(Decoder.Text.class))
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(DecodedTextMessageSink.class);
|
|
|
|
|
metadata.setTextMetadata(msgMetadata, onMsg);
|
|
|
|
|
}
|
|
|
|
|
else if (interfaceType.equals(Decoder.Binary.class))
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(DecodedBinaryMessageSink.class);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
}
|
|
|
|
|
else if (interfaceType.equals(Decoder.TextStream.class))
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(DecodedTextStreamMessageSink.class);
|
|
|
|
|
metadata.setTextMetadata(msgMetadata, onMsg);
|
|
|
|
|
}
|
|
|
|
|
else if (interfaceType.equals(Decoder.BinaryStream.class))
|
|
|
|
|
{
|
|
|
|
|
msgMetadata.setSinkClass(DecodedBinaryStreamMessageSink.class);
|
|
|
|
|
metadata.setBinaryMetadata(msgMetadata, onMsg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void assertSignatureValid(Class<?> endpointClass, Method method, Class<? extends Annotation> annotationClass)
|
|
|
|
|
{
|
|
|
|
|
// Test modifiers
|
|
|
|
|
int mods = method.getModifiers();
|
|
|
|
|
if (!Modifier.isPublic(mods))
|
|
|
|
|
{
|
|
|
|
|
StringBuilder err = new StringBuilder();
|
|
|
|
|
err.append("@").append(annotationClass.getSimpleName());
|
|
|
|
|
err.append(" method must be public: ");
|
|
|
|
|
ReflectUtils.append(err, endpointClass, method);
|
|
|
|
|
throw new InvalidSignatureException(err.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Modifier.isStatic(mods))
|
|
|
|
|
{
|
|
|
|
|
StringBuilder err = new StringBuilder();
|
|
|
|
|
err.append("@").append(annotationClass.getSimpleName());
|
|
|
|
|
err.append(" method must not be static: ");
|
|
|
|
|
ReflectUtils.append(err, endpointClass, method);
|
|
|
|
|
throw new InvalidSignatureException(err.toString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test return type
|
|
|
|
|
Class<?> returnType = method.getReturnType();
|
|
|
|
|
if ((returnType == Void.TYPE) || (returnType == Void.class))
|
|
|
|
|
{
|
|
|
|
|
// Void is 100% valid, always
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!OnMessage.class.isAssignableFrom(annotationClass))
|
|
|
|
|
{
|
|
|
|
|
StringBuilder err = new StringBuilder();
|
|
|
|
|
err.append("@").append(annotationClass.getSimpleName());
|
|
|
|
|
err.append(" return must be void: ");
|
|
|
|
|
ReflectUtils.append(err, endpointClass, method);
|
|
|
|
|
throw new InvalidSignatureException(err.toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* <p>
|
|
|
|
|
* Gives a {@link MethodHandles.Lookup} instance to be used to find methods in server classes.
|
|
|
|
|