org.codehaus.groovy.grails.web.mapping.DefaultUrlMappingsHolder.java Source code

Java tutorial

Introduction

Here is the source code for org.codehaus.groovy.grails.web.mapping.DefaultUrlMappingsHolder.java

Source

/*
 * Copyright 2004-2005 Graeme Rocher
 *
 * 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 org.codehaus.groovy.grails.web.mapping;

import grails.util.CollectionUtils;
import grails.util.Holders;
import groovy.lang.Closure;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.groovy.grails.commons.GrailsControllerClass;
import org.codehaus.groovy.grails.validation.ConstrainedProperty;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.HttpMethod;
import org.springframework.web.context.WebApplicationContext;

import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.googlecode.concurrentlinkedhashmap.Weigher;

/**
 * Default implementation of the UrlMappingsHolder interface that takes a list of mappings and
 * then sorts them according to their precedence rules as defined in the implementation of Comparable.
 *
 * @see org.codehaus.groovy.grails.web.mapping.UrlMapping
 * @see Comparable
 *
 * @author Graeme Rocher
 * @since 0.4
 */
@SuppressWarnings("rawtypes")
public class DefaultUrlMappingsHolder implements UrlMappings {

    private static final transient Log LOG = LogFactory.getLog(DefaultUrlMappingsHolder.class);
    private static final int DEFAULT_MAX_WEIGHTED_CAPACITY = 5000;

    private int maxWeightedCacheCapacity = DEFAULT_MAX_WEIGHTED_CAPACITY;
    private Map<String, UrlMappingInfo> cachedMatches;
    private Map<UriToUrlMappingKey, List<UrlMappingInfo>> cachedListMatches;

    private enum CustomListWeigher implements Weigher<List<UrlMappingInfo>> {
        INSTANCE;
        public int weightOf(List<UrlMappingInfo> values) {
            return values.size() + 1;
        }
    }

    private List<UrlMapping> urlMappings = new ArrayList<UrlMapping>();
    private UrlMapping[] mappings;
    private List excludePatterns;
    private Map<UrlMappingKey, UrlMapping> mappingsLookup = new HashMap<UrlMappingKey, UrlMapping>();
    private Map<String, UrlMapping> namedMappings = new HashMap<String, UrlMapping>();
    private UrlMappingsList mappingsListLookup = new UrlMappingsList();
    private Set<String> DEFAULT_CONTROLLER_PARAMS = CollectionUtils.newSet(UrlMapping.CONTROLLER,
            UrlMapping.ACTION);
    private Set<String> DEFAULT_ACTION_PARAMS = CollectionUtils.newSet(UrlMapping.ACTION);
    private UrlCreatorCache urlCreatorCache;
    // capacity of the UrlCreatoreCache is the estimated number of char's stored in cached objects
    private int urlCreatorMaxWeightedCacheCapacity = 160000;

    public DefaultUrlMappingsHolder(List<UrlMapping> mappings) {
        this(mappings, null, false);
    }

    public DefaultUrlMappingsHolder(List<UrlMapping> mappings, List excludePatterns) {
        this(mappings, excludePatterns, false);
    }

    public DefaultUrlMappingsHolder(List<UrlMapping> mappings, List excludePatterns, boolean doNotCallInit) {
        urlMappings = mappings;
        this.excludePatterns = excludePatterns;
        if (!doNotCallInit) {
            initialize();
        }
    }

    @Override
    public Collection<UrlMapping> addMappings(Closure mappings) {
        WebApplicationContext applicationContext = (WebApplicationContext) Holders.findApplicationContext();

        UrlMappingEvaluator evaluator = new DefaultUrlMappingEvaluator(applicationContext);

        List<UrlMapping> newMappings = evaluator.evaluateMappings(mappings);
        this.urlMappings.addAll(newMappings);
        initialize();
        return newMappings;
    }

