http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/PropertiesReader.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/util/PropertiesReader.java b/core/src/main/java/com/opensymphony/xwork2/util/PropertiesReader.java new file mode 100644 index 0000000..a25a679 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/PropertiesReader.java @@ -0,0 +1,546 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.opensymphony.xwork2.util; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +/** + * This class is used to read properties lines. These lines do + * not terminate with new-line chars but rather when there is no + * backslash sign a the end of the line. This is used to + * concatenate multiple lines for readability. + * <p/> + * This class was pulled out of Jakarta Commons Configuration and + * Jakarta Commons Lang trunk revision 476093 + */ +public class PropertiesReader extends LineNumberReader { + /** + * Stores the comment lines for the currently processed property. + */ + private List<String> commentLines; + + /** + * Stores the name of the last read property. + */ + private String propertyName; + + /** + * Stores the value of the last read property. + */ + private String propertyValue; + + /** + * Stores the list delimiter character. + */ + private char delimiter; + + /** + * Constant for the supported comment characters. + */ + static final String COMMENT_CHARS = "#!"; + + /** + * Constant for the radix of hex numbers. + */ + private static final int HEX_RADIX = 16; + + /** + * Constant for the length of a unicode literal. + */ + private static final int UNICODE_LEN = 4; + + /** + * The list of possible key/value separators + */ + private static final char[] SEPARATORS = new char[]{'=', ':'}; + + /** + * The white space characters used as key/value separators. + */ + private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'}; + + /** + * Constructor. + * + * @param reader A Reader. + */ + public PropertiesReader(Reader reader) { + this(reader, ','); + } + + /** + * Creates a new instance of <code>PropertiesReader</code> and sets + * the underlaying reader and the list delimiter. + * + * @param reader the reader + * @param listDelimiter the list delimiter character + * @since 1.3 + */ + public PropertiesReader(Reader reader, char listDelimiter) { + super(reader); + commentLines = new ArrayList<String>(); + delimiter = listDelimiter; + } + + /** + * Tests whether a line is a comment, i.e. whether it starts with a comment + * character. + * + * @param line the line + * @return a flag if this is a comment line + * @since 1.3 + */ + boolean isCommentLine(String line) { + String s = line.trim(); + // blanc lines are also treated as comment lines + return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0; + } + + /** + * Reads a property line. Returns null if Stream is + * at EOF. Concatenates lines ending with "\". + * Skips lines beginning with "#" or "!" and empty lines. + * The return value is a property definition (<code><name></code> + * = <code><value></code>) + * + * @return A string containing a property value or null + * @throws IOException in case of an I/O error + */ + public String readProperty() throws IOException { + commentLines.clear(); + StringBuilder buffer = new StringBuilder(); + + while (true) { + String line = readLine(); + if (line == null) { + // EOF + return null; + } + + if (isCommentLine(line)) { + commentLines.add(line); + continue; + } + + line = line.trim(); + + if (checkCombineLines(line)) { + line = line.substring(0, line.length() - 1); + buffer.append(line); + } else { + buffer.append(line); + break; + } + } + return buffer.toString(); + } + + /** + * Parses the next property from the input stream and stores the found + * name and value in internal fields. These fields can be obtained using + * the provided getter methods. The return value indicates whether EOF + * was reached (<b>false</b>) or whether further properties are + * available (<b>true</b>). + * + * @return a flag if further properties are available + * @throws IOException if an error occurs + * @since 1.3 + */ + public boolean nextProperty() throws IOException { + String line = readProperty(); + + if (line == null) { + return false; // EOF + } + + // parse the line + String[] property = parseProperty(line); + propertyName = unescapeJava(property[0]); + propertyValue = unescapeJava(property[1], delimiter); + return true; + } + + /** + * Returns the comment lines that have been read for the last property. + * + * @return the comment lines for the last property returned by + * <code>readProperty()</code> + * @since 1.3 + */ + public List<String> getCommentLines() { + return commentLines; + } + + /** + * Returns the name of the last read property. This method can be called + * after <code>{@link #nextProperty()}</code> was invoked and its + * return value was <b>true</b>. + * + * @return the name of the last read property + * @since 1.3 + */ + public String getPropertyName() { + return propertyName; + } + + /** + * Returns the value of the last read property. This method can be + * called after <code>{@link #nextProperty()}</code> was invoked and + * its return value was <b>true</b>. + * + * @return the value of the last read property + * @since 1.3 + */ + public String getPropertyValue() { + return propertyValue; + } + + /** + * Checks if the passed in line should be combined with the following. + * This is true, if the line ends with an odd number of backslashes. + * + * @param line the line + * @return a flag if the lines should be combined + */ + private boolean checkCombineLines(String line) { + int bsCount = 0; + for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) { + bsCount++; + } + + return bsCount % 2 == 1; + } + + /** + * Parse a property line and return the key and the value in an array. + * + * @param line the line to parse + * @return an array with the property's key and value + * @since 1.2 + */ + private String[] parseProperty(String line) { + // sorry for this spaghetti code, please replace it as soon as + // possible with a regexp when the Java 1.3 requirement is dropped + + String[] result = new String[2]; + StringBuilder key = new StringBuilder(); + StringBuilder value = new StringBuilder(); + + // state of the automaton: + // 0: key parsing + // 1: antislash found while parsing the key + // 2: separator crossing + // 3: value parsing + int state = 0; + + for (int pos = 0; pos < line.length(); pos++) { + char c = line.charAt(pos); + + switch (state) { + case 0: + if (c == '\\') { + state = 1; + } else if (contains(WHITE_SPACE, c)) { + // switch to the separator crossing state + state = 2; + } else if (contains(SEPARATORS, c)) { + // switch to the value parsing state + state = 3; + } else { + key.append(c); + } + + break; + + case 1: + if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c)) { + // this is an escaped separator or white space + key.append(c); + } else { + // another escaped character, the '\' is preserved + key.append('\\'); + key.append(c); + } + + // return to the key parsing state + state = 0; + + break; + + case 2: + if (contains(WHITE_SPACE, c)) { + // do nothing, eat all white spaces + state = 2; + } else if (contains(SEPARATORS, c)) { + // switch to the value parsing state + state = 3; + } else { + // any other character indicates we encoutered the beginning of the value + value.append(c); + + // switch to the value parsing state + state = 3; + } + + break; + + case 3: + value.append(c); + break; + } + } + + result[0] = key.toString().trim(); + result[1] = value.toString().trim(); + + return result; + } + + /** + * <p>Unescapes any Java literals found in the <code>String</code> to a + * <code>Writer</code>.</p> This is a slightly modified version of the + * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't + * drop escaped separators (i.e '\,'). + * + * @param str the <code>String</code> to unescape, may be null + * @param delimiter the delimiter for multi-valued properties + * @return the processed string + * @throws IllegalArgumentException if the Writer is <code>null</code> + */ + protected static String unescapeJava(String str, char delimiter) { + if (str == null) { + return null; + } + int sz = str.length(); + StringBuilder out = new StringBuilder(sz); + StringBuffer unicode = new StringBuffer(UNICODE_LEN); + boolean hadSlash = false; + boolean inUnicode = false; + for (int i = 0; i < sz; i++) { + char ch = str.charAt(i); + if (inUnicode) { + // if in unicode, then we're reading unicode + // values in somehow + unicode.append(ch); + if (unicode.length() == UNICODE_LEN) { + // unicode now contains the four hex digits + // which represents our unicode character + try { + int value = Integer.parseInt(unicode.toString(), HEX_RADIX); + out.append((char) value); + unicode.setLength(0); + inUnicode = false; + hadSlash = false; + } catch (NumberFormatException nfe) { + throw new RuntimeException("Unable to parse unicode value: " + unicode, nfe); + } + } + continue; + } + + if (hadSlash) { + // handle an escaped value + hadSlash = false; + + if (ch == '\\') { + out.append('\\'); + } else if (ch == '\'') { + out.append('\''); + } else if (ch == '\"') { + out.append('"'); + } else if (ch == 'r') { + out.append('\r'); + } else if (ch == 'f') { + out.append('\f'); + } else if (ch == 't') { + out.append('\t'); + } else if (ch == 'n') { + out.append('\n'); + } else if (ch == 'b') { + out.append('\b'); + } else if (ch == delimiter) { + out.append('\\'); + out.append(delimiter); + } else if (ch == 'u') { + // uh-oh, we're in unicode country.... + inUnicode = true; + } else { + out.append(ch); + } + + continue; + } else if (ch == '\\') { + hadSlash = true; + continue; + } + out.append(ch); + } + + if (hadSlash) { + // then we're in the weird case of a \ at the end of the + // string, let's output it anyway. + out.append('\\'); + } + + return out.toString(); + } + + /** + * <p>Checks if the object is in the given array.</p> + * <p/> + * <p>The method returns <code>false</code> if a <code>null</code> array is passed in.</p> + * + * @param array the array to search through + * @param objectToFind the object to find + * @return <code>true</code> if the array contains the object + */ + public boolean contains(char[] array, char objectToFind) { + if (array == null) { + return false; + } + for (char anArray : array) { + if (objectToFind == anArray) { + return true; + } + } + return false; + } + + /** + * <p>Unescapes any Java literals found in the <code>String</code>. + * For example, it will turn a sequence of <code>'\'</code> and + * <code>'n'</code> into a newline character, unless the <code>'\'</code> + * is preceded by another <code>'\'</code>.</p> + * + * @param str the <code>String</code> to unescape, may be null + * @return a new unescaped <code>String</code>, <code>null</code> if null string input + */ + public static String unescapeJava(String str) { + if (str == null) { + return null; + } + try { + StringWriter writer = new StringWriter(str.length()); + unescapeJava(writer, str); + return writer.toString(); + } catch (IOException ioe) { + // this should never ever happen while writing to a StringWriter + ioe.printStackTrace(); + return null; + } + } + + /** + * <p>Unescapes any Java literals found in the <code>String</code> to a + * <code>Writer</code>.</p> + * <p/> + * <p>For example, it will turn a sequence of <code>'\'</code> and + * <code>'n'</code> into a newline character, unless the <code>'\'</code> + * is preceded by another <code>'\'</code>.</p> + * <p/> + * <p>A <code>null</code> string input has no effect.</p> + * + * @param out the <code>Writer</code> used to output unescaped characters + * @param str the <code>String</code> to unescape, may be null + * @throws IllegalArgumentException if the Writer is <code>null</code> + * @throws IOException if error occurs on underlying Writer + */ + public static void unescapeJava(Writer out, String str) throws IOException { + if (out == null) { + throw new IllegalArgumentException("The Writer must not be null"); + } + if (str == null) { + return; + } + int sz = str.length(); + StringBuffer unicode = new StringBuffer(4); + boolean hadSlash = false; + boolean inUnicode = false; + for (int i = 0; i < sz; i++) { + char ch = str.charAt(i); + if (inUnicode) { + // if in unicode, then we're reading unicode + // values in somehow + unicode.append(ch); + if (unicode.length() == 4) { + // unicode now contains the four hex digits + // which represents our unicode character + try { + int value = Integer.parseInt(unicode.toString(), 16); + out.write((char) value); + unicode.setLength(0); + inUnicode = false; + hadSlash = false; + } catch (NumberFormatException nfe) { + throw new RuntimeException("Unable to parse unicode value: " + unicode, nfe); + } + } + continue; + } + if (hadSlash) { + // handle an escaped value + hadSlash = false; + switch (ch) { + case '\\': + out.write('\\'); + break; + case '\'': + out.write('\''); + break; + case '\"': + out.write('"'); + break; + case 'r': + out.write('\r'); + break; + case 'f': + out.write('\f'); + break; + case 't': + out.write('\t'); + break; + case 'n': + out.write('\n'); + break; + case 'b': + out.write('\b'); + break; + case 'u': { + // uh-oh, we're in unicode country.... + inUnicode = true; + break; + } + default: + out.write(ch); + break; + } + continue; + } else if (ch == '\\') { + hadSlash = true; + continue; + } + out.write(ch); + } + if (hadSlash) { + // then we're in the weird case of a \ at the end of the + // string, let's output it anyway. + out.write('\\'); + } + } +}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/ResolverUtil.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/util/ResolverUtil.java b/core/src/main/java/com/opensymphony/xwork2/util/ResolverUtil.java new file mode 100644 index 0000000..e7222cd --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/ResolverUtil.java @@ -0,0 +1,466 @@ +/* Copyright 2005-2006 Tim Fennell + * + * 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 com.opensymphony.xwork2.util; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.net.URL; +import java.net.URLDecoder; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +/** + * <p>ResolverUtil is used to locate classes that are available in the/a class path and meet + * arbitrary conditions. The two most common conditions are that a class implements/extends + * another class, or that is it annotated with a specific annotation. However, through the use + * of the {@link Test} class it is possible to search using arbitrary conditions.</p> + * + * <p>A ClassLoader is used to locate all locations (directories and jar files) in the class + * path that contain classes within certain packages, and then to load those classes and + * check them. By default the ClassLoader returned by + * {@code Thread.currentThread().getContextClassLoader()} is used, but this can be overridden + * by calling {@link #setClassLoader(ClassLoader)} prior to invoking any of the {@code find()} + * methods.</p> + * + * <p>General searches are initiated by calling the + * {@link #find(com.opensymphony.xwork2.util.ResolverUtil.Test, String...)} ()} method and supplying + * a package name and a Test instance. This will cause the named package <b>and all sub-packages</b> + * to be scanned for classes that meet the test. There are also utility methods for the common + * use cases of scanning multiple packages for extensions of particular classes, or classes + * annotated with a specific annotation.</p> + * + * <p>The standard usage pattern for the ResolverUtil class is as follows:</p> + * + *<pre> + *ResolverUtil<ActionBean> resolver = new ResolverUtil<ActionBean>(); + *resolver.findImplementation(ActionBean.class, pkg1, pkg2); + *resolver.find(new CustomTest(), pkg1); + *resolver.find(new CustomTest(), pkg2); + *Collection<ActionBean> beans = resolver.getClasses(); + *</pre> + * + * <p>This class was copied from Stripes - http://stripes.mc4j.org/confluence/display/stripes/Home + * </p> + * + * @author Tim Fennell + */ +public class ResolverUtil<T> { + /** An instance of Log to use for logging in this class. */ + private static final Logger LOG = LogManager.getLogger(ResolverUtil.class); + + /** + * A simple interface that specifies how to test classes to determine if they + * are to be included in the results produced by the ResolverUtil. + */ + public static interface Test { + /** + * Will be called repeatedly with candidate classes. Must return True if a class + * is to be included in the results, false otherwise. + */ + boolean matches(Class type); + + boolean matches(URL resource); + + boolean doesMatchClass(); + boolean doesMatchResource(); + } + + public static abstract class ClassTest implements Test { + public boolean matches(URL resource) { + throw new UnsupportedOperationException(); + } + + public boolean doesMatchClass() { + return true; + } + public boolean doesMatchResource() { + return false; + } + } + + public static abstract class ResourceTest implements Test { + public boolean matches(Class cls) { + throw new UnsupportedOperationException(); + } + + public boolean doesMatchClass() { + return false; + } + public boolean doesMatchResource() { + return true; + } + } + + /** + * A Test that checks to see if each class is assignable to the provided class. Note + * that this test will match the parent type itself if it is presented for matching. + */ + public static class IsA extends ClassTest { + private Class parent; + + /** Constructs an IsA test using the supplied Class as the parent class/interface. */ + public IsA(Class parentType) { this.parent = parentType; } + + /** Returns true if type is assignable to the parent type supplied in the constructor. */ + public boolean matches(Class type) { + return type != null && parent.isAssignableFrom(type); + } + + @Override public String toString() { + return "is assignable to " + parent.getSimpleName(); + } + } + + /** + * A Test that checks to see if each class name ends with the provided suffix. + */ + public static class NameEndsWith extends ClassTest { + private String suffix; + + /** Constructs a NameEndsWith test using the supplied suffix. */ + public NameEndsWith(String suffix) { this.suffix = suffix; } + + /** Returns true if type name ends with the suffix supplied in the constructor. */ + public boolean matches(Class type) { + return type != null && type.getName().endsWith(suffix); + } + + @Override public String toString() { + return "ends with the suffix " + suffix; + } + } + + /** + * A Test that checks to see if each class is annotated with a specific annotation. If it + * is, then the test returns true, otherwise false. + */ + public static class AnnotatedWith extends ClassTest { + private Class<? extends Annotation> annotation; + + /** Construts an AnnotatedWith test for the specified annotation type. */ + public AnnotatedWith(Class<? extends Annotation> annotation) { this.annotation = annotation; } + + /** Returns true if the type is annotated with the class provided to the constructor. */ + public boolean matches(Class type) { + return type != null && type.isAnnotationPresent(annotation); + } + + @Override public String toString() { + return "annotated with @" + annotation.getSimpleName(); + } + } + + public static class NameIs extends ResourceTest { + private String name; + + public NameIs(String name) { this.name = "/" + name; } + + public boolean matches(URL resource) { + return (resource.getPath().endsWith(name)); + } + + @Override public String toString() { + return "named " + name; + } + } + + /** The set of matches being accumulated. */ + private Set<Class<? extends T>> classMatches = new HashSet<Class<?extends T>>(); + + /** The set of matches being accumulated. */ + private Set<URL> resourceMatches = new HashSet<URL>(); + + /** + * The ClassLoader to use when looking for classes. If null then the ClassLoader returned + * by Thread.currentThread().getContextClassLoader() will be used. + */ + private ClassLoader classloader; + + /** + * Provides access to the classes discovered so far. If no calls have been made to + * any of the {@code find()} methods, this set will be empty. + * + * @return the set of classes that have been discovered. + */ + public Set<Class<? extends T>> getClasses() { + return classMatches; + } + + public Set<URL> getResources() { + return resourceMatches; + } + + + /** + * Returns the classloader that will be used for scanning for classes. If no explicit + * ClassLoader has been set by the calling, the context class loader will be used. + * + * @return the ClassLoader that will be used to scan for classes + */ + public ClassLoader getClassLoader() { + return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader; + } + + /** + * Sets an explicit ClassLoader that should be used when scanning for classes. If none + * is set then the context classloader will be used. + * + * @param classloader a ClassLoader to use when scanning for classes + */ + public void setClassLoader(ClassLoader classloader) { this.classloader = classloader; } + + /** + * Attempts to discover classes that are assignable to the type provided. In the case + * that an interface is provided this method will collect implementations. In the case + * of a non-interface class, subclasses will be collected. Accumulated classes can be + * accessed by calling {@link #getClasses()}. + * + * @param parent the class of interface to find subclasses or implementations of + * @param packageNames one or more package names to scan (including subpackages) for classes + */ + public void findImplementations(Class parent, String... packageNames) { + if (packageNames == null) return; + + Test test = new IsA(parent); + for (String pkg : packageNames) { + findInPackage(test, pkg); + } + } + + /** + * Attempts to discover classes who's name ends with the provided suffix. Accumulated classes can be + * accessed by calling {@link #getClasses()}. + * + * @param suffix The class name suffix to match + * @param packageNames one or more package names to scan (including subpackages) for classes + */ + public void findSuffix(String suffix, String... packageNames) { + if (packageNames == null) return; + + Test test = new NameEndsWith(suffix); + for (String pkg : packageNames) { + findInPackage(test, pkg); + } + } + + /** + * Attempts to discover classes that are annotated with to the annotation. Accumulated + * classes can be accessed by calling {@link #getClasses()}. + * + * @param annotation the annotation that should be present on matching classes + * @param packageNames one or more package names to scan (including subpackages) for classes + */ + public void findAnnotated(Class<? extends Annotation> annotation, String... packageNames) { + if (packageNames == null) return; + + Test test = new AnnotatedWith(annotation); + for (String pkg : packageNames) { + findInPackage(test, pkg); + } + } + + public void findNamedResource(String name, String... pathNames) { + if (pathNames == null) return; + + Test test = new NameIs(name); + for (String pkg : pathNames) { + findInPackage(test, pkg); + } + } + + /** + * Attempts to discover classes that pass the test. Accumulated + * classes can be accessed by calling {@link #getClasses()}. + * + * @param test the test to determine matching classes + * @param packageNames one or more package names to scan (including subpackages) for classes + */ + public void find(Test test, String... packageNames) { + if (packageNames == null) return; + + for (String pkg : packageNames) { + findInPackage(test, pkg); + } + } + + /** + * Scans for classes starting at the package provided and descending into subpackages. + * Each class is offered up to the Test as it is discovered, and if the Test returns + * true the class is retained. Accumulated classes can be fetched by calling + * {@link #getClasses()}. + * + * @param test an instance of {@link Test} that will be used to filter classes + * @param packageName the name of the package from which to start scanning for + * classes, e.g. {@code net.sourceforge.stripes} + */ + public void findInPackage(Test test, String packageName) { + packageName = packageName.replace('.', '/'); + ClassLoader loader = getClassLoader(); + Enumeration<URL> urls; + + try { + urls = loader.getResources(packageName); + } + catch (IOException ioe) { + if (LOG.isWarnEnabled()) { + LOG.warn("Could not read package: " + packageName, ioe); + } + return; + } + + while (urls.hasMoreElements()) { + try { + String urlPath = urls.nextElement().getFile(); + urlPath = URLDecoder.decode(urlPath, "UTF-8"); + + // If it's a file in a directory, trim the stupid file: spec + if ( urlPath.startsWith("file:") ) { + urlPath = urlPath.substring(5); + } + + // Else it's in a JAR, grab the path to the jar + if (urlPath.indexOf('!') > 0) { + urlPath = urlPath.substring(0, urlPath.indexOf('!')); + } + + if (LOG.isInfoEnabled()) { + LOG.info("Scanning for classes in [" + urlPath + "] matching criteria: " + test); + } + File file = new File(urlPath); + if ( file.isDirectory() ) { + loadImplementationsInDirectory(test, packageName, file); + } + else { + loadImplementationsInJar(test, packageName, file); + } + } + catch (IOException ioe) { + if (LOG.isWarnEnabled()) { + LOG.warn("could not read entries", ioe); + } + } + } + } + + + /** + * Finds matches in a physical directory on a filesystem. Examines all + * files within a directory - if the File object is not a directory, and ends with <i>.class</i> + * the file is loaded and tested to see if it is acceptable according to the Test. Operates + * recursively to find classes within a folder structure matching the package structure. + * + * @param test a Test used to filter the classes that are discovered + * @param parent the package name up to this directory in the package hierarchy. E.g. if + * /classes is in the classpath and we wish to examine files in /classes/org/apache then + * the values of <i>parent</i> would be <i>org/apache</i> + * @param location a File object representing a directory + */ + private void loadImplementationsInDirectory(Test test, String parent, File location) { + File[] files = location.listFiles(); + StringBuilder builder = null; + + for (File file : files) { + builder = new StringBuilder(100); + builder.append(parent).append("/").append(file.getName()); + String packageOrClass = ( parent == null ? file.getName() : builder.toString() ); + + if (file.isDirectory()) { + loadImplementationsInDirectory(test, packageOrClass, file); + } + else if (isTestApplicable(test, file.getName())) { + addIfMatching(test, packageOrClass); + } + } + } + + private boolean isTestApplicable(Test test, String path) { + return test.doesMatchResource() || path.endsWith(".class") && test.doesMatchClass(); + } + + /** + * Finds matching classes within a jar files that contains a folder structure + * matching the package structure. If the File is not a JarFile or does not exist a warning + * will be logged, but no error will be raised. + * + * @param test a Test used to filter the classes that are discovered + * @param parent the parent package under which classes must be in order to be considered + * @param jarfile the jar file to be examined for classes + */ + private void loadImplementationsInJar(Test test, String parent, File jarfile) { + + try { + JarEntry entry; + JarInputStream jarStream = new JarInputStream(new FileInputStream(jarfile)); + + while ( (entry = jarStream.getNextJarEntry() ) != null) { + String name = entry.getName(); + if (!entry.isDirectory() && name.startsWith(parent) && isTestApplicable(test, name)) { + addIfMatching(test, name); + } + } + } + catch (IOException ioe) { + LOG.error("Could not search jar file '" + jarfile + "' for classes matching criteria: " + + test + " due to an IOException", ioe); + } + } + + /** + * Add the class designated by the fully qualified class name provided to the set of + * resolved classes if and only if it is approved by the Test supplied. + * + * @param test the test used to determine if the class matches + * @param fqn the fully qualified name of a class + */ + protected void addIfMatching(Test test, String fqn) { + try { + ClassLoader loader = getClassLoader(); + if (test.doesMatchClass()) { + String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.'); + if (LOG.isDebugEnabled()) { + LOG.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]"); + } + + Class type = loader.loadClass(externalName); + if (test.matches(type) ) { + classMatches.add( (Class<T>) type); + } + } + if (test.doesMatchResource()) { + URL url = loader.getResource(fqn); + if (url == null) { + url = loader.getResource(fqn.substring(1)); + } + if (url != null && test.matches(url)) { + resourceMatches.add(url); + } + } + } + catch (Throwable t) { + if (LOG.isWarnEnabled()) { + LOG.warn("Could not examine class '" + fqn + "' due to a " + + t.getClass().getName() + " with message: " + t.getMessage()); + } + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java b/core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java new file mode 100644 index 0000000..db1af6d --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java @@ -0,0 +1,298 @@ +/* + * Copyright 2002-2006,2009 The Apache Software Foundation. + * + * 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 com.opensymphony.xwork2.util; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.conversion.impl.XWorkConverter; +import com.opensymphony.xwork2.inject.Container; + +import java.util.*; + + +/** + * Utility class for text parsing. + * + * @author Jason Carreira + * @author Rainer Hermanns + * @author tm_jee + * + * @version $Date$ $Id$ + */ +public class TextParseUtil { + + private static final int MAX_RECURSION = 1; + + /** + * Converts all instances of ${...}, and %{...} in <code>expression</code> to the value returned + * by a call to {@link ValueStack#findValue(java.lang.String)}. If an item cannot + * be found on the stack (null is returned), then the entire variable ${...} is not + * displayed, just as if the item was on the stack but returned an empty string. + * + * @param expression an expression that hasn't yet been translated + * @return the parsed expression + */ + public static String translateVariables(String expression, ValueStack stack) { + return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, null).toString(); + } + + + /** + * Function similarly as {@link #translateVariables(char, String, ValueStack)} + * except for the introduction of an additional <code>evaluator</code> that allows + * the parsed value to be evaluated by the <code>evaluator</code>. The <code>evaluator</code> + * could be null, if it is it will just be skipped as if it is just calling + * {@link #translateVariables(char, String, ValueStack)}. + * + * <p/> + * + * A typical use-case would be when we need to URL Encode the parsed value. To do so + * we could just supply a URLEncodingEvaluator for example. + * + * @param expression + * @param stack + * @param evaluator The parsed Value evaluator (could be null). + * @return the parsed (and possibly evaluated) variable String. + */ + public static String translateVariables(String expression, ValueStack stack, ParsedValueEvaluator evaluator) { + return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, evaluator).toString(); + } + + /** + * Converts all instances of ${...} in <code>expression</code> to the value returned + * by a call to {@link ValueStack#findValue(java.lang.String)}. If an item cannot + * be found on the stack (null is returned), then the entire variable ${...} is not + * displayed, just as if the item was on the stack but returned an empty string. + * + * @param open + * @param expression + * @param stack + * @return Translated variable String + */ + public static String translateVariables(char open, String expression, ValueStack stack) { + return translateVariables(open, expression, stack, String.class, null).toString(); + } + + /** + * Converted object from variable translation. + * + * @param open + * @param expression + * @param stack + * @param asType + * @return Converted object from variable translation. + */ + public static Object translateVariables(char open, String expression, ValueStack stack, Class asType) { + return translateVariables(open, expression, stack, asType, null); + } + + /** + * Converted object from variable translation. + * + * @param open + * @param expression + * @param stack + * @param asType + * @param evaluator + * @return Converted object from variable translation. + */ + public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) { + return translateVariables(new char[]{open} , expression, stack, asType, evaluator, MAX_RECURSION); + } + + /** + * Converted object from variable translation. + * + * @param openChars + * @param expression + * @param stack + * @param asType + * @param evaluator + * @return Converted object from variable translation. + */ + public static Object translateVariables(char[] openChars, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) { + return translateVariables(openChars, expression, stack, asType, evaluator, MAX_RECURSION); + } + + /** + * Converted object from variable translation. + * + * @param open + * @param expression + * @param stack + * @param asType + * @param evaluator + * @return Converted object from variable translation. + */ + public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) { + return translateVariables(new char[]{open}, expression, stack, asType, evaluator, maxLoopCount); + } + + /** + * Converted object from variable translation. + * + * @param openChars + * @param expression + * @param stack + * @param asType + * @param evaluator + * @return Converted object from variable translation. + */ + public static Object translateVariables(char[] openChars, String expression, final ValueStack stack, final Class asType, final ParsedValueEvaluator evaluator, int maxLoopCount) { + + ParsedValueEvaluator ognlEval = new ParsedValueEvaluator() { + public Object evaluate(String parsedValue) { + Object o = stack.findValue(parsedValue, asType); + if (evaluator != null && o != null) { + o = evaluator.evaluate(o.toString()); + } + return o; + } + }; + + TextParser parser = ((Container)stack.getContext().get(ActionContext.CONTAINER)).getInstance(TextParser.class); + + return parser.evaluate(openChars, expression, ognlEval, maxLoopCount); + } + + /** + * @see #translateVariablesCollection(char[], String, ValueStack, boolean, ParsedValueEvaluator, int) + * + * @param expression + * @param stack + * @param excludeEmptyElements + * @param evaluator + * @return + */ + public static Collection<String> translateVariablesCollection(String expression, ValueStack stack, boolean excludeEmptyElements, ParsedValueEvaluator evaluator) { + return translateVariablesCollection(new char[]{'$', '%'}, expression, stack, excludeEmptyElements, evaluator, MAX_RECURSION); + } + + /** + * Resolves given expression on given ValueStack. If found element is a + * collection each element will be converted to String. If just a single + * object is found it is converted to String and wrapped in a collection. + * + * @param openChars + * @param expression + * @param stack + * @param excludeEmptyElements + * @param evaluator + * @param maxLoopCount + * @return + */ + public static Collection<String> translateVariablesCollection( + char[] openChars, String expression, final ValueStack stack, boolean excludeEmptyElements, + final ParsedValueEvaluator evaluator, int maxLoopCount) { + + ParsedValueEvaluator ognlEval = new ParsedValueEvaluator() { + public Object evaluate(String parsedValue) { + return stack.findValue(parsedValue); // no asType !!! + } + }; + + Map<String, Object> context = stack.getContext(); + TextParser parser = ((Container)context.get(ActionContext.CONTAINER)).getInstance(TextParser.class); + + Object result = parser.evaluate(openChars, expression, ognlEval, maxLoopCount); + + Collection<String> resultCol; + if (result instanceof Collection) { + @SuppressWarnings("unchecked") + Collection<Object> casted = (Collection<Object>)result; + resultCol = new ArrayList<>(); + + XWorkConverter conv = ((Container)context.get(ActionContext.CONTAINER)).getInstance(XWorkConverter.class); + + for (Object element : casted) { + String stringElement = (String)conv.convertValue(context, element, String.class); + if (shallBeIncluded(stringElement, excludeEmptyElements)) { + if (evaluator != null) { + stringElement = evaluator.evaluate(stringElement).toString(); + } + resultCol.add(stringElement); + } + } + } else { + resultCol = new ArrayList<>(); + String resultStr = translateVariables(expression, stack, evaluator); + if (shallBeIncluded(resultStr, excludeEmptyElements)) { + resultCol.add(resultStr); + } + } + + return resultCol; + } + + /** + * Tests if given string is not null and not empty when excluding of empty + * elements is requested. + * + * @param str String to check. + * @param excludeEmptyElements Whether empty elements shall be excluded. + * @return True if given string can be included in collection. + */ + private static boolean shallBeIncluded(String str, boolean excludeEmptyElements) { + return !excludeEmptyElements || ((str != null) && (str.length() > 0)); + } + + /** + * Returns a set from comma delimted Strings. + * @param s The String to parse. + * @return A set from comma delimted Strings. + */ + public static Set<String> commaDelimitedStringToSet(String s) { + Set<String> set = new HashSet<>(); + String[] split = s.split(","); + for (String aSplit : split) { + String trimmed = aSplit.trim(); + if (trimmed.length() > 0) + set.add(trimmed); + } + return set; + } + + + /** + * A parsed value evaluator for {@link TextParseUtil}. It could be supplied by + * calling {@link TextParseUtil#translateVariables(char, String, ValueStack, Class, ParsedValueEvaluator)}. + * + * <p/> + * + * By supplying this <code>ParsedValueEvaluator</code>, the parsed value + * (parsed against the value stack) value will be + * given to <code>ParsedValueEvaluator</code> to be evaluated before the + * translateVariable process goes on. + * + * <p/> + * + * A typical use-case would be to have a custom <code>ParseValueEvaluator</code> + * to URL Encode the parsed value. + * + * @author tm_jee + * + * @version $Date$ $Id$ + */ + public static interface ParsedValueEvaluator { + + /** + * Evaluated the value parsed by Ognl value stack. + * + * @param parsedValue - value parsed by ognl value stack + * @return return the evaluted value. + */ + Object evaluate(String parsedValue); + } +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/TextParser.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/util/TextParser.java b/core/src/main/java/com/opensymphony/xwork2/util/TextParser.java new file mode 100644 index 0000000..54b18ef --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/TextParser.java @@ -0,0 +1,11 @@ +package com.opensymphony.xwork2.util; + +/** + * Used to parse expressions like ${foo.bar} or %{bar.foo} but it is up tp the TextParser's + * implementation what kind of opening char to use (#, $, %, etc) + */ +public interface TextParser { + + Object evaluate(char[] openChars, String expression, TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount); + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/URLUtil.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/util/URLUtil.java b/core/src/main/java/com/opensymphony/xwork2/util/URLUtil.java new file mode 100644 index 0000000..006fd5e --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/URLUtil.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2003,2009 The Apache Software Foundation. + * + * 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 com.opensymphony.xwork2.util; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * Helper class to extract file paths from different urls + */ +public class URLUtil { + + private static final Logger LOG = LogManager.getLogger(URLUtil.class); + + /** + * Verify That the given String is in valid URL format. + * @param url The url string to verify. + * @return a boolean indicating whether the URL seems to be incorrect. + */ + @Deprecated + public static boolean verifyUrl(String url) { + LOG.debug("Checking if url [{}] is valid", url); + if (url == null) { + return false; + } + + if (url.startsWith("https://")) { + // URL doesn't understand the https protocol, hack it + url = "http://" + url.substring(8); + } + + try { + URL u = new URL(url); + URI uri = u.toURI(); // perform a additional url syntax check + if (uri.getHost() == null) { + LOG.debug("Url [{}] does not contains a valid host: {}", url, uri); + return false; + } + return true; + } catch (MalformedURLException | URISyntaxException e) { + LOG.debug("Url [{}] is invalid: {}", url, e.getMessage(), e); + return false; + } + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/ValueStack.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/util/ValueStack.java b/core/src/main/java/com/opensymphony/xwork2/util/ValueStack.java new file mode 100644 index 0000000..b59bcee --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/ValueStack.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2007,2009 The Apache Software Foundation. + * + * 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 com.opensymphony.xwork2.util; + +import java.util.Map; + +/** + * ValueStack allows multiple beans to be pushed in and dynamic EL expressions to be evaluated against it. When + * evaluating an expression, the stack will be searched down the stack, from the latest objects pushed in to the + * earliest, looking for a bean with a getter or setter for the given property or a method of the given name (depending + * on the expression being evaluated). + */ +public interface ValueStack { + + public static final String VALUE_STACK = "com.opensymphony.xwork2.util.ValueStack.ValueStack"; + + public static final String REPORT_ERRORS_ON_NO_PROP = "com.opensymphony.xwork2.util.ValueStack.ReportErrorsOnNoProp"; + + /** + * Gets the context for this value stack. The context holds all the information in the value stack and it's surroundings. + * + * @return the context. + */ + public abstract Map<String, Object> getContext(); + + /** + * Sets the default type to convert to if no type is provided when getting a value. + * + * @param defaultType the new default type + */ + public abstract void setDefaultType(Class defaultType); + + /** + * Set a override map containing <code>key -> values</code> that takes precedent when doing find operations on the ValueStack. + * <p/> + * See the unit test for ValueStackTest for examples. + * + * @param overrides overrides map. + */ + public abstract void setExprOverrides(Map<Object, Object> overrides); + + /** + * Gets the override map if anyone exists. + * + * @return the override map, <tt>null</tt> if not set. + */ + public abstract Map<Object, Object> getExprOverrides(); + + /** + * Get the CompoundRoot which holds the objects pushed onto the stack + * + * @return the root + */ + public abstract CompoundRoot getRoot(); + + /** + * Attempts to set a property on a bean in the stack with the given expression using the default search order. + * + * @param expr the expression defining the path to the property to be set. + * @param value the value to be set into the named property + */ + public abstract void setValue(String expr, Object value); + + /** + * Attempts to set a property on a bean in the stack with the given expression using the default search order. + * N.B.: unlike #setValue(String,Object) it doesn't allow eval expression. + * @param expr the expression defining the path to the property to be set. + * @param value the value to be set into the named property + */ + void setParameter(String expr, Object value); + + /** + * Attempts to set a property on a bean in the stack with the given expression using the default search order. + * + * @param expr the expression defining the path to the property to be set. + * @param value the value to be set into the named property + * @param throwExceptionOnFailure a flag to tell whether an exception should be thrown if there is no property with + * the given name. + */ + public abstract void setValue(String expr, Object value, boolean throwExceptionOnFailure); + + public abstract String findString(String expr); + public abstract String findString(String expr, boolean throwExceptionOnFailure); + + /** + * Find a value by evaluating the given expression against the stack in the default search order. + * + * @param expr the expression giving the path of properties to navigate to find the property value to return + * @return the result of evaluating the expression + */ + public abstract Object findValue(String expr); + + public abstract Object findValue(String expr, boolean throwExceptionOnFailure); + + /** + * Find a value by evaluating the given expression against the stack in the default search order. + * + * @param expr the expression giving the path of properties to navigate to find the property value to return + * @param asType the type to convert the return value to + * @return the result of evaluating the expression + */ + public abstract Object findValue(String expr, Class asType); + public abstract Object findValue(String expr, Class asType, boolean throwExceptionOnFailure); + + /** + * Get the object on the top of the stack <b>without</b> changing the stack. + * + * @return the object on the top. + * @see CompoundRoot#peek() + */ + public abstract Object peek(); + + /** + * Get the object on the top of the stack and <b>remove</b> it from the stack. + * + * @return the object on the top of the stack + * @see CompoundRoot#pop() + */ + public abstract Object pop(); + + /** + * Put this object onto the top of the stack + * + * @param o the object to be pushed onto the stack + * @see CompoundRoot#push(Object) + */ + public abstract void push(Object o); + + /** + * Sets an object on the stack with the given key + * so it is retrievable by {@link #findValue(String)}, {@link #findValue(String, Class)} + * + * @param key the key + * @param o the object + */ + public abstract void set(String key, Object o); + + /** + * Get the number of objects in the stack + * + * @return the number of objects in the stack + */ + public abstract int size(); + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/ValueStackFactory.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/util/ValueStackFactory.java b/core/src/main/java/com/opensymphony/xwork2/util/ValueStackFactory.java new file mode 100644 index 0000000..aa8256d --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/ValueStackFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2007,2009 The Apache Software Foundation. + * + * 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 com.opensymphony.xwork2.util; + +/** + * Factory that creates a value stack, defaulting to the OgnlValueStackFactory + */ +public interface ValueStackFactory { + + /** + * Get a new instance of {@link com.opensymphony.xwork2.util.ValueStack} + * + * @return a new {@link com.opensymphony.xwork2.util.ValueStack}. + */ + ValueStack createValueStack(); + + /** + * Get a new instance of {@link com.opensymphony.xwork2.util.ValueStack} + * + * @param stack an existing stack to include. + * @return a new {@link com.opensymphony.xwork2.util.ValueStack}. + */ + ValueStack createValueStack(ValueStack stack); + +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/WildcardHelper.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/util/WildcardHelper.java b/core/src/main/java/com/opensymphony/xwork2/util/WildcardHelper.java new file mode 100644 index 0000000..86a3b9a --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/WildcardHelper.java @@ -0,0 +1,463 @@ +/* + * $Id$ + * + * Copyright 2003-2004 The Apache Software Foundation. + * + * 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 com.opensymphony.xwork2.util; + +import java.util.Map; + +/** + * This class is an utility class that perform wilcard-patterns matching and + * isolation taken from Apache Cocoon. + * + * @version $Rev$ $Date: 2005-05-07 12:11:38 -0400 (Sat, 07 May 2005) + * $ + */ +public class WildcardHelper implements PatternMatcher<int[]> { + /** + * 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; + + /** + * Determines if the pattern contains any * characters + * + * @param pattern The pattern + * @return True if no wildcards are found + */ + public boolean isLiteral(String pattern) { + return (pattern == null || pattern.indexOf('*') == -1); + } + + /** + * <p> 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:</p> + * + * <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> + * + * <p>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.</p> + * + * @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). + * @throws NullPointerException If data is null. + */ + public int[] compilePattern(String data) { + // 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 boolean match(Map<String, String> map, String data, int[] expr) { + 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 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 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 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); + } +} http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/WildcardUtil.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/util/WildcardUtil.java b/core/src/main/java/com/opensymphony/xwork2/util/WildcardUtil.java new file mode 100644 index 0000000..8e6a573 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/WildcardUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright 2010 Yahoo, Inc. All rights reserved. + * + * 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 com.opensymphony.xwork2.util; + +import java.util.regex.Pattern; + +/** + * Helper class to convert wildcard expression to regular expression + */ +public class WildcardUtil { + + /** + * Convert wildcard pattern to Pattern + * @param pattern String containing wildcard pattern + * @return compiled regular expression as a Pattern + */ + public static Pattern compileWildcardPattern(String pattern) { + StringBuilder buf = new StringBuilder(pattern); + + for (int i=buf.length()-1; i>=0; i--) + { + char c = buf.charAt(i); + if (c == '*' && (i == 0 || buf.charAt(i-1) != '\\')) + { + buf.insert(i+1, '?'); + buf.insert(i, '.'); + } + else if (c == '*') + { + i--; // skip backslash, too + } + else if (needsBackslashToBeLiteralInRegex(c)) + { + buf.insert(i, '\\'); + } + } + + return Pattern.compile(buf.toString()); + } + + /** + * @param c character to test + * @return true if the given character must be escaped to be a literal + * inside a regular expression. + */ + + private static final String theSpecialRegexCharList = ".[]\\?*+{}|()^$"; + + public static boolean needsBackslashToBeLiteralInRegex( + char c) + { + return (theSpecialRegexCharList.indexOf(c) >= 0); + } + +}
