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