ValidatorTest.java

/*
 * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.server.model;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.logging.Logger;

import javax.ws.rs.BeanParam;
import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.Context;
import javax.ws.rs.sse.SseEventSink;

import javax.inject.Singleton;

import org.glassfish.jersey.Severity;
import org.glassfish.jersey.internal.BootstrapConfigurator;
import org.glassfish.jersey.internal.Errors;
import org.glassfish.jersey.internal.inject.InjectionManager;
import org.glassfish.jersey.internal.inject.Injections;
import org.glassfish.jersey.internal.inject.PerLookup;
import org.glassfish.jersey.internal.util.Producer;
import org.glassfish.jersey.message.internal.MessageBodyFactory;
import org.glassfish.jersey.server.ApplicationHandler;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ContainerResponse;
import org.glassfish.jersey.server.RequestContextBuilder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerBootstrapBag;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.TestConfigConfigurator;
import org.glassfish.jersey.server.internal.inject.ParamExtractorConfigurator;
import org.glassfish.jersey.server.internal.inject.ValueParamProviderConfigurator;
import org.glassfish.jersey.server.model.internal.ModelErrors;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Taken from Jersey 1: jersey-server:com.sun.jersey.server.impl.modelapi.validation.ResourceModelValidatorTest.java
 *
 * @author Jakub Podlesak
 */
public class ValidatorTest {

    private static final Logger LOGGER = Logger.getLogger(ValidatorTest.class.getName());

    @Path("rootNonAmbigCtors")
    public static class TestRootResourceNonAmbigCtors {

        // TODO: hmmm, even if this is not ambiguous, it is strange; shall we warn, the 1st and the 2nd ctor won't be used?
        public TestRootResourceNonAmbigCtors(@QueryParam("s") String s) {
        }

        public TestRootResourceNonAmbigCtors(@QueryParam("n") int n) {
        }

        public TestRootResourceNonAmbigCtors(@QueryParam("s") String s, @QueryParam("n") int n) {
        }

        @GET
        public String getIt() {
            return "it";
        }
    }

