View Javadoc

1   package org.cateproject.view.cache;
2   
3   import java.lang.reflect.Method;
4   import java.util.ArrayList;
5   import java.util.Arrays;
6   import java.util.Collection;
7   import java.util.Collections;
8   import java.util.Comparator;
9   import java.util.HashMap;
10  import java.util.LinkedHashMap;
11  import java.util.LinkedHashSet;
12  import java.util.List;
13  import java.util.Map;
14  import java.util.Set;
15  import java.util.SortedSet;
16  import java.util.TreeSet;
17  import java.util.concurrent.ConcurrentHashMap;
18  
19  import javax.servlet.ServletException;
20  import javax.servlet.http.HttpServletRequest;
21  
22  import org.cateproject.controller.Cacheable;
23  import org.slf4j.Logger;
24  import org.slf4j.LoggerFactory;
25  import org.springframework.beans.factory.annotation.Autowired;
26  import org.springframework.core.OrderComparator;
27  import org.springframework.core.annotation.AnnotationUtils;
28  import org.springframework.http.MediaType;
29  import org.springframework.http.server.ServerHttpRequest;
30  import org.springframework.http.server.ServletServerHttpRequest;
31  import org.springframework.util.AntPathMatcher;
32  import org.springframework.util.ClassUtils;
33  import org.springframework.util.ObjectUtils;
34  import org.springframework.util.PathMatcher;
35  import org.springframework.util.StringUtils;
36  import org.springframework.web.HttpRequestMethodNotSupportedException;
37  import org.springframework.web.bind.annotation.RequestMapping;
38  import org.springframework.web.bind.annotation.RequestMethod;
39  import org.springframework.web.bind.annotation.support.HandlerMethodResolver;
40  import org.springframework.web.context.request.RequestAttributes;
41  import org.springframework.web.context.request.RequestContextHolder;
42  import org.springframework.web.context.request.ServletRequestAttributes;
43  import org.springframework.web.servlet.HandlerExecutionChain;
44  import org.springframework.web.servlet.HandlerMapping;
45  import org.springframework.web.servlet.View;
46  import org.springframework.web.servlet.handler.AbstractHandlerMapping;
47  import org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver;
48  import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver;
49  import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
50  import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
51  import org.springframework.web.util.UrlPathHelper;
52  
53  public class CateContentTypeResolverImpl  implements CateContentTypeResolver {
54  	private static final Logger logger = LoggerFactory.getLogger(CateContentTypeResolverImpl.class);
55  	private SortedSet<AbstractHandlerMapping> handlerMappings = new TreeSet<AbstractHandlerMapping>(new OrderComparator());
56  	
57  	private UrlPathHelper urlPathHelper = new UrlPathHelper();
58  
59  	private PathMatcher pathMatcher = new AntPathMatcher();
60  
61  	private MethodNameResolver methodNameResolver = new InternalPathMethodNameResolver();
62  	
63  	private Map<String, String> extensions = new HashMap<String,String>();
64  	
65  	ContentNegotiatingViewResolver viewResolver;
66      
67  	@Autowired
68      public void setHandlerMappings(Collection<AbstractHandlerMapping> handlerMappings) {
69  		this.handlerMappings.addAll(handlerMappings);
70  	}
71  
72  	public void setViewResolver(ContentNegotiatingViewResolver viewResolver) {
73  		this.viewResolver = viewResolver;
74  	}
75  
76  	/**
77       * Because we use this in conjunction with the 
78       * ContentNegotiatingViewResolver we need to reverse the map of
79       * MediaType to extension to do the reverse lookup
80       */
81      public void setExtensions(Map<String,String> mediaTypes) {
82        for(String value : mediaTypes.keySet()) {
83            this.extensions.put(mediaTypes.get(value),value);
84        }
85      }  
86  	
87  	private final Map<Class<?>, ServletHandlerMethodResolver> methodResolverCache = new ConcurrentHashMap<Class<?>, ServletHandlerMethodResolver>();
88  
89      private ServletHandlerMethodResolver getMethodResolver(Object handler) {
90  	    Class handlerClass = ClassUtils.getUserClass(handler);
91  	    ServletHandlerMethodResolver resolver = this.methodResolverCache.get(handlerClass);
92  	    if (resolver == null) {
93  		    resolver = new ServletHandlerMethodResolver(handlerClass);
94  		    this.methodResolverCache.put(handlerClass, resolver);
95  	    }
96  	    return resolver;
97      }
98      
99  	public String resolveContentType(HttpServletRequest request) throws Exception {
100 		Object handler = null;
101 	    for(AbstractHandlerMapping handlerMapping : handlerMappings) {
102 	        HandlerExecutionChain handlerExecutionChain = handlerMapping.getHandler(request);
103 	        if(handlerExecutionChain != null) {
104 	            handler = handlerExecutionChain.getHandler();
105 	            break;
106 	        }
107 	    }
108 	    
109 	    if(handler != null) {
110 	    	logger.debug("Got handler " + handler + " for request " + request.getRequestURI());
111 	    	 final ServletHandlerMethodResolver methodResolver = getMethodResolver(handler);
112 	    	 final Method handlerMethod = methodResolver.resolveHandlerMethod(request);
113 
114 	         handlerMethod.setAccessible(true);
115 	         if(handlerMethod.isAnnotationPresent(Cacheable.class)) {
116 	               Cacheable cacheable = handlerMethod.getAnnotation(Cacheable.class);
117 	               logger.debug("Resolving view for view name " + cacheable.value());
118 	               RequestAttributes requestAttributes = new ServletRequestAttributes(request);
119 	               RequestContextHolder.setRequestAttributes(requestAttributes);
120 	               View view = viewResolver.resolveViewName(cacheable.value(), request.getLocale());
121 	               if(view == null) {
122 	            	   logger.debug("No view for view name " + cacheable.value() + " returning null ");
123 	            	   return null;
124 	               } else {
125 	                   String contentType = view.getContentType();
126 	                   if(contentType.indexOf(";") == -1) {
127 	                	   logger.debug("Found view for view name " + cacheable.value() + " content Type " + contentType + " returning extension " + extensions.get(contentType));
128 	                	   return extensions.get(contentType);
129 	                   } else {
130 	                	   logger.debug("Found view for view name " + cacheable.value() + " content Type " + contentType.substring(0, contentType.indexOf(";")) + " returning extension " + extensions.get(contentType.substring(0, contentType.indexOf(";"))));
131 	                	   return extensions.get(contentType.substring(0, contentType.indexOf(";")));
132 	                   }
133 	               }
134 	           } else {
135 	        	   logger.debug("Handler method does not have cacheable annotation, returning null");
136 	               return null;
137 	           }
138 	       } else {
139 	    	   logger.debug("Could not find handler method for request, returning null");
140 	           return null;
141 	       }
142 	}
143 	
144 	/**
145 	 * Holder for request mapping metadata. Allows for finding a best matching candidate.
146 	 */
147 	static class RequestMappingInfo {
148 
149 		String[] paths = new String[0];
150 
151 		List<String> matchedPaths = Collections.emptyList();
152 
153 		RequestMethod[] methods = new RequestMethod[0];
154 
155 		String[] params = new String[0];
156 
157 		String[] headers = new String[0];
158 
159 		public String bestMatchedPath() {
160 			return (!this.matchedPaths.isEmpty() ? this.matchedPaths.get(0) : null);
161 		}
162 
163 		public boolean matches(HttpServletRequest request) {
164 			return ServletAnnotationMappingUtils.checkRequestMethod(this.methods, request) &&
165 					ServletAnnotationMappingUtils.checkParameters(this.params, request) &&
166 					ServletAnnotationMappingUtils.checkHeaders(this.headers, request);
167 		}
168 
169 		@Override
170 		public boolean equals(Object obj) {
171 			RequestMappingInfo other = (RequestMappingInfo) obj;
172 			return (Arrays.equals(this.paths, other.paths) && Arrays.equals(this.methods, other.methods) &&
173 					Arrays.equals(this.params, other.params) && Arrays.equals(this.headers, other.headers));
174 		}
175 
176 		@Override
177 		public int hashCode() {
178 			return (Arrays.hashCode(this.paths) * 23 + Arrays.hashCode(this.methods) * 29 +
179 					Arrays.hashCode(this.params) * 31 + Arrays.hashCode(this.headers));
180 		}
181 	}
182 	
183 	/**
184 	 * Comparator capable of sorting {@link RequestMappingInfo}s (RHIs) so that sorting a list with this comparator will
185 	 * result in:
186 	 * <ul>
187 	 * <li>RHIs with {@linkplain RequestMappingInfo#matchedPaths better matched paths} take prescedence
188 	 * over those with a weaker match (as expressed by the {@linkplain PathMatcher#getPatternComparator(String) path
189 	 * pattern comparator}.) Typically, this means that patterns without wild cards and uri templates will be ordered
190 	 * before those without.</li>
191 	 * <li>RHIs with one single {@linkplain RequestMappingInfo#methods request method} will be
192 	 * ordered before those without a method, or with more than one method.</li>
193 	 * <li>RHIs with more {@linkplain RequestMappingInfo#params request parameters} will be ordered before those with
194 	 * less parameters</li>
195 	 * </ol>
196 	 */
197 	static class RequestMappingInfoComparator implements Comparator<RequestMappingInfo> {
198 
199 		private final Comparator<String> pathComparator;
200 
201 		private final ServerHttpRequest request;
202 
203 		RequestMappingInfoComparator(Comparator<String> pathComparator, HttpServletRequest request) {
204 			this.pathComparator = pathComparator;
205 			this.request = new ServletServerHttpRequest(request);
206 		}
207 
208 		public int compare(RequestMappingInfo info1, RequestMappingInfo info2) {
209 			int pathComparison = pathComparator.compare(info1.bestMatchedPath(), info2.bestMatchedPath());
210 			if (pathComparison != 0) {
211 				return pathComparison;
212 			}
213 			int info1ParamCount = info1.params.length;
214 			int info2ParamCount = info2.params.length;
215 			if (info1ParamCount != info2ParamCount) {
216 				return info2ParamCount - info1ParamCount;
217 			}
218 			int info1HeaderCount = info1.headers.length;
219 			int info2HeaderCount = info2.headers.length;
220 			if (info1HeaderCount != info2HeaderCount) {
221 				return info2HeaderCount - info1HeaderCount;
222 			}
223 			int acceptComparison = compareAcceptHeaders(info1, info2);
224 			if (acceptComparison != 0) {
225 				return acceptComparison;
226 			}
227 			int info1MethodCount = info1.methods.length;
228 			int info2MethodCount = info2.methods.length;
229 			if (info1MethodCount == 0 && info2MethodCount > 0) {
230 				return 1;
231 			}
232 			else if (info2MethodCount == 0 && info1MethodCount > 0) {
233 				return -1;
234 			}
235 			else if (info1MethodCount == 1 & info2MethodCount > 1) {
236 				return -1;
237 			}
238 			else if (info2MethodCount == 1 & info1MethodCount > 1) {
239 				return 1;
240 			}
241 			return 0;
242 		}
243 
244 		private int compareAcceptHeaders(RequestMappingInfo info1, RequestMappingInfo info2) {
245 			List<MediaType> requestAccepts = request.getHeaders().getAccept();
246 			MediaType.sortBySpecificity(requestAccepts);
247 			
248 			List<MediaType> info1Accepts = getAcceptHeaderValue(info1);
249 			List<MediaType> info2Accepts = getAcceptHeaderValue(info2);
250 
251 			for (MediaType requestAccept : requestAccepts) {
252 				int pos1 = indexOfIncluded(info1Accepts, requestAccept);
253 				int pos2 = indexOfIncluded(info2Accepts, requestAccept);
254 				if (pos1 != pos2) {
255 					return pos2 - pos1;
256 				}
257 			}
258 			return 0;
259 		}
260 
261 		private int indexOfIncluded(List<MediaType> infoAccepts, MediaType requestAccept) {
262 			for (int i = 0; i < infoAccepts.size(); i++) {
263 				MediaType info1Accept = infoAccepts.get(i);
264 				if (requestAccept.includes(info1Accept)) {
265 					return i;
266 				}
267 			}
268 			return -1;
269 		}
270 
271 		private List<MediaType> getAcceptHeaderValue(RequestMappingInfo info) {
272 			for (String header : info.headers) {
273 				int separator = header.indexOf('=');
274 				if (separator != -1) {
275 					String key = header.substring(0, separator);
276 					String value = header.substring(separator + 1);
277 					if ("Accept".equalsIgnoreCase(key)) {
278 						return MediaType.parseMediaTypes(value);
279 					}
280 				}
281 			}
282 			return Collections.emptyList();
283 		}
284 	}
285 
286 	private class ServletHandlerMethodResolver extends HandlerMethodResolver {
287 
288 		private ServletHandlerMethodResolver(Class<?> handlerType) {
289 			init(handlerType);
290 		}
291 
292 		public Method resolveHandlerMethod(HttpServletRequest request) throws ServletException {
293 			String lookupPath = urlPathHelper.getLookupPathForRequest(request);
294 			Comparator<String> pathComparator = pathMatcher.getPatternComparator(lookupPath);
295 			Map<RequestMappingInfo, Method> targetHandlerMethods = new LinkedHashMap<RequestMappingInfo, Method>();
296 			Set<String> allowedMethods = new LinkedHashSet<String>(7);
297 			String resolvedMethodName = null;
298 			for (Method handlerMethod : getHandlerMethods()) {
299 				RequestMappingInfo mappingInfo = new RequestMappingInfo();
300 				RequestMapping mapping = AnnotationUtils.findAnnotation(handlerMethod, RequestMapping.class);
301 				mappingInfo.paths = mapping.value();
302 				if (!hasTypeLevelMapping() || !Arrays.equals(mapping.method(), getTypeLevelMapping().method())) {
303 					mappingInfo.methods = mapping.method();
304 				}
305 				if (!hasTypeLevelMapping() || !Arrays.equals(mapping.params(), getTypeLevelMapping().params())) {
306 					mappingInfo.params = mapping.params();
307 				}
308 				if (!hasTypeLevelMapping() || !Arrays.equals(mapping.headers(), getTypeLevelMapping().headers())) {
309 					mappingInfo.headers = mapping.headers();
310 				}
311 				boolean match = false;
312 				if (mappingInfo.paths.length > 0) {
313 					List<String> matchedPaths = new ArrayList<String>(mappingInfo.paths.length);
314 					for (String mappedPattern : mappingInfo.paths) {
315 						if (!hasTypeLevelMapping() && !mappedPattern.startsWith("/")) {
316 							mappedPattern = "/" + mappedPattern;
317 						}
318 						String matchedPattern = getMatchedPattern(mappedPattern, lookupPath, request);
319 						if (matchedPattern != null) {
320 							if (mappingInfo.matches(request)) {
321 								match = true;
322 								matchedPaths.add(matchedPattern);
323 							}
324 							else {
325 								for (RequestMethod requestMethod : mappingInfo.methods) {
326 									allowedMethods.add(requestMethod.toString());
327 								}
328 								break;
329 							}
330 						}
331 					}
332 					Collections.sort(matchedPaths, pathComparator);
333 					mappingInfo.matchedPaths = matchedPaths;
334 				}
335 				else {
336 					// No paths specified: parameter match sufficient.
337 					match = mappingInfo.matches(request);
338 					if (match && mappingInfo.methods.length == 0 && mappingInfo.params.length == 0 &&
339 							resolvedMethodName != null && !resolvedMethodName.equals(handlerMethod.getName())) {
340 						match = false;
341 					}
342 					else {
343 						for (RequestMethod requestMethod : mappingInfo.methods) {
344 							allowedMethods.add(requestMethod.toString());
345 						}
346 					}
347 				}
348 				if (match) {
349 					Method oldMappedMethod = targetHandlerMethods.put(mappingInfo, handlerMethod);
350 					if (oldMappedMethod != null && oldMappedMethod != handlerMethod) {
351 						if (methodNameResolver != null && mappingInfo.paths.length == 0) {
352 							if (!oldMappedMethod.getName().equals(handlerMethod.getName())) {
353 								if (resolvedMethodName == null) {
354 									resolvedMethodName = methodNameResolver.getHandlerMethodName(request);
355 								}
356 								if (!resolvedMethodName.equals(oldMappedMethod.getName())) {
357 									oldMappedMethod = null;
358 								}
359 								if (!resolvedMethodName.equals(handlerMethod.getName())) {
360 									if (oldMappedMethod != null) {
361 										targetHandlerMethods.put(mappingInfo, oldMappedMethod);
362 										oldMappedMethod = null;
363 									}
364 									else {
365 										targetHandlerMethods.remove(mappingInfo);
366 									}
367 								}
368 							}
369 						}
370 						if (oldMappedMethod != null) {
371 							throw new IllegalStateException(
372 									"Ambiguous handler methods mapped for HTTP path '" + lookupPath + "': {" +
373 											oldMappedMethod + ", " + handlerMethod +
374 											"}. If you intend to handle the same path in multiple methods, then factor " +
375 											"them out into a dedicated handler class with that path mapped at the type level!");
376 						}
377 					}
378 				}
379 			}
380 			if (!targetHandlerMethods.isEmpty()) {
381 				List<RequestMappingInfo> matches = new ArrayList<RequestMappingInfo>(targetHandlerMethods.keySet());
382 				RequestMappingInfoComparator requestMappingInfoComparator =
383 						new RequestMappingInfoComparator(pathComparator, request);
384 				Collections.sort(matches, requestMappingInfoComparator);
385 				RequestMappingInfo bestMappingMatch = matches.get(0);
386 				String bestMatchedPath = bestMappingMatch.bestMatchedPath();
387 				if (bestMatchedPath != null) {
388 					extractHandlerMethodUriTemplates(bestMatchedPath, lookupPath, request);
389 				}
390 				return targetHandlerMethods.get(bestMappingMatch);
391 			}
392 			else {
393 				if (!allowedMethods.isEmpty()) {
394 					throw new HttpRequestMethodNotSupportedException(request.getMethod(),
395 							StringUtils.toStringArray(allowedMethods));
396 				}
397 				else {
398 					throw new NoSuchRequestHandlingMethodException(lookupPath, request.getMethod(),
399 							request.getParameterMap());
400 				}
401 			}
402 		}
403 
404 		/**
405 		 * Determines the matched pattern for the given methodLevelPattern and path.
406 		 * <p>Uses the following algorithm: <ol> <li>If there is a type-level mapping with path information, it is {@linkplain
407 		 * PathMatcher#combine(String, String) combined} with the method-level pattern. <li>If there is a {@linkplain
408 		 * HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE best matching pattern} in the request, it is combined with the
409 		 * method-level pattern. <li>Otherwise,
410 		 */
411 		private String getMatchedPattern(String methodLevelPattern, String lookupPath, HttpServletRequest request) {
412 			if (hasTypeLevelMapping() && (!ObjectUtils.isEmpty(getTypeLevelMapping().value()))) {
413 				String[] typeLevelPatterns = getTypeLevelMapping().value();
414 				for (String typeLevelPattern : typeLevelPatterns) {
415 					if (!typeLevelPattern.startsWith("/")) {
416 						typeLevelPattern = "/" + typeLevelPattern;
417 					}
418 					String combinedPattern = pathMatcher.combine(typeLevelPattern, methodLevelPattern);
419 					if (isPathMatchInternal(combinedPattern, lookupPath)) {
420 						return combinedPattern;
421 					}
422 				}
423 				return null;
424 			}
425 			String bestMatchingPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
426 			if (StringUtils.hasText(bestMatchingPattern) && bestMatchingPattern.endsWith("*")) {
427 				String combinedPattern = pathMatcher.combine(bestMatchingPattern, methodLevelPattern);
428 				if (!combinedPattern.equals(bestMatchingPattern) &&
429 						(isPathMatchInternal(combinedPattern, lookupPath))) {
430 					return combinedPattern;
431 				}
432 			}
433 			if (isPathMatchInternal(methodLevelPattern, lookupPath)) {
434 				return methodLevelPattern;
435 			}
436 			return null;
437 		}
438 
439 		private boolean isPathMatchInternal(String pattern, String lookupPath) {
440 			if (pattern.equals(lookupPath) || pathMatcher.match(pattern, lookupPath)) {
441 				return true;
442 			}
443 			boolean hasSuffix = pattern.indexOf('.') != -1;
444 			if (!hasSuffix && pathMatcher.match(pattern + ".*", lookupPath)) {
445 				return true;
446 			}
447 			boolean endsWithSlash = pattern.endsWith("/");
448 			if (!endsWithSlash && pathMatcher.match(pattern + "/", lookupPath)) {
449 				return true;
450 			}
451 			return false;
452 		}
453 
454 		@SuppressWarnings("unchecked")
455 		private void extractHandlerMethodUriTemplates(String mappedPattern,
456 				String lookupPath,
457 				HttpServletRequest request) {
458 
459 			Map<String, String> variables =
460 					(Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
461 
462 			int patternVariableCount = StringUtils.countOccurrencesOf(mappedPattern, "{");
463 			
464 			if ( (variables == null || patternVariableCount != variables.size())  
465 					&& pathMatcher.match(mappedPattern, lookupPath)) {
466 				variables = pathMatcher.extractUriTemplateVariables(mappedPattern, lookupPath);
467 				request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, variables);
468 			}
469 		}
470 	}
471 }