blob: 1938ed3e8d48e907aa311cfa20ecbd2d81e64e43 [file] [log] [blame]
/*
* Copyright (c) 2012, 2019 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.servlet;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.ws.rs.RuntimeType;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.servlet.FilterConfig;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.glassfish.jersey.internal.ServiceFinderBinder;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.glassfish.jersey.internal.inject.InjectionManager;
import org.glassfish.jersey.internal.inject.Providers;
import org.glassfish.jersey.internal.inject.ReferencingFactory;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.glassfish.jersey.internal.util.collection.Ref;
import org.glassfish.jersey.internal.util.collection.Value;
import org.glassfish.jersey.internal.util.collection.Values;
import org.glassfish.jersey.message.internal.HeaderValueException;
import org.glassfish.jersey.message.internal.MediaTypes;
import org.glassfish.jersey.process.internal.RequestScoped;
import org.glassfish.jersey.server.ApplicationHandler;
import org.glassfish.jersey.server.BackgroundSchedulerLiteral;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.internal.InternalServerProperties;
import org.glassfish.jersey.server.spi.RequestScopedInitializer;
import org.glassfish.jersey.servlet.internal.LocalizationMessages;
import org.glassfish.jersey.servlet.internal.PersistenceUnitBinder;
import org.glassfish.jersey.servlet.internal.ResponseWriter;
import org.glassfish.jersey.servlet.internal.ServletContainerProviderFactory;
import org.glassfish.jersey.servlet.internal.Utils;
import org.glassfish.jersey.servlet.internal.spi.ExtendedServletContainerProvider;
import org.glassfish.jersey.servlet.internal.spi.RequestContextProvider;
import org.glassfish.jersey.servlet.internal.spi.RequestScopedInitializerProvider;
import org.glassfish.jersey.servlet.internal.spi.ServletContainerProvider;
import org.glassfish.jersey.servlet.spi.AsyncContextDelegate;
import org.glassfish.jersey.servlet.spi.AsyncContextDelegateProvider;
import org.glassfish.jersey.servlet.spi.FilterUrlMappingsProvider;
import org.glassfish.jersey.uri.UriComponent;
/**
* An common Jersey web component that may be extended by a Servlet and/or
* Filter implementation, or encapsulated by a Servlet or Filter implementation.
*
* @author Paul Sandoz
* @author Jakub Podlesak
* @author Marek Potociar
* @author Martin Matula
* @author Libor Kramolis
*/
public class WebComponent {
private static final Logger LOGGER = Logger.getLogger(WebComponent.class.getName());
private static final Type REQUEST_TYPE = (new GenericType<Ref<HttpServletRequest>>() {}).getType();
private static final Type RESPONSE_TYPE = (new GenericType<Ref<HttpServletResponse>>() {}).getType();
private static final AsyncContextDelegate DEFAULT_ASYNC_DELEGATE = new AsyncContextDelegate() {
@Override
public void suspend() throws IllegalStateException {
throw new UnsupportedOperationException(LocalizationMessages.ASYNC_PROCESSING_NOT_SUPPORTED());
}
@Override
public void complete() {
}
};
private final RequestScopedInitializerProvider requestScopedInitializer;
private final boolean requestResponseBindingExternalized;
private static final RequestScopedInitializerProvider DEFAULT_REQUEST_SCOPE_INITIALIZER_PROVIDER =
context -> (RequestScopedInitializer) injectionManager -> {
injectionManager.<Ref<HttpServletRequest>>getInstance(REQUEST_TYPE).set(context.getHttpServletRequest());
injectionManager.<Ref<HttpServletResponse>>getInstance(RESPONSE_TYPE).set(context.getHttpServletResponse());
};
/**
* Return the first found {@link AsyncContextDelegateProvider}
* (via {@link Providers#getAllProviders(InjectionManager, Class)}) or {@code #DEFAULT_ASYNC_DELEGATE} if
* other delegate cannot be found.
*
* @return a non-null AsyncContextDelegateProvider.
*/
private AsyncContextDelegateProvider getAsyncExtensionDelegate() {
final Iterator<AsyncContextDelegateProvider> providers = Providers.getAllProviders(appHandler.getInjectionManager(),
AsyncContextDelegateProvider.class).iterator();
if (providers.hasNext()) {
return providers.next();
}
return (request, response) -> DEFAULT_ASYNC_DELEGATE;
}
@SuppressWarnings("JavaDoc")
private static class HttpServletRequestReferencingFactory extends ReferencingFactory<HttpServletRequest> {
@Inject
public HttpServletRequestReferencingFactory(final Provider<Ref<HttpServletRequest>> referenceFactory) {
super(referenceFactory);
}
}
@SuppressWarnings("JavaDoc")
private static class HttpServletResponseReferencingFactory extends ReferencingFactory<HttpServletResponse> {
@Inject
public HttpServletResponseReferencingFactory(final Provider<Ref<HttpServletResponse>> referenceFactory) {
super(referenceFactory);
}
}
private final class WebComponentBinder extends AbstractBinder {
private final Map<String, Object> applicationProperties;
/**
* Create binder for {@link WebComponent} passing a map of properties to determine whether certain features are allowed
* or
* not.
*
* @param applicationProperties map of properties to determine whether certain features are allowed or not.
*/
private WebComponentBinder(final Map<String, Object> applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Override
protected void configure() {
if (!requestResponseBindingExternalized) {
// request
bindFactory(HttpServletRequestReferencingFactory.class).to(HttpServletRequest.class)
.proxy(true).proxyForSameScope(false).in(RequestScoped.class);
bindFactory(ReferencingFactory.referenceFactory())
.to(new GenericType<Ref<HttpServletRequest>>() {}).in(RequestScoped.class);
// response
bindFactory(HttpServletResponseReferencingFactory.class).to(HttpServletResponse.class)
.proxy(true).proxyForSameScope(false).in(RequestScoped.class);
bindFactory(ReferencingFactory.referenceFactory())
.to(new GenericType<Ref<HttpServletResponse>>() {}).in(RequestScoped.class);
}
bindFactory(webConfig::getServletContext).to(ServletContext.class).in(Singleton.class);
final ServletConfig servletConfig = webConfig.getServletConfig();
if (webConfig.getConfigType() == WebConfig.ConfigType.ServletConfig) {
bindFactory(() -> servletConfig).to(ServletConfig.class).in(Singleton.class);
// @PersistenceUnit
final Enumeration initParams = servletConfig.getInitParameterNames();
while (initParams.hasMoreElements()) {
final String initParamName = (String) initParams.nextElement();
if (initParamName.startsWith(PersistenceUnitBinder.PERSISTENCE_UNIT_PREFIX)) {
install(new PersistenceUnitBinder(servletConfig));
break;
}
}
} else {
bindFactory(webConfig::getFilterConfig).to(FilterConfig.class).in(Singleton.class);
}
bindFactory(() -> webConfig).to(WebConfig.class).in(Singleton.class);
install(new ServiceFinderBinder<>(AsyncContextDelegateProvider.class, applicationProperties, RuntimeType.SERVER));
install(new ServiceFinderBinder<>(FilterUrlMappingsProvider.class, applicationProperties, RuntimeType.SERVER));
}
}
/**
* Jersey application handler.
*/
final ApplicationHandler appHandler;
/**
* Jersey background task scheduler - used for scheduling request timeout event handling tasks.
*/
final ScheduledExecutorService backgroundTaskScheduler;
/**
* Web component configuration.
*/
final WebConfig webConfig;
/**
* If {@code true} and deployed as filter, the unmatched requests will be forwarded.
*/
final boolean forwardOn404;
/**
* Cached value of configuration property
* {@link org.glassfish.jersey.server.ServerProperties#RESPONSE_SET_STATUS_OVER_SEND_ERROR}.
* If {@code true} method {@link HttpServletResponse#setStatus} is used over {@link HttpServletResponse#sendError}.
*/
final boolean configSetStatusOverSendError;
/**
* Asynchronous context delegate provider.
*/
private final AsyncContextDelegateProvider asyncExtensionDelegate;
/**
* Flag whether query parameters should be kept as entity form params if a servlet filter consumes entity and
* Jersey has to retrieve form params from servlet request parameters.
*/
private final boolean queryParamsAsFormParams;
/**
* Create and initialize new web component instance.
*
* @param webConfig we component configuration.
* @param resourceConfig Jersey application configuration.
* @throws ServletException in case the Jersey application cannot be created from the supplied
* resource configuration.
*/
public WebComponent(final WebConfig webConfig, ResourceConfig resourceConfig) throws ServletException {
this.webConfig = webConfig;
if (resourceConfig == null) {
resourceConfig = createResourceConfig(webConfig);
}
final ServletContainerProvider[] allServletContainerProviders =
ServletContainerProviderFactory.getAllServletContainerProviders();
// SPI/extension hook to configure ResourceConfig
configure(resourceConfig, allServletContainerProviders);
boolean rrbExternalized = false;
RequestScopedInitializerProvider rsiProvider = null;
for (final ServletContainerProvider servletContainerProvider : allServletContainerProviders) {
if (servletContainerProvider instanceof ExtendedServletContainerProvider) {
final ExtendedServletContainerProvider extendedProvider =
(ExtendedServletContainerProvider) servletContainerProvider;
if (extendedProvider.bindsServletRequestResponse()) {
rrbExternalized = true;
}
if (rsiProvider == null) { // try to take the first non-null provider
rsiProvider = extendedProvider.getRequestScopedInitializerProvider();
}
}
}
requestScopedInitializer = rsiProvider != null ? rsiProvider : DEFAULT_REQUEST_SCOPE_INITIALIZER_PROVIDER;
requestResponseBindingExternalized = rrbExternalized;
final AbstractBinder webComponentBinder = new WebComponentBinder(resourceConfig.getProperties());
resourceConfig.register(webComponentBinder);
final Object locator = webConfig.getServletContext()
.getAttribute(ServletProperties.SERVICE_LOCATOR);
this.appHandler = new ApplicationHandler(resourceConfig, webComponentBinder, locator);
this.asyncExtensionDelegate = getAsyncExtensionDelegate();
this.forwardOn404 = webConfig.getConfigType() == WebConfig.ConfigType.FilterConfig
&& resourceConfig.isProperty(ServletProperties.FILTER_FORWARD_ON_404);
this.queryParamsAsFormParams = !resourceConfig.isProperty(ServletProperties.QUERY_PARAMS_AS_FORM_PARAMS_DISABLED);
this.configSetStatusOverSendError = ServerProperties.getValue(resourceConfig.getProperties(),
ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, false, Boolean.class);
this.backgroundTaskScheduler = appHandler.getInjectionManager()
.getInstance(ScheduledExecutorService.class, BackgroundSchedulerLiteral.INSTANCE);
}
/**
* Dispatch client requests to a resource class.
*
* @param baseUri the base URI of the request.
* @param requestUri the URI of the request.
* @param servletRequest the {@link javax.servlet.http.HttpServletRequest} object that
* contains the request the client made to
* the Web component.
* @param servletResponse the {@link javax.servlet.http.HttpServletResponse} object that
* contains the response the Web component returns
* to the client.
* @return lazily initialized response status code {@link Value value provider}. If not resolved in the moment of call to
* {@link Value#get()}, {@code -1} is returned.
* @throws java.io.IOException if an input or output error occurs
* while the Web component is handling the
* HTTP request.
* @throws javax.servlet.ServletException if the HTTP request cannot be handled.
*/
public Value<Integer> service(
final URI baseUri,
final URI requestUri,
final HttpServletRequest servletRequest,
final HttpServletResponse servletResponse) throws ServletException, IOException {
final ResponseWriter responseWriter = serviceImpl(baseUri, requestUri, servletRequest, servletResponse);
return Values.lazy(new Value<Integer>() {
@Override
public Integer get() {
return responseWriter.responseContextResolved() ? responseWriter.getResponseStatus() : -1;
}
});
}
/**
* Dispatch client requests to a resource class.
*
* @param baseUri the base URI of the request.
* @param requestUri the URI of the request.
* @param servletRequest the {@link javax.servlet.http.HttpServletRequest} object that
* contains the request the client made to
* the Web component.
* @param servletResponse the {@link javax.servlet.http.HttpServletResponse} object that
* contains the response the Web component returns
* to the client.
* @return returns {@link ResponseWriter}, Servlet's {@link org.glassfish.jersey.server.spi.ContainerResponseWriter}
* implementation, into which processed request response was written to.
* @throws java.io.IOException if an input or output error occurs
* while the Web component is handling the
* HTTP request.
* @throws javax.servlet.ServletException if the HTTP request cannot be handled.
*/
/* package */ ResponseWriter serviceImpl(
final URI baseUri,
final URI requestUri,
final HttpServletRequest servletRequest,
final HttpServletResponse servletResponse) throws ServletException, IOException {
final ResponseWriter responseWriter = new ResponseWriter(
forwardOn404,
configSetStatusOverSendError,
servletResponse,
asyncExtensionDelegate.createDelegate(servletRequest, servletResponse),
backgroundTaskScheduler);
try {
final ContainerRequest requestContext = new ContainerRequest(baseUri, requestUri, servletRequest.getMethod(),
getSecurityContext(servletRequest), new ServletPropertiesDelegate(servletRequest),
appHandler.getConfiguration()
);
initContainerRequest(requestContext, servletRequest, servletResponse, responseWriter);
appHandler.handle(requestContext);
} catch (final HeaderValueException hve) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, LocalizationMessages.HEADER_VALUE_READ_FAILED(), hve);
}
final Response.Status status = Response.Status.BAD_REQUEST;
if (configSetStatusOverSendError) {
servletResponse.reset();
//noinspection deprecation
servletResponse.setStatus(status.getStatusCode(), status.getReasonPhrase());
} else {
servletResponse.sendError(status.getStatusCode(), status.getReasonPhrase());
}
} catch (final Exception e) {
throw new ServletException(e);
}
return responseWriter;
}
/**
* Initialize {@code ContainerRequest} instance to used used to handle {@code servletRequest}.
*/
private void initContainerRequest(
final ContainerRequest requestContext,
final HttpServletRequest servletRequest,
final HttpServletResponse servletResponse,
final ResponseWriter responseWriter) throws IOException {
requestContext.setEntityStream(servletRequest.getInputStream());
requestContext.setRequestScopedInitializer(requestScopedInitializer.get(new RequestContextProvider() {
@Override
public HttpServletRequest getHttpServletRequest() {
return servletRequest;
}
@Override
public HttpServletResponse getHttpServletResponse() {
return servletResponse;
}
}));
requestContext.setWriter(responseWriter);
addRequestHeaders(servletRequest, requestContext);
// Check if any servlet filters have consumed a request entity
// of the media type application/x-www-form-urlencoded
// This can happen if a filter calls request.getParameter(...)
filterFormParameters(servletRequest, requestContext);
}
/**
* Get default {@link javax.ws.rs.core.SecurityContext} for given {@code request}.
*
* @param request http servlet request to create a security context for.
* @return a non-null security context instance.
*/
private static SecurityContext getSecurityContext(final HttpServletRequest request) {
return new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return request.getUserPrincipal();
}
@Override
public boolean isUserInRole(final String role) {
return request.isUserInRole(role);
}
@Override
public boolean isSecure() {
return request.isSecure();
}
@Override
public String getAuthenticationScheme() {
return request.getAuthType();
}
};
}
/**
* Create a {@link ResourceConfig} instance from given {@link WebConfig}.
*
* @param config web config to create resource config from.
* @return resource config instance.
* @throws ServletException if an error has occurred.
*/
private static ResourceConfig createResourceConfig(final WebConfig config) throws ServletException {
final ServletContext servletContext = config.getServletContext();
// check if ResourceConfig has already been created, if so use it
ResourceConfig resourceConfig = Utils.retrieve(config.getServletContext(), config.getName());
if (resourceConfig != null) {
return resourceConfig;
}
final Map<String, Object> initParams = getInitParams(config);
final Map<String, Object> contextParams = Utils.getContextParams(servletContext);
// check if the JAX-RS application config class property is present
final String jaxrsApplicationClassName = config.getInitParameter(ServletProperties.JAXRS_APPLICATION_CLASS);
if (jaxrsApplicationClassName == null) {
// If no resource config class property is present, create default config
resourceConfig = new ResourceConfig().addProperties(initParams).addProperties(contextParams);
final String webApp = config.getInitParameter(ServletProperties.PROVIDER_WEB_APP);
if (webApp != null && !"false".equals(webApp)) {
resourceConfig.registerFinder(new WebAppResourcesScanner(servletContext));
}
return resourceConfig;
}
try {
final Class<? extends javax.ws.rs.core.Application> jaxrsApplicationClass = AccessController.doPrivileged(
ReflectionHelper.<javax.ws.rs.core.Application>classForNameWithExceptionPEA(jaxrsApplicationClassName)
);
if (javax.ws.rs.core.Application.class.isAssignableFrom(jaxrsApplicationClass)) {
return ResourceConfig.forApplicationClass(jaxrsApplicationClass)
.addProperties(initParams).addProperties(contextParams);
} else {
throw new ServletException(LocalizationMessages.RESOURCE_CONFIG_PARENT_CLASS_INVALID(
jaxrsApplicationClassName, javax.ws.rs.core.Application.class));
}
} catch (final PrivilegedActionException e) {
throw new ServletException(
LocalizationMessages.RESOURCE_CONFIG_UNABLE_TO_LOAD(jaxrsApplicationClassName), e.getCause());
} catch (final ClassNotFoundException e) {
throw new ServletException(LocalizationMessages.RESOURCE_CONFIG_UNABLE_TO_LOAD(jaxrsApplicationClassName), e);
}
}
/**
* SPI/extension hook to configure ResourceConfig.
*
* @param resourceConfig Jersey application configuration.
* @throws ServletException if an error has occurred.
*/
private void configure(final ResourceConfig resourceConfig,
final ServletContainerProvider[] allServletContainerProviders) throws ServletException {
for (final ServletContainerProvider servletContainerProvider : allServletContainerProviders) {
servletContainerProvider.configure(resourceConfig);
}
}
/**
* Copy request headers present in {@code request} into {@code requestContext} ignoring {@code null} values.
*
* @param request http servlet request to copy headers from.
* @param requestContext container request to copy headers to.
*/
@SuppressWarnings("unchecked")
private void addRequestHeaders(final HttpServletRequest request, final ContainerRequest requestContext) {
final Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()) {
final String name = names.nextElement();
final Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
final String value = values.nextElement();
if (value != null) { // filter out null values
requestContext.header(name, value);
}
}
}
}
/**
* Extract init params from {@link WebConfig}.
*
* @param webConfig actual servlet context.
* @return map representing current init parameters.
*/
private static Map<String, Object> getInitParams(final WebConfig webConfig) {
final Map<String, Object> props = new HashMap<>();
final Enumeration names = webConfig.getInitParameterNames();
while (names.hasMoreElements()) {
final String name = (String) names.nextElement();
props.put(name, webConfig.getInitParameter(name));
}
return props;
}
/**
* Extract parameters contained in {@link HttpServletRequest servlet request} and put them into
* {@link ContainerRequest container request} under
* {@value org.glassfish.jersey.server.internal.InternalServerProperties#FORM_DECODED_PROPERTY} property (as {@link Form}
* instance).
*
* @param servletRequest http servlet request to extract params from.
* @param containerRequest container request to put {@link Form} property to.
*/
private void filterFormParameters(final HttpServletRequest servletRequest, final ContainerRequest containerRequest) {
if (MediaTypes.typeEqual(MediaType.APPLICATION_FORM_URLENCODED_TYPE, containerRequest.getMediaType())
&& !containerRequest.hasEntity()) {
final Form form = new Form();
final Enumeration parameterNames = servletRequest.getParameterNames();
final String queryString = servletRequest.getQueryString();
final List<String> queryParams = queryString != null ? getDecodedQueryParamList(queryString)
: Collections.<String>emptyList();
final boolean keepQueryParams = queryParamsAsFormParams || queryParams.isEmpty();
final MultivaluedMap<String, String> formMap = form.asMap();
while (parameterNames.hasMoreElements()) {
final String name = (String) parameterNames.nextElement();
final List<String> values = Arrays.asList(servletRequest.getParameterValues(name));
formMap.put(name, keepQueryParams ? values : filterQueryParams(name, values, queryParams));
}
if (!formMap.isEmpty()) {
containerRequest.setProperty(InternalServerProperties.FORM_DECODED_PROPERTY, form);
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, LocalizationMessages.FORM_PARAM_CONSUMED(containerRequest.getRequestUri()));
}
}
}
}
private List<String> getDecodedQueryParamList(final String queryString) {
final List<String> params = new ArrayList<>();
for (final String param : queryString.split("&")) {
params.add(UriComponent.decode(param, UriComponent.Type.QUERY_PARAM));
}
return params;
}
/**
* From given list of values remove values that represents values of query params of the same name as the processed form
* parameter.
*
* @param name name of form/query parameter.
* @param values values of form/query parameter.
* @param params collection of unprocessed query parameters.
* @return list of form param values for given name without values of query param of the same name.
*/
private List<String> filterQueryParams(final String name, final List<String> values, final Collection<String> params) {
return values.stream()
.filter(s -> !params.remove(name + "=" + s) && !params.remove(name + "[]=" + s))
.collect(Collectors.toList());
}
/**
* Get {@link ApplicationHandler} used by this web component.
*
* @return The application handler
*/
public ApplicationHandler getAppHandler() {
return appHandler;
}
}