/* Copyright 2007 Ben Gunter * * 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 net.sourceforge.stripes.controller; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.Map.Entry; import javax.servlet.http.HttpServletRequest; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.exception.UrlBindingConflictException; import net.sourceforge.stripes.util.HttpUtil; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.bean.ParseException; /** * <p> * Provides access to {@link UrlBinding} objects. Bindings are used in two contexts: * <ul> * <li><strong>As a prototype:</strong> Binding prototypes provide static information about the * binding, such as the URI path, string literals, parameter names and default values. However, the * parameters associated with a prototype do not have a value since they are not evaluated against a * live request.</li> * <li><strong>"Live":</strong> Bindings that have been evaluated against a live servlet request * or request URI are exactly like their prototypes except that the parameter values associated with * them contain the values (if any) that were extracted from the URI.</li> * </ul> * </p> * * @author Ben Gunter * @since Stripes 1.5 * @see UrlBinding * @see UrlBindingParameter */ public class UrlBindingFactory { private static final Log log = Log.getInstance(UrlBindingFactory.class); /** Maps {@link ActionBean} classes to {@link UrlBinding}s */ private final Map<Class<? extends ActionBean>, UrlBinding> classCache = new HashMap<Class<? extends ActionBean>, UrlBinding>(); /** Maps simple paths to {@link UrlBinding}s */ private final Map<String, UrlBinding> pathCache = new HashMap<String, UrlBinding>(); /** Keeps a list of all the paths that could not be cached due to conflicts between URL bindings */ private final Map<String, List<UrlBinding>> pathConflicts = new HashMap<String, List<UrlBinding>>(); /** Holds the set of paths that are cached, sorted from longest to shortest */ private final Map<String, Set<UrlBinding>> prefixCache = new TreeMap<String, Set<UrlBinding>>( new Comparator<String>() { public int compare(String a, String b) { int cmp = b.length() - a.length(); return cmp == 0 ? a.compareTo(b) : cmp; } }); /** * Get all the classes implementing {@link ActionBean} */ public Collection<Class<? extends ActionBean>> getActionBeanClasses() { return Collections.unmodifiableSet(classCache.keySet()); } /** * Get the {@link UrlBinding} prototype associated with the given {@link ActionBean} type. This * method may return null if no binding is associated with the given type. * * @param type a class that implements {@link ActionBean} * @return a binding object if one is defined or null if not */ public UrlBinding getBindingPrototype(Class<? extends ActionBean> type) { UrlBinding binding = classCache.get(type); if (binding != null) return binding; binding = parseUrlBinding(type); if (binding != null) addBinding(type, binding); return binding; } /** * Examines a URI (as returned by {@link HttpUtil#getRequestedPath(HttpServletRequest)}) and * returns the associated binding prototype, if any. No attempt is made to extract parameter * values from the URI. This is intended as a fast means to get static information associated * with a given request URI. * * @param uri a request URI * @return a binding prototype, or null if the URI does not match */ public UrlBinding getBindingPrototype(String uri) { // Look for an exact match to the URI first UrlBinding prototype = pathCache.get(uri); if (prototype != null) { log.debug("Matched ", uri, " to ", prototype); return prototype; } else if (pathConflicts.containsKey(uri)) { List<String> strings = new ArrayList<String>(); for (UrlBinding conflict : pathConflicts.get(uri)) strings.add(conflict.toString()); throw new UrlBindingConflictException(uri, strings); } // Get all the bindings whose prefix matches the URI Set<UrlBinding> candidates = null; for (Entry<String, Set<UrlBinding>> entry : prefixCache.entrySet()) { if (uri.startsWith(entry.getKey())) { candidates = entry.getValue(); break; } } // If none matched or exactly one matched then return now if (candidates == null) { log.debug("No URL binding matches ", uri); return null; } else if (candidates.size() == 1) { log.debug("Matched ", uri, " to ", candidates); return candidates.iterator().next(); } // Now find the one that matches deepest into the URI with the fewest components int maxIndex = 0, minComponentCount = Integer.MAX_VALUE, maxComponentMatch = 0; List<String> conflicts = null; for (UrlBinding binding : candidates) { int idx = binding.getPath().length(); List<Object> components = binding.getComponents(); int componentCount = components.size(), componentMatch = 0; for (Object component : components) { if (!(component instanceof String)) continue; String string = (String) component; int at = uri.indexOf(string, idx); if (at >= 0) { idx = at + string.length(); ++componentMatch; } else if (binding.getSuffix() != null) { // Prefer suffix matches string = binding.getSuffix(); at = uri.indexOf(string, idx); if (at >= 0) { idx = at + string.length(); ++componentMatch; } break; } else { break; } } boolean betterMatch = idx > maxIndex || (idx == maxIndex && (componentCount < minComponentCount || componentMatch > maxComponentMatch)); if (betterMatch) { if (conflicts != null) conflicts.clear(); prototype = binding; maxIndex = idx; minComponentCount = componentCount; maxComponentMatch = componentMatch; } else if (idx == maxIndex && componentCount == minComponentCount) { if (conflicts == null) { conflicts = new ArrayList<String>(candidates.size()); conflicts.add(prototype.toString()); } conflicts.add(binding.toString()); prototype = null; } } log.debug("Matched @", maxIndex, " ", uri, " to ", prototype == null ? conflicts : prototype); if (prototype == null) { throw new UrlBindingConflictException(uri, conflicts); } return prototype; } /** * Examines a servlet request and returns the associated binding prototype, if any. No attempt * is made to extract parameter values from the URI. This is intended as a fast means to get * static information associated with a given request. * * @param request a servlet request * @return a binding prototype, or null if the request URI does not match */ public UrlBinding getBindingPrototype(HttpServletRequest request) { return getBindingPrototype(HttpUtil.getRequestedPath(request)); } /** * Examines a URI (as returned by {@link HttpUtil#getRequestedPath(HttpServletRequest)}) and * returns the associated binding, if any. Parameters will be extracted from the URI, and the * {@link UrlBindingParameter} objects returned by {@link UrlBinding#getParameters()} will * contain the values that are present in the URI. * * @param uri a request URI * @return a binding prototype, or null if the URI does not match */ public UrlBinding getBinding(String uri) { UrlBinding prototype = getBindingPrototype(uri); if (prototype == null) return null; // check for literal suffix in prototype and ignore it if found int length = uri.length(); String suffix = prototype.getSuffix(); if (suffix != null && uri.endsWith(suffix)) { length -= suffix.length(); } // ignore trailing slashes in the URI while (length > 0 && uri.charAt(length - 1) == '/') --length; // extract the request parameters and add to new binding object ArrayList<Object> components = new ArrayList<Object>(prototype.getComponents().size()); int index = prototype.getPath().length(); UrlBindingParameter current = null; String value = null; Iterator<Object> iter = prototype.getComponents().iterator(); while (index < length && iter.hasNext()) { Object component = iter.next(); if (component instanceof String) { // extract the parameter value from the URI String literal = (String) component; int end = uri.indexOf(literal, index); if (end >= 0) { value = uri.substring(index, end); index = end + literal.length(); } else { value = uri.substring(index, length); index = length; } // add to the binding if (current != null && value != null && value.length() > 0) { components.add(new UrlBindingParameter(current, value)); components.add(component); current = null; value = null; } } else if (component instanceof UrlBindingParameter) { current = (UrlBindingParameter) component; } } // if component iterator ended before end of string, then grab remainder of string if (index < length) { value = uri.substring(index, length); } // parameter was last component in list if (current != null && value != null && value.length() > 0) { components.add(new UrlBindingParameter(current, value)); } // ensure all components are included so default parameter values are available while (iter.hasNext()) { Object component = iter.next(); if (component instanceof UrlBindingParameter) { components.add(new UrlBindingParameter((UrlBindingParameter) component)); } else { components.add(component); } } return new UrlBinding(prototype.getBeanType(), prototype.getPath(), components); } /** * Examines a servlet request and returns the associated binding, if any. Parameters will be * extracted from the request, and the {@link UrlBindingParameter} objects returned by * {@link UrlBinding#getParameters()} will contain the values that are present in the request. * * @param request a servlet request * @return if the request matches a defined binding, then this method should return that * binding. Otherwise, this method should return null. */ public UrlBinding getBinding(HttpServletRequest request) { return getBinding(HttpUtil.getRequestedPath(request)); } /** * Get all the {@link ActionBean}s classes that have been found. * * @return an immutable collection of {@link ActionBean} classes */ public HashMap<String, Class<? extends ActionBean>> getPathMap() { HashMap<String, Class<? extends ActionBean>> map = new HashMap<String, Class<? extends ActionBean>>(); for (Entry<String, UrlBinding> entry : pathCache.entrySet()) { if (entry.getValue() != null) { map.put(entry.getKey(), entry.getValue().getBeanType()); } } return map; } /** * Map an {@link ActionBean} to a URL. * * @param beanType the {@link ActionBean} class * @param binding the URL binding */ public void addBinding(Class<? extends ActionBean> beanType, UrlBinding binding) { /* * Search for a class that has already been added with the same name as the class being * added now. If one is found then remove its information first and then proceed with adding * it. I know this is not technically correct because two classes from two different class * loaders can have the same name, but this feature is valuable for extensions that reload * classes and I consider it highly unlikely to be a problem in practice. */ Class<? extends ActionBean> existing = null; for (Class<? extends ActionBean> c : classCache.keySet()) { if (c.getName().equals(beanType.getName())) { existing = c; break; } } if (existing != null) removeBinding(existing); // And now we can safely add the class for (String path : getCachedPaths(binding)) { cachePath(path, binding); } for (String prefix : getCachedPrefixes(binding)) { cachePrefix(prefix, binding); } classCache.put(beanType, binding); } /** * Removes an {@link ActionBean}'s URL binding. * * @param beanType the {@link ActionBean} class */ public synchronized void removeBinding(Class<? extends ActionBean> beanType) { UrlBinding binding = classCache.get(beanType); if (binding == null) return; Set<UrlBinding> resolvedConflicts = null; for (String path : getCachedPaths(binding)) { log.debug("Clearing cached path ", path, " for ", binding); pathCache.remove(path); List<UrlBinding> conflicts = pathConflicts.get(path); if (conflicts != null) { log.debug("Removing ", binding, " from conflicts list ", conflicts); conflicts.remove(binding); if (conflicts.size() == 1) { if (resolvedConflicts == null) { resolvedConflicts = new LinkedHashSet<UrlBinding>(); } resolvedConflicts.add(pathCache.get(conflicts.get(0))); conflicts.clear(); } if (conflicts.isEmpty()) pathConflicts.remove(path); } } for (String prefix : getCachedPrefixes(binding)) { Set<UrlBinding> bindings = prefixCache.get(prefix); if (bindings != null) { log.debug("Clearing cached prefix ", prefix, " for ", binding); bindings.remove(binding); if (bindings.isEmpty()) prefixCache.remove(prefix); } } classCache.remove(beanType); if (resolvedConflicts != null) { log.debug("Resolved conflicts with ", resolvedConflicts); for (UrlBinding conflict : resolvedConflicts) { removeBinding(conflict.getBeanType()); addBinding(conflict.getBeanType(), conflict); } } } /** * Get a list of the request paths that will be wired directly to an ActionBean. In some cases, * a single path might be valid for more than one ActionBean. In such a case, a warning will be * logged at startup and an exception will be thrown if the conflicting path is requested. */ protected Set<String> getCachedPaths(UrlBinding binding) { Set<String> paths = new TreeSet<String>(); // Wire some paths directly to the ActionBean (path, path + /, path + suffix, etc.) paths.add(binding.getPath()); paths.add(binding.toString()); if (!binding.getPath().endsWith("/")) paths.add(binding.getPath() + '/'); if (binding.getSuffix() != null) paths.add(binding.getPath() + binding.getSuffix()); return paths; } /** * Get a list of the request path prefixes that <em>could</em> map to an ActionBean. A single * prefix may map to multiple ActionBeans. In such a case, we attempt to determine the best * match based on the literal strings and parameters defined in the ActionBeans' URL bindings. * If no single ActionBean is determined to be a best match, then an exception is thrown to * report the conflict. */ protected Set<String> getCachedPrefixes(UrlBinding binding) { Set<String> prefixes = new TreeSet<String>(); // Add binding as a candidate for some prefixes (path + /, path + leading literal, etc.) if (binding.getPath().endsWith("/")) prefixes.add(binding.getPath()); else prefixes.add(binding.getPath() + '/'); List<Object> components = binding.getComponents(); if (components != null && !components.isEmpty() && components.get(0) instanceof String) { prefixes.add(binding.getPath() + components.get(0)); } return prefixes; } /** * Map a path directly to a binding. If the path matches more than one binding, then a warning * will be logged indicating such a condition, and the path will not be cached for any binding. * * @param path The path to cache * @param binding The binding to which the path should map */ protected void cachePath(String path, UrlBinding binding) { if (pathCache.containsKey(path)) { // Put a null value in the map to indicate a conflict UrlBinding conflict = pathCache.put(path, null); // Construct a list of conflicting bindings List<UrlBinding> conflicts = pathConflicts.get(path); if (conflicts == null) { conflicts = new ArrayList<UrlBinding>(); conflicts.add(conflict); pathConflicts.put(path, conflicts); } conflicts.add(binding); // If there is exactly one binding for this path that declares no parameters, then it is // a static binding and should take precedence over dynamic ones. UrlBinding statik = null; if (conflicts.size() > 1) { for (UrlBinding ub : conflicts) { if (ub.getParameters().isEmpty()) { if (statik == null) { statik = ub; } else { statik = null; break; } } } } // Replace the path cache entry if necessary and log a warning if (statik == null) { log.debug("The path ", path, " for ", binding.getBeanType().getName(), " @ ", binding, " conflicts with ", conflicts); } else { log.debug("For path ", path, ", static binding ", statik, " supersedes conflicting bindings ", conflicts); pathCache.put(path, statik); } } else { log.debug("Wiring path ", path, " to ", binding.getBeanType().getName(), " @ ", binding); pathCache.put(path, binding); } } /** * Add a binding to the set of bindings associated with a prefix. * * @param prefix The prefix to cache * @param binding The binding to map to the prefix */ protected void cachePrefix(String prefix, UrlBinding binding) { log.debug("Wiring prefix ", prefix, "* to ", binding.getBeanType().getName(), " @ ", binding); // Look up existing set of bindings to which the prefix maps Set<UrlBinding> bindings = prefixCache.get(prefix); // If necessary, create and store a new set of bindings if (bindings == null) { bindings = new TreeSet<UrlBinding>(new Comparator<UrlBinding>() { public int compare(UrlBinding o1, UrlBinding o2) { int cmp = o1.getComponents().size() - o2.getComponents().size(); if (cmp == 0) cmp = o1.toString().compareTo(o2.toString()); return cmp; } }); prefixCache.put(prefix, bindings); } // Add the binding to the set bindings.add(binding); } /** * Look for a binding pattern for the given {@link ActionBean} class, specified by the * {@link net.sourceforge.stripes.action.UrlBinding} annotation. If the annotation is found, * create and return a {@link UrlBinding} object for the class. Otherwise, return null. * * @param beanType The {@link ActionBean} type whose binding is to be parsed * @return A {@link UrlBinding} if one is specified, or null if not. * @throws ParseException If the pattern cannot be parsed */ public static UrlBinding parseUrlBinding(Class<? extends ActionBean> beanType) { // check that class is annotated net.sourceforge.stripes.action.UrlBinding annotation = beanType .getAnnotation(net.sourceforge.stripes.action.UrlBinding.class); if (annotation == null) return null; else return parseUrlBinding(beanType, annotation.value()); } /** * Parse the binding pattern and create a {@link UrlBinding} object for the {@link ActionBean} * class. If pattern is null, then return null. * * @param beanType The {@link ActionBean} type to be mapped to the pattern. * @param pattern The URL binding pattern to parse. * @return A {@link UrlBinding} or null if the pattern is null * @throws ParseException If the pattern cannot be parsed */ public static UrlBinding parseUrlBinding(Class<? extends ActionBean> beanType, String pattern) { // check that value is not null if (pattern == null) return null; // make sure it starts with / if (!pattern.startsWith("/")) { throw new ParseException(pattern, "A URL binding must begin with /"); } // parse the pattern String path = null; List<Object> components = new ArrayList<Object>(); boolean brace = false, escape = false; char[] chars = pattern.toCharArray(); StringBuilder buf = new StringBuilder(pattern.length()); char c = 0; for (int i = 0; i < chars.length; i++) { c = chars[i]; if (!escape) { switch (c) { case '{': if (!brace) { brace = true; if (path == null) { // extract trailing non-alphanum chars as a literal to trim the path int end = buf.length() - 1; while (end >= 0 && !Character.isJavaIdentifierPart(buf.charAt(end))) --end; if (end < 0) { path = buf.toString(); } else { ++end; path = buf.substring(0, end); components.add(buf.substring(end)); } } else { components.add(buf.toString()); } buf.setLength(0); continue; } break; case '}': if (brace) { brace = false; components.add(parseUrlBindingParameter(beanType, buf.toString())); buf.setLength(0); continue; } break; case '\\': escape = true; // Preserve escape characters for parameter name parser if (brace) { buf.append(c); } continue; } } // append the char buf.append(c); escape = false; } // Were we led to expect more characters? if (escape) throw new ParseException(pattern, "Expression must not end with escape character"); else if (brace) throw new ParseException(pattern, "Unterminated left brace ('{') in expression"); // handle whatever is left if (buf.length() > 0) { if (path == null) path = buf.toString(); else if (c == '}') components.add(parseUrlBindingParameter(beanType, buf.toString())); else components.add(buf.toString()); } return new UrlBinding(beanType, path, components); } /** * Parses a parameter specification into name and default value and returns a * {@link UrlBindingParameter} with the corresponding name and default value properties set * accordingly. * * @param beanClass the bean class to which the binding applies * @param string the parameter string * @return a parameter object * @throws ParseException if the pattern cannot be parsed */ public static UrlBindingParameter parseUrlBindingParameter( Class<? extends ActionBean> beanClass, String string) { char[] chars = string.toCharArray(); char c; boolean escape = false; StringBuilder name = new StringBuilder(); StringBuilder defaultValue = new StringBuilder(); StringBuilder current = name; for (int i = 0; i < chars.length; i++) { c = chars[i]; if (!escape) { switch (c) { case '\\': escape = true; continue; case '=': current = defaultValue; continue; } } current.append(c); escape = false; } // Parameter name must not be empty if (name.length() < 1) { throw new ParseException(string, "Empty parameter name in URL binding for " + beanClass.getName()); } String dflt = defaultValue.length() < 1 ? null : defaultValue.toString(); if (dflt != null && UrlBindingParameter.PARAMETER_NAME_EVENT.equals(name.toString())) { throw new ParseException(string, "In ActionBean class " + beanClass.getName() + ", the " + UrlBindingParameter.PARAMETER_NAME_EVENT + " parameter may not be assigned a default value. Its default value is" + " determined by the @DefaultHandler annotation."); } return new UrlBindingParameter(beanClass, name.toString(), null, dflt) { @Override public String getValue() { throw new UnsupportedOperationException( "getValue() is not implemented for URL parameter prototypes"); } }; } @Override public String toString() { return String.valueOf(classCache); } }