From 7595659071f3bfeb84e96a225844c0f41fa7ef0d Mon Sep 17 00:00:00 2001 From: NicoNes Date: Mon, 16 Oct 2023 09:37:25 -0400 Subject: [PATCH] Regression in release-4.0 branch (#1176) * Rollback to previous revision to fix regression introduced by commit ae1f6ef7f4db9957079b8c383041a4537f33f35b Signed-off-by: Nicolas NESMON (@NicoNes) * Re apply only relevant changes form commit ae1f6ef7f4db9957079b8c383041a4537f33f35b Signed-off-by: Nicolas NESMON (@NicoNes) * Re apply import ordering changes from commit ae1f6ef7f4db9957079b8c383041a4537f33f35b Signed-off-by: Nicolas NESMON (@NicoNes) --------- Signed-off-by: Nicolas NESMON (@NicoNes) Co-authored-by: Santiago Pericas-Geertsen --- .../BasicJavaSeBootstrapExample.java | 24 +- ...tAuthenticationJavaSeBootstrapExample.java | 22 +- .../ExplicitJavaSeBootstrapExample.java | 17 +- .../ExternalConfigJavaSeBootstrapExample.java | 22 +- .../HttpsJavaSeBootstrapExample.java | 17 +- .../NativeJavaSeBootstrapExample.java | 17 +- ...ropertyProviderJavaSeBootstrapExample.java | 22 +- .../bootstrap/TlsJavaSeBootstrapExample.java | 17 +- .../examples/bootstrap/package-info.java | 2 +- .../jaxrs/examples/link/LinkExamples.java | 2 +- .../examples/multipart/MultipartClient.java | 71 ++++ .../examples/multipart/MultipartResource.java | 96 +++++ .../examples/multipart/package-info.java | 4 + .../jaxrs/examples/sse/ItemStoreResource.java | 7 +- .../sse/ServerSentEventsResource.java | 3 + .../main/java/jakarta/ws/rs/SeBootstrap.java | 136 +++++- .../jakarta/ws/rs/client/ClientBuilder.java | 40 +- .../ws/rs/client/ClientRequestContext.java | 52 ++- .../ws/rs/client/ClientResponseContext.java | 42 ++ .../java/jakarta/ws/rs/client/Entity.java | 12 +- .../jakarta/ws/rs/client/FactoryFinder.java | 56 ++- .../client/ResponseProcessingException.java | 5 +- .../ws/rs/container/CompletionCallback.java | 27 +- .../rs/container/ContainerRequestContext.java | 42 ++ .../container/ContainerResponseContext.java | 52 ++- .../java/jakarta/ws/rs/core/CacheControl.java | 12 +- .../main/java/jakarta/ws/rs/core/Cookie.java | 164 +++++++- .../java/jakarta/ws/rs/core/EntityPart.java | 398 ++++++++++++++++++ .../jakarta/ws/rs/core/GenericEntity.java | 3 +- .../java/jakarta/ws/rs/core/HttpHeaders.java | 52 ++- .../java/jakarta/ws/rs/core/MediaType.java | 4 + .../java/jakarta/ws/rs/core/NewCookie.java | 240 ++++++++++- .../java/jakarta/ws/rs/core/PathSegment.java | 3 +- .../java/jakarta/ws/rs/core/Response.java | 37 ++ .../main/java/jakarta/ws/rs/core/Variant.java | 14 +- .../java/jakarta/ws/rs/ext/FactoryFinder.java | 56 ++- .../jakarta/ws/rs/ext/RuntimeDelegate.java | 36 +- .../java/jakarta/ws/rs/sse/FactoryFinder.java | 56 ++- .../java/jakarta/ws/rs/sse/SseEventSink.java | 4 +- .../jakarta/ws/rs/sse/SseEventSource.java | 14 +- .../java/jakarta/ws/rs/SeBootstrapTest.java | 113 ++++- .../java/jakarta/ws/rs/core/CookieTest.java | 81 +++- .../ws/rs/core/NewCookieBuilderTest.java | 100 +++++ .../jakarta/ws/rs/core/NewCookieTest.java | 185 ++++++-- 44 files changed, 2042 insertions(+), 337 deletions(-) create mode 100644 examples/src/main/java/jaxrs/examples/multipart/MultipartClient.java create mode 100644 examples/src/main/java/jaxrs/examples/multipart/MultipartResource.java create mode 100644 examples/src/main/java/jaxrs/examples/multipart/package-info.java create mode 100644 jaxrs-api/src/main/java/jakarta/ws/rs/core/EntityPart.java create mode 100644 jaxrs-api/src/test/java/jakarta/ws/rs/core/NewCookieBuilderTest.java diff --git a/examples/src/main/java/jaxrs/examples/bootstrap/BasicJavaSeBootstrapExample.java b/examples/src/main/java/jaxrs/examples/bootstrap/BasicJavaSeBootstrapExample.java index fd1abc7d2..f090dcdb3 100644 --- a/examples/src/main/java/jaxrs/examples/bootstrap/BasicJavaSeBootstrapExample.java +++ b/examples/src/main/java/jaxrs/examples/bootstrap/BasicJavaSeBootstrapExample.java @@ -19,9 +19,6 @@ import java.net.URI; import jakarta.ws.rs.SeBootstrap; -import jakarta.ws.rs.SeBootstrap.Configuration; -import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.UriBuilder; /** * Basic Java SE bootstrap example. @@ -46,21 +43,12 @@ private BasicJavaSeBootstrapExample() { * @param args unused command line arguments * @throws InterruptedException when process is killed */ - public static final void main(final String[] args) throws InterruptedException { - final Application application = new HelloWorld(); - - final SeBootstrap.Configuration requestedConfiguration = SeBootstrap.Configuration.builder().build(); - - SeBootstrap.start(application, requestedConfiguration).thenAccept(instance -> { - Runtime.getRuntime() - .addShutdownHook(new Thread(() -> instance.stop() - .thenAccept(stopResult -> System.out.printf("Stop result: %s [Native stop result: %s].%n", - stopResult, stopResult.unwrap(Object.class))))); - - final Configuration actualConfigurarion = instance.configuration(); - final URI uri = UriBuilder.newInstance().scheme(actualConfigurarion.protocol().toLowerCase()) - .host(actualConfigurarion.host()).port(actualConfigurarion.port()) - .path(actualConfigurarion.rootPath()).build(); + public static void main(final String[] args) throws InterruptedException { + SeBootstrap.start(HelloWorld.class).thenAccept(instance -> { + instance.stopOnShutdown(stopResult -> + System.out.printf("Stop result: %s [Native stop result: %s].%n", stopResult, + stopResult.unwrap(Object.class))); + final URI uri = instance.configuration().baseUri(); System.out.printf("Instance %s running at %s [Native handle: %s].%n", instance, uri, instance.unwrap(Object.class)); System.out.println("Send SIGKILL to shutdown."); diff --git a/examples/src/main/java/jaxrs/examples/bootstrap/ClientAuthenticationJavaSeBootstrapExample.java b/examples/src/main/java/jaxrs/examples/bootstrap/ClientAuthenticationJavaSeBootstrapExample.java index 3a78496de..5972385fc 100644 --- a/examples/src/main/java/jaxrs/examples/bootstrap/ClientAuthenticationJavaSeBootstrapExample.java +++ b/examples/src/main/java/jaxrs/examples/bootstrap/ClientAuthenticationJavaSeBootstrapExample.java @@ -19,10 +19,7 @@ import java.net.URI; import jakarta.ws.rs.SeBootstrap; -import jakarta.ws.rs.SeBootstrap.Configuration; import jakarta.ws.rs.SeBootstrap.Configuration.SSLClientAuthentication; -import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.UriBuilder; /** * Java SE Bootstrap Example using TLS Client Authentication. @@ -62,22 +59,15 @@ private ClientAuthenticationJavaSeBootstrapExample() { * @param args unused command line arguments * @throws InterruptedException when process is killed */ - public static final void main(final String[] args) throws InterruptedException { - final Application application = new HelloWorld(); - + public static void main(final String[] args) throws InterruptedException { final SeBootstrap.Configuration requestedConfiguration = SeBootstrap.Configuration.builder().protocol("HTTPS") .sslClientAuthentication(SSLClientAuthentication.MANDATORY).build(); - SeBootstrap.start(application, requestedConfiguration).thenAccept(instance -> { - Runtime.getRuntime() - .addShutdownHook(new Thread(() -> instance.stop() - .thenAccept(stopResult -> System.out.printf("Stop result: %s [Native stop result: %s].%n", - stopResult, stopResult.unwrap(Object.class))))); - - final Configuration actualConfigurarion = instance.configuration(); - final URI uri = UriBuilder.newInstance().scheme(actualConfigurarion.protocol().toLowerCase()) - .host(actualConfigurarion.host()).port(actualConfigurarion.port()) - .path(actualConfigurarion.rootPath()).build(); + SeBootstrap.start(HelloWorld.class, requestedConfiguration).thenAccept(instance -> { + instance.stopOnShutdown(stopResult -> + System.out.printf("Stop result: %s [Native stop result: %s].%n", stopResult, + stopResult.unwrap(Object.class))); + final URI uri = instance.configuration().baseUri(); System.out.printf("Instance %s running at %s [Native handle: %s].%n", instance, uri, instance.unwrap(Object.class)); System.out.println("Send SIGKILL to shutdown."); diff --git a/examples/src/main/java/jaxrs/examples/bootstrap/ExplicitJavaSeBootstrapExample.java b/examples/src/main/java/jaxrs/examples/bootstrap/ExplicitJavaSeBootstrapExample.java index 62187fbf8..3f7df46e5 100644 --- a/examples/src/main/java/jaxrs/examples/bootstrap/ExplicitJavaSeBootstrapExample.java +++ b/examples/src/main/java/jaxrs/examples/bootstrap/ExplicitJavaSeBootstrapExample.java @@ -19,10 +19,8 @@ import java.net.URI; import jakarta.ws.rs.SeBootstrap; -import jakarta.ws.rs.SeBootstrap.Configuration; import jakarta.ws.rs.SeBootstrap.Configuration.SSLClientAuthentication; import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.UriBuilder; /** * Java SE bootstrap example with explicit configuration. @@ -56,7 +54,7 @@ private ExplicitJavaSeBootstrapExample() { * {@code NONE, OPTIONAL, MANDATORY}. * @throws InterruptedException when process is killed */ - public static final void main(final String[] args) throws InterruptedException { + public static void main(final String[] args) throws InterruptedException { final Application application = new HelloWorld(); final String protocol = args[0]; @@ -69,15 +67,10 @@ public static final void main(final String[] args) throws InterruptedException { .port(port).rootPath(rootPath).sslClientAuthentication(clientAuth).build(); SeBootstrap.start(application, requestedConfiguration).thenAccept(instance -> { - Runtime.getRuntime() - .addShutdownHook(new Thread(() -> instance.stop() - .thenAccept(stopResult -> System.out.printf("Stop result: %s [Native stop result: %s].%n", - stopResult, stopResult.unwrap(Object.class))))); - - final Configuration actualConfigurarion = instance.configuration(); - final URI uri = UriBuilder.newInstance().scheme(actualConfigurarion.protocol().toLowerCase()) - .host(actualConfigurarion.host()).port(actualConfigurarion.port()) - .path(actualConfigurarion.rootPath()).build(); + instance.stopOnShutdown(stopResult -> + System.out.printf("Stop result: %s [Native stop result: %s].%n", stopResult, + stopResult.unwrap(Object.class))); + final URI uri = instance.configuration().baseUri(); System.out.printf("Instance %s running at %s [Native handle: %s].%n", instance, uri, instance.unwrap(Object.class)); System.out.println("Send SIGKILL to shutdown."); diff --git a/examples/src/main/java/jaxrs/examples/bootstrap/ExternalConfigJavaSeBootstrapExample.java b/examples/src/main/java/jaxrs/examples/bootstrap/ExternalConfigJavaSeBootstrapExample.java index e02cf81eb..4642995de 100644 --- a/examples/src/main/java/jaxrs/examples/bootstrap/ExternalConfigJavaSeBootstrapExample.java +++ b/examples/src/main/java/jaxrs/examples/bootstrap/ExternalConfigJavaSeBootstrapExample.java @@ -18,13 +18,10 @@ import java.net.URI; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; - import jakarta.ws.rs.SeBootstrap; -import jakarta.ws.rs.SeBootstrap.Configuration; import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.UriBuilder; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; /** * Java SE bootstrap example utilizing an external configuration system. @@ -71,7 +68,7 @@ private ExternalConfigJavaSeBootstrapExample() { * @param args unused command line arguments * @throws InterruptedException when process is killed */ - public static final void main(final String[] args) throws InterruptedException { + public static void main(final String[] args) throws InterruptedException { final Application application = new HelloWorld(); final Config config = ConfigProvider.getConfig(); @@ -80,15 +77,10 @@ public static final void main(final String[] args) throws InterruptedException { .build(); SeBootstrap.start(application, requestedConfiguration).thenAccept(instance -> { - Runtime.getRuntime() - .addShutdownHook(new Thread(() -> instance.stop() - .thenAccept(stopResult -> System.out.printf("Stop result: %s [Native stop result: %s].%n", - stopResult, stopResult.unwrap(Object.class))))); - - final Configuration actualConfigurarion = instance.configuration(); - final URI uri = UriBuilder.newInstance().scheme(actualConfigurarion.protocol().toLowerCase()) - .host(actualConfigurarion.host()).port(actualConfigurarion.port()) - .path(actualConfigurarion.rootPath()).build(); + instance.stopOnShutdown(stopResult -> + System.out.printf("Stop result: %s [Native stop result: %s].%n", stopResult, + stopResult.unwrap(Object.class))); + final URI uri = instance.configuration().baseUri(); System.out.printf("Instance %s running at %s [Native handle: %s].%n", instance, uri, instance.unwrap(Object.class)); System.out.println("Send SIGKILL to shutdown."); diff --git a/examples/src/main/java/jaxrs/examples/bootstrap/HttpsJavaSeBootstrapExample.java b/examples/src/main/java/jaxrs/examples/bootstrap/HttpsJavaSeBootstrapExample.java index 481edd01d..2dc86205d 100644 --- a/examples/src/main/java/jaxrs/examples/bootstrap/HttpsJavaSeBootstrapExample.java +++ b/examples/src/main/java/jaxrs/examples/bootstrap/HttpsJavaSeBootstrapExample.java @@ -19,9 +19,7 @@ import java.net.URI; import jakarta.ws.rs.SeBootstrap; -import jakarta.ws.rs.SeBootstrap.Configuration; import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.UriBuilder; /** * Java SE bootstrap example using HTTPS. @@ -57,21 +55,16 @@ private HttpsJavaSeBootstrapExample() { * @param args unused command line arguments * @throws InterruptedException when process is killed */ - public static final void main(final String[] args) throws InterruptedException { + public static void main(final String[] args) throws InterruptedException { final Application application = new HelloWorld(); final SeBootstrap.Configuration requestedConfiguration = SeBootstrap.Configuration.builder().protocol("HTTPS").build(); SeBootstrap.start(application, requestedConfiguration).thenAccept(instance -> { - Runtime.getRuntime() - .addShutdownHook(new Thread(() -> instance.stop() - .thenAccept(stopResult -> System.out.printf("Stop result: %s [Native stop result: %s].%n", - stopResult, stopResult.unwrap(Object.class))))); - - final Configuration actualConfigurarion = instance.configuration(); - final URI uri = UriBuilder.newInstance().scheme(actualConfigurarion.protocol().toLowerCase()) - .host(actualConfigurarion.host()).port(actualConfigurarion.port()) - .path(actualConfigurarion.rootPath()).build(); + instance.stopOnShutdown(stopResult -> + System.out.printf("Stop result: %s [Native stop result: %s].%n", stopResult, + stopResult.unwrap(Object.class))); + final URI uri = instance.configuration().baseUri(); System.out.printf("Instance %s running at %s [Native handle: %s].%n", instance, uri, instance.unwrap(Object.class)); System.out.println("Send SIGKILL to shutdown."); diff --git a/examples/src/main/java/jaxrs/examples/bootstrap/NativeJavaSeBootstrapExample.java b/examples/src/main/java/jaxrs/examples/bootstrap/NativeJavaSeBootstrapExample.java index b64b8dc3d..e36f5f361 100644 --- a/examples/src/main/java/jaxrs/examples/bootstrap/NativeJavaSeBootstrapExample.java +++ b/examples/src/main/java/jaxrs/examples/bootstrap/NativeJavaSeBootstrapExample.java @@ -19,9 +19,7 @@ import java.net.URI; import jakarta.ws.rs.SeBootstrap; -import jakarta.ws.rs.SeBootstrap.Configuration; import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.UriBuilder; /** * Java SE bootstrap example demonstrating the use of native properties. @@ -56,7 +54,7 @@ private NativeJavaSeBootstrapExample() { * @throws InterruptedException when process is killed * @throws ClassNotFoundException when Jersey's Grizzly backend is not on the classpath */ - public static final void main(final String[] args) throws InterruptedException, ClassNotFoundException { + public static void main(final String[] args) throws InterruptedException, ClassNotFoundException { final Application application = new HelloWorld(); final SeBootstrap.Configuration requestedConfiguration = SeBootstrap.Configuration.builder() @@ -65,15 +63,10 @@ public static final void main(final String[] args) throws InterruptedException, .build(); SeBootstrap.start(application, requestedConfiguration).thenAccept(instance -> { - Runtime.getRuntime() - .addShutdownHook(new Thread(() -> instance.stop() - .thenAccept(stopResult -> System.out.printf("Stop result: %s [Native stop result: %s].%n", - stopResult, stopResult.unwrap(Object.class))))); - - final Configuration actualConfigurarion = instance.configuration(); - final URI uri = UriBuilder.newInstance().scheme(actualConfigurarion.protocol().toLowerCase()) - .host(actualConfigurarion.host()).port(actualConfigurarion.port()) - .path(actualConfigurarion.rootPath()).build(); + instance.stopOnShutdown(stopResult -> + System.out.printf("Stop result: %s [Native stop result: %s].%n", stopResult, + stopResult.unwrap(Object.class))); + final URI uri = instance.configuration().baseUri(); System.out.printf("Instance %s running at %s [Native handle: %s].%n", instance, uri, instance.unwrap(Object.class)); System.out.println("Send SIGKILL to shutdown."); diff --git a/examples/src/main/java/jaxrs/examples/bootstrap/PropertyProviderJavaSeBootstrapExample.java b/examples/src/main/java/jaxrs/examples/bootstrap/PropertyProviderJavaSeBootstrapExample.java index af0302fd7..ee213d134 100644 --- a/examples/src/main/java/jaxrs/examples/bootstrap/PropertyProviderJavaSeBootstrapExample.java +++ b/examples/src/main/java/jaxrs/examples/bootstrap/PropertyProviderJavaSeBootstrapExample.java @@ -18,13 +18,10 @@ import java.net.URI; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; - import jakarta.ws.rs.SeBootstrap; -import jakarta.ws.rs.SeBootstrap.Configuration; import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.UriBuilder; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; /** * Java SE bootstrap example utilizing a property provider. @@ -69,7 +66,7 @@ private PropertyProviderJavaSeBootstrapExample() { * @param args unused command line arguments * @throws InterruptedException when process is killed */ - public static final void main(final String[] args) throws InterruptedException { + public static void main(final String[] args) throws InterruptedException { final Application application = new HelloWorld(); final Config config = ConfigProvider.getConfig(); @@ -78,15 +75,10 @@ public static final void main(final String[] args) throws InterruptedException { .protocol("HTTPS").build(); SeBootstrap.start(application, requestedConfiguration).thenAccept(instance -> { - Runtime.getRuntime() - .addShutdownHook(new Thread(() -> instance.stop() - .thenAccept(stopResult -> System.out.printf("Stop result: %s [Native stop result: %s].%n", - stopResult, stopResult.unwrap(Object.class))))); - - final Configuration actualConfigurarion = instance.configuration(); - final URI uri = UriBuilder.newInstance().scheme(actualConfigurarion.protocol().toLowerCase()) - .host(actualConfigurarion.host()).port(actualConfigurarion.port()) - .path(actualConfigurarion.rootPath()).build(); + instance.stopOnShutdown(stopResult -> + System.out.printf("Stop result: %s [Native stop result: %s].%n", stopResult, + stopResult.unwrap(Object.class))); + final URI uri = instance.configuration().baseUri(); System.out.printf("Instance %s running at %s [Native handle: %s].%n", instance, uri, instance.unwrap(Object.class)); System.out.println("Send SIGKILL to shutdown."); diff --git a/examples/src/main/java/jaxrs/examples/bootstrap/TlsJavaSeBootstrapExample.java b/examples/src/main/java/jaxrs/examples/bootstrap/TlsJavaSeBootstrapExample.java index 32ee38a89..88a5f1b32 100644 --- a/examples/src/main/java/jaxrs/examples/bootstrap/TlsJavaSeBootstrapExample.java +++ b/examples/src/main/java/jaxrs/examples/bootstrap/TlsJavaSeBootstrapExample.java @@ -27,9 +27,7 @@ import java.security.KeyStore; import jakarta.ws.rs.SeBootstrap; -import jakarta.ws.rs.SeBootstrap.Configuration; import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.UriBuilder; /** * Java SE bootstrap example using TLS customization. @@ -56,7 +54,7 @@ private TlsJavaSeBootstrapExample() { * @throws IOException in case file access fails * @throws InterruptedException when process is killed */ - public static final void main(final String[] args) + public static void main(final String[] args) throws GeneralSecurityException, IOException, InterruptedException { final Application application = new HelloWorld(); @@ -75,15 +73,10 @@ public static final void main(final String[] args) .sslContext(sslContext).build(); SeBootstrap.start(application, requestedConfiguration).thenAccept(instance -> { - Runtime.getRuntime() - .addShutdownHook(new Thread(() -> instance.stop() - .thenAccept(stopResult -> System.out.printf("Stop result: %s [Native stop result: %s].%n", - stopResult, stopResult.unwrap(Object.class))))); - - final Configuration actualConfigurarion = instance.configuration(); - final URI uri = UriBuilder.newInstance().scheme(actualConfigurarion.protocol().toLowerCase()) - .host(actualConfigurarion.host()).port(actualConfigurarion.port()) - .path(actualConfigurarion.rootPath()).build(); + instance.stopOnShutdown(stopResult -> + System.out.printf("Stop result: %s [Native stop result: %s].%n", stopResult, + stopResult.unwrap(Object.class))); + final URI uri = instance.configuration().baseUri(); System.out.printf("Instance %s running at %s [Native handle: %s].%n", instance, uri, instance.unwrap(Object.class)); System.out.println("Send SIGKILL to shutdown."); diff --git a/examples/src/main/java/jaxrs/examples/bootstrap/package-info.java b/examples/src/main/java/jaxrs/examples/bootstrap/package-info.java index 6515d1f3d..75ad1856f 100644 --- a/examples/src/main/java/jaxrs/examples/bootstrap/package-info.java +++ b/examples/src/main/java/jaxrs/examples/bootstrap/package-info.java @@ -17,6 +17,6 @@ * * @author Markus KARG (markus@headcrashing.eu) * @since 3.1 - * @see jakarta.ws.rs.SeBootstrap; + * @see jakarta.ws.rs.SeBootstrap */ package jaxrs.examples.bootstrap; diff --git a/examples/src/main/java/jaxrs/examples/link/LinkExamples.java b/examples/src/main/java/jaxrs/examples/link/LinkExamples.java index 4f725ef94..976e68b2a 100644 --- a/examples/src/main/java/jaxrs/examples/link/LinkExamples.java +++ b/examples/src/main/java/jaxrs/examples/link/LinkExamples.java @@ -51,7 +51,7 @@ public Response example2() { * 1-step process: Build Response and add a link directly to it using either a String or a URI. * * @return response. - * @throws URISyntaxException + * @throws URISyntaxException if URI is invalid */ public Response example3() throws URISyntaxException { Response r; diff --git a/examples/src/main/java/jaxrs/examples/multipart/MultipartClient.java b/examples/src/main/java/jaxrs/examples/multipart/MultipartClient.java new file mode 100644 index 000000000..602b5d16e --- /dev/null +++ b/examples/src/main/java/jaxrs/examples/multipart/MultipartClient.java @@ -0,0 +1,71 @@ +/******************************************************************* +* Copyright (c) 2021 Eclipse Foundation +* +* This specification document is made available under the terms +* of the Eclipse Foundation Specification License v1.0, which is +* available at https://www.eclipse.org/legal/efsl.php. +*******************************************************************/ +package jaxrs.examples.multipart; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +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.EntityPart; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + + +public class MultipartClient { + private static final Logger LOG = Logger.getLogger(MultipartClient.class.getName()); + + public boolean sendPdfs(Path dir) throws IOException { + List parts = Files.list(dir).map(this::toPart).collect(Collectors.toList()); + Client client = ClientBuilder.newClient(); + WebTarget target = client.target("http://localhost:9080/multipart?dirName=abc"); + Entity> entity = Entity.entity(parts, MediaType.MULTIPART_FORM_DATA); + Response response = target.request().post(entity); + return response.getStatus() == 200; + } + + private EntityPart toPart(Path file) { + String filename = file.getFileName().toString(); + try { + return EntityPart.withName(filename) + .content(filename, Files.newInputStream(file)) + .mediaType("application/pdf") + .build(); + } catch (IOException ioex) { + LOG.log(Level.WARNING, "Failed to process file {0}", file); + return null; + } + } + + public List retrievePdfs(String remoteDirName) throws IOException { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target("http://localhost:9080/multipart").queryParam("dirName", remoteDirName); + Response response = target.request(MediaType.MULTIPART_FORM_DATA).get(); + List parts = response.readEntity(new GenericType>() { }); + return parts.stream().map(part -> { + try (InputStream is = part.getContent()) { + Path file = Files.createFile(Paths.get(part.getFileName().orElse(part.getName() + ".pdf"))); + Files.copy(is, file); + return file; + } catch (IOException ioex) { + LOG.log(Level.WARNING, "Failed to process attachment part {0}", part); + return null; + } + }).collect(Collectors.toList()); + } +} diff --git a/examples/src/main/java/jaxrs/examples/multipart/MultipartResource.java b/examples/src/main/java/jaxrs/examples/multipart/MultipartResource.java new file mode 100644 index 000000000..4e3862670 --- /dev/null +++ b/examples/src/main/java/jaxrs/examples/multipart/MultipartResource.java @@ -0,0 +1,96 @@ +/******************************************************************* +* Copyright (c) 2021 Eclipse Foundation +* +* This specification document is made available under the terms +* of the Eclipse Foundation Specification License v1.0, which is +* available at https://www.eclipse.org/legal/efsl.php. +*******************************************************************/ +package jaxrs.examples.multipart; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.NotSupportedException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.EntityPart; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/multipart") +public class MultipartResource { + + private static final String PDF_ROOT_DIR = System.getProperty("pdf.root.dir", "/myPDFs"); + + @GET + @Produces(MediaType.MULTIPART_FORM_DATA) + public List getAllPdfFilesInDirectory(@QueryParam("dirName") String dirName) throws IOException { + File dir = getDirectoryIfExists(dirName); + List parts = new ArrayList<>(); + for (File f : dir.listFiles()) { + parts.add(EntityPart.withFileName(f.getName()).content(new FileInputStream(f)) + .mediaType("application/pdf") + .build()); + } + return parts; + } + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response postNewPdfFiles(@QueryParam("dirName") String dirName, List parts) throws IOException { + File dir = getDirectoryIfExists(dirName); + for (EntityPart p : parts) { + File f = new File(dir, p.getFileName().orElseThrow(BadRequestException::new)); + if (f.exists()) { + throw new WebApplicationException(409); // 409 CONFLICT + } + try (InputStream content = p.getContent()) { + Files.copy(content, f.toPath()); + } + } + return Response.ok().build(); + } + + @Path("/apply") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response applyForJob(@FormParam("name") String name, + @FormParam("recentPhoto") InputStream photoStream, + @FormParam("resume") EntityPart resume) { + + String resumeFileName = resume.getFileName().orElseThrow(NotSupportedException::new); + + if (resumeFileName.toLowerCase().endsWith(".pdf")) { + processPdfResume(resume.getContent()); + } + // handle other file types, like Word docs, etc. + + // process new application... + return Response.ok("Application received").build(); + } + + private File getDirectoryIfExists(String dirName) { + File dir = new File(PDF_ROOT_DIR, dirName); + if (!dir.exists()) { + throw new NotFoundException("dirName, " + dirName + ", does not exist"); + } + return dir; + } + + private void processPdfResume(InputStream is) { + // ... + } +} diff --git a/examples/src/main/java/jaxrs/examples/multipart/package-info.java b/examples/src/main/java/jaxrs/examples/multipart/package-info.java new file mode 100644 index 000000000..ca0094526 --- /dev/null +++ b/examples/src/main/java/jaxrs/examples/multipart/package-info.java @@ -0,0 +1,4 @@ +/** + * Async examples. + */ +package jaxrs.examples.multipart; diff --git a/examples/src/main/java/jaxrs/examples/sse/ItemStoreResource.java b/examples/src/main/java/jaxrs/examples/sse/ItemStoreResource.java index 3e7c61f68..6e28b8278 100644 --- a/examples/src/main/java/jaxrs/examples/sse/ItemStoreResource.java +++ b/examples/src/main/java/jaxrs/examples/sse/ItemStoreResource.java @@ -17,8 +17,6 @@ import java.util.logging.Level; import java.util.logging.Logger; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.FormParam; @@ -36,6 +34,9 @@ import jakarta.ws.rs.sse.SseBroadcaster; import jakarta.ws.rs.sse.SseEventSink; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + /** * A resource for storing named items. * @@ -83,7 +84,7 @@ public String listItems() { } /** - * Receive & process commands sent by the test client that control the internal resource state. + * Receive & process commands sent by the test client that control the internal resource state. *

* Following is the list of recognized commands: *

    diff --git a/examples/src/main/java/jaxrs/examples/sse/ServerSentEventsResource.java b/examples/src/main/java/jaxrs/examples/sse/ServerSentEventsResource.java index 903fd29c2..af85ef861 100644 --- a/examples/src/main/java/jaxrs/examples/sse/ServerSentEventsResource.java +++ b/examples/src/main/java/jaxrs/examples/sse/ServerSentEventsResource.java @@ -96,7 +96,10 @@ public void startDomain(@PathParam("id") final String id, SseEventSink sseEventS sseEventSink.close(); } catch (final InterruptedException e) { e.printStackTrace(); + } catch (IOException ioe) { + //handle I/O error } + }); } } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/SeBootstrap.java b/jaxrs-api/src/main/java/jakarta/ws/rs/SeBootstrap.java index afbef44e5..5cf909305 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/SeBootstrap.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/SeBootstrap.java @@ -16,12 +16,16 @@ package jakarta.ws.rs; -import javax.net.ssl.SSLContext; +import java.net.URI; import java.util.Optional; import java.util.concurrent.CompletionStage; import java.util.function.BiFunction; +import java.util.function.Consumer; + +import javax.net.ssl.SSLContext; import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.ext.RuntimeDelegate; /** @@ -139,6 +143,65 @@ static CompletionStage start(final Application application, final Conf return RuntimeDelegate.getInstance().bootstrap(application, configuration); } + /** + * Starts the provided application using a default configuration. + * + *

    + * This method is intended to be used in Java SE environments only. The outcome of invocations in Jakarta EE container + * environments is undefined. + *

    + * + * @param application The application to start up. + * @return {@code CompletionStage} (possibly asynchronously) producing handle of the running application + * {@link SeBootstrap.Instance instance}. + * @see Configuration + * @since 3.1 + */ + static CompletionStage start(final Application application) { + Configuration configuration = Configuration.builder().build(); + return start(application, configuration); + } + + /** + * Starts the provided application using the specified configuration. Creates application instance + * from class using default constructor. Injection is not supported. + * + *

    + * This method is intended to be used in Java SE environments only. The outcome of invocations in Jakarta EE container + * environments is undefined. + *

    + * + * @param clazz The application class. + * @param configuration Provides information needed for bootstrapping the application. + * @return {@code CompletionStage} (possibly asynchronously) producing handle of the running application + * {@link SeBootstrap.Instance instance}. + * @see Configuration + * @since 3.1 + */ + static CompletionStage start(final Class clazz, final Configuration configuration) { + return RuntimeDelegate.getInstance().bootstrap(clazz, configuration); + } + + /** + * Starts the provided application using a default configuration. Creates application instance + * from class using default constructor. Injection is not supported. + * + *

    + * This method is intended to be used in Java SE environments only. The outcome of invocations in Jakarta EE container + * environments is undefined. + *

    + * + * @param clazz The application class. + * @return {@code CompletionStage} (possibly asynchronously) producing handle of the running application + * {@link SeBootstrap.Instance instance}. + * @see Configuration + * @since 3.1 + */ + static CompletionStage start(final Class clazz) { + Configuration configuration = Configuration.builder().build(); + return start(clazz, configuration); + } + /** * Provides information needed by the JAX-RS implementation for bootstrapping an application. *

    @@ -150,7 +213,7 @@ static CompletionStage start(final Application application, final Conf * @author Markus KARG (markus@headcrashing.eu) * @since 3.1 */ - public static interface Configuration { + interface Configuration { /** * Configuration key for the protocol an application is bound to. @@ -164,7 +227,7 @@ public static interface Configuration { * * @since 3.1 */ - static final String PROTOCOL = "jakarta.ws.rs.SeBootstrap.Protocol"; + String PROTOCOL = "jakarta.ws.rs.SeBootstrap.Protocol"; /** * Configuration key for the hostname or IP address an application is bound to. @@ -181,7 +244,7 @@ public static interface Configuration { * * @since 3.1 */ - static final String HOST = "jakarta.ws.rs.SeBootstrap.Host"; + String HOST = "jakarta.ws.rs.SeBootstrap.Host"; /** * Configuration key for the TCP port an application is bound to. @@ -198,7 +261,7 @@ public static interface Configuration { * * @since 3.1 */ - static final String PORT = "jakarta.ws.rs.SeBootstrap.Port"; + String PORT = "jakarta.ws.rs.SeBootstrap.Port"; /** * Configuration key for the root path an application is bound to. @@ -208,7 +271,7 @@ public static interface Configuration { * * @since 3.1 */ - static final String ROOT_PATH = "jakarta.ws.rs.SeBootstrap.RootPath"; + String ROOT_PATH = "jakarta.ws.rs.SeBootstrap.RootPath"; /** * Configuration key for the secure socket configuration to be used. @@ -218,7 +281,7 @@ public static interface Configuration { * * @since 3.1 */ - static final String SSL_CONTEXT = "jakarta.ws.rs.SeBootstrap.SSLContext"; + String SSL_CONTEXT = "jakarta.ws.rs.SeBootstrap.SSLContext"; /** * Configuration key for the secure socket client authentication policy. @@ -232,7 +295,7 @@ public static interface Configuration { * * @since 3.1 */ - static final String SSL_CLIENT_AUTHENTICATION = "jakarta.ws.rs.SeBootstrap.SSLClientAuthentication"; + String SSL_CLIENT_AUTHENTICATION = "jakarta.ws.rs.SeBootstrap.SSLClientAuthentication"; /** * Secure socket client authentication policy @@ -246,7 +309,7 @@ public static interface Configuration { * @author Markus KARG (markus@headcrashing.eu) * @since 3.1 */ - public enum SSLClientAuthentication { + enum SSLClientAuthentication { /** * Server will not request client authentication. @@ -275,14 +338,14 @@ public enum SSLClientAuthentication { * * @since 3.1 */ - static final int FREE_PORT = 0; + int FREE_PORT = 0; /** * Special value for {@link #PORT} property indicating that the implementation MUST use its default port. * * @since 3.1 */ - static final int DEFAULT_PORT = -1; + int DEFAULT_PORT = -1; /** * Returns the value of the property with the given name, or {@code null} if there is no property of that name. @@ -360,7 +423,7 @@ default int port() { * Same as if calling {@link #property(String) (String) property(ROOT_PATH)}. *

    * - * @return root path to be used, e. g. {@code "/"}. + * @return root path to be used, e.g. {@code "/"}. * @throws ClassCastException if root path is not a {@link String}. * @see SeBootstrap.Configuration#ROOT_PATH * @since 3.1 @@ -399,6 +462,28 @@ default SSLClientAuthentication sslClientAuthentication() { return (SSLClientAuthentication) property(SSL_CLIENT_AUTHENTICATION); } + /** + * Returns a {@link UriBuilder} that includes the application root path. + * + * @return a {@link UriBuilder} for the application. + * @since 3.1 + */ + default UriBuilder baseUriBuilder() { + return UriBuilder.newInstance().scheme(protocol().toLowerCase()) + .host(host()).port(port()).path(rootPath()); + } + + /** + * Convenience method that returns a built the {@link URI} for the application. + * + * @return a built {@link URI} for the application. + * @see Configuration#baseUriBuilder() + * @since 3.1 + */ + default URI baseUri() { + return baseUriBuilder().build(); + } + /** * Creates a new bootstrap configuration builder instance. * @@ -407,7 +492,7 @@ default SSLClientAuthentication sslClientAuthentication() { */ static Builder builder() { return RuntimeDelegate.getInstance().createConfigurationBuilder(); - }; + } /** * Builder for bootstrap {@link Configuration}. @@ -415,7 +500,7 @@ static Builder builder() { * @author Markus KARG (markus@headcrashing.eu) * @since 3.1 */ - static interface Builder { + interface Builder { /** * Builds a bootstrap configuration instance from the provided property values. @@ -575,7 +660,7 @@ default Builder from(Object externalConfig) { * @author Markus KARG (markus@headcrashing.eu) * @since 3.1 */ - public interface Instance { + interface Instance { /** * Provides access to the configuration actually used by the implementation used to create this instance. @@ -589,7 +674,7 @@ public interface Instance { * @return The configuration actually used to create this instance. * @since 3.1 */ - public Configuration configuration(); + Configuration configuration(); /** * Initiate immediate shutdown of running application instance. @@ -597,7 +682,7 @@ public interface Instance { * @return {@code CompletionStage} asynchronously shutting down this application instance. * @since 3.1 */ - public CompletionStage stop(); + CompletionStage stop(); /** * Result of stopping the application instance. @@ -605,7 +690,7 @@ public interface Instance { * @author Markus KARG (markus@headcrashing.eu) * @since 3.1 */ - public interface StopResult { + interface StopResult { /** * Provides access to the wrapped native shutdown result. @@ -621,7 +706,7 @@ public interface StopResult { * @throws ClassCastException if the result is not {@code null} or is not assignable to the type {@code T}. * @since 3.1 */ - public T unwrap(Class nativeClass); + T unwrap(Class nativeClass); } /** @@ -637,7 +722,18 @@ public interface StopResult { * @throws ClassCastException if the handle is not {@code null} and is not assignable to the type {@code T}. * @since 3.1 */ - public T unwrap(Class nativeClass); + T unwrap(Class nativeClass); + + /** + * Registers a consumer for a {@link StopResult} which will be executed in a new thread + * during the JVM shutdown phase. + * + * @param consumer The consumer. + * @since 3.1 + */ + default void stopOnShutdown(Consumer consumer) { + Runtime.getRuntime().addShutdownHook(new Thread(() -> stop().thenAccept(consumer))); + } } } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientBuilder.java b/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientBuilder.java index b07c2ba6f..efe304d4d 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientBuilder.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientBuilder.java @@ -19,7 +19,9 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import java.net.URL; +import java.security.AccessController; import java.security.KeyStore; +import java.security.PrivilegedAction; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -59,16 +61,14 @@ public static ClientBuilder newBuilder() { try { Object delegate = FactoryFinder.find(JAXRS_DEFAULT_CLIENT_BUILDER_PROPERTY, ClientBuilder.class); if (!(delegate instanceof ClientBuilder)) { - Class pClass = ClientBuilder.class; - String classnameAsResource = pClass.getName().replace('.', '/') + ".class"; - ClassLoader loader = pClass.getClassLoader(); - if (loader == null) { - loader = ClassLoader.getSystemClassLoader(); + final CreateErrorMessageAction action = new CreateErrorMessageAction(delegate); + final String errorMessage; + if (System.getSecurityManager() == null) { + errorMessage = action.run(); + } else { + errorMessage = AccessController.doPrivileged(action); } - URL targetTypeURL = loader.getResource(classnameAsResource); - throw new LinkageError("ClassCastException: attempting to cast" - + delegate.getClass().getClassLoader().getResource(classnameAsResource) - + " to " + targetTypeURL); + throw new LinkageError(errorMessage); } return (ClientBuilder) delegate; } catch (Exception ex) { @@ -271,4 +271,26 @@ public ClientBuilder keyStore(final KeyStore keyStore, final String password) { * @return a new client instance. */ public abstract Client build(); + + private static final class CreateErrorMessageAction implements PrivilegedAction { + private final Object delegate; + + private CreateErrorMessageAction(final Object delegate) { + this.delegate = delegate; + } + + @Override + public String run() { + Class pClass = ClientBuilder.class; + String classnameAsResource = pClass.getName().replace('.', '/') + ".class"; + ClassLoader loader = pClass.getClassLoader(); + if (loader == null) { + loader = ClassLoader.getSystemClassLoader(); + } + URL targetTypeURL = loader.getResource(classnameAsResource); + return "ClassCastException: attempting to cast" + + delegate.getClass().getClassLoader().getResource(classnameAsResource) + + " to " + targetTypeURL; + } + } } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientRequestContext.java b/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientRequestContext.java index f25280156..6bf01a0ba 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientRequestContext.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientRequestContext.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Predicate; import jakarta.ws.rs.core.Configuration; import jakarta.ws.rs.core.Cookie; @@ -179,7 +180,7 @@ public default boolean hasProperty(String name) { /** * Get a message header as a single string value. * - * Each single header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one + * Each single non-string header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one * is available via {@link jakarta.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value * class or using its {@code toString} method if a header delegate is not available. * @@ -192,6 +193,55 @@ public default boolean hasProperty(String name) { */ public String getHeaderString(String name); + /** + * Checks whether a header with a specific name and value (or item of the token-separated value list) exists. + * + * Each single non-string header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one + * is available via {@link jakarta.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value + * class or using its {@code toString} method if a header delegate is not available. + * + *

    + * For example: {@code containsHeaderString("cache-control", ",", "no-store"::equalsIgnoreCase)} will return {@code true} if + * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value + * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform} + * (missing comma), or the value {@code no - store} (whitespace within value). + * + * @param name the message header. + * @param valueSeparatorRegex Separates the header value into single values. {@code null} does not split. + * @param valuePredicate value must fulfil this predicate. + * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value + * matching the predicate, or having at least one whitespace-trimmed single value in a token-separated list of single values. + * @see #getHeaders() + * @see #getHeaderString(String) + * @since 4.0 + */ + public boolean containsHeaderString(String name, String valueSeparatorRegex, Predicate valuePredicate); + + /** + * Checks whether a header with a specific name and value (or item of the comma-separated value list) exists. + * + * Each single non-string header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one + * is available via {@link jakarta.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value + * class or using its {@code toString} method if a header delegate is not available. + * + *

    + * For example: {@code containsHeaderString("cache-control", "no-store"::equalsIgnoreCase)} will return {@code true} if + * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value + * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform} + * (missing comma), or the value {@code no - store} (whitespace within value). + * + * @param name the message header. + * @param valuePredicate value must fulfil this predicate. + * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value + * matching the predicate, or having at least one whitespace-trimmed single value in a comma-separated list of single values. + * @see #getHeaders() + * @see #getHeaderString(String) + * @since 4.0 + */ + public default boolean containsHeaderString(String name, Predicate valuePredicate) { + return containsHeaderString(name, ",", valuePredicate); + } + /** * Get message date. * diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientResponseContext.java b/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientResponseContext.java index 87feabdd1..756e4289d 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientResponseContext.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/client/ClientResponseContext.java @@ -22,6 +22,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import jakarta.ws.rs.core.EntityTag; import jakarta.ws.rs.core.Link; @@ -88,6 +89,47 @@ public interface ClientResponseContext { */ public String getHeaderString(String name); + /** + * Checks whether a header with a specific name and value (or item of the token-separated value list) exists. + * + *

    + * For example: {@code containsHeaderString("cache-control", ",", "no-store"::equalsIgnoreCase)} will return {@code true} if + * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value + * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform} + * (missing comma), or the value {@code no - store} (whitespace within value). + * + * @param name the message header. + * @param valueSeparatorRegex Separates the header value into single values. {@code null} does not split. + * @param valuePredicate value must fulfil this predicate. + * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value + * matching the predicate, or having at least one whitespace-trimmed single value in a token-separated list of single values. + * @see #getHeaders() + * @see #getHeaderString(String) + * @since 4.0 + */ + public boolean containsHeaderString(String name, String valueSeparatorRegex, Predicate valuePredicate); + + /** + * Checks whether a header with a specific name and value (or item of the comma-separated value list) exists. + * + *

    + * For example: {@code containsHeaderString("cache-control", "no-store"::equalsIgnoreCase)} will return {@code true} if + * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value + * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform} + * (missing comma), or the value {@code no - store} (whitespace within value). + * + * @param name the message header. + * @param valuePredicate value must fulfil this predicate. + * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value + * matching the predicate, or having at least one whitespace-trimmed single value in a comma-separated list of single values. + * @see #getHeaders() + * @see #getHeaderString(String) + * @since 4.0 + */ + public default boolean containsHeaderString(String name, Predicate valuePredicate) { + return containsHeaderString(name, ",", valuePredicate); + } + /** * Get the allowed HTTP methods from the Allow HTTP header. * diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/client/Entity.java b/jaxrs-api/src/main/java/jakarta/ws/rs/client/Entity.java index df0d85fc8..4ec8984b6 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/client/Entity.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/client/Entity.java @@ -19,6 +19,7 @@ import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Locale; +import java.util.Objects; import jakarta.ws.rs.core.Form; import jakarta.ws.rs.core.MediaType; @@ -260,13 +261,13 @@ public boolean equals(final Object o) { Entity entity1 = (Entity) o; - if (!Arrays.equals(annotations, entity1.annotations)) { + if (!Objects.equals(annotations, entity1.annotations)) { return false; } - if (entity != null ? !entity.equals(entity1.entity) : entity1.entity != null) { + if (!Objects.equals(entity, entity1.entity)) { return false; } - if (variant != null ? !variant.equals(entity1.variant) : entity1.variant != null) { + if (!Objects.equals(variant, entity1.variant)) { return false; } @@ -275,10 +276,7 @@ public boolean equals(final Object o) { @Override public int hashCode() { - int result = entity != null ? entity.hashCode() : 0; - result = 31 * result + (variant != null ? variant.hashCode() : 0); - result = 31 * result + Arrays.hashCode(annotations); - return result; + return Objects.hash(annotations, entity, variant); } @Override diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/client/FactoryFinder.java b/jaxrs-api/src/main/java/jakarta/ws/rs/client/FactoryFinder.java index 9801cd87b..74deb0599 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/client/FactoryFinder.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/client/FactoryFinder.java @@ -21,9 +21,7 @@ import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedAction; -import java.util.Iterator; import java.util.Properties; -import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; @@ -45,6 +43,11 @@ private FactoryFinder() { } static ClassLoader getContextClassLoader() { + // For performance reasons, check if a security manager is installed. If not there is no need to use a + // privileged action. + if (System.getSecurityManager() == null) { + return Thread.currentThread().getContextClassLoader(); + } return AccessController.doPrivileged((PrivilegedAction) () -> { ClassLoader cl = null; try { @@ -107,24 +110,16 @@ private static Object newInstance(final String className, final ClassLoader clas static Object find(final String factoryId, final Class service) throws ClassNotFoundException { ClassLoader classLoader = getContextClassLoader(); - try { - Iterator iterator = ServiceLoader.load(service, FactoryFinder.getContextClassLoader()).iterator(); - - if (iterator.hasNext()) { - return iterator.next(); - } - } catch (Exception | ServiceConfigurationError ex) { - LOGGER.log(Level.FINER, "Failed to load service " + factoryId + ".", ex); + // First try the TCCL + Object result = findFirstService(factoryId, classLoader, service); + if (result != null) { + return result; } - try { - Iterator iterator = ServiceLoader.load(service, FactoryFinder.class.getClassLoader()).iterator(); - - if (iterator.hasNext()) { - return iterator.next(); - } - } catch (Exception | ServiceConfigurationError ex) { - LOGGER.log(Level.FINER, "Failed to load service " + factoryId + ".", ex); + // Next try the class loader from the FactoryFinder + result = findFirstService(factoryId, getClassLoader(), service); + if (result != null) { + return result; } // try to read from $java.home/lib/jaxrs.properties @@ -168,4 +163,29 @@ static Object find(final String factoryId, final Class service) throws Cl throw new ClassNotFoundException( "Provider for " + factoryId + " cannot be found", null); } + + private static ClassLoader getClassLoader() { + if (System.getSecurityManager() == null) { + return FactoryFinder.class.getClassLoader(); + } + return AccessController.doPrivileged((PrivilegedAction) FactoryFinder.class::getClassLoader); + } + + private static T findFirstService(final String factoryId, final ClassLoader cl, final Class service) { + final PrivilegedAction action = () -> { + try { + final ServiceLoader loader = ServiceLoader.load(service, cl); + if (loader.iterator().hasNext()) { + return loader.iterator().next(); + } + } catch (Exception e) { + LOGGER.log(Level.FINER, "Failed to load service " + factoryId + ".", e); + } + return null; + }; + if (System.getSecurityManager() == null) { + return action.run(); + } + return AccessController.doPrivileged(action); + } } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/client/ResponseProcessingException.java b/jaxrs-api/src/main/java/jakarta/ws/rs/client/ResponseProcessingException.java index 8c24cf4a5..e9b25d51b 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/client/ResponseProcessingException.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/client/ResponseProcessingException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2021 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 @@ -31,6 +31,9 @@ public class ResponseProcessingException extends ProcessingException { private static final long serialVersionUID = -4923161617935731839L; + /** + * The response instance for which the processing failed. + */ private final Response response; /** diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/container/CompletionCallback.java b/jaxrs-api/src/main/java/jakarta/ws/rs/container/CompletionCallback.java index e66207d3d..43211a7d1 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/container/CompletionCallback.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/container/CompletionCallback.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2021 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,9 +19,9 @@ /** * A request processing callback that receives request processing completion events. *

    - * A completion callback is invoked when the whole request processing is over, i.e. once a response for the request has - * been processed and sent back to the client or in when an unmapped exception or error is being propagated to the - * container. + * A completion callback is always invoked when the whole request processing is over, i.e. once a response for the request has + * been processed and sent back to the client (including processing by a custom exception mapper) or when an unmapped + * exception or error is being propagated to the default exception mapper. *

    * * @author Marek Potociar @@ -29,20 +29,15 @@ */ public interface CompletionCallback { /** - * A completion callback notification method that will be invoked when the request processing is finished, after a - * response is processed and is sent back to the client or when an unmapped throwable has been propagated to the hosting - * I/O container. - *

    - * An unmapped throwable is propagated to the hosting I/O container in case no {@link jakarta.ws.rs.ext.ExceptionMapper - * exception mapper} has been found for a throwable indicating a request processing failure. In this case a - * non-{@code null} unmapped throwable instance is passed to the method. Note that the throwable instance represents the - * actual unmapped exception thrown during the request processing, before it has been wrapped into an I/O - * container-specific exception that was used to propagate the throwable to the hosting I/O container. - *

    + * An unmapped throwable is propagated to the default exception mapper in case no {@link jakarta.ws.rs.ext.ExceptionMapper + * exception mapper} has been found for a request processing failure. In this case a non-{@code null} unmapped throwable + * instance is passed to the method. Note that the throwable instance represents the actual unmapped exception thrown during + * the request processing before it has been mapped to the response by the default exception mapper. * * @param throwable is {@code null}, if the request processing has completed with a response that has been sent to the - * client. In case the request processing resulted in an unmapped exception or error that has been propagated to the - * hosting I/O container, this parameter contains the unmapped exception instance. + * client (including processing by a custom exception mapper). In case the request processing resulted in an unmapped + * exception or error that has yet to be propagated to the default exception mapper, this parameter contains the unmapped + * exception instance. */ public void onComplete(Throwable throwable); } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/container/ContainerRequestContext.java b/jaxrs-api/src/main/java/jakarta/ws/rs/container/ContainerRequestContext.java index b7e593e27..9d05bbefd 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/container/ContainerRequestContext.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/container/ContainerRequestContext.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Predicate; import jakarta.ws.rs.core.Cookie; import jakarta.ws.rs.core.MediaType; @@ -228,6 +229,47 @@ public default boolean hasProperty(String name) { */ public String getHeaderString(String name); + /** + * Checks whether a header with a specific name and value (or item of the token-separated value list) exists. + * + *

    + * For example: {@code containsHeaderString("cache-control", ",", "no-store"::equalsIgnoreCase)} will return {@code true} if + * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value + * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform} + * (missing comma), or the value {@code no - store} (whitespace within value). + * + * @param name the message header. + * @param valueSeparatorRegex Separates the header value into single values. {@code null} does not split. + * @param valuePredicate value must fulfil this predicate. + * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value + * matching the predicate, or having at least one whitespace-trimmed single value in a token-separated list of single values. + * @see #getHeaders() + * @see #getHeaderString(String) + * @since 4.0 + */ + public boolean containsHeaderString(String name, String valueSeparatorRegex, Predicate valuePredicate); + + /** + * Checks whether a header with a specific name and value (or item of the comma-separated value list) exists. + * + *

    + * For example: {@code containsHeaderString("cache-control", "no-store"::equalsIgnoreCase)} will return {@code true} if + * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value + * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform} + * (missing comma), or the value {@code no - store} (whitespace within value). + * + * @param name the message header. + * @param valuePredicate value must fulfil this predicate. + * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value + * matching the predicate, or having at least one whitespace-trimmed single value in a comma-separated list of single values. + * @see #getHeaders() + * @see #getHeaderString(String) + * @since 4.0 + */ + public default boolean containsHeaderString(String name, Predicate valuePredicate) { + return containsHeaderString(name, ",", valuePredicate); + } + /** * Get message date. * diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/container/ContainerResponseContext.java b/jaxrs-api/src/main/java/jakarta/ws/rs/container/ContainerResponseContext.java index 5a3246f2f..6d6b74405 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/container/ContainerResponseContext.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/container/ContainerResponseContext.java @@ -24,6 +24,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import jakarta.ws.rs.core.EntityTag; import jakarta.ws.rs.core.Link; @@ -101,7 +102,7 @@ public interface ContainerResponseContext { /** * Get a message header as a single string value. * - * Each single header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one + * Each single non-string header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one * is available via {@link jakarta.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value * class or using its {@code toString} method if a header delegate is not available. * @@ -114,6 +115,55 @@ public interface ContainerResponseContext { */ public String getHeaderString(String name); + /** + * Checks whether a header with a specific name and value (or item of the token-separated value list) exists. + * + * Each single non-string header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one + * is available via {@link jakarta.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value + * class or using its {@code toString} method if a header delegate is not available. + * + *

    + * For example: {@code containsHeaderString("cache-control", ",", "no-store"::equalsIgnoreCase)} will return {@code true} if + * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value + * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform} + * (missing comma), or the value {@code no - store} (whitespace within value). + * + * @param name the message header. + * @param valueSeparatorRegex Separates the header value into single values. {@code null} does not split. + * @param valuePredicate value must fulfil this predicate. + * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value + * matching the predicate, or having at least one whitespace-trimmed single value in a token-separated list of single values. + * @see #getHeaders() + * @see #getHeaderString(String) + * @since 4.0 + */ + public boolean containsHeaderString(String name, String valueSeparatorRegex, Predicate valuePredicate); + + /** + * Checks whether a header with a specific name and value (or item of the comma-separated value list) exists. + * + * Each single non-string header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one + * is available via {@link jakarta.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value + * class or using its {@code toString} method if a header delegate is not available. + * + *

    + * For example: {@code containsHeaderString("cache-control", "no-store"::equalsIgnoreCase)} will return {@code true} if + * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value + * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform} + * (missing comma), or the value {@code no - store} (whitespace within value). + * + * @param name the message header. + * @param valuePredicate value must fulfil this predicate. + * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value + * matching the predicate, or having at least one whitespace-trimmed single value in a comma-separated list of single values. + * @see #getHeaders() + * @see #getHeaderString(String) + * @since 4.0 + */ + public default boolean containsHeaderString(String name, Predicate valuePredicate) { + return containsHeaderString(name, ",", valuePredicate); + } + /** * Get the allowed HTTP methods from the Allow HTTP header. * diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/core/CacheControl.java b/jaxrs-api/src/main/java/jakarta/ws/rs/core/CacheControl.java index d372c9345..081cf32dd 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/core/CacheControl.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/core/CacheControl.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import jakarta.ws.rs.ext.RuntimeDelegate; import jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate; @@ -339,15 +340,8 @@ public String toString() { */ @Override public int hashCode() { - int hash = 7; - hash = 41 * hash + (this.privateFlag ? 1 : 0); - hash = 41 * hash + (this.noCache ? 1 : 0); - hash = 41 * hash + (this.noStore ? 1 : 0); - hash = 41 * hash + (this.noTransform ? 1 : 0); - hash = 41 * hash + (this.mustRevalidate ? 1 : 0); - hash = 41 * hash + (this.proxyRevalidate ? 1 : 0); - hash = 41 * hash + this.maxAge; - hash = 41 * hash + this.sMaxAge; + int hash = Objects.hash(this.privateFlag, this.noCache, this.noStore, this.noTransform, this.mustRevalidate, + this.proxyRevalidate, this.maxAge, this.sMaxAge); hash = 41 * hash + hashCodeOf(this.privateFields); hash = 41 * hash + hashCodeOf(this.noCacheFields); hash = 41 * hash + hashCodeOf(this.cacheExtension); diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/core/Cookie.java b/jaxrs-api/src/main/java/jakarta/ws/rs/core/Cookie.java index 47934a586..109abc019 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/core/Cookie.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/core/Cookie.java @@ -16,6 +16,8 @@ package jakarta.ws.rs.core; +import java.util.Objects; + import jakarta.ws.rs.ext.RuntimeDelegate; import jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate; @@ -54,7 +56,9 @@ public class Cookie { * @param domain the host domain for which the cookie is valid. * @param version the version of the specification to which the cookie complies. * @throws IllegalArgumentException if name is {@code null}. + * @deprecated This constructor will be removed in a future version. Please use {@link Cookie.Builder} instead. */ + @Deprecated public Cookie(final String name, final String value, final String path, final String domain, final int version) throws IllegalArgumentException { if (name == null) { @@ -75,7 +79,9 @@ public Cookie(final String name, final String value, final String path, final St * @param path the URI path for which the cookie is valid. * @param domain the host domain for which the cookie is valid. * @throws IllegalArgumentException if name is {@code null}. + * @deprecated This constructor will be removed in a future version. Please use {@link Cookie.Builder} instead. */ + @Deprecated public Cookie(final String name, final String value, final String path, final String domain) throws IllegalArgumentException { this(name, value, path, domain, DEFAULT_VERSION); @@ -87,12 +93,32 @@ public Cookie(final String name, final String value, final String path, final St * @param name the name of the cookie. * @param value the value of the cookie. * @throws IllegalArgumentException if name is {@code null}. + * @deprecated This constructor will be removed in a future version. Please use {@link Cookie.Builder} instead. */ + @Deprecated public Cookie(final String name, final String value) throws IllegalArgumentException { this(name, value, null, null); } + /** + * Create a new instance from the supplied {@link AbstractCookieBuilder} instance. + * + * @param builder the builder. + * @throws IllegalArgumentException if {@code builder.name} is {@code null}. + * @since 3.1 + */ + protected Cookie(AbstractCookieBuilder builder) throws IllegalArgumentException { + if (builder.name == null) { + throw new IllegalArgumentException("name==null"); + } + this.name = builder.name; + this.value = builder.value; + this.version = builder.version; + this.domain = builder.domain; + this.path = builder.path; + } + /** * Creates a new instance of {@code Cookie} by parsing the supplied string. * @@ -173,13 +199,7 @@ public String toString() { */ @Override public int hashCode() { - int hash = 7; - hash = 97 * hash + (this.name != null ? this.name.hashCode() : 0); - hash = 97 * hash + (this.value != null ? this.value.hashCode() : 0); - hash = 97 * hash + this.version; - hash = 97 * hash + (this.path != null ? this.path.hashCode() : 0); - hash = 97 * hash + (this.domain != null ? this.domain.hashCode() : 0); - return hash; + return Objects.hash(this.name, this.value, this.version, this.path, this.domain); } /** @@ -199,21 +219,141 @@ public boolean equals(final Object obj) { return false; } final Cookie other = (Cookie) obj; - if (this.name != other.name && (this.name == null || !this.name.equals(other.name))) { + if (!Objects.equals(this.name, other.name)) { return false; } - if (this.value != other.value && (this.value == null || !this.value.equals(other.value))) { + if (!Objects.equals(this.value, other.value)) { return false; } - if (this.version != other.version) { + if (!Objects.equals(this.version, other.version)) { return false; } - if (this.path != other.path && (this.path == null || !this.path.equals(other.path))) { + if (!Objects.equals(this.path, other.path)) { return false; } - if (this.domain != other.domain && (this.domain == null || !this.domain.equals(other.domain))) { + if (!Objects.equals(this.domain, other.domain)) { return false; } return true; } + + /** + * JAX-RS {@link Cookie} builder class. + *

    + * Cookie builder provides methods that let you conveniently configure and subsequently build a new + * {@code Cookie} instance. + *

    + * For example: + * + *
    +     * Cookie cookie = new Cookie.Builder("name")
    +     *         .path("/")
    +     *         .domain("domain.com")
    +     *         .build();
    +     * 
    + * + * @since 3.1 + */ + public static class Builder extends AbstractCookieBuilder { + + /** + * Create a new instance. + * + * @param name the name of the cookie. + */ + public Builder(String name) { + super(name); + } + + @Override + public Cookie build() { + return new Cookie(this); + } + + } + + /** + * JAX-RS abstract {@link Cookie} builder class. + * + * @param the current AbstractCookieBuilder type. + * + * @since 3.1 + */ + public abstract static class AbstractCookieBuilder> { + + private final String name; + + private String value; + private int version = DEFAULT_VERSION; + private String path; + private String domain; + + /** + * Create a new instance. + * + * @param name the name of the cookie. + */ + public AbstractCookieBuilder(String name) { + this.name = name; + } + + /** + * Set the value of the cookie. + * + * @param value the value of the cookie. + * @return the updated builder instance. + */ + public T value(String value) { + this.value = value; + return self(); + } + + /** + * Set the version of the cookie. Defaults to {@link Cookie#DEFAULT_VERSION} + * + * @param version the version of the specification to which the cookie complies. + * @return the updated builder instance. + */ + public T version(int version) { + this.version = version; + return self(); + } + + /** + * Set the path of the cookie. + * + * @param path the URI path for which the cookie is valid. + * @return the updated builder instance. + */ + public T path(String path) { + this.path = path; + return self(); + } + + /** + * Set the domain of the cookie. + * + * @param domain the host domain for which the cookie is valid. + * @return the updated builder instance. + */ + public T domain(String domain) { + this.domain = domain; + return self(); + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + + /** + * Build a new {@link Cookie} instance using all the configuration previously specified in this builder. + * + * @return a new {@link Cookie} instance. + * @throws IllegalArgumentException if name is {@code null}. + */ + public abstract Cookie build(); + + } + } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/core/EntityPart.java b/jaxrs-api/src/main/java/jakarta/ws/rs/core/EntityPart.java new file mode 100644 index 000000000..8186ed399 --- /dev/null +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/core/EntityPart.java @@ -0,0 +1,398 @@ +/******************************************************************* +* Copyright (c) 2021 Eclipse Foundation +* +* This specification document is made available under the terms +* of the Eclipse Foundation Specification License v1.0, which is +* available at https://www.eclipse.org/legal/efsl.php. +*******************************************************************/ +package jakarta.ws.rs.core; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.ext.RuntimeDelegate; + +/** + * A {@code EntityPart} is one part of a multipart entity. As defined in + * RFC 7578, a multipart + * request or response must have a content type of "multipart/form-data" with a + * {@code boundary} parameter indicating where one part ends the next may begin. + *

    + * Multipart entities may be received in a resource method as a collection of + * parts (e.g. {@code List}) or as a form parameter (ex: + * {@code @FormParam("part1Name") EntityPart part1}). + *

    + *

    + * Likewise, a client may receive a multipart response by reading the returned + * entity as a collection of EntityParts (ex: {@code response.readEntity(new + * GenericType>() {})}). + *

    + *

    + * In order to send a multipart entity either as a client request or a response + * from a resource method, you may create the Lists using + * {@code EntityPart.Builder}. For example: + *

    + * + *
    + * Client c = ClientBuilder.newClient();
    + *WebTarget target = c.target(someURL);
    + *List<EntityPart> parts = Arrays.asList(
    + *     EntityPart.withName("name1").fileName("file1.doc").content(stream1).build(),
    + *     EntityPart.withName("name2").fileName("file2.doc").content(stream2).build(),
    + *     EntityPart.withName("name3").fileName("file3.xml").content(myObject, MyClass.class).mediaType("application/xml").build());
    + *GenericEntity<List<EntityPart>> genericEntity = new GenericEntity<>(parts){};
    + *Entity entity = Entity.entity(genericEntity, MediaType.MULTIPART_FORM_DATA);
    + *Response r = target.request().post(entity);
    + * 
    + * + * Note that when building a EntityPart, the name and content are required. + * Other properties such as headers, file name, and media type are optional. + * + * It is the responsibility of the implementation code to close the content + * input streams when sending the multipart content. Closing the stream before + * the implementation has sent it could result in unexpected exceptions. It is + * the responsibility of the calling code to close the stream when receiving the + * multipart content. + * + * @since 3.1 + */ +public interface EntityPart { + + /** + * Creates a new {@code EntityPart.Builder} instance. + * + * @param partName name of the part to create within the multipart entity + * @return {@link Builder} for building new {@link EntityPart} instances + */ + static Builder withName(String partName) { + return RuntimeDelegate.getInstance().createEntityPartBuilder(partName); + } + + /** + * Creates a new {@code EntityPart.Builder} instance that sets the part + * {@code name} and {@code fileName} to the passed in {@code partAndFileName} + * value. + *

    + * Logically, this is the same as {@code EntityPart.withName(x).fileName(x)}. + *

    + * + * @param partAndFileName name and filename of the part to create within the + * multipart entity + * @return {@link Builder} for building new {@link EntityPart} instances + */ + static Builder withFileName(String partAndFileName) { + return RuntimeDelegate.getInstance().createEntityPartBuilder(partAndFileName).fileName(partAndFileName); + } + + /** + * Returns the name of this part within the multipart entity. This will be the + * "name" attribute of the {@code Content-Disposition} header for this part. + * + * @return the part name + */ + String getName(); + + /** + * Returns the filename of this part. This will be the "filename" attribute of + * the {@code Content-Disposition} header for this part. A filename is not + * required in a part, so if a filename is not present it will return + * {@code Optional.empty()}. + * + * @return an {@code Optional} indicating the filename if present + */ + Optional getFileName(); + + /** + * Returns the input stream for this part. This is the content body of the part + * and is accessed as a stream to avoid loading potentially large amounts of + * data into the heap. + * + * It is the responsibility of the calling code to close this stream after + * receiving it. + * + * @return an {@code InputStream} representing the content of this part + */ + InputStream getContent(); + + /** + * Converts the content stream for this part to the specified class and returns + * it. The implementation must convert the stream by finding a + * {@link jakarta.ws.rs.ext.MessageBodyReader} that handles the specified type + * as well as the {@link MediaType} of the part. If no + * {@link jakarta.ws.rs.ext.MessageBodyReader} can be found to perform the + * conversion, this method will throw an {@code IllegalArgumentException}. + * + * The implementation is required to close the content stream when this method + * is invoked, so it may only be invoked once. Subsequent invocations will + * result in an {@code IllegalStateException}. Likewise this method will throw + * an {@code IllegalStateException} if it is called after calling + * {@link #getContent} or {@link #getContent(GenericType)}. + * + * @param type the {@code Class} that the implementation should convert this + * part to + * @param the entity type + * @return an instance of the specified {@code Class} representing the content + * of this part + * @throws IllegalArgumentException if no + * {@link jakarta.ws.rs.ext.MessageBodyReader} + * can handle the conversion of this part to + * the specified type + * @throws IllegalStateException if this method or any of the other + * {@code getContent} methods has already been + * invoked + * @throws IOException if the + * {@link jakarta.ws.rs.ext.MessageBodyReader#readFrom(Class, + * java.lang.reflect.Type, java.lang.annotation.Annotation[], MediaType, MultivaluedMap, InputStream)} + * method throws an {@code IOException} + * @throws WebApplicationException if the + * {@link jakarta.ws.rs.ext.MessageBodyReader#readFrom(Class, + * java.lang.reflect.Type, java.lang.annotation.Annotation[], MediaType, MultivaluedMap, InputStream)} + * method throws an + * {@code WebApplicationException} + */ + //CHECKSTYLE:OFF - More than 3 Exceptions are desired here + T getContent(Class type) throws IllegalArgumentException, IllegalStateException, IOException, + WebApplicationException; + //CHECKSTYLE:ON + + /** + * Converts the content stream for this part to the specified type and returns + * it. The implementation must convert the stream by finding a + * {@link jakarta.ws.rs.ext.MessageBodyReader} that handles the specified type + * as well as the {@link MediaType} of the part. If no + * {@link jakarta.ws.rs.ext.MessageBodyReader} can be found to perform the + * conversion, this method will throw an {@code IllegalArgumentException}. + * + * The implementation is required to close the content stream when this method + * is invoked, so it may only be invoked once. Subsequent invocations will + * result in an {@code IllegalStateException}. Likewise this method will throw + * an {@code IllegalStateException} if it is called after calling + * {@link #getContent} or {@link #getContent(Class)}. + * + * @param type the generic type that the implementation should convert this part + * to + * @param the entity type + * @return an instance of the specified generic type representing the content of + * this part + * @throws IllegalArgumentException if no + * {@link jakarta.ws.rs.ext.MessageBodyReader} + * can handle the conversion of this part to + * the specified type + * @throws IllegalStateException if this method or any of the other + * {@code getContent} methods has already been + * invoked + * @throws IOException if the + * {@link jakarta.ws.rs.ext.MessageBodyReader#readFrom(Class, + * java.lang.reflect.Type, java.lang.annotation.Annotation[], MediaType, MultivaluedMap, InputStream)} + * method throws an {@code IOException} + * @throws WebApplicationException if the + * {@link jakarta.ws.rs.ext.MessageBodyReader#readFrom(Class, + * java.lang.reflect.Type, java.lang.annotation.Annotation[], MediaType, MultivaluedMap, InputStream)} + * method throws an + * {@code WebApplicationException} + */ + //CHECKSTYLE:OFF - More than 3 Exceptions are desired here + T getContent(GenericType type) throws IllegalArgumentException, IllegalStateException, IOException, + WebApplicationException; + //CHECKSTYLE:ON + + /** + * Returns an immutable multivalued map of headers for this specific part. + * + * @return immutable {@code MultivaluedMap} of part headers + */ + MultivaluedMap getHeaders(); + + /** + * Returns the content type of this part, and equivalent to calling + * {@code MediaType.valueOf(part.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE))}. + * + * @return the media type for this part + */ + MediaType getMediaType(); + + /** + * Builder for {@link EntityPart} instances. + * + * @since 3.1 + */ + interface Builder { + + /** + * Sets the media type for the EntityPart. This will also set the + * {@code Content-Type} header for this part. + * + * @param mediaType the media type for the part to be built + * @return the updated builder + * @throws IllegalArgumentException if {@code mediaType} is {@code null} + */ + Builder mediaType(MediaType mediaType) throws IllegalArgumentException; + + /** + * Convenience method for setting the media type for the EntityPart. This will + * also set the {@code Content-Type} header for this part. This call is + * effectively the same as + * {@code mediaType(MediaType.valueOf(mediaTypeString))}. + * + * @param mediaTypeString the media type for the part to be built + * @return the updated builder + * @throws IllegalArgumentException if {@code mediaTypeString} cannot be parsed + * or is {@code null} + */ + Builder mediaType(String mediaTypeString) throws IllegalArgumentException; + + /** + * Adds a new header or replaces a previously added header and sets the header + * value(s). + * + * @param headerName the header name + * @param headerValues the header value(s) + * @return the updated builder + * @throws IllegalArgumentException if {@code headerName} is {@code null} + */ + Builder header(String headerName, String... headerValues) throws IllegalArgumentException; + + /** + * Adds new headers or replaces previously added headers. The behavior of this + * method would be the same as if iterating over the entry set and invoking the + * {@link #header(String, String...)} method. + * + * @param newHeaders the multivalued map of headers to add to this part + * @return the updated builder + * @throws IllegalArgumentException if {@code newHeaders} is {@code null} + */ + Builder headers(MultivaluedMap newHeaders) throws IllegalArgumentException; + + /** + * Sets the file name for this part. The file name will be specified as an + * attribute in the {@code Content-Disposition} header of this part. When this + * method is called, the default media type used for the built part will be + * "application/octet-stream" if not otherwise specified. + * + * @param fileName the file name for this part + * @return the updated builder + * @throws IllegalArgumentException if {@code fileName} is {@code null} + */ + Builder fileName(String fileName) throws IllegalArgumentException; + + /** + * Sets the content for this part. The content of this builder must be specified + * before invoking the {@link #build()} method. + *

    + * The {@code InputStream} will be closed by the implementation code after + * sending the multipart data. Closing the stream before it is sent could result + * in unexpected behavior. + *

    + * + * @param content {@code InputStream} of the content of this part + * @return the updated builder + * @throws IllegalArgumentException if {@code content} is {@code null} + */ + Builder content(InputStream content) throws IllegalArgumentException; + + /** + * Convenience method, equivalent to calling + * {@code fileName(fileName).content(content)}. + * + * @param fileName the filename of the part. + * @param content the content stream of the part. + * @return the updated builder. + * @throws IllegalArgumentException if either parameter is {@code null}. + */ + default Builder content(String fileName, InputStream content) throws IllegalArgumentException { + return this.fileName(fileName).content(content); + } + + /** + * Sets the content for this part. The content of this builder must be specified + * before invoking the {@link #build()} method. + *

    + * If the content is specified using this method, then the {@link #build()} + * method is responsible for finding a registered + * {@link jakarta.ws.rs.ext.MessageBodyWriter} that is capable of writing the + * object type specified here using the default {@link MediaType} or the + * {@link MediaType} specified in the {@link #mediaType(MediaType)} or + * {@link #mediaType(String)} methods and using any headers specified via the + * {@link #header(String, String...)} or {@link #headers(MultivaluedMap)} + * methods. + *

    + * + * @param content the object to be used as the content + * @param type the type of this object which will be used when selecting the + * appropriate {@link jakarta.ws.rs.ext.MessageBodyWriter} + * @param the entity type + * @return the updated builder. + * @throws IllegalArgumentException if {@code content} is {@code null} + */ + Builder content(T content, Class type) throws IllegalArgumentException; + + /** + * Sets the content for this part. The content of this builder must be specified + * before invoking the {@link #build()} method. + *

    + * If the content is specified using this method, then the {@link #build()} + * method is responsible for finding a registered + * {@link jakarta.ws.rs.ext.MessageBodyWriter} that is capable of writing the + * object's class type specified here using the default {@link MediaType} or the + * {@link MediaType} specified in the {@link #mediaType(MediaType)} or + * {@link #mediaType(String)} methods and using any headers specified via the + * {@link #header(String, String...)} or {@link #headers(MultivaluedMap)} + * methods. + *

    + *

    + * This is the equivalent of calling + * {@code content(content, content.getClass())}. + *

    + * + * @param content the object to be used as the content + * @throws IllegalArgumentException if {@code content} is {@code null} + * @return the updated builder. + */ + default Builder content(Object content) throws IllegalArgumentException { + return this.content(content, content.getClass()); + } + + /** + * Sets the content for this part. The content of this builder must be specified + * before invoking the {@link #build()} method. + *

    + * If the content is specified using this method, then the {@link #build()} + * method is responsible for finding a registered + * {@link jakarta.ws.rs.ext.MessageBodyWriter} that is capable of writing the + * object type specified here using the default {@link MediaType} or the + * {@link MediaType} specified in the {@link #mediaType(MediaType)} or + * {@link #mediaType(String)} methods and using any headers specified via the + * {@link #header(String, String...)} or {@link #headers(MultivaluedMap)} + * methods. + *

    + * + * @param content the object to be used as the content + * @param type the generic type of this object which will be used when + * selecting the appropriate + * {@link jakarta.ws.rs.ext.MessageBodyWriter} + * @param the entity type + * @return the updated builder. + * @throws IllegalArgumentException if {@code content} is {@code null} + */ + Builder content(T content, GenericType type) throws IllegalArgumentException; + + /** + * Builds a new EntityPart instance using the provided property values. + * + * @return {@link EntityPart} instance built from the provided property values. + * @throws IllegalStateException if the content was not specified or no + * matching + * {@link jakarta.ws.rs.ext.MessageBodyWriter} + * was found. + * @throws IOException if the underlying + * {@link jakarta.ws.rs.ext.MessageBodyWriter} + * throws an {@code IOException} + * @throws WebApplicationException if the underlying + * {@link jakarta.ws.rs.ext.MessageBodyWriter} + * throws a {@code WebApplicationException} + */ + EntityPart build() throws IllegalStateException, IOException, WebApplicationException; + } +} diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/core/GenericEntity.java b/jaxrs-api/src/main/java/jakarta/ws/rs/core/GenericEntity.java index 6cf0913b0..3d904b395 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/core/GenericEntity.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/core/GenericEntity.java @@ -23,6 +23,7 @@ import java.lang.reflect.GenericArrayType; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.Objects; /** * Represents a message entity of a generic type {@code T}. @@ -179,7 +180,7 @@ public boolean equals(final Object obj) { @Override public int hashCode() { - return entity.hashCode() + type.hashCode() * 37 + 5; + return Objects.hash(entity, type); } @Override diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/core/HttpHeaders.java b/jaxrs-api/src/main/java/jakarta/ws/rs/core/HttpHeaders.java index aeaf1e145..e191cd070 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/core/HttpHeaders.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/core/HttpHeaders.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Predicate; /** * An injectable interface that provides access to HTTP header information. All methods throw @@ -46,7 +47,7 @@ public interface HttpHeaders { *

    * Get a HTTP header as a single string value. *

    - * Each single header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one + * Each single non-string header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one * is available via {@link jakarta.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value * class or using its {@code toString} method if a header delegate is not available. * @@ -59,6 +60,55 @@ public interface HttpHeaders { */ public String getHeaderString(String name); + /** + * Checks whether a header with a specific name and value (or item of the token-separated value list) exists. + * + * Each single non-string header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one + * is available via {@link jakarta.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value + * class or using its {@code toString} method if a header delegate is not available. + * + *

    + * For example: {@code containsHeaderString("cache-control", ",", "no-store"::equalsIgnoreCase)} will return {@code true} if + * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value + * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform} + * (missing comma), or the value {@code no - store} (whitespace within value). + * + * @param name the message header. + * @param valueSeparatorRegex Separates the header value into single values. {@code null} does not split. + * @param valuePredicate value must fulfil this predicate. + * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value + * matching the predicate, or having at least one whitespace-trimmed single value in a token-separated list of single values. + * @see #getRequestHeaders() + * @see #getHeaderString(String) + * @since 4.0 + */ + public boolean containsHeaderString(String name, String valueSeparatorRegex, Predicate valuePredicate); + + /** + * Checks whether a header with a specific name and value (or item of the comma-separated value list) exists. + * + * Each single non-string header value is converted to String using a {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one + * is available via {@link jakarta.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} for the header value + * class or using its {@code toString} method if a header delegate is not available. + * + *

    + * For example: {@code containsHeaderString("cache-control", "no-store"::equalsIgnoreCase)} will return {@code true} if + * a {@code Cache-Control} header exists that has the value {@code no-store}, the value {@code No-Store} or the value + * {@code Max-Age, NO-STORE, no-transform}, but {@code false} when it has the value {@code no-store;no-transform} + * (missing comma), or the value {@code no - store} (whitespace within value). + * + * @param name the message header. + * @param valuePredicate value must fulfil this predicate. + * @return {@code true} if and only if a header with the given name exists, having either a whitespace-trimmed value + * matching the predicate, or having at least one whitespace-trimmed single value in a comma-separated list of single values. + * @see #getRequestHeaders() + * @see #getHeaderString(String) + * @since 4.0 + */ + public default boolean containsHeaderString(String name, Predicate valuePredicate) { + return containsHeaderString(name, ",", valuePredicate); + } + /** * Get the values of HTTP request headers. The returned Map is case-insensitive wrt. keys and is read-only. The method * never returns {@code null}. diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/core/MediaType.java b/jaxrs-api/src/main/java/jakarta/ws/rs/core/MediaType.java index 72fedad98..6dba6da88 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/core/MediaType.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/core/MediaType.java @@ -82,11 +82,15 @@ public class MediaType { public static final MediaType APPLICATION_XHTML_XML_TYPE = new MediaType("application", "xhtml+xml"); /** * A {@code String} constant representing {@value #APPLICATION_SVG_XML} media type. + * @deprecated since 4.0, use a custom string instead. Will be removed in a future release of this API. */ + @Deprecated(forRemoval = true) public static final String APPLICATION_SVG_XML = "application/svg+xml"; /** * A {@link MediaType} constant representing {@value #APPLICATION_SVG_XML} media type. + * @deprecated since 4.0, use a custom {@code MediaType} instead. Will be removed in a future release of this API. */ + @Deprecated(forRemoval = true) public static final MediaType APPLICATION_SVG_XML_TYPE = new MediaType("application", "svg+xml"); /** * A {@code String} constant representing {@value #APPLICATION_JSON} media type. diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/core/NewCookie.java b/jaxrs-api/src/main/java/jakarta/ws/rs/core/NewCookie.java index a5b5aed84..f630dceeb 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/core/NewCookie.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/core/NewCookie.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2021 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 jakarta.ws.rs.core; import java.util.Date; +import java.util.Objects; import jakarta.ws.rs.ext.RuntimeDelegate; import jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate; @@ -55,7 +56,9 @@ public class NewCookie extends Cookie { * @param name the name of the cookie. * @param value the value of the cookie. * @throws IllegalArgumentException if name is {@code null}. + * @deprecated This constructor will be removed in a future version. Please use {@link NewCookie.Builder} instead. */ + @Deprecated public NewCookie(final String name, final String value) { this(name, value, null, null, DEFAULT_VERSION, null, DEFAULT_MAX_AGE, null, false, false, null); } @@ -71,7 +74,9 @@ public NewCookie(final String name, final String value) { * @param maxAge the maximum age of the cookie in seconds. * @param secure specifies whether the cookie will only be sent over a secure connection. * @throws IllegalArgumentException if name is {@code null}. + * @deprecated This constructor will be removed in a future version. Please use {@link NewCookie.Builder} instead. */ + @Deprecated public NewCookie(final String name, final String value, final String path, @@ -95,7 +100,9 @@ public NewCookie(final String name, * @param httpOnly if {@code true} make the cookie HTTP only, i.e. only visible as part of an HTTP request. * @throws IllegalArgumentException if name is {@code null}. * @since 2.0 + * @deprecated This constructor will be removed in a future version. Please use {@link NewCookie.Builder} instead. */ + @Deprecated public NewCookie(final String name, final String value, final String path, @@ -119,7 +126,9 @@ public NewCookie(final String name, * @param maxAge the maximum age of the cookie in seconds * @param secure specifies whether the cookie will only be sent over a secure connection * @throws IllegalArgumentException if name is {@code null}. + * @deprecated This constructor will be removed in a future version. Please use {@link NewCookie.Builder} instead. */ + @Deprecated public NewCookie(final String name, final String value, final String path, @@ -146,7 +155,9 @@ public NewCookie(final String name, * @param httpOnly if {@code true} make the cookie HTTP only, i.e. only visible as part of an HTTP request. * @throws IllegalArgumentException if name is {@code null}. * @since 2.0 + * @deprecated This constructor will be removed in a future version. Please use {@link NewCookie.Builder} instead. */ + @Deprecated public NewCookie(final String name, final String value, final String path, @@ -176,7 +187,9 @@ public NewCookie(final String name, * @param sameSite specifies the value of the {@code SameSite} cookie attribute * @throws IllegalArgumentException if name is {@code null}. * @since 3.1 + * @deprecated This constructor will be removed in a future version. Please use {@link NewCookie.Builder} instead. */ + @Deprecated public NewCookie(final String name, final String value, final String path, @@ -202,7 +215,9 @@ public NewCookie(final String name, * * @param cookie the cookie to clone. * @throws IllegalArgumentException if cookie is {@code null}. + * @deprecated This constructor will be removed in a future version. Please use {@link NewCookie.Builder} instead. */ + @Deprecated public NewCookie(final Cookie cookie) { this(cookie, null, DEFAULT_MAX_AGE, null, false, false, null); } @@ -215,7 +230,9 @@ public NewCookie(final Cookie cookie) { * @param maxAge the maximum age of the cookie in seconds. * @param secure specifies whether the cookie will only be sent over a secure connection. * @throws IllegalArgumentException if cookie is {@code null}. + * @deprecated This constructor will be removed in a future version. Please use {@link NewCookie.Builder} instead. */ + @Deprecated public NewCookie(final Cookie cookie, final String comment, final int maxAge, final boolean secure) { this(cookie, comment, maxAge, null, secure, false, null); } @@ -231,7 +248,9 @@ public NewCookie(final Cookie cookie, final String comment, final int maxAge, fi * @param httpOnly if {@code true} make the cookie HTTP only, i.e. only visible as part of an HTTP request. * @throws IllegalArgumentException if cookie is {@code null}. * @since 2.0 + * @deprecated This constructor will be removed in a future version. Please use {@link NewCookie.Builder} instead. */ + @Deprecated public NewCookie(final Cookie cookie, final String comment, final int maxAge, final Date expiry, final boolean secure, final boolean httpOnly) { this(cookie, comment, maxAge, expiry, secure, httpOnly, null); } @@ -249,7 +268,9 @@ public NewCookie(final Cookie cookie, final String comment, final int maxAge, fi * @param sameSite specifies the value of the {@code SameSite} cookie attribute * @throws IllegalArgumentException if cookie is {@code null}. * @since 3.1 + * @deprecated This constructor will be removed in a future version. Please use {@link NewCookie.Builder} instead. */ + @Deprecated public NewCookie(final Cookie cookie, final String comment, final int maxAge, final Date expiry, final boolean secure, final boolean httpOnly, final SameSite sameSite) { super(cookie == null ? null : cookie.getName(), @@ -265,6 +286,23 @@ public NewCookie(final Cookie cookie, final String comment, final int maxAge, fi this.sameSite = sameSite; } + /** + * Create a new instance from the supplied {@link AbstractNewCookieBuilder} instance. + * + * @param builder the builder. + * @throws IllegalArgumentException if {@code builder.name} is {@code null}. + * @since 3.1 + */ + protected NewCookie(AbstractNewCookieBuilder builder) { + super(builder); + this.comment = builder.comment; + this.maxAge = builder.maxAge; + this.expiry = builder.expiry; + this.secure = builder.secure; + this.httpOnly = builder.httpOnly; + this.sameSite = builder.sameSite; + } + /** * Creates a new instance of NewCookie by parsing the supplied string. * @@ -386,14 +424,8 @@ public String toString() { */ @Override public int hashCode() { - int hash = super.hashCode(); - hash = 59 * hash + (this.comment != null ? this.comment.hashCode() : 0); - hash = 59 * hash + this.maxAge; - hash = 59 + hash + (this.expiry != null ? this.expiry.hashCode() : 0); - hash = 59 * hash + (this.secure ? 1 : 0); - hash = 59 * hash + (this.httpOnly ? 1 : 0); - hash = 59 * hash + this.sameSite.ordinal(); - return hash; + return Objects.hash(getName(), getValue(), getVersion(), getPath(), getDomain(), + comment, maxAge, expiry, secure, httpOnly, sameSite); } /** @@ -413,29 +445,29 @@ public boolean equals(final Object obj) { return false; } final NewCookie other = (NewCookie) obj; - if (this.getName() != other.getName() && (this.getName() == null || !this.getName().equals(other.getName()))) { + if (!Objects.equals(this.getName(), other.getName())) { return false; } - if (this.getValue() != other.getValue() && (this.getValue() == null || !this.getValue().equals(other.getValue()))) { + if (!Objects.equals(this.getValue(), other.getValue())) { return false; } if (this.getVersion() != other.getVersion()) { return false; } - if (this.getPath() != other.getPath() && (this.getPath() == null || !this.getPath().equals(other.getPath()))) { + if (!Objects.equals(this.getPath(), other.getPath())) { return false; } - if (this.getDomain() != other.getDomain() && (this.getDomain() == null || !this.getDomain().equals(other.getDomain()))) { + if (!Objects.equals(this.getDomain(), other.getDomain())) { return false; } - if (this.comment != other.comment && (this.comment == null || !this.comment.equals(other.comment))) { + if (!Objects.equals(this.comment, other.comment)) { return false; } if (this.maxAge != other.maxAge) { return false; } - if (this.expiry != other.expiry && (this.expiry == null || !this.expiry.equals(other.expiry))) { + if (!Objects.equals(this.expiry, other.expiry)) { return false; } @@ -475,4 +507,182 @@ public enum SameSite { } + /** + * JAX-RS {@link NewCookie} builder class. + *

    + * New Cookie builder provides methods that let you conveniently configure and subsequently build a new + * {@code NewCookie} instance. + *

    + * For example: + * + *
    +     * NewCookie cookie = new NewCookie.Builder("name")
    +     *         .path("/")
    +     *         .domain("domain.com")
    +     *         .sameSite(SameSite.LAX)
    +     *         .build();
    +     * 
    + * + * @since 3.1 + */ + public static class Builder extends AbstractNewCookieBuilder { + + /** + * Create a new instance. + * + * @param name the name of the cookie. + */ + public Builder(String name) { + super(name); + } + + /** + * Create a new instance supplementing the information in the supplied cookie. + * + * @param cookie the cookie to clone. + */ + public Builder(Cookie cookie) { + super(cookie); + } + + @Override + public NewCookie build() { + return new NewCookie(this); + } + + } + + /** + * JAX-RS abstract {@link NewCookie} builder class. + * + * @param the current AbstractNewCookieBuilder type. + * + * @since 3.1 + */ + public abstract static class AbstractNewCookieBuilder> extends AbstractCookieBuilder> { + + private String comment; + private int maxAge = DEFAULT_MAX_AGE; + private Date expiry; + private boolean secure; + private boolean httpOnly; + private SameSite sameSite; + + /** + * Create a new instance. + * + * @param name the name of the cookie. + */ + public AbstractNewCookieBuilder(String name) { + super(name); + } + + /** + * Create a new instance supplementing the information in the supplied cookie. + * + * @param cookie the cookie to clone. + */ + public AbstractNewCookieBuilder(Cookie cookie) { + super(cookie == null ? null : cookie.getName()); + if (cookie != null) { + value(cookie.getValue()); + path(cookie.getPath()); + domain(cookie.getDomain()); + version(cookie.getVersion()); + } + } + + /** + * Set the comment associated with the cookie. + * + * @param comment the comment. + * @return the updated builder instance. + */ + public T comment(String comment) { + this.comment = comment; + return self(); + } + + /** + * Set the maximum age of the the cookie in seconds. Cookies older than the maximum age are discarded. A cookie can be + * unset by sending a new cookie with maximum age of 0 since it will overwrite any existing cookie and then be + * immediately discarded. The default value of {@code -1} indicates that the cookie will be discarded at the end of the + * browser/application session. + * + * @param maxAge the maximum age in seconds. + * @return the updated builder instance. + * @see #expiry(Date) + */ + public T maxAge(int maxAge) { + this.maxAge = maxAge; + return self(); + } + + /** + * Set the cookie expiry date. Cookies whose expiry date has passed are discarded. A cookie can be unset by setting a + * new cookie with an expiry date in the past, typically the lowest possible date that can be set. + *

    + * Note that it is recommended to use {@link #maxAge(int) Max-Age} to control cookie expiration, however some browsers + * do not understand {@code Max-Age}, in which case setting {@code Expires} parameter may be necessary. + *

    + * + * @param expiry the cookie expiry date + * @return the updated builder instance. + * @see #maxAge(int) + */ + public T expiry(Date expiry) { + this.expiry = expiry; + return self(); + } + + /** + * Whether the cookie will only be sent over a secure connection. Defaults to {@code false}. + * + * @param secure specifies whether the cookie will only be sent over a secure connection. + * @return the updated builder instance. + */ + public T secure(boolean secure) { + this.secure = secure; + return self(); + } + + /** + * Whether the cookie will only be visible as part of an HTTP request. Defaults to {@code false}. + * + * @param httpOnly if {@code true} make the cookie HTTP only, i.e. only visible as part of an HTTP request. + * @return the updated builder instance. + */ + public T httpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + return self(); + } + + /** + * Set the attribute that controls whether the cookie is sent with cross-origin requests, providing protection against + * cross-site request forgery. + * + * @param sameSite specifies the value of the {@code SameSite} cookie attribute. + * @return the updated builder instance. + */ + public T sameSite(SameSite sameSite) { + this.sameSite = sameSite; + return self(); + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + + /** + * Build a new {@link NewCookie} instance using all the configuration previously specified in this builder. + * + * @return a new {@link NewCookie} instance. + * @throws IllegalArgumentException if name is {@code null}. + */ + @Override + public abstract NewCookie build(); + + } + } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/core/PathSegment.java b/jaxrs-api/src/main/java/jakarta/ws/rs/core/PathSegment.java index effc6bdbe..375e13a54 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/core/PathSegment.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/core/PathSegment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2021 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 @@ -32,7 +32,6 @@ public interface PathSegment { /** * Get the path segment. - *

    * * @return the path segment */ diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/core/Response.java b/jaxrs-api/src/main/java/jakarta/ws/rs/core/Response.java index 82e85d704..39c44e104 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/core/Response.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/core/Response.java @@ -463,6 +463,22 @@ public MultivaluedMap getHeaders() { */ public abstract String getHeaderString(String name); + /** + * Check if the response is closed. The method returns {@code true} if the response is closed, + * returns {@code false} otherwise. + * + * @return {@code true} if the response has been {@link #close() closed}, {@code false} otherwise. + * @since 3.1 + */ + public boolean isClosed() { + try { + hasEntity(); + return false; + } catch (IllegalStateException ignored) { + return true; + } + } + /** * Create a new ResponseBuilder by performing a shallow copy of an existing Response. *

    @@ -1173,6 +1189,13 @@ public enum Status implements StatusType { * @since 2.0 */ PARTIAL_CONTENT(206, "Partial Content"), + /** + * 300 Multiple Choices, see HTTP/1.1: + * Semantics and Content. + * + * @since 3.1 + */ + MULTIPLE_CHOICES(300, "Multiple Choices"), /** * 301 Moved Permanently, see HTTP/1.1 * documentation. @@ -1206,6 +1229,13 @@ public enum Status implements StatusType { * documentation. */ TEMPORARY_REDIRECT(307, "Temporary Redirect"), + /** + * 308 Permanent Redirect, see RFC 7538: + * The Hypertext Transfer Protocol Status Code 308 (Permanent Redirect). + * + * @since 3.1 + */ + PERMANENT_REDIRECT(308, "Permanent Redirect"), /** * 400 Bad Request, see HTTP/1.1 * documentation. @@ -1334,6 +1364,13 @@ public enum Status implements StatusType { * @since 2.1 */ REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"), + /** + * 451 Unavailable For Legal Reasons, see RFC 7725: + * An HTTP Status Code to Report Legal Obstacles. + * + * @since 3.1 + */ + UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable For Legal Reasons"), /** * 500 Internal Server Error, see HTTP/1.1 * documentation. diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/core/Variant.java b/jaxrs-api/src/main/java/jakarta/ws/rs/core/Variant.java index d7b98c39d..b48bc6b10 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/core/Variant.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/core/Variant.java @@ -19,6 +19,7 @@ import java.io.StringWriter; import java.util.List; import java.util.Locale; +import java.util.Objects; import jakarta.ws.rs.ext.RuntimeDelegate; @@ -198,11 +199,7 @@ public static VariantListBuilder encodings(final String... encodings) { */ @Override public int hashCode() { - int hash = 7; - hash = 29 * hash + (this.language != null ? this.language.hashCode() : 0); - hash = 29 * hash + (this.mediaType != null ? this.mediaType.hashCode() : 0); - hash = 29 * hash + (this.encoding != null ? this.encoding.hashCode() : 0); - return hash; + return Objects.hash(this.language, this.mediaType, this.encoding); } /** @@ -220,14 +217,13 @@ public boolean equals(final Object obj) { return false; } final Variant other = (Variant) obj; - if (this.language != other.language && (this.language == null || !this.language.equals(other.language))) { + if (!Objects.equals(this.language, other.language)) { return false; } - if (this.mediaType != other.mediaType && (this.mediaType == null || !this.mediaType.equals(other.mediaType))) { + if (!Objects.equals(this.mediaType, other.mediaType)) { return false; } - // noinspection StringEquality - return this.encoding == other.encoding || (this.encoding != null && this.encoding.equals(other.encoding)); + return Objects.equals(this.encoding, other.encoding); } @Override diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/ext/FactoryFinder.java b/jaxrs-api/src/main/java/jakarta/ws/rs/ext/FactoryFinder.java index d3a91c8f6..bbd5afd96 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/ext/FactoryFinder.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/ext/FactoryFinder.java @@ -21,9 +21,7 @@ import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedAction; -import java.util.Iterator; import java.util.Properties; -import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; @@ -45,6 +43,11 @@ private FactoryFinder() { } private static ClassLoader getContextClassLoader() { + // For performance reasons, check if a security manager is installed. If not there is no need to use a + // privileged action. + if (System.getSecurityManager() == null) { + return Thread.currentThread().getContextClassLoader(); + } return AccessController.doPrivileged((PrivilegedAction) () -> { ClassLoader cl = null; try { @@ -107,24 +110,16 @@ private static Object newInstance(final String className, final ClassLoader clas static Object find(final String factoryId, final Class service) throws ClassNotFoundException { ClassLoader classLoader = getContextClassLoader(); - try { - Iterator iterator = ServiceLoader.load(service, FactoryFinder.getContextClassLoader()).iterator(); - - if (iterator.hasNext()) { - return iterator.next(); - } - } catch (Exception | ServiceConfigurationError ex) { - LOGGER.log(Level.FINER, "Failed to load service " + factoryId + ".", ex); + // First try the TCCL + Object result = findFirstService(factoryId, classLoader, service); + if (result != null) { + return result; } - try { - Iterator iterator = ServiceLoader.load(service, FactoryFinder.class.getClassLoader()).iterator(); - - if (iterator.hasNext()) { - return iterator.next(); - } - } catch (Exception | ServiceConfigurationError ex) { - LOGGER.log(Level.FINER, "Failed to load service " + factoryId + ".", ex); + // Next try the class loader from the FactoryFinder + result = findFirstService(factoryId, getClassLoader(), service); + if (result != null) { + return result; } // try to read from $java.home/lib/jaxrs.properties @@ -168,4 +163,29 @@ static Object find(final String factoryId, final Class service) throws Cl throw new ClassNotFoundException( "Provider for " + factoryId + " cannot be found", null); } + + private static ClassLoader getClassLoader() { + if (System.getSecurityManager() == null) { + return FactoryFinder.class.getClassLoader(); + } + return AccessController.doPrivileged((PrivilegedAction) FactoryFinder.class::getClassLoader); + } + + private static T findFirstService(final String factoryId, final ClassLoader cl, final Class service) { + final PrivilegedAction action = () -> { + try { + final ServiceLoader loader = ServiceLoader.load(service, cl); + if (loader.iterator().hasNext()) { + return loader.iterator().next(); + } + } catch (Exception e) { + LOGGER.log(Level.FINER, "Failed to load service " + factoryId + ".", e); + } + return null; + }; + if (System.getSecurityManager() == null) { + return action.run(); + } + return AccessController.doPrivileged(action); + } } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/ext/RuntimeDelegate.java b/jaxrs-api/src/main/java/jakarta/ws/rs/ext/RuntimeDelegate.java index 05c65b41f..3570e5164 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/ext/RuntimeDelegate.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/ext/RuntimeDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2021 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,9 +23,10 @@ import jakarta.ws.rs.SeBootstrap; import jakarta.ws.rs.SeBootstrap.Instance; import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.EntityPart; import jakarta.ws.rs.core.Link; -import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.UriBuilder; +import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.Variant.VariantListBuilder; /** @@ -239,8 +240,8 @@ public static interface HeaderDelegate { /** * Perform startup of the application in Java SE environments. *

    - * This method is not intended to be invoked by applications. Call {@link SeBootstrap#start(Application, Configuration)} - * instead. + * This method is not intended to be invoked by applications. Call {@link SeBootstrap#start(Application, + * SeBootstrap.Configuration)} instead. *

    * * @param application The application to start up. @@ -249,4 +250,31 @@ public static interface HeaderDelegate { * instance}. */ public abstract CompletionStage bootstrap(Application application, SeBootstrap.Configuration configuration); + + /** + * Perform startup of the application in Java SE environments. + *

    + * This method is not intended to be invoked by applications. Call {@link SeBootstrap#start(Class, + * SeBootstrap.Configuration)} instead. + *

    + * + * @param clazz The application class to instantiate and start. + * @param configuration The bootstrap configuration. + * @return {@code CompletionStage} asynchronously producing handle of the running application {@link SeBootstrap.Instance + * instance}. + */ + public abstract CompletionStage bootstrap(Class clazz, SeBootstrap.Configuration configuration); + + /** + * Create a new instance of a {@link jakarta.ws.rs.core.EntityPart.Builder}. + *

    + * This method is not intended to be invoked by applications. Call {@link EntityPart#withName(String)} instead. + *

    + * + * @param partName name for this part within the multipart body. + * @return new {@code EntityPart.Builder} instance with specified part name + * @throws java.lang.IllegalArgumentException if {@code partName} is {@code null}. + * @since 3.1 + */ + public abstract EntityPart.Builder createEntityPartBuilder(String partName) throws IllegalArgumentException; } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/sse/FactoryFinder.java b/jaxrs-api/src/main/java/jakarta/ws/rs/sse/FactoryFinder.java index fa614afab..40440ae87 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/sse/FactoryFinder.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/sse/FactoryFinder.java @@ -21,9 +21,7 @@ import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedAction; -import java.util.Iterator; import java.util.Properties; -import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; @@ -45,6 +43,11 @@ private FactoryFinder() { } private static ClassLoader getContextClassLoader() { + // For performance reasons, check if a security manager is installed. If not there is no need to use a + // privileged action. + if (System.getSecurityManager() == null) { + return Thread.currentThread().getContextClassLoader(); + } return AccessController.doPrivileged((PrivilegedAction) () -> { ClassLoader cl = null; try { @@ -107,24 +110,16 @@ private static Object newInstance(final String className, final ClassLoader clas static Object find(final String factoryId, final Class service) throws ClassNotFoundException { ClassLoader classLoader = getContextClassLoader(); - try { - Iterator iterator = ServiceLoader.load(service, FactoryFinder.getContextClassLoader()).iterator(); - - if (iterator.hasNext()) { - return iterator.next(); - } - } catch (Exception | ServiceConfigurationError ex) { - LOGGER.log(Level.FINER, "Failed to load service " + factoryId + ".", ex); + // First try the TCCL + Object result = findFirstService(factoryId, classLoader, service); + if (result != null) { + return result; } - try { - Iterator iterator = ServiceLoader.load(service, FactoryFinder.class.getClassLoader()).iterator(); - - if (iterator.hasNext()) { - return iterator.next(); - } - } catch (Exception | ServiceConfigurationError ex) { - LOGGER.log(Level.FINER, "Failed to load service " + factoryId + ".", ex); + // Next try the class loader from the FactoryFinder + result = findFirstService(factoryId, getClassLoader(), service); + if (result != null) { + return result; } // try to read from $java.home/lib/jaxrs.properties @@ -168,4 +163,29 @@ static Object find(final String factoryId, final Class service) throws Cl throw new ClassNotFoundException( "Provider for " + factoryId + " cannot be found", null); } + + private static ClassLoader getClassLoader() { + if (System.getSecurityManager() == null) { + return FactoryFinder.class.getClassLoader(); + } + return AccessController.doPrivileged((PrivilegedAction) FactoryFinder.class::getClassLoader); + } + + private static T findFirstService(final String factoryId, final ClassLoader cl, final Class service) { + final PrivilegedAction action = () -> { + try { + final ServiceLoader loader = ServiceLoader.load(service, cl); + if (loader.iterator().hasNext()) { + return loader.iterator().next(); + } + } catch (Exception e) { + LOGGER.log(Level.FINER, "Failed to load service " + factoryId + ".", e); + } + return null; + }; + if (System.getSecurityManager() == null) { + return action.run(); + } + return AccessController.doPrivileged(action); + } } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/sse/SseEventSink.java b/jaxrs-api/src/main/java/jakarta/ws/rs/sse/SseEventSink.java index ae2e55704..26c375353 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/sse/SseEventSink.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/sse/SseEventSink.java @@ -16,6 +16,7 @@ package jakarta.ws.rs.sse; +import java.io.IOException; import java.util.concurrent.CompletionStage; /** @@ -71,7 +72,8 @@ public interface SseEventSink extends AutoCloseable { *

    * Subsequent calls have no effect and are ignored. Once the {@link SseEventSink} is closed, invoking any method other * than this one and {@link #isClosed()} would result in an {@link IllegalStateException} being thrown. + * @throws IOException if an I/O error occurs. */ @Override - void close(); + void close() throws IOException; } diff --git a/jaxrs-api/src/main/java/jakarta/ws/rs/sse/SseEventSource.java b/jaxrs-api/src/main/java/jakarta/ws/rs/sse/SseEventSource.java index bc5f06d54..7df3f1fc5 100644 --- a/jaxrs-api/src/main/java/jakarta/ws/rs/sse/SseEventSource.java +++ b/jaxrs-api/src/main/java/jakarta/ws/rs/sse/SseEventSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2021 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 @@ -72,6 +72,11 @@ *

    * In the case of an error condition response, the Throwable passed to the onError consumer * should be a WebApplicationException containing the invalid Response object. + *

    + * Note that if, for any of the registered event consumers, an invocation of {@link Consumer#accept(Object) + * Consumer<InboundSseEvent>#accept(InboundSseEvent)} method throws an exception, this is not an error condition. + * Thus onError is not invoked and event processing is not stopped. + * Users are encouraged to handle exceptions on their own as part of the event processing logic. * * @author Marek Potociar * @since 2.1 @@ -136,6 +141,13 @@ static Builder newBuilder() { } } + /** + * Set the SSE streaming endpoint. + * + * @param endpoint SSE streaming endpoint. Must not be {@code null}. + * @return updated event source builder instance. + * @throws NullPointerException in case the supplied web target is {@code null}. + */ protected abstract Builder target(WebTarget endpoint); /** diff --git a/jaxrs-api/src/test/java/jakarta/ws/rs/SeBootstrapTest.java b/jaxrs-api/src/test/java/jakarta/ws/rs/SeBootstrapTest.java index ba3c14132..bc09f2e97 100644 --- a/jaxrs-api/src/test/java/jakarta/ws/rs/SeBootstrapTest.java +++ b/jaxrs-api/src/test/java/jakarta/ws/rs/SeBootstrapTest.java @@ -1,26 +1,30 @@ package jakarta.ws.rs; -import javax.net.ssl.SSLContext; -import java.util.concurrent.CompletionStage; - -import jakarta.ws.rs.SeBootstrap; -import jakarta.ws.rs.SeBootstrap.Configuration; -import jakarta.ws.rs.SeBootstrap.Configuration.SSLClientAuthentication; -import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.ext.RuntimeDelegate; -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.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import java.net.URI; +import java.util.concurrent.CompletionStage; + +import javax.net.ssl.SSLContext; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.core.UriBuilder; +import jakarta.ws.rs.SeBootstrap.Configuration; +import jakarta.ws.rs.SeBootstrap.Configuration.SSLClientAuthentication; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.ext.RuntimeDelegate; + /** * Unit tests for {@link SeBootstrap} * @@ -34,7 +38,7 @@ public final class SeBootstrapTest { * installed RuntimeDelegate. */ @BeforeEach - public final void setUp() { + public void setUp() { RuntimeDelegate.setInstance(mock(RuntimeDelegate.class)); } @@ -43,7 +47,7 @@ public final void setUp() { * use a possibly cluttered instance. */ @AfterEach - public final void tearDown() { + public void tearDown() { RuntimeDelegate.setInstance(null); } @@ -54,7 +58,7 @@ public final void tearDown() { * @since 3.1 */ @Test - public final void shouldDelegateApplicationStartupToRuntimeDelegate() { + public void shouldDelegateApplicationStartupToRuntimeDelegate() { // given final Application application = mock(Application.class); final Configuration configuration = mock(Configuration.class); @@ -69,6 +73,30 @@ public final void shouldDelegateApplicationStartupToRuntimeDelegate() { assertThat(actualCompletionStage, is(sameInstance(nativeCompletionStage))); } + /** + * Assert that {@link SeBootstrap#start(Class, Configuration)} will delegate to + * {@link RuntimeDelegate#bootstrap(Class, Configuration)}. + * + * @since 3.1 + */ + @Test + public void shouldDelegateClassApplicationStartupToRuntimeDelegate() { + // given + final Application application = mock(Application.class); + final Class clazz = application.getClass(); + final Configuration configuration = mock(Configuration.class); + @SuppressWarnings("unchecked") + final CompletionStage nativeCompletionStage = mock(CompletionStage.class); + given(RuntimeDelegate.getInstance().bootstrap(clazz, configuration)) + .willReturn(nativeCompletionStage); + + // when + final CompletionStage actualCompletionStage = SeBootstrap.start(clazz, configuration); + + // then + assertThat(actualCompletionStage, is(sameInstance(nativeCompletionStage))); + } + /** * Assert that {@link SeBootstrap.Configuration#builder()} will delegate to * {@link RuntimeDelegate#createConfigurationBuilder()}. @@ -76,7 +104,7 @@ public final void shouldDelegateApplicationStartupToRuntimeDelegate() { * @since 3.1 */ @Test - public final void shouldDelegateConfigurationBuilderCreationToRuntimeDelegate() { + public void shouldDelegateConfigurationBuilderCreationToRuntimeDelegate() { // given final SeBootstrap.Configuration.Builder nativeConfigurationBuilder = mock(SeBootstrap.Configuration.Builder.class); given(RuntimeDelegate.getInstance().createConfigurationBuilder()).willReturn(nativeConfigurationBuilder); @@ -95,7 +123,7 @@ public final void shouldDelegateConfigurationBuilderCreationToRuntimeDelegate() * @since 3.1 */ @Test - public final void shouldReturnSameConfigurationBuilderInstanceWhenLoadingExternalConfiguration() { + public void shouldReturnSameConfigurationBuilderInstanceWhenLoadingExternalConfiguration() { // given final SeBootstrap.Configuration.Builder previousConfigurationBuilder = spy(SeBootstrap.Configuration.Builder.class); final Object someExternalConfiguration = mock(Object.class); @@ -117,7 +145,7 @@ public final void shouldReturnSameConfigurationBuilderInstanceWhenLoadingExterna * @since 3.1 */ @Test - public final void shouldPushCorrespondingPropertiesIntoConfigurationBuilder() { + public void shouldPushCorrespondingPropertiesIntoConfigurationBuilder() { // given final String someProtocolValue = mockString(); final String someHostValue = mockString(); @@ -152,7 +180,7 @@ public final void shouldPushCorrespondingPropertiesIntoConfigurationBuilder() { * @since 3.1 */ @Test - public final void shouldPullCorrespondingPropertiesFromConfiguration() { + public void shouldPullCorrespondingPropertiesFromConfiguration() { // given final String someProtocolValue = mockString(); final String someHostValue = mockString(); @@ -186,6 +214,53 @@ public final void shouldPullCorrespondingPropertiesFromConfiguration() { assertThat(actualSSLClientAuthenticationValue, is(sameInstance(someSSLClientAuthenticationValue))); } + /** + * Assert that a default {@code Configuration} is used when not passed to the + * {@code SeBootstrap.start} method. + * + * @since 3.1 + */ + @Test + public void shouldUseDefaultConfigurationIfOmitted() { + // given + final Application application = mock(Application.class); + final Configuration configuration = mock(Configuration.class); + SeBootstrap.Configuration.Builder builder = mock(SeBootstrap.Configuration.Builder.class); + given(SeBootstrap.Configuration.builder()).willReturn(builder); + given(builder.build()).willReturn(configuration); + + // when + SeBootstrap.start(application); + + // then + verify(RuntimeDelegate.getInstance()).bootstrap(application, configuration); + } + + /** + * Assert that calling {@code Configuration.baseUri} returns the correct URI as set + * by the mocked classes. + * + * @since 3.1 + */ + @Test + public void shouldReturnSameUri() { + // given + final URI uri = URI.create("http://localhost:8080/foo"); + final UriBuilder uriBuilder = mock(UriBuilder.class); + given(uriBuilder.build()).willReturn(uri); + final Configuration configuration = mock(Configuration.class); + given(configuration.baseUri()).willCallRealMethod(); + given(configuration.baseUriBuilder()).willReturn(uriBuilder); + final SeBootstrap.Instance instance = mock(SeBootstrap.Instance.class); + given(instance.configuration()).willReturn(configuration); + + // when + URI returnedUri = instance.configuration().baseUri(); + + // then + assertThat(uri, is(returnedUri)); + } + private static String mockString() { return Integer.toString(mockInt()); } diff --git a/jaxrs-api/src/test/java/jakarta/ws/rs/core/CookieTest.java b/jaxrs-api/src/test/java/jakarta/ws/rs/core/CookieTest.java index afa6ff7d9..ab5720a90 100644 --- a/jaxrs-api/src/test/java/jakarta/ws/rs/core/CookieTest.java +++ b/jaxrs-api/src/test/java/jakarta/ws/rs/core/CookieTest.java @@ -20,27 +20,84 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class CookieTest extends BaseDelegateTest { - /** - * Test of equals method, of class Cookie and NewCookie. - */ @Test - public void testEquals() { - Object nullObj = null; + public final void shouldReturnFalseWhenComparingCookieToNullObject() { + Cookie cookie = new Cookie("name", "value"); + assertFalse(cookie.equals(null)); + + cookie = new Cookie.Builder("name").value("value").build(); + assertFalse(cookie.equals(null)); + } + + @Test + public final void shouldReturnFalseWhenComparingCookieToNewCookie() { Cookie cookie = new Cookie("name", "value"); - Cookie cookie1 = new Cookie("name", "value"); - Cookie cookie2 = new Cookie("name", "value2"); NewCookie newCookie = new NewCookie("name", "value"); - NewCookie newCookie1 = new NewCookie("name", "value"); - NewCookie newCookie2 = new NewCookie("name", "value2"); - assertFalse(cookie.equals(nullObj)); assertFalse(cookie.equals(newCookie)); + + Cookie thisCookie = new Cookie.Builder("name").value("value").build(); + NewCookie thatNewCookie = new NewCookie.Builder("name").value("value").build(); + assertFalse(thisCookie.equals(thatNewCookie)); + } + + @Test + public final void shouldReturnFalseWhenComparingCookiesThatHaveDifferentValues() { + Cookie cookie = new Cookie("name", "value"); + Cookie cookie2 = new Cookie("name", "value2"); assertFalse(cookie.equals(cookie2)); + + Cookie thisCookie = new Cookie.Builder("name").value("value").build(); + Cookie thatCookie = new Cookie.Builder("name").value("value2").build(); + assertFalse(thisCookie.equals(thatCookie)); + } + + @Test + public final void shouldReturnTrueWhenComparingCookiesThatHaveSameValues() { + + Cookie cookie = new Cookie("name", "value"); + Cookie cookie1 = new Cookie("name", "value"); + NewCookie newCookie = new NewCookie("name", "value"); assertTrue(cookie.equals(cookie1)); assertTrue(cookie.equals(newCookie.toCookie())); - assertTrue(newCookie.equals(newCookie1)); - assertFalse(newCookie.equals(newCookie2)); + + Cookie thisCookie = new Cookie.Builder("name").value("value").build(); + Cookie thatCookie = new Cookie.Builder("name").value("value").build(); + assertTrue(thisCookie.equals(thatCookie)); + + thatCookie = new NewCookie.Builder("name").value("value").build().toCookie(); + assertTrue(thisCookie.equals(thatCookie)); } + + @Test + public final void shouldThrowAnIllegalArgumentExceptionWhenBuildingCookieWithNullName() { + + try { + new Cookie(null, "value"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + new Cookie(null, "value", "path", "domain"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + new Cookie(null, "value", "path", "domain", Cookie.DEFAULT_VERSION); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + new Cookie.Builder(null).build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + } + } diff --git a/jaxrs-api/src/test/java/jakarta/ws/rs/core/NewCookieBuilderTest.java b/jaxrs-api/src/test/java/jakarta/ws/rs/core/NewCookieBuilderTest.java new file mode 100644 index 000000000..3a629f5dc --- /dev/null +++ b/jaxrs-api/src/test/java/jakarta/ws/rs/core/NewCookieBuilderTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2010, 2020 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 jakarta.ws.rs.core; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Unit tests for {@link NewCookie.Builder} + * + * @author Nicolas NESMON + * @since 3.1 + */ +public final class NewCookieBuilderTest extends BaseDelegateTest { + + @Test + public final void shouldReturnSuppliedCookieInformationWhenSuppliedCookieIsNotNull() { + + String expectedName = "name"; + String expectedValue = "value"; + int expectedVersion = 2; + String expectedPath = "/"; + String expectedDomain = "localhost"; + + Cookie cookie = new Cookie.Builder(expectedName) + .value(expectedValue) + .version(expectedVersion) + .path(expectedPath) + .domain(expectedDomain) + .build(); + NewCookie newCookie = new NewCookie.Builder(cookie).build(); + + assertEquals(expectedName, newCookie.getName()); + assertEquals(expectedValue, newCookie.getValue()); + assertEquals(expectedVersion, newCookie.getVersion()); + assertEquals(expectedPath, newCookie.getPath()); + assertEquals(expectedDomain, newCookie.getDomain()); + } + + @Test + public final void shouldThrowAnIllegalArgumentExceptionWhenSuppliedCookieIsNull() { + + try { + new NewCookie.Builder((Cookie) null).build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + new NewCookie.Builder((Cookie) null).comment("comment").maxAge(120).secure(true).build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public final void shouldReturnNullWhenSameSiteIsNotSet() { + NewCookie newCookie = new NewCookie.Builder("name").build(); + assertNull(newCookie.getSameSite()); + } + + @Test + public final void shouldReturnNullWhenSameSiteIsSetToNull() { + NewCookie newCookie = new NewCookie.Builder("name").sameSite(null).build(); + assertNull(newCookie.getSameSite()); + } + + @Test + public final void shouldReturnSuppliedValueWhenSameSiteIsSetToNonNullValue() { + + NewCookie.Builder newCookieBuilder = new NewCookie.Builder("name"); + + NewCookie newCookie = newCookieBuilder.sameSite(NewCookie.SameSite.NONE).build(); + assertEquals(NewCookie.SameSite.NONE, newCookie.getSameSite()); + + newCookie = newCookieBuilder.sameSite(NewCookie.SameSite.LAX).build(); + assertEquals(NewCookie.SameSite.LAX, newCookie.getSameSite()); + + newCookie = newCookieBuilder.sameSite(NewCookie.SameSite.STRICT).build(); + assertEquals(NewCookie.SameSite.STRICT, newCookie.getSameSite()); + } + +} diff --git a/jaxrs-api/src/test/java/jakarta/ws/rs/core/NewCookieTest.java b/jaxrs-api/src/test/java/jakarta/ws/rs/core/NewCookieTest.java index 4b2c88e71..7cffb2a94 100644 --- a/jaxrs-api/src/test/java/jakarta/ws/rs/core/NewCookieTest.java +++ b/jaxrs-api/src/test/java/jakarta/ws/rs/core/NewCookieTest.java @@ -16,54 +16,189 @@ package jakarta.ws.rs.core; +import jakarta.ws.rs.core.NewCookie.SameSite; +import java.util.Date; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; public class NewCookieTest extends BaseDelegateTest { - /** - * Test of valueOf method, of class NewCookie. - */ @Test - public void testCtor() { - Cookie c = new Cookie("name", "value"); - NewCookie nc = new NewCookie(c); - assertEquals(nc.getName(), c.getName()); + public void testSameSite() { + + NewCookie sameSiteOmit = new NewCookie("name", "value", "/", "localhost", 1, null, 0, null, false, false); + assertNull(sameSiteOmit.getSameSite()); + sameSiteOmit = new NewCookie.Builder("name").build(); + assertNull(sameSiteOmit.getSameSite()); + + NewCookie sameSiteNull = new NewCookie("name", "value", "/", "localhost", 1, null, 0, null, false, false, null); + assertNull(sameSiteNull.getSameSite()); + sameSiteNull = new NewCookie.Builder("name").sameSite(null).build(); + assertNull(sameSiteNull.getSameSite()); + + NewCookie sameSiteNone = new NewCookie("name", "value", "/", "localhost", 1, null, 0, null, false, false, NewCookie.SameSite.NONE); + assertEquals(NewCookie.SameSite.NONE, sameSiteNone.getSameSite()); + sameSiteNone = new NewCookie.Builder("name").sameSite(NewCookie.SameSite.NONE).build(); + assertEquals(NewCookie.SameSite.NONE, sameSiteNone.getSameSite()); + + NewCookie sameSiteLax = new NewCookie("name", "value", "/", "localhost", 1, null, 0, null, false, false, NewCookie.SameSite.LAX); + assertEquals(NewCookie.SameSite.LAX, sameSiteLax.getSameSite()); + sameSiteLax = new NewCookie.Builder("name").sameSite(NewCookie.SameSite.LAX).build(); + assertEquals(NewCookie.SameSite.LAX, sameSiteLax.getSameSite()); + + NewCookie sameSiteStrict = new NewCookie("name", "value", "/", "localhost", 1, null, 0, null, false, false, NewCookie.SameSite.STRICT); + assertEquals(NewCookie.SameSite.STRICT, sameSiteStrict.getSameSite()); + sameSiteStrict = new NewCookie.Builder("name").sameSite(NewCookie.SameSite.STRICT).build(); + assertEquals(NewCookie.SameSite.STRICT, sameSiteStrict.getSameSite()); + + } + + @Test + public final void shouldReturnFalseWhenComparingNewCookieToNullObject() { + NewCookie newCookie = new NewCookie("name", "value"); + assertFalse(newCookie.equals(null)); + + newCookie = new NewCookie.Builder("name").value("value").build(); + assertFalse(newCookie.equals(null)); + } + + @Test + public final void shouldReturnFalseWhenComparingNewCookieToCookie() { + Cookie cookie = new Cookie("name", "value"); + NewCookie newCookie = new NewCookie("name", "value"); + assertFalse(newCookie.equals(cookie)); + + NewCookie thisNewCookie = new NewCookie.Builder("name").value("value").build(); + Cookie thatCookie = new Cookie.Builder("name").value("value").build(); + assertFalse(thisNewCookie.equals(thatCookie)); + } + + @Test + public final void shouldReturnFalseWhenComparingNewCookiesThatHaveDifferentValues() { + NewCookie newCookie = new NewCookie("name", "value"); + NewCookie newCookie2 = new NewCookie("name", "value2"); + assertFalse(newCookie.equals(newCookie2)); + + NewCookie thisNewCookie = new NewCookie.Builder("name").value("value").build(); + NewCookie thatNewCookie = new NewCookie.Builder("name").value("value2").build(); + assertFalse(thisNewCookie.equals(thatNewCookie)); + } + + @Test + public final void shouldReturnTrueWhenComparingNewCookiesThatHaveSameValues() { + NewCookie newCookie = new NewCookie("name", "value"); + NewCookie newCookie1 = new NewCookie("name", "value"); + assertTrue(newCookie.equals(newCookie1)); + + NewCookie thisNewCookie = new NewCookie.Builder("name").value("value").build(); + NewCookie thatNewCookie = new NewCookie.Builder("name").value("value").build(); + assertTrue(thisNewCookie.equals(thatNewCookie)); + } + + @Test + public final void shouldReturnSuppliedCookiePropertiesWhenBuildingNewCookiesFromCookie() { + Cookie cookie = new Cookie("name", "value", "path", "domain", Cookie.DEFAULT_VERSION); + NewCookie newCookie = new NewCookie(cookie); + assertEquals(newCookie.getName(), cookie.getName()); + assertEquals(newCookie.getPath(), cookie.getPath()); + assertEquals(newCookie.getDomain(), cookie.getDomain()); + assertEquals(newCookie.getVersion(), cookie.getVersion()); + + cookie = new Cookie.Builder("name") + .value("value") + .path("path") + .domain("domain") + .version(Cookie.DEFAULT_VERSION) + .build(); + newCookie = new NewCookie.Builder(cookie).build(); + assertEquals(newCookie.getName(), cookie.getName()); + assertEquals(newCookie.getPath(), cookie.getPath()); + assertEquals(newCookie.getDomain(), cookie.getDomain()); + assertEquals(newCookie.getVersion(), cookie.getVersion()); + } + + @Test + public final void shouldThrowAnIllegalArgumentExceptionWhenBuildingNewCookieWithNullName() { + + try { + new NewCookie(null, null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + new NewCookie(null, "value", "path", "domain", "comment", NewCookie.DEFAULT_MAX_AGE, false); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + new NewCookie(null, "value", "path", "domain", "comment", NewCookie.DEFAULT_MAX_AGE, false, false); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + new NewCookie(null, "value", "path", "domain", Cookie.DEFAULT_VERSION, "comment", NewCookie.DEFAULT_MAX_AGE, false); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + new NewCookie(null, "value", "path", "domain", Cookie.DEFAULT_VERSION, "comment", NewCookie.DEFAULT_MAX_AGE, new Date(), false, false); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + try { - nc = new NewCookie(null); + new NewCookie(null, "value", "path", "domain", Cookie.DEFAULT_VERSION, "comment", NewCookie.DEFAULT_MAX_AGE, new Date(), false, false, SameSite.LAX); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { } + try { - nc = new NewCookie(null, "comment", 120, true); + new NewCookie.Builder((String) null).build(); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { } } @Test - public void testSameSite() { - NewCookie sameSiteOmit = new NewCookie("name", "value", "/", "localhost", 1, - null, 0, null, false, false); - assertNull(sameSiteOmit.getSameSite()); + public final void shouldThrowAnIllegalArgumentExceptionWhenBuildingNewCookieFromNullCookie() { - NewCookie sameSiteNull = new NewCookie("name", "value", "/", "localhost", 1, - null, 0, null, false, false, null); - assertNull(sameSiteNull.getSameSite()); + try { + new NewCookie((Cookie)null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } - NewCookie sameSiteNone = new NewCookie("name", "value", "/", "localhost", 1, - null, 0, null, false, false, NewCookie.SameSite.NONE); - assertEquals(NewCookie.SameSite.NONE, sameSiteNone.getSameSite()); + try { + new NewCookie(null, "comment", NewCookie.DEFAULT_MAX_AGE, false); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } - NewCookie sameSiteLax = new NewCookie("name", "value", "/", "localhost", 1, - null, 0, null, false, false, NewCookie.SameSite.LAX); - assertEquals(NewCookie.SameSite.LAX, sameSiteLax.getSameSite()); + try { + new NewCookie(null, "comment", NewCookie.DEFAULT_MAX_AGE, new Date(), false, false); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } - NewCookie sameSiteStrict = new NewCookie("name", "value", "/", "localhost", 1, - null, 0, null, false, false, NewCookie.SameSite.STRICT); - assertEquals(NewCookie.SameSite.STRICT, sameSiteStrict.getSameSite()); + try { + new NewCookie(null, "comment", NewCookie.DEFAULT_MAX_AGE, new Date(), false, false, SameSite.LAX); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + new NewCookie.Builder((Cookie) null).build(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } } }