1 /* Copyright 2004-2005 Graeme Rocher
2  *
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 package org.codehaus.groovy.grails.web.mapping;
16 
17 import java.io.PrintWriter;
18 import java.io.StringWriter;
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.SortedSet;
28 import java.util.TreeSet;
29 
30 import org.apache.commons.logging.Log;
31 import org.apache.commons.logging.LogFactory;
32 import org.codehaus.groovy.grails.commons.GrailsControllerClass;
33 import org.codehaus.groovy.grails.validation.ConstrainedProperty;
34 import org.springframework.core.style.ToStringCreator;
35 
36 import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
37 import com.googlecode.concurrentlinkedhashmap.Weigher;
38 
39 /**
40  * Default implementation of the UrlMappingsHolder interface that takes a list of mappings and
41  * then sorts them according to their precedence rules as defined in the implementation of Comparable.
42  *
43  * @see org.codehaus.groovy.grails.web.mapping.UrlMapping
44  * @see Comparable
45  *
46  * @author Graeme Rocher
47  * @since 0.4
48  */
49 @SuppressWarnings({"serial","rawtypes"})
50 public class DefaultUrlMappingsHolder implements UrlMappingsHolder {
51 
52     private static final transient Log LOG = LogFactory.getLog(DefaultUrlMappingsHolder.class);
53     private static final int DEFAULT_MAX_WEIGHTED_CAPACITY = 5000;
54 
55     private int maxWeightedCacheCapacity = DEFAULT_MAX_WEIGHTED_CAPACITY;
56     private Map<String, UrlMappingInfo> cachedMatches;
57     private Map<String, List<UrlMappingInfo>> cachedListMatches;
58     private enum CustomListWeigher implements Weigher<List<UrlMappingInfo>> {
59     	INSTANCE;
weightOf(List<UrlMappingInfo> values)60     	public int weightOf(List<UrlMappingInfo> values) {
61     		return values.size() + 1;
62     	}
63     }
64 
65     private List<UrlMapping> urlMappings = new ArrayList<UrlMapping>();
66     private UrlMapping[] mappings;
67     private List excludePatterns;
68     private Map<UrlMappingKey, UrlMapping> mappingsLookup = new HashMap<UrlMappingKey, UrlMapping>();
69     private Map<String, UrlMapping> namedMappings = new HashMap<String, UrlMapping>();
70     private UrlMappingsList mappingsListLookup = new UrlMappingsList();
71     private Set<String> DEFAULT_CONTROLLER_PARAMS = new HashSet<String>() {{
72         add(UrlMapping.CONTROLLER);
73         add(UrlMapping.ACTION);
74     }};
75     private Set<String> DEFAULT_ACTION_PARAMS = new HashSet<String>() {{
76         add(UrlMapping.ACTION);
77     }};
78     private UrlCreatorCache urlCreatorCache;
79     // capacity of the UrlCreatoreCache is the estimated number of char's stored in cached objects
80     private int urlCreatorMaxWeightedCacheCapacity = 160000;
81 
DefaultUrlMappingsHolder(List<UrlMapping> mappings)82     public DefaultUrlMappingsHolder(List<UrlMapping> mappings) {
83         this(mappings, null, false);
84     }
85 
DefaultUrlMappingsHolder(List<UrlMapping> mappings, List excludePatterns)86     public DefaultUrlMappingsHolder(List<UrlMapping> mappings, List excludePatterns) {
87     	this(mappings, excludePatterns, false);
88     }
89 
DefaultUrlMappingsHolder(List<UrlMapping> mappings, List excludePatterns, boolean doNotCallInit)90     public DefaultUrlMappingsHolder(List<UrlMapping> mappings, List excludePatterns, boolean doNotCallInit) {
91         urlMappings = mappings;
92         this.excludePatterns = excludePatterns;
93         if(!doNotCallInit) {
94         	initialize();
95         }
96     }
97 
initialize()98     public void initialize() {
99         sortMappings();
100 
101         cachedMatches = new ConcurrentLinkedHashMap.Builder<String, UrlMappingInfo>()
102             .maximumWeightedCapacity(maxWeightedCacheCapacity)
103             .build();
104         cachedListMatches = new ConcurrentLinkedHashMap.Builder<String, List<UrlMappingInfo>>()
105             .maximumWeightedCapacity(maxWeightedCacheCapacity)
106             .weigher(CustomListWeigher.INSTANCE)
107             .build();
108         if(urlCreatorMaxWeightedCacheCapacity > 0) {
109         	urlCreatorCache = new UrlCreatorCache(urlCreatorMaxWeightedCacheCapacity);
110         }
111 
112         mappings = urlMappings.toArray(new UrlMapping[urlMappings.size()]);
113 
114         for (UrlMapping mapping : mappings) {
115             String mappingName = mapping.getMappingName();
116             if (mappingName != null) {
117                 namedMappings.put(mappingName, mapping);
118             }
119             String controllerName = mapping.getControllerName() instanceof String ? mapping.getControllerName().toString() : null;
120             String actionName = mapping.getActionName() instanceof String ? mapping.getActionName().toString() : null;
121 
122             ConstrainedProperty[] params = mapping.getConstraints();
123             Set<String> requiredParams = new HashSet<String>();
124             int optionalIndex = -1;
125             for (int j = 0; j < params.length; j++) {
126                 ConstrainedProperty param = params[j];
127                 if (!param.isNullable()) {
128                     requiredParams.add(param.getPropertyName());
129                 }
130                 else {
131                     optionalIndex = j;
132                     break;
133                 }
134             }
135             UrlMappingKey key = new UrlMappingKey(controllerName, actionName, requiredParams);
136             mappingsLookup.put(key, mapping);
137 
138             UrlMappingsListKey listKey = new UrlMappingsListKey(controllerName, actionName);
139             mappingsListLookup.put(listKey, key);
140 
141             if (LOG.isDebugEnabled()) {
142                 LOG.debug("Reverse mapping: " + key + " -> " + mapping);
143             }
144             Set<String> requiredParamsAndOptionals = new HashSet<String>(requiredParams);
145             if (optionalIndex > -1) {
146                 for (int j = optionalIndex; j < params.length; j++) {
147                     ConstrainedProperty param = params[j];
148                     requiredParamsAndOptionals.add(param.getPropertyName());
149                     key = new UrlMappingKey(controllerName, actionName, new HashSet<String>(requiredParamsAndOptionals));
150                     mappingsLookup.put(key, mapping);
151 
152                     listKey = new UrlMappingsListKey(controllerName, actionName);
153                     mappingsListLookup.put(listKey, key);
154 
155                     if (LOG.isDebugEnabled()) {
156                         LOG.debug("Reverse mapping: " + key + " -> " + mapping);
157                     }
158                 }
159             }
160         }
161     }
162 
163 	@SuppressWarnings("unchecked")
sortMappings()164 	private void sortMappings() {
165 		List<ResponseCodeUrlMapping> responseCodeUrlMappings = new ArrayList<ResponseCodeUrlMapping>();
166 		Iterator<UrlMapping> iter = urlMappings.iterator();
167 		while (iter.hasNext()) {
168 			UrlMapping mapping = iter.next();
169 			if (mapping instanceof ResponseCodeUrlMapping) {
170 				responseCodeUrlMappings.add((ResponseCodeUrlMapping)mapping);
171 				iter.remove();
172 			}
173 		}
174 
175 		Collections.sort(urlMappings);
176 		urlMappings.addAll(responseCodeUrlMappings);
177 		Collections.reverse(urlMappings);
178 	}
179 
getUrlMappings()180 	public UrlMapping[] getUrlMappings() {
181         return mappings;
182     }
183 
getExcludePatterns()184     public List getExcludePatterns() {
185         return excludePatterns;
186     }
187 
188     /**
189      * @see UrlMappingsHolder#getReverseMapping(String, String, java.util.Map)
190      */
191     @SuppressWarnings("unchecked")
getReverseMapping(final String controller, final String action, Map params)192     public UrlCreator getReverseMapping(final String controller, final String action, Map params) {
193         if (params == null) params = Collections.EMPTY_MAP;
194 
195         if(urlCreatorCache != null) {
196 	        UrlCreatorCache.ReverseMappingKey key=urlCreatorCache.createKey(controller, action, params);
197 	        UrlCreator creator=urlCreatorCache.lookup(key);
198 	        if(creator==null) {
199 	        	creator=resolveUrlCreator(controller, action, params);
200 	        	creator=urlCreatorCache.putAndDecorate(key, creator);
201 	        }
202 	        // preserve previous side-effect, remove mappingName from params
203 	        params.remove("mappingName");
204 	        return creator;
205         } else {
206         	// cache is disabled
207         	return resolveUrlCreator(controller, action, params);
208         }
209     }
210 
resolveUrlCreator(final String controller, final String action, Map params)211 	private UrlCreator resolveUrlCreator(final String controller,
212 			final String action, Map params) {
213 		UrlMapping mapping = null;
214 
215         mapping = namedMappings.get(params.remove("mappingName"));
216         if (mapping == null) {
217             mapping = lookupMapping(controller, action, params);
218         }
219         if (mapping == null || (mapping instanceof ResponseCodeUrlMapping)) {
220             mapping = mappingsLookup.get(new UrlMappingKey(controller, action, Collections.EMPTY_SET));
221         }
222         if (mapping == null || (mapping instanceof ResponseCodeUrlMapping)) {
223             Set<String> lookupParams = new HashSet<String>(DEFAULT_ACTION_PARAMS);
224             Set<String> paramKeys = new HashSet<String>(params.keySet());
225             paramKeys.removeAll(lookupParams);
226             lookupParams.addAll(paramKeys);
227             mapping = mappingsLookup.get(new UrlMappingKey(controller, null, lookupParams));
228             if (mapping == null) {
229                 lookupParams.removeAll(paramKeys);
230                 mapping = mappingsLookup.get(new UrlMappingKey(controller, null, lookupParams));
231             }
232         }
233         if (mapping == null || (mapping instanceof ResponseCodeUrlMapping)) {
234             Set<String> lookupParams = new HashSet<String>(DEFAULT_CONTROLLER_PARAMS);
235             Set<String> paramKeys = new HashSet<String>(params.keySet());
236             paramKeys.removeAll(lookupParams);
237 
238             lookupParams.addAll(paramKeys);
239             mapping = mappingsLookup.get(new UrlMappingKey(null, null, lookupParams));
240             if (mapping == null) {
241                 lookupParams.removeAll(paramKeys);
242                 mapping = mappingsLookup.get(new UrlMappingKey(null, null, lookupParams));
243             }
244         }
245         UrlCreator creator;
246         if (mapping == null || (mapping instanceof ResponseCodeUrlMapping)) {
247         	creator=new DefaultUrlCreator(controller, action);
248         } else {
249         	creator=mapping;
250         }
251         return creator;
252 	}
253 
254     /**
255      * Performs a match uses reverse mappings to looks up a mapping from the
256      * controller, action and params. This is refactored to use a list of mappings
257      * identified by only controller and action and then matches the mapping to select
258      * the mapping that best matches the params (most possible matches).
259      *
260      * @param controller The controller name
261      * @param action The action name
262      * @param params The params
263      * @return A UrlMapping instance or null
264      */
265     @SuppressWarnings("unchecked")
lookupMapping(String controller, String action, Map params)266     protected UrlMapping lookupMapping(String controller, String action, Map params) {
267         final UrlMappingsListKey lookupKey = new UrlMappingsListKey(controller, action);
268         SortedSet mappingKeysSet = mappingsListLookup.get(lookupKey);
269 
270         final String actionName = lookupKey.action;
271         boolean secondAttempt = false;
272         final boolean isIndexAction = GrailsControllerClass.INDEX_ACTION.equals(actionName);
273         if (null == mappingKeysSet && actionName != null) {
274             lookupKey.action=null;
275             mappingKeysSet = mappingsListLookup.get(lookupKey);
276             secondAttempt = true;
277         }
278         if (null == mappingKeysSet) return null;
279 
280         UrlMappingKey[] mappingKeys = (UrlMappingKey[]) mappingKeysSet.toArray(new UrlMappingKey[mappingKeysSet.size()]);
281         for (int i = mappingKeys.length; i > 0; i--) {
282             UrlMappingKey mappingKey = mappingKeys[i - 1];
283             if (params.keySet().containsAll(mappingKey.paramNames)) {
284                 final UrlMapping mapping = mappingsLookup.get(mappingKey);
285                 if (canInferAction(actionName, secondAttempt, isIndexAction, mapping)) {
286                     return mapping;
287                 }
288                 if (!secondAttempt) {
289                     return mapping;
290                 }
291             }
292         }
293         return null;
294     }
295 
canInferAction(String actionName, boolean secondAttempt, boolean indexAction, UrlMapping mapping)296     private boolean canInferAction(String actionName, boolean secondAttempt, boolean indexAction, UrlMapping mapping) {
297         return secondAttempt && (indexAction || mapping.hasRuntimeVariable(GrailsControllerClass.ACTION) || (mapping.isRestfulMapping() && UrlMappingEvaluator.DEFAULT_REST_MAPPING.containsValue(actionName)));
298     }
299 
300     /**
301      * @see org.codehaus.groovy.grails.web.mapping.UrlMappingsHolder#match(String)
302      */
match(String uri)303     public UrlMappingInfo match(String uri) {
304         UrlMappingInfo info = null;
305         if (cachedMatches.containsKey(uri)) {
306             return cachedMatches.get(uri);
307         }
308 
309         for (UrlMapping mapping : mappings) {
310             if (LOG.isDebugEnabled()) {
311                 LOG.debug("Attempting to match URI [" + uri + "] with pattern [" + mapping.getUrlData().getUrlPattern() + "]");
312             }
313 
314             info = mapping.match(uri);
315             if (info != null) {
316                 cachedMatches.put(uri, info);
317                 break;
318             }
319         }
320 
321         return info;
322     }
323 
matchAll(String uri)324     public UrlMappingInfo[] matchAll(String uri) {
325         List<UrlMappingInfo> matchingUrls = new ArrayList<UrlMappingInfo>();
326         if (cachedListMatches.containsKey(uri)) {
327             matchingUrls = cachedListMatches.get(uri);
328         }
329         else {
330             for (UrlMapping mapping : mappings) {
331                 if (LOG.isDebugEnabled()) {
332                     LOG.debug("Attempting to match URI [" + uri + "] with pattern [" + mapping.getUrlData().getUrlPattern() + "]");
333                 }
334 
335                 UrlMappingInfo current = mapping.match(uri);
336                 if (current != null) {
337                     if (LOG.isDebugEnabled()) {
338                         LOG.debug("Matched URI [" + uri + "] with pattern [" + mapping.getUrlData().getUrlPattern() + "], adding to posibilities");
339                     }
340 
341                     matchingUrls.add(current);
342                 }
343             }
344             cachedListMatches.put(uri, matchingUrls);
345         }
346         return matchingUrls.toArray(new UrlMappingInfo[matchingUrls.size()]);
347     }
348 
matchAll(String uri, String httpMethod)349     public UrlMappingInfo[] matchAll(String uri, String httpMethod) {
350         return matchAll(uri);
351     }
352 
matchStatusCode(int responseCode)353     public UrlMappingInfo matchStatusCode(int responseCode) {
354         for (UrlMapping mapping : mappings) {
355             if (mapping instanceof ResponseCodeUrlMapping) {
356                 ResponseCodeUrlMapping responseCodeUrlMapping = (ResponseCodeUrlMapping) mapping;
357                 if (responseCodeUrlMapping.getExceptionType() != null) continue;
358                 final UrlMappingInfo current = responseCodeUrlMapping.match(responseCode);
359                 if (current != null) return current;
360             }
361         }
362 
363         return null;
364     }
365 
matchStatusCode(int responseCode, Throwable e)366     public UrlMappingInfo matchStatusCode(int responseCode, Throwable e) {
367         for (UrlMapping mapping : mappings) {
368             if (mapping instanceof ResponseCodeUrlMapping) {
369                 ResponseCodeUrlMapping responseCodeUrlMapping = (ResponseCodeUrlMapping) mapping;
370                 final UrlMappingInfo current = responseCodeUrlMapping.match(responseCode);
371                 if (current != null) {
372                     if (responseCodeUrlMapping.getExceptionType() != null &&
373                             responseCodeUrlMapping.getExceptionType().isInstance(e)) {
374                         return current;
375                     }
376                 }
377             }
378         }
379         return null;
380     }
381 
382     @Override
toString()383     public String toString() {
384         StringWriter sw = new StringWriter();
385         PrintWriter pw = new PrintWriter(sw);
386         pw.println("URL Mappings");
387         pw.println("------------");
388         for (UrlMapping mapping : mappings) {
389             pw.println(mapping);
390         }
391         pw.flush();
392         return sw.toString();
393     }
394 
395     /**
396      * A class used as a key to lookup a UrlMapping based on controller, action and parameter names
397      */
398     @SuppressWarnings("unchecked")
399     class UrlMappingKey implements Comparable {
400         String controller;
401         String action;
402         Set<String> paramNames = Collections.EMPTY_SET;
403 
UrlMappingKey(String controller, String action, Set<String> paramNames)404         public UrlMappingKey(String controller, String action, Set<String> paramNames) {
405             this.controller = controller;
406             this.action = action;
407             this.paramNames = paramNames;
408         }
409 
410         @Override
equals(Object o)411         public boolean equals(Object o) {
412             if (this == o) return true;
413             if (o == null || getClass() != o.getClass()) return false;
414 
415             UrlMappingKey that = (UrlMappingKey) o;
416 
417             if (action != null && !action.equals(that.action)) return false;
418             if (controller != null && !controller.equals(that.controller)) return false;
419             if (!paramNames.equals(that.paramNames)) return false;
420 
421             return true;
422         }
423 
424         @Override
hashCode()425         public int hashCode() {
426             int result;
427             result = (controller != null ? controller.hashCode() : 0);
428             result = 31 * result + (action != null ? action.hashCode() : 0);
429             result = 31 * result + paramNames.hashCode();
430             return result;
431         }
432 
433         @Override
toString()434         public String toString() {
435             return new ToStringCreator(this).append("controller", controller)
436                                             .append("action",action)
437                                             .append("params", paramNames)
438                                             .toString();
439         }
440 
compareTo(Object o)441         public int compareTo(Object o) {
442             final int BEFORE = -1;
443             final int EQUAL = 0;
444             final int AFTER = 1;
445 
446             //this optimization is usually worthwhile, and can always be added
447             if (this == o) return EQUAL;
448 
449             final UrlMappingKey other = (UrlMappingKey)o;
450 
451             if (paramNames.size() < other.paramNames.size()) return BEFORE;
452             if (paramNames.size() > other.paramNames.size()) return AFTER;
453 
454             int comparison = controller != null ? controller.compareTo(other.controller) : EQUAL;
455             if (comparison != EQUAL) return comparison;
456 
457             comparison = action != null ? action.compareTo(other.action) : EQUAL;
458             if (comparison != EQUAL) return comparison;
459 
460             return EQUAL;
461         }
462     }
463 
464     /**
465      * A class used as a key to lookup a all UrlMappings based on only controller and action.
466      */
467     class UrlMappingsListKey {
468         String controller;
469         String action;
470 
UrlMappingsListKey(String controller, String action)471         public UrlMappingsListKey(String controller, String action) {
472             this.controller = controller;
473             this.action = action;
474         }
475 
476         @Override
equals(Object o)477         public boolean equals(Object o) {
478             if (this == o) return true;
479             if (o == null || getClass() != o.getClass()) return false;
480 
481             UrlMappingsListKey that = (UrlMappingsListKey) o;
482 
483             if (action != null && !action.equals(that.action)) return false;
484             if (controller != null && !controller.equals(that.controller)) return false;
485 
486             return true;
487         }
488 
489         @Override
hashCode()490         public int hashCode() {
491             int result;
492             result = (controller != null ? controller.hashCode() : 0);
493             result = 31 * result + (action != null ? action.hashCode() : 0);
494             return result;
495         }
496 
497         @Override
toString()498         public String toString() {
499             return new ToStringCreator(this).append("controller", controller)
500                                             .append("action",action)
501                                             .toString();
502         }
503     }
504 
505     class UrlMappingsList {
506         // A map from a UrlMappingsListKey to a list of UrlMappingKeys
507         private Map<UrlMappingsListKey, SortedSet<UrlMappingKey>> lookup =
508             new HashMap<UrlMappingsListKey, SortedSet<UrlMappingKey>>();
509 
put(UrlMappingsListKey key, UrlMappingKey mapping)510         public void put(UrlMappingsListKey key, UrlMappingKey mapping) {
511             SortedSet<UrlMappingKey> mappingsList = lookup.get(key);
512             if (null == mappingsList) {
513                 mappingsList = new TreeSet<UrlMappingKey>();
514                 lookup.put(key, mappingsList);
515             }
516             mappingsList.add(mapping);
517         }
518 
get(UrlMappingsListKey key)519         public SortedSet<UrlMappingKey> get(UrlMappingsListKey key) {
520             return lookup.get(key);
521         }
522     }
523 
setMaxWeightedCacheCapacity(int maxWeightedCacheCapacity)524 	public void setMaxWeightedCacheCapacity(int maxWeightedCacheCapacity) {
525 		this.maxWeightedCacheCapacity = maxWeightedCacheCapacity;
526 	}
527 
setUrlCreatorMaxWeightedCacheCapacity( int urlCreatorMaxWeightedCacheCapacity)528 	public void setUrlCreatorMaxWeightedCacheCapacity(
529 			int urlCreatorMaxWeightedCacheCapacity) {
530 		this.urlCreatorMaxWeightedCacheCapacity = urlCreatorMaxWeightedCacheCapacity;
531 	}
532 }
533