org.glassfish.jersey.server.internal.routing.MethodSelectingRouter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jaxrs-ri Show documentation
Show all versions of jaxrs-ri Show documentation
A bundle project producing JAX-RS RI bundles. The primary artifact is an "all-in-one" OSGi-fied JAX-RS RI bundle
(jaxrs-ri.jar).
Attached to that are two compressed JAX-RS RI archives. The first archive (jaxrs-ri.zip) consists of binary RI bits and
contains the API jar (under "api" directory), RI libraries (under "lib" directory) as well as all external
RI dependencies (under "ext" directory). The secondary archive (jaxrs-ri-src.zip) contains buildable JAX-RS RI source
bundle and contains the API jar (under "api" directory), RI sources (under "src" directory) as well as all external
RI dependencies (under "ext" directory). The second archive also contains "build.xml" ANT script that builds the RI
sources. To build the JAX-RS RI simply unzip the archive, cd to the created jaxrs-ri directory and invoke "ant" from
the command line.
/*
* Copyright (c) 2012, 2023 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.internal.routing;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.NotAllowedException;
import javax.ws.rs.NotSupportedException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.glassfish.jersey.internal.guava.Primitives;
import org.glassfish.jersey.internal.routing.ContentTypeDeterminer;
import org.glassfish.jersey.internal.routing.CombinedMediaType;
import org.glassfish.jersey.internal.routing.RequestSpecificConsumesProducesAcceptor;
import org.glassfish.jersey.message.MessageBodyWorkers;
import org.glassfish.jersey.message.ReaderModel;
import org.glassfish.jersey.message.WriterModel;
import org.glassfish.jersey.message.internal.AcceptableMediaType;
import org.glassfish.jersey.message.internal.MediaTypes;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ContainerResponse;
import org.glassfish.jersey.server.internal.LocalizationMessages;
import org.glassfish.jersey.server.internal.process.RequestProcessingContext;
import org.glassfish.jersey.server.model.Invocable;
import org.glassfish.jersey.server.model.Parameter;
import org.glassfish.jersey.server.model.ResourceMethod;
/**
* A single router responsible for selecting a single method from all the methods
* bound to the same routed request path.
*
* The method selection algorithm selects the handling method based on the HTTP request
* method name, requested media type as well as defined resource method media type
* capabilities.
*
* @author Jakub Podlesak
* @author Marek Potociar
*/
final class MethodSelectingRouter extends ContentTypeDeterminer implements Router {
private static final Logger LOGGER = Logger.getLogger(MethodSelectingRouter.class.getName());
private static final Comparator CONSUMES_PRODUCES_ACCEPTOR_COMPARATOR =
new Comparator() {
@Override
public int compare(final ConsumesProducesAcceptor o1, final ConsumesProducesAcceptor o2) {
// Make sure that annotated (@Consumes, @Produces) goes first.
final ResourceMethod model1 = o1.methodRouting.method;
final ResourceMethod model2 = o2.methodRouting.method;
// @Consumes on method.
int compared = compare(model1.getConsumedTypes(), model2.getConsumedTypes());
if (compared != 0) {
return compared;
}
compared = compare(model1.getProducedTypes(), model2.getProducedTypes());
if (compared != 0) {
return compared;
}
compared = MediaTypes.PARTIAL_ORDER_COMPARATOR.compare(o1.consumes.getMediaType(),
o2.consumes.getMediaType());
if (compared != 0) {
return compared;
}
return MediaTypes.PARTIAL_ORDER_COMPARATOR.compare(o1.produces.getMediaType(),
o2.produces.getMediaType());
}
private int compare(List mediaTypeList1, List mediaTypeList2) {
mediaTypeList1 = mediaTypeList1.isEmpty() ? MediaTypes.WILDCARD_TYPE_SINGLETON_LIST : mediaTypeList1;
mediaTypeList2 = mediaTypeList2.isEmpty() ? MediaTypes.WILDCARD_TYPE_SINGLETON_LIST : mediaTypeList2;
return MediaTypes.MEDIA_TYPE_LIST_COMPARATOR.compare(mediaTypeList1, mediaTypeList2);
}
};
private final Map> consumesProducesAcceptors;
private final Router router;
/**
* Create a new {@code MethodSelectingRouter} for all the methods on the same path.
*
* The router selects the method that best matches the request based on
* produce/consume information from the resource method models.
*
* @param workers message body workers.
* @param methodRoutings [method model, method methodAcceptorPair] pairs.
*/
MethodSelectingRouter(MessageBodyWorkers workers, List methodRoutings) {
super(workers);
this.consumesProducesAcceptors = new HashMap<>();
final Set httpMethods = new HashSet<>();
for (final MethodRouting methodRouting : methodRoutings) {
final String httpMethod = methodRouting.method.getHttpMethod();
httpMethods.add(httpMethod);
List httpMethodBoundAcceptors = consumesProducesAcceptors.get(httpMethod);
if (httpMethodBoundAcceptors == null) {
httpMethodBoundAcceptors = new LinkedList<>();
consumesProducesAcceptors.put(httpMethod, httpMethodBoundAcceptors);
}
addAllConsumesProducesCombinations(httpMethodBoundAcceptors, methodRouting);
}
// Sort acceptors for added HTTP methods - primary based on @Consumes, @Produces present on method, secondary on consumes,
// produces values of the acceptor.
for (final String httpMethod : httpMethods) {
Collections.sort(consumesProducesAcceptors.get(httpMethod), CONSUMES_PRODUCES_ACCEPTOR_COMPARATOR);
}
if (!consumesProducesAcceptors.containsKey(HttpMethod.HEAD)) {
this.router = createHeadEnrichedRouter();
} else {
this.router = createInternalRouter();
}
}
Set getHttpMethods() {
return consumesProducesAcceptors.keySet();
}
/**
* Represents a 1-1-1 relation between input and output media type and an methodAcceptorPair.
* E.g. for a single resource method
*
* @Consumes("*/*")
* @Produces("text/plain","text/html")
* @GET
* public String myGetMethod() {
* return "S";
* }
*
* the following two relations would be generated:
*
*
* consumes
* produces
* method
*
*
* */*
* text/plain
* myGetMethod
*
*
*
* */*
* text/html
* myGetMethod
*
*
*
*/
private static class ConsumesProducesAcceptor {
final CombinedMediaType.EffectiveMediaType consumes;
final CombinedMediaType.EffectiveMediaType produces;
final MethodRouting methodRouting;
private ConsumesProducesAcceptor(
CombinedMediaType.EffectiveMediaType consumes,
CombinedMediaType.EffectiveMediaType produces,
MethodRouting methodRouting) {
this.methodRouting = methodRouting;
this.consumes = consumes;
this.produces = produces;
}
/**
* Determines whether this {@code ConsumesProducesAcceptor} router can process the {@code request}.
*
* @param requestContext The request to be tested.
* @return True if the {@code request} can be processed by this router, false otherwise.
*/
boolean isConsumable(ContainerRequest requestContext, MediaType contentType) {
return contentType == null || consumes.getMediaType().isCompatible(contentType);
}
@Override
public String toString() {
return String.format("%s->%s:%s", consumes.getMediaType(), produces.getMediaType(), methodRouting);
}
@Override
@SuppressWarnings("RedundantIfStatement")
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ConsumesProducesAcceptor)) {
return false;
}
final ConsumesProducesAcceptor that = (ConsumesProducesAcceptor) o;
if (consumes != null ? !consumes.equals(that.consumes) : that.consumes != null) {
return false;
}
if (methodRouting != null ? !methodRouting.equals(that.methodRouting) : that.methodRouting != null) {
return false;
}
if (produces != null ? !produces.equals(that.produces) : that.produces != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = consumes != null ? consumes.hashCode() : 0;
result = 31 * result + (produces != null ? produces.hashCode() : 0);
result = 31 * result + (methodRouting != null ? methodRouting.hashCode() : 0);
return result;
}
}
/**
* Helper class to select matching resource method to be invoked.
*/
private static class MethodSelector {
RequestSpecificConsumesProducesAcceptor selected;
List sameFitnessAcceptors;
MethodSelector(RequestSpecificConsumesProducesAcceptor i) {
selected = i;
sameFitnessAcceptors = null;
}
void consider(RequestSpecificConsumesProducesAcceptor i) {
final int theLessTheBetter = i.compareTo(selected);
if (theLessTheBetter < 0) {
selected = i;
sameFitnessAcceptors = null;
} else {
if (theLessTheBetter == 0 && (selected.getMethodRouting() != i.getMethodRouting())) {
getSameFitnessList().add(i);
}
}
}
List getSameFitnessList() {
if (sameFitnessAcceptors == null) {
sameFitnessAcceptors = new LinkedList<>();
}
return sameFitnessAcceptors;
}
}
private Router createInternalRouter() {
return new Router() {
@Override
public Continuation apply(RequestProcessingContext requestContext) {
return Continuation.of(requestContext, getMethodRouter(requestContext));
}
};
}
@Override
public Continuation apply(RequestProcessingContext requestContext) {
return router.apply(requestContext);
}
private void addAllConsumesProducesCombinations(final List acceptors,
final MethodRouting methodRouting) {
final ResourceMethod resourceMethod = methodRouting.method;
final Set effectiveInputTypes = new LinkedHashSet<>();
boolean consumesFromWorkers = fillMediaTypes(effectiveInputTypes, resourceMethod,
resourceMethod.getConsumedTypes(), true);
final Set effectiveOutputTypes = new LinkedHashSet<>();
boolean producesFromWorkers = fillMediaTypes(effectiveOutputTypes, resourceMethod,
resourceMethod.getProducedTypes(), false);
final Set acceptorSet = new HashSet<>();
for (MediaType consumes : effectiveInputTypes) {
for (MediaType produces : effectiveOutputTypes) {
acceptorSet.add(new ConsumesProducesAcceptor(
new CombinedMediaType.EffectiveMediaType(consumes, consumesFromWorkers),
new CombinedMediaType.EffectiveMediaType(produces, producesFromWorkers),
methodRouting));
}
}
acceptors.addAll(acceptorSet);
}
private boolean fillMediaTypes(final Set effectiveTypes,
final ResourceMethod resourceMethod,
final List methodTypes,
final boolean inputTypes) {
// Add method types to the resulting list iff there is more than just */*
if (methodTypes.size() > 1 || !methodTypes.contains(MediaType.WILDCARD_TYPE)) {
effectiveTypes.addAll(methodTypes);
}
boolean mediaTypesFromWorkers = effectiveTypes.isEmpty();
if (mediaTypesFromWorkers) {
final Invocable invocableMethod = resourceMethod.getInvocable();
// If not predefined from method - get it from workers.
if (inputTypes) {
fillInputTypesFromWorkers(effectiveTypes, invocableMethod);
} else {
fillOutputTypesFromWorkers(effectiveTypes, invocableMethod.getRawResponseType());
}
mediaTypesFromWorkers = !effectiveTypes.isEmpty();
// If still empty - get all available.
if (!mediaTypesFromWorkers) {
if (inputTypes) {
effectiveTypes.addAll(workers.getMessageBodyReaderMediaTypesByType(Object.class));
} else {
effectiveTypes.addAll(workers.getMessageBodyWriterMediaTypesByType(Object.class));
}
mediaTypesFromWorkers = true;
}
// As a last resort add */* when no message providers are in effect
// i.e. if no entity arg in resource method for a reader not to throw 415
// and if void return type for a writer not to throw 406.
final boolean noEntityArgInResourceMethod = inputTypes && getEntityParam(invocableMethod) == null;
final boolean voidReturnType = !inputTypes && invocableMethod.getRawResponseType() == void.class;
if (noEntityArgInResourceMethod || voidReturnType) {
effectiveTypes.add(MediaType.WILDCARD_TYPE);
}
}
return mediaTypesFromWorkers;
}
private void fillOutputTypesFromWorkers(final Set effectiveOutputTypes, final Class> returnEntityType) {
effectiveOutputTypes.addAll(workers.getMessageBodyWriterMediaTypesByType(returnEntityType));
}
private void fillInputTypesFromWorkers(final Set effectiveInputTypes, final Invocable invocableMethod) {
for (Parameter p : invocableMethod.getParameters()) {
if (p.getSource() == Parameter.Source.ENTITY) {
effectiveInputTypes.addAll(workers.getMessageBodyReaderMediaTypesByType(p.getRawType()));
// there's at most one entity parameter
break;
}
}
}
private Parameter getEntityParam(final Invocable invocable) {
for (final Parameter parameter : invocable.getParameters()) {
if (parameter.getSource() == Parameter.Source.ENTITY
&& !ContainerRequestContext.class.isAssignableFrom(parameter.getRawType())) {
// there's at most one entity parameter
return parameter;
}
}
return null;
}
private List getMethodRouter(final RequestProcessingContext context) {
final ContainerRequest request = context.request();
final List acceptors = consumesProducesAcceptors.get(request.getMethod());
if (acceptors == null) {
throw new NotAllowedException(
Response.status(Status.METHOD_NOT_ALLOWED).allow(consumesProducesAcceptors.keySet()).build());
}
final List satisfyingAcceptors = new LinkedList<>();
final Set differentInvokableMethods = Collections.newSetFromMap(new IdentityHashMap<>());
final MediaType requestContentType = request.getMediaType();
for (ConsumesProducesAcceptor cpi : acceptors) {
if (cpi.isConsumable(request, requestContentType)) {
satisfyingAcceptors.add(cpi);
differentInvokableMethods.add(cpi.methodRouting.method);
}
}
if (satisfyingAcceptors.isEmpty()) {
throw new NotSupportedException();
}
final List acceptableMediaTypes = request.getQualifiedAcceptableMediaTypes();
final MediaType effectiveContentType = requestContentType == null ? MediaType.WILDCARD_TYPE : requestContentType;
final MethodSelector methodSelector = selectMethod(acceptableMediaTypes, satisfyingAcceptors, effectiveContentType,
differentInvokableMethods.size() == 1);
if (methodSelector.selected != null) {
final RequestSpecificConsumesProducesAcceptor selected = methodSelector.selected;
if (methodSelector.sameFitnessAcceptors != null) {
reportMethodSelectionAmbiguity(acceptableMediaTypes, methodSelector.selected,
methodSelector.sameFitnessAcceptors);
}
context.push(new Function() {
@Override
public ContainerResponse apply(final ContainerResponse responseContext) {
// we only need to compute and set the effective media type if:
// - it hasn't been set already, and
// - either there is an entity, or we are responding to a HEAD request
if (responseContext.getMediaType() == null
&& ((responseContext.hasEntity() || HttpMethod.HEAD.equals(request.getMethod())))) {
MediaType effectiveResponseType = determineResponseMediaType(
responseContext.getEntityClass(),
responseContext.getEntityType(),
methodSelector.selected,
acceptableMediaTypes);
if (MediaTypes.isWildcard(effectiveResponseType)) {
if (effectiveResponseType.isWildcardType()
|| "application".equalsIgnoreCase(effectiveResponseType.getType())) {
effectiveResponseType = MediaType.APPLICATION_OCTET_STREAM_TYPE;
} else {
throw new NotAcceptableException();
}
}
responseContext.setMediaType(effectiveResponseType);
}
return responseContext;
}
});
return selected.getMethodRouting().routers;
}
throw new NotAcceptableException();
}
/**
* Determine the {@link MediaType} of the {@link Response} based on writers suitable for the given entity class,
* pre-selected method and acceptable media types.
*
* @param entityClass entity class to determine the media type for.
* @param entityType entity type for writers.
* @param selectedMethod pre-selected (invoked) method.
* @param acceptableMediaTypes acceptable media types from request.
* @return media type of the response.
*/
private MediaType determineResponseMediaType(
final Class> entityClass,
final Type entityType,
final RequestSpecificConsumesProducesAcceptor selectedMethod,
final List acceptableMediaTypes) {
// Return pre-selected MediaType.
if (usePreSelectedMediaType(selectedMethod, acceptableMediaTypes)) {
return selectedMethod.getProduces().getCombinedType();
}
final ResourceMethod resourceMethod = selectedMethod.getMethodRouting().method;
final Invocable invocable = resourceMethod.getInvocable();
// Entity class can be null when considering HEAD method || empty entity.
final Class> responseEntityClass = entityClass == null ? invocable.getRawRoutingResponseType() : entityClass;
final Method handlingMethod = invocable.getHandlingMethod();
// Media types producible by method.
final List methodProducesTypes = !resourceMethod.getProducedTypes().isEmpty()
? resourceMethod.getProducedTypes() : Collections.singletonList(MediaType.WILDCARD_TYPE);
return super.determineResponseMediaType(responseEntityClass, entityType, selectedMethod, acceptableMediaTypes,
methodProducesTypes, handlingMethod.getDeclaredAnnotations());
}
private static boolean usePreSelectedMediaType(final RequestSpecificConsumesProducesAcceptor selectedMethod,
final List acceptableMediaTypes) {
// Resource method is annotated with @Produces and this annotation contains only one MediaType.
if (!selectedMethod.producesFromProviders()
&& selectedMethod.getMethodRouting().method.getProducedTypes().size() == 1) {
return true;
}
// There is only one (non-wildcard) acceptable media type - at this point the pre-selected method has to be chosen so
// there are compatible writers (not necessarily writeable ones).
return acceptableMediaTypes.size() == 1 && !MediaTypes.isWildcard(acceptableMediaTypes.get(0));
}
private boolean isWriteable(final RequestSpecificConsumesProducesAcceptor candidate) {
final Invocable invocable = candidate.getMethodRouting().method.getInvocable();
final Class> responseType = Primitives.wrap(invocable.getRawRoutingResponseType());
if (Response.class.isAssignableFrom(responseType)
|| Void.class.isAssignableFrom(responseType)) {
return true;
}
final Type genericType = invocable.getRoutingResponseType();
final Type genericReturnType = genericType instanceof GenericType
? ((GenericType) genericType).getType() : genericType;
for (final WriterModel model : workers.getWritersModelsForType(responseType)) {
if (model.isWriteable(
responseType,
genericReturnType,
invocable.getHandlingMethod().getDeclaredAnnotations(),
candidate.getProduces().getCombinedType())) {
return true;
}
}
return false;
}
private boolean isReadable(final RequestSpecificConsumesProducesAcceptor candidate) {
final Invocable invocable = candidate.getMethodRouting().method.getInvocable();
final Method handlingMethod = invocable.getHandlingMethod();
final Parameter entityParam = getEntityParam(invocable);
if (entityParam == null) {
return true;
} else {
final Class> entityType = entityParam.getRawType();
for (final ReaderModel model : workers.getReaderModelsForType(entityType)) {
if (model.isReadable(
entityType,
entityParam.getType(),
handlingMethod.getDeclaredAnnotations(),
candidate.getConsumes().getCombinedType())) {
return true;
}
}
}
return false;
}
/**
* Select method to be invoked. Method is chosen among the given set of acceptors (if they are compatible with acceptable
* media types).
*
* @param acceptableMediaTypes media types acceptable by the client.
* @param satisfyingAcceptors pre-computed acceptors.
* @param effectiveContentType media type of incoming entity.
* @param singleInvokableMethod flag determining whether only one method to be invoked has been found among satisfying
* acceptors.
* @return method to be invoked.
*/
private MethodSelector selectMethod(final List acceptableMediaTypes,
final List satisfyingAcceptors,
final MediaType effectiveContentType,
final boolean singleInvokableMethod) {
// Selected method we have a reader and writer for.
final MethodSelector method = new MethodSelector(null);
// If we cannot find a writer at this point use the best alternative.
final MethodSelector alternative = new MethodSelector(null);
for (final MediaType acceptableMediaType : acceptableMediaTypes) {
for (final ConsumesProducesAcceptor satisfiable : satisfyingAcceptors) {
final CombinedMediaType produces =
CombinedMediaType.create(acceptableMediaType, satisfiable.produces);
if (produces != CombinedMediaType.NO_MATCH) {
final CombinedMediaType consumes =
CombinedMediaType.create(effectiveContentType, satisfiable.consumes);
final RequestSpecificConsumesProducesAcceptor candidate =
new RequestSpecificConsumesProducesAcceptor<>(
consumes,
produces,
satisfiable.produces.isDerived(),
satisfiable.methodRouting);
if (singleInvokableMethod) {
// Only one possible method and it's compatible.
return new MethodSelector(candidate);
} else if (candidate.compareTo(method.selected) < 0) {
// Candidate is better than the previous one.
if (method.selected == null
|| candidate.getMethodRouting().method != method.selected.getMethodRouting().method) {
// No candidate so far or better candidate.
if (isReadable(candidate) && isWriteable(candidate)) {
method.consider(candidate);
} else {
alternative.consider(candidate);
}
} else {
// Same resource method - better candidate, no need to compare anything else.
method.consider(candidate);
}
}
}
}
}
return method.selected != null ? method : alternative;
}
private void reportMethodSelectionAmbiguity(List acceptableTypes,
RequestSpecificConsumesProducesAcceptor selected,
List sameFitnessAcceptors) {
if (LOGGER.isLoggable(Level.WARNING)) {
StringBuilder msgBuilder =
new StringBuilder(LocalizationMessages.AMBIGUOUS_RESOURCE_METHOD(acceptableTypes)).append('\n');
msgBuilder.append('\t').append(selected.getMethodRouting().method).append('\n');
final Set reportedMethods = new HashSet<>();
reportedMethods.add(selected.getMethodRouting().method);
for (RequestSpecificConsumesProducesAcceptor i : sameFitnessAcceptors) {
if (!reportedMethods.contains(i.getMethodRouting().method)) {
msgBuilder.append('\t').append(i.getMethodRouting().method).append('\n');
}
reportedMethods.add(i.getMethodRouting().method);
}
LOGGER.log(Level.WARNING, msgBuilder.toString());
}
}
private Router createHeadEnrichedRouter() {
return new Router() {
@Override
public Continuation apply(final RequestProcessingContext context) {
final ContainerRequest request = context.request();
if (HttpMethod.HEAD.equals(request.getMethod())) {
request.setMethodWithoutException(HttpMethod.GET);
context.push(
new Function() {
@Override
public ContainerResponse apply(final ContainerResponse responseContext) {
responseContext.getRequestContext().setMethodWithoutException(HttpMethod.HEAD);
return responseContext;
}
}
);
}
return Continuation.of(context, getMethodRouter(context));
}
};
}
}