TCK tests for Java SE Bootstrap API (and other post-3.0 additions) (#931)
Initial set of tests forming a new TCK for Jakarta RESTful Web Services.
* Removed unused import
* TCK for SeBootstrap
* TCK for UriBuilder
* README.md for TCK
* fixed typos
diff --git a/.travis.yml b/.travis.yml
index 44becc4..32de347 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,6 +27,8 @@
- mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
- cd $TRAVIS_BUILD_DIR/jaxrs-spec
- mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
+ - cd $TRAVIS_BUILD_DIR/jaxrs-tck
+ - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
script:
- cd $TRAVIS_BUILD_DIR/jaxrs-api
@@ -35,3 +37,5 @@
- mvn verify -B
- cd $TRAVIS_BUILD_DIR/jaxrs-spec
- mvn verify -B
+ - cd $TRAVIS_BUILD_DIR/jaxrs-tck
+ - mvn verify -B
diff --git a/Jenkinsfile b/Jenkinsfile
index 02c9289..4157858 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -28,6 +28,9 @@
dir ('jaxrs-spec') {
sh "$MVN deploy"
}
+ dir ('jaxrs-tck') {
+ sh "$MVN deploy"
+ }
}
}
}
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 87e5d28..aabda6b 100644
--- a/jaxrs-api/src/test/java/jakarta/ws/rs/SeBootstrapTest.java
+++ b/jaxrs-api/src/test/java/jakarta/ws/rs/SeBootstrapTest.java
@@ -17,7 +17,6 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.SeBootstrap.Configuration;
import jakarta.ws.rs.SeBootstrap.Configuration.SSLClientAuthentication;
import jakarta.ws.rs.core.Application;
diff --git a/jaxrs-tck/pom.xml b/jaxrs-tck/pom.xml
new file mode 100644
index 0000000..fd020d6
--- /dev/null
+++ b/jaxrs-tck/pom.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+ Copyright (c) 2020 Markus Karg. All rights reserved.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Public License v. 2.0, which is available at
+ http://www.eclipse.org/legal/epl-2.0.
+
+ This Source Code may also be made available under the following Secondary
+ Licenses when the conditions for such availability set forth in the
+ Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ version 2 with the GNU Classpath Exception, which is available at
+ https://www.gnu.org/software/classpath/license.html.
+
+ SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>jakarta.ws.rs</groupId>
+ <artifactId>jakarta.ws.rs-tck</artifactId>
+ <version>3.1-SNAPSHOT</version>
+
+ <name>Jakarta REST TCK</name>
+ <description>Technology Compatibility Kit for Jakarta RESTful Web Services</description>
+ <url>https://github.com/eclipse-ee4j/jaxrs-api</url>
+
+ <properties>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ </properties>
+
+ <organization>
+ <name>Eclipse Foundation</name>
+ <url>https://www.eclipse.org/org/foundation/</url>
+ </organization>
+
+ <developers>
+ <developer>
+ <id>developers</id>
+ <name>JAX-RS API Developers</name>
+ <email>jaxrs-dev@eclipse.org</email>
+ <url>https://github.com/eclipse-ee4j/jaxrs-api/graphs/contributors</url>
+ </developer>
+ </developers>
+
+ <issueManagement>
+ <system>Github</system>
+ <url>https://github.com/eclipse-ee4j/jaxrs-api/issues</url>
+ </issueManagement>
+
+ <mailingLists>
+ <mailingList>
+ <name>JAX-RS Developer Discussions</name>
+ <archive>jaxrs-dev@eclipse.org</archive>
+ </mailingList>
+ </mailingLists>
+
+ <licenses>
+ <license>
+ <name>EPL-2.0</name>
+ <url>http://www.eclipse.org/legal/epl-2.0</url>
+ <distribution>repo</distribution>
+ </license>
+ <license>
+ <name>GPL-2.0-with-classpath-exception</name>
+ <url>https://www.gnu.org/software/classpath/license.html</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <scm>
+ <connection>scm:git:https://github.com/eclipse-ee4j/jaxrs-api</connection>
+ <url>https://github.com/eclipse-ee4j/jaxrs-api</url>
+ <tag>HEAD</tag>
+ </scm>
+
+ <dependencies>
+ <dependency>
+ <groupId>jakarta.ws.rs</groupId>
+ <artifactId>jakarta.ws.rs-api</artifactId>
+ <version>3.1-SNAPSHOT</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <version>[5.5.2, 5.6-A00)</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-library</artifactId>
+ <version>[2.2, 2.3-A00)</version>
+ </dependency>
+ </dependencies>
+</project>
\ No newline at end of file
diff --git a/jaxrs-tck/src/main/java/jakarta/ws/rs/tck/sebootstrap/SeBootstrapIT.java b/jaxrs-tck/src/main/java/jakarta/ws/rs/tck/sebootstrap/SeBootstrapIT.java
new file mode 100644
index 0000000..356f63f
--- /dev/null
+++ b/jaxrs-tck/src/main/java/jakarta/ws/rs/tck/sebootstrap/SeBootstrapIT.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (c) 2020 Markus Karg. 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.tck.sebootstrap;
+
+import static java.util.concurrent.TimeUnit.HOURS;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.SeBootstrap;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.UriBuilder;
+
+/**
+ * Compliance Test for Java SE Bootstrap API of Jakarta REST API
+ *
+ * @author Markus KARG (markus@headcrashing.eu)
+ * @since 3.1
+ */
+@Timeout(value = 1, unit = HOURS)
+public final class SeBootstrapIT {
+
+ /**
+ * Verifies that an instance will boot using default configuration.
+ *
+ * @throws ExecutionException if the instance didn't boot correctly
+ * @throws InterruptedException if the test took much longer than usually
+ * expected
+ */
+ @Test
+ public final void shouldBootInstanceUsingDefaults() throws InterruptedException, ExecutionException {
+ // given
+ final int expectedResponse = mockInt();
+ final Application application = new StaticApplication(expectedResponse);
+ final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder();
+ final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.build();
+
+ // when
+ final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application,
+ requestedConfiguration);
+ final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get();
+ final SeBootstrap.Configuration actualConfiguration = instance.configuration();
+ final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol())
+ .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath())
+ .path("application/resource")).request().get(int.class);
+
+ // then
+ assertThat(actualResponse, is(expectedResponse));
+ assertThat(actualConfiguration.protocol(), is("HTTP"));
+ assertThat(actualConfiguration.host(), is("localhost"));
+ assertThat(actualConfiguration.port(), is(greaterThan(0)));
+ assertThat(actualConfiguration.rootPath(), is("/"));
+ instance.stop().toCompletableFuture().get();
+ }
+
+ /**
+ * Verifies that an instance will boot using explicit configuration given by
+ * properties.
+ *
+ * @throws ExecutionException if the instance didn't boot correctly
+ * @throws InterruptedException if the test took much longer than usually
+ * expected
+ * @throws IOException if no IP port was free
+ */
+ @Test
+ public final void shouldBootInstanceUsingProperties() throws InterruptedException, ExecutionException, IOException {
+ // given
+ final int expectedResponse = mockInt();
+ final Application application = new StaticApplication(expectedResponse);
+ final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder();
+ final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder
+ .property(SeBootstrap.Configuration.PROTOCOL, "HTTP")
+ .property(SeBootstrap.Configuration.HOST, "localhost")
+ .property(SeBootstrap.Configuration.PORT, someFreeIpPort())
+ .property(SeBootstrap.Configuration.ROOT_PATH, "/root/path").build();
+
+ // when
+ final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application,
+ requestedConfiguration);
+ final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get();
+ final SeBootstrap.Configuration actualConfiguration = instance.configuration();
+ final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol())
+ .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath())
+ .path("application/resource")).request().get(int.class);
+
+ // then
+ assertThat(actualResponse, is(expectedResponse));
+ assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol()));
+ assertThat(actualConfiguration.host(), is(requestedConfiguration.host()));
+ assertThat(actualConfiguration.port(), is(requestedConfiguration.port()));
+ assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath()));
+ instance.stop().toCompletableFuture().get();
+ }
+
+ /**
+ * Verifies that an instance will boot using explicit configuration given by
+ * convenience methods.
+ *
+ * @throws ExecutionException if the instance didn't boot correctly
+ * @throws InterruptedException if the test took much longer than usually
+ * expected
+ * @throws IOException if no IP port was free
+ */
+ @Test
+ public final void shouldBootInstanceUsingConvenienceMethods()
+ throws InterruptedException, ExecutionException, IOException {
+ // given
+ final int expectedResponse = mockInt();
+ final Application application = new StaticApplication(expectedResponse);
+ final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder();
+ final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.protocol("HTTP")
+ .host("localhost").port(someFreeIpPort()).rootPath("/root/path").build();
+
+ // when
+ final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application,
+ requestedConfiguration);
+ final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get();
+ final SeBootstrap.Configuration actualConfiguration = instance.configuration();
+ final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol())
+ .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath())
+ .path("application/resource")).request().get(int.class);
+
+ // then
+ assertThat(actualResponse, is(expectedResponse));
+ assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol()));
+ assertThat(actualConfiguration.host(), is(requestedConfiguration.host()));
+ assertThat(actualConfiguration.port(), is(requestedConfiguration.port()));
+ assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath()));
+ instance.stop().toCompletableFuture().get();
+ }
+
+ /**
+ * Verifies that an instance will boot using external configuration.
+ *
+ * @throws ExecutionException if the instance didn't boot correctly
+ * @throws InterruptedException if the test took much longer than usually
+ * expected
+ * @throws IOException if no IP port was free
+ */
+ @Test
+ public final void shouldBootInstanceUsingExternalConfiguration()
+ throws InterruptedException, ExecutionException, IOException {
+ // given
+ final int someFreeIpPort = someFreeIpPort();
+ final int expectedResponse = mockInt();
+ final Application application = new StaticApplication(expectedResponse);
+ final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder();
+ final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder
+ .from((property, type) -> {
+ switch (property) {
+ case SeBootstrap.Configuration.PROTOCOL:
+ return Optional.of("HTTP");
+ case SeBootstrap.Configuration.HOST:
+ return Optional.of("localhost");
+ case SeBootstrap.Configuration.PORT:
+ return Optional.of(someFreeIpPort);
+ case SeBootstrap.Configuration.ROOT_PATH:
+ return Optional.of("/root/path");
+ default:
+ return Optional.empty();
+ }
+ }).build();
+
+ // when
+ final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application,
+ requestedConfiguration);
+ final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get();
+ final SeBootstrap.Configuration actualConfiguration = instance.configuration();
+ final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol())
+ .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath())
+ .path("application/resource")).request().get(int.class);
+
+ // then
+ assertThat(actualResponse, is(expectedResponse));
+ assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol()));
+ assertThat(actualConfiguration.host(), is(requestedConfiguration.host()));
+ assertThat(actualConfiguration.port(), is(requestedConfiguration.port()));
+ assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath()));
+ instance.stop().toCompletableFuture().get();
+ }
+
+ /**
+ * Verifies that an instance will ignore unknown configuration parameters.
+ *
+ * @throws ExecutionException if the instance didn't boot correctly
+ * @throws InterruptedException if the test took much longer than usually
+ * expected
+ * @throws IOException if no IP port was free
+ */
+ @Test
+ public final void shouldBootInstanceDespiteUnknownConfigurationParameters()
+ throws InterruptedException, ExecutionException, IOException {
+ // given
+ final int expectedResponse = mockInt();
+ final Application application = new StaticApplication(expectedResponse);
+ final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder();
+ final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.protocol("HTTP")
+ .host("localhost").port(someFreeIpPort()).rootPath("/root/path").from((property, type) -> {
+ switch (property) {
+ case "jakarta.ws.rs.tck.sebootstrap.SeBootstrapIT$Unknown_1":
+ return Optional.of("Silently ignored value A");
+ default:
+ return Optional.empty();
+ }
+ }).property("jakarta.ws.rs.tck.sebootstrap.SeBootstrapIT$Unknown_2", "Silently ignored value B")
+ .from(new Object()).build();
+
+ // when
+ final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application,
+ requestedConfiguration);
+ final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get();
+ final SeBootstrap.Configuration actualConfiguration = instance.configuration();
+ final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol())
+ .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath())
+ .path("application/resource")).request().get(int.class);
+
+ // then
+ assertThat(actualResponse, is(expectedResponse));
+ assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol()));
+ assertThat(actualConfiguration.host(), is(requestedConfiguration.host()));
+ assertThat(actualConfiguration.port(), is(requestedConfiguration.port()));
+ assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath()));
+ instance.stop().toCompletableFuture().get();
+ }
+
+ /**
+ * Verifies that an instance will boot using a self-detected free IP port.
+ *
+ * @throws ExecutionException if the instance didn't boot correctly
+ * @throws InterruptedException if the test took much longer than usually
+ * expected
+ */
+ @Test
+ public final void shouldBootInstanceUsingSelfDetectedFreeIpPort()
+ throws InterruptedException, ExecutionException, IOException {
+ // given
+ final int expectedResponse = mockInt();
+ final Application application = new StaticApplication(expectedResponse);
+ final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder();
+ final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.protocol("HTTP")
+ .host("localhost").port(SeBootstrap.Configuration.FREE_PORT).rootPath("/root/path").build();
+
+ // when
+ final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application,
+ requestedConfiguration);
+ final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get();
+ final SeBootstrap.Configuration actualConfiguration = instance.configuration();
+ final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol())
+ .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath())
+ .path("application/resource")).request().get(int.class);
+
+ // then
+ assertThat(actualResponse, is(expectedResponse));
+ assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol()));
+ assertThat(actualConfiguration.host(), is(requestedConfiguration.host()));
+ assertThat(actualConfiguration.port(), is(greaterThan(0)));
+ assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath()));
+ instance.stop().toCompletableFuture().get();
+ }
+
+ /**
+ * Verifies that an instance will boot using the implementation's default IP
+ * port.
+ *
+ * @throws ExecutionException if the instance didn't boot correctly
+ * @throws InterruptedException if the test took much longer than usually
+ * expected
+ */
+ @Test
+ public final void shouldBootInstanceUsingImplementationsDefaultIpPort()
+ throws InterruptedException, ExecutionException, IOException {
+ // given
+ final int expectedResponse = mockInt();
+ final Application application = new StaticApplication(expectedResponse);
+ final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder();
+ final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.protocol("HTTP")
+ .host("localhost").port(SeBootstrap.Configuration.DEFAULT_PORT).rootPath("/root/path").build();
+
+ // when
+ final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application,
+ requestedConfiguration);
+ final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get();
+ final SeBootstrap.Configuration actualConfiguration = instance.configuration();
+ final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol())
+ .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath())
+ .path("application/resource")).request().get(int.class);
+
+ // then
+ assertThat(actualResponse, is(expectedResponse));
+ assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol()));
+ assertThat(actualConfiguration.host(), is(requestedConfiguration.host()));
+ assertThat(actualConfiguration.port(), is(greaterThan(0)));
+ assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath()));
+ instance.stop().toCompletableFuture().get();
+ }
+
+ private static Client client;
+
+ @BeforeAll
+ static void createClient() {
+ SeBootstrapIT.client = ClientBuilder.newClient();
+ }
+
+ @AfterAll
+ static void disposeClient() {
+ SeBootstrapIT.client.close();
+ }
+
+ @ApplicationPath("application")
+ public static final class StaticApplication extends Application {
+
+ private final StaticResource staticResource;
+
+ private StaticApplication(final long staticResponse) {
+ this.staticResource = new StaticResource(staticResponse);
+ }
+
+ @Override
+ public final Set<Object> getSingletons() {
+ return Collections.<Object>singleton(staticResource);
+ }
+
+ @Path("resource")
+ public static final class StaticResource {
+
+ private final long staticResponse;
+
+ private StaticResource(final long staticResponse) {
+ this.staticResponse = staticResponse;
+ }
+
+ @GET
+ public final long staticResponse() {
+ return this.staticResponse;
+ }
+ }
+ };
+
+ private static final int someFreeIpPort() throws IOException {
+ try (final ServerSocket serverSocket = new ServerSocket(0)) {
+ return serverSocket.getLocalPort();
+ }
+ }
+
+ private static final int mockInt() {
+ return (int) Math.round(Integer.MAX_VALUE * Math.random());
+ }
+}
diff --git a/jaxrs-tck/src/main/java/jakarta/ws/rs/tck/uribuilder/UriBuilderIT.java b/jaxrs-tck/src/main/java/jakarta/ws/rs/tck/uribuilder/UriBuilderIT.java
new file mode 100644
index 0000000..98290a2
--- /dev/null
+++ b/jaxrs-tck/src/main/java/jakarta/ws/rs/tck/uribuilder/UriBuilderIT.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2020 Markus Karg. 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.tck.uribuilder;
+
+import static java.util.concurrent.TimeUnit.HOURS;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.ExecutionException;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import jakarta.ws.rs.core.UriBuilder;
+import jakarta.ws.rs.core.UriBuilderException;
+
+/**
+ * Compliance Test for URI Builder API of Jakarta REST API
+ *
+ * @author Markus KARG (markus@headcrashing.eu)
+ * @since 3.1
+ */
+@Timeout(value = 1, unit = HOURS)
+public final class UriBuilderIT {
+
+ /**
+ * Verifies that a valid instance can be created from scratch.
+ *
+ * @throws ExecutionException if the instance didn't boot correctly
+ * @throws InterruptedException if the test took much longer than usually
+ * expected
+ */
+ @Test
+ public final void shouldBuildValidInstanceFromScratch()
+ throws InterruptedException, ExecutionException, URISyntaxException {
+ // given
+ final UriBuilder uriBuilder = UriBuilder.newInstance();
+
+ // when
+ final URI uri = uriBuilder.scheme("scheme").host("host").port(1).build();
+
+ // then
+ assertThat(uri.toString(), is("scheme://host:1"));
+ }
+
+ /**
+ * Verifies that no invalid URI can be created from scratch.
+ *
+ * @throws ExecutionException if the instance didn't boot correctly
+ * @throws InterruptedException if the test took much longer than usually
+ * expected
+ */
+ @Test
+ public final void shouldNotBuildInvalidUriFromScratch() throws InterruptedException, ExecutionException {
+ // given
+ final UriBuilder uriBuilder = UriBuilder.newInstance();
+
+ // then
+ assertThrows(UriBuilderException.class, /* when */ uriBuilder::build);
+ }
+}
diff --git a/jersey-tck/README.md b/jersey-tck/README.md
new file mode 100644
index 0000000..758d181
--- /dev/null
+++ b/jersey-tck/README.md
@@ -0,0 +1,17 @@
+# Jakarta REST TCK
+
+The **Jakarta REST TCK** is a standalone kit for testing compliance of an implementation with the Jakarta REST specification.
+
+**Note:** This TCK contains *only* tests being added since Jakarta REST 3.0. To proof compliance as part of a certification review, additional test have to be performed. Those are part of the Jakarta Platform TCK for historic reasons.
+
+
+## Performing Test
+
+While the test kit *is not dependent* of Maven, the most easy way to perform the tests is to create a copy of the sample Maven project found in the `jersey-tck` folder and adjust it to the actual needs of the implementation to be actually tested:
+* Replace the dependency to Jersey by a dependency to the implementation to be actually tested.
+* Execute `mvn verify`.
+* Find the test result as part of Maven's output on the console or refer to the surefire reports.
+
+**Note:** Certainly the same can be performed using *other* build tools, like Gradle, or even by running a standalone Jupiter API compatible test runner (e. g. JUnit 5 Console Runner), as long as the Jakarta REST TCK JAR file and the implementation to test are both found on the classpath.
+
+**Hint:** The test project can safely get stored as part of the implementation, so it can be easily executed as part of the QA process.
diff --git a/jersey-tck/pom.xml b/jersey-tck/pom.xml
new file mode 100644
index 0000000..35dad66
--- /dev/null
+++ b/jersey-tck/pom.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.glassfish.jersey.core</groupId>
+ <artifactId>jersey-tck</artifactId>
+ <version>3.1-SNAPSHOT</version>
+ <packaging>pom</packaging>
+
+ <name>Jakarta REST Compliance</name>
+ <description>This test verifies the compliance of Eclipse Jersey with Jakarta REST</description>
+
+ <properties>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ </properties>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.glassfish.jersey</groupId>
+ <artifactId>jersey-bom</artifactId>
+ <version>3.0.0</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>jakarta.ws.rs</groupId>
+ <artifactId>jakarta.ws.rs-tck</artifactId>
+ <version>3.1-SNAPSHOT</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.inject</groupId>
+ <artifactId>jersey-hk2</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.core</groupId>
+ <artifactId>jersey-server</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.containers</groupId>
+ <artifactId>jersey-container-grizzly2-http</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <version>3.0.0-M5</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ <configuration>
+ <dependenciesToScan>jakarta.ws.rs:jakarta.ws.rs-tck</dependenciesToScan>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
\ No newline at end of file