Extract base class from RequestMappingHandlerMapping, one that allows for discovering request mappings from something other than annotations

This commit is contained in:
Rossen Stoyanchev
2011-06-14 09:20:07 +00:00
parent b2be59c2e4
commit fa0b683161
7 changed files with 285 additions and 212 deletions

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
package org.springframework.web.servlet.mvc.method;
import java.util.ArrayList;
import java.util.Collection;

View File

@@ -0,0 +1,228 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;
/**
* An {@link AbstractHandlerMethodMapping} variant that uses {@link RequestMappingInfo} to represent request
* mapping conditions.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1.0
*/
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
@Override
protected void handlerMethodsInitialized(Map<RequestMappingInfo, HandlerMethod> handlerMethods) {
List<RequestMappingInfo> mappings = new ArrayList<RequestMappingInfo>(handlerMethods.keySet());
while (mappings.size() > 1) {
RequestMappingInfo mapping = mappings.remove(0);
for (RequestMappingInfo otherMapping : mappings) {
// further validate mapping conditions
}
}
}
@Override
protected Set<String> getMappingPaths(RequestMappingInfo mapping) {
return mapping.getPatterns();
}
/**
* Returns a new {@link RequestMappingInfo} with attributes matching to the current request or {@code null}.
*
* @see RequestMappingInfo#getMatchingRequestMapping(String, HttpServletRequest, PathMatcher)
*/
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo mapping,
String lookupPath,
HttpServletRequest request) {
return mapping.getMatchingRequestMapping(lookupPath, request, getPathMatcher());
}
/**
* Returns a {@link Comparator} that can be used to sort and select the best matching {@link RequestMappingInfo}.
*/
@Override
protected Comparator<RequestMappingInfo> getMappingComparator(String lookupPath, HttpServletRequest request) {
return new RequestMappingInfoComparator(lookupPath, request);
}
/**
* Exposes URI template variables and producible media types as request attributes.
*
* @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
* @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
*/
@Override
protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
super.handleMatch(info, lookupPath, request);
String pattern = info.getPatterns().iterator().next();
Map<String, String> uriTemplateVariables = getPathMatcher().extractUriTemplateVariables(pattern, lookupPath);
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
if (!info.getProduces().isEmpty()) {
Set<MediaType> mediaTypes = info.getProduces().getMediaTypes();
request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
}
}
/**
* Iterates all {@link RequestMappingInfo}s looking for mappings that match by URL but not by HTTP method.
*
* @throws HttpRequestMethodNotSupportedException if there are matches by URL but not by HTTP method
*/
@Override
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos,
String lookupPath,
HttpServletRequest request) throws ServletException {
Set<String> allowedMethods = new HashSet<String>(6);
Set<MediaType> consumableMediaTypes = new HashSet<MediaType>();
Set<MediaType> producibleMediaTypes = new HashSet<MediaType>();
for (RequestMappingInfo info : requestMappingInfos) {
for (String pattern : info.getPatterns()) {
if (getPathMatcher().match(pattern, lookupPath)) {
if (!info.getMethods().match(request)) {
for (RequestMethod method : info.getMethods().getMethods()) {
allowedMethods.add(method.name());
}
}
if (!info.getConsumes().match(request)) {
consumableMediaTypes.addAll(info.getConsumes().getMediaTypes());
}
if (!info.getProduces().match(request)) {
producibleMediaTypes.addAll(info.getProduces().getMediaTypes());
}
}
}
}
if (!allowedMethods.isEmpty()) {
throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods);
}
else if (!consumableMediaTypes.isEmpty()) {
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
contentType = MediaType.parseMediaType(request.getContentType());
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(consumableMediaTypes));
}
else if (!producibleMediaTypes.isEmpty()) {
throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes));
}
else {
return null;
}
}
/**
* A comparator for {@link RequestMappingInfo}s. Effective comparison can only be done in the context of a specific
* request. For example not all {@link RequestMappingInfo} patterns may apply to the current request. Therefore an
* HttpServletRequest is required as input.
*
* <p>Furthermore, the following assumptions are made about the input RequestMappings: <ul><li>Each RequestMappingInfo
* has been fully matched to the request <li>The RequestMappingInfo contains matched patterns only <li>Patterns are
* ordered with the best matching pattern at the top </ul>
*
* @see RequestMappingInfoHandlerMapping#getMatchingMapping(RequestMappingInfo, String, HttpServletRequest)
*/
private class RequestMappingInfoComparator implements Comparator<RequestMappingInfo> {
private Comparator<String> patternComparator;
private List<MediaType> requestAcceptHeader;
public RequestMappingInfoComparator(String lookupPath, HttpServletRequest request) {
this.patternComparator = getPathMatcher().getPatternComparator(lookupPath);
String acceptHeader = request.getHeader("Accept");
this.requestAcceptHeader = MediaType.parseMediaTypes(acceptHeader);
MediaType.sortByQualityValue(this.requestAcceptHeader);
}
public int compare(RequestMappingInfo mapping, RequestMappingInfo otherMapping) {
int result = comparePatterns(mapping.getPatterns(), otherMapping.getPatterns());
if (result != 0) {
return result;
}
result = mapping.getParams().compareTo(otherMapping.getParams());
if (result != 0) {
return result;
}
result = mapping.getHeaders().compareTo(otherMapping.getHeaders());
if (result != 0) {
return result;
}
result = mapping.getConsumes().compareTo(otherMapping.getConsumes());
if (result != 0) {
return result;
}
result = mapping.getProduces().compareTo(otherMapping.getProduces(), this.requestAcceptHeader);
if (result != 0) {
return result;
}
result = mapping.getMethods().compareTo(otherMapping.getMethods());
if (result != 0) {
return result;
}
return 0;
}
private int comparePatterns(Set<String> patterns, Set<String> otherPatterns) {
Iterator<String> iterator = patterns.iterator();
Iterator<String> iteratorOther = otherPatterns.iterator();
while (iterator.hasNext() && iteratorOther.hasNext()) {
int result = patternComparator.compare(iterator.next(), iteratorOther.next());
if (result != 0) {
return result;
}
}
if (iterator.hasNext()) {
return -1;
}
else if (iteratorOther.hasNext()) {
return 1;
}
else {
return 0;
}
}
}
}