    @Test
    public void testRootResourceNonAmbigConstructors() throws Exception {
        LOGGER.info("No issue should be reported if more public ctors exists with the same number of params, "
                + "but another just one is presented with more params at a root resource:");
        Resource resource = Resource.builder(TestRootResourceNonAmbigCtors.class).build();
        ServerBootstrapBag serverBag = initializeApplication();
        ComponentModelValidator validator = new ComponentModelValidator(
                serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
        validator.validate(resource);
        assertTrue(validator.getIssueList().isEmpty());
    }


    public static class MyBeanParam {
        @HeaderParam("h")
        String hParam;
    }


    @Singleton
    @Path("rootSingleton/{p}")
    public static class TestCantInjectFieldsForSingleton {

        @MatrixParam("m")
        String matrixParam;
        @QueryParam("q")
        String queryParam;
        @PathParam("p")
        String pParam;
        @CookieParam("c")
        String cParam;
        @HeaderParam("h")
        String hParam;
        @BeanParam
        MyBeanParam beanParam;

        @GET
        public String getIt() {
            return "it";
        }
    }

    public static interface ChildOfContainerRequestFilter extends ContainerRequestFilter {
    }

    @Path("rootSingleton/{p}")
    public static class TestCantInjectFieldsForProvider implements ChildOfContainerRequestFilter {

        @MatrixParam("m")
        String matrixParam;
        @QueryParam("q")
        String queryParam;
        @PathParam("p")
        String pParam;
        @CookieParam("c")
        String cParam;
        @HeaderParam("h")
        String hParam;
        @BeanParam
        MyBeanParam beanParam;


        @GET
        public String getIt() {
            return "it";
        }

        @Override
        public void filter(ContainerRequestContext containerRequestContext) throws IOException {
        }
    }


    @Singleton
    @Path("rootSingletonConstructorB/{p}")
    public static class TestCantInjectConstructorParamsForSingleton {
        public TestCantInjectConstructorParamsForSingleton() {
        }

        public TestCantInjectConstructorParamsForSingleton(@QueryParam("q") String queryParam) {
        }

        @GET
        public String getIt() {
            return "it";
        }
    }

    @Singleton
    @Path("rootSingletonConstructorB/{p}")
    public static class TestCantInjectMethodParamsForSingleton {

        @GET
        public String getIt(@QueryParam("q") String queryParam) {
            return "it";
        }
    }

    @Singleton
    @Path("sseEventSinkWithReturnType")
    public static class TestSseEventSinkValidations {
        @Path("notVoid")
        @GET
        public SseEventSink nonVoidReturnType(@Context SseEventSink eventSink) {
            return eventSink;
        }

        @Path("multiple")
        @GET
        public void multipleInjection(@Context SseEventSink firstSink, @Context SseEventSink secondSink) {
            // do nothing
        }
    }

    @Path("rootRelaxedParser")
    @Produces(" a/b, c/d ")
    @Consumes({"e/f,g/h", " i/j"})
    public static class TestRelaxedProducesConsumesParserRules {

        @GET
        @Produces({"e/f,g/h", " i/j"})
        @Consumes(" a/b, c/d ")
        public String getIt(@QueryParam("q") String queryParam) {
            return "it";
        }
    }

    @Test
    public void testRelaxedProducesConsumesParserRules() throws Exception {
        LOGGER.info("An issue should not be reported with the relaxed Produces/Consumes values parser.");
        List<ResourceModelIssue> issues = testResourceValidation(TestCantInjectMethodParamsForSingleton.class);
        assertTrue(issues.isEmpty());
    }

    @Test
    public void testSingletonFieldsInjection() throws Exception {
        LOGGER.info("An issue should be reported if injection is required for a singleton life-cycle:");
        List<ResourceModelIssue> issues = testResourceValidation(TestCantInjectFieldsForSingleton.class);
        assertTrue(!issues.isEmpty());
        assertEquals(6, issues.size());
    }

    @Test
    public void testCantReturnFromEventSinkInjectedMethod() {
        LOGGER.info("An issue should be reported if method with injected SseEventSink parameter does not return void.");
        final List<ResourceModelIssue> issues = testResourceValidation(TestSseEventSinkValidations.class);
        assertTrue(!issues.isEmpty());
        assertEquals(2, issues.size());
    }


    @Test
    public void testProviderFieldsInjection() throws Exception {
        LOGGER.info("An issue should be reported if injection is required for a class which is provider and "
                + "therefore singleton:");
        List<ResourceModelIssue> issues = testResourceValidation(TestCantInjectFieldsForProvider.class);
        assertTrue(!issues.isEmpty());
        assertEquals(7, issues.size());
    }


    @Test
    public void testSingletonConstructorParamsInjection() throws Exception {
        LOGGER.info("An issue should be reported if injection is required for a singleton life-cycle:");
        List<ResourceModelIssue> issues = testResourceValidation(TestCantInjectConstructorParamsForSingleton.class);
        assertTrue(!issues.isEmpty());
        assertEquals(1, issues.size());
    }

    @Test
    public void testSingletonMethodParamsInjection() throws Exception {
        LOGGER.info("An issue should not be reported as injections into the methods are allowed.");
        List<ResourceModelIssue> issues = testResourceValidation(TestCantInjectMethodParamsForSingleton.class);
        assertTrue(issues.isEmpty());
    }

    @Path("resourceAsProvider")
    public static class ResourceAsProvider implements ContainerRequestFilter {

        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
        }

        @GET
        public String get() {
            return "get";
        }
    }

    @Singleton
    @PerLookup
    @Path("resourceMultipleScopes")
    public static class ResourceWithMultipleScopes implements ContainerRequestFilter {

        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
        }

