/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * http://glassfish.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package org.glassfish.jersey.tests.api; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.TestProperties; import org.junit.Test; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.WebApplicationException; import javax.ws.rs.container.AsyncResponse; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; import javax.ws.rs.container.DynamicFeature; import javax.ws.rs.container.PreMatching; import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.container.Suspended; import javax.ws.rs.core.Context; import javax.ws.rs.core.FeatureContext; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.WriterInterceptor; import javax.ws.rs.ext.WriterInterceptorContext; import java.io.IOException; import java.net.URI; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Logger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Test if the location response header relative URI is correctly resolved within complex cases with interceptors, filters, * exception mappers, etc. * * @author Adam Lindenthal (adam.lindenthal at oracle.com) */ public class LocationHeaderFiltersTest extends JerseyTest { private static final Logger LOGGER = Logger.getLogger(LocationHeaderBasicTest.class.getName()); static ExecutorService executor; @Override protected ResourceConfig configure() { enable(TestProperties.LOG_TRAFFIC); return new ResourceConfig( MyTest.class, LocationManipulationDynamicBinding.class, AbortingPreMatchingRequestFilter.class, BaseUriChangingPreMatchingFilter.class, TestExceptionMapper.class ); } /** * Prepare test infrastructure. * * In this case it prepares executor thread pool of size one and initializes the thread. * @throws Exception */ @Override public void setUp() throws Exception { super.setUp(); /* thread pool for custom executor async test */ LocationHeaderFiltersTest.executor = Executors.newFixedThreadPool(1); // Force the thread to be eagerly instantiated - this prevents the instantiation later and ensures, that the thread // will not be a child thread of the request handling thread, so the thread-local baseUri variable will not be inherited. LocationHeaderFiltersTest.executor.submit(new Runnable() { @Override public void run() { LOGGER.info("Thread pool initialized."); } }); } /** * Test JAX-RS resource */ @SuppressWarnings("VoidMethodAnnotatedWithGET") @Path(value = "/ResponseTest") public static class MyTest { /* injected request URI for assertions in the resource methods */ @Context private UriInfo uriInfo; /** * Resource method for the test with uri rewritten in the filter * @return test response with relative location uri */ @GET @Path("locationTestWithFilter") public Response locationTestWithFilter() { final URI uri = URI.create("location"); LOGGER.info("URI Created in the resource method > " + uri); return Response.created(uri).build(); } /** * Resource method for the test with uri rewritten in the interceptor * @return test response with relative location uri and with body * (write interceptors are not triggered for entity-less responses) */ @GET @Path("locationWithInterceptor") public Response locationTestWithInterceptor() { final URI uri = URI.create("foo"); return Response.created(uri).entity("Return from locationTestWithInterceptor").type("text/plain").build(); } /** * Resource method for testing URI absolutization after the abortion in the post-matching filter. * @return dummy response - this string should never be propagated to the client (the processing chain * will be aborted in the filter before this resource method is even called. * However, it is needed here, because the filter is bound to the resource method name. */ @GET @Path("locationAborted") public String locationTestAborted() { assertTrue("The resource method locationTestAborted() should not have been called. The post-matching filter was " + "not configured correctly. ", false); return "DUMMY_RESPONSE"; // this string should never reach the client (the resource method will not be called) } /** * Resource method for testing URI absolutization after the abortion in the pre-matching filter. * @return dummy response - this string should never be propagated to the client (the processing chain will be * executorComparisonFailed in the filter before this resource method is even called. * However, it is needed here, because the filter is bound to the resource method name. */ @GET @Path("locationAbortedPreMatching") public String locationTestPreMatchingAborted() { assertTrue("The resource method locationTestPreMatchingAborted() should not have been called. The pre-matching " + "filter was not configured correctly. ", false); return "DUMMY_RESPONSE"; // this string should never reach the client (the resource method will not be called) } /** * Resource method for the test of ResponseFilters in the sync case. * Returns response with a relative URI, which is than absolutized by Jersey. * Later the {@link UriCheckingResponseFilter} is triggered and checks the URI again - the check itself is done in the * filter. * Based on the result of the check, the filter returns the original status (201 - Created) or an * error status (500 - Internal Server Error) with an error message in the response body. */ @GET @Path("responseFilterSync") public Response responseFilterSync() { return Response.created(URI.create("responseFilterSync")).build(); } /** * Resource method for the test of ResponseFilters in the async case. It runs in the separate thread created on request. * * Returns response with a relative URI, which is than absolutized by Jersey. * Later the {@link UriCheckingResponseFilter} is triggered and checks the URI again - the check itself is done in the * filter. * Based on the result of the check, the filter returns the original status (201 - Created) or an * error status (500 - Internal Server Error) with an error message in the response body. */ @GET @Path("responseFilterAsync") public void responseFilterAsync(@Suspended final AsyncResponse asyncResponse) { new Thread(new Runnable() { @Override public void run() { final Response result = Response.created(URI.create("responseFilterAsync")).build(); asyncResponse.resume(result); } }).start(); } /** * Resource method for the test of ResponseFilters in the async/executor case. It runs in a thread created out of the * request scope. * * Returns response with a relative URI, which is than absolutized by Jersey. * Later the {@link UriCheckingResponseFilter} is triggered and checks the URI again - the check itself is done in the * filter. * Based on the result of the check, the filter returns the original status (201 - Created) or an * error status (500 - Internal Server Error) with an error message in the response body. */ @GET @Path("responseFilterAsyncExecutor") public void responseFilterAsyncExecutor(@Suspended final AsyncResponse asyncResponse) { executor.submit(new Runnable() { @Override public void run() { final Response result = Response.created(URI.create("responseFilterAsyncExecutor")).build(); asyncResponse.resume(result); } }); } /** * Resource method for testing for testing the URI absolutization in the exception mapper in the synchronous case. * * Method always throws {@link WebApplicationException}, which is defined to be handled by {@link TestExceptionMapper}. * The exception mapper then creates the response with relative URI and response is routed * into {@link UriCheckingResponseFilter}, which checks if th URI was correctly absolutized. * @return does not return any response */ @GET @Path("exceptionMapperSync") public Response exceptionMapperSync() { throw new WebApplicationException(); } /** * Resource method for testing for testing the URI absolutization in the exception mapper in the asynchronous case. * New thread is started for the resource method processing. * * Method always "throws" {@link WebApplicationException} (in case of async methods, * the exceptions are not thrown directly, but passed to {@link AsyncResponse#resume(Throwable)}), * which is defined to be handled by {@link TestExceptionMapper}. * * The exception mapper then creates the response with relative URI and response is routed * into {@link UriCheckingResponseFilter}, which checks if th URI was correctly absolutized. */ @GET @Path("exceptionMapperAsync") public void exceptionMapperAsync(@Suspended final AsyncResponse asyncResponse) { new Thread(new Runnable() { @Override public void run() { asyncResponse.resume(new WebApplicationException()); } }).start(); } /** * Resource method for testing for testing the URI absolutization in the exception mapper in the asynchronous case. * A thread from executor thread pool (created out of request scope) is used for processing the resource method. * * Method always "throws" {@link WebApplicationException} (in case of async methods, * the exceptions are not thrown directly, but passed to {@link AsyncResponse#resume(Throwable)}), * which is defined to be handled by {@link TestExceptionMapper}. * * The exception mapper then creates the response with relative URI and response is routed * into {@link UriCheckingResponseFilter}, which checks if th URI was correctly absolutized. */ @GET @Path("exceptionMapperExecutor") public void exceptionMapperExecutor(@Suspended final AsyncResponse asyncResponse) { executor.submit(new Runnable() { @Override public void run() { asyncResponse.resume(new WebApplicationException()); } }); } /** * Resource method for testing correct baseUri and request overwrite in the prematching filter. * Should never be called by the test, as {@link MyTest#redirectedUri()} should be called instead. */ @GET @Path("filterChangedBaseUri") public Response locationWithChangedBaseUri() { fail("Method should not expected to be called, as prematching filter should have changed the request uri."); return Response.created(URI.create("new")).build(); } /** * Not called by the test directly, but after prematching filter redirect from * {@link MyTest#locationWithChangedBaseUri()}. * * @return {@code 201 Created} response with location resolved against new baseUri. */ @GET @Path("newUri") public Response redirectedUri() { return Response.created(URI.create("newRedirected")).build(); } } /** * Test the URI created in the post-matching request filter. */ @Test public void testAbortFilter() { checkResponseFilter("ResponseTest/locationAborted", "uriAfterAbortion/SUCCESS"); } /** * Test the URI created in the pre-matching request filter. */ @Test public void testAbortPreMatchingFilter() { checkResource("ResponseTest/locationAbortedPreMatching", "uriAfterPreMatchingAbortion/SUCCESS"); } /** * Test with URI Rewritten in the container response filter; * Filters do have access to the response headers and can manipulate the location uri so that it contains a relative address. * This test incorporates a filter which replaces the uri with a relative one. However we expect to have absolute uri at * the end of the chain. */ @Test public void testAbsoluteUriWithFilter() { checkResource("ResponseTest/locationTestWithFilter", "ResponseTest/UriChangedByFilter"); } /** * Test with URI Rewritten in the writer interceptor; * Interceptors do have access to the response headers and can manipulate the location uri so that it contains a relative * address. * This test incorporates an interceptor which replaces the uri with a relative one. However we expect to have absolute uri * at the end of the chain. */ @Test public void testAbsoluteUriWithInterceptor() { checkResource("ResponseTest/locationWithInterceptor", "ResponseTest/UriChangedByInterceptor"); } /** * Test, that uri is correct in the response filter when created in the exception mapper (synchronous). */ @Test public void testExceptionMapperSync() { checkResponseFilter("ResponseTest/exceptionMapperSync", "EXCEPTION_MAPPER"); } /** * Test, that uri is correct in the response filter when created in the exception mapper (asynchronous). */ @Test public void testExceptionMapperAsync() { checkResponseFilter("ResponseTest/exceptionMapperAsync", "EXCEPTION_MAPPER"); } /** * Test, that uri is correct in the response filter when created in the exception mapper (asynchronous/executor). */ @Test public void testExceptionMapperExecutor() { checkResponseFilter("ResponseTest/exceptionMapperExecutor", "EXCEPTION_MAPPER"); } /** * Test the baseUri and requestUri change in the prematching filter. */ @Test public void testLocationBaseUriChangedByPrematchingFilter() { final Response response = target().path("ResponseTest/filterChangedBaseUri").request().get(); assertEquals("http://www.server.com/newRedirected", response.getHeaderString("Location")); } /** * Test, that uri is correct in the response filter (synchronous). */ @Test public void testResponseFilterSync() { checkResponseFilter("ResponseTest/responseFilterSync", "responseFilterSync"); } /** * Test, that uri is correct in the response filter (asynchronous). */ @Test public void testResponseFilterAsync() { checkResponseFilter("ResponseTest/responseFilterAsync", "responseFilterAsync"); } /** * Test, that uri is correct in the response filter (asynchronous/executor). */ @Test public void testResponseFilterAsyncExecutor() { checkResponseFilter("ResponseTest/responseFilterAsyncExecutor", "responseFilterAsyncExecutor"); } /** * Response filter - replaces the Location header with a relative uri. */ public static class LocationManipulationFilter implements ContainerResponseFilter { @Override public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext) throws IOException { final MultivaluedMap<String, ?> headers = responseContext.getHeaders(); final List<URI> locations = (List<URI>) headers.get(HttpHeaders.LOCATION); locations.set(0, URI.create("ResponseTest/UriChangedByFilter")); LOGGER.info("LocationManipulationFilter applied."); } } /** * Response filter - check if the URI is absolute. If it is not correctly absolutized, * it changes the response to 500 - Internal Server Error and sets the error message into the response body. */ public static class UriCheckingResponseFilter implements ContainerResponseFilter { @Override public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext) throws IOException { final URI location = responseContext.getLocation(); if (!location.isAbsolute()) { responseContext.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); responseContext.setEntity("Response location was not absolute in UriCheckingFilter. Location value: " + location); } } } /** * Request Filter which aborts the current request calling ContainerRequestContext.abortWith(). * * The returned response is passed to Response.created() and immediately tested for absolutization. * This is necessary, as the relative URI would be absolutized later anyway and would reach the calling test method as * an absolute URI and there would be no way to determine where the URI conversion was done. * * The result of the test is propagated back to the test method by separate URI values in case of a success or a failure. */ public static class AbortingRequestFilter implements ContainerRequestFilter { @Override public void filter(final ContainerRequestContext requestContext) throws IOException { LOGGER.info("Aborting request in the request filter. Returning status created."); final String successRelativeUri = "uriAfterAbortion/SUCCESS"; Response response = Response.created(URI.create(successRelativeUri)).build(); if (!response.getLocation().toString().equals(requestContext.getUriInfo().getBaseUri() + successRelativeUri)) { response = Response.created(URI.create("uriAfterAbortion/FAILURE")).build(); } requestContext.abortWith(response); } } /** * Request Filter which aborts the current request calling ContainerRequestContext.abortWith(). * * The returned response is passed to Response.created() and immediately tested for absolutization. * This is necessary, as the relative URI would be absolutized later anyway and would reach the calling test method as * an absolute URI and there would be no way to determine where the URI conversion was done. * * The result of the test is propagated back to the test method by separate URI values in case of a success or a failure. */ @PreMatching public static class AbortingPreMatchingRequestFilter implements ContainerRequestFilter { @Override public void filter(final ContainerRequestContext requestContext) throws IOException { if (requestContext.getUriInfo().getAbsolutePath().toString().endsWith("locationAbortedPreMatching")) { LOGGER.info("Aborting request in the request filter. Returning status created."); final String successRelativeUri = "uriAfterPreMatchingAbortion/SUCCESS"; Response response = Response.created(URI.create(successRelativeUri)).build(); if (!response.getLocation().toString().equals(requestContext.getUriInfo().getBaseUri() + successRelativeUri)) { response = Response.created(URI.create("uriAfterPreMatchingAbortion/FAILURE")).build(); } requestContext.abortWith(response); } } } /** * Request prematching filter which changes request URI and base URI. * * As a result, different resource mathod should be matched and invoked and return location resolved against the new * base URI. */ @PreMatching public static class BaseUriChangingPreMatchingFilter implements ContainerRequestFilter { @Override public void filter(final ContainerRequestContext requestContext) throws IOException { if (requestContext.getUriInfo().getAbsolutePath().toString().endsWith("filterChangedBaseUri")) { final URI requestUri = requestContext.getUriInfo().getRequestUri(); // NOTE, that the trailing slash matters, without it, the URI is nod valid and is not correctly resolved by the // URI.resolve() method. final URI baseUri = URI.create("http://www.server.com/"); requestContext.setRequestUri(baseUri, requestUri.resolve("newUri")); } } } /** * Writer interceptor - replaces the Location header with a relative uri. */ public static class LocationManipulationInterceptor implements WriterInterceptor { @Override public void aroundWriteTo(final WriterInterceptorContext context) throws IOException, WebApplicationException { final MultivaluedMap<String, ?> headers = context.getHeaders(); final List<URI> locations = (List<URI>) headers.get(HttpHeaders.LOCATION); locations.set(0, URI.create("ResponseTest/UriChangedByInterceptor")); LOGGER.info("LocationManipulationInterceptor applied."); context.proceed(); } } /** * Exception mapper which creates a test response with a relative URI. */ public static class TestExceptionMapper implements ExceptionMapper<WebApplicationException> { @Override public Response toResponse(final WebApplicationException exception) { exception.printStackTrace(); return Response.created(URI.create("EXCEPTION_MAPPER")).build(); } } /** * Registers the filter and interceptor and binds it to the resource methods of interest. */ public static class LocationManipulationDynamicBinding implements DynamicFeature { @Override public void configure(final ResourceInfo resourceInfo, final FeatureContext context) { if (MyTest.class.equals(resourceInfo.getResourceClass())) { final String methodName = resourceInfo.getResourceMethod().getName(); if (methodName.contains("locationTestWithFilter")) { context.register(LocationManipulationFilter.class); LOGGER.info("LocationManipulationFilter registered."); } if (methodName.contains("locationTestWithInterceptor")) { context.register(LocationManipulationInterceptor.class); LOGGER.info("LocationManipulationInterceptor registered."); } if (methodName.contains("locationTestAborted")) { context.register(AbortingRequestFilter.class); LOGGER.info("AbortingRequestFilter registered."); } if (methodName.contains("responseFilterSync") || methodName.contains("responseFilterAsync") || methodName.contains("locationTestAborted") || methodName.contains("exceptionMapperSync") || methodName.contains("exceptionMapperAsync") || methodName.contains("exceptionMapperExecutor")) { context.register(UriCheckingResponseFilter.class); LOGGER.info("UriCheckingResponseFilter registered."); } if (methodName.contains("locationWithChangedBaseUri")) { context.register(BaseUriChangingPreMatchingFilter.class); LOGGER.info("BaseUriChangingPreMatchingFilter registered."); } } } } private Response checkResource(final String resourcePath, final String expectedRelativeUri) { final Response response = target().path(resourcePath).request(MediaType.TEXT_PLAIN).get(Response.class); final String location = response.getHeaderString(HttpHeaders.LOCATION); LOGGER.info("Location resolved from response > " + location); assertEquals(getBaseUri() + expectedRelativeUri, location); return response; } private void checkResponseFilter(final String resourcePath, final String expectedRelativeUri) { final Response response = target().path(resourcePath).request().get(Response.class); assertNotEquals("Message from response filter: " + response.readEntity(String.class), response.getStatus(), Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); assertEquals(getBaseUri() + expectedRelativeUri, response.getLocation().toString()); } }