Merge remote-tracking branch 'origin/3.1' into 'origin/4.0' Signed-off-by: Maxim Nesen <maxim.nesen@oracle.com>
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java index adf8e03..e9d55c1 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
@@ -18,7 +18,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.net.URI; import java.util.Iterator; import java.util.List; @@ -48,7 +47,6 @@ import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.timeout.IdleStateEvent; import org.glassfish.jersey.uri.internal.JerseyUriBuilder; @@ -146,7 +144,21 @@ ClientRequest newReq = new ClientRequest(jerseyRequest); newReq.setUri(newUri); restrictRedirectRequest(newReq, cr); - connector.execute(newReq, redirectUriHistory, responseAvailable); + + final NettyConnector newConnector = new NettyConnector(newReq.getClient()); + newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() { + @Override + public boolean complete(ClientResponse value) { + newConnector.close(); + return responseAvailable.complete(value); + } + + @Override + public boolean completeExceptionally(Throwable ex) { + newConnector.close(); + return responseAvailable.completeExceptionally(ex); + } + }); } } catch (IllegalArgumentException e) { responseAvailable.completeExceptionally(
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java index 4544157..ebdfda4 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
@@ -416,11 +416,7 @@ // headers if (!jerseyRequest.hasEntity()) { setHeaders(jerseyRequest, nettyRequest.headers(), false); - - // host header - http 1.1 - if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) { - nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost()); - } + setHostHeader(jerseyRequest, nettyRequest); } if (jerseyRequest.hasEntity()) { @@ -468,9 +464,7 @@ @Override public OutputStream getOutputStream(int contentLength) throws IOException { replaceHeaders(jerseyRequest, nettyRequest.headers()); // WriterInterceptor changes - if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) { - nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost()); - } + setHostHeader(jerseyRequest, nettyRequest); headersSet.countDown(); return entityWriter.getOutputStream(); @@ -620,4 +614,18 @@ private static boolean additionalProxyHeadersToKeep(String key) { return key.length() > 2 && (key.charAt(0) == 'x' || key.charAt(0) == 'X') && (key.charAt(1) == '-'); } + + private static void setHostHeader(ClientRequest jerseyRequest, HttpRequest nettyRequest) { + // host header - http 1.1 + if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) { + int requestPort = jerseyRequest.getUri().getPort(); + final String hostHeader; + if (requestPort != 80 && requestPort != 443) { + hostHeader = jerseyRequest.getUri().getHost() + ":" + requestPort; + } else { + hostHeader = jerseyRequest.getUri().getHost(); + } + nettyRequest.headers().add(HttpHeaderNames.HOST, hostHeader); + } + } }
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/FollowRedirectsTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/FollowRedirectsTest.java index 3a1cf69..d63b904 100644 --- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/FollowRedirectsTest.java +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/FollowRedirectsTest.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -24,16 +24,20 @@ import java.util.logging.Logger; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; import org.glassfish.jersey.logging.LoggingFeature; import org.glassfish.jersey.netty.connector.internal.RedirectException; import org.glassfish.jersey.server.ResourceConfig; @@ -60,6 +64,11 @@ return "GET"; } + @POST + public String post() { + return "POST"; + } + @GET @Path("redirect") public Response redirect() { @@ -77,6 +86,12 @@ public Response redirect2() { return Response.seeOther(URI.create(TEST_URL_REF.get() + "/redirect")).build(); } + + @POST + @Path("status307") + public Response status307() { + return Response.temporaryRedirect(URI.create(TEST_URL_REF.get())).build(); + } } @Override @@ -169,4 +184,15 @@ assertEquals(200, r.getStatus()); assertEquals("GET", r.readEntity(String.class)); } + + @Test + public void testRedirect307PostBuffered() { + try (Response response = target("test/status307") + .property(ClientProperties.FOLLOW_REDIRECTS, true) + .property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED) + .request().post(Entity.entity("Something", MediaType.TEXT_PLAIN_TYPE))) { + assertEquals(200, response.getStatus()); + assertEquals("POST", response.readEntity(String.class)); + } + } }
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HostHeaderTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HostHeaderTest.java new file mode 100644 index 0000000..a456acd --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HostHeaderTest.java
@@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.netty.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +public class HostHeaderTest extends JerseyTest { + + private static final String HTTP_HEADER_NAME = "HTTP_PORT_INT"; + + @Path("/") + public static class HostHeaderTestEchoResource { + + @POST + public String post(@Context HttpHeaders headers) { + return get(headers); + } + + @GET + public String get(@Context HttpHeaders headers) { + String sPort = headers.getHeaderString(HTTP_HEADER_NAME); + String hostPort = headers.getHeaderString(HttpHeaders.HOST); + int indexColon = hostPort.indexOf(':'); + if (indexColon != -1) { + hostPort = hostPort.substring(indexColon + 1); + } + if (sPort.equals(hostPort.trim())) { + return GET.class.getName(); + } else { + return "Expected port " + sPort + " but found " + hostPort; + } + } + } + + @Override + protected Application configure() { + return new ResourceConfig(HostHeaderTestEchoResource.class); + } + + @Test + public void testHostHeaderAndPort() { + int port = getPort(); + ClientConfig config = new ClientConfig(); + config.connectorProvider(new NettyConnectorProvider()); + try (Response response = ClientBuilder.newClient(config).target(target().getUri()) + .request() + .header(HTTP_HEADER_NAME, port) + .get()) { + MatcherAssert.assertThat(response.getStatus(), Matchers.is(200)); + MatcherAssert.assertThat(response.readEntity(String.class), Matchers.is(GET.class.getName())); + } + } + + @Test + public void testHostHeaderAndPortAfterRemovedFromFilter() { + int port = getPort(); + ClientConfig config = new ClientConfig(); + config.connectorProvider(new NettyConnectorProvider()); + try (Response response = ClientBuilder.newClient(config) + .target(target().getUri()) + .request() + .header(HTTP_HEADER_NAME, port) + .post(Entity.entity("xxx", MediaType.TEXT_PLAIN_TYPE))) { + MatcherAssert.assertThat(response.getStatus(), Matchers.is(200)); + MatcherAssert.assertThat(response.readEntity(String.class), Matchers.is(GET.class.getName())); + } + } + +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java index 306e336..dd96928 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java
@@ -21,9 +21,6 @@ import java.net.Proxy; import java.net.URL; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; import jakarta.ws.rs.client.Client; @@ -290,16 +287,12 @@ * @throws java.io.IOException in case the connection cannot be provided. */ default HttpURLConnection getConnection(URL url, Proxy proxy) throws IOException { - synchronized (this){ - return (proxy == null) ? getConnection(url) : (HttpURLConnection) url.openConnection(proxy); - } + return (proxy == null) ? getConnection(url) : (HttpURLConnection) url.openConnection(proxy); } } private static class DefaultConnectionFactory implements ConnectionFactory { - private final ConcurrentHashMap<URL, Lock> locks = new ConcurrentHashMap<>(); - @Override public HttpURLConnection getConnection(final URL url) throws IOException { return connect(url, null); @@ -311,13 +304,7 @@ } private HttpURLConnection connect(URL url, Proxy proxy) throws IOException { - Lock lock = locks.computeIfAbsent(url, u -> new ReentrantLock()); - lock.lock(); - try { - return (proxy == null) ? (HttpURLConnection) url.openConnection() : (HttpURLConnection) url.openConnection(proxy); - } finally { - lock.unlock(); - } + return (proxy == null) ? (HttpURLConnection) url.openConnection() : (HttpURLConnection) url.openConnection(proxy); } }
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java index 162e4a0..9ed06d0 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -17,6 +17,7 @@ package org.glassfish.jersey.client.innate.inject; import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.innate.inject.BlindBinder; import org.glassfish.jersey.innate.inject.ClassBinding; import org.glassfish.jersey.innate.inject.InstanceBinding; import org.glassfish.jersey.innate.inject.SupplierClassBinding; @@ -52,10 +53,12 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; -import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import java.util.logging.Level; @@ -75,8 +78,6 @@ private final MultivaluedMap<Type, SupplierInstanceBinding<?>> supplierTypeInstanceBindings = new MultivaluedHashMap<>(); private final MultivaluedMap<Type, SupplierClassBinding<?>> supplierTypeClassBindings = new MultivaluedHashMap<>(); - private final MultivaluedMap<DisposableSupplier, Object> disposableSupplierObjects = new MultivaluedHashMap<>(); - private final Instances instances = new Instances(); private final Types types = new Types(); @@ -89,11 +90,12 @@ * @param <TYPE> the type for which the instance is created, either Class, or ParametrizedType (for instance * Provider<SomeClass>). */ - private class TypedInstances<TYPE> { + private class TypedInstances<TYPE extends Type> { private final MultivaluedMap<TYPE, InstanceContext<?>> singletonInstances = new MultivaluedHashMap<>(); private ThreadLocal<MultivaluedMap<TYPE, InstanceContext<?>>> threadInstances = new ThreadLocal<>(); - private final List<Object> threadPredestroyables = Collections.synchronizedList(new LinkedList<>()); private final ReentrantLock singletonInstancesLock = new ReentrantLock(); + private ThreadLocal<MultivaluedMap<DisposableSupplier, Object>> disposableSupplierObjects = + ThreadLocal.withInitial(() -> new MultivaluedHashMap<>()); private <T> List<InstanceContext<?>> _getSingletons(TYPE clazz) { List<InstanceContext<?>> si; @@ -107,7 +109,7 @@ } @SuppressWarnings("unchecked") - <T> T _addSingleton(TYPE clazz, T instance, Binding<?, ?> binding, Annotation[] qualifiers) { + <T> T _addSingleton(TYPE clazz, T instance, Binding<?, ?> binding, Annotation[] qualifiers, boolean destroy) { singletonInstancesLock.lock(); try { // check existing singleton with a qualifier already created by another thread io a meantime @@ -121,8 +123,8 @@ return (T) qualified.get(0).instance; } } - singletonInstances.add(clazz, new InstanceContext<>(instance, binding, qualifiers)); - threadPredestroyables.add(instance); + InstanceContext<?> instanceContext = new InstanceContext<>(instance, binding, qualifiers, !destroy); + singletonInstances.add(clazz, instanceContext); return instance; } finally { singletonInstancesLock.unlock(); @@ -131,11 +133,11 @@ @SuppressWarnings("unchecked") <T> T addSingleton(TYPE clazz, T t, Binding<?, ?> binding, Annotation[] instanceQualifiers) { - T t2 = _addSingleton(clazz, t, binding, instanceQualifiers); + T t2 = _addSingleton(clazz, t, binding, instanceQualifiers, true); if (t2 == t) { for (Type contract : binding.getContracts()) { if (!clazz.equals(contract) && isClass(contract)) { - _addSingleton((TYPE) contract, t, binding, instanceQualifiers); + _addSingleton((TYPE) contract, t, binding, instanceQualifiers, false); } } } @@ -151,21 +153,22 @@ return list; } - private <T> void _addThreadInstance(TYPE clazz, T instance, Binding<T, ?> binding, Annotation[] qualifiers) { + private <T> void _addThreadInstance( + TYPE clazz, T instance, Binding<T, ?> binding, Annotation[] qualifiers, boolean destroy) { MultivaluedMap<TYPE, InstanceContext<?>> map = threadInstances.get(); if (map == null) { map = new MultivaluedHashMap<>(); threadInstances.set(map); } - map.add(clazz, new InstanceContext<>(instance, binding, qualifiers)); - threadPredestroyables.add(instance); + InstanceContext<?> instanceContext = new InstanceContext<>(instance, binding, qualifiers, !destroy); + map.add(clazz, instanceContext); } <T> void addThreadInstance(TYPE clazz, T t, Binding<T, ?> binding, Annotation[] instanceQualifiers) { - _addThreadInstance(clazz, t, binding, instanceQualifiers); + _addThreadInstance(clazz, t, binding, instanceQualifiers, true); for (Type contract : binding.getContracts()) { if (!clazz.equals(contract) && isClass(contract)) { - _addThreadInstance((TYPE) contract, t, binding, instanceQualifiers); + _addThreadInstance((TYPE) contract, t, binding, instanceQualifiers, false); } } } @@ -183,28 +186,78 @@ private <T> List<InstanceContext<?>> _getContexts(TYPE clazz) { List<InstanceContext<?>> si = _getSingletons(clazz); List<InstanceContext<?>> ti = _getThreadInstances(clazz); - if (si == null && ti != null) { - si = ti; - } else if (ti != null) { - si.addAll(ti); - } - return si; + return InstanceContext.merge(si, ti); } <T> T getInstance(TYPE clazz, Annotation[] annotations) { List<T> i = getInstances(clazz, annotations); if (i != null) { checkUnique(i); - return i.get(0); + return instanceOrSupply(clazz, i.get(0)); } return null; } + private <T> T instanceOrSupply(TYPE clazz, T t) { + if (!Class.class.isInstance(clazz) || ((Class) clazz).isInstance(t)) { + return t; + } else if (Supplier.class.isInstance(t)) { + return (T) registerDisposableSupplierAndGet((Supplier) t, this); + } else if (Provider.class.isInstance(t)) { + return (T) ((Provider) t).get(); + } else { + return t; + } + } + void dispose() { singletonInstances.forEach((clazz, instances) -> instances.forEach(instance -> preDestroy(instance.getInstance()))); - threadPredestroyables.forEach(NonInjectionManager.this::preDestroy); + disposeThreadInstances(true); /* The java.lang.ThreadLocal$ThreadLocalMap$Entry[] keeps references to this NonInjectionManager */ threadInstances = null; + disposableSupplierObjects = null; + } + + void disposeThreadInstances(boolean allThreadInstances) { + MultivaluedMap<TYPE, InstanceContext<?>> ti = threadInstances.get(); + if (ti == null) { + return; + } + Set<Map.Entry<TYPE, List<InstanceContext<?>>>> tiSet = ti.entrySet(); + Iterator<Map.Entry<TYPE, List<InstanceContext<?>>>> tiSetIt = tiSet.iterator(); + while (tiSetIt.hasNext()) { + Map.Entry<TYPE, List<InstanceContext<?>>> entry = tiSetIt.next(); + Iterator<InstanceContext<?>> listIt = entry.getValue().iterator(); + while (listIt.hasNext()) { + InstanceContext<?> instanceContext = listIt.next(); + if (allThreadInstances || instanceContext.getBinding().getScope() != PerThread.class) { + listIt.remove(); + if (DisposableSupplier.class.isInstance(instanceContext.getInstance())) { + MultivaluedMap<DisposableSupplier, Object> disposeMap = disposableSupplierObjects.get(); + Iterator<Map.Entry<DisposableSupplier, List<Object>>> disposeMapIt = disposeMap.entrySet().iterator(); + while (disposeMapIt.hasNext()) { + Map.Entry<DisposableSupplier, List<Object>> disposeMapEntry = disposeMapIt.next(); + if (disposeMapEntry.getKey() == /* identity */ instanceContext.getInstance()) { + Iterator<Object> disposeMapEntryIt = disposeMapEntry.getValue().iterator(); + while (disposeMapEntryIt.hasNext()) { + Object disposeInstance = disposeMapEntryIt.next(); + ((DisposableSupplier) instanceContext.getInstance()).dispose(disposeInstance); + disposeMapEntryIt.remove(); + } + } + if (disposeMapEntry.getValue().isEmpty()) { + disposeMapIt.remove(); + } + } + } + instanceContext.destroy(NonInjectionManager.this); + } + if (entry.getValue().isEmpty()) { + tiSetIt.remove(); + } + } + } + disposableSupplierObjects.remove(); } } @@ -215,9 +268,19 @@ } public NonInjectionManager() { + final Binding binding = new BlindBinder() { + @Override + protected void configure() { + bind(NonInjectionRequestScope.class).to(RequestScope.class).in(Singleton.class); + } + }.getBindings().iterator().next(); + RequestScope scope = new NonInjectionRequestScope(this); + instances.addSingleton(RequestScope.class, scope, binding, null); + types.addSingleton(RequestScope.class, scope, binding, null); } public NonInjectionManager(boolean warning) { + this(); if (warning) { logger.warning(LocalizationMessages.NONINJECT_FALLBACK()); } else { @@ -227,20 +290,21 @@ @Override public void completeRegistration() { - instances._addSingleton(InjectionManager.class, this, new InjectionManagerBinding(), null); + instances._addSingleton(InjectionManager.class, this, new InjectionManagerBinding(), null, false); } @Override public void shutdown() { shutdown = true; - - disposableSupplierObjects.forEach((supplier, objects) -> objects.forEach(supplier::dispose)); - disposableSupplierObjects.clear(); - instances.dispose(); types.dispose(); } + void disposeRequestScopedInstances() { + instances.disposeThreadInstances(false); + types.disposeThreadInstances(false); + } + @Override public boolean isShutdown() { return shutdown; @@ -419,12 +483,7 @@ return (T) this; } if (RequestScope.class.equals(createMe)) { - if (!isRequestScope) { - isRequestScope = true; - return (T) new NonInjectionRequestScope(); - } else { - throw new IllegalStateException(LocalizationMessages.NONINJECT_REQUESTSCOPE_CREATED()); - } + throw new IllegalStateException(LocalizationMessages.NONINJECT_REQUESTSCOPE_CREATED()); } ClassBindings<T> classBindings = classBindings(createMe); @@ -439,12 +498,7 @@ return (T) this; } if (RequestScope.class.equals(createMe)) { - if (!isRequestScope) { - isRequestScope = true; - return (T) new NonInjectionRequestScope(); - } else { - throw new IllegalStateException(LocalizationMessages.NONINJECT_REQUESTSCOPE_CREATED()); - } + throw new IllegalStateException(LocalizationMessages.NONINJECT_REQUESTSCOPE_CREATED()); } ClassBindings<T> classBindings = classBindings(createMe); @@ -543,7 +597,9 @@ @Override public void preDestroy(Object preDestroyMe) { - Method preDestroy = getAnnotatedMethod(preDestroyMe, PreDestroy.class); + Method preDestroy = Method.class.isInstance(preDestroyMe) + ? (Method) preDestroyMe + : getAnnotatedMethod(preDestroyMe, PreDestroy.class); if (preDestroy != null) { ensureAccessible(preDestroy); try { @@ -575,20 +631,27 @@ * @return The proxy for the instance supplied by a supplier or the instance if not required to be proxied. */ @SuppressWarnings("unchecked") - private <T> T createSupplierProxyIfNeeded(Boolean createProxy, Class<T> iface, Supplier<T> supplier) { + private <T> T createSupplierProxyIfNeeded( + Boolean createProxy, Class<T> iface, Supplier<Supplier<T>> supplier, TypedInstances<?> typedInstances) { if (createProxy != null && createProxy && iface.isInterface()) { T proxy = (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[]{iface}, new InvocationHandler() { - final SingleRegisterSupplier<T> singleSupplierRegister = new SingleRegisterSupplier<>(supplier); + final Set<Object> instances = new HashSet<>(); + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - T t = singleSupplierRegister.get(); + Supplier<T> supplierT = supplier.get(); + T t = supplierT.get(); + if (DisposableSupplier.class.isInstance(supplierT) && !instances.contains(t)) { + MultivaluedMap<DisposableSupplier, Object> map = typedInstances.disposableSupplierObjects.get(); + map.add((DisposableSupplier) supplierT, t); + } Object ret = method.invoke(t, args); return ret; } }); return proxy; } else { - return registerDisposableSupplierAndGet(supplier); + return registerDisposableSupplierAndGet(supplier.get(), typedInstances); } } @@ -600,8 +663,8 @@ private class SingleRegisterSupplier<T> { private final LazyValue<T> once; - private SingleRegisterSupplier(Supplier<T> supplier) { - once = Values.lazy((Value<T>) () -> registerDisposableSupplierAndGet(supplier)); + private SingleRegisterSupplier(Supplier<T> supplier, TypedInstances<?> instances) { + once = Values.lazy((Value<T>) () -> registerDisposableSupplierAndGet(supplier, instances)); } T get() { @@ -609,10 +672,10 @@ } } - private <T> T registerDisposableSupplierAndGet(Supplier<T> supplier) { + private <T> T registerDisposableSupplierAndGet(Supplier<T> supplier, TypedInstances<?> typedInstances) { T instance = supplier.get(); if (DisposableSupplier.class.isInstance(supplier)) { - disposableSupplierObjects.add((DisposableSupplier<T>) supplier, instance); + typedInstances.disposableSupplierObjects.get().add((DisposableSupplier<T>) supplier, instance); } return instance; } @@ -686,7 +749,7 @@ * @param <X> The expected return type for the TYPE. * @param <TYPE> The Type for which a {@link Binding} has been created. */ - private abstract class XBindings<X, TYPE> { + private abstract class XBindings<X, TYPE extends Type> { protected final List<InstanceBinding<X>> instanceBindings = new LinkedList<>(); protected final List<SupplierInstanceBinding<X>> supplierInstanceBindings = new LinkedList<>(); @@ -767,12 +830,8 @@ private X _create(SupplierInstanceBinding<X> binding) { Supplier<X> supplier = binding.getSupplier(); - X t = registerDisposableSupplierAndGet(supplier); - if (Singleton.class.equals(binding.getScope())) { - _addInstance(t, binding); - } else if (_isPerThread(binding.getScope())) { - _addThreadInstance(t, binding); - } + X t = registerDisposableSupplierAndGet(supplier, instances); + t = addInstance(type, t, binding); return t; } @@ -836,20 +895,17 @@ protected abstract X _createAndStore(ClassBinding<X> binding); - protected <T> T _addInstance(TYPE type, T instance, Binding<?, ?> binding) { + protected <T> T _addSingletonInstance(TYPE type, T instance, Binding<?, ?> binding) { return instances.addSingleton(type, instance, binding, instancesQualifiers); } - protected void _addThreadInstance(TYPE type, Object instance, Binding binding) { - instances.addThreadInstance(type, instance, binding, instancesQualifiers); - } - - protected <T> T _addInstance(T instance, Binding<?, ?> binding) { - return instances.addSingleton(type, instance, binding, instancesQualifiers); - } - - protected void _addThreadInstance(Object instance, Binding binding) { - instances.addThreadInstance(type, instance, binding, instancesQualifiers); + protected <T> T addInstance(TYPE type, T instance, Binding binding) { + if (Singleton.class.equals(binding.getScope())) { + instance = instances.addSingleton(type, instance, binding, instancesQualifiers); + } else if (_isPerThread(binding.getScope())) { + instances.addThreadInstance(type, instance, binding, instancesQualifiers); + } + return instance; } } @@ -909,28 +965,27 @@ } protected T _create(SupplierClassBinding<T> binding) { - Supplier<T> supplier = instances.getInstance(binding.getSupplierClass(), null); - if (supplier == null) { - supplier = justCreate(binding.getSupplierClass()); - if (Singleton.class.equals(binding.getSupplierScope())) { - supplier = instances.addSingleton(binding.getSupplierClass(), supplier, binding, null); - } else if (_isPerThread(binding.getSupplierScope())) { - instances.addThreadInstance(binding.getSupplierClass(), supplier, binding, null); + Supplier<Supplier<T>> supplierSupplier = () -> { + Supplier<T> supplier = instances.getInstance(binding.getSupplierClass(), null); + if (supplier == null) { + supplier = justCreate(binding.getSupplierClass()); + if (Singleton.class.equals(binding.getSupplierScope())) { + supplier = instances.addSingleton(binding.getSupplierClass(), supplier, binding, null); + } else if (_isPerThread(binding.getSupplierScope()) || binding.getSupplierScope() == null) { + instances.addThreadInstance(binding.getSupplierClass(), supplier, binding, null); + } } - } + return supplier; + }; - T t = createSupplierProxyIfNeeded(binding.isProxiable(), (Class<T>) type, supplier); - if (Singleton.class.equals(binding.getScope())) { - t = _addInstance(type, t, binding); - } else if (_isPerThread(binding.getScope())) { - _addThreadInstance(type, t, binding); - } + T t = createSupplierProxyIfNeeded(binding.isProxiable(), (Class<T>) type, supplierSupplier, instances); +// t = addInstance(type, t, binding); The supplier here creates instances that ought not to be registered as beans return t; } protected T _createAndStore(ClassBinding<T> binding) { T result = justCreate(binding.getService()); - result = _addInstance(binding.getService(), result, binding); + result = addInstance(binding.getService(), result, binding); return result; } } @@ -943,19 +998,15 @@ protected T _create(SupplierClassBinding<T> binding) { Supplier<T> supplier = justCreate(binding.getSupplierClass()); - T t = registerDisposableSupplierAndGet(supplier); - if (Singleton.class.equals(binding.getScope())) { - t = _addInstance(type, t, binding); - } else if (_isPerThread(binding.getScope())) { - _addThreadInstance(type, t, binding); - } + T t = registerDisposableSupplierAndGet(supplier, instances); + t = addInstance(type, t, binding); return t; } @Override protected T _createAndStore(ClassBinding<T> binding) { T result = justCreate(binding.getService()); - result = _addInstance(type, result, binding); + result = addInstance(type, result, binding); return result; } @@ -976,7 +1027,7 @@ return NonInjectionManager.this.getInstance(actualTypeArgument); } } - }); + }, instances); @Override public Object get() { @@ -999,13 +1050,16 @@ private final T instance; private final Binding<?, ?> binding; private final Annotation[] createdWithQualifiers; + private boolean destroyed = false; - private InstanceContext(T instance, Binding<?, ?> binding, Annotation[] qualifiers) { + private InstanceContext(T instance, Binding<?, ?> binding, Annotation[] qualifiers, boolean destroyed) { this.instance = instance; this.binding = binding; this.createdWithQualifiers = qualifiers; + this.destroyed = destroyed; } + public Binding<?, ?> getBinding() { return binding; } @@ -1014,6 +1068,13 @@ return instance; } + public void destroy(NonInjectionManager nonInjectionManager) { + if (!destroyed) { + destroyed = true; + nonInjectionManager.preDestroy(instance); + } + } + @SuppressWarnings("unchecked") static <T> List<T> toInstances(List<InstanceContext<?>> instances, Annotation[] qualifiers) { return instances != null @@ -1032,6 +1093,15 @@ : null; } + private static List<InstanceContext<?>> merge(List<InstanceContext<?>> i1, List<InstanceContext<?>> i2) { + if (i1 == null) { + i1 = i2; + } else if (i2 != null) { + i1.addAll(i2); + } + return i1; + } + private boolean hasQualifiers(Annotation[] requested) { if (requested != null) { classLoop:
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionRequestScope.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionRequestScope.java index 258780d..b8b23b2 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionRequestScope.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionRequestScope.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -20,14 +20,20 @@ import org.glassfish.jersey.internal.util.LazyUid; import org.glassfish.jersey.process.internal.RequestScope; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; public class NonInjectionRequestScope extends RequestScope { + + private final NonInjectionManager nonInjectionManager; + + public NonInjectionRequestScope(NonInjectionManager nonInjectionManager) { + this.nonInjectionManager = nonInjectionManager; + } + @Override public org.glassfish.jersey.process.internal.RequestContext createContext() { - return new Instance(); + return new Instance(nonInjectionManager); } /** @@ -35,6 +41,8 @@ */ public static final class Instance implements org.glassfish.jersey.process.internal.RequestContext { + private final NonInjectionManager injectionManager; + private static final ExtendedLogger logger = new ExtendedLogger(Logger.getLogger(Instance.class.getName()), Level.FINEST); /* @@ -48,10 +56,11 @@ /** * Holds the number of snapshots of this scope. */ - private final AtomicInteger referenceCounter; + private int referenceCounter; - private Instance() { - this.referenceCounter = new AtomicInteger(1); + private Instance(NonInjectionManager injectionManager) { + this.injectionManager = injectionManager; + this.referenceCounter = 1; } /** @@ -65,7 +74,7 @@ @Override public NonInjectionRequestScope.Instance getReference() { // TODO: replace counter with a phantom reference + reference queue-based solution - referenceCounter.incrementAndGet(); + referenceCounter++; return this; } @@ -77,7 +86,9 @@ */ @Override public void release() { - referenceCounter.decrementAndGet(); + if (0 == --referenceCounter) { + injectionManager.disposeRequestScopedInstances(); + } } @Override
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java index ca73f4c..630cb96 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java
@@ -84,7 +84,7 @@ private static final String ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY = "sun.net.http.allowRestrictedHeaders"; // Avoid multi-thread uses of HttpsURLConnection.getDefaultSSLSocketFactory() because it does not implement a // proper lazy-initialization. See https://github.com/jersey/jersey/issues/3293 - private static final Value<SSLSocketFactory> DEFAULT_SSL_SOCKET_FACTORY = + private static final LazyValue<SSLSocketFactory> DEFAULT_SSL_SOCKET_FACTORY = Values.lazy((Value<SSLSocketFactory>) () -> HttpsURLConnection.getDefaultSSLSocketFactory()); // The list of restricted headers is extracted from sun.net.www.protocol.http.HttpURLConnection private static final String[] restrictedHeaders = { @@ -387,6 +387,10 @@ sniUri = request.getUri(); } + if (!DEFAULT_SSL_SOCKET_FACTORY.isInitialized() && "HTTPS".equalsIgnoreCase(sniUri.getScheme())) { + DEFAULT_SSL_SOCKET_FACTORY.get(); + } + proxy.ifPresent(clientProxy -> ClientProxy.setBasicAuthorizationHeader(request.getHeaders(), proxy.get())); uc = this.connectionFactory.getConnection(sniUri.toURL(), proxy.isPresent() ? proxy.get().proxy() : null); uc.setDoInput(true);
diff --git a/core-common/pom.xml b/core-common/pom.xml index 71494de..3c46e09 100644 --- a/core-common/pom.xml +++ b/core-common/pom.xml
@@ -419,6 +419,9 @@ <profile> <id>securityOff</id> + <activation> + <jdk>[24,)</jdk> + </activation> <properties> <surefire.security.argline /> </properties> @@ -427,12 +430,17 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> - <configuration> - <excludes> - <exclude>**/SecurityManagerConfiguredTest.java</exclude> - <exclude>**/ReflectionHelperTest.java</exclude> - </excludes> - </configuration> + <executions> + <execution> + <id>default-test</id> + <configuration> + <excludes> + <exclude>**/SecurityManagerConfiguredTest.java</exclude> + <exclude>**/ReflectionHelperTest.java</exclude> + </excludes> + </configuration> + </execution> + </executions> </plugin> </plugins> </build>
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/io/InputStreamWrapper.java b/core-common/src/main/java/org/glassfish/jersey/innate/io/InputStreamWrapper.java index c2109fe..abcee6b 100644 --- a/core-common/src/main/java/org/glassfish/jersey/innate/io/InputStreamWrapper.java +++ b/core-common/src/main/java/org/glassfish/jersey/innate/io/InputStreamWrapper.java
@@ -18,6 +18,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; /** * Generic wrapper template for InputStream. @@ -55,6 +56,22 @@ } @Override + public byte[] readAllBytes() throws IOException { + return getWrappedIOE().readAllBytes(); + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + return getWrappedIOE().readNBytes(b, off, len); + } + + @Override + public long transferTo(OutputStream out) throws IOException { + return getWrappedIOE().transferTo(out); + } + + + @Override public long skip(long n) throws IOException { return getWrappedIOE().skip(n); }
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java index 5798695..aafb278 100644 --- a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java +++ b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 Payara Foundation and/or its affiliates. * * This program and the accompanying materials are made available under the @@ -125,7 +125,7 @@ @Singleton public static class StringConstructor extends ParamConverterCompliance implements ParamConverterProvider { - private StringConstructor(boolean canReturnNull) { + protected StringConstructor(boolean canReturnNull) { super(canReturnNull); } @@ -154,7 +154,7 @@ @Singleton public static class TypeValueOf extends ParamConverterCompliance implements ParamConverterProvider { - private TypeValueOf(boolean canReturnNull) { + protected TypeValueOf(boolean canReturnNull) { super(canReturnNull); } @@ -182,7 +182,7 @@ @Singleton public static class TypeFromString extends ParamConverterCompliance implements ParamConverterProvider { - private TypeFromString(boolean canReturnNull) { + protected TypeFromString(boolean canReturnNull) { super(canReturnNull); } @@ -210,7 +210,7 @@ @Singleton public static class TypeFromStringEnum extends TypeFromString { - private TypeFromStringEnum(boolean canReturnNull) { + protected TypeFromStringEnum(boolean canReturnNull) { super(canReturnNull); } @@ -225,7 +225,7 @@ @Singleton public static class CharacterProvider extends ParamConverterCompliance implements ParamConverterProvider { - private CharacterProvider(boolean canReturnNull) { + protected CharacterProvider(boolean canReturnNull) { super(canReturnNull); } @@ -270,7 +270,7 @@ @Singleton public static class DateProvider extends ParamConverterCompliance implements ParamConverterProvider { - private DateProvider(boolean canReturnNull) { + protected DateProvider(boolean canReturnNull) { super(canReturnNull); } @@ -346,7 +346,7 @@ // Delegates to this provider when the type of Optional is extracted. private final InjectionManager manager; - public OptionalCustomProvider(InjectionManager manager, boolean canReturnNull) { + protected OptionalCustomProvider(InjectionManager manager, boolean canReturnNull) { super(canReturnNull); this.manager = manager; } @@ -402,6 +402,8 @@ @Singleton public static class OptionalProvider implements ParamConverterProvider { + protected OptionalProvider() {} + @Override public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) { final Optionals optionals = Optionals.getOptional(rawType);
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/EntityInputStream.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/EntityInputStream.java index f94d5fd..2610e17 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/EntityInputStream.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/EntityInputStream.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -22,6 +22,7 @@ import jakarta.ws.rs.ProcessingException; +import org.glassfish.jersey.innate.io.InputStreamWrapper; import org.glassfish.jersey.internal.LocalizationMessages; /** @@ -33,7 +34,7 @@ * * @author Marek Potociar */ -public class EntityInputStream extends InputStream { +public class EntityInputStream extends InputStreamWrapper { private InputStream input; private boolean closed = false; @@ -64,40 +65,6 @@ this.input = input; } - @Override - public int read() throws IOException { - return input.read(); - } - - @Override - public int read(byte[] b) throws IOException { - return input.read(b); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - return input.read(b, off, len); - } - - @Override - public long skip(long n) throws IOException { - return input.skip(n); - } - - @Override - public int available() throws IOException { - return input.available(); - } - - @Override - public void mark(int readLimit) { - input.mark(readLimit); - } - - @Override - public boolean markSupported() { - return input.markSupported(); - } /** * {@inheritDoc} @@ -232,4 +199,9 @@ public final void setWrappedStream(InputStream wrapped) { input = wrapped; } + + @Override + protected InputStream getWrapped() { + return input; + } }
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundJaxrsResponse.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundJaxrsResponse.java index 4db8b21..87fc281 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundJaxrsResponse.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundJaxrsResponse.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -26,6 +26,8 @@ import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -200,7 +202,12 @@ @Override public void close() throws ProcessingException { closed = true; - context.close(); + try { + context.close(); + } catch (Exception e) { + // Just log the exception + Logger.getLogger(OutboundJaxrsResponse.class.getName()).log(Level.FINE, e.getMessage(), e); + } if (buffered) { // release buffer context.setEntity(null);
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java index b1b7745..ceb1540 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
@@ -18,6 +18,7 @@ import java.io.IOException; import java.io.OutputStream; +import java.io.UncheckedIOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.ArrayList; @@ -27,8 +28,6 @@ import java.util.Locale; import java.util.Set; import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.stream.Collectors; import jakarta.ws.rs.core.Configuration; @@ -557,6 +556,7 @@ /** * Closes the context. Flushes and closes the entity stream. + * @throws UncheckedIOException if IO errors */ public void close() { if (hasEntity()) { @@ -567,11 +567,7 @@ } es.close(); } catch (IOException e) { - // Happens when the client closed connection before receiving the full response. - // This is OK and not interesting in the vast majority of the cases - // hence the log level set to FINE to make sure it does not flood the log unnecessarily - // (especially for clients disconnecting from SSE listening, which is very common). - Logger.getLogger(OutboundMessageContext.class.getName()).log(Level.FINE, e.getMessage(), e); + throw new UncheckedIOException(e); } finally { // In case some of the output stream wrapper does not delegate close() call we // close the root stream manually to make sure it commits the data. @@ -579,8 +575,7 @@ try { committingOutputStream.close(); } catch (IOException e) { - // Just log the exception - Logger.getLogger(OutboundMessageContext.class.getName()).log(Level.FINE, e.getMessage(), e); + throw new UncheckedIOException(e); } } }
diff --git a/core-common/src/main/java/org/glassfish/jersey/model/Parameter.java b/core-common/src/main/java/org/glassfish/jersey/model/Parameter.java index 4f7af42..4183568 100644 --- a/core-common/src/main/java/org/glassfish/jersey/model/Parameter.java +++ b/core-common/src/main/java/org/glassfish/jersey/model/Parameter.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -446,14 +446,20 @@ parameterClass); } - private static String getValue(Annotation a) { try { - Method m = a.annotationType().getMethod("value"); - if (m.getReturnType() != String.class) { - return null; + Method[] methods = a.annotationType().getMethods(); + for (Method method : methods) { + if ("value".equals(method.getName())) { + if (method.getReturnType() != String.class) { + return null; + } else { + return (String) method.invoke(a); + } + } } - return (String) m.invoke(a); + LOGGER.log(Level.FINER, () -> + String.format("Unable to get the %s annotation value property", a.getClass().getName())); } catch (Exception ex) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER,
diff --git a/core-common/src/test/java/org/glassfish/jersey/internal/config/ExternalPropertiesConfigurationFactoryTest.java b/core-common/src/test/java/org/glassfish/jersey/internal/config/ExternalPropertiesConfigurationFactoryTest.java index 6f1f7b1..ff3e9ef 100644 --- a/core-common/src/test/java/org/glassfish/jersey/internal/config/ExternalPropertiesConfigurationFactoryTest.java +++ b/core-common/src/test/java/org/glassfish/jersey/internal/config/ExternalPropertiesConfigurationFactoryTest.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -30,11 +30,14 @@ public class ExternalPropertiesConfigurationFactoryTest { + private static boolean isSecurityManager; + /** * Predefine some properties to be read from config */ @BeforeAll public static void setUp() { + isSecurityManager = System.getSecurityManager() != null; System.setProperty(CommonProperties.ALLOW_SYSTEM_PROPERTIES_PROVIDER, Boolean.TRUE.toString()); System.setProperty("jersey.config.server.provider.scanning.recursive", "PASSED"); @@ -53,7 +56,11 @@ public void readSystemPropertiesTest() { final Object result = readExternalPropertiesMap().get("jersey.config.server.provider.scanning.recursive"); - Assertions.assertNull(result); + if (isSecurityManager) { + Assertions.assertNull(result); + } else { + Assertions.assertEquals("PASSED", result); + } Assertions.assertEquals(Boolean.TRUE, getConfig().isProperty(CommonProperties.JSON_PROCESSING_FEATURE_DISABLE)); Assertions.assertEquals(Boolean.TRUE, @@ -81,8 +88,11 @@ inputProperties.put("org.jersey.microprofile.config.added", "ADDED"); getConfig().mergeProperties(inputProperties); final Object result = readExternalPropertiesMap().get("jersey.config.server.provider.scanning.recursive"); - Assertions.assertNull(result); - Assertions.assertNull(readExternalPropertiesMap().get("org.jersey.microprofile.config.added")); + final Object resultAdded = readExternalPropertiesMap().get("org.jersey.microprofile.config.added"); + if (isSecurityManager) { + Assertions.assertNull(result); + Assertions.assertNull(resultAdded); + } } }
diff --git a/core-server/pom.xml b/core-server/pom.xml index 51e59b2..c04560b 100644 --- a/core-server/pom.xml +++ b/core-server/pom.xml
@@ -261,6 +261,9 @@ <profiles> <profile> <id>securityOff</id> + <activation> + <jdk>[24,)</jdk> + </activation> <properties> <surefire.security.argline /> </properties>
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java b/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java index 10656d9..d824e28 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java
@@ -26,6 +26,8 @@ import java.net.URI; import java.util.Date; import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.Map; import java.util.Set; import java.util.function.Predicate; @@ -411,9 +413,14 @@ public void close() { if (!closed) { closed = true; - messageContext.close(); - requestContext.getResponseWriter().commit(); - requestContext.setWorkers(null); + try { + messageContext.close(); + requestContext.setWorkers(null); + requestContext.getResponseWriter().commit(); + } catch (Exception e) { + Logger.getLogger(ContainerResponse.class.getName()).log(Level.FINE, e.getMessage(), e); + requestContext.getResponseWriter().failure(e); + } } }
diff --git a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterDateTest.java b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterDateTest.java index d316d67..a1f6deb 100644 --- a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterDateTest.java +++ b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterDateTest.java
@@ -40,6 +40,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class ParamConverterDateTest extends AbstractTest { + private final String format = "EEE MMM dd HH:mm:ss Z yyyy"; + private final SimpleDateFormat formatter = new SimpleDateFormat(format, new Locale("US")); @Path("/") public static class DateResource { @@ -55,7 +57,7 @@ public void testDateResource() throws ExecutionException, InterruptedException { initiateWebApplication(getBinder(), ParamConverterDateTest.DateResource.class); final ContainerResponse responseContext = getResponseContext(UriBuilder.fromPath("/") - .queryParam("d", new Date()).build().toString()); + .queryParam("d", formatter.format(new Date())).build().toString()); assertEquals(200, responseContext.getStatus()); } @@ -80,8 +82,6 @@ ); } try { - final String format = "EEE MMM dd HH:mm:ss Z yyyy"; - final SimpleDateFormat formatter = new SimpleDateFormat(format, new Locale("US")); return rawType.cast(formatter.parse(value)); } catch (final ParseException ex) { throw new ExtractorException(ex);
diff --git a/docs/src/main/docbook/media.xml b/docs/src/main/docbook/media.xml index 34d2a7d..f27d2f0 100644 --- a/docs/src/main/docbook/media.xml +++ b/docs/src/main/docbook/media.xml
@@ -1129,7 +1129,7 @@ public class JsonbContextResolver implements ContextResolver<Jsonb> { @Override - public Jsonb getContext(Class>?< type) { + public Jsonb getContext(Class<?> type) { JsonbConfig config = new JsonbConfig(); // configure JsonbConfig ...
diff --git a/examples/groovy/pom.xml b/examples/groovy/pom.xml index ab29adf..766a1a9 100644 --- a/examples/groovy/pom.xml +++ b/examples/groovy/pom.xml
@@ -125,10 +125,12 @@ <goal>removeTestStubs</goal> <goal>groovydoc</goal> </goals> + <configuration> + <targetBytecode>11</targetBytecode> + </configuration> </execution> </executions> </plugin> - <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId>
diff --git a/examples/osgi-helloworld-webapp/pom.xml b/examples/osgi-helloworld-webapp/pom.xml index 7bdd2de..22625ff 100644 --- a/examples/osgi-helloworld-webapp/pom.xml +++ b/examples/osgi-helloworld-webapp/pom.xml
@@ -25,16 +25,21 @@ <name>jersey-examples-osgi-helloworld-webapp</name> <packaging>pom</packaging> - <modules> - <module>war-bundle</module> - <module>functional-test</module> - <module>lib-bundle</module> - <module>additional-bundle</module> - <module>alternate-version-bundle</module> - </modules> - <profiles> <profile> + <id>securityOn</id> + <activation> + <jdk>[11,24)</jdk> + </activation> + <modules> + <module>war-bundle</module> + <module>functional-test</module> + <module>lib-bundle</module> + <module>additional-bundle</module> + <module>alternate-version-bundle</module> + </modules> + </profile> + <profile> <id>pre-release</id> <build> <plugins>
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java index 953944b..2db4cd1 100644 --- a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -65,7 +65,7 @@ switch (event.getType()) { case ON_EXCEPTION: - if (!isNotFoundException(event)) { + if (!isClientError(event) || observations.get(containerRequest) != null) { break; } startObservation(event); @@ -102,13 +102,14 @@ observations.put(event.getContainerRequest(), new ObservationScopeAndContext(scope, jerseyContext)); } - private boolean isNotFoundException(RequestEvent event) { + private boolean isClientError(RequestEvent event) { Throwable t = event.getException(); if (t == null) { return false; } - String className = t.getClass().getCanonicalName(); - return className.equals("jakarta.ws.rs.NotFoundException") || className.equals("jakarta.ws.rs.NotFoundException"); + String className = t.getClass().getSuperclass().getCanonicalName(); + return className.equals("jakarta.ws.rs.ClientErrorException") + || className.equals("javax.ws.rs.ClientErrorException"); } private static class ObservationScopeAndContext {
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java index 8813762..ec821a5 100644 --- a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java
@@ -20,6 +20,7 @@ import java.util.logging.Logger; import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; @@ -38,6 +39,7 @@ import io.micrometer.tracing.test.simple.SpansAssert; import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener; import org.glassfish.jersey.micrometer.server.ObservationRequestEventListener; +import org.glassfish.jersey.micrometer.server.mapper.ResourceGoneExceptionMapper; import org.glassfish.jersey.micrometer.server.resources.TestResource; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; @@ -85,6 +87,7 @@ final ResourceConfig config = new ResourceConfig(); config.register(listener); config.register(TestResource.class); + config.register(ResourceGoneExceptionMapper.class); return config; } @@ -131,6 +134,53 @@ .hasTag("outcome", "SUCCESS") .hasTag("status", "200") .hasTag("uri", "/sub-resource/sub-hello/{name}"); + assertThat(observationRegistry.getCurrentObservation()).isNull(); + } + + @Test + void errorResourcesAreTimed() { + try { + target("throws-exception").request().get(); + } + catch (Exception ignored) { + } + try { + target("throws-webapplication-exception").request().get(); + } + catch (Exception ignored) { + } + try { + target("throws-mappable-exception").request().get(); + } + catch (Exception ignored) { + } + try { + target("produces-text-plain").request(MediaType.APPLICATION_JSON).get(); + } + catch (Exception ignored) { + } + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-exception", "500", "SERVER_ERROR", "IllegalArgumentException")) + .timer() + .count()).isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-webapplication-exception", "401", "CLIENT_ERROR", "NotAuthorizedException")) + .timer() + .count()).isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-mappable-exception", "410", "CLIENT_ERROR", "ResourceGoneException")) + .timer() + .count()).isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("UNKNOWN", "406", "CLIENT_ERROR", "NotAcceptableException")) + .timer() + .count()).isEqualTo(1); + + assertThat(observationRegistry.getCurrentObservation()).isNull(); } private static Iterable<Tag> tagsFrom(String uri, String status, String outcome, String exception) {
diff --git a/incubator/pom.xml b/incubator/pom.xml index c63b52e..24cae58 100644 --- a/incubator/pom.xml +++ b/incubator/pom.xml
@@ -39,7 +39,6 @@ <module>cdi-inject-weld</module> <module>declarative-linking</module> <module>gae-integration</module> - <module>html-json</module> <module>injectless-client</module> <module>kryo</module> <module>open-tracing</module> @@ -53,4 +52,16 @@ <scope>test</scope> </dependency> </dependencies> + + <profiles> + <profile> + <id>HTML-JSON-FOR-PRE-JDK24</id> + <activation> + <jdk>[11, 24)</jdk> + </activation> + <modules> + <module>html-json</module> + </modules> + </profile> + </profiles> </project>
diff --git a/inject/cdi2-se/src/test/java/org/glassfish/jersey/inject/cdi/se/DisposableSupplierTest.java b/inject/cdi2-se/src/test/java/org/glassfish/jersey/inject/cdi/se/DisposableSupplierTest.java index 6bade9b..10eec31 100644 --- a/inject/cdi2-se/src/test/java/org/glassfish/jersey/inject/cdi/se/DisposableSupplierTest.java +++ b/inject/cdi2-se/src/test/java/org/glassfish/jersey/inject/cdi/se/DisposableSupplierTest.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -17,6 +17,8 @@ package org.glassfish.jersey.inject.cdi.se; import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -368,9 +370,17 @@ // All instances should be the same because they are request scoped. ComposedObject instance = injectionManager.getInstance(ComposedObject.class); - assertEquals("1", instance.getFirst()); - assertEquals("2", instance.getSecond()); - assertEquals("3", instance.getThird()); + Set<String> set1 = new HashSet<String>() {{ + add("1"); + add("2"); + add("3"); + }}; + Set<String> set2 = new HashSet<String>() {{ + add(instance.getFirst().toString()); + add(instance.getSecond().toString()); + add(instance.getThird().toString()); + }}; + assertEquals(set1, set2); }); Supplier<String> cleanedSupplier = atomicSupplier.get();
diff --git a/inject/hk2/src/test/java/org/glassfish/jersey/inject/hk2/DisposableSupplierTest.java b/inject/hk2/src/test/java/org/glassfish/jersey/inject/hk2/DisposableSupplierTest.java index 89d2db3..bb290a2 100644 --- a/inject/hk2/src/test/java/org/glassfish/jersey/inject/hk2/DisposableSupplierTest.java +++ b/inject/hk2/src/test/java/org/glassfish/jersey/inject/hk2/DisposableSupplierTest.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -17,6 +17,8 @@ package org.glassfish.jersey.inject.hk2; import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -374,9 +376,17 @@ // All instances should be the same because they are request scoped. ComposedObject instance = injectionManager.getInstance(ComposedObject.class); - assertEquals("1", instance.first); - assertEquals("2", instance.second); - assertEquals("3", instance.third); + Set<String> set1 = new HashSet<String>() {{ + add("1"); + add("2"); + add("3"); + }}; + Set<String> set2 = new HashSet<String>() {{ + add(instance.first.toString()); + add(instance.second.toString()); + add(instance.third.toString()); + }}; + assertEquals(set1, set2); }); Supplier<String> cleanedSupplier = atomicSupplier.get();
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JacksonFeature.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JacksonFeature.java index 5411720..9391bc6 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JacksonFeature.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JacksonFeature.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -29,8 +29,10 @@ import org.glassfish.jersey.jackson.internal.DefaultJacksonJaxbJsonProvider; import org.glassfish.jersey.jackson.internal.FilteringJacksonJaxbJsonProvider; import org.glassfish.jersey.jackson.internal.JacksonFilteringFeature; +import org.glassfish.jersey.jackson.internal.JaxrsFeatureBag; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.base.JsonMappingExceptionMapper; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.base.JsonParseExceptionMapper; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.JaxRSFeature; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider; import org.glassfish.jersey.message.MessageProperties; import org.glassfish.jersey.message.filtering.EntityFilteringFeature; @@ -41,7 +43,7 @@ * @author Stepan Kopriva * @author Michal Gajdos */ -public class JacksonFeature implements Feature { +public class JacksonFeature extends JaxrsFeatureBag<JacksonFeature> implements Feature { /** * Define whether to use Jackson's exception mappers ore not @@ -100,6 +102,16 @@ return this; } + /** + * Register {@link JaxRSFeature} with the Jackson providers. + * @param feature the {@link JaxRSFeature} to be enabled or disabled. + * @param state {@code true} for enabling the feature, {@code false} for disabling. + * @return JacksonFeature with {@link JaxRSFeature} registered to be set on a created Jackson provider. + */ + public JacksonFeature jaxrsFeature(JaxRSFeature feature, boolean state) { + return super.jaxrsFeature(feature, state); + } + private static final String JSON_FEATURE = JacksonFeature.class.getSimpleName(); @Override @@ -138,6 +150,10 @@ context.property(MessageProperties.JSON_MAX_STRING_LENGTH, maxStringLength); } + if (hasJaxrsFeature()) { + context.property(JaxrsFeatureBag.JAXRS_FEATURE, this); + } + return true; } }
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JaxRSFeatureObjectMapper.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JaxRSFeatureObjectMapper.java new file mode 100644 index 0000000..d759e56 --- /dev/null +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JaxRSFeatureObjectMapper.java
@@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.glassfish.jersey.jackson.internal.AbstractObjectMapper; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.JaxRSFeature; + + +/** + * The Jackson {@link ObjectMapper} supporting {@link JaxRSFeature}s. + */ +public class JaxRSFeatureObjectMapper extends AbstractObjectMapper { + + public JaxRSFeatureObjectMapper() { + super(); + } + + /** + * Method for changing state of an on/off {@link org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.JaxRSFeature} + * features. + */ + public ObjectMapper configure(JaxRSFeature f, boolean state) { + jaxrsFeatureBag.jaxrsFeature(f, state); + return this; + } + + /** + * Method for enabling specified {@link org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.JaxRSFeature}s + * for parser instances this object mapper creates. + */ + public ObjectMapper enable(JaxRSFeature... features) { + if (features != null) { + for (JaxRSFeature f : features) { + jaxrsFeatureBag.jaxrsFeature(f, true); + } + } + return this; + } + + /** + * Method for disabling specified {@link org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.JaxRSFeature}s + * for parser instances this object mapper creates. + */ + public ObjectMapper disable(JaxRSFeature... features) { + if (features != null) { + for (JaxRSFeature f : features) { + jaxrsFeatureBag.jaxrsFeature(f, false); + } + } + return this; + } +}
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/AbstractObjectMapper.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/AbstractObjectMapper.java new file mode 100644 index 0000000..2f331cf --- /dev/null +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/AbstractObjectMapper.java
@@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jackson.internal; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Internal ObjectMapper with {@link JaxrsFeatureBag}. + */ +public abstract class AbstractObjectMapper extends ObjectMapper { + protected AbstractObjectMapper() { + + } + protected JaxrsFeatureBag jaxrsFeatureBag = new JaxrsFeatureBag(); +}
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java index ac0b5c9..0b9222c 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java
@@ -18,7 +18,6 @@ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.StreamReadConstraints; -import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.core.json.PackageVersion; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.Module; @@ -41,6 +40,7 @@ import jakarta.inject.Singleton; import jakarta.ws.rs.core.Configuration; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.ext.Providers; /** @@ -53,9 +53,7 @@ @Inject public DefaultJacksonJaxbJsonProvider(@Context Providers providers, @Context Configuration config) { - super(new JacksonMapperConfigurator(null, DEFAULT_ANNOTATIONS)); - this.commonConfig = config; - _providers = providers; + this(providers, config, DEFAULT_ANNOTATIONS); } //do not register JaxbAnnotationModule because it brakes default annotations processing @@ -65,6 +63,20 @@ super(new JacksonMapperConfigurator(null, annotationsToUse)); this.commonConfig = config; _providers = providers; + + Object jaxrsFeatureBag = config.getProperty(JaxrsFeatureBag.JAXRS_FEATURE); + if (jaxrsFeatureBag != null && (JaxrsFeatureBag.class.isInstance(jaxrsFeatureBag))) { + ((JaxrsFeatureBag) jaxrsFeatureBag).configureJaxrsFeatures(this); + } + } + + @Override + protected ObjectMapper _locateMapperViaProvider(Class<?> type, MediaType mediaType) { + ObjectMapper mapper = super._locateMapperViaProvider(type, mediaType); + if (AbstractObjectMapper.class.isInstance(mapper)) { + ((AbstractObjectMapper) mapper).jaxrsFeatureBag.configureJaxrsFeatures(this); + } + return mapper; } @Override
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/JaxrsFeatureBag.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/JaxrsFeatureBag.java new file mode 100644 index 0000000..4711b56 --- /dev/null +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/JaxrsFeatureBag.java
@@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jackson.internal; + +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.base.ProviderBase; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.JaxRSFeature; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Internal holder class for {@link JaxRSFeature} settings and their values. + */ +public class JaxrsFeatureBag<T extends JaxrsFeatureBag> { + protected static final String JAXRS_FEATURE = "jersey.config.jackson.jaxrs.feature"; + + private static class JaxRSFeatureState { + /* package */ final JaxRSFeature feature; + /* package */ final boolean state; + public JaxRSFeatureState(JaxRSFeature feature, boolean state) { + this.feature = feature; + this.state = state; + } + } + + private Optional<List<JaxRSFeatureState>> jaxRSFeature = Optional.empty(); + + public T jaxrsFeature(JaxRSFeature feature, boolean state) { + if (!jaxRSFeature.isPresent()) { + jaxRSFeature = Optional.of(new ArrayList<>()); + } + jaxRSFeature.ifPresent(list -> list.add(new JaxrsFeatureBag.JaxRSFeatureState(feature, state))); + return (T) this; + } + + protected boolean hasJaxrsFeature() { + return jaxRSFeature.isPresent(); + } + + /* package */ void configureJaxrsFeatures(ProviderBase providerBase) { + jaxRSFeature.ifPresent(list -> list.stream().forEach(state -> providerBase.configure(state.feature, state.state))); + } +}
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java index f4bec3e..fd5d9aa 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java
@@ -129,7 +129,7 @@ } return _jaxbIntrospectorClass.newInstance(); } catch (Exception e) { - throw new IllegalStateException("Failed to instantiate JaxbAnnotationIntrospector: "+e.getMessage(), e); + throw new IllegalStateException("Failed to instantiate JakartaXmlBindAnnotationIntrospector: "+e.getMessage(), e); } default: throw new IllegalStateException();
diff --git a/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/DefaultJsonJacksonProviderForBothModulesTest.java b/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/DefaultJsonJacksonProviderForBothModulesTest.java index 59d19c1..e9b621a 100644 --- a/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/DefaultJsonJacksonProviderForBothModulesTest.java +++ b/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/DefaultJsonJacksonProviderForBothModulesTest.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -23,6 +23,9 @@ import jakarta.ws.rs.core.Application; +import java.util.List; +import java.util.Arrays; +import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; public class DefaultJsonJacksonProviderForBothModulesTest extends JerseyTest { @@ -36,8 +39,12 @@ public final void testDisabledModule() { final String response = target("entity/simple") .request().get(String.class); + String expected = "{\"name\":\"Hello\",\"value\":\"World\"}"; + List<String> response_list = Arrays.asList(response.replaceAll("[{}]", "").split(",")); + List<String> expected_list = Arrays.asList(expected.replaceAll("[{}]", "").split(",")); + Collections.sort(response_list); - assertEquals("{\"name\":\"Hello\",\"value\":\"World\"}", response); + assertEquals(expected_list, response_list); } }
diff --git a/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/JaxRSFeatureTest.java b/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/JaxRSFeatureTest.java new file mode 100644 index 0000000..86b6200 --- /dev/null +++ b/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/JaxRSFeatureTest.java
@@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.jackson.internal; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.jackson.JaxRSFeatureObjectMapper; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.JaxRSFeature; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.Providers; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class JaxRSFeatureTest { + @Test + public void testJaxrsFeatureOnJacksonFeature() { + Client client = ClientBuilder.newClient() + .register(new JacksonFeature().jaxrsFeature(JaxRSFeature.READ_FULL_STREAM, false)) + .register(JaxrsFeatureFilter.class); + + try (Response r = client.target("http://xxx.yyy").request().get()) { + MatcherAssert.assertThat(r.getStatus(), Matchers.is(200)); + } + } + + @Test + public void testJaxrsFeatureOnContextResolver() { + Client client = ClientBuilder.newClient() + .register(JacksonFeature.class) + .register(JaxrsFetureContextResolver.class) + .register(JaxrsFeatureFilter.class); + + try (Response r = client.target("http://xxx.yyy").request().get()) { + MatcherAssert.assertThat(r.getStatus(), Matchers.is(200)); + } + } + + + public static class JaxrsFeatureFilter implements ClientRequestFilter { + private final DefaultJacksonJaxbJsonProvider jacksonProvider; + @Inject + public JaxrsFeatureFilter(Providers allProviders) { + jacksonProvider = (DefaultJacksonJaxbJsonProvider) + allProviders.getMessageBodyReader(Object.class, Object.class, null, MediaType.APPLICATION_JSON_TYPE); + try { + jacksonProvider.readFrom(Object.class, Object.class, null, MediaType.APPLICATION_JSON_TYPE, null, + new ByteArrayInputStream("{}".getBytes())); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + Response.Status status = jacksonProvider.isEnabled(JaxRSFeature.READ_FULL_STREAM) + ? Response.Status.FORBIDDEN + : Response.Status.OK; + requestContext.abortWith(Response.status(status).build()); + } + } + + public static class JaxrsFetureContextResolver implements ContextResolver<ObjectMapper> { + + @Override + public ObjectMapper getContext(Class<?> type) { + JaxRSFeatureObjectMapper objectMapper = new JaxRSFeatureObjectMapper(); + objectMapper.disable(JaxRSFeature.READ_FULL_STREAM); + return objectMapper; + } + } +}
diff --git a/pom.xml b/pom.xml index e4fc357..a43743e 100644 --- a/pom.xml +++ b/pom.xml
@@ -2088,7 +2088,7 @@ <resources.mvn.plugin.version>3.3.1</resources.mvn.plugin.version> <shade.mvn.plugin.version>3.6.0</shade.mvn.plugin.version> <source.mvn.plugin.version>3.3.1</source.mvn.plugin.version> - <surefire.mvn.plugin.version>3.3.1</surefire.mvn.plugin.version> + <surefire.mvn.plugin.version>3.5.2</surefire.mvn.plugin.version> <war.mvn.plugin.version>3.4.0</war.mvn.plugin.version> <wiremock.mvn.plugin.version>2.11.0</wiremock.mvn.plugin.version> <xml.mvn.plugin.version>1.1.0</xml.mvn.plugin.version> @@ -2117,7 +2117,7 @@ <findbugs.glassfish.version>1.7</findbugs.glassfish.version> <freemarker.version>2.3.33</freemarker.version> <gae.version>2.0.29</gae.version> - <groovy.version>4.0.23</groovy.version> + <groovy.version>5.0.0-alpha-11</groovy.version> <gson.version>2.11.0</gson.version> <!--versions, extracted here due to maven-enforcer-plugin --> @@ -2150,7 +2150,7 @@ <jmh.version>1.37</jmh.version> <jmockit.version>1.49</jmockit.version> <junit4.version>4.13.2</junit4.version> - <junit5.version>5.11.0</junit5.version> + <junit5.version>5.11.4</junit5.version> <junit-platform-suite.version>1.11.0</junit-platform-suite.version> <junit-platform-suite.legacy.version>1.10.0</junit-platform-suite.legacy.version> <kryo.version>4.0.3</kryo.version>
diff --git a/test-framework/maven/container-runner-maven-plugin/pom.xml b/test-framework/maven/container-runner-maven-plugin/pom.xml index b6b6d41..a462a7b 100644 --- a/test-framework/maven/container-runner-maven-plugin/pom.xml +++ b/test-framework/maven/container-runner-maven-plugin/pom.xml
@@ -36,7 +36,6 @@ </description> <properties> -<!-- <groovy.version>3.0.21</groovy.version>--> <groovy-eclipse-compiler.version>3.7.0</groovy-eclipse-compiler.version> <groovy-eclipse-batch.version>3.0.8-01</groovy-eclipse-batch.version> <maven.version>3.9.2</maven.version>
diff --git a/tests/e2e-inject/non-inject/pom.xml b/tests/e2e-inject/non-inject/pom.xml new file mode 100644 index 0000000..a0f81ed --- /dev/null +++ b/tests/e2e-inject/non-inject/pom.xml
@@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Copyright (c) 2024, 2025 Oracle and/or its affiliates. All rights reserved. + + This program and the accompanying materials are made available under the + terms of the Eclipse Public License v. 2.0, which is available at + http://www.eclipse.org/legal/epl-2.0. + + This Source Code may also be made available under the following Secondary + Licenses when the conditions for such availability set forth in the + Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + version 2 with the GNU Classpath Exception, which is available at + https://www.gnu.org/software/classpath/license.html. + + SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +--> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>e2e-inject</artifactId> + <groupId>org.glassfish.jersey.tests</groupId> + <version>4.0.99-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>e2e-inject-noninject</artifactId> + + <dependencies> + <dependency> + <groupId>org.glassfish.jersey.incubator</groupId> + <artifactId>jersey-injectless-client</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + +</project>
diff --git a/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/DisposableSuplierTest.java b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/DisposableSuplierTest.java new file mode 100644 index 0000000..0d36f34 --- /dev/null +++ b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/DisposableSuplierTest.java
@@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.inject.noninject; + +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.DisposableSupplier; +import org.glassfish.jersey.process.internal.RequestScoped; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +public class DisposableSuplierTest { + private static final AtomicInteger disposeCounter = new AtomicInteger(0); + private static final String HOST = "http://somewhere.anywhere"; + + public interface ResponseObject { + Response getResponse(); + } + + private static class TestDisposableSupplier implements DisposableSupplier<ResponseObject> { + AtomicInteger counter = new AtomicInteger(300); + + @Override + public void dispose(ResponseObject instance) { + disposeCounter.incrementAndGet(); + } + + @Override + public ResponseObject get() { + return new ResponseObject() { + + @Override + public Response getResponse() { + return Response.ok().build(); + } + }; + } + } + + private static class DisposableSupplierInjectingFilter implements ClientRequestFilter { + private final ResponseObject responseSupplier; + + @Inject + private DisposableSupplierInjectingFilter(ResponseObject responseSupplier) { + this.responseSupplier = responseSupplier; + } + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(responseSupplier.getResponse()); + } + } + + @Test + public void testDisposeCount() { + disposeCounter.set(0); + int CNT = 4; + Client client = ClientBuilder.newClient() + .register(new AbstractBinder() { + @Override + protected void configure() { + bindFactory(TestDisposableSupplier.class).to(ResponseObject.class) + .proxy(true).proxyForSameScope(false).in(RequestScoped.class); + } + }).register(DisposableSupplierInjectingFilter.class); + + for (int i = 0; i != CNT; i++) { + try (Response response = client.target(HOST).request().get()) { + MatcherAssert.assertThat(response.getStatus(), Matchers.is(200)); + } + } + + MatcherAssert.assertThat(disposeCounter.get(), Matchers.is(CNT)); + } +}
diff --git a/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/InstanceListSizeTest.java b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/InstanceListSizeTest.java new file mode 100644 index 0000000..2acb538 --- /dev/null +++ b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/InstanceListSizeTest.java
@@ -0,0 +1,195 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.inject.noninject; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.innate.inject.NonInjectionManager; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class InstanceListSizeTest { + + private static final String TEST_HEADER = "TEST"; + private static final String HOST = "https://anywhere.any"; + + @Test + public void leakTest() throws ExecutionException, InterruptedException { + int CNT = 100; + Client client = ClientBuilder.newClient(); + client.register(InjectionManagerGrowChecker.class); + Response response = client.target(HOST).request().header(TEST_HEADER, "0").get(); + int status = response.getStatus(); + response.close(); + //Create instance in NonInjectionManager$TypedInstances.threadPredestroyables + + for (int i = 0; i <= CNT; i++) { + final String header = String.valueOf(i + 1); + try (Response r = client.target(HOST).request() + .header(TEST_HEADER, header) + .async() + .post(Entity.text("text")).get()) { + int stat = r.getStatus(); + MatcherAssert.assertThat( + "NonInjectionManager#Types#disposableSupplierObjects is increasing", stat, Matchers.is(202)); + } + } + //Create 10 instance in NonInjectionManager$TypedInstances.threadPredestroyables + + for (int i = 0; i <= CNT; i++) { + final String header = String.valueOf(i + CNT + 2); + final Object text = CompletableFuture.supplyAsync(() -> { + Response test = client.target(HOST).request() + .header("TEST", header) + .post(Entity.text("text")); + int stat = test.getStatus(); + test.close(); + MatcherAssert.assertThat( + "NonInjectionManager#Types#disposableSupplierObjects is increasing", stat, Matchers.is(202)); + + return null; + }).join(); + } + //Create 10 instance in NonInjectionManager$TypedInstances.threadPredestroyables + + response = client.target(HOST).request().header(TEST_HEADER, 2 * CNT + 3).get(); + status = response.getStatus(); + MatcherAssert.assertThat(status, Matchers.is(202)); + response.close(); + } + + private static class InjectionManagerGrowChecker implements ClientRequestFilter { + private boolean first = true; + private int disposableSize = 0; + private int threadInstancesSize = 0; + private HttpHeaders headers; + private int headerCnt = 0; + + @Inject + public InjectionManagerGrowChecker(HttpHeaders headers) { + this.headers = headers; + } + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + Response.Status status = Response.Status.ACCEPTED; + if (headerCnt++ != Integer.parseInt(headers.getHeaderString("TEST"))) { + status = Response.Status.BAD_REQUEST; + } + + NonInjectionManager nonInjectionManager = getInjectionManager(requestContext); + Object types = getDeclaredField(nonInjectionManager, "types"); + Object instances = getDeclaredField(nonInjectionManager, "instances"); + if (first) { + first = false; + disposableSize = getThreadInstances(types, "disposableSupplierObjects") + + getThreadInstances(instances, "disposableSupplierObjects"); + threadInstancesSize = getThreadInstances(types, "threadInstances") + + getThreadInstances(instances, "threadInstances"); + } else { + int newPredestroyableSize = getThreadInstances(types, "disposableSupplierObjects") + + getThreadInstances(instances, "disposableSupplierObjects"); + if (newPredestroyableSize > disposableSize + 1 /* a new service to get disposed */) { + status = Response.Status.EXPECTATION_FAILED; + } + int newThreadInstances = getThreadInstances(types, "threadInstances") + + getThreadInstances(instances, "threadInstances"); + if (newThreadInstances > threadInstancesSize) { + status = Response.Status.PRECONDITION_FAILED; + } + } + + requestContext.abortWith(Response.status(status).build()); + } + } + + private static NonInjectionManager getInjectionManager(ClientRequestContext context) { + ClientRequest request = ((ClientRequest) context); + try { + Method clientConfigMethod = ClientRequest.class.getDeclaredMethod("getClientConfig"); + clientConfigMethod.setAccessible(true); + ClientConfig clientConfig = (ClientConfig) clientConfigMethod.invoke(request); + + Method runtimeMethod = ClientConfig.class.getDeclaredMethod("getRuntime"); + runtimeMethod.setAccessible(true); + Object clientRuntime = runtimeMethod.invoke(clientConfig); + Class<?> clientRuntimeClass = clientRuntime.getClass(); + + Method injectionManagerMethod = clientRuntimeClass.getDeclaredMethod("getInjectionManager"); + injectionManagerMethod.setAccessible(true); + InjectionManager injectionManager = (InjectionManager) injectionManagerMethod.invoke(clientRuntime); + return (NonInjectionManager) injectionManager; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Object getDeclaredField(NonInjectionManager nonInjectionManager, String name) { + try { + Field typesField = NonInjectionManager.class.getDeclaredField(name); + typesField.setAccessible(true); + return typesField.get(nonInjectionManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static int getThreadInstances(Object typedInstances, String threadLocalName) { + try { + Field threadLocalField = + typedInstances.getClass().getSuperclass().getDeclaredField(threadLocalName); + threadLocalField.setAccessible(true); + ThreadLocal<MultivaluedMap<?,?>> threadLocal = + (ThreadLocal<MultivaluedMap<?, ?>>) threadLocalField.get(typedInstances); + MultivaluedMap<?, ?> map = threadLocal.get(); + if (map == null) { + return 0; + } else { + int cnt = 0; + Set<? extends Map.Entry<?, ? extends List<?>>> set = map.entrySet(); + for (Map.Entry<?, ? extends List<?>> entry : map.entrySet()) { + cnt += entry.getValue().size(); + } + return cnt; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + +}
diff --git a/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/PreDestroyTest.java b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/PreDestroyTest.java new file mode 100644 index 0000000..0f43b44 --- /dev/null +++ b/tests/e2e-inject/non-inject/src/test/org/glassfish/jersey/tests/e2e/inject/noninject/PreDestroyTest.java
@@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.inject.noninject; + +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.internal.inject.DisposableSupplier; +import org.glassfish.jersey.process.internal.RequestScoped; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +public class PreDestroyTest { + private static final AtomicInteger disposeCounter = new AtomicInteger(0); + private static final String HOST = "http://somewhere.anywhere"; + + public interface ResponseObject { + Response getResponse(); + } + + public static class ResponseObjectImpl implements ResponseObject { + + public ResponseObjectImpl() { + + } + + @PreDestroy + public void preDestroy() { + disposeCounter.incrementAndGet(); + } + + @Override + public Response getResponse() { + return Response.ok().build(); + } + } + + private static class PreDestroyInjectingFilter implements ClientRequestFilter { + private final ResponseObject responseSupplier; + + @Inject + private PreDestroyInjectingFilter(ResponseObject responseSupplier) { + this.responseSupplier = responseSupplier; + } + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(responseSupplier.getResponse()); + } + } + + @Test + public void testPreDestroyCount() { + disposeCounter.set(0); + int CNT = 4; + Client client = ClientBuilder.newClient() + .register(new AbstractBinder() { + @Override + protected void configure() { + bind(ResponseObjectImpl.class).to(ResponseObject.class) + .proxy(true).proxyForSameScope(false).in(RequestScoped.class); + } + }).register(PreDestroyInjectingFilter.class); + + for (int i = 0; i != CNT; i++) { + try (Response response = client.target(HOST).request().get()) { + MatcherAssert.assertThat(response.getStatus(), Matchers.is(200)); + } + } + + MatcherAssert.assertThat(disposeCounter.get(), Matchers.is(1)); + } +}
diff --git a/tests/e2e-inject/pom.xml b/tests/e2e-inject/pom.xml index 99b266e..ff15309 100644 --- a/tests/e2e-inject/pom.xml +++ b/tests/e2e-inject/pom.xml
@@ -36,5 +36,6 @@ <module>cdi2-se</module> <module>cdi-inject-weld</module> <module>hk2</module> + <module>non-inject</module> </modules> </project>
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/Issue5783Test.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/Issue5783Test.java new file mode 100644 index 0000000..ab7b301 --- /dev/null +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/Issue5783Test.java
@@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.server; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +public class Issue5783Test extends JerseyTest { + + private static final String ERROR = "Intentional issue5783 exception"; + private static volatile String exceptionMessage; + + @Override + protected Application configure() { + return new ResourceConfig(Resource.class, ResponseFilter.class); + } + + @Test + public void closeException() throws InterruptedException { + target("/test").request().get(); + assertEquals(ERROR, exceptionMessage); + } + + @Path("/test") + public static class Resource { + + @GET + public Response closeException(@Context ContainerRequest request) { + // Save the exception when response.getRequestContext().getResponseWriter().failure(e) + ContainerResponseWriter writer = request.getResponseWriter(); + ContainerResponseWriter proxy = (ContainerResponseWriter) Proxy.newProxyInstance( + ContainerResponseWriter.class.getClassLoader(), + new Class<?>[]{ContainerResponseWriter.class}, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("failure".equals(method.getName())) { + exceptionMessage = ((Throwable) args[0]).getCause().getMessage(); + } + return method.invoke(writer, args); + } + }); + request.setWriter(proxy); + return Response.ok().build(); + } + } + + @Provider + public static class ResponseFilter implements ContainerResponseFilter { + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + // Hack it to make ContainerResponse#close throws one exception + responseContext.setEntity("something"); + responseContext.setEntityStream(new ByteArrayOutputStream() { + @Override + public void close() throws IOException { + throw new IOException(ERROR); + } + }); + } + } +}
diff --git a/tests/e2e-tls/pom.xml b/tests/e2e-tls/pom.xml index 1d5d247..84f1706 100644 --- a/tests/e2e-tls/pom.xml +++ b/tests/e2e-tls/pom.xml
@@ -147,4 +147,25 @@ </http.patch.addopens> </properties> + <profiles> + <profile> + <id>JDK_17-</id> + <activation> + <jdk>[11,17)</jdk> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <!-- The certificate is not working with JDK 11 --> + <excludes><exclude>**/ConcurrentHttpsUrlConnectionTest*</exclude></excludes> + </configuration> + </plugin> + </plugins> + </build> + </profile> + </profiles> + </project>
diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/connector/ConcurrentHttpsUrlConnectionTest.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/connector/ConcurrentHttpsUrlConnectionTest.java new file mode 100644 index 0000000..444cee6 --- /dev/null +++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/connector/ConcurrentHttpsUrlConnectionTest.java
@@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.tls.connector; + +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MediaType; + +import java.io.InputStream; +import java.net.URL; +import java.security.KeyStore; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManagerFactory; + +/** + * Jersey client seems to be not thread-safe: + * When the first GET request is in progress, + * all parallel requests from other Jersey client instances fail + * with SSLHandshakeException: PKIX path building failed. + * <p> + * Once the first GET request is completed, + * all subsequent requests work without error. + * <p> + * BUG 5749 + */ +public class ConcurrentHttpsUrlConnectionTest { + private static int THREAD_NUMBER = 5; + + private static volatile int responseCounter = 0; + + private static SSLContext createContext() throws Exception { + URL url = ConcurrentHttpsUrlConnectionTest.class.getResource("keystore.jks"); + KeyStore keyStore = KeyStore.getInstance("JKS"); + try (InputStream is = url.openStream()) { + keyStore.load(is, "password".toCharArray()); + } + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(keyStore, "password".toCharArray()); + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(keyStore); + SSLContext context = SSLContext.getInstance("TLS"); + context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + return context; + } + + @Test + public void testSSLConnections() throws Exception { + if (THREAD_NUMBER == 1) { + System.out.println("\nThis is the working case (THREAD_NUMBER==1). Set THREAD_NUMBER > 1 to reproduce the error! \n"); + } + + final HttpsServer server = new HttpsServer(createContext()); + Executors.newFixedThreadPool(1).submit(server); + + // set THREAD_NUMBER > 1 to reproduce an issue + ExecutorService executorService2clients = Executors.newFixedThreadPool(THREAD_NUMBER); + + final ClientBuilder builder = ClientBuilder.newBuilder().sslContext(createContext()) + .hostnameVerifier(new HostnameVerifier() { + public boolean verify(String arg0, SSLSession arg1) { + return true; + } + }); + + AtomicInteger counter = new AtomicInteger(0); + + for (int i = 0; i < THREAD_NUMBER; i++) { + executorService2clients.submit(new Runnable() { + @Override + public void run() { + try { + Client client = builder.build(); + String ret = client.target("https://127.0.0.1:" + server.getPort() + "/" + new Random().nextInt()) + .request(MediaType.TEXT_HTML) + .get(new GenericType<String>() { + }); + System.out.print(++responseCounter + ". Server returned: " + ret); + } catch (Exception e) { + //get an exception here, if jersey lib is buggy and THREAD_NUMBER > 1: + //jakarta.ws.rs.ProcessingException: jakarta.net.ssl.SSLHandshakeException: PKIX path building failed: + e.printStackTrace(); + } finally { + System.out.println(counter.incrementAndGet()); + } + } + }); + } + + while (counter.get() != THREAD_NUMBER) { + Thread.sleep(100L); + } + server.stop(); + } +}
diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/connector/HttpsServer.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/connector/HttpsServer.java new file mode 100644 index 0000000..31214f1 --- /dev/null +++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/connector/HttpsServer.java
@@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.tls.connector; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; + +class HttpsServer implements Runnable { + private final SSLServerSocket sslServerSocket; + private boolean closed = false; + + public HttpsServer(SSLContext context) throws Exception { + sslServerSocket = (SSLServerSocket) context.getServerSocketFactory().createServerSocket(0); + } + + public int getPort() { + return sslServerSocket.getLocalPort(); + } + + @Override + public void run() { + System.out.printf("Server started on port %d%n", getPort()); + while (!closed) { + SSLSocket s; + try { + s = (SSLSocket) sslServerSocket.accept(); + } catch (IOException e2) { + s = null; + } + final SSLSocket socket = s; + new Thread(new Runnable() { + public void run() { + try { + if (socket != null) { + InputStream is = new BufferedInputStream(socket.getInputStream()); + byte[] data = new byte[2048]; + int len = is.read(data); + if (len <= 0) { + throw new IOException("no data received"); + } + //System.out.printf("Server received: %s\n", new String(data, 0, len)); + PrintWriter writer = new PrintWriter(socket.getOutputStream()); + writer.println("HTTP/1.1 200 OK"); + writer.println("Content-Type: text/html"); + writer.println(); + writer.println("Hello from server!"); + writer.flush(); + writer.close(); + socket.close(); + } + } catch (Exception e1) { + e1.printStackTrace(); + } + } + }).start(); + } + } + + void stop() { + try { + closed = true; + sslServerSocket.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +}
diff --git a/tests/e2e-tls/src/test/resources/org/glassfish/jersey/tests/e2e/tls/connector/keystore.jks b/tests/e2e-tls/src/test/resources/org/glassfish/jersey/tests/e2e/tls/connector/keystore.jks new file mode 100644 index 0000000..130aa71 --- /dev/null +++ b/tests/e2e-tls/src/test/resources/org/glassfish/jersey/tests/e2e/tls/connector/keystore.jks Binary files differ
diff --git a/tests/integration/microprofile/rest-client/pom.xml b/tests/integration/microprofile/rest-client/pom.xml index bd10500..ab7cf39 100644 --- a/tests/integration/microprofile/rest-client/pom.xml +++ b/tests/integration/microprofile/rest-client/pom.xml
@@ -107,6 +107,15 @@ </dependency> </dependencies> </profile> + <profile> + <id>securityOff</id> + <activation> + <jdk>[24,)</jdk> + </activation> + <properties> + <surefire.security.argline /> + </properties> + </profile> </profiles> <properties>
diff --git a/tests/pom.xml b/tests/pom.xml index 2025a8a..68b7502 100644 --- a/tests/pom.xml +++ b/tests/pom.xml
@@ -47,7 +47,6 @@ <module>e2e-testng</module> <module>e2e-tls</module> <module>integration</module> - <module>jmockit</module> <module>mem-leaks</module> <module>osgi</module> <module>release-test</module> @@ -117,5 +116,32 @@ <build> </build> </profile> + <profile> + <id>JDK11+</id> + <activation> + <jdk>[11,)</jdk> + </activation> + <modules> + <module>release-test</module> + </modules> + </profile> + <profile> + <id>JDK17+</id> + <activation> + <jdk>[17,)</jdk> + </activation> + <modules> + <module>version-agnostic</module> + </modules> + </profile> + <profile> + <id>JMOCKIT-MODULE-FOR-PRE-JDK24</id> + <activation> + <jdk>[11,24)</jdk> + </activation> + <modules> + <module>jmockit</module> + </modules> + </profile> </profiles> </project>