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
78
79
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
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
185
186
187
188
189
190
191
192
193
194
195
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
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
406
407
408
409
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 }