/* * Copyright 2002-2013 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 com.revolsys.ui.web.rest.interceptor; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; 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.BridgeMethodResolver; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.PathMatcher; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; import com.revolsys.ui.web.annotation.RequestMapping; import com.revolsys.util.Property; /** * Support class for resolving web method annotations in a handler type. * Processes {@code @RequestMapping}, {@code @InitBinder}, * {@code @ModelAttribute} and {@code @SessionAttributes}. * * <p>Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} * and {@link org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}. * * @author Juergen Hoeller * @since 2.5.2 * @see com.revolsys.ui.web.annotation.RequestMapping * @see org.springframework.web.bind.annotation.InitBinder * @see org.springframework.web.bind.annotation.ModelAttribute * @see org.springframework.web.bind.annotation.SessionAttributes */ public class AnnotationHandlerMethodResolver { private final Set<WebMethodHandler> handlerMethods = new LinkedHashSet<>(); private final RequestMapping typeLevelMapping; private final WebAnnotationMethodHandlerAdapter adapter; public AnnotationHandlerMethodResolver(final WebAnnotationMethodHandlerAdapter adapter, final Class<?> handlerType) { this.adapter = adapter; final Set<Class<?>> handlerTypes = new LinkedHashSet<>(); Class<?> specificHandlerType = null; if (!Proxy.isProxyClass(handlerType)) { handlerTypes.add(handlerType); specificHandlerType = handlerType; } handlerTypes.addAll(Arrays.asList(handlerType.getInterfaces())); for (final Class<?> currentHandlerType : handlerTypes) { final Class<?> targetClass; if (specificHandlerType == null) { targetClass = currentHandlerType; } else { targetClass = specificHandlerType; } ReflectionUtils.doWithMethods(currentHandlerType, (method) -> { final Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass); final Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); if (isHandlerMethod(specificMethod) && (bridgedMethod == specificMethod || !isHandlerMethod(bridgedMethod))) { AnnotationHandlerMethodResolver.this.handlerMethods .add(new WebMethodHandler(AnnotationHandlerMethodResolver.this.adapter, method)); } }, ReflectionUtils.USER_DECLARED_METHODS); } this.typeLevelMapping = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class); } @SuppressWarnings("unchecked") private void extractHandlerMethodUriTemplates(final String mappedPath, final String lookupPath, final HttpServletRequest request) { Map<String, String> variables = null; final boolean hasSuffix = mappedPath.indexOf('.') != -1; if (!hasSuffix && this.adapter.pathMatcher.match(mappedPath + ".*", lookupPath)) { final String realPath = mappedPath + ".*"; if (this.adapter.pathMatcher.match(realPath, lookupPath)) { variables = this.adapter.pathMatcher.extractUriTemplateVariables(realPath, lookupPath); } } if (variables == null && !mappedPath.startsWith("/")) { String realPath = "/**/" + mappedPath; if (this.adapter.pathMatcher.match(realPath, lookupPath)) { variables = this.adapter.pathMatcher.extractUriTemplateVariables(realPath, lookupPath); } else { realPath = realPath + ".*"; if (this.adapter.pathMatcher.match(realPath, lookupPath)) { variables = this.adapter.pathMatcher.extractUriTemplateVariables(realPath, lookupPath); } } } if (!CollectionUtils.isEmpty(variables)) { final Map<String, String> typeVariables = (Map<String, String>)request .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); if (typeVariables != null) { variables.putAll(typeVariables); } request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, variables); } } /** * Determines the matched pattern for the given methodLevelPattern and path. * <p> * Uses the following algorithm: * <ol> * <li>If there is a type-level mapping with path information, it is * {@linkplain PathMatcher#combine(String, String) combined} with the * method-level pattern. * <li>If there is a * {@linkplain HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE best matching * pattern} in the request, it is combined with the method-level pattern. * <li>Otherwise, */ private String getMatchedPattern(final String methodLevelPattern, final String lookupPath, final HttpServletRequest request) { if (hasTypeLevelMapping() && !ObjectUtils.isEmpty(this.typeLevelMapping.value())) { final String[] typeLevelPatterns = this.typeLevelMapping.value(); for (String typeLevelPattern : typeLevelPatterns) { if (!typeLevelPattern.startsWith("/")) { typeLevelPattern = "/" + typeLevelPattern; } final String combinedPattern = this.adapter.pathMatcher.combine(typeLevelPattern, methodLevelPattern); if (isPathMatchInternal(combinedPattern, lookupPath)) { return combinedPattern; } } return null; } final String bestMatchingPattern = (String)request .getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); if (Property.hasValue(bestMatchingPattern)) { final String combinedPattern = this.adapter.pathMatcher.combine(bestMatchingPattern, methodLevelPattern); if (!combinedPattern.equals(bestMatchingPattern) && isPathMatchInternal(combinedPattern, lookupPath)) { return combinedPattern; } } if (isPathMatchInternal(methodLevelPattern, lookupPath)) { return methodLevelPattern; } return null; } public final boolean hasHandlerMethods() { return !this.handlerMethods.isEmpty(); } public boolean hasTypeLevelMapping() { return this.typeLevelMapping != null; } protected boolean isHandlerMethod(final Method method) { return AnnotationUtils.findAnnotation(method, RequestMapping.class) != null; } private boolean isPathMatchInternal(final String pattern, final String lookupPath) { if (pattern.equals(lookupPath) || this.adapter.pathMatcher.match(pattern, lookupPath)) { return true; } final boolean hasSuffix = pattern.indexOf('.') != -1; if (!hasSuffix && this.adapter.pathMatcher.match(pattern + ".*", lookupPath)) { return true; } final boolean endsWithSlash = pattern.endsWith("/"); if (!endsWithSlash && this.adapter.pathMatcher.match(pattern + "/", lookupPath)) { return true; } return false; } public WebMethodHandler resolveHandlerMethod(final HttpServletRequest request) throws ServletException { final String lookupPath = this.adapter.urlPathHelper.getLookupPathForRequest(request); final Comparator<String> pathComparator = this.adapter.pathMatcher .getPatternComparator(lookupPath); final Map<RequestMappingInfo, WebMethodHandler> targetHandlerMethods = new LinkedHashMap<>(); final Set<String> allowedMethods = new LinkedHashSet<>(7); String resolvedMethodName = null; for (final WebMethodHandler webMethodHandler : this.handlerMethods) { final Method handlerMethod = webMethodHandler.getMethod(); final RequestMappingInfo mappingInfo = new RequestMappingInfo(); final RequestMapping mapping = AnnotationUtils.findAnnotation(handlerMethod, RequestMapping.class); mappingInfo.paths = mapping.value(); if (!hasTypeLevelMapping() || !Arrays.equals(mapping.method(), this.typeLevelMapping.method())) { mappingInfo.methods = mapping.method(); } boolean match = false; if (mappingInfo.paths.length > 0) { final List<String> matchedPaths = new ArrayList<>(mappingInfo.paths.length); for (final String methodLevelPattern : mappingInfo.paths) { final String matchedPattern = getMatchedPattern(methodLevelPattern, lookupPath, request); if (matchedPattern != null) { if (mappingInfo.matches(request)) { match = true; matchedPaths.add(matchedPattern); } else { for (final RequestMethod requestMethod : mappingInfo.methods) { allowedMethods.add(requestMethod.toString()); } break; } } } Collections.sort(matchedPaths, pathComparator); mappingInfo.matchedPaths = matchedPaths; } else { // No paths specified: parameter match sufficient. match = mappingInfo.matches(request); if (match && mappingInfo.methods.length == 0 && resolvedMethodName != null && !resolvedMethodName.equals(handlerMethod.getName())) { match = false; } else { for (final RequestMethod requestMethod : mappingInfo.methods) { allowedMethods.add(requestMethod.toString()); } } } if (match) { WebMethodHandler oldMappedMethod = targetHandlerMethods.put(mappingInfo, webMethodHandler); if (oldMappedMethod != null && oldMappedMethod != webMethodHandler) { if (this.adapter.methodNameResolver != null && mappingInfo.paths.length == 0) { if (!oldMappedMethod.getMethod().getName().equals(handlerMethod.getName())) { if (resolvedMethodName == null) { resolvedMethodName = this.adapter.methodNameResolver.getHandlerMethodName(request); } if (!resolvedMethodName.equals(oldMappedMethod.getMethod().getName())) { oldMappedMethod = null; } if (!resolvedMethodName.equals(handlerMethod.getName())) { if (oldMappedMethod != null) { targetHandlerMethods.put(mappingInfo, oldMappedMethod); oldMappedMethod = null; } else { targetHandlerMethods.remove(mappingInfo); } } } } if (oldMappedMethod != null) { throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" + lookupPath + "': {" + oldMappedMethod + ", " + handlerMethod + "}. If you intend to handle the same path in multiple methods, then factor " + "them out into a dedicated handler class with that path mapped at the type level!"); } } } } if (!targetHandlerMethods.isEmpty()) { final List<RequestMappingInfo> matches = new ArrayList<>(targetHandlerMethods.keySet()); final RequestMappingInfoComparator requestMappingInfoComparator = new RequestMappingInfoComparator( pathComparator); Collections.sort(matches, requestMappingInfoComparator); final RequestMappingInfo bestMappingMatch = matches.get(0); final String bestMatchedPath = bestMappingMatch.bestMatchedPath(); if (bestMatchedPath != null) { extractHandlerMethodUriTemplates(bestMatchedPath, lookupPath, request); } return targetHandlerMethods.get(bestMappingMatch); } else { if (!allowedMethods.isEmpty()) { throw new HttpRequestMethodNotSupportedException(request.getMethod(), StringUtils.toStringArray(allowedMethods)); } else { throw new NoSuchRequestHandlingMethodException(lookupPath, request.getMethod(), request.getParameterMap()); } } } }