org.glassfish.jersey.server.internal.routing.MethodSelectingRouter Maven / Gradle / Ivy
The newest version!
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2012-2013 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.server.internal.routing;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.inject.Inject;
import javax.inject.Provider;
import org.glassfish.jersey.message.MessageBodyWorkers;
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.RespondingContext;
import org.glassfish.jersey.server.model.Invocable;
import org.glassfish.jersey.server.model.Parameter;
import org.glassfish.jersey.server.model.ResourceMethod;
import com.google.common.base.Function;
import com.google.common.collect.Sets;
/**
* 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 (jakub.podlesak at oracle.com)
* @author Marek Potociar (marek.potociar at oracle.com)
*/
final class MethodSelectingRouter implements Router {
private static final Logger LOGGER = Logger.getLogger(MethodSelectingRouter.class.getName());
private final Provider respondingContextFactory;
private final MessageBodyWorkers workers;
private final Map> consumesProducesAcceptors;
private final Router router;
/**
* Injectable builder of a {@link MethodSelectingRouter} instance.
*/
static class Builder {
@Inject
private Provider respondingContextFactory;
/**
* Create a new {@link 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 methodAcceptorPairs [method model, method methodAcceptorPair] pairs.
* @return new {@link MethodSelectingRouter}
*/
public MethodSelectingRouter build(
final MessageBodyWorkers workers, final List methodAcceptorPairs) {
return new MethodSelectingRouter(respondingContextFactory,
workers,
methodAcceptorPairs);
}
}
private MethodSelectingRouter(
Provider respondingContextFactory,
MessageBodyWorkers msgWorkers,
List methodAcceptorPairs) {
this.respondingContextFactory = respondingContextFactory;
this.workers = msgWorkers;
this.consumesProducesAcceptors = new HashMap>();
for (final MethodAcceptorPair methodAcceptorPair : methodAcceptorPairs) {
String httpMethod = methodAcceptorPair.model.getHttpMethod();
List httpMethodBoundAcceptors = consumesProducesAcceptors.get(httpMethod);
if (httpMethodBoundAcceptors == null) {
httpMethodBoundAcceptors = new LinkedList();
consumesProducesAcceptors.put(httpMethod, httpMethodBoundAcceptors);
}
addAllConsumesProducesCombinations(httpMethodBoundAcceptors, methodAcceptorPair);
}
if (!consumesProducesAcceptors.containsKey(HttpMethod.HEAD)) {
this.router = createHeadEnrichedRouter();
} else {
this.router = createInternalRouter();
}
}
/**
* 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 {
private CombinedClientServerMediaType.EffectiveMediaType consumes;
private CombinedClientServerMediaType.EffectiveMediaType produces;
private MethodAcceptorPair methodAcceptorPair;
private ConsumesProducesAcceptor(
CombinedClientServerMediaType.EffectiveMediaType consumes,
CombinedClientServerMediaType.EffectiveMediaType produces,
MethodAcceptorPair methodAcceptorPair) {
this.methodAcceptorPair = methodAcceptorPair;
this.consumes = consumes;
this.produces = produces;
}
/**
* Returns the {@link CombinedClientServerMediaType.EffectiveMediaType extended media type} which can be
* consumed by {@link ResourceMethod resource method} of this {@link ConsumesProducesAcceptor router}.
*
* @return Consumed type.
*/
public CombinedClientServerMediaType.EffectiveMediaType getConsumes() {
return consumes;
}
/**
* Returns the {@link CombinedClientServerMediaType.EffectiveMediaType extended media type} which can be
* produced by {@link ResourceMethod resource method} of this {@link ConsumesProducesAcceptor router}.
*
* @return Produced type.
*/
public CombinedClientServerMediaType.EffectiveMediaType getProduces() {
return produces;
}
/**
* Determines whether this {@link 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 = requestContext.getMediaType();
return contentType == null || consumes.getMediaType().isCompatible(contentType);
}
@Override
public String toString() {
return String.format("%s->%s:%s", consumes.getMediaType(), produces.getMediaType(), methodAcceptorPair);
}
}
/**
* The same as above ConsumesProducesAcceptor,
* only concrete request content-type and accept header info is included in addition.
*
* @see org.glassfish.jersey.server.internal.routing.CombinedClientServerMediaType
*/
private static class RequestSpecificConsumesProducesAcceptor implements Comparable {
CombinedClientServerMediaType consumes;
CombinedClientServerMediaType produces;
MethodAcceptorPair methodAcceptorPair;
RequestSpecificConsumesProducesAcceptor(CombinedClientServerMediaType consumes, CombinedClientServerMediaType produces,
MethodAcceptorPair methodAcceptorPair) {
this.methodAcceptorPair = methodAcceptorPair;
this.consumes = consumes;
this.produces = produces;
}
@Override
public String toString() {
return String.format("%s->%s:%s", consumes, produces, methodAcceptorPair);
}
@Override
public int compareTo(Object o) {
if (o == null) {
return 1;
}
if (!(o instanceof RequestSpecificConsumesProducesAcceptor)) {
return 1;
}
RequestSpecificConsumesProducesAcceptor other = (RequestSpecificConsumesProducesAcceptor) o;
final int consumedComparison = CombinedClientServerMediaType.COMPARATOR.compare(consumes, other.consumes);
return (consumedComparison != 0) ? consumedComparison : CombinedClientServerMediaType.COMPARATOR.compare(produces,
other.produces);
}
}
/**
* 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 theGreaterTheBetter = i.compareTo(selected);
if (theGreaterTheBetter > 0) {
selected = i;
sameFitnessAcceptors = null;
} else {
if (theGreaterTheBetter == 0 && (selected.methodAcceptorPair != i.methodAcceptorPair)) {
getSameFitnessList().add(i);
}
}
}
List getSameFitnessList() {
if (sameFitnessAcceptors == null) {
sameFitnessAcceptors = new LinkedList();
}
return sameFitnessAcceptors;
}
}
private Router createInternalRouter() {
return new Router() {
@Override
public Continuation apply(ContainerRequest requestContext) {
return Continuation.of(requestContext, getMethodRouter(requestContext));
}
};
}
@Override
public Continuation apply(ContainerRequest requestContext) {
return router.apply(requestContext);
}
private void addAllConsumesProducesCombinations(List list,
MethodAcceptorPair methodAcceptorPair) {
final Set effectiveInputTypes = new LinkedHashSet();
ResourceMethod resourceMethod = methodAcceptorPair.model;
boolean consumesFromWorkers = fillMediaTypes(effectiveInputTypes, resourceMethod, resourceMethod.getConsumedTypes(),
true);
final Set effectiveOutputTypes = new LinkedHashSet();
boolean producesFromWorkers = fillMediaTypes(effectiveOutputTypes, resourceMethod, resourceMethod.getProducedTypes(),
false);
for (MediaType consumes : effectiveInputTypes) {
for (MediaType produces : effectiveOutputTypes) {
list.add(new ConsumesProducesAcceptor(new CombinedClientServerMediaType.EffectiveMediaType(consumes,
consumesFromWorkers),
new CombinedClientServerMediaType.EffectiveMediaType(produces, producesFromWorkers), methodAcceptorPair));
}
}
}
private boolean fillMediaTypes(Set effectiveTypes, ResourceMethod resourceMethod, List methodTypes,
boolean inputTypes) {
boolean consumesFromWorkers = false;
effectiveTypes.addAll(methodTypes);
if (effectiveTypes.isEmpty()) {
if (workers != null) {
final Invocable invocableMethod = resourceMethod.getInvocable();
if (inputTypes) {
fillInputTypesFromWorkers(effectiveTypes, invocableMethod);
} else {
fillOutputParameters(effectiveTypes, invocableMethod);
}
consumesFromWorkers = !effectiveTypes.isEmpty();
}
}
if (effectiveTypes.isEmpty()) {
effectiveTypes.add(MediaType.WILDCARD_TYPE);
}
return consumesFromWorkers;
}
private void fillOutputParameters(Set effectiveOutputTypes, Invocable invocableMethod) {
final List messageBodyWriterMediaTypes = workers.getMessageBodyWriterMediaTypes(
invocableMethod.getRawResponseType(),
invocableMethod.getResponseType(),
invocableMethod.getHandlingMethod().getDeclaredAnnotations());
effectiveOutputTypes.addAll(messageBodyWriterMediaTypes);
}
private void fillInputTypesFromWorkers(Set effectiveInputTypes, Invocable invocableMethod) {
for (Parameter p : invocableMethod.getParameters()) {
if (p.getSource() == Parameter.Source.ENTITY) {
final List messageBodyReaderMediaTypes = workers.getMessageBodyReaderMediaTypes(
p.getRawType(), p.getType(), p.getDeclaredAnnotations());
effectiveInputTypes.addAll(messageBodyReaderMediaTypes);
// there's at most one entity parameter
break;
}
}
}
private List getMethodRouter(final ContainerRequest requestContext) {
List acceptors = consumesProducesAcceptors.get(requestContext.getMethod());
if (acceptors == null) {
throw new NotAllowedException(
Response.status(Status.METHOD_NOT_ALLOWED).allow(consumesProducesAcceptors.keySet()).build());
}
List satisfyingAcceptors = new LinkedList();
for (ConsumesProducesAcceptor cpi : acceptors) {
if (cpi.isConsumable(requestContext)) {
satisfyingAcceptors.add(cpi);
}
}
if (satisfyingAcceptors.isEmpty()) {
throw new NotSupportedException();
}
final List acceptableMediaTypes = requestContext.getAcceptableMediaTypes();
final MethodSelector methodSelector = new MethodSelector(null);
for (MediaType acceptableMediaType : acceptableMediaTypes) {
for (final ConsumesProducesAcceptor satisfiable : satisfyingAcceptors) {
if (satisfiable.produces.getMediaType().isCompatible(acceptableMediaType)) {
final MediaType requestContentType = requestContext.getMediaType();
final MediaType effectiveContentType = requestContentType == null ? MediaType.WILDCARD_TYPE :
requestContentType;
final RequestSpecificConsumesProducesAcceptor candidate = new RequestSpecificConsumesProducesAcceptor(
CombinedClientServerMediaType.create(effectiveContentType, satisfiable.getConsumes()),
CombinedClientServerMediaType.create(acceptableMediaType, satisfiable.getProduces()),
satisfiable.methodAcceptorPair);
methodSelector.consider(candidate);
}
}
}
if (methodSelector.selected != null) {
final RequestSpecificConsumesProducesAcceptor selected = methodSelector.selected;
if (methodSelector.sameFitnessAcceptors != null) {
reportMethodSelectionAmbiguity(acceptableMediaTypes, selected, methodSelector.sameFitnessAcceptors);
}
respondingContextFactory.get().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(responseContext.getRequestContext().getMethod()))) {
MediaType effectiveResponseType = selected.produces.getCombinedMediaType();
if (isWildcard(effectiveResponseType)) {
if (effectiveResponseType.isWildcardType() || effectiveResponseType.getType()
.equalsIgnoreCase("application")) {
effectiveResponseType = MediaType.APPLICATION_OCTET_STREAM_TYPE;
} else {
throw new NotAcceptableException();
}
}
responseContext.setMediaType(effectiveResponseType);
}
return responseContext;
}
});
return selected.methodAcceptorPair.router;
}
throw new NotAcceptableException();
}
private boolean isWildcard(final MediaType effectiveResponseType) {
return effectiveResponseType.isWildcardType() || effectiveResponseType.isWildcardSubtype();
}
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.methodAcceptorPair.model).append('\n');
final Set reportedMethods = Sets.newHashSet();
reportedMethods.add(selected.methodAcceptorPair.model);
for (RequestSpecificConsumesProducesAcceptor i : sameFitnessAcceptors) {
if (!reportedMethods.contains(i.methodAcceptorPair.model)) {
msgBuilder.append('\t').append(i.methodAcceptorPair.model).append('\n');
}
reportedMethods.add(i.methodAcceptorPair.model);
}
LOGGER.log(Level.WARNING, msgBuilder.toString());
}
}
private Router createHeadEnrichedRouter() {
return new Router() {
@Override
public Continuation apply(final ContainerRequest requestContext) {
if (HttpMethod.HEAD.equals(requestContext.getMethod())) {
requestContext.setMethodWithoutException(HttpMethod.GET);
respondingContextFactory.get().push(
new Function() {
@Override
public ContainerResponse apply(ContainerResponse responseContext) {
responseContext.getRequestContext().setMethodWithoutException(HttpMethod.HEAD);
return responseContext;
}
}
);
}
return Continuation.of(requestContext, getMethodRouter(requestContext));
}
};
}
}