/******************************************************************************* * Copyright (c) 2012-2016 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.everrest.core.impl; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.Iterables; import org.everrest.core.ApplicationContext; import org.everrest.core.GenericContainerRequest; import org.everrest.core.GenericContainerResponse; import org.everrest.core.ObjectFactory; import org.everrest.core.ResourceBinder; import org.everrest.core.impl.async.AsynchronousJob; import org.everrest.core.impl.header.AcceptMediaType; import org.everrest.core.impl.header.MediaTypeHelper; import org.everrest.core.impl.resource.AbstractResourceDescriptor; import org.everrest.core.method.MethodInvoker; import org.everrest.core.resource.ResourceDescriptor; import org.everrest.core.resource.ResourceMethodDescriptor; import org.everrest.core.resource.SubResourceLocatorDescriptor; import org.everrest.core.resource.SubResourceMethodDescriptor; import org.everrest.core.uri.UriPattern; import org.everrest.core.util.Tracer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.PathSegment; import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.function.Predicate; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Throwables.propagateIfPossible; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.stream.Collectors.toList; import static javax.ws.rs.core.HttpHeaders.ALLOW; import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; import static javax.ws.rs.core.HttpHeaders.LOCATION; import static javax.ws.rs.core.MediaType.TEXT_PLAIN; import static javax.ws.rs.core.Response.Status.ACCEPTED; import static javax.ws.rs.core.Response.Status.METHOD_NOT_ALLOWED; import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE; import static javax.ws.rs.core.Response.Status.NOT_FOUND; import static javax.ws.rs.core.Response.Status.UNSUPPORTED_MEDIA_TYPE; import static org.everrest.core.impl.header.HeaderHelper.convertToString; import static org.everrest.core.impl.header.MediaTypeHelper.findFistCompatibleAcceptMediaType; /** * Lookup resource which can serve request. * * @author andrew00x */ public class RequestDispatcher { /** Logger. */ private static final Logger LOG = LoggerFactory.getLogger(RequestDispatcher.class); /** See {@link org.everrest.core.ResourceBinder}. */ private final ResourceBinder resourceBinder; private final LoadingCache<Class<?>, ResourceDescriptor> locatorDescriptorCache; /** * Constructs new instance of RequestDispatcher. * * @param resourceBinder * See {@link org.everrest.core.ResourceBinder} */ public RequestDispatcher(ResourceBinder resourceBinder) { checkNotNull(resourceBinder); this.resourceBinder = resourceBinder; locatorDescriptorCache = CacheBuilder.newBuilder() .concurrencyLevel(8) .maximumSize(256) .expireAfterAccess(10, MINUTES) .build(new CacheLoader<Class<?>, ResourceDescriptor>() { @Override public ResourceDescriptor load(Class<?> aClass) { return new AbstractResourceDescriptor(aClass); } }); } /** * Dispatch {@link org.everrest.core.impl.ContainerRequest} to resource which can serve request. * * @param request * See {@link org.everrest.core.GenericContainerRequest} * @param response * See {@link org.everrest.core.GenericContainerResponse} */ public void dispatch(GenericContainerRequest request, GenericContainerResponse response) { ApplicationContext context = ApplicationContext.getCurrent(); String requestPath = getRequestPathWithoutMatrixParameters(context); List<String> parameterValues = context.getParameterValues(); ObjectFactory<ResourceDescriptor> resourceFactory = getRootResource(parameterValues, requestPath); if (resourceFactory == null) { LOG.debug("Root resource not found for {}", requestPath); response.setResponse(Response.status(NOT_FOUND) .entity(String.format("There is no any resources matched to request path %s", requestPath)) .type(TEXT_PLAIN) .build()); return; } // Take the tail of the request path, the tail will be requested path // for lower resources, e. g. ResourceClass -> Sub-resource method/locator String newRequestPath = getPathTail(parameterValues); context.addMatchedURI(requestPath.substring(0, requestPath.lastIndexOf(newRequestPath))); context.setParameterNames(resourceFactory.getObjectModel().getUriPattern().getParameterNames()); Object resource = resourceFactory.getInstance(context); dispatch(request, response, context, resourceFactory.getObjectModel(), resource, newRequestPath); } public ResourceBinder getResources() { return resourceBinder; } private String getRequestPathWithoutMatrixParameters(ApplicationContext context) { List<PathSegment> requestPathSegments = context.getPathSegments(false); if (requestPathSegments.isEmpty()) { return "/"; } StringBuilder requestPath = new StringBuilder(); for (PathSegment pathSegment : requestPathSegments) { requestPath.append('/'); requestPath.append(pathSegment.getPath()); } return requestPath.toString(); } /** * Get last element from path parameters. This element will be used as request path for child resources. * * @param parameterValues * See {@link ApplicationContext#getParameterValues()} * @return last element from given list or empty string if last element is null */ private String getPathTail(List<String> parameterValues) { int i = parameterValues.size() - 1; return parameterValues.get(i) == null ? "" : parameterValues.get(i); } /** * Process resource methods, sub-resource methods and sub-resource locators to find the best one for serve request. * * @param request * See {@link org.everrest.core.GenericContainerRequest} * @param response * See {@link org.everrest.core.GenericContainerResponse} * @param context * See {@link ApplicationContext} * @param resourceDescriptor * the root resource descriptor or resource descriptor which was created by previous sub-resource locator * @param resource * instance of resource class * @param requestPath * request path, it is relative path to the base URI or other resource which was called before * (one of sub-resource locators) */ private void dispatch(GenericContainerRequest request, GenericContainerResponse response, ApplicationContext context, ResourceDescriptor resourceDescriptor, Object resource, String requestPath) { List<String> parameterValues = context.getParameterValues(); String lastParameterValue = Iterables.getLast(parameterValues); boolean resourceMethodRequested = lastParameterValue == null || "/".equals(lastParameterValue); Map<String, List<ResourceMethodDescriptor>> resourceMethods = resourceDescriptor.getResourceMethods(); if (resourceMethodRequested && !resourceMethods.isEmpty()) { List<ResourceMethodDescriptor> matchedResourceMethods = new ArrayList<>(); boolean match = processResourceMethod(resourceMethods, request, response, matchedResourceMethods); if (match) { ResourceMethodDescriptor mostMatchedResourceMethod = matchedResourceMethods.get(0); if (Tracer.isTracingEnabled()) { Tracer.trace("Matched resource method for method \"%s\", media type \"%s\" = (%s)", request.getMethod(), request.getMediaType(), mostMatchedResourceMethod.getMethod()); } invokeResourceMethod(mostMatchedResourceMethod, resource, context, request, response); } else { LOG.debug("Not found resource method for method {}", request.getMethod()); } } else { Map<UriPattern, Map<String, List<SubResourceMethodDescriptor>>> subResourceMethods = resourceDescriptor.getSubResourceMethods(); Map<UriPattern, SubResourceLocatorDescriptor> subResourceLocators = resourceDescriptor.getSubResourceLocators(); List<SubResourceMethodDescriptor> matchedSubResourceMethods = new ArrayList<>(); boolean match = processSubResourceMethod(subResourceMethods, requestPath, request, response, parameterValues, matchedSubResourceMethods); List<SubResourceLocatorDescriptor> matchedSubResourceLocators = new ArrayList<>(); match |= processSubResourceLocator(subResourceLocators, requestPath, parameterValues, matchedSubResourceLocators); if (match) { response.setResponse(null); boolean foundMatchedSubResourceMethods = !matchedSubResourceMethods.isEmpty(); boolean foundMatchedSubResourceLocators = !matchedSubResourceLocators.isEmpty(); if (foundMatchedSubResourceMethods && Tracer.isTracingEnabled()) { Tracer.trace("Matched sub-resource method for method \"%s\", path \"%s\", media type \"%s\" = (%s)", request.getMethod(), requestPath, request.getMediaType(), matchedSubResourceMethods.get(0).getMethod()); } if (foundMatchedSubResourceLocators && Tracer.isTracingEnabled()) { Tracer.trace("Matched sub-resource locator for path \"%s\", media type \"%s\" = (%s)", requestPath, request.getMediaType(), matchedSubResourceLocators.get(0).getMethod()); } if (foundMatchedSubResourceMethods && (!foundMatchedSubResourceLocators || compareSubResources(matchedSubResourceMethods.get(0), matchedSubResourceLocators.get(0)) < 0)) { if (Tracer.isTracingEnabled()) { Tracer.trace("Sub-resource method (%s) selected", matchedSubResourceMethods.get(0).getMethod()); } invokeSubResourceMethod(requestPath, matchedSubResourceMethods.get(0), resource, context, request, response); } else { if (Tracer.isTracingEnabled()) { Tracer.trace("Sub-resource locator (%s) selected", matchedSubResourceLocators.get(0).getMethod()); } invokeSubResourceLocator(requestPath, matchedSubResourceLocators.get(0), resource, context, request, response); } } else { LOG.debug("Not found sub-resource methods nor sub-resource locators for path {} and method {}", requestPath, request.getMethod()); } } } /** * Invoke resource methods. * * @param resourceMethod * See {@link org.everrest.core.resource.ResourceMethodDescriptor} * @param resource * instance of resource class * @param context * See {@link ApplicationContext} * @param request * See {@link org.everrest.core.GenericContainerRequest} * @param response * See {@link org.everrest.core.GenericContainerResponse} * @see org.everrest.core.resource.ResourceMethodDescriptor */ private void invokeResourceMethod(ResourceMethodDescriptor resourceMethod, Object resource, ApplicationContext context, GenericContainerRequest request, GenericContainerResponse response) { context.addMatchedResource(resource); doInvokeResource(resourceMethod, resource, context, request, response); } /** * Invoke sub-resource methods. * * @param requestPath * request path * @param subResourceMethod * See {@link org.everrest.core.resource.SubResourceMethodDescriptor} * @param resource * instance of resource class * @param context * See {@link ApplicationContext} * @param request * See {@link org.everrest.core.GenericContainerRequest} * @param response * See {@link org.everrest.core.GenericContainerResponse} * @see org.everrest.core.resource.SubResourceMethodDescriptor */ private void invokeSubResourceMethod(String requestPath, SubResourceMethodDescriptor subResourceMethod, Object resource, ApplicationContext context, GenericContainerRequest request, GenericContainerResponse response) { context.addMatchedResource(resource); context.addMatchedURI(requestPath); context.setParameterNames(subResourceMethod.getUriPattern().getParameterNames()); doInvokeResource(subResourceMethod, resource, context, request, response); } private void doInvokeResource(ResourceMethodDescriptor method, Object resource, ApplicationContext context, GenericContainerRequest request, GenericContainerResponse response) { MethodInvoker invoker = context.getMethodInvoker(method); Object result = invoker.invokeMethod(resource, method, context); processResponse(result, request, response, method.produces(), context); } /** * Invoke sub-resource locators. * * @param requestPath * request path * @param subResourceLocator * See {@link org.everrest.core.resource.SubResourceLocatorDescriptor} * @param resource * instance of resource class * @param context * See {@link ApplicationContext} * @param request * See {@link org.everrest.core.GenericContainerRequest} * @param response * See {@link org.everrest.core.GenericContainerResponse} * @see org.everrest.core.resource.SubResourceLocatorDescriptor */ private void invokeSubResourceLocator(String requestPath, SubResourceLocatorDescriptor subResourceLocator, Object resource, ApplicationContext context, GenericContainerRequest request, GenericContainerResponse response) { context.addMatchedResource(resource); String newRequestPath = getPathTail(context.getParameterValues()); context.addMatchedURI(requestPath.substring(0, requestPath.lastIndexOf(newRequestPath))); context.setParameterNames(subResourceLocator.getUriPattern().getParameterNames()); MethodInvoker invoker = context.getMethodInvoker(subResourceLocator); Object newResource = invoker.invokeMethod(resource, subResourceLocator, context); ResourceDescriptor descriptor; try { descriptor = locatorDescriptorCache.get(newResource.getClass()); } catch (ExecutionException e) { propagateIfPossible(e.getCause()); throw new RuntimeException(e.getCause()); } @SuppressWarnings("unchecked") List<LifecycleComponent> perRequestComponents = (List<LifecycleComponent>)context.getAttributes().get("org.everrest.lifecycle.PerRequest"); if (perRequestComponents == null) { context.getAttributes().put("org.everrest.lifecycle.PerRequest", perRequestComponents = new ArrayList<>()); } // We do nothing for initialize resource since it is created by other resource but we lets to process 'destroy' method. perRequestComponents.add(new LifecycleComponent(newResource)); if (Tracer.isTracingEnabled()) { Tracer.trace("Sub-resource for request path \"%s\" = (%s)", newRequestPath, newResource); } dispatch(request, response, context, descriptor, newResource, newRequestPath); } /** * Compare two sub-resources. One of it is {@link org.everrest.core.resource.SubResourceMethodDescriptor} and other * one id * {@link org.everrest.core.resource.SubResourceLocatorDescriptor}. First compare UriPattern, see {@link * org.everrest.core.uri.UriPattern#URIPATTERN_COMPARATOR}. * NOTE * URI comparator compare UriPatterns for descending sorting. So it it return negative integer then it minds * SubResourceMethodDescriptor has higher priority by UriPattern comparison. If comparator return positive integer * then SubResourceLocatorDescriptor has higher priority. And finally if zero was returned then UriPattern is * equals, in this case SubResourceMethodDescriptor must be selected. * * @param subResourceMethod * See {@link org.everrest.core.resource.SubResourceMethodDescriptor} * @param subResourceLocator * See {@link org.everrest.core.resource.SubResourceLocatorDescriptor} * @return result of comparison sub-resources */ private int compareSubResources(SubResourceMethodDescriptor subResourceMethod, SubResourceLocatorDescriptor subResourceLocator) { int result = UriPattern.URIPATTERN_COMPARATOR.compare(subResourceMethod.getUriPattern(), subResourceLocator.getUriPattern()); // NOTE If patterns are the same sub-resource method has priority return result == 0 ? -1 : result; } /** * Process result of invoked method, and set {@link javax.ws.rs.core.Response} parameters dependent of returned * object. * * @param methodInvocationResult * result of invoked method * @param request * See {@link org.everrest.core.GenericContainerRequest} * @param response * See {@link org.everrest.core.GenericContainerResponse} * @param produces * list of method produces media types * @param context * @see org.everrest.core.resource.ResourceMethodDescriptor * @see org.everrest.core.resource.SubResourceMethodDescriptor * @see org.everrest.core.resource.SubResourceLocatorDescriptor */ private void processResponse(Object methodInvocationResult, GenericContainerRequest request, GenericContainerResponse response, List<MediaType> produces, ApplicationContext context) { if (response.getResponse() != null) { // Response may be set for asynchronous jobs. return; } if (methodInvocationResult == null || methodInvocationResult.getClass() == void.class || methodInvocationResult.getClass() == Void.class) { response.setResponse(Response.noContent().build()); } else if (methodInvocationResult instanceof AsynchronousJob) { final String internalJobUri = ((AsynchronousJob)methodInvocationResult).getJobURI(); final String externalJobUri = context.getBaseUriBuilder().path(internalJobUri).build().toString(); response.setResponse(Response.status(ACCEPTED) .header(LOCATION, externalJobUri) .entity(externalJobUri) .type(TEXT_PLAIN).build()); } else { MediaType contentType = request.getAcceptableMediaType(produces); if (Response.class.isAssignableFrom(methodInvocationResult.getClass())) { Response resultResponse = (Response)methodInvocationResult; if (resultResponse.getMetadata().getFirst(CONTENT_TYPE) == null && resultResponse.getEntity() != null) { resultResponse.getMetadata().putSingle(CONTENT_TYPE, contentType); } response.setResponse(resultResponse); } else { response.setResponse(Response.ok(methodInvocationResult, contentType).build()); } } } /** * Process resource methods. * * @param <T> * ResourceMethodDescriptor extension * @param resourceMethods * resource methods * @param request * See {@link org.everrest.core.GenericContainerRequest} * @param response * See {@link org.everrest.core.GenericContainerResponse} * @param matchedMethods * list for matched method resources * @return true if at least one resource method found false otherwise */ private <T extends ResourceMethodDescriptor> boolean processResourceMethod(Map<String, List<T>> resourceMethods, GenericContainerRequest request, GenericContainerResponse response, List<T> matchedMethods) { final String httpMethod = request.getMethod(); List<T> resourceMethodsByHttpMethod = resourceMethods.get(httpMethod); if (resourceMethodsByHttpMethod == null || resourceMethodsByHttpMethod.size() == 0) { response.setResponse(Response.status(METHOD_NOT_ALLOWED) .header(ALLOW, convertToString(getAllow(resourceMethods))) .entity(String.format("%s method is not allowed", httpMethod)) .type(TEXT_PLAIN) .build()); return false; } List<T> resourceMethodCandidates = new ArrayList<>(); MediaType contentType = request.getMediaType(); if (contentType == null) { resourceMethodCandidates.addAll(resourceMethodsByHttpMethod); } else { resourceMethodCandidates.addAll(resourceMethodsByHttpMethod.stream() .filter(resourceMethod -> MediaTypeHelper.isConsume(resourceMethod.consumes(), contentType)) .collect(toList())); } if (resourceMethodCandidates.isEmpty()) { response.setResponse(Response.status(UNSUPPORTED_MEDIA_TYPE) .entity(String.format("Media type %s is not supported", contentType)) .type(TEXT_PLAIN) .build()); return false; } List<AcceptMediaType> acceptMediaTypes = request.getAcceptMediaTypeList(); resourceMethodCandidates = resourceMethodCandidates.stream() .filter(notAcceptableFilter(acceptMediaTypes)) .sorted(byAcceptMediaTypeComparator(acceptMediaTypes)) .collect(toList()); if (resourceMethodCandidates.isEmpty()) { response.setResponse(Response.status(NOT_ACCEPTABLE).entity("Not Acceptable").type(TEXT_PLAIN).build()); return false; } matchedMethods.addAll(resourceMethodCandidates); return true; } private <T extends ResourceMethodDescriptor> Comparator<T> byAcceptMediaTypeComparator(List<AcceptMediaType> acceptMediaTypes) { return (resourceMethodOne, resourceMethodTwo) -> Float.compare(findFistCompatibleAcceptMediaType(acceptMediaTypes, resourceMethodTwo.produces()).getQvalue(), findFistCompatibleAcceptMediaType(acceptMediaTypes, resourceMethodOne.produces()).getQvalue()); } private <T extends ResourceMethodDescriptor> Predicate<T> notAcceptableFilter(List<AcceptMediaType> acceptMediaTypes) { return resourceMethod -> findFistCompatibleAcceptMediaType(acceptMediaTypes, resourceMethod.produces()) != null; } private <T extends ResourceMethodDescriptor> Collection<String> getAllow(Map<String, List<T>> resourceMethods) { List<String> allowed = new ArrayList<>(); for (Map.Entry<String, List<T>> entry : resourceMethods.entrySet()) { if (entry.getValue() == null || entry.getValue().isEmpty()) { continue; } allowed.add(entry.getKey()); } return allowed; } /** * Process sub-resource methods. * * @param subResourceMethods * sub-resource methods * @param requestedPath * part of requested path * @param request * See {@link org.everrest.core.GenericContainerRequest} * @param response * See {@link org.everrest.core.GenericContainerResponse} * @param capturedValues * the list for keeping template values. See * {@link javax.ws.rs.core.UriInfo#getPathParameters()} * @param matchedMethods * list for method resources * @return true if at least one sub-resource method found false otherwise */ private boolean processSubResourceMethod(Map<UriPattern, Map<String, List<SubResourceMethodDescriptor>>> subResourceMethods, String requestedPath, GenericContainerRequest request, GenericContainerResponse response, List<String> capturedValues, List<SubResourceMethodDescriptor> matchedMethods) { Map<String, List<SubResourceMethodDescriptor>> resourceMethods = null; for (Entry<UriPattern, Map<String, List<SubResourceMethodDescriptor>>> entry : subResourceMethods.entrySet()) { if (entry.getKey().match(requestedPath, capturedValues)) { String lastCapturedValue = Iterables.getLast(capturedValues); if (lastCapturedValue == null || "/".equals(lastCapturedValue)) { resourceMethods = entry.getValue(); if (resourceMethods.containsKey(request.getMethod())) { break; } } } } if (resourceMethods == null) { response.setResponse(Response.status(NOT_FOUND) .entity(String.format("There is no any resources matched to request path %s", requestedPath)) .type(TEXT_PLAIN) .build()); return false; } return processResourceMethod(resourceMethods, request, response, matchedMethods); } /** * Process sub-resource locators. * * @param subResourceLocators * sub-resource locators * @param requestedPath * part of requested path * @param capturingValues * the list for keeping template values * @param locators * list for sub-resource locators * @return true if at least one SubResourceLocatorDescriptor found false otherwise */ private boolean processSubResourceLocator(Map<UriPattern, SubResourceLocatorDescriptor> subResourceLocators, String requestedPath, List<String> capturingValues, List<SubResourceLocatorDescriptor> locators) { locators.addAll(subResourceLocators.entrySet().stream() .filter(e -> e.getKey().match(requestedPath, capturingValues)) .map(e -> e.getValue()) .collect(toList())); return !locators.isEmpty(); } /** * Get root resource. * * @param parameterValues * is taken from context * @param requestPath * is taken from context * @return root resource or {@code null} */ private ObjectFactory<ResourceDescriptor> getRootResource(List<String> parameterValues, String requestPath) { ObjectFactory<ResourceDescriptor> resourceFactory = resourceBinder.getMatchedResource(requestPath, parameterValues); if (resourceFactory != null) { if (Tracer.isTracingEnabled()) { ResourceDescriptor resourceDescriptor = resourceFactory.getObjectModel(); Tracer.trace("Matched root resource for request path \"%s\" = (@Path \"%s\", %s)", requestPath, resourceDescriptor.getPathValue().getPath(), resourceDescriptor.getObjectClass()); } } return resourceFactory; } }