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