diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldsMap.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldsMap.java index 1f66b95a0e72..680df4b73bb5 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldsMap.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFieldsMap.java @@ -98,6 +98,14 @@ public List remove(Object key) return null; } + @Override + public boolean containsKey(Object key) + { + if (key instanceof String s) + return httpFields.contains(s); + return false; + } + @Override public Set>> entrySet() { @@ -178,6 +186,14 @@ public List remove(Object key) throw new UnsupportedOperationException(); } + @Override + public boolean containsKey(Object key) + { + if (key instanceof String s) + return httpFields.contains(s); + return false; + } + @Override public Set>> entrySet() { diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/AbstractMessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/AbstractMessageSink.java index 979098405bd8..16c60cd221cc 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/AbstractMessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/AbstractMessageSink.java @@ -42,7 +42,7 @@ public abstract class AbstractMessageSink implements MessageSink { private final CoreSession session; - private final MethodHolder methodHandle; + private final MethodHolder methodHolder; private final boolean autoDemand; /** @@ -56,7 +56,7 @@ public abstract class AbstractMessageSink implements MessageSink public AbstractMessageSink(CoreSession session, MethodHolder methodHolder, boolean autoDemand) { this.session = Objects.requireNonNull(session, "CoreSession"); - this.methodHandle = Objects.requireNonNull(methodHolder, "MethodHolder"); + this.methodHolder = Objects.requireNonNull(methodHolder, "MethodHolder"); this.autoDemand = autoDemand; } @@ -75,7 +75,7 @@ public CoreSession getCoreSession() */ public MethodHolder getMethodHolder() { - return methodHandle; + return methodHolder; } /** diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/DispatchedMessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/DispatchedMessageSink.java index 44ea17285b2a..13a03b214c19 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/DispatchedMessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/DispatchedMessageSink.java @@ -19,6 +19,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; @@ -48,8 +50,10 @@ public abstract class DispatchedMessageSink extends AbstractMessageSink { private final Executor executor; + private final AtomicBoolean wasCallbackFailed = new AtomicBoolean(false); private volatile CompletableFuture dispatchComplete; private MessageSink typeSink; + private Consumer onError; public DispatchedMessageSink(CoreSession session, MethodHolder methodHolder, boolean autoDemand) { @@ -59,6 +63,15 @@ public DispatchedMessageSink(CoreSession session, MethodHolder methodHolder, boo executor = session.getWebSocketComponents().getExecutor(); } + public DispatchedMessageSink(CoreSession session, MethodHolder methodHolder, boolean autoDemand, Consumer onError) + { + super(session, methodHolder, autoDemand); + if (!autoDemand) + throw new IllegalArgumentException("%s must be auto-demanding".formatted(getClass().getSimpleName())); + this.executor = session.getWebSocketComponents().getExecutor(); + this.onError = onError; + } + public abstract MessageSink newMessageSink(); public void accept(Frame frame, final Callback callback) @@ -105,17 +118,31 @@ public void accept(Frame frame, final Callback callback) { autoDemand(); } - else + // We only need to handle the error here if none of the callbacks were ever failed. + else if (!wasCallbackFailed.get()) { if (failure instanceof CompletionException completionException) failure = completionException.getCause(); - CloseStatus closeStatus = new CloseStatus(CloseStatus.SERVER_ERROR, failure); - getCoreSession().close(closeStatus, Callback.NOOP); + if (onError == null) + { + CloseStatus closeStatus = new CloseStatus(CloseStatus.SERVER_ERROR, failure); + getCoreSession().close(closeStatus, Callback.NOOP); + } + else + { + onError.accept(failure); + } } }); } + frameCallback = Callback.from(frameCallback::succeeded, throwable -> + { + if (throwable != null) + wasCallbackFailed.set(true); + callback.failed(throwable); + }); typeSink.accept(frame, frameCallback); } diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/InputStreamMessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/InputStreamMessageSink.java index bbf826747cbc..00a9c7413598 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/InputStreamMessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/InputStreamMessageSink.java @@ -13,6 +13,8 @@ package org.eclipse.jetty.websocket.core.messages; +import java.util.function.Consumer; + import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.util.MethodHolder; @@ -23,6 +25,11 @@ public InputStreamMessageSink(CoreSession session, MethodHolder methodHolder, bo super(session, methodHolder, autoDemand); } + public InputStreamMessageSink(CoreSession session, MethodHolder methodHolder, boolean autoDemand, Consumer onError) + { + super(session, methodHolder, autoDemand, onError); + } + @Override public MessageSink newMessageSink() { diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageOutputStream.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageOutputStream.java index 61c02988630b..c4bf56768632 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageOutputStream.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/MessageOutputStream.java @@ -171,6 +171,9 @@ public void close() throws IOException { try { + if (closed) + return; + flush(true); buffer.release(); if (LOG.isDebugEnabled()) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ReaderMessageSink.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ReaderMessageSink.java index fb6cc87f1e7f..c0b407251469 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ReaderMessageSink.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/messages/ReaderMessageSink.java @@ -13,6 +13,8 @@ package org.eclipse.jetty.websocket.core.messages; +import java.util.function.Consumer; + import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.util.MethodHolder; @@ -23,6 +25,11 @@ public ReaderMessageSink(CoreSession session, MethodHolder methodHolder, boolean super(session, methodHolder, autoDemand); } + public ReaderMessageSink(CoreSession session, MethodHolder methodHolder, boolean autoDemand, Consumer onError) + { + super(session, methodHolder, autoDemand, onError); + } + @Override public MessageReader newMessageSink() { diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/InvokerUtils.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/InvokerUtils.java index e8a6e6c68e81..6e54c8bb672c 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/InvokerUtils.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/InvokerUtils.java @@ -35,6 +35,7 @@ public static class Arg private boolean required = false; private boolean convertible = false; private Class convertedType; + boolean toBeRemoved = false; public Arg(Class type) { @@ -113,6 +114,16 @@ public boolean isRequired() { return required; } + + @Override + public String toString() + { + return String.format("Arg[%s]", + type.getSimpleName() + + (name != null ? ", name=" + name : "") + + (required ? ", required" : "") + + (convertible ? ", convertible" : "")); + } } public interface ParamIdentifier @@ -207,45 +218,69 @@ public static MethodHandle mutatedInvoker(MethodHandles.Lookup lookup, Class return mutatedInvoker(lookup, targetClass, true, method, paramIdentifier, namedVariables, callingArgs); } - private static MethodHandle mutatedInvoker(MethodHandles.Lookup lookup, Class targetClass, boolean throwOnFailure, - Method method, ParamIdentifier paramIdentifier, - String[] namedVariables, Arg... rawCallingArgs) + private static MethodHandle mutatedInvoker(MethodHandles.Lookup lookup, + Class targetClass, + boolean throwOnFailure, + Method method, + ParamIdentifier paramIdentifier, + String[] namedVariables, + Arg... rawCallingArgs) { Class[] parameterTypes = method.getParameterTypes(); - // Construct Actual Calling Args. - // This is the array of args, arriving as all the named variables (usually static in nature), - // then the raw calling arguments (very dynamic in nature) - Arg[] callingArgs = new Arg[rawCallingArgs.length + (namedVariables == null ? 0 : namedVariables.length)]; + // Build "parameterArgs" to represent the method's actual parameters. + // Where parameterArgs[0] always represents the targetClass instance. + boolean hasNamedParamArgs = false; + Arg[] parameterArgs = new Arg[parameterTypes.length + 1]; + parameterArgs[0] = new Arg(targetClass); + for (int i = 0; i < parameterTypes.length; i++) { - int callingArgIdx = 0; - if (namedVariables != null) - { - for (String namedVariable : namedVariables) - { - callingArgs[callingArgIdx++] = new Arg(String.class, namedVariable).convertible(); - } - } + Arg paramArg = paramIdentifier.getParamArg(method, parameterTypes[i], i); + if (paramArg.name != null) + hasNamedParamArgs = true; + parameterArgs[i + 1] = paramArg; + } + + // Construct the calling args array which combine namedVariables and rawCallingArgs. + int namedCount = (namedVariables == null) ? 0 : namedVariables.length; + Arg[] callingArgs = new Arg[namedCount + rawCallingArgs.length]; + int idx = 0; - for (Arg rawCallingArg : rawCallingArgs) + if (namedVariables != null) + { + for (String nv : namedVariables) { - callingArgs[callingArgIdx++] = rawCallingArg; + callingArgs[idx++] = new Arg(String.class, nv).convertible(); } } - // Build up Arg list representing the MethodHandle parameters - // ParamIdentifier is used to find named parameters (like jakarta.websocket's @PathParam declaration) - boolean hasNamedParamArgs = false; - Arg[] parameterArgs = new Arg[parameterTypes.length + 1]; - parameterArgs[0] = new Arg(targetClass); // first type is always the calling object instance type - for (int i = 0; i < parameterTypes.length; i++) + for (Arg rawArg : rawCallingArgs) { - Arg arg = paramIdentifier.getParamArg(method, parameterTypes[i], i); - if (arg.name != null) + callingArgs[idx++] = rawArg; + } + + // If a parameter arg is named but does not match a calling arg, it should be added to calling args + // but marked that it should be removed and given a null value. + for (Arg paramArg : parameterArgs) + { + if (paramArg.name == null) + continue; + + boolean found = false; + for (Arg callingArg : callingArgs) { - hasNamedParamArgs = true; + if (callingArg.matches(paramArg)) + { + found = true; + break; + } + } + if (!found) + { + callingArgs = Arrays.copyOf(callingArgs, callingArgs.length + 1); + callingArgs[callingArgs.length - 1] = paramArg; + paramArg.toBeRemoved = true; } - parameterArgs[i + 1] = arg; } // Parameter to Calling Argument mapping. @@ -263,20 +298,18 @@ private static MethodHandle mutatedInvoker(MethodHandles.Lookup lookup, Class throw new InvalidSignatureException(err.toString()); } - // Establish MethodType for supplied calling args + // Establish MethodType for supplied calling args. boolean hasNamedCallingArgs = false; boolean hasConvertibleTypes = false; - List> cTypes = new ArrayList<>(); + List> callingTypes = new ArrayList<>(1 + callingArgs.length); + callingTypes.add(targetClass); // param0 = instance + for (Arg arg : callingArgs) { - cTypes.add(targetClass); // targetClass always at index 0 - for (Arg arg : callingArgs) - { - if (arg.name != null) - hasNamedCallingArgs = true; - if (arg.convertible) - hasConvertibleTypes = true; - cTypes.add(arg.getType()); - } + if (arg.name != null) + hasNamedCallingArgs = true; + if (arg.convertible) + hasConvertibleTypes = true; + callingTypes.add(arg.getType()); } try @@ -286,12 +319,12 @@ private static MethodHandle mutatedInvoker(MethodHandles.Lookup lookup, Class // the calling 'refc' type of where the method is declared, not the targetClass. // That behavior of #unreflect() results in a MethodType referring to the // base/abstract/interface where the method is declared, and not the targetClass - MethodType callingType = MethodType.methodType(method.getReturnType(), cTypes); - MethodType rawType = MethodType.methodType(method.getReturnType(), method.getParameterTypes()); + MethodType callingType = MethodType.methodType(method.getReturnType(), callingTypes); + MethodType rawType = MethodType.methodType(method.getReturnType(), parameterTypes); MethodHandle methodHandle = lookup.findVirtual(targetClass, method.getName(), rawType); // If callingType and rawType are the same (and there's no named args), - // then there's no need to reorder / permute / drop args + // then there's no need to reorder / permute / drop args. if (!hasNamedCallingArgs && !hasNamedParamArgs && rawType.equals(callingType)) return methodHandle; @@ -308,24 +341,25 @@ private static MethodHandle mutatedInvoker(MethodHandles.Lookup lookup, Class boolean[] usedCallingArgs = new boolean[callingArgs.length]; Arrays.fill(usedCallingArgs, false); - // Iterate through each parameterArg and attempt to find an associated callingArg + // Iterate through each parameterArg and attempt to find an associated callingArg. for (int pi = 1; pi < parameterArgs.length; pi++) { - int ref = -1; + int matchIndex = -1; + Arg paramArg = parameterArgs[pi]; - // Find a reference to argument in callArgs + // Look for an unused callingArg that matches this paramArg. for (int ci = 0; ci < callingArgs.length; ci++) { - if (!usedCallingArgs[ci] && callingArgs[ci].matches(parameterArgs[pi])) + if (!usedCallingArgs[ci] && callingArgs[ci].matches(paramArg)) { - ref = ci + 1; // add 1 to compensate for parameter 0 + matchIndex = ci + 1; // add 1 to compensate for parameter 0 usedCallingArgs[ci] = true; break; } } // Didn't find an unused callingArg that fits this parameterArg - if (ref < 0) + if (matchIndex < 0) { if (!throwOnFailure) return null; @@ -340,12 +374,11 @@ private static MethodHandle mutatedInvoker(MethodHandles.Lookup lookup, Class throw new InvalidSignatureException(err.toString()); } - - reorderMap[pi] = ref; + reorderMap[pi] = matchIndex; } // Remaining unused callingArgs are to be placed at end of specified reorderMap - for (int ri = parameterArgs.length; ri <= reorderMap.length; ri++) + for (int ri = parameterArgs.length; ri < reorderMap.length; ) { for (int uci = 0; uci < usedCallingArgs.length; uci++) { @@ -371,7 +404,7 @@ private static MethodHandle mutatedInvoker(MethodHandles.Lookup lookup, Class } } - // Drop excess (not mapped to a method parameter) calling args + // Drop excess (not mapped to a method parameter) calling args. int idxDrop = parameterArgs.length; int dropLength = reorderMap.length - idxDrop; if (dropLength > 0) @@ -388,19 +421,42 @@ private static MethodHandle mutatedInvoker(MethodHandles.Lookup lookup, Class if (hasConvertibleTypes) { // Use converted Types for callingArgs - cTypes = new ArrayList<>(); - cTypes.add(targetClass); // targetClass always at index 0 + callingTypes = new ArrayList<>(); + callingTypes.add(targetClass); // targetClass always at index 0 for (Arg arg : callingArgs) { - cTypes.add(arg.getConvertedType()); + callingTypes.add(arg.getConvertedType()); } - callingType = MethodType.methodType(method.getReturnType(), cTypes); + callingType = MethodType.methodType(method.getReturnType(), callingTypes); } // Reorder calling args to parameter args methodHandle = MethodHandles.permuteArguments(methodHandle, callingType, reorderMap); - // Return method handle + // Bind any named parameters not in the namedVariables list to be null. + // We go from the highest methodHandleIndex because insertArguments will remove that index. + for (int methodHandleIndex = reorderMap.length - 1; methodHandleIndex >= 0; methodHandleIndex--) + { + // find the index of the parameter arg + int parameterArgIndex = -1; + for (int j = 0; j < reorderMap.length; j++) + { + if (reorderMap[j] == methodHandleIndex) + { + parameterArgIndex = j; + break; + } + } + + // Now we know parameterArgIndex is the corresponding arg to this index on the methodHandle. + if (parameterArgIndex < parameterArgs.length) + { + Arg arg = parameterArgs[parameterArgIndex]; + if (arg.toBeRemoved) + methodHandle = MethodHandles.insertArguments(methodHandle, methodHandleIndex, (String)null); + } + } + return methodHandle; } catch (IllegalAccessException | NoSuchMethodException e) diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/ReflectUtils.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/ReflectUtils.java index 7b0d50bebf42..2ab662dd183d 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/ReflectUtils.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/util/ReflectUtils.java @@ -15,16 +15,21 @@ import java.lang.annotation.Annotation; import java.lang.invoke.MethodType; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.regex.Pattern; -import java.util.stream.Stream; import org.eclipse.jetty.websocket.core.exception.DuplicateAnnotationException; @@ -141,19 +146,49 @@ public static Method findAnnotatedMethod(Class pojo, Class pojo, Class anno) { - Class clazz = pojo; - List methods = new ArrayList<>(); - while ((clazz != null) && Object.class.isAssignableFrom(clazz)) + Set seenSignatures = new HashSet<>(); + List annotatedMethods = new ArrayList<>(); + + for (Class clazz = pojo; (clazz != null) && Object.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) { - Stream.of(clazz.getDeclaredMethods()) - .filter(method -> !method.isSynthetic() && (method.getAnnotation(anno) != null)) - .forEach(methods::add); - clazz = clazz.getSuperclass(); + for (Method method : clazz.getDeclaredMethods()) + { + if (method.isSynthetic() || method.getAnnotation(anno) == null) + continue; + if (seenSignatures.add(new MethodSignature(method))) + annotatedMethods.add(method); + } } - if (methods.isEmpty()) + if (annotatedMethods.isEmpty()) return null; - return methods.toArray(new Method[0]); + return annotatedMethods.toArray(new Method[0]); + } + + private static class MethodSignature + { + private final String name; + private final Class[] parameterTypes; + + MethodSignature(Method method) + { + this.name = method.getName(); + this.parameterTypes = method.getParameterTypes(); + } + + @Override + public boolean equals(Object o) + { + if (o instanceof MethodSignature that) + return Objects.equals(name, that.name) && Arrays.equals(parameterTypes, that.parameterTypes); + return false; + } + + @Override + public int hashCode() + { + return 31 * name.hashCode() + Arrays.hashCode(parameterTypes); + } } /** @@ -172,6 +207,15 @@ public static Class findGenericClassFor(Class baseClass, Class ifaceCla return null; } + public static Type findGenericTypeFor(Class baseClass, Class ifaceClass) + { + GenericRef ref = new GenericRef(baseClass, ifaceClass); + if (resolveGenericRef(ref, baseClass)) + return ref.genericType; + + return null; + } + private static int findTypeParameterIndex(Class clazz, TypeVariable needVar) { TypeVariable[] params = clazz.getTypeParameters(); @@ -368,4 +412,61 @@ public static void append(StringBuilder str, MethodType methodType) } str.append(")"); } + + /** + * Check if a type is assignable from another type. + * This only handles Class, ParameterizedType, and GenericArrayType, and does not handle wildcard types or type variables. + * + * @param superType the superType. + * @param subType the subType. + * @return true if the superType is assignable from the subType. + */ + public static boolean isAssignableFrom(Type superType, Type subType) + { + if (superType instanceof Class superClass && subType instanceof Class subClass) + return superClass.isAssignableFrom(subClass); + + if (superType instanceof ParameterizedType pSuperType && subType instanceof ParameterizedType pSubType) + { + if (!((Class)pSubType.getRawType()).isAssignableFrom((Class)pSuperType.getRawType())) + return false; + + Type[] subTypeArgs = pSubType.getActualTypeArguments(); + Type[] superTypeArgs = pSuperType.getActualTypeArguments(); + if (subTypeArgs.length != superTypeArgs.length) + return false; + + for (int i = 0; i < subTypeArgs.length; i++) + { + if (!isAssignableFrom(subTypeArgs[i], superTypeArgs[i])) + return false; + } + return true; + } + + if (superType instanceof ParameterizedType pSuperType && subType instanceof Class subClass) + return ((Class)pSuperType.getRawType()).isAssignableFrom(subClass); + + if (superType instanceof GenericArrayType superTypeArray && subType instanceof GenericArrayType subTypeArray) + return isAssignableFrom(superTypeArray.getGenericComponentType(), subTypeArray.getGenericComponentType()); + + return false; + } + + public static Class getClassFromType(Type type) + { + if (type instanceof Class) + return (Class)type; + + if (type instanceof ParameterizedType) + return (Class)((ParameterizedType)type).getRawType(); + + if (type instanceof GenericArrayType gType) + { + Class componentClass = getClassFromType(gType.getGenericComponentType()); + return componentClass != null ? Array.newInstance(componentClass, 0).getClass() : null; + } + + return null; + } } diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/com/acme/websocket/PongSocketStringReturn.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/com/acme/websocket/PongSocketStringReturn.java new file mode 100644 index 000000000000..8503c57819cb --- /dev/null +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jakarta-tests/src/test/java/com/acme/websocket/PongSocketStringReturn.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package com.acme.websocket; + +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.PongMessage; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; +import org.eclipse.jetty.util.BufferUtil; + +@ServerEndpoint(value = "/pong-socket-string-return", configurator = PongContextListener.Config.class) +public class PongSocketStringReturn +{ + private String path; + + @OnOpen + public void onOpen(Session session, EndpointConfig config) + { + path = (String)config.getUserProperties().get("path"); + } + + @OnMessage + public String onPong(PongMessage pong) + { + return "PongSocket.onPong(PongMessage)[" + path + "]:" + BufferUtil.toString(pong.getApplicationData()); + } +} diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/JakartaWebSocketClientContainer.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/JakartaWebSocketClientContainer.java index 222375ab6a20..d0643250de79 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/JakartaWebSocketClientContainer.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/JakartaWebSocketClientContainer.java @@ -140,7 +140,7 @@ private CompletableFuture connect(JakartaClientUpgradeRequest upgradeRe { if (error != null) { - futureSession.completeExceptionally(convertCause(error)); + futureSession.completeExceptionally(error); return; } @@ -156,18 +156,6 @@ private CompletableFuture connect(JakartaClientUpgradeRequest upgradeRe return futureSession; } - public static Throwable convertCause(Throwable error) - { - if (error instanceof UpgradeException || - error instanceof WebSocketTimeoutException) - return new IOException(error); - - if (error instanceof InvalidWebSocketException) - return new DeploymentException(error.getMessage(), error); - - return error; - } - private Session connect(ConfiguredEndpoint configuredEndpoint, URI destURI) throws IOException, DeploymentException { if (configuredEndpoint == null) @@ -188,7 +176,7 @@ private Session connect(ConfiguredEndpoint configuredEndpoint, URI destURI) thro upgradeRequest.addExtensions(new JakartaWebSocketExtensionConfig(ext)); } - if (clientEndpointConfig.getPreferredSubprotocols().size() > 0) + if (!clientEndpointConfig.getPreferredSubprotocols().isEmpty()) upgradeRequest.setSubProtocols(clientEndpointConfig.getPreferredSubprotocols()); } @@ -207,6 +195,13 @@ private Session connect(ConfiguredEndpoint configuredEndpoint, URI destURI) thro throw (DeploymentException)cause; if (cause instanceof IOException) throw (IOException)cause; + if (cause instanceof UpgradeException) + throw new DeploymentException(cause.getMessage(), cause); + if (cause instanceof WebSocketTimeoutException) + throw new IOException(cause); + if (cause instanceof InvalidWebSocketException) + throw new DeploymentException(e.getMessage(), e); + throw new IOException(cause); } catch (TimeoutException e) @@ -215,7 +210,7 @@ private Session connect(ConfiguredEndpoint configuredEndpoint, URI destURI) thro } catch (Throwable e) { - throw new IOException("Unable to connect to " + destURI, e); + throw new DeploymentException("Unable to connect to " + destURI, e); } } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/JakartaWebSocketClientFrameHandlerFactory.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/JakartaWebSocketClientFrameHandlerFactory.java index 00ab4f6aa695..a051fd63cc14 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/JakartaWebSocketClientFrameHandlerFactory.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/JakartaWebSocketClientFrameHandlerFactory.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.ee11.websocket.jakarta.client; import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.DeploymentException; import jakarta.websocket.EndpointConfig; import org.eclipse.jetty.ee11.websocket.jakarta.client.internal.BasicClientEndpointConfig; import org.eclipse.jetty.ee11.websocket.jakarta.common.JakartaWebSocketContainer; @@ -40,7 +41,7 @@ public EndpointConfig newDefaultEndpointConfig(Class endpointClass) } @Override - public JakartaWebSocketFrameHandlerMetadata getMetadata(Class endpointClass, EndpointConfig endpointConfig) + public JakartaWebSocketFrameHandlerMetadata getMetadata(Class endpointClass, EndpointConfig endpointConfig) throws DeploymentException { if (jakarta.websocket.Endpoint.class.isAssignableFrom(endpointClass)) return createEndpointMetadata(endpointConfig); diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/internal/JakartaClientUpgradeRequest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/internal/JakartaClientUpgradeRequest.java index 2a5a6830a5cb..8185e042851f 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/internal/JakartaClientUpgradeRequest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-client/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/client/internal/JakartaClientUpgradeRequest.java @@ -16,6 +16,7 @@ import java.net.URI; import java.security.Principal; +import jakarta.websocket.DeploymentException; import org.eclipse.jetty.ee11.websocket.jakarta.client.JakartaWebSocketClientContainer; import org.eclipse.jetty.ee11.websocket.jakarta.common.JakartaWebSocketFrameHandler; import org.eclipse.jetty.ee11.websocket.jakarta.common.UpgradeRequest; @@ -27,7 +28,7 @@ public class JakartaClientUpgradeRequest extends CoreClientUpgradeRequest implem { private final JakartaWebSocketFrameHandler frameHandler; - public JakartaClientUpgradeRequest(JakartaWebSocketClientContainer clientContainer, WebSocketCoreClient coreClient, URI requestURI, Object websocketPojo) + public JakartaClientUpgradeRequest(JakartaWebSocketClientContainer clientContainer, WebSocketCoreClient coreClient, URI requestURI, Object websocketPojo) throws DeploymentException { super(coreClient, requestURI); frameHandler = clientContainer.newFrameHandler(websocketPojo, this); diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketAsyncRemote.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketAsyncRemote.java index 12ed09524010..c82a1b7e26c6 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketAsyncRemote.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketAsyncRemote.java @@ -13,11 +13,9 @@ package org.eclipse.jetty.ee11.websocket.jakarta.common; -import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.Future; -import jakarta.websocket.EncodeException; import jakarta.websocket.Encoder; import jakarta.websocket.SendHandler; import jakarta.websocket.SendResult; @@ -86,6 +84,10 @@ public Future sendObject(Object data) { sendObject(data, future); } + catch (IllegalArgumentException e) + { + throw e; + } catch (Throwable t) { future.failed(t); @@ -93,81 +95,75 @@ public Future sendObject(Object data) return future; } - @SuppressWarnings( - {"rawtypes", "unchecked"}) + @SuppressWarnings({"rawtypes", "unchecked"}) @Override public void sendObject(Object data, SendHandler handler) { assertMessageNotNull(data); assertSendHandlerNotNull(handler); if (LOG.isDebugEnabled()) - { LOG.debug("sendObject({},{})", data, handler); - } Encoder encoder = session.getEncoders().getInstanceFor(data.getClass()); if (encoder == null) - { throw new IllegalArgumentException("No encoder for type: " + data.getClass()); - } - if (encoder instanceof Encoder.Text) + if (encoder instanceof Encoder.Text textEncoder) { - Encoder.Text etxt = (Encoder.Text)encoder; try { - String msg = etxt.encode(data); + String msg = textEncoder.encode(data); sendText(msg, handler); - return; } - catch (EncodeException e) + catch (Throwable t) { - handler.onResult(new SendResult(e)); + handler.onResult(new SendResult(t)); } + return; } - else if (encoder instanceof Encoder.TextStream) + else if (encoder instanceof Encoder.TextStream textStreamEncoder) { - Encoder.TextStream etxt = (Encoder.TextStream)encoder; - SendHandlerCallback callback = new SendHandlerCallback(handler); - try (MessageWriter writer = newMessageWriter()) + try { - writer.setCallback(callback); - etxt.encode(data, writer); - return; + MessageWriter writer = newMessageWriter(); + writer.setCallback(new SendHandlerCallback(handler)); + textStreamEncoder.encode(data, writer); + writer.close(); } - catch (EncodeException | IOException e) + catch (Throwable t) { - handler.onResult(new SendResult(e)); + handler.onResult(new SendResult(t)); } + return; } - else if (encoder instanceof Encoder.Binary) + else if (encoder instanceof Encoder.Binary binaryEncoder) { - Encoder.Binary ebin = (Encoder.Binary)encoder; try { - ByteBuffer buf = ebin.encode(data); + ByteBuffer buf = binaryEncoder.encode(data); sendBinary(buf, handler); - return; } - catch (EncodeException e) + catch (Throwable t) { - handler.onResult(new SendResult(e)); + handler.onResult(new SendResult(t)); } + return; } - else if (encoder instanceof Encoder.BinaryStream) + else if (encoder instanceof Encoder.BinaryStream binaryStreamEncoder) { - Encoder.BinaryStream ebin = (Encoder.BinaryStream)encoder; SendHandlerCallback callback = new SendHandlerCallback(handler); - try (MessageOutputStream out = newMessageOutputStream()) + try { + MessageOutputStream out = newMessageOutputStream(); out.setCallback(callback); - ebin.encode(data, out); - return; + binaryStreamEncoder.encode(data, out); + out.close(); } - catch (EncodeException | IOException e) + catch (Throwable t) { - handler.onResult(new SendResult(e)); + handler.onResult(new SendResult(t)); } + return; } throw new IllegalArgumentException("Unknown encoder type: " + encoder); diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketContainer.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketContainer.java index 4f97d13f890b..5ae540af058d 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketContainer.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketContainer.java @@ -22,6 +22,7 @@ import java.util.concurrent.Executor; import java.util.function.Consumer; +import jakarta.websocket.DeploymentException; import jakarta.websocket.Extension; import jakarta.websocket.WebSocketContainer; import org.eclipse.jetty.io.ByteBufferPool; @@ -155,7 +156,7 @@ public Set getOpenSessions() return sessionTracker.getSessions(); } - public JakartaWebSocketFrameHandler newFrameHandler(Object websocketPojo, UpgradeRequest upgradeRequest) + public JakartaWebSocketFrameHandler newFrameHandler(Object websocketPojo, UpgradeRequest upgradeRequest) throws DeploymentException { return getFrameHandlerFactory().newJakartaWebSocketFrameHandler(websocketPojo, upgradeRequest); } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandler.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandler.java index edb8777d31fa..f49dc893ecb4 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandler.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandler.java @@ -34,6 +34,7 @@ import org.eclipse.jetty.ee11.websocket.jakarta.common.messages.DecodedBinaryStreamMessageSink; import org.eclipse.jetty.ee11.websocket.jakarta.common.messages.DecodedTextMessageSink; import org.eclipse.jetty.ee11.websocket.jakarta.common.messages.DecodedTextStreamMessageSink; +import org.eclipse.jetty.util.Blocker; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.thread.AutoLock; @@ -42,6 +43,7 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.OpCode; +import org.eclipse.jetty.websocket.core.exception.CloseException; import org.eclipse.jetty.websocket.core.exception.ProtocolException; import org.eclipse.jetty.websocket.core.exception.WebSocketException; import org.eclipse.jetty.websocket.core.messages.MessageSink; @@ -139,6 +141,8 @@ public void onOpen(CoreSession coreSession, Callback callback) closeHandle = InvokerUtils.bindTo(closeHandle, session); errorHandle = InvokerUtils.bindTo(errorHandle, session); pongHandle = InvokerUtils.bindTo(pongHandle, session); + if (pongHandle != null) + pongHandle = JakartaWebSocketFrameHandlerFactory.wrapNonVoidReturnType(pongHandle, session); JakartaWebSocketMessageMetadata actualTextMetadata = JakartaWebSocketMessageMetadata.copyOf(textMetadata); if (actualTextMetadata != null) @@ -161,10 +165,10 @@ public void onOpen(CoreSession coreSession, Callback callback) if (actualBinaryMetadata.isMaxMessageSizeSet()) session.setMaxBinaryMessageBufferSize(actualBinaryMetadata.getMaxMessageSize()); - MethodHolder methodHandle = actualBinaryMetadata.getMethodHolder(); - methodHandle = InvokerUtils.bindTo(methodHandle, endpointInstance, endpointConfig, session); - methodHandle = JakartaWebSocketFrameHandlerFactory.wrapNonVoidReturnType(methodHandle, session); - actualBinaryMetadata.setMethodHolder(methodHandle); + MethodHolder methodHolder = actualBinaryMetadata.getMethodHolder(); + methodHolder = InvokerUtils.bindTo(methodHolder, endpointInstance, endpointConfig, session); + methodHolder = JakartaWebSocketFrameHandlerFactory.wrapNonVoidReturnType(methodHolder, session); + actualBinaryMetadata.setMethodHolder(methodHolder); binarySink = JakartaWebSocketFrameHandlerFactory.createMessageSink(session, actualBinaryMetadata); binaryMetadata = actualBinaryMetadata; @@ -228,9 +232,51 @@ public Map getUserProperties() return wrappedConfig; } + public void handleError(Throwable error) + { + try (Blocker.Callback callback = Blocker.callback()) + { + onError(error, callback); + callback.block(); + } + catch (Throwable t) + { + t.addSuppressed(error); + CloseStatus closeStatus = new CloseStatus(CloseStatus.SERVER_ERROR, t); + getSession().getCoreSession().close(closeStatus, Callback.NOOP); + } + } + + public void handleError(Throwable error, Callback callback) + { + Throwable unwrappedError = error; + if (unwrappedError instanceof WebSocketException webSocketException && webSocketException.getCause() != null) + unwrappedError = webSocketException.getCause(); + onError(unwrappedError, callback); + if (error instanceof CloseException closeException) + { + CloseStatus closeStatus = new CloseStatus(closeException.getStatusCode(), closeException); + getSession().getCoreSession().close(closeStatus, Callback.NOOP); + } + coreSession.demand(); + } + @Override public void onFrame(Frame frame, Callback callback) { + Callback frameCallback = callback; + callback = Callback.from(frameCallback::succeeded, x -> + { + // If it is a recoverable error, we can continue processing frames. + if (session.isOpen()) + { + handleError(x, frameCallback); + return; + } + + frameCallback.failed(x); + }); + switch (frame.getOpCode()) { case OpCode.TEXT: diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerFactory.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerFactory.java index 966fff0ba37a..70c7aac72dd6 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerFactory.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerFactory.java @@ -20,14 +20,17 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import jakarta.websocket.CloseReason; import jakarta.websocket.Decoder; +import jakarta.websocket.DeploymentException; import jakarta.websocket.Endpoint; import jakarta.websocket.EndpointConfig; import jakarta.websocket.OnClose; @@ -72,6 +75,11 @@ public abstract class JakartaWebSocketFrameHandlerFactory } } + static InvokerUtils.Arg[] getArgsFor(Type objectType) + { + return getArgsFor(ReflectUtils.getClassFromType(objectType)); + } + static InvokerUtils.Arg[] getArgsFor(Class objectType) { return new InvokerUtils.Arg[]{new InvokerUtils.Arg(Session.class), new InvokerUtils.Arg(objectType).required()}; @@ -111,11 +119,11 @@ public JakartaWebSocketFrameHandlerFactory(JakartaWebSocketContainer container, this.paramIdentifier = paramIdentifier == null ? InvokerUtils.PARAM_IDENTITY : paramIdentifier; } - public abstract JakartaWebSocketFrameHandlerMetadata getMetadata(Class endpointClass, EndpointConfig endpointConfig); + public abstract JakartaWebSocketFrameHandlerMetadata getMetadata(Class endpointClass, EndpointConfig endpointConfig) throws DeploymentException; public abstract EndpointConfig newDefaultEndpointConfig(Class endpointClass); - public JakartaWebSocketFrameHandler newJakartaWebSocketFrameHandler(Object endpointInstance, UpgradeRequest upgradeRequest) + public JakartaWebSocketFrameHandler newJakartaWebSocketFrameHandler(Object endpointInstance, UpgradeRequest upgradeRequest) throws DeploymentException { Object endpoint; EndpointConfig config; @@ -188,7 +196,15 @@ public static MessageSink createMessageSink(JakartaWebSocketSession session, Jak try { MethodHandles.Lookup lookup = getServerMethodHandleLookup(); - if (AbstractDecodedMessageSink.class.isAssignableFrom(msgMetadata.getSinkClass())) + if (AbstractDecodedMessageSink.Stream.class.isAssignableFrom(msgMetadata.getSinkClass())) + { + MethodHandle ctorHandle = lookup.findConstructor(msgMetadata.getSinkClass(), + MethodType.methodType(void.class, CoreSession.class, MethodHolder.class, List.class, Consumer.class)); + List registeredDecoders = msgMetadata.getRegisteredDecoders(); + Consumer onError = session.getFrameHandler()::handleError; + return (MessageSink)ctorHandle.invoke(session.getCoreSession(), msgMetadata.getMethodHolder(), registeredDecoders, onError); + } + else if (AbstractDecodedMessageSink.class.isAssignableFrom(msgMetadata.getSinkClass())) { MethodHandle ctorHandle = lookup.findConstructor(msgMetadata.getSinkClass(), MethodType.methodType(void.class, CoreSession.class, MethodHolder.class, List.class)); @@ -220,17 +236,17 @@ public static MessageSink createMessageSink(JakartaWebSocketSession session, Jak } } - public static MethodHolder wrapNonVoidReturnType(MethodHolder holder, JakartaWebSocketSession session) + public static MethodHolder wrapNonVoidReturnType(MethodHolder handle, JakartaWebSocketSession session) { - if (holder == null) + if (handle == null) return null; - if (holder.returnType() == Void.TYPE) - return holder; + if (handle.returnType() == Void.TYPE) + return handle; return args -> { - session.filterReturnType(holder.invoke(args)); + session.filterReturnType(handle.invoke(args)); return null; }; } @@ -267,84 +283,94 @@ protected JakartaWebSocketFrameHandlerMetadata createEndpointMetadata(EndpointCo return metadata; } - protected JakartaWebSocketFrameHandlerMetadata discoverJakartaFrameHandlerMetadata(Class endpointClass, JakartaWebSocketFrameHandlerMetadata metadata) + protected JakartaWebSocketFrameHandlerMetadata discoverJakartaFrameHandlerMetadata(Class endpointClass, JakartaWebSocketFrameHandlerMetadata metadata) throws DeploymentException { 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) + try { - 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); - } + // 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); + } - // 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); - } + // 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); + } - // OnMessage [0..2] - Method[] onMessages = ReflectUtils.findAnnotatedMethods(endpointClass, OnMessage.class); - if (onMessages != null && onMessages.length > 0) - { - for (Method onMsg : onMessages) + // OnError [0..1] + onmethod = ReflectUtils.findAnnotatedMethod(endpointClass, OnError.class); + if (onmethod != null) { - assertSignatureValid(endpointClass, onMsg, OnMessage.class); - OnMessage onMessageAnno = onMsg.getAnnotation(OnMessage.class); + 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); + } - long annotationMaxMessageSize = onMessageAnno.maxMessageSize(); - if (annotationMaxMessageSize > Integer.MAX_VALUE) + // OnMessage [0..2] + Method[] onMessages = ReflectUtils.findAnnotatedMethods(endpointClass, OnMessage.class); + if (onMessages != null && onMessages.length > 0) + { + for (Method onMsg : onMessages) { - throw new InvalidWebSocketException(String.format("Value too large: %s#%s - @OnMessage.maxMessageSize=%,d > Integer.MAX_VALUE", + 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. - JakartaWebSocketMessageMetadata msgMetadata = new JakartaWebSocketMessageMetadata(); - if (annotationMaxMessageSize != -1) - msgMetadata.setMaxMessageSize((int)annotationMaxMessageSize); + // Create MessageMetadata and set annotated maxMessageSize if it is not the default value. + JakartaWebSocketMessageMetadata msgMetadata = new JakartaWebSocketMessageMetadata(); + if (annotationMaxMessageSize != -1) + msgMetadata.setMaxMessageSize((int)annotationMaxMessageSize); - // Function to search for matching MethodHandle for the endpointClass given a signature. - Function getMethodHandle = (signature) -> - InvokerUtils.optionalMutatedInvoker(lookup, endpointClass, onMsg, paramIdentifier, metadata.getNamedTemplateVariables(), signature); + // Function to search for matching MethodHandle for the endpointClass given a signature. + Function 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; + // 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; + // 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); + throw new InvalidSignatureException("Unable to match @OnMessage " + onMsg); + } } } + catch (DeploymentException e) + { + throw e; + } + catch (Throwable t) + { + throw new DeploymentException("Failed to deploy endpoint", t); + } return metadata; } @@ -408,17 +434,18 @@ private boolean matchDecoders(Method onMsg, JakartaWebSocketFrameHandlerMetadata List decoders = new ArrayList<>(); Class interfaceType = firstDecoder.interfaceType; metadata.getAvailableDecoders().stream().filter(decoder -> - decoder.interfaceType.equals(interfaceType) && (getMethodHandle.apply(getArgsFor(decoder.objectType)) != null)) + decoder.interfaceType.equals(interfaceType) && (getMethodHandle.apply(getArgsFor(decoder.objectType)) != null)) .forEach(decoders::add); msgMetadata.setRegisteredDecoders(decoders); // Get the general methodHandle which applies to all the decoders in the list. - Class objectType = firstDecoder.objectType; + Type objectType = firstDecoder.objectType; for (RegisteredDecoder decoder : decoders) { - if (decoder.objectType.isAssignableFrom(objectType)) + if (ReflectUtils.isAssignableFrom(objectType, decoder.objectType)) objectType = decoder.objectType; } + MethodHandle methodHandle = getMethodHandle.apply(getArgsFor(objectType)); msgMetadata.setMethodHolder(MethodHolder.from(methodHandle)); @@ -447,7 +474,7 @@ else if (interfaceType.equals(Decoder.BinaryStream.class)) return true; } - private void assertSignatureValid(Class endpointClass, Method method, Class annotationClass) + private void assertSignatureValid(Class endpointClass, Method method, Class annotationClass) throws DeploymentException { // Test modifiers int mods = method.getModifiers(); @@ -457,7 +484,7 @@ private void assertSignatureValid(Class endpointClass, Method method, Class endpointClass, Method method, Class endpointClass, Method method, Class Frame.MAX_CONTROL_PAYLOAD) + throw new IllegalArgumentException("Pong payload is too large"); + FutureCallback b = new FutureCallback(); sendFrame(new Frame(OpCode.PING).setPayload(data), b, batch); b.block(); @@ -233,6 +227,9 @@ public void sendPong(ByteBuffer data) throws IOException, IllegalArgumentExcepti if (LOG.isDebugEnabled()) LOG.debug("sendPong({})", BufferUtil.toDetailString(data)); + if (BufferUtil.remaining(data) > Frame.MAX_CONTROL_PAYLOAD) + throw new IllegalArgumentException("Pong payload is too large"); + FutureCallback b = new FutureCallback(); sendFrame(new Frame(OpCode.PONG).setPayload(data), b, batch); b.block(); diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketSession.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketSession.java index a680f4115157..13992dd56b9f 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketSession.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketSession.java @@ -36,8 +36,10 @@ import org.eclipse.jetty.ee11.websocket.jakarta.common.decoders.AvailableDecoders; import org.eclipse.jetty.ee11.websocket.jakarta.common.encoders.AvailableEncoders; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.core.CloseStatus; import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.ExtensionConfig; +import org.eclipse.jetty.websocket.core.exception.CloseException; import org.eclipse.jetty.websocket.core.util.ReflectUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -188,8 +190,12 @@ public void close(CloseReason closeReason) { try { - coreSession.close(closeReason.getCloseCode().getCode(), closeReason.getReasonPhrase(), Callback.NOOP); - } + int closeCode = closeReason.getCloseCode().getCode(); + coreSession.close(closeCode, closeReason.getReasonPhrase(), Callback.from(() -> + { + if (!CloseStatus.isOrdinary(closeCode)) + frameHandler.handleError(new CloseException(closeCode, "Abnormal Close")); + })); } catch (Throwable t) { LOG.trace("IGNORED", t); @@ -540,7 +546,7 @@ public Map getUserProperties() @Override public boolean isOpen() { - return coreSession.isOutputOpen(); + return coreSession.isOutputOpen() || coreSession.isInputOpen(); } /** diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/AvailableDecoders.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/AvailableDecoders.java index 06ef53e6bc56..11143222ffec 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/AvailableDecoders.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/AvailableDecoders.java @@ -16,6 +16,7 @@ import java.io.Closeable; import java.io.InputStream; import java.io.Reader; +import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Iterator; @@ -129,7 +130,7 @@ private void registerAll(List> decoders) private void add(Class decoder, Class interfaceClass) { - Class objectType = ReflectUtils.findGenericClassFor(decoder, interfaceClass); + Type objectType = ReflectUtils.findGenericTypeFor(decoder, interfaceClass); if (objectType == null) { String err = "Unknown Decoder Object type declared for interface " + @@ -137,25 +138,6 @@ private void add(Class decoder, Class inte throw new InvalidWebSocketException(err); } - // Validate the decoder to be added against the existing registered decoders. - for (RegisteredDecoder registered : registeredDecoders) - { - if (!registered.primitive && objectType.equals(registered.objectType)) - { - // Streaming decoders can only have one decoder per object type. - if (interfaceClass.equals(Decoder.TextStream.class) || interfaceClass.equals(Decoder.BinaryStream.class)) - throw new InvalidWebSocketException("Multiple decoders for objectType" + objectType); - - // If we have the same objectType, then the interfaceTypes must be the same to form a decoder list. - if (!registered.interfaceType.equals(interfaceClass)) - throw new InvalidWebSocketException("Multiple decoders with different interface types for objectType " + objectType); - } - - // If this decoder is already registered for this interface type we can skip adding a duplicate. - if (registered.decoder.equals(decoder) && registered.interfaceType.equals(interfaceClass)) - return; - } - registeredDecoders.add(new RegisteredDecoder(decoder, interfaceClass, objectType, config, components)); } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/CharacterDecoder.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/CharacterDecoder.java index e9165ba46790..9a9e1f3ccbdd 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/CharacterDecoder.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/CharacterDecoder.java @@ -15,6 +15,7 @@ import jakarta.websocket.DecodeException; import jakarta.websocket.Decoder; +import org.eclipse.jetty.util.StringUtil; /** * Default implementation of the {@link jakarta.websocket.Decoder.Text} Message to {@link Character} decoder @@ -32,15 +33,6 @@ public Character decode(String s) throws DecodeException @Override public boolean willDecode(String s) { - if (s == null) - { - return false; - } - if (s.length() == 1) - { - return true; - } - // can only parse 1 character - return false; + return !StringUtil.isEmpty(s); } } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/RegisteredDecoder.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/RegisteredDecoder.java index 0a8449d77eb3..c8d31c7bb081 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/RegisteredDecoder.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/decoders/RegisteredDecoder.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.ee11.websocket.jakarta.common.decoders; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; import jakarta.websocket.Decoder; import jakarta.websocket.EndpointConfig; @@ -22,6 +23,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.eclipse.jetty.websocket.core.util.ReflectUtils.isAssignableFrom; + public class RegisteredDecoder { private static final Logger LOG = LoggerFactory.getLogger(RegisteredDecoder.class); @@ -30,19 +33,19 @@ public class RegisteredDecoder public final Class decoder; // The jakarta.websocket.Decoder.* type (eg: Decoder.Binary, Decoder.BinaryStream, Decoder.Text, Decoder.TextStream) public final Class interfaceType; - public final Class objectType; + public final Type objectType; public final boolean primitive; public final EndpointConfig config; private final WebSocketComponents components; private Decoder instance; - public RegisteredDecoder(Class decoder, Class interfaceType, Class objectType, EndpointConfig endpointConfig, WebSocketComponents components) + public RegisteredDecoder(Class decoder, Class interfaceType, Type objectType, EndpointConfig endpointConfig, WebSocketComponents components) { this(decoder, interfaceType, objectType, endpointConfig, components, false); } - public RegisteredDecoder(Class decoder, Class interfaceType, Class objectType, EndpointConfig endpointConfig, WebSocketComponents components, boolean primitive) + public RegisteredDecoder(Class decoder, Class interfaceType, Type objectType, EndpointConfig endpointConfig, WebSocketComponents components, boolean primitive) { this.decoder = decoder; this.interfaceType = interfaceType; @@ -57,11 +60,12 @@ public boolean implementsInterface(Class type) return interfaceType.isAssignableFrom(type); } - public boolean isType(Class type) + public boolean isType(Type type) { - return objectType.isAssignableFrom(type); + return isAssignableFrom(objectType, type); } + @SuppressWarnings("unchecked") public T getInstance() { if (instance == null) @@ -105,7 +109,7 @@ public String toString() str.append(RegisteredDecoder.class.getSimpleName()); str.append('[').append(decoder.getName()); str.append(',').append(interfaceType.getName()); - str.append(',').append(objectType.getName()); + str.append(',').append(objectType.getTypeName()); if (primitive) { str.append(",PRIMITIVE"); diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/encoders/AvailableEncoders.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/encoders/AvailableEncoders.java index 73d6e35b4c9e..b155d151ad0d 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/encoders/AvailableEncoders.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/encoders/AvailableEncoders.java @@ -15,6 +15,7 @@ import java.io.Closeable; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.List; @@ -149,7 +150,7 @@ public void registerAll(List> encoders) private void add(Class encoder, Class interfaceClass) { - Class objectType = ReflectUtils.findGenericClassFor(encoder, interfaceClass); + Type objectType = ReflectUtils.findGenericTypeFor(encoder, interfaceClass); if (objectType == null) { StringBuilder err = new StringBuilder(); @@ -160,33 +161,7 @@ private void add(Class encoder, Class inte throw new InvalidWebSocketException(err.toString()); } - try - { - RegisteredEncoder conflicts = registeredEncoders.stream() - .filter(registered -> registered.isType(objectType)) - .filter(registered -> !registered.primitive) - .findFirst() - .get(); - - if (conflicts.encoder.equals(encoder) && conflicts.implementsInterface(interfaceClass)) - { - // Same encoder as what is there already, don't bother adding it again. - return; - } - - StringBuilder err = new StringBuilder(); - err.append("Duplicate Encoder Object type "); - err.append(objectType.getName()); - err.append(" in "); - err.append(encoder.getName()); - err.append(", previously declared in "); - err.append(conflicts.encoder.getName()); - throw new InvalidWebSocketException(err.toString()); - } - catch (NoSuchElementException e) - { - registeredEncoders.addFirst(new RegisteredEncoder(encoder, interfaceClass, objectType)); - } + registeredEncoders.addFirst(new RegisteredEncoder(encoder, interfaceClass, objectType)); } public List supporting(Class interfaceType) diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/encoders/RegisteredEncoder.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/encoders/RegisteredEncoder.java index e7fa468ae0b0..166c80e589a1 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/encoders/RegisteredEncoder.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/encoders/RegisteredEncoder.java @@ -13,26 +13,30 @@ package org.eclipse.jetty.ee11.websocket.jakarta.common.encoders; +import java.lang.reflect.Type; + import jakarta.websocket.Encoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.eclipse.jetty.websocket.core.util.ReflectUtils.isAssignableFrom; + public class RegisteredEncoder { private static final Logger LOG = LoggerFactory.getLogger(RegisteredEncoder.class); public final Class encoder; public final Class interfaceType; - public final Class objectType; + public final Type objectType; public final boolean primitive; public Encoder instance; - public RegisteredEncoder(Class encoder, Class interfaceType, Class objectType) + public RegisteredEncoder(Class encoder, Class interfaceType, Type objectType) { this(encoder, interfaceType, objectType, false); } - public RegisteredEncoder(Class encoder, Class interfaceType, Class objectType, boolean primitive) + public RegisteredEncoder(Class encoder, Class interfaceType, Type objectType, boolean primitive) { this.encoder = encoder; this.interfaceType = interfaceType; @@ -45,9 +49,9 @@ public boolean implementsInterface(Class type) return interfaceType.isAssignableFrom(type); } - public boolean isType(Class type) + public boolean isType(Type type) { - return objectType.isAssignableFrom(type); + return isAssignableFrom(objectType, type); } public void destroyInstance() @@ -74,7 +78,7 @@ public String toString() str.append(RegisteredEncoder.class.getSimpleName()); str.append('[').append(encoder.getName()); str.append(',').append(interfaceType.getName()); - str.append(',').append(objectType.getName()); + str.append(',').append(objectType.getTypeName()); if (primitive) { str.append(",PRIMITIVE"); diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/AbstractDecodedMessageSink.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/AbstractDecodedMessageSink.java index b30fdab99ac2..84cfd4f1962e 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/AbstractDecodedMessageSink.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/AbstractDecodedMessageSink.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.ee11.websocket.jakarta.common.messages; import java.util.List; +import java.util.function.Consumer; import java.util.stream.Collectors; import jakarta.websocket.CloseReason; @@ -34,10 +35,17 @@ public abstract class AbstractDecodedMessageSink implements MessageSink private final MethodHolder _methodHolder; private final MessageSink _messageSink; + protected final Consumer _onError; public AbstractDecodedMessageSink(CoreSession coreSession, MethodHolder methodHolder) + { + this(coreSession, methodHolder, null); + } + + public AbstractDecodedMessageSink(CoreSession coreSession, MethodHolder methodHolder, Consumer onError) { _methodHolder = methodHolder; + _onError = onError; try { @@ -107,9 +115,12 @@ public abstract static class Stream extends AbstractDecodedMe public Stream(CoreSession coreSession, MethodHolder methodHolder, List decoders) { - super(coreSession, methodHolder); - if (decoders.size() != 1) - throw new IllegalArgumentException("Require exactly one decoder for " + this.getClass()); + this(coreSession, methodHolder, decoders, null); + } + + public Stream(CoreSession coreSession, MethodHolder methodHolder, List decoders, Consumer onError) + { + super(coreSession, methodHolder, onError); _decoder = decoders.get(0).getInstance(); } } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/DecodedBinaryStreamMessageSink.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/DecodedBinaryStreamMessageSink.java index c3442e3b52d4..dca5d6cbf37b 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/DecodedBinaryStreamMessageSink.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/DecodedBinaryStreamMessageSink.java @@ -17,6 +17,7 @@ import java.io.InputStream; import java.lang.invoke.WrongMethodTypeException; import java.util.List; +import java.util.function.Consumer; import jakarta.websocket.CloseReason; import jakarta.websocket.DecodeException; @@ -32,7 +33,12 @@ public class DecodedBinaryStreamMessageSink extends AbstractDecodedMessageSin { public DecodedBinaryStreamMessageSink(CoreSession session, MethodHolder methodHolder, List decoders) { - super(session, methodHolder, decoders); + this(session, methodHolder, decoders, null); + } + + public DecodedBinaryStreamMessageSink(CoreSession session, MethodHolder methodHolder, List decoders, Consumer onError) + { + super(session, methodHolder, decoders, onError); } @Override @@ -46,7 +52,7 @@ MessageSink newMessageSink(CoreSession coreSession) return null; }; - return new InputStreamMessageSink(coreSession, methodHolder, true); + return new InputStreamMessageSink(coreSession, methodHolder, true, _onError); } public void onStreamStart(InputStream stream) diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/DecodedTextStreamMessageSink.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/DecodedTextStreamMessageSink.java index 04d64065f74a..1c46db58e06a 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/DecodedTextStreamMessageSink.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/DecodedTextStreamMessageSink.java @@ -17,6 +17,7 @@ import java.io.Reader; import java.lang.invoke.WrongMethodTypeException; import java.util.List; +import java.util.function.Consumer; import jakarta.websocket.CloseReason; import jakarta.websocket.DecodeException; @@ -32,11 +33,16 @@ public class DecodedTextStreamMessageSink extends AbstractDecodedMessageSink. { public DecodedTextStreamMessageSink(CoreSession session, MethodHolder methodHolder, List decoders) { - super(session, methodHolder, decoders); + this(session, methodHolder, decoders, null); + } + + public DecodedTextStreamMessageSink(CoreSession session, MethodHolder methodHolder, List decoders, Consumer onError) + { + super(session, methodHolder, decoders, onError); } @Override - MessageSink newMessageSink(CoreSession coreSession) throws Exception + MessageSink newMessageSink(CoreSession coreSession) { MethodHolder methodHolder = args -> { @@ -46,7 +52,7 @@ MessageSink newMessageSink(CoreSession coreSession) throws Exception return null; }; - return new ReaderMessageSink(coreSession, methodHolder, true); + return new ReaderMessageSink(coreSession, methodHolder, true, _onError); } public void onStreamStart(Reader reader) diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/AbstractJakartaWebSocketFrameHandlerTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/AbstractJakartaWebSocketFrameHandlerTest.java index a4e86bbc970d..f9dced815660 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/AbstractJakartaWebSocketFrameHandlerTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/AbstractJakartaWebSocketFrameHandlerTest.java @@ -17,6 +17,7 @@ import java.util.Map; import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.DeploymentException; import jakarta.websocket.EndpointConfig; import org.eclipse.jetty.ee11.websocket.jakarta.common.decoders.AvailableDecoders; import org.eclipse.jetty.ee11.websocket.jakarta.common.encoders.AvailableEncoders; @@ -68,7 +69,7 @@ public AbstractJakartaWebSocketFrameHandlerTest() uriParams = new HashMap<>(); } - protected JakartaWebSocketFrameHandler newJakartaFrameHandler(Object websocket) + protected JakartaWebSocketFrameHandler newJakartaFrameHandler(Object websocket) throws DeploymentException { JakartaWebSocketFrameHandlerFactory factory = container.getFrameHandlerFactory(); ConfiguredEndpoint endpoint = new ConfiguredEndpoint(websocket, endpointConfig); diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/DummyFrameHandlerFactory.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/DummyFrameHandlerFactory.java index c73773169430..36c304da9096 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/DummyFrameHandlerFactory.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/DummyFrameHandlerFactory.java @@ -15,6 +15,7 @@ import jakarta.websocket.ClientEndpoint; import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.DeploymentException; import jakarta.websocket.EndpointConfig; import org.eclipse.jetty.websocket.core.util.InvokerUtils; @@ -32,7 +33,7 @@ public EndpointConfig newDefaultEndpointConfig(Class endpointClass) } @Override - public JakartaWebSocketFrameHandlerMetadata getMetadata(Class endpointClass, EndpointConfig endpointConfig) + public JakartaWebSocketFrameHandlerMetadata getMetadata(Class endpointClass, EndpointConfig endpointConfig) throws DeploymentException { if (jakarta.websocket.Endpoint.class.isAssignableFrom(endpointClass)) { diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerBadSignaturesTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerBadSignaturesTest.java index 2b3fc81a0107..17646fb99f92 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerBadSignaturesTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerBadSignaturesTest.java @@ -15,6 +15,7 @@ import jakarta.websocket.ClientEndpoint; import jakarta.websocket.CloseReason; +import jakarta.websocket.DeploymentException; import jakarta.websocket.OnClose; import jakarta.websocket.OnError; import jakarta.websocket.OnOpen; @@ -24,14 +25,16 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; public class JakartaWebSocketFrameHandlerBadSignaturesTest extends AbstractJakartaWebSocketFrameHandlerTest { private void assertBadSocket(Object socket, String expectedString) throws Exception { - Exception e = assertThrows(InvalidSignatureException.class, () -> newJakartaFrameHandler(socket)); - assertThat(e.getMessage(), containsString(expectedString)); + Exception e = assertThrows(DeploymentException.class, () -> newJakartaFrameHandler(socket)); + assertThat(e.getCause(), instanceOf(InvalidSignatureException.class)); + assertThat(e.getCause().getMessage(), containsString(expectedString)); } @SuppressWarnings("UnusedParameters") diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerOnMessageBinaryTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerOnMessageBinaryTest.java index 2d6e73c9ade5..a26559826286 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerOnMessageBinaryTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerOnMessageBinaryTest.java @@ -18,6 +18,7 @@ import java.util.concurrent.TimeUnit; import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.DeploymentException; import jakarta.websocket.OnMessage; import jakarta.websocket.Session; import org.eclipse.jetty.ee11.websocket.jakarta.common.sockets.TrackingSocket; @@ -26,12 +27,14 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException; +import org.eclipse.jetty.websocket.core.exception.InvalidWebSocketException; import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -68,9 +71,10 @@ public void onMessage() @Test public void testInvokeMessage() throws Exception { - assertThrows(InvalidSignatureException.class, () -> + Throwable t = assertThrows(DeploymentException.class, () -> assertOnMessageInvocation(new MessageSocket(), containsString("onMessage()")) ); + assertThat(t.getCause(), instanceOf(InvalidWebSocketException.class)); } @ClientEndpoint @@ -103,13 +107,14 @@ public void onMessage(Session session) @Test public void testInvokeMessageSession() throws Exception { - assertThrows(InvalidSignatureException.class, () -> + Throwable t = assertThrows(DeploymentException.class, () -> assertOnMessageInvocation(new MessageSessionSocket(), allOf( containsString("onMessage(JakartaWebSocketSession@"), containsString(MessageSessionSocket.class.getName()) )) ); + assertThat(t.getCause(), instanceOf(InvalidSignatureException.class)); } @ClientEndpoint diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerOnMessageTextTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerOnMessageTextTest.java index e30f55018a83..4df5dd806f7c 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerOnMessageTextTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/JakartaWebSocketFrameHandlerOnMessageTextTest.java @@ -18,6 +18,7 @@ import java.util.concurrent.TimeUnit; import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.DeploymentException; import jakarta.websocket.OnMessage; import jakarta.websocket.Session; import org.eclipse.jetty.ee11.websocket.jakarta.common.sockets.TrackingSocket; @@ -32,6 +33,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; public class JakartaWebSocketFrameHandlerOnMessageTextTest extends AbstractJakartaWebSocketFrameHandlerTest @@ -72,8 +74,9 @@ public void onMessage() public void testAmbiguousEmptyMessage() throws Exception { MessageSocket socket = new MessageSocket(); - Exception e = assertThrows(InvalidSignatureException.class, () -> onText(socket, "Hello World")); - assertThat(e.getMessage(), containsString("@OnMessage public void " + MessageSocket.class.getName() + "#onMessage")); + Exception e = assertThrows(DeploymentException.class, () -> onText(socket, "Hello World")); + assertThat(e.getCause(), instanceOf(InvalidSignatureException.class)); + assertThat(e.getCause().getMessage(), containsString("@OnMessage public void " + MessageSocket.class.getName() + ".onMessage")); } @ClientEndpoint @@ -109,8 +112,9 @@ public void onMessage(Session session) public void testAmbiguousMessageSession() throws Exception { MessageSessionSocket socket = new MessageSessionSocket(); - Exception e = assertThrows(InvalidSignatureException.class, () -> onText(socket, "Hello World")); - assertThat(e.getMessage(), containsString("@OnMessage public void " + MessageSessionSocket.class.getName() + "#onMessage")); + Exception e = assertThrows(DeploymentException.class, () -> onText(socket, "Hello World")); + assertThat(e.getCause(), instanceOf(InvalidSignatureException.class)); + assertThat(e.getCause().getMessage(), containsString("@OnMessage public void " + MessageSessionSocket.class.getName() + ".onMessage")); } @ClientEndpoint diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/AbstractMessageSinkTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/AbstractMessageSinkTest.java index 48228de90728..27779ad297e7 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/AbstractMessageSinkTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/messages/AbstractMessageSinkTest.java @@ -41,7 +41,7 @@ else if (Decoder.BinaryStream.class.isAssignableFrom(clazz)) else throw new IllegalStateException(); - return List.of(new RegisteredDecoder(clazz, interfaceType, objectType, ClientEndpointConfig.Builder.create().build(), components)); + return List.of(new RegisteredDecoder(clazz, interfaceType, objectType, ClientEndpointConfig.Builder.create().build(), components, false)); } public MethodHolder getAcceptHandle(Consumer copy, Class type) diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/util/InvokerUtilsStaticParamsTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/util/InvokerUtilsStaticParamsTest.java index daa865113422..a69a16be5eff 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/util/InvokerUtilsStaticParamsTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-common/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/common/util/InvokerUtilsStaticParamsTest.java @@ -13,22 +13,29 @@ package org.eclipse.jetty.ee11.websocket.jakarta.common.util; +import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnOpen; import jakarta.websocket.Session; +import jakarta.websocket.server.PathParam; import org.eclipse.jetty.ee11.websocket.jakarta.common.JakartaWebSocketFrameHandlerFactory; import org.eclipse.jetty.util.annotation.Name; +import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException; import org.eclipse.jetty.websocket.core.util.InvokerUtils; import org.eclipse.jetty.websocket.core.util.MethodHolder; import org.eclipse.jetty.websocket.core.util.ReflectUtils; import org.junit.jupiter.api.Test; +import static org.eclipse.jetty.ee11.websocket.jakarta.common.JakartaWebSocketFrameHandlerFactory.bindTemplateVariables; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNull; public class InvokerUtilsStaticParamsTest { @@ -37,7 +44,7 @@ public static class Foo { public String onFruit(@Name("fruit") String fruit) { - return String.format("onFruit('%s')", fruit); + return String.format("onFruit('%s')", (fruit == null) ? "null" : fruit); } public String onCount(@Name("count") int count) @@ -55,7 +62,7 @@ public String onColorMessage(Session session, String message, @Name("color") Str return String.format("onColorMessage(%s, '%s', '%s')", color); } } - + private static MethodHandles.Lookup lookup = MethodHandles.lookup(); private MethodHolder getMethodHolder(Method method, String[] namedVariables, InvokerUtils.Arg... args) @@ -84,7 +91,8 @@ public void testOnlyParamString() throws Throwable templateValues.put("fruit", "pear"); // Bind the static values, in same order as declared - methodHolder = JakartaWebSocketFrameHandlerFactory.bindTemplateVariables(methodHolder, namedVariables, templateValues); + methodHolder = bindTemplateVariables(methodHolder, namedVariables, templateValues); + // Assign an instance to call. Foo foo = new Foo(); methodHolder = methodHolder.bindTo(foo); @@ -112,7 +120,7 @@ public void testOnlyParamInt() throws Throwable templateValues.put("count", "2222"); // Bind the static values for the variables, in same order as the variables were declared - methodHolder = JakartaWebSocketFrameHandlerFactory.bindTemplateVariables(methodHolder, namedVariables, templateValues); + methodHolder = bindTemplateVariables(methodHolder, namedVariables, templateValues); // Assign an instance to call. Foo foo = new Foo(); @@ -143,7 +151,7 @@ public void testLabeledParamStringInt() throws Throwable templateValues.put("count", "444"); // Bind the static values for the variables, in same order as the variables were declared - methodHolder = JakartaWebSocketFrameHandlerFactory.bindTemplateVariables(methodHolder, namedVariables, templateValues); + methodHolder = bindTemplateVariables(methodHolder, namedVariables, templateValues); // Assign an instance to call. Foo foo = new Foo(); @@ -153,4 +161,88 @@ public void testLabeledParamStringInt() throws Throwable String result = (String)methodHolder.invoke("cherry"); assertThat("Result", result, is("onLabeledCount('cherry', 444)")); } + + @Test + public void testNonExistentParams() throws Throwable + { + class PathParamEndpoint + { + private String pathParam; + private String nonExistent1; + private String nonExistent2; + private String nonExistent3; + + @OnOpen + public void onOpen(@PathParam("non-existent1") String nonExistent1, + EndpointConfig config, + @PathParam("non-existent2") String nonExistent2, + @PathParam("param1") String pathParam, + Session session, + @PathParam("non-existent3") String nonExistent3 + ) + { + this.pathParam = pathParam; + this.nonExistent1 = nonExistent1; + this.nonExistent2 = nonExistent2; + this.nonExistent3 = nonExistent3; + } + } + + Method method = ReflectUtils.findAnnotatedMethod(PathParamEndpoint.class, OnOpen.class); + + // Declared Variable Names + final String[] namedVariables = new String[]{ + "param1", + "param2", + "param3" + }; + + // Some point later an actual instance is needed, which has static named parameters + Map templateValues = new HashMap<>(); + templateValues.put("param1", "value1"); + templateValues.put("param2", "value2"); + templateValues.put("param3", "value3"); + + // Bind the static values, in same order as declared + final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class); + final InvokerUtils.Arg ENDPOINT_CONFIG = new InvokerUtils.Arg(EndpointConfig.class); + MethodHolder methodHolder = MethodHolder.from(InvokerUtils.mutatedInvoker(lookup, PathParamEndpoint.class, method, new PathParamIdentifier(), namedVariables, SESSION, ENDPOINT_CONFIG)); + + methodHolder = bindTemplateVariables(methodHolder, namedVariables, templateValues); + + // Assign an instance to call. + PathParamEndpoint foo = new PathParamEndpoint(); + methodHolder = methodHolder.bindTo(foo).bindTo(null).bindTo(null); + + // Call method against instance + methodHolder.invoke(); + assertThat(foo.pathParam, is("value1")); + assertNull(foo.nonExistent1); + assertNull(foo.nonExistent2); + assertNull(foo.nonExistent3); + } + + @SuppressWarnings("unused") + public static class PathParamIdentifier implements InvokerUtils.ParamIdentifier + { + @Override + public InvokerUtils.Arg getParamArg(Method method, Class paramType, int idx) + { + Annotation[] annos = method.getParameterAnnotations()[idx]; + if (annos != null || (annos.length > 0)) + { + for (Annotation anno : annos) + { + if (anno.annotationType().equals(PathParam.class)) + { + if (!String.class.isAssignableFrom(paramType)) + throw new InvalidSignatureException("Unsupported PathParam Type: " + paramType); + PathParam pathParam = (PathParam)anno; + return new InvokerUtils.Arg(paramType, pathParam.value()); + } + } + } + return new InvokerUtils.Arg(paramType); + } + } } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/JakartaWebSocketServerContainer.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/JakartaWebSocketServerContainer.java index ac236eb34cf7..0f7db41763a3 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/JakartaWebSocketServerContainer.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/JakartaWebSocketServerContainer.java @@ -138,6 +138,7 @@ public String toString() private final JakartaWebSocketServerFrameHandlerFactory frameHandlerFactory; private List> deferredEndpointClasses; private List deferredEndpointConfigs; + private boolean failed = false; /** * Main entry point for {@link JakartaWebSocketServletContainerInitializer}. @@ -202,64 +203,88 @@ private void validateEndpointConfig(ServerEndpointConfig config) throws Deployme @Override public void addEndpoint(Class endpointClass) throws DeploymentException { - if (endpointClass == null) + try { - throw new DeploymentException("Unable to deploy null endpoint class"); - } + if (failed) + throw new DeploymentException("Previous endpoint failed to deploy"); - if (isStarted() || isStarting()) - { - ServerEndpoint anno = endpointClass.getAnnotation(ServerEndpoint.class); - if (anno == null) + if (endpointClass == null) { - throw new DeploymentException(String.format("Class must be @%s annotated: %s", ServerEndpoint.class.getName(), endpointClass.getName())); + throw new DeploymentException("Unable to deploy null endpoint class"); } - if (LOG.isDebugEnabled()) + if (isStarted() || isStarting()) { - LOG.debug("addEndpoint({})", endpointClass); + ServerEndpoint anno = endpointClass.getAnnotation(ServerEndpoint.class); + if (anno == null) + { + throw new DeploymentException(String.format("Class must be @%s annotated: %s", ServerEndpoint.class.getName(), endpointClass.getName())); + } + + if (LOG.isDebugEnabled()) + { + LOG.debug("addEndpoint({})", endpointClass); + } + + ServerEndpointConfig config = new AnnotatedServerEndpointConfig(this, endpointClass, anno); + validateEndpointConfig(config); + addEndpointMapping(config); + } + else + { + if (deferredEndpointClasses == null) + deferredEndpointClasses = new ArrayList<>(); + deferredEndpointClasses.add(endpointClass); } - - ServerEndpointConfig config = new AnnotatedServerEndpointConfig(this, endpointClass, anno); - validateEndpointConfig(config); - addEndpointMapping(config); } - else + catch (DeploymentException e) { - if (deferredEndpointClasses == null) - deferredEndpointClasses = new ArrayList<>(); - deferredEndpointClasses.add(endpointClass); + webSocketMappings.clear(); + failed = true; + throw e; } } @Override public void addEndpoint(ServerEndpointConfig providedConfig) throws DeploymentException { - if (providedConfig == null) - throw new DeploymentException("ServerEndpointConfig is null"); - - if (isStarted() || isStarting()) + try { - // Decorate the provided Configurator. - components.getObjectFactory().decorate(providedConfig.getConfigurator()); + if (failed) + throw new DeploymentException("Previous endpoint failed to deploy"); - // If we have annotations merge the annotated ServerEndpointConfig with the provided one. - Class endpointClass = providedConfig.getEndpointClass(); - ServerEndpoint anno = endpointClass.getAnnotation(ServerEndpoint.class); - ServerEndpointConfig config = (anno == null) ? providedConfig - : new AnnotatedServerEndpointConfig(this, endpointClass, anno, providedConfig); + if (providedConfig == null) + throw new DeploymentException("ServerEndpointConfig is null"); - if (LOG.isDebugEnabled()) - LOG.debug("addEndpoint({}) path={} endpoint={}", config, config.getPath(), endpointClass); + if (isStarted() || isStarting()) + { + // Decorate the provided Configurator. + components.getObjectFactory().decorate(providedConfig.getConfigurator()); + + // If we have annotations merge the annotated ServerEndpointConfig with the provided one. + Class endpointClass = providedConfig.getEndpointClass(); + ServerEndpoint anno = endpointClass.getAnnotation(ServerEndpoint.class); + ServerEndpointConfig config = (anno == null) ? providedConfig + : new AnnotatedServerEndpointConfig(this, endpointClass, anno, providedConfig); + + if (LOG.isDebugEnabled()) + LOG.debug("addEndpoint({}) path={} endpoint={}", config, config.getPath(), endpointClass); - validateEndpointConfig(config); - addEndpointMapping(config); + validateEndpointConfig(config); + addEndpointMapping(config); + } + else + { + if (deferredEndpointConfigs == null) + deferredEndpointConfigs = new ArrayList<>(); + deferredEndpointConfigs.add(providedConfig); + } } - else + catch (DeploymentException e) { - if (deferredEndpointConfigs == null) - deferredEndpointConfigs = new ArrayList<>(); - deferredEndpointConfigs.add(providedConfig); + webSocketMappings.clear(); + failed = true; + throw e; } } @@ -270,12 +295,18 @@ private void addEndpointMapping(ServerEndpointConfig config) throws DeploymentEx frameHandlerFactory.getMetadata(config.getEndpointClass(), config); JakartaWebSocketCreator creator = new JakartaWebSocketCreator(this, config, getExtensionRegistry()); PathSpec pathSpec = new UriTemplatePathSpec(config.getPath()); + if (webSocketMappings.getWebSocketNegotiator(pathSpec) != null) + throw new DeploymentException("Duplicate WebSocket mapping for path: " + config.getPath()); webSocketMappings.addMapping(pathSpec, creator, frameHandlerFactory, defaultCustomizer); } catch (InvalidSignatureException e) { throw new DeploymentException(e.getMessage(), e); } + catch (DeploymentException e) + { + throw e; + } catch (Throwable t) { throw new DeploymentException("Unable to deploy: " + config.getEndpointClass().getName(), t); diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/JakartaWebSocketServerFrameHandlerFactory.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/JakartaWebSocketServerFrameHandlerFactory.java index 6daebc2bc644..f77e388b874c 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/JakartaWebSocketServerFrameHandlerFactory.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/JakartaWebSocketServerFrameHandlerFactory.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.ee11.websocket.jakarta.server; +import jakarta.websocket.DeploymentException; import jakarta.websocket.EndpointConfig; import jakarta.websocket.server.ServerEndpoint; import org.eclipse.jetty.ee11.websocket.jakarta.client.JakartaWebSocketClientFrameHandlerFactory; @@ -22,6 +23,7 @@ import org.eclipse.jetty.ee11.websocket.jakarta.server.internal.PathParamIdentifier; import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec; import org.eclipse.jetty.websocket.core.FrameHandler; +import org.eclipse.jetty.websocket.core.exception.InvalidWebSocketException; import org.eclipse.jetty.websocket.core.server.FrameHandlerFactory; import org.eclipse.jetty.websocket.core.server.ServerUpgradeRequest; import org.eclipse.jetty.websocket.core.server.ServerUpgradeResponse; @@ -34,7 +36,7 @@ public JakartaWebSocketServerFrameHandlerFactory(JakartaWebSocketContainer conta } @Override - public JakartaWebSocketFrameHandlerMetadata getMetadata(Class endpointClass, EndpointConfig endpointConfig) + public JakartaWebSocketFrameHandlerMetadata getMetadata(Class endpointClass, EndpointConfig endpointConfig) throws DeploymentException { if (jakarta.websocket.Endpoint.class.isAssignableFrom(endpointClass)) return createEndpointMetadata(endpointConfig); @@ -52,6 +54,13 @@ public JakartaWebSocketFrameHandlerMetadata getMetadata(Class endpointClass, @Override public FrameHandler newFrameHandler(Object websocketPojo, ServerUpgradeRequest upgradeRequest, ServerUpgradeResponse upgradeResponse) { - return newJakartaWebSocketFrameHandler(websocketPojo, new JakartaServerUpgradeRequest(upgradeRequest)); + try + { + return newJakartaWebSocketFrameHandler(websocketPojo, new JakartaServerUpgradeRequest(upgradeRequest)); + } + catch (DeploymentException e) + { + throw new InvalidWebSocketException(e.getMessage(), e); + } } } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/JakartaWebSocketCreator.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/JakartaWebSocketCreator.java index dfe0fbe8fd15..d23262f40c5e 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/JakartaWebSocketCreator.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/JakartaWebSocketCreator.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.ee11.websocket.jakarta.common.JakartaWebSocketContainer; import org.eclipse.jetty.ee11.websocket.jakarta.common.JakartaWebSocketExtension; import org.eclipse.jetty.ee11.websocket.jakarta.common.ServerEndpointConfigWrapper; +import org.eclipse.jetty.ee11.websocket.jakarta.server.JakartaWebSocketServerContainer; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec; @@ -143,6 +144,7 @@ public Map getUserProperties() // Wrap the config with the path spec information. config = new PathParamServerEndpointConfig(config, pathParams); + request.setAttribute(JakartaWebSocketServerContainer.PATH_PARAM_ATTRIBUTE, pathParams); } else { diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/JsrHandshakeRequest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/JsrHandshakeRequest.java index e889fb8d05b3..90983c951935 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/JsrHandshakeRequest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/JsrHandshakeRequest.java @@ -15,6 +15,8 @@ import java.net.URI; import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -61,13 +63,47 @@ public Map> getParameterMap() { if (parameterMap == null) { - Fields requestParams = Request.extractQueryParameters(delegate); parameterMap = new HashMap<>(); + + // Add query parameters to the parameter map. + Fields requestParams = Request.extractQueryParameters(delegate); for (String name : requestParams.getNames()) { - parameterMap.put(name, requestParams.getValues(name)); + parameterMap.compute(name, (key, values) -> + { + if (values == null) + values = new ArrayList<>(); + values.addAll(requestParams.getValues(name)); + return values; + }); + } + + // Add path parameters to the parameter map. + Map pathParams = getPathParams(); + if (pathParams != null) + { + for (Map.Entry entry : pathParams.entrySet()) + { + parameterMap.compute(entry.getKey(), (key, values) -> + { + if (values == null) + values = new ArrayList<>(); + values.add(entry.getValue()); + return values; + }); + } } + + // Make the lists unmodifiable. + for (Map.Entry> entry : parameterMap.entrySet()) + { + entry.setValue(Collections.unmodifiableList(entry.getValue())); + } + + // The map should be unmodifiable according to the spec. + parameterMap = Collections.unmodifiableMap(parameterMap); } + return parameterMap; } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/PathParamIdentifier.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/PathParamIdentifier.java index 707366ec6e30..7a2a25d10e4e 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/PathParamIdentifier.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-server/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/server/internal/PathParamIdentifier.java @@ -38,7 +38,11 @@ public InvokerUtils.Arg getParamArg(Method method, Class paramType, int idx) { validateType(paramType); PathParam pathParam = (PathParam)anno; - return new InvokerUtils.Arg(paramType, pathParam.value()); + String value = pathParam.value(); + // WebSocket TCK requires us to strip any remaining { and } from the value. + if (value != null && value.startsWith("{") && value.endsWith("}")) + value = value.substring(1, value.length() - 1); + return new InvokerUtils.Arg(paramType, value); } } } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/WSServer.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/WSServer.java index 33d2151bd73f..fd50ef12fa3c 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/WSServer.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/WSServer.java @@ -31,6 +31,7 @@ import org.eclipse.jetty.toolchain.test.JAR; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.resource.ResourceFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,6 +78,7 @@ protected Handler createRootHandler(Server server) public class WebApp { private final WebAppContext context; + private final ResourceFactory resourceFactory; private final Path contextDir; private final Path webInf; private final Path classesDir; @@ -103,6 +105,8 @@ private WebApp(String contextName) context.setBaseResourceAsPath(contextDir); context.setAttribute("org.eclipse.jetty.websocket.jakarta", Boolean.TRUE); context.addConfiguration(new JakartaWebSocketConfiguration()); + + resourceFactory = ResourceFactory.of(context); } public WebAppContext getWebAppContext() @@ -141,8 +145,7 @@ public void copyClass(Class clazz) throws Exception assertThat("Class URL for: " + clazz, classUrl, notNullValue()); Path destFile = classesDir.resolve(endpointPath); FS.ensureDirExists(destFile.getParent()); - File srcFile = new File(classUrl.toURI()); - IO.copy(srcFile.toPath(), destFile); + resourceFactory.newResource(classUrl).copyTo(destFile); } public void copyLib(Class clazz, String jarFileName) throws URISyntaxException, IOException diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/matchers/IsMessageHandlerType.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/matchers/IsMessageHandlerType.java index cfc24b15d9a3..7649cdcb9897 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/matchers/IsMessageHandlerType.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/matchers/IsMessageHandlerType.java @@ -81,7 +81,7 @@ else if (MessageHandler.Partial.class.isAssignableFrom(handlerClass)) switch (expectedType) { case PONG: - return PongMessage.class.isAssignableFrom(registeredDecoder.objectType); + return registeredDecoder.isType(PongMessage.class); case BINARY: return (Decoder.Binary.class.isAssignableFrom(registeredDecoder.interfaceType) || Decoder.BinaryStream.class.isAssignableFrom(registeredDecoder.interfaceType)); diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/matchers/IsMessageHandlerTypeRegistered.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/matchers/IsMessageHandlerTypeRegistered.java index 85809a869594..459e3f0cf2ff 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/matchers/IsMessageHandlerTypeRegistered.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/main/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/matchers/IsMessageHandlerTypeRegistered.java @@ -63,7 +63,7 @@ protected boolean matchesSafely(JakartaWebSocketSession session) if (expectedType == MessageType.PONG) { - if (PongMessage.class.isAssignableFrom(registeredDecoder.objectType)) + if (registeredDecoder.isType(PongMessage.class)) return true; continue; } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/com/acme/websocket/PongSocketStringReturn.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/com/acme/websocket/PongSocketStringReturn.java new file mode 100644 index 000000000000..8503c57819cb --- /dev/null +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/com/acme/websocket/PongSocketStringReturn.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package com.acme.websocket; + +import jakarta.websocket.EndpointConfig; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.PongMessage; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; +import org.eclipse.jetty.util.BufferUtil; + +@ServerEndpoint(value = "/pong-socket-string-return", configurator = PongContextListener.Config.class) +public class PongSocketStringReturn +{ + private String path; + + @OnOpen + public void onOpen(Session session, EndpointConfig config) + { + path = (String)config.getUserProperties().get("path"); + } + + @OnMessage + public String onPong(PongMessage pong) + { + return "PongSocket.onPong(PongMessage)[" + path + "]:" + BufferUtil.toString(pong.getApplicationData()); + } +} diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaOnCloseTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaOnCloseTest.java index a47191b75aab..d13fe5f66702 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaOnCloseTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/JakartaOnCloseTest.java @@ -39,6 +39,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -213,16 +214,17 @@ public void onErrorOccurringAfterOnClose() throws Exception // Initiate close on client to cause the server to throw in onClose. clientEndpoint.session.close(); - // Test the receives the normal close, and throws in onClose. + // The server ignores the error and closes normally once close callback is succeeded. assertTrue(serverEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); assertThat(serverEndpoint.closeReason.getCloseCode(), is(CloseCodes.NORMAL_CLOSURE)); assertTrue(serverEndpoint.errorLatch.await(5, TimeUnit.SECONDS)); assertThat(serverEndpoint.error, instanceOf(RuntimeException.class)); assertThat(serverEndpoint.error.getMessage(), containsString("trigger onError from server onClose")); + // The client also ignores the close error and has normal close status. assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); - assertThat(clientEndpoint.closeReason.getCloseCode(), is(CloseCodes.UNEXPECTED_CONDITION)); - assertThat(clientEndpoint.closeReason.getReasonPhrase(), containsString("trigger onError from server onClose")); + assertThat(clientEndpoint.closeReason.getCloseCode(), is(CloseCodes.NORMAL_CLOSURE)); + assertThat(clientEndpoint.closeReason.getReasonPhrase(), emptyString()); assertTrue(clientEndpoint.errorLatch.await(5, TimeUnit.SECONDS)); assertThat(clientEndpoint.error, instanceOf(RuntimeException.class)); assertThat(clientEndpoint.error.getMessage(), containsString("trigger onError from client onClose")); diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/ProgrammaticWebSocketUpgradeTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/ProgrammaticWebSocketUpgradeTest.java index 15ca85199b61..14fd63f51882 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/ProgrammaticWebSocketUpgradeTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/ProgrammaticWebSocketUpgradeTest.java @@ -190,11 +190,11 @@ public void testWebSocketUpgradeFailure() throws Exception try { client.connectToServer(socket, uri); - fail("expected IOException"); + fail("expected Exception"); } - catch (IOException ioe) + catch (DeploymentException e) { - assertInstanceOf(UpgradeException.class, ioe.getCause()); + assertInstanceOf(UpgradeException.class, e.getCause()); } } } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/AvailableDecodersTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/AvailableDecodersTest.java index 902f335285b3..fa9813db35fd 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/AvailableDecodersTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/AvailableDecodersTest.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Date; +import java.util.List; import java.util.NoSuchElementException; import java.util.TimeZone; @@ -322,9 +323,16 @@ public void testCustomDecoderValidDualBinary() throws DecodeException @Test public void testCustomDecoderRegisterDuplicate() { - // has duplicated support for the same target Type - Exception e = assertThrows(InvalidWebSocketException.class, () -> init(BadDualDecoder.class)); - assertThat(e.getMessage(), containsString("Multiple decoders with different interface types")); + init(BadDualDecoder.class); + System.err.println(availableDecoders); + + List binaryDecoders = availableDecoders.getBinaryDecoders(Fruit.class); + assertThat(binaryDecoders.size(), is(1)); + assertThat(binaryDecoders.get(0).decoder, equalTo(BadDualDecoder.class)); + + List textDecoders = availableDecoders.getTextDecoders(Fruit.class); + assertThat(textDecoders.size(), is(1)); + assertThat(textDecoders.get(0).decoder, equalTo(BadDualDecoder.class)); } @Test diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/AvailableEncodersTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/AvailableEncodersTest.java index c8c1bcf29fdb..13fa24c01494 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/AvailableEncodersTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/AvailableEncodersTest.java @@ -35,6 +35,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -278,18 +279,26 @@ public void testCustomEncoderValidDualBinary() throws IllegalAccessException, In public void testCustomEncoderRegisterDuplicate() { // has duplicated support for the same target Type - Exception e = assertThrows(InvalidWebSocketException.class, () -> encoders.register(BadDualEncoder.class)); - assertThat(e.getMessage(), containsString("Duplicate")); + encoders.register(BadDualEncoder.class); + assertThat(encoders.getEncoderFor(Integer.class), equalTo(BadDualEncoder.class)); } @Test - public void testCustomEncoderRegisterOtherDuplicate() + public void testCustomEncoderRegisterOtherDuplicate() throws Exception { - // Register DateEncoder (decodes java.util.Date) + // Register TimeEncoder (decodes to java.util.Date) + encoders.register(TimeEncoder.class); + + // Register DateEncoder (which also wants to decode to java.util.Date) encoders.register(DateEncoder.class); - // Register TimeEncoder (which also wants to decode java.util.Date) - Exception e = assertThrows(InvalidWebSocketException.class, () -> encoders.register(TimeEncoder.class)); - assertThat(e.getMessage(), containsString("Duplicate")); + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + calendar.set(Calendar.YEAR, 2016); + calendar.set(Calendar.MONTH, Calendar.AUGUST); + calendar.set(Calendar.DAY_OF_MONTH, 22); + Date val = calendar.getTime(); + + // Because of the duplicate the implementation selects just one of the Encoder implementations. + assertTextEncoder(Date.class, val, "2016.08.22"); } } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/DecoderListTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/DecoderListTest.java index 7c7004916817..fb18db443491 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/DecoderListTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/coders/DecoderListTest.java @@ -174,25 +174,22 @@ public void testDecoderOrder() throws Exception } @Test - public void testStreamDecoders() + public void testStreamDecoders() throws Exception { - // Stream decoders will not be able to form a decoder list as they don't implement willDecode(). - Throwable error = assertThrows(Throwable.class, () -> - start(container -> - { - ServerEndpointConfig endpointConfig = ServerEndpointConfig.Builder.create(TextDecoderListEndpoint.class, "/") - .decoders(List.of(TextStreamDecoder1.class, TextStreamDecoder2.class)) - .build(); - container.addEndpoint(endpointConfig); - }) - ); + start(container -> + { + ServerEndpointConfig endpointConfig = ServerEndpointConfig.Builder.create(TextDecoderListEndpoint.class, "/") + .decoders(List.of(TextStreamDecoder1.class, TextStreamDecoder2.class)) + .build(); + container.addEndpoint(endpointConfig); + }); - assertThat(error, instanceOf(RuntimeException.class)); - Throwable cause = error.getCause(); - assertThat(cause, instanceOf(DeploymentException.class)); - Throwable invalidWebSocketException = cause.getCause(); - assertThat(invalidWebSocketException, instanceOf(InvalidWebSocketException.class)); - assertThat(invalidWebSocketException.getMessage(), containsString("Multiple decoders for objectTypeclass java.lang.String")); + // The TextStreamDecoder1 should be the one used as it was first in the list. + EventSocket clientEndpoint = new EventSocket(); + Session session = client.connectToServer(clientEndpoint, serverUri); + session.getBasicRemote().sendText("message"); + String response = clientEndpoint.textMessages.poll(3, TimeUnit.SECONDS); + assertThat(response, is("Decoder1: message")); } public static class TextDecoderListEndpoint extends Endpoint diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/handlers/MessageHandlerTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/handlers/MessageHandlerTest.java index 868398258144..af7f77e095c3 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/handlers/MessageHandlerTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/handlers/MessageHandlerTest.java @@ -15,7 +15,9 @@ import java.net.URI; import java.nio.ByteBuffer; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -76,9 +78,11 @@ public void before() throws Exception Stream argumentsStream = Stream.concat(getBinaryHandlers(), getTextHandlers()); for (Class c : getClassListFromArguments(argumentsStream)) { + System.err.println("deploying " + "/" + c.getSimpleName()); container.addEndpoint(ServerEndpointConfig.Builder.create(c, "/" + c.getSimpleName()).build()); } + System.err.println("deploying " + "/" + LongMessageHandler.class.getSimpleName()); container.addEndpoint(ServerEndpointConfig.Builder.create(LongMessageHandler.class, "/" + LongMessageHandler.class.getSimpleName()).build()); }); @@ -155,8 +159,8 @@ public void testLongDecoderHandler() throws Exception assertThat(clientEndpoint.closeReason.getReasonPhrase(), is("standard close")); } - private List> getClassListFromArguments(Stream stream) + private Set> getClassListFromArguments(Stream stream) { - return stream.map(arguments -> (Class)arguments.get()[0]).collect(Collectors.toList()); + return stream.map(arguments -> (Class)arguments.get()[0]).collect(Collectors.toCollection(LinkedHashSet::new)); } } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/server/DeploymentTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/server/DeploymentTest.java index 547582e26dc8..9792e97629c0 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/server/DeploymentTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/server/DeploymentTest.java @@ -21,6 +21,7 @@ import jakarta.websocket.ContainerProvider; import jakarta.websocket.DecodeException; import jakarta.websocket.Decoder; +import jakarta.websocket.DeploymentException; import jakarta.websocket.EndpointConfig; import jakarta.websocket.OnMessage; import jakarta.websocket.OnOpen; @@ -83,7 +84,7 @@ public void testBadPathParamSignature() throws Exception Throwable error = assertThrows(Throwable.class, () -> client.connectToServer(clientSocket, server.getWsUri().resolve(app1.getContextPath() + "/badonclose/a"))); - assertThat(error, Matchers.instanceOf(IOException.class)); + assertThat(error, Matchers.instanceOf(DeploymentException.class)); assertThat(error.getMessage(), Matchers.containsString("503 Service Unavailable")); } diff --git a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/server/PingPongTest.java b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/server/PingPongTest.java index aa4abe324315..00c7fac4bb3c 100644 --- a/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/server/PingPongTest.java +++ b/jetty-ee11/jetty-ee11-websocket/jetty-ee11-websocket-jakarta-tests/src/test/java/org/eclipse/jetty/ee11/websocket/jakarta/tests/server/PingPongTest.java @@ -23,10 +23,12 @@ import com.acme.websocket.PongContextListener; import com.acme.websocket.PongMessageEndpoint; import com.acme.websocket.PongSocket; +import com.acme.websocket.PongSocketStringReturn; import org.eclipse.jetty.ee11.websocket.jakarta.tests.Timeouts; import org.eclipse.jetty.ee11.websocket.jakarta.tests.WSServer; import org.eclipse.jetty.ee11.websocket.jakarta.tests.framehandlers.FrameHandlerTracker; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.Frame; @@ -56,6 +58,8 @@ public static void startServer() throws Exception app.copyClass(PongContextListener.class); app.copyClass(PongMessageEndpoint.class); app.copyClass(PongSocket.class); + app.copyClass(PongSocketStringReturn.class); + app.copyClass(BufferUtil.class); app.deploy(); server.start(); @@ -123,4 +127,16 @@ public void testPongSocket() throws Exception }, "PongSocket.onPong(PongMessage)[/pong-socket]:hello"); }); } + + @Test + public void testPongSocketReturnsString() throws Exception + { + assertTimeout(Duration.ofMillis(6000), () -> + { + assertEcho("/app/pong-socket-string-return", (session) -> + { + session.sendFrame(new Frame(OpCode.PONG).setPayload("hello"), Callback.NOOP, false); + }, "PongSocket.onPong(PongMessage)[/pong-socket-string-return]:hello"); + }); + } }