mrdon 2003/09/28 18:24:21 Modified: src/share/org/apache/struts/action RequestProcessor.java Added: src/share/org/apache/struts/util ActionMappingMatcher.java WildcardHelper.java Log: Added optional wildcard support for action mappings PR: 21813 Revision Changes Path 1.35 +17 -6 jakarta-struts/src/share/org/apache/struts/action/RequestProcessor.java Index: RequestProcessor.java =================================================================== RCS file: /home/cvs/jakarta-struts/src/share/org/apache/struts/action/RequestProcessor.java,v retrieving revision 1.34 retrieving revision 1.35 diff -u -r1.34 -r1.35 --- RequestProcessor.java 11 Sep 2003 01:18:45 -0000 1.34 +++ RequestProcessor.java 29 Sep 2003 01:24:21 -0000 1.35 @@ -82,6 +82,7 @@ import org.apache.struts.config.ModuleConfig; import org.apache.struts.taglib.html.Constants; import org.apache.struts.upload.MultipartRequestWrapper; +import org.apache.struts.util.ActionMappingMatcher; import org.apache.struts.util.MessageResources; import org.apache.struts.util.RequestUtils; @@ -145,6 +146,10 @@ */ protected ActionServlet servlet = null; + /** + * Matches action mapping paths against compiled wildcard patterns + */ + protected ActionMappingMatcher matcher = null; // --------------------------------------------------------- Public Methods @@ -184,7 +189,7 @@ this.servlet = servlet; this.moduleConfig = moduleConfig; - + matcher = new ActionMappingMatcher(moduleConfig); } /** @@ -659,9 +664,15 @@ String path) throws IOException { - // Is there a directly defined mapping for this path? + // Is there a directly defined or wildcard-matched mapping for this + // path? ActionMapping mapping = (ActionMapping) moduleConfig.findActionConfig(path); + if (mapping == null) { + mapping = matcher.match(path); + } + + // If a mapping is found, put it in the request and return it if (mapping != null) { request.setAttribute(Globals.MAPPING_KEY, mapping); return (mapping); 1.1 jakarta-struts/src/share/org/apache/struts/util/ActionMappingMatcher.java Index: ActionMappingMatcher.java =================================================================== /* * ==================================================================== * * The Apache Software License, Version 1.1 * * Copyright (c) 1999-2001 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowlegement: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowlegement may appear in the software itself, * if and wherever such third-party acknowlegements normally appear. * * 4. The names "The Jakarta Project", "Struts", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact [EMAIL PROTECTED] * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Group. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package org.apache.struts.util; // Struts imports: import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionForward; import org.apache.struts.config.ActionConfig; import org.apache.struts.config.ExceptionConfig; import org.apache.struts.config.ForwardConfig; import org.apache.struts.config.ModuleConfig; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.HashMap; import java.util.Iterator; /** * Matches paths against pre-compiled wildcard expressions pulled from * action mappings. It uses the wildcard matcher from the Apache * Cocoon project. * * @author Don Brown */ public class ActionMappingMatcher { /** * The logging instance */ private static final Log log = LogFactory.getLog(ActionMappingMatcher.class); /** * The compiled paths and their associated ActionMapping's */ private List compiledPaths; /** * Finds and precompiles the wildcard patterns from the ActionMapping * "path" attributes. * ActionMapping's will be evaluated in the order they exist in the * Struts config file. Only paths that actually contain a wildcard * will be compiled. * * @param appConfig The ModuleConfig we are associated with */ public ActionMappingMatcher(ModuleConfig appConfig) { compiledPaths = new ArrayList(); ActionConfig[] configs = appConfig.findActionConfigs(); int[] pattern; String path; for (int x = 0; x < configs.length; x++) { path = configs[x].getPath(); if (path.indexOf('*') > -1) { if (path.length() > 0 && path.charAt(0) == '/') { path = path.substring(1); } if (log.isDebugEnabled()) { log.debug("Compiling action mapping path '" + path + "'"); } pattern = WildcardHelper.compilePattern(path); compiledPaths.add(new Mapping(pattern, configs[x])); } } } /** * Matches the path against the compiled wildcard patterns. * * @param path The portion of the request URI for selecting a * mapping * @return The action mapping if matched, else null */ public ActionMapping match(String path) { ActionMapping mapping = null; if (compiledPaths.size() > 0) { if (log.isDebugEnabled()) { log.debug("Attempting to match '" + path + "' to a wildcard pattern"); } if (path.length() > 0 && path.charAt(0) == '/') { path = path.substring(1); } Mapping m; HashMap vars = new HashMap(); for (Iterator i = compiledPaths.iterator(); i.hasNext();) { m = (Mapping) i.next(); if (WildcardHelper.match(vars, path, m.getPattern())) { mapping = convertActionMapping( path, (ActionMapping) m.getActionConfig(), vars); } } } return mapping; } /** * Clones the ActionMapping and its children, replacing various properties * with the values of the wildcard-matched strings. * * @param path The requested path * @param orig The original ActionMapping * @param vars A Map of wildcard-matched strings * @return A cloned ActionMapping with appropriate properties replaced * with wildcard-matched values */ protected ActionMapping convertActionMapping(String path, ActionMapping orig, Map vars) { ActionMapping mapping = new ActionMapping(); mapping.setModuleConfig(orig.getModuleConfig()); mapping.setMultipartClass(orig.getMultipartClass()); mapping.setName(convertParam(orig.getName(), vars)); if (path.charAt(0) != '/') { path = "/" + path; } mapping.setPath(path); mapping.setPrefix(orig.getPrefix()); mapping.setScope(orig.getScope()); mapping.setSuffix(orig.getSuffix()); mapping.setUnknown(orig.getUnknown()); mapping.setValidate(orig.getValidate()); mapping.setType(convertParam(orig.getType(), vars)); mapping.setRoles(convertParam(orig.getRoles(), vars)); mapping.setParameter(convertParam(orig.getParameter(), vars)); mapping.setAttribute(convertParam(orig.getAttribute(), vars)); mapping.setForward(convertParam(orig.getForward(), vars)); mapping.setInclude(convertParam(orig.getInclude(), vars)); mapping.setInput(convertParam(orig.getInput(), vars)); ExceptionConfig[] exConfigs = orig.findExceptionConfigs(); for (int x = 0; x < exConfigs.length; x++) { mapping.addExceptionConfig(exConfigs[x]); } ForwardConfig[] fConfigs = orig.findForwardConfigs(); ForwardConfig cfg; for (int x = 0; x < fConfigs.length; x++) { cfg = new ActionForward(); cfg.setContextRelative(fConfigs[x].getContextRelative()); cfg.setName(fConfigs[x].getName()); cfg.setPath(convertParam(fConfigs[x].getPath(), vars)); mapping.addForwardConfig(cfg); } return mapping; } /** * Inserts into a value wildcard-matched strings where specified. * * @param val The value to convert * @param vars A Map of wildcard-matched strings * @return The new value */ protected String convertParam(String val, Map vars) { if (val == null) { return null; } else if (val.indexOf("{") == -1) { return val; } Map.Entry entry; StringBuffer key = new StringBuffer("{0}"); StringBuffer ret = new StringBuffer(val); int x; for (Iterator i = vars.entrySet().iterator(); i.hasNext();) { entry = (Map.Entry) i.next(); key.setCharAt(1, ((String) entry.getKey()).charAt(0)); x = ret.toString().indexOf(key.toString()); if (x > -1) { ret.replace(x, x + 3, (String) entry.getValue()); } } return ret.toString(); } /** * Stores a compiled wildcard pattern and the ActionConfig it came from */ private class Mapping { /** The compiled pattern */ private int[] pattern; /** The original ActionConfig */ private ActionConfig config; /** * Contructs a read-only Mapping instance * * @param pattern The compiled pattern * @param config The original ActionConfig */ public Mapping(int[] pattern, ActionConfig config) { this.pattern = pattern; this.config = config; } /** * Gets the compiled wildcard pattern * * @return The compiled pattern */ public int[] getPattern() { return this.pattern; } /** * Gets the ActionConfig that contains the pattern * * @return The associated ActionConfig */ public ActionConfig getActionConfig() { return this.config; } } } 1.1 jakarta-struts/src/share/org/apache/struts/util/WildcardHelper.java Index: WildcardHelper.java =================================================================== /* * ==================================================================== * * The Apache Software License, Version 1.1 * * Copyright (c) 1999-2001 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowlegement: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowlegement may appear in the software itself, * if and wherever such third-party acknowlegements normally appear. * * 4. The names "The Jakarta Project", "Struts", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact [EMAIL PROTECTED] * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Group. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package org.apache.struts.util; // java util imports: import java.util.HashMap; /** * This class is an utility class that perform wilcard-patterns matching and * isolation taken from Apache Cocoon. * * @author <a href="mailto:[EMAIL PROTECTED]">Pierpaolo Fumagalli</a> * (Apache Software Foundation) * @author <a href="mailto:[EMAIL PROTECTED]">Giacomo Pati</a> * @author <a href="mailto:[EMAIL PROTECTED]">Stefano Mazzocchi</a> * @author <a href="mailto:[EMAIL PROTECTED]">Berin Loritsch</a> * @version CVS $Id: WildcardHelper.java,v 1.1 2003/09/29 01:24:21 mrdon Exp $ */ public class WildcardHelper { /** The int representing '*' in the pattern <code>int []</code>. */ protected static final int MATCH_FILE = -1; /** The int representing '**' in the pattern <code>int []</code>. */ protected static final int MATCH_PATH = -2; /** The int representing begin in the pattern <code>int []</code>. */ protected static final int MATCH_BEGIN = -4; /** The int representing end in pattern <code>int []</code>. */ protected static final int MATCH_THEEND = -5; /** The int value that terminates the pattern <code>int []</code>. */ protected static final int MATCH_END = -3; /** * Translate the given <code>String</code> into a <code>int []</code> * representing the pattern matchable by this class. * <br> * This function translates a <code>String</code> into an int array * converting the special '*' and '\' characters. * <br> * Here is how the conversion algorithm works: * <ul> * <li>The '*' character is converted to MATCH_FILE, meaning that zero * or more characters (excluding the path separator '/') are to * be matched.</li> * <li>The '**' sequence is converted to MATCH_PATH, meaning that zero * or more characters (including the path separator '/') are to * be matched.</li> * <li>The '\' character is used as an escape sequence ('\*' is * translated in '*', not in MATCH_FILE). If an exact '\' character * is to be matched the source string must contain a '\\'. * sequence.</li> * </ul> * When more than two '*' characters, not separated by another character, * are found their value is considered as '**' (MATCH_PATH). * <br> * The array is always terminated by a special value (MATCH_END). * <br> * All MATCH* values are less than zero, while normal characters are equal * or greater. * * @param data The string to translate. * @return The encoded string as an int array, terminated by the MATCH_END * value (don't consider the array length). * @exception NullPointerException If data is null. */ public static int[] compilePattern(String data) throws NullPointerException { // Prepare the arrays int expr[] = new int[data.length() + 2]; char buff[] = data.toCharArray(); // Prepare variables for the translation loop int y = 0; boolean slash = false; // Must start from beginning expr[y++] = MATCH_BEGIN; if (buff.length > 0) { if (buff[0] == '\\') { slash = true; } else if (buff[0] == '*') { expr[y++] = MATCH_FILE; } else { expr[y++] = buff[0]; } // Main translation loop for (int x = 1; x < buff.length; x++) { // If the previous char was '\' simply copy this char. if (slash) { expr[y++] = buff[x]; slash = false; // If the previous char was not '\' we have to do a bunch of // checks } else { // If this char is '\' declare that and continue if (buff[x] == '\\') { slash = true; // If this char is '*' check the previous one } else if (buff[x] == '*') { // If the previous character als was '*' match a path if (expr[y - 1] <= MATCH_FILE) { expr[y - 1] = MATCH_PATH; } else { expr[y++] = MATCH_FILE; } } else { expr[y++] = buff[x]; } } } } // Must match end at the end expr[y] = MATCH_THEEND; return expr; } /** * Match a pattern agains a string and isolates wildcard replacement into a * <code>Stack</code>. * * @param map The map to store matched values * @param data The string to match * @param expr The compiled wildcard expression * @return True if a match * @throws NullPointerException If any parameters are null */ public static boolean match (HashMap map, String data, int[] expr) throws NullPointerException { if (map == null) { throw new NullPointerException ("No map provided"); } if (data == null) { throw new NullPointerException ("No data provided"); } if (expr == null) { throw new NullPointerException ("No pattern expression provided"); } char buff[] = data.toCharArray(); // Allocate the result buffer char rslt[] = new char[expr.length + buff.length]; // The previous and current position of the expression character // (MATCH_*) int charpos = 0; // The position in the expression, input, translation and result arrays int exprpos = 0; int buffpos = 0; int rsltpos = 0; int offset = -1; // The matching count int mcount = 0; // We want the complete data be in {0} map.put(Integer.toString(mcount), data); // First check for MATCH_BEGIN boolean matchBegin = false; if (expr[charpos] == MATCH_BEGIN) { matchBegin = true; exprpos = ++charpos; } // Search the fist expression character (except MATCH_BEGIN - already // skipped) while (expr[charpos] >= 0) { charpos++; } // The expression charater (MATCH_*) int exprchr = expr[charpos]; while (true) { // Check if the data in the expression array before the current // expression character matches the data in the input buffer if (matchBegin) { if (!matchArray(expr, exprpos, charpos, buff, buffpos)) { return (false); } matchBegin = false; } else { offset = indexOfArray (expr, exprpos, charpos, buff, buffpos); if (offset < 0) { return (false); } } // Check for MATCH_BEGIN if (matchBegin) { if (offset != 0) { return (false); } matchBegin = false; } // Advance buffpos buffpos += (charpos - exprpos); // Check for END's if (exprchr == MATCH_END) { if (rsltpos > 0) { map.put(Integer.toString(++mcount), new String(rslt, 0, rsltpos)); } // Don't care about rest of input buffer return (true); } else if (exprchr == MATCH_THEEND) { if (rsltpos > 0) { map.put(Integer.toString(++mcount), new String(rslt, 0, rsltpos)); } // Check that we reach buffer's end return (buffpos == buff.length); } // Search the next expression character exprpos = ++charpos; while (expr[charpos] >= 0) { charpos++; } int prevchr = exprchr; exprchr = expr[charpos]; // We have here prevchr == * or **. offset = (prevchr == MATCH_FILE) ? indexOfArray (expr, exprpos, charpos, buff, buffpos) : lastIndexOfArray (expr, exprpos, charpos, buff, buffpos); if (offset < 0) { return (false); } // Copy the data from the source buffer into the result buffer // to substitute the expression character if (prevchr == MATCH_PATH) { while (buffpos < offset) { rslt[rsltpos++] = buff[buffpos++]; } } else { // Matching file, don't copy '/' while (buffpos < offset) { if (buff[buffpos] == '/') { return (false); } rslt[rsltpos++] = buff[buffpos++]; } } map.put(Integer.toString(++mcount), new String (rslt, 0, rsltpos)); rsltpos = 0; } } /** * Get the offset of a part of an int array within a char array. * <br> * This method return the index in d of the first occurrence after dpos of * that part of array specified by r, starting at rpos and terminating at * rend. * * @param r The array containing the data that need to be matched in d. * @param rpos The index of the first character in r to look for. * @param rend The index of the last character in r to look for plus 1. * @param d The array of char that should contain a part of r. * @param dpos The starting offset in d for the matching. * @return The offset in d of the part of r matched in d or -1 if that was * not found. */ protected static int indexOfArray (int r[], int rpos, int rend, char d[], int dpos) { // Check if pos and len are legal if (rend < rpos) { throw new IllegalArgumentException ("rend < rpos"); } // If we need to match a zero length string return current dpos if (rend == rpos) { return (d.length); //?? dpos? } // If we need to match a 1 char length string do it simply if ((rend - rpos) == 1) { // Search for the specified character for (int x = dpos; x < d.length; x++) { if (r[rpos] == d[x]) { return (x); } } } // Main string matching loop. It gets executed if the characters to // match are less then the characters left in the d buffer while ((dpos + rend - rpos) <= d.length) { // Set current startpoint in d int y = dpos; // Check every character in d for equity. If the string is matched // return dpos for (int x = rpos; x <= rend; x++) { if (x == rend) { return (dpos); } if (r[x] != d[y++]) { break; } } // Increase dpos to search for the same string at next offset dpos++; } // The remaining chars in d buffer were not enough or the string // wasn't matched return (-1); } /** * Get the offset of a last occurance of an int array within a char array. * <br> * This method return the index in d of the last occurrence after dpos of * that part of array specified by r, starting at rpos and terminating at * rend. * * @param r The array containing the data that need to be matched in d. * @param rpos The index of the first character in r to look for. * @param rend The index of the last character in r to look for plus 1. * @param d The array of char that should contain a part of r. * @param dpos The starting offset in d for the matching. * @return The offset in d of the last part of r matched in d or -1 if * that was not found. */ protected static int lastIndexOfArray (int r[], int rpos, int rend, char d[], int dpos) { // Check if pos and len are legal if (rend < rpos) { throw new IllegalArgumentException ("rend < rpos"); } // If we need to match a zero length string return current dpos if (rend == rpos) { return (d.length); //?? dpos? } // If we need to match a 1 char length string do it simply if ((rend - rpos) == 1) { // Search for the specified character for (int x = d.length - 1; x > dpos; x--) { if (r[rpos] == d[x]) { return (x); } } } // Main string matching loop. It gets executed if the characters to // match are less then the characters left in the d buffer int l = d.length - (rend - rpos); while (l >= dpos) { // Set current startpoint in d int y = l; // Check every character in d for equity. If the string is matched // return dpos for (int x = rpos; x <= rend; x++) { if (x == rend) { return (l); } if (r[x] != d[y++]) { break; } } // Decrease l to search for the same string at next offset l--; } // The remaining chars in d buffer were not enough or the string // wasn't matched return (-1); } /** * Matches elements of array r from rpos to rend with array d, starting * from dpos. * <br> * This method return true if elements of array r from rpos to rend * equals elements of array d starting from dpos to dpos+(rend-rpos). * * @param r The array containing the data that need to be matched in d. * @param rpos The index of the first character in r to look for. * @param rend The index of the last character in r to look for. * @param d The array of char that should start from a part of r. * @param dpos The starting offset in d for the matching. * @return true if array d starts from portion of array r. */ protected static boolean matchArray (int r[], int rpos, int rend, char d[], int dpos) { if (d.length - dpos < rend - rpos) { return (false); } for (int i = rpos; i < rend; i++) { if (r[i] != d[dpos++]) { return (false); } } return (true); } }
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]