        @GET
        public String get() {
            return "get";
        }
    }

    @Test
    public void testResourceAsProvider() throws Exception {
        LOGGER.info("An issue should be reported as resource implements provider but does not define scope.");
        List<ResourceModelIssue> issues = testResourceValidation(ResourceAsProvider.class);
        assertEquals(1, issues.size());
    }

    @Test
    public void testResourceWithMultipleScopes() throws Exception {
        LOGGER.info("An issue should not be reported as resource defines multiple scopes.");
        List<ResourceModelIssue> issues = testResourceValidation(ResourceWithMultipleScopes.class);
        assertEquals(1, issues.size());
    }


    private List<ResourceModelIssue> testResourceValidation(final Class<?>... resourceClasses) {
        return Errors.process(new Producer<List<ResourceModelIssue>>() {
            @Override
            public List<ResourceModelIssue> call() {
                List<Resource> resources = new ArrayList<>();
                for (Class<?> clazz : resourceClasses) {
                    final Resource res = Resource.builder(clazz).build();
                    if (res.getPath() != null) {
                        resources.add(res);
                    }
                }

                ResourceModel model = new ResourceModel.Builder(resources, false).build();
                ServerBootstrapBag serverBag = initializeApplication();
                ComponentModelValidator validator = new ComponentModelValidator(
                        serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
                validator.validate(model);
                return ModelErrors.getErrorsAsResourceModelIssues();
            }
        });
    }


    public static class TestNonPublicRM {

        @POST
        public String publicPost() {
            return "public";
        }

        @GET
        private String privateGet() {
            return "private";
        }
    }

    @Test
    public void testNonPublicRM() throws Exception {
        LOGGER.info("An issue should be reported if a resource method is not public:");

        List<ResourceModelIssue> issues = testResourceValidation(TestNonPublicRM.class);
        assertTrue(!issues.isEmpty());
    }

    public static class TestMoreThanOneEntity {

        @PUT
        public void put(String one, String two) {
        }
    }

    @Test
    @Disabled("Multiple entity validation not implemented yet.")
    // TODO implement validation
    public void suspendedTestMoreThanOneEntity() throws Exception {
        LOGGER.info("An issue should be reported if a resource method takes more than one entity params:");
        List<ResourceModelIssue> issues = testResourceValidation(TestMoreThanOneEntity.class);

        assertTrue(!issues.isEmpty());
    }

    @Path("test")
    public static class TestGetRMReturningVoid {

        @GET
        public void getMethod() {
        }
    }

    @Test
    public void testGetRMReturningVoid() throws Exception {
        LOGGER.info("An issue should be reported if a non-async get method returns void:");
        List<ResourceModelIssue> issues = testResourceValidation(TestGetRMReturningVoid.class);
        assertFalse(issues.isEmpty());
        assertNotEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    public static class TestAsyncGetRMReturningVoid {

        @GET
        public void getMethod(@Suspended AsyncResponse ar) {
        }
    }

    @Test
    public void testAsyncGetRMReturningVoid() throws Exception {
        LOGGER.info("An issue should NOT be reported if a async get method returns void:");
        List<ResourceModelIssue> issues = testResourceValidation(TestAsyncGetRMReturningVoid.class);
        assertTrue(issues.isEmpty());
    }

    @Path("test")
    public static class TestGetRMConsumingEntity {

        @GET
        public String getMethod(Object o) {
            return "it";
        }
    }

    @Test
    public void testGetRMConsumingEntity() throws Exception {
        LOGGER.info("An issue should be reported if a get method consumes an entity:");
        List<ResourceModelIssue> issues = testResourceValidation(TestGetRMConsumingEntity.class);
        assertFalse(issues.isEmpty());
        assertNotEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Path("test")
    public static class TestGetRMConsumingFormParam {

        @GET
        public String getMethod(@FormParam("f") String s1, @FormParam("g") String s2) {
            return "it";
        }
    }

    @Test
    public void testGetRMConsumingFormParam() throws Exception {
        LOGGER.info("An issue should be reported if a get method consumes a form param:");
        List<ResourceModelIssue> issues = testResourceValidation(TestGetRMConsumingFormParam.class);
        assertTrue(issues.size() == 1);
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Path("test")
    public static class TestSRLReturningVoid {

        @Path("srl")
        public void srLocator() {
        }
    }

    @Test
    public void testSRLReturningVoid() throws Exception {
        LOGGER.info("An issue should be reported if a sub-resource locator returns void:");
        Resource resource = Resource.builder(TestSRLReturningVoid.class).build();
        ServerBootstrapBag serverBag = initializeApplication();
        ComponentModelValidator validator = new ComponentModelValidator(
                serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
        validator.validate(resource);
        assertTrue(validator.fatalIssuesFound());
    }

    @Path("test")
    public static class TestGetSRMReturningVoid {

        @GET
        @Path("srm")
        public void getSRMethod() {
        }
    }

    @Test
    public void testGetSRMReturningVoid() throws Exception {
        LOGGER.info("An issue should be reported if a get sub-resource method returns void:");
        List<ResourceModelIssue> issues = testResourceValidation(TestGetSRMReturningVoid.class);

        assertFalse(issues.isEmpty());
        assertNotEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Path("test")
    public static class TestGetSRMConsumingEntity {

        @Path("p")
        @GET
        public String getMethod(Object o) {
            return "it";
        }
    }

    @Test
    public void testGetSRMConsumingEntity() throws Exception {
        LOGGER.info("An issue should be reported if a get method consumes an entity:");
        List<ResourceModelIssue> issues = testResourceValidation(TestGetSRMConsumingEntity.class);

        assertFalse(issues.isEmpty());
        assertNotEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Path("root")
    public static class TestGetSRMConsumingFormParam {

        @GET
        @Path("p")
        public String getMethod(@FormParam("f") String formParam) {
            return "it";
        }
    }

    @Test
    public void testGetSRMConsumingFormParam() throws Exception {
        LOGGER.info("An issue should be reported if a get method consumes a form param:");
        List<ResourceModelIssue> issues = testResourceValidation(TestGetSRMConsumingFormParam.class);

        assertFalse(issues.isEmpty());
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Path("rootMultipleHttpMethodDesignatorsRM")
    public static class TestMultipleHttpMethodDesignatorsRM {

        @GET
        @PUT
        public String getPutIt() {
            return "it";
        }
    }

    @Test
    public void testMultipleHttpMethodDesignatorsRM() throws Exception {
        LOGGER.info("An issue should be reported if more than one HTTP method designator exist on a resource "
                + "method:");
        Resource resource = Resource.builder(TestMultipleHttpMethodDesignatorsRM.class).build();
        ServerBootstrapBag serverBag = initializeApplication();
        ComponentModelValidator validator = new ComponentModelValidator(
                serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
        validator.validate(resource);
        assertTrue(validator.fatalIssuesFound());
    }

    @Path("rootMultipleHttpMethodDesignatorsSRM")
    public static class TestMultipleHttpMethodDesignatorsSRM {

        @Path("srm")
        @POST
        @PUT
        public String postPutIt() {
            return "it";
        }
    }

    @Test
    public void testMultipleHttpMethodDesignatorsSRM() throws Exception {
        LOGGER.info("An issue should be reported if more than one HTTP method designator exist on a sub-resource "
                + "method:");
        Resource resource = Resource.builder(TestMultipleHttpMethodDesignatorsSRM.class).build();
        ServerBootstrapBag serverBag = initializeApplication();
        ComponentModelValidator validator = new ComponentModelValidator(
                serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
        validator.validate(resource);
        assertTrue(validator.fatalIssuesFound());
    }

    @Path("rootEntityParamOnSRL")
    public static class TestEntityParamOnSRL {

        @Path("srl")
        public String locator(String s) {
            return "it";
        }
    }

    @Test
    public void testEntityParamOnSRL() throws Exception {
        LOGGER.info("An issue should be reported if an entity parameter exists on a sub-resource locator:");
        Resource resource = Resource.builder(TestEntityParamOnSRL.class).build();
        ServerBootstrapBag serverBag = initializeApplication();
        ComponentModelValidator validator = new ComponentModelValidator(
                serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
        validator.validate(resource);
        assertTrue(validator.fatalIssuesFound());
    }

    @Path(value = "/DeleteTest")
    public static class TestNonConflictingHttpMethodDelete {

        static String html_content =
                "<html>" + "<head><title>Delete text/html</title></head>"
                        + "<body>Delete text/html</body></html>";

        @DELETE
        @Produces(value = "text/plain")
        public String getPlain() {
            return "Delete text/plain";
        }

        @DELETE
        @Produces(value = "text/html")
        public String getHtml() {
            return html_content;
        }

        @DELETE
        @Path(value = "/sub")
        @Produces(value = "text/html")
        public String getSub() {
            return html_content;
        }
    }

    @Test
    public void testNonConflictingHttpMethodDelete() throws Exception {
        LOGGER.info("No issue should be reported if produced mime types differ");
        List<ResourceModelIssue> issues = testResourceValidation(TestNonConflictingHttpMethodDelete.class);

        assertTrue(issues.isEmpty());
    }

    @Path(value = "/AmbigParamTest")
    public static class TestAmbiguousParams {

        @QueryParam("q")
        @HeaderParam("q")
        private int a;

        @QueryParam("b")
        @HeaderParam("b")
        @MatrixParam("q")
        public void setB(String b) {
        }

        @GET
        @Path("a")
        public String get(@PathParam("a") @QueryParam("a") String a) {
            return "hi";
        }

        @GET
        @Path("b")
        public String getSub(@PathParam("a") @QueryParam("b") @MatrixParam("c") String a,
                             @MatrixParam("m") @QueryParam("m") int i) {
            return "hi";
        }

        @Path("c")
        public Object getSubLoc(@MatrixParam("m") @CookieParam("c") String a) {
            return null;
        }
    }

    @Test
    public void testAmbiguousParams() throws Exception {
        LOGGER.info("A warning should be reported if ambiguous source of a parameter is seen");
        Errors.process(new Runnable() {
            @Override
            public void run() {
                Resource resource = Resource.builder(TestAmbiguousParams.class).build();
                ServerBootstrapBag serverBag = initializeApplication();
                ComponentModelValidator validator = new ComponentModelValidator(
                        serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
                validator.validate(resource);

                assertTrue(!validator.fatalIssuesFound());
                assertEquals(4, validator.getIssueList().size());
                assertEquals(6, Errors.getErrorMessages().size());
            }
        });
    }

    @Path(value = "/EmptyPathSegmentTest")
    public static class TestEmptyPathSegment {

        @GET
        @Path("/")
        public String get() {
            return "hi";
        }
    }

    @Test
    public void testEmptyPathSegment() throws Exception {
        LOGGER.info("A warning should be reported if @Path with \"/\" or empty string value is seen");
        Resource resource = Resource.builder(TestEmptyPathSegment.class).build();
        ServerBootstrapBag serverBag = initializeApplication();
        ComponentModelValidator validator = new ComponentModelValidator(
                serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
        validator.validate(resource);

        assertTrue(!validator.fatalIssuesFound());
        assertEquals(1, validator.getIssueList().size());
    }


    public static class TypeVariableResource<T, V> {
        @QueryParam("v")
        V fieldV;

        V methodV;

        @QueryParam("v")
        public void set(V methodV) {
            this.methodV = methodV;
        }

        @GET
        public String get(@BeanParam() V getV) {
            return getV.toString() + fieldV.toString() + methodV.toString();
        }

        @POST
        public T post(T t) {
            return t;
        }

        @Path("sub")
        @POST
        public T postSub(T t) {
            return t;
        }
    }

    @Test
    public void testTypeVariableResource() throws Exception {
        LOGGER.info("");
        Errors.process(new Runnable() {
            @Override
            public void run() {
                Resource resource = Resource.builder(TypeVariableResource.class).build();
                ServerBootstrapBag serverBag = initializeApplication();
                ComponentModelValidator validator = new ComponentModelValidator(
                        serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
                validator.validate(resource);

                assertTrue(!validator.fatalIssuesFound());
                assertEquals(5, validator.getIssueList().size());
                assertEquals(7, Errors.getErrorMessages().size());
            }
        });
    }

    public static class ParameterizedTypeResource<T, V> {
        @QueryParam("v")
        Collection<V> fieldV;

        List<List<V>> methodV;

        @QueryParam("v")
        public void set(List<List<V>> methodV) {
            this.methodV = methodV;
        }

        @GET
        public String get(@QueryParam("v") List<V> getV) {
            return "";
        }

        @POST
        public Collection<T> post(Collection<T> t) {
            return t;
        }

        @Path("sub")
        @POST
        public Collection<T> postSub(Collection<T> t) {
            return t;
        }
    }

    public static class ConcreteParameterizedTypeResource extends ParameterizedTypeResource<String, String> {
    }

    @Test
    public void testParameterizedTypeResource() throws Exception {
        LOGGER.info("");
        Resource resource = Resource.builder(ConcreteParameterizedTypeResource.class).build();
        ServerBootstrapBag serverBag = initializeApplication();
        ComponentModelValidator validator = new ComponentModelValidator(
                serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
        validator.validate(resource);

        assertTrue(!validator.fatalIssuesFound());
        assertEquals(0, validator.getIssueList().size());
    }

    public static class GenericArrayResource<T, V> {
        @QueryParam("v")
        V[] fieldV;

        V[] methodV;

        @QueryParam("v")
        public void set(V[] methodV) {
            this.methodV = methodV;
        }

        @POST
        public T[] post(T[] t) {
            return t;
        }

        @Path("sub")
        @POST
        public T[] postSub(T[] t) {
            return t;
        }
    }

    public static class ConcreteGenericArrayResource extends GenericArrayResource<String, String> {
    }

    @Test
    public void testGenericArrayResource() throws Exception {
        LOGGER.info("");
        Resource resource = Resource.builder(ConcreteGenericArrayResource.class).build();
        ServerBootstrapBag serverBag = initializeApplication();
        ComponentModelValidator validator = new ComponentModelValidator(
                serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
        validator.validate(resource);

        assertTrue(!validator.fatalIssuesFound());
        assertEquals(0, validator.getIssueList().size());
    }

    // TODO: test multiple root resources with the same uriTemplate (in WebApplicationImpl.processRootResources ?)

    @Path("test1")
    public static class PercentEncodedTest {
        @GET
        @Path("%5B%5D")
        public String percent() {
            return "percent";
        }

        @GET
        @Path("[]")
        public String notEncoded() {
            return "not-encoded";
        }
    }

    @Test
    public void testPercentEncoded() throws Exception {
        List<ResourceModelIssue> issues = testResourceValidation(PercentEncodedTest.class);
        assertEquals(1, issues.size());
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }


    @Path("test2")
    public static class PercentEncodedCaseSensitiveTest {
        @GET
        @Path("%5B%5D")
        public String percent() {
            return "percent";
        }

        @GET
        @Path("%5b%5d")
        public String notEncoded() {
            return "not-encoded";
        }
    }

    @Test
    public void testPercentEncodedCaseSensitive() throws Exception {
        List<ResourceModelIssue> issues = testResourceValidation(PercentEncodedCaseSensitiveTest.class);
        assertEquals(1, issues.size());
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Path("ambiguous-parameter")
    public static class AmbiguousParameterResource {
        @POST
        public String moreNonAnnotatedParameters(@HeaderParam("something") String header, String entity1, String entity2) {
            return "x";
        }
    }

    @Test
    public void testNotAnnotatedParameters() throws Exception {
        Resource resource = Resource.builder(AmbiguousParameterResource.class).build();
        ServerBootstrapBag serverBag = initializeApplication();
        ComponentModelValidator validator = new ComponentModelValidator(
                serverBag.getValueParamProviders(), serverBag.getMessageBodyWorkers());
        validator.validate(resource);

        final List<ResourceModelIssue> errorMessages = validator.getIssueList();
        assertEquals(1, errorMessages.size());
        assertEquals(Severity.FATAL, errorMessages.get(0).getSeverity());
    }


    public static class SubResource {
        public static final String MESSAGE = "Got it!";

        @GET
        public String getIt() {
            return MESSAGE;
        }
    }

    /**
     * Should report warning during validation as Resource cannot have resource method and sub resource locators on the same path.
     */
    @Path("failRoot")
    public static class MethodAndLocatorResource {


        @Path("/")
        public SubResource getSubResourceLocator() {
            return new SubResource();
        }

        @GET
        public String get() {
            return "should never be called - fails during validation";
        }
    }


    @Test
    public void testLocatorAndMethodValidation() throws Exception {
        LOGGER.info("Should report warning during validation as Resource cannot have resource method and sub "
                + "resource locators on the same path.");
        List<ResourceModelIssue> issues = testResourceValidation(MethodAndLocatorResource.class);
        assertEquals(1, issues.size());
        assertNotEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    /**
     * Should report warning during validation as Resource cannot have resource method and sub resource locators on the same path.
     */
    @Path("failRoot")
    public static class MethodAndLocatorResource2 {


        @Path("a")
        public SubResource getSubResourceLocator() {
            return new SubResource();
        }

        @GET
        @Path("a")
        public String get() {
            return "should never be called - fails during validation";
        }
    }


    @Test
    public void testLocatorAndMethod2Validation() throws Exception {
        LOGGER.info("Should report warning during validation as Resource cannot have resource method and sub "
                + "resource locators on the same path.");
        List<ResourceModelIssue> issues = testResourceValidation(MethodAndLocatorResource2.class);
        assertEquals(1, issues.size());
        assertNotEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    /**
     * Warning should be reported informing wich locator will be used in runtime
     */
    @Path("locator")
    public static class TwoLocatorsResource {
        @Path("a")
        public SubResource locator() {
            return new SubResource();
        }

        @Path("a")
        public SubResource locator2() {
            return new SubResource();
        }
    }

    @Test
    public void testLocatorPathValidationFail() throws Exception {
        LOGGER.info("Should report error during validation as Resource cannot have ambiguous sub resource locators.");
        List<ResourceModelIssue> issues = testResourceValidation(TwoLocatorsResource.class);
        assertEquals(1, issues.size());
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Path("root")
    public static class ResourceRoot {
        @GET
        @Path("sub-root") // in path collision with ResourceSubRoot.get()
        public String get() {
            return "should never be called - fails during validation";
        }
    }

    @Path("root/sub-root")
    public static class ResourceSubPathRoot {
        @GET
        public String get() {
            return "should never be called - fails during validation";
        }
    }

    @Path("root")
    public static class ResourceRootNotUnique {
        @GET
        @Path("sub-root") // in path collision with ResourceSubRoot.get()
        public String get() {
            return "should never be called - fails during validation";
        }
    }

    @Test
    @Disabled
    // TODO: need to add validation to detect ambiguous problems of ResourceSubPathRoot and two other resources.
    public void testTwoOverlappingSubResourceValidation() throws Exception {
        List<ResourceModelIssue> issues = testResourceValidation(ResourceRoot.class, ResourceSubPathRoot.class);
        assertEquals(1, issues.size());
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Test
    @Disabled
    public void testTwoOverlappingResourceValidation() throws Exception {
        List<ResourceModelIssue> issues = testResourceValidation(ResourceRoot.class, ResourceRootNotUnique.class);
        assertEquals(1, issues.size());
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Path("root")
    public static class EmptyResource {
        public String get() {
            return "not a get method.";
        }
    }

    @Test
    public void testEmptyResourcel() throws Exception {
        LOGGER.info("Should report warning during validation as Resource cannot have resource method and sub "
                + "resource locators on the same path.");
        List<ResourceModelIssue> issues = testResourceValidation(EmptyResource.class);
        assertEquals(1, issues.size());
        assertFalse(issues.get(0).getSeverity() == Severity.FATAL);
    }


    @Path("{abc}")
    public static class AmbiguousResource1 {
        @Path("x")
        @GET
        public String get() {
            return "get";
        }
    }

    @Path("{def}")
    public static class AmbiguousResource2 {
        @Path("x")
        @GET
        public String get() {
            return "get";
        }
    }

    @Path("unique")
    public static class UniqueResource {
        @Path("x")
        public String get() {
            return "get";
        }
    }

    @Test
    public void testAmbiguousResources() throws Exception {
        LOGGER.info("Should report warning during validation error as resource path patterns are ambiguous ({abc} and {def} "
                + "results into same path pattern).");
        List<ResourceModelIssue> issues = testResourceValidation(AmbiguousResource1.class, AmbiguousResource2.class,
                UniqueResource.class);
        assertEquals(1, issues.size());
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }


    @Path("{abc}")
    public static class AmbiguousLocatorResource1 {
        @Path("x")
        public SubResource locator() {
            return new SubResource();
        }
    }

    @Path("{def}")
    public static class AmbiguousLocatorResource2 {
        @Path("x")
        public SubResource locator2() {
            return new SubResource();
        }
    }

    @Test
    public void testAmbiguousResourceLocators() throws Exception {
        LOGGER.info("Should report warning during validation error as resource path patterns are ambiguous ({abc} and {def} "
                + "results into same path pattern).");
        List<ResourceModelIssue> issues = testResourceValidation(AmbiguousLocatorResource1.class,
                AmbiguousLocatorResource2.class);
        assertEquals(1, issues.size());
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Path("resource")
    public static class ResourceMethodWithVoidReturnType {
        @GET
        @Path("error")
        public void error() {
        }
    }

    @Test
    public void testVoidReturnType() throws Exception {
        LOGGER.info("Should report hint during validation as @GET resource method returns void.");
        List<ResourceModelIssue> issues = testResourceValidation(ResourceMethodWithVoidReturnType.class);

        assertEquals(1, issues.size());
        assertEquals(Severity.HINT, issues.get(0).getSeverity());
    }

    @Path("paramonsetter")
    public static class ResourceWithParamOnSetter {
        @QueryParam("id")
        public void setId(String id) {
        }

        @GET
        public String get() {
            return "get";
        }
    }

    @Test
    public void testParamOnSetterIsOk() {
        LOGGER.info("Validation should report no issues.");
        List<ResourceModelIssue> issues = testResourceValidation(ResourceWithParamOnSetter.class);

        assertEquals(0, issues.size());
    }

    @Path("paramonresourcepath")
    public static class ResourceWithParamOnResourcePathAnnotatedMethod {
        @QueryParam("id")
        @Path("fail")
        public String query() {
            return "post";
        }
    }

    @Test
    public void testParamOnResourcePathAnnotatedMethodFails() {
        LOGGER.info("Should report fatal during validation as @Path method should not be annotated with parameter annotation");
        List<ResourceModelIssue> issues = testResourceValidation(ResourceWithParamOnResourcePathAnnotatedMethod.class);

        assertEquals(1, issues.size());
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    @Path("paramonresourceget")
    public static class ResourceGETMethodFails {
        @QueryParam("id")
        @GET
        public String get(@PathParam("abc") String id) {
            return "get";
        }
    }

    @Test
    public void testParamOnResourceGETMethodFails() {
        LOGGER.info("Should report fatal during validation as @GET method should not be annotated with parameter annotation");
        List<ResourceModelIssue> issues = testResourceValidation(ResourceGETMethodFails.class);

        assertEquals(1, issues.size());
        assertEquals(Severity.FATAL, issues.get(0).getSeverity());
    }

    /**
     * Test of disabled validation failing on errors.
     */
    @Path("test-disable-validation-fail-on-error")
    public static class TestDisableValidationFailOnErrorResource {
        @GET
        public String get() {
            return "PASSED";
        }
    }

    @Test
    public void testDisableFailOnErrors() throws ExecutionException, InterruptedException {
        final ResourceConfig rc = new ResourceConfig(
                AmbiguousLocatorResource1.class,
                AmbiguousLocatorResource2.class,
                AmbiguousParameterResource.class,
                AmbiguousResource1.class,
                AmbiguousResource2.class,
                ConcreteGenericArrayResource.class,
                ConcreteParameterizedTypeResource.class,
                EmptyResource.class,
                GenericArrayResource.class,
                MethodAndLocatorResource.class,
                MethodAndLocatorResource2.class,
                MyBeanParam.class,
                ParameterizedTypeResource.class,
                PercentEncodedCaseSensitiveTest.class,
                PercentEncodedTest.class,
                ResourceAsProvider.class,
                ResourceGETMethodFails.class,
                ResourceMethodWithVoidReturnType.class,
                ResourceRoot.class,
                ResourceRootNotUnique.class,
                ResourceSubPathRoot.class,
                ResourceWithMultipleScopes.class,
                ResourceWithParamOnResourcePathAnnotatedMethod.class,
                TestAmbiguousParams.class,
                TestAsyncGetRMReturningVoid.class,
                TestEmptyPathSegment.class,
                TestEntityParamOnSRL.class,
                TestGetRMConsumingEntity.class,
                TestGetRMConsumingFormParam.class,
                TestGetRMReturningVoid.class,
                TestGetSRMConsumingEntity.class,
                TestGetSRMConsumingFormParam.class,
                TestGetSRMReturningVoid.class,
                TestMoreThanOneEntity.class,
                TestMultipleHttpMethodDesignatorsRM.class,
                TestMultipleHttpMethodDesignatorsSRM.class,
                TestNonConflictingHttpMethodDelete.class,
                TestNonPublicRM.class,
                TestRelaxedProducesConsumesParserRules.class,
                TestRootResourceNonAmbigCtors.class,
                TestSRLReturningVoid.class,
                TwoLocatorsResource.class,
                TypeVariableResource.class,
                UniqueResource.class,

                TestDisableValidationFailOnErrorResource.class // we should still be able to invoke a GET on this one.
        );
        rc.property(ServerProperties.RESOURCE_VALIDATION_IGNORE_ERRORS, true);
        ApplicationHandler ah = new ApplicationHandler(rc);

        final ContainerRequest request = RequestContextBuilder.from("/test-disable-validation-fail-on-error", "GET").build();
        ContainerResponse response = ah.apply(request).get();

        assertEquals(200, response.getStatus());
        assertEquals("PASSED", response.getEntity());
    }

    private ServerBootstrapBag initializeApplication() {
        List<BootstrapConfigurator> bootstrapConfigurators = Collections.unmodifiableList(Arrays.asList(
                new TestConfigConfigurator(),
                new ParamExtractorConfigurator(),
                new ValueParamProviderConfigurator(),
                new MessageBodyFactory.MessageBodyWorkersConfigurator()));

        InjectionManager injectionManager = Injections.createInjectionManager();
        ServerBootstrapBag bootstrapBag = new ServerBootstrapBag();
        bootstrapConfigurators.forEach(configurer -> configurer.init(injectionManager, bootstrapBag));
        injectionManager.completeRegistration();
        bootstrapConfigurators.forEach(configurer -> configurer.postInit(injectionManager, bootstrapBag));
        return bootstrapBag;
    }
}