View File

@@ -17,42 +17,25 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.servlet.mvc.method.condition.RequestConditionFactory;
/**
* An {@link AbstractHandlerMethodMapping} variant that uses {@link RequestMappingInfo}s for the registration and the
* lookup of {@link HandlerMethod}s.
* A sub-class of {@link RequestMappingInfoHandlerMapping} that prepares {@link RequestMappingInfo}s
* from @{@link RequestMapping} annotations on @{@link Controller} classes.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1.0
*/
public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping {
/**
* {@inheritDoc} The handler determination in this method is made based on the presence of a type-level {@link
@@ -63,17 +46,6 @@ public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<R
return AnnotationUtils.findAnnotation(beanType, Controller.class) != null;
}
@Override
protected void handlerMethodsInitialized(Map<RequestMappingInfo, HandlerMethod> handlerMethods) {
List<RequestMappingInfo> mappings = new ArrayList<RequestMappingInfo>(handlerMethods.keySet());
while (mappings.size() > 1) {
RequestMappingInfo mapping = mappings.remove(0);
for (RequestMappingInfo otherMapping : mappings) {
// further validate mapping conditions
}
}
}
/**
* Provides a {@link RequestMappingInfo} for the given method. <p>Only {@link RequestMapping @RequestMapping}-annotated
* methods are considered. Type-level {@link RequestMapping @RequestMapping} annotations are also detected and their
@@ -112,170 +84,4 @@ public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<R
RequestConditionFactory.parseProduces(annotation.produces(), annotation.headers()));
}
@Override
protected Set<String> getMappingPaths(RequestMappingInfo mapping) {
return mapping.getPatterns();
}
/**
* Returns a new {@link RequestMappingInfo} with attributes matching to the current request or {@code null}.
*
* @see RequestMappingInfo#getMatchingRequestMapping(String, HttpServletRequest, PathMatcher)
*/
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo mapping,
String lookupPath,
HttpServletRequest request) {
return mapping.getMatchingRequestMapping(lookupPath, request, getPathMatcher());
}
/**
* Returns a {@link Comparator} that can be used to sort and select the best matching {@link RequestMappingInfo}.
*/
@Override
protected Comparator<RequestMappingInfo> getMappingComparator(String lookupPath, HttpServletRequest request) {
return new RequestMappingInfoComparator(lookupPath, request);
}
/**
* Exposes URI template variables and producible media types as request attributes.
*
* @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
* @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
*/
@Override
protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
super.handleMatch(info, lookupPath, request);
String pattern = info.getPatterns().iterator().next();
Map<String, String> uriTemplateVariables = getPathMatcher().extractUriTemplateVariables(pattern, lookupPath);
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
if (!info.getProduces().isEmpty()) {
Set<MediaType> mediaTypes = info.getProduces().getMediaTypes();
request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
}
}
/**
* Iterates all {@link RequestMappingInfo}s looking for mappings that match by URL but not by HTTP method.
*
* @throws HttpRequestMethodNotSupportedException if there are matches by URL but not by HTTP method
*/
@Override
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos,
String lookupPath,
HttpServletRequest request) throws ServletException {
Set<String> allowedMethods = new HashSet<String>(6);
Set<MediaType> consumableMediaTypes = new HashSet<MediaType>();
Set<MediaType> producibleMediaTypes = new HashSet<MediaType>();
for (RequestMappingInfo info : requestMappingInfos) {
for (String pattern : info.getPatterns()) {
if (getPathMatcher().match(pattern, lookupPath)) {
if (!info.getMethods().match(request)) {
for (RequestMethod method : info.getMethods().getMethods()) {
allowedMethods.add(method.name());
}
}
if (!info.getConsumes().match(request)) {
consumableMediaTypes.addAll(info.getConsumes().getMediaTypes());
}
if (!info.getProduces().match(request)) {
producibleMediaTypes.addAll(info.getProduces().getMediaTypes());
}
}
}
}
if (!allowedMethods.isEmpty()) {
throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods);
}
else if (!consumableMediaTypes.isEmpty()) {
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
contentType = MediaType.parseMediaType(request.getContentType());
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(consumableMediaTypes));
}
else if (!producibleMediaTypes.isEmpty()) {
throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes));
}
else {
return null;
}
}
/**
* A comparator for {@link RequestMappingInfo}s. Effective comparison can only be done in the context of a specific
* request. For example not all {@link RequestMappingInfo} patterns may apply to the current request. Therefore an
* HttpServletRequest is required as input.
*
* <p>Furthermore, the following assumptions are made about the input RequestMappings: <ul><li>Each RequestMappingInfo
* has been fully matched to the request <li>The RequestMappingInfo contains matched patterns only <li>Patterns are
* ordered with the best matching pattern at the top </ul>
*
* @see RequestMappingHandlerMapping#getMatchingMapping(RequestMappingInfo, String, HttpServletRequest)
*/
private class RequestMappingInfoComparator implements Comparator<RequestMappingInfo> {
private Comparator<String> patternComparator;
private List<MediaType> requestAcceptHeader;
public RequestMappingInfoComparator(String lookupPath, HttpServletRequest request) {
this.patternComparator = getPathMatcher().getPatternComparator(lookupPath);
String acceptHeader = request.getHeader("Accept");
this.requestAcceptHeader = MediaType.parseMediaTypes(acceptHeader);
MediaType.sortByQualityValue(this.requestAcceptHeader);
}
public int compare(RequestMappingInfo mapping, RequestMappingInfo otherMapping) {
int result = comparePatterns(mapping.getPatterns(), otherMapping.getPatterns());
if (result != 0) {
return result;
}
result = mapping.getParams().compareTo(otherMapping.getParams());
if (result != 0) {
return result;
}
result = mapping.getHeaders().compareTo(otherMapping.getHeaders());
if (result != 0) {
return result;
}
result = mapping.getConsumes().compareTo(otherMapping.getConsumes());
if (result != 0) {
return result;
}
result = mapping.getProduces().compareTo(otherMapping.getProduces(), this.requestAcceptHeader);
if (result != 0) {
return result;
}
result = mapping.getMethods().compareTo(otherMapping.getMethods());
if (result != 0) {
return result;
}
return 0;
}
private int comparePatterns(Set<String> patterns, Set<String> otherPatterns) {
Iterator<String> iterator = patterns.iterator();
Iterator<String> iteratorOther = otherPatterns.iterator();
while (iterator.hasNext() && iteratorOther.hasNext()) {
int result = patternComparator.compare(iterator.next(), iteratorOther.next());
if (result != 0) {
return result;
}
}
if (iterator.hasNext()) {
return -1;
}
else if (iteratorOther.hasNext()) {
return 1;
}
else {
return 0;
}
}
}
}

View File

@@ -19,8 +19,7 @@ package org.springframework.web.servlet.mvc.method.condition;
import javax.servlet.http.HttpServletRequest;
/**
* Defines the contract for conditions that must be met before an incoming request matches a {@link
* org.springframework.web.servlet.mvc.method.annotation.RequestMappingInfo RequestKey}.
* Defines the contract for conditions that must be met given an incoming request.
*
* <p>Implementations of this interface are created by the {@link RequestConditionFactory}.
*