    public void initialize() {
        sortMappings();

        cachedMatches = new ConcurrentLinkedHashMap.Builder<String, UrlMappingInfo>()
                .maximumWeightedCapacity(maxWeightedCacheCapacity).build();
        cachedListMatches = new ConcurrentLinkedHashMap.Builder<UriToUrlMappingKey, List<UrlMappingInfo>>()
                .maximumWeightedCapacity(maxWeightedCacheCapacity).weigher(CustomListWeigher.INSTANCE).build();
        if (urlCreatorMaxWeightedCacheCapacity > 0) {
            urlCreatorCache = new UrlCreatorCache(urlCreatorMaxWeightedCacheCapacity);
        }

        mappings = urlMappings.toArray(new UrlMapping[urlMappings.size()]);

        for (UrlMapping mapping : mappings) {
            String mappingName = mapping.getMappingName();
            if (mappingName != null) {
                namedMappings.put(mappingName, mapping);
            }
            String controllerName = mapping.getControllerName() instanceof String
                    ? mapping.getControllerName().toString()
                    : null;
            String actionName = mapping.getActionName() instanceof String ? mapping.getActionName().toString()
                    : null;
            String pluginName = mapping.getPluginName() instanceof String ? mapping.getPluginName().toString()
                    : null;
            String httpMethod = mapping.getHttpMethod();
            String version = mapping.getVersion();
            String namespace = mapping.getNamespace() instanceof String ? mapping.getNamespace().toString() : null;

            ConstrainedProperty[] params = mapping.getConstraints();
            Set<String> requiredParams = new HashSet<String>();
            int optionalIndex = -1;
            for (int j = 0; j < params.length; j++) {
                ConstrainedProperty param = params[j];
                if (!param.isNullable()) {
                    requiredParams.add(param.getPropertyName());
                } else {
                    optionalIndex = j;
                    break;
                }
            }
            UrlMappingKey key = new UrlMappingKey(controllerName, actionName, namespace, pluginName, httpMethod,
                    version, requiredParams);
            mappingsLookup.put(key, mapping);

            UrlMappingsListKey listKey = new UrlMappingsListKey(controllerName, actionName, namespace, pluginName,
                    httpMethod, version);
            mappingsListLookup.put(listKey, key);

            if (LOG.isDebugEnabled()) {
                LOG.debug("Reverse mapping: " + key + " -> " + mapping);
            }
            Set<String> requiredParamsAndOptionals = new HashSet<String>(requiredParams);
            if (optionalIndex > -1) {
                for (int j = optionalIndex; j < params.length; j++) {
                    ConstrainedProperty param = params[j];
                    requiredParamsAndOptionals.add(param.getPropertyName());
                    key = new UrlMappingKey(controllerName, actionName, namespace, pluginName, httpMethod, version,
                            new HashSet<String>(requiredParamsAndOptionals));
                    mappingsLookup.put(key, mapping);

                    listKey = new UrlMappingsListKey(controllerName, actionName, namespace, pluginName, httpMethod,
                            version);
                    mappingsListLookup.put(listKey, key);

                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Reverse mapping: " + key + " -> " + mapping);
                    }
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void sortMappings() {
        List<ResponseCodeUrlMapping> responseCodeUrlMappings = new ArrayList<ResponseCodeUrlMapping>();
        Iterator<UrlMapping> iter = urlMappings.iterator();
        while (iter.hasNext()) {
            UrlMapping mapping = iter.next();
            if (mapping instanceof ResponseCodeUrlMapping) {
                responseCodeUrlMappings.add((ResponseCodeUrlMapping) mapping);
                iter.remove();
            }
        }

        Collections.sort(urlMappings);
        urlMappings.addAll(responseCodeUrlMappings);
        Collections.reverse(urlMappings);
    }

    public UrlMapping[] getUrlMappings() {
        return mappings;
    }

    public List getExcludePatterns() {
        return excludePatterns;
    }

    public UrlCreator getReverseMapping(final String controller, final String action, Map params) {
        return getReverseMapping(controller, action, null, null, params);
    }

    @Override
    public UrlCreator getReverseMapping(String controller, String action, String pluginName, Map params) {
        return getReverseMapping(controller, action, null, pluginName, null, params);
    }

    @Override
    public UrlCreator getReverseMapping(String controller, String action, String namespace, String pluginName,
            String httpMethod, Map params) {
        return getReverseMapping(controller, action, namespace, pluginName, httpMethod, UrlMapping.ANY_VERSION,
                params);
    }

    @Override
    public UrlCreator getReverseMapping(String controller, String action, String namespace, String pluginName,
            String httpMethod, String version, Map params) {
        if (params == null)
            params = Collections.EMPTY_MAP;

        if (urlCreatorCache != null) {
            UrlCreatorCache.ReverseMappingKey key = urlCreatorCache.createKey(controller, action, namespace,
                    pluginName, httpMethod, params);
            UrlCreator creator = urlCreatorCache.lookup(key);
            if (creator == null) {
                creator = resolveUrlCreator(controller, action, namespace, pluginName, httpMethod, version, params,
                        true);
                creator = urlCreatorCache.putAndDecorate(key, creator);
            }
            // preserve previous side-effect, remove mappingName from params
            params.remove("mappingName");
            return creator;
        }
        // cache is disabled
        return resolveUrlCreator(controller, action, namespace, pluginName, httpMethod, version, params, true);
    }

    /**
     * @see UrlMappingsHolder#getReverseMapping(String, String, java.util.Map)
     */
    public UrlCreator getReverseMapping(final String controller, final String action, final String namespace,
            final String pluginName, Map params) {
        return getReverseMapping(controller, action, namespace, pluginName, null, params);
    }

    public UrlCreator getReverseMappingNoDefault(String controller, String action, Map params) {
        return getReverseMappingNoDefault(controller, action, null, null, null, params);
    }

    @Override
    public UrlCreator getReverseMappingNoDefault(String controller, String action, String namespace,
            String pluginName, String httpMethod, Map params) {
        return getReverseMappingNoDefault(controller, action, namespace, pluginName, httpMethod,
                UrlMapping.ANY_VERSION, params);
    }

    @Override
    public UrlCreator getReverseMappingNoDefault(String controller, String action, String namespace,
            String pluginName, String httpMethod, String version, Map params) {
        if (params == null)
            params = Collections.EMPTY_MAP;

        if (urlCreatorCache != null) {
            UrlCreatorCache.ReverseMappingKey key = urlCreatorCache.createKey(controller, action, namespace,
                    pluginName, httpMethod, params);
            UrlCreator creator = urlCreatorCache.lookup(key);
            if (creator == null) {
                creator = resolveUrlCreator(controller, action, namespace, pluginName, httpMethod, version, params,
                        false);
                if (creator != null) {
                    creator = urlCreatorCache.putAndDecorate(key, creator);
                }
            }
            // preserve previous side-effect, remove mappingName from params
            params.remove("mappingName");
            return creator;
        }
        // cache is disabled
        return resolveUrlCreator(controller, action, namespace, pluginName, httpMethod, version, params, true);
    }

    @SuppressWarnings("unchecked")
    private UrlCreator resolveUrlCreator(final String controller, final String action, final String namespace,
            final String pluginName, String httpMethod, String version, Map params, boolean useDefault) {
        UrlMapping mapping = null;

        if (httpMethod == null) {
            httpMethod = UrlMapping.ANY_HTTP_METHOD;
        }
        mapping = namedMappings.get(params.remove("mappingName"));
        if (mapping == null) {
            mapping = lookupMapping(controller, action, namespace, pluginName, httpMethod, version, params);
            if (mapping == null) {
                lookupMapping(controller, action, namespace, pluginName, UrlMapping.ANY_HTTP_METHOD, version,
                        params);
            }
        }
        if (mapping == null || (mapping instanceof ResponseCodeUrlMapping)) {
            UrlMappingKey lookupKey = new UrlMappingKey(controller, action, namespace, pluginName, httpMethod,
                    version, Collections.EMPTY_SET);
            mapping = mappingsLookup.get(lookupKey);
            if (mapping == null) {
                lookupKey.httpMethod = UrlMapping.ANY_HTTP_METHOD;
                mapping = mappingsLookup.get(lookupKey);
            }
        }
        if (mapping == null || (mapping instanceof ResponseCodeUrlMapping)) {
            Set<String> lookupParams = new HashSet<String>(DEFAULT_ACTION_PARAMS);
            Set<String> paramKeys = new HashSet<String>(params.keySet());
            paramKeys.removeAll(lookupParams);
            lookupParams.addAll(paramKeys);
            UrlMappingKey lookupKey = new UrlMappingKey(controller, null, namespace, pluginName, httpMethod,
                    version, lookupParams);
            mapping = mappingsLookup.get(lookupKey);
            if (mapping == null) {
                lookupKey.httpMethod = UrlMapping.ANY_HTTP_METHOD;
                mapping = mappingsLookup.get(lookupKey);
            }
            if (mapping == null) {
                lookupParams.removeAll(paramKeys);
                UrlMappingKey lookupKeyModifiedParams = new UrlMappingKey(controller, null, namespace, pluginName,
                        httpMethod, version, lookupParams);
                mapping = mappingsLookup.get(lookupKeyModifiedParams);
                if (mapping == null) {
                    lookupKeyModifiedParams.httpMethod = UrlMapping.ANY_HTTP_METHOD;
                    mapping = mappingsLookup.get(lookupKeyModifiedParams);
                }
            }
        }
        if (mapping == null || (mapping instanceof ResponseCodeUrlMapping)) {
            Set<String> lookupParams = new HashSet<String>(DEFAULT_CONTROLLER_PARAMS);
            Set<String> paramKeys = new HashSet<String>(params.keySet());
            paramKeys.removeAll(lookupParams);

            lookupParams.addAll(paramKeys);
            UrlMappingKey lookupKey = new UrlMappingKey(null, null, namespace, pluginName, httpMethod, version,
                    lookupParams);
            mapping = mappingsLookup.get(lookupKey);
            if (mapping == null) {
                lookupKey.httpMethod = UrlMapping.ANY_HTTP_METHOD;
                mapping = mappingsLookup.get(lookupKey);
            }
            if (mapping == null) {
                lookupParams.removeAll(paramKeys);
                UrlMappingKey lookupKeyModifiedParams = new UrlMappingKey(null, null, namespace, pluginName,
                        httpMethod, version, lookupParams);
                mapping = mappingsLookup.get(lookupKeyModifiedParams);
                if (mapping == null) {
                    lookupKeyModifiedParams.httpMethod = UrlMapping.ANY_HTTP_METHOD;
                    mapping = mappingsLookup.get(lookupKeyModifiedParams);
                }
            }
        }
        UrlCreator creator = null;
        if (mapping == null || (mapping instanceof ResponseCodeUrlMapping)) {
            if (useDefault) {
                creator = new DefaultUrlCreator(controller, action);
            }
        } else {
            creator = mapping;
        }
        return creator;
    }

    /**
     * Performs a match uses reverse mappings to looks up a mapping from the
     * controller, action and params. This is refactored to use a list of mappings
     * identified by only controller and action and then matches the mapping to select
     * the mapping that best matches the params (most possible matches).
     *
     *
     * @param controller The controller name
     * @param action The action name
     * @param httpMethod The HTTP method
     * @param version
     * @param params The params  @return A UrlMapping instance or null
     */
    @SuppressWarnings("unchecked")
    protected UrlMapping lookupMapping(String controller, String action, String namespace, String pluginName,
            String httpMethod, String version, Map params) {
        final UrlMappingsListKey lookupKey = new UrlMappingsListKey(controller, action, namespace, pluginName,
                httpMethod, version);
        SortedSet mappingKeysSet = mappingsListLookup.get(lookupKey);

        final String actionName = lookupKey.action;
        boolean secondAttempt = false;
        final boolean isIndexAction = GrailsControllerClass.INDEX_ACTION.equals(actionName);
        if (null == mappingKeysSet) {
            lookupKey.httpMethod = UrlMapping.ANY_HTTP_METHOD;
            mappingKeysSet = mappingsListLookup.get(lookupKey);
        }
        if (null == mappingKeysSet && actionName != null) {
            lookupKey.action = null;

            mappingKeysSet = mappingsListLookup.get(lookupKey);
            secondAttempt = true;
        }
        if (null == mappingKeysSet)
            return null;

        Set<String> lookupParams = new HashSet<String>(params.keySet());
        if (secondAttempt) {
            lookupParams.removeAll(DEFAULT_ACTION_PARAMS);
            lookupParams.addAll(DEFAULT_ACTION_PARAMS);
        }

        UrlMappingKey[] mappingKeys = (UrlMappingKey[]) mappingKeysSet
                .toArray(new UrlMappingKey[mappingKeysSet.size()]);
        for (int i = mappingKeys.length; i > 0; i--) {
            UrlMappingKey mappingKey = mappingKeys[i - 1];
            if (lookupParams.containsAll(mappingKey.paramNames)) {
                final UrlMapping mapping = mappingsLookup.get(mappingKey);
                if (canInferAction(actionName, secondAttempt, isIndexAction, mapping)) {
                    return mapping;
                }
                if (!secondAttempt) {
                    return mapping;
                }
            }
        }
        return null;
    }

    private boolean canInferAction(String actionName, boolean secondAttempt, boolean indexAction,
            UrlMapping mapping) {
        return secondAttempt && (indexAction || mapping.hasRuntimeVariable(GrailsControllerClass.ACTION));
    }

    /**
     * @see org.codehaus.groovy.grails.web.mapping.UrlMappingsHolder#match(String)
     */
    public UrlMappingInfo match(String uri) {
        UrlMappingInfo info = null;
        if (cachedMatches.containsKey(uri)) {
            return cachedMatches.get(uri);
        }

        for (UrlMapping mapping : mappings) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Attempting to match URI [" + uri + "] with pattern ["
                        + mapping.getUrlData().getUrlPattern() + "]");
            }

            info = mapping.match(uri);
            if (info != null) {
                cachedMatches.put(uri, info);
                break;
            }
        }

        return info;
    }

    public UrlMappingInfo[] matchAll(String uri) {
        return matchAll(uri, (String) null);
    }

    public UrlMappingInfo[] matchAll(String uri, String httpMethod) {
        boolean anyHttpMethod = httpMethod != null && httpMethod.equalsIgnoreCase(UrlMapping.ANY_HTTP_METHOD);
        List<UrlMappingInfo> matchingUrls = new ArrayList<UrlMappingInfo>();
        UriToUrlMappingKey cacheKey = new UriToUrlMappingKey(uri, httpMethod, UrlMapping.ANY_VERSION);
        if (cachedListMatches.containsKey(cacheKey)) {
            matchingUrls = cachedListMatches.get(cacheKey);
        } else {
            for (UrlMapping mapping : mappings) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Attempting to match URI [" + uri + "] with pattern ["
                            + mapping.getUrlData().getUrlPattern() + "]");
                }

                UrlMappingInfo current = mapping.match(uri);
                if (current != null) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Matched URI [" + uri + "] with pattern [" + mapping.getUrlData().getUrlPattern()
                                + "], adding to posibilities");
                    }

                    String mappingHttpMethod = current.getHttpMethod();
                    if (mappingHttpMethod == null || anyHttpMethod
                            || mappingHttpMethod.equalsIgnoreCase(UrlMapping.ANY_HTTP_METHOD)
                            || mappingHttpMethod.equalsIgnoreCase(httpMethod))
                        matchingUrls.add(current);
                }
            }
            cachedListMatches.put(cacheKey, matchingUrls);
        }
        return matchingUrls.toArray(new UrlMappingInfo[matchingUrls.size()]);
    }

    public UrlMappingInfo[] matchAll(String uri, String httpMethod, String version) {
        boolean anyHttpMethod = httpMethod != null && httpMethod.equals(UrlMapping.ANY_HTTP_METHOD);
        boolean anyVersion = version != null && version.equals(UrlMapping.ANY_VERSION);

        List<UrlMappingInfo> matchingUrls = new ArrayList<UrlMappingInfo>();
        UriToUrlMappingKey cacheKey = new UriToUrlMappingKey(uri, httpMethod, version);
        if (cachedListMatches.containsKey(cacheKey)) {
            matchingUrls = cachedListMatches.get(cacheKey);
        } else {
            for (UrlMapping mapping : mappings) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Attempting to match URI [" + uri + "] with pattern ["
                            + mapping.getUrlData().getUrlPattern() + "]");
                }

                UrlMappingInfo current = mapping.match(uri);
                if (current != null) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Matched URI [" + uri + "] with pattern [" + mapping.getUrlData().getUrlPattern()
                                + "], adding to posibilities");
                    }

                    String mappingHttpMethod = current.getHttpMethod();
                    String mappingVersion = current.getVersion();
                    boolean isValidHttpMethod = mappingHttpMethod == null || anyHttpMethod
                            || mappingHttpMethod.equalsIgnoreCase(UrlMapping.ANY_HTTP_METHOD)
                            || mappingHttpMethod.equalsIgnoreCase(httpMethod);
                    boolean isValidVersion = mappingVersion == null || anyVersion
                            || mappingVersion.equals(UrlMapping.ANY_VERSION) || mappingVersion.equals(version);
                    if (isValidHttpMethod && isValidVersion) {
                        matchingUrls.add(current);
                    }
                }
            }
            cachedListMatches.put(cacheKey, matchingUrls);
        }
        return matchingUrls.toArray(new UrlMappingInfo[matchingUrls.size()]);
    }

    @Override
    public UrlMappingInfo[] matchAll(String uri, HttpMethod httpMethod) {
        return matchAll(uri, httpMethod.toString(), UrlMapping.ANY_VERSION);
    }

    @Override
    public UrlMappingInfo[] matchAll(String uri, HttpMethod httpMethod, String version) {
        return matchAll(uri, httpMethod.toString(), version);
    }

    public UrlMappingInfo matchStatusCode(int responseCode) {
        for (UrlMapping mapping : mappings) {
            if (mapping instanceof ResponseCodeUrlMapping) {
                ResponseCodeUrlMapping responseCodeUrlMapping = (ResponseCodeUrlMapping) mapping;
                if (responseCodeUrlMapping.getExceptionType() != null)
                    continue;
                final UrlMappingInfo current = responseCodeUrlMapping.match(responseCode);
                if (current != null)
                    return current;
            }
        }

        return null;
    }

    @Override
    public Set<HttpMethod> allowedMethods(String uri) {
        UrlMappingInfo[] urlMappingInfos = matchAll(uri, UrlMapping.ANY_HTTP_METHOD);
        Set<HttpMethod> methods = new HashSet<HttpMethod>();

        for (UrlMappingInfo urlMappingInfo : urlMappingInfos) {
            if (urlMappingInfo.getHttpMethod() == null
                    || urlMappingInfo.getHttpMethod().equals(UrlMapping.ANY_HTTP_METHOD)) {
                methods.addAll(Arrays.asList(HttpMethod.values()));
                break;
            } else {
                HttpMethod method = HttpMethod.valueOf(urlMappingInfo.getHttpMethod().toUpperCase());
                methods.add(method);
            }
        }

        return Collections.unmodifiableSet(methods);
    }

    public UrlMappingInfo matchStatusCode(int responseCode, Throwable e) {
        for (UrlMapping mapping : mappings) {
            if (mapping instanceof ResponseCodeUrlMapping) {
                ResponseCodeUrlMapping responseCodeUrlMapping = (ResponseCodeUrlMapping) mapping;
                final UrlMappingInfo current = responseCodeUrlMapping.match(responseCode);
                if (current != null) {
                    if (responseCodeUrlMapping.getExceptionType() != null
                            && responseCodeUrlMapping.getExceptionType().isInstance(e)) {
                        return current;
                    }
                }
            }
        }
        return null;
    }

    @Override
    public String toString() {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        pw.println("URL Mappings");
        pw.println("------------");
        for (UrlMapping mapping : mappings) {
            pw.println(mapping);
        }
        pw.flush();
        return sw.toString();
    }

    class UriToUrlMappingKey {
        String uri;
        String httpMethod;
        String version;

        UriToUrlMappingKey(String uri, String httpMethod, String version) {
            this.uri = uri;
            this.httpMethod = httpMethod;
            this.version = version;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            UriToUrlMappingKey that = (UriToUrlMappingKey) o;

            if (httpMethod != null ? !httpMethod.equals(that.httpMethod) : that.httpMethod != null) {
                return false;
            }
            if (version != null ? !version.equals(that.version) : that.version != null) {
                return false;
            }
            if (uri != null ? !uri.equals(that.uri) : that.uri != null) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = uri != null ? uri.hashCode() : 0;
            result = 31 * result + (httpMethod != null ? httpMethod.hashCode() : 0);
            result = 31 * result + (version != null ? version.hashCode() : 0);
            return result;
        }
    }

    /**
     * A class used as a key to lookup a UrlMapping based on controller, action and parameter names
     */
    @SuppressWarnings("unchecked")
    class UrlMappingKey implements Comparable {
        String controller;
        String action;
        String namespace;
        String pluginName;
        String httpMethod;
        String version;
        Set<String> paramNames = Collections.EMPTY_SET;

        public UrlMappingKey(String controller, String action, String namespace, String pluginName,
                String httpMethod, String version, Set<String> paramNames) {
            this.controller = controller;
            this.action = action;
            this.namespace = namespace;
            this.pluginName = pluginName;
            if (httpMethod != null) {
                this.httpMethod = httpMethod;
            }
            if (version != null) {
                this.version = version;
            }
            this.paramNames = paramNames;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            UrlMappingKey that = (UrlMappingKey) o;

            if (action != null && !action.equals(that.action))
                return false;
            if (controller != null && !controller.equals(that.controller))
                return false;
            if (namespace != null && !namespace.equals(that.namespace))
                return false;
            if (pluginName != null && !pluginName.equals(that.pluginName))
                return false;
            if (httpMethod != null && !httpMethod.equals(that.httpMethod))
                return false;
            if (version != null && !version.equals(that.version))
                return false;
            if (!paramNames.equals(that.paramNames))
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result;
            result = (controller != null ? controller.hashCode() : 0);
            result = 31 * result + (action != null ? action.hashCode() : 0);
            result = 31 * result + (namespace != null ? namespace.hashCode() : 0);
            result = 31 * result + (pluginName != null ? pluginName.hashCode() : 0);
            result = 31 * result + (httpMethod != null ? httpMethod.hashCode() : 0);
            result = 31 * result + (version != null ? version.hashCode() : 0);
            result = 31 * result + paramNames.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return new ToStringCreator(this).append(UrlMapping.CONTROLLER, controller)
                    .append(UrlMapping.ACTION, action).append(UrlMapping.NAMESPACE, namespace)
                    .append(UrlMapping.PLUGIN, pluginName).append("httpMethod", httpMethod)
                    .append("params", paramNames).toString();
        }

        public int compareTo(Object o) {
            final int BEFORE = -1;
            final int EQUAL = 0;
            final int AFTER = 1;

            //this optimization is usually worthwhile, and can always be added
            if (this == o)
                return EQUAL;

            final UrlMappingKey other = (UrlMappingKey) o;

            if (paramNames.size() < other.paramNames.size())
                return BEFORE;
            if (paramNames.size() > other.paramNames.size())
                return AFTER;

            int comparison = controller != null ? controller.compareTo(other.controller) : EQUAL;
            if (comparison != EQUAL)
                return comparison;

            comparison = action != null ? action.compareTo(other.action) : EQUAL;
            if (comparison != EQUAL)
                return comparison;

            comparison = namespace != null ? namespace.compareTo(other.namespace) : EQUAL;
            if (comparison != EQUAL)
                return comparison;

            comparison = pluginName != null ? pluginName.compareTo(other.pluginName) : EQUAL;
            if (comparison != EQUAL)
                return comparison;

            comparison = httpMethod != null ? httpMethod.compareTo(other.httpMethod) : EQUAL;
            if (comparison != EQUAL)
                return comparison;

            return EQUAL;
        }
    }

    /**
     * A class used as a key to lookup a all UrlMappings based on only controller and action.
     */
    class UrlMappingsListKey {
        String controller;
        String action;
        String namespace;
        String pluginName;
        String httpMethod;
        String version;

        public UrlMappingsListKey(String controller, String action, String namespace, String pluginName,
                String httpMethod, String version) {
            this.controller = controller;
            this.action = action;
            this.namespace = namespace;
            this.pluginName = pluginName;
            if (httpMethod != null) {
                this.httpMethod = httpMethod;
            }
            if (version != null) {
                this.version = version;
            }
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            UrlMappingsListKey that = (UrlMappingsListKey) o;

            if (action != null && !action.equals(that.action))
                return false;
            if (controller != null && !controller.equals(that.controller))
                return false;
            if (namespace != null && !namespace.equals(that.namespace))
                return false;
            if (pluginName != null && !pluginName.equals(that.pluginName))
                return false;
            if (httpMethod != null && !httpMethod.equals(that.httpMethod))
                return false;
            if (version != null && !version.equals(that.version))
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result;
            result = (controller != null ? controller.hashCode() : 0);
            result = 31 * result + (action != null ? action.hashCode() : 0);
            result = 31 * result + (namespace != null ? namespace.hashCode() : 0);
            result = 31 * result + (pluginName != null ? pluginName.hashCode() : 0);
            result = 31 * result + (httpMethod != null ? httpMethod.hashCode() : 0);
            result = 31 * result + (version != null ? version.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return new ToStringCreator(this).append(UrlMapping.CONTROLLER, controller)
                    .append(UrlMapping.ACTION, action).append(UrlMapping.NAMESPACE, namespace)
                    .append(UrlMapping.PLUGIN, pluginName).append(UrlMapping.HTTP_METHOD, httpMethod)
                    .append(UrlMapping.VERSION, version).toString();
        }
    }

    class UrlMappingsList {
        // A map from a UrlMappingsListKey to a list of UrlMappingKeys
        private Map<UrlMappingsListKey, SortedSet<UrlMappingKey>> lookup = new HashMap<UrlMappingsListKey, SortedSet<UrlMappingKey>>();

        public void put(UrlMappingsListKey key, UrlMappingKey mapping) {
            SortedSet<UrlMappingKey> mappingsList = lookup.get(key);
            if (null == mappingsList) {
                mappingsList = new TreeSet<UrlMappingKey>();
                lookup.put(key, mappingsList);
            }
            mappingsList.add(mapping);
        }

        public SortedSet<UrlMappingKey> get(UrlMappingsListKey key) {
            return lookup.get(key);
        }
    }

    public void setMaxWeightedCacheCapacity(int maxWeightedCacheCapacity) {
        this.maxWeightedCacheCapacity = maxWeightedCacheCapacity;
    }

    public void setUrlCreatorMaxWeightedCacheCapacity(int urlCreatorMaxWeightedCacheCapacity) {
        this.urlCreatorMaxWeightedCacheCapacity = urlCreatorMaxWeightedCacheCapacity;
    }
}