You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2015/06/17 23:09:36 UTC

[36/57] [partial] struts git commit: Merges xwork packages into struts

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/ClassPathFinder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/ClassPathFinder.java b/core/src/main/java/com/opensymphony/xwork2/util/ClassPathFinder.java
new file mode 100644
index 0000000..5743885
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/ClassPathFinder.java
@@ -0,0 +1,177 @@
+/*
+ * $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 com.opensymphony.xwork2.XWorkException;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.HashMap;
+import java.util.Vector;
+
+/**
+ * This class is an utility class that will search through the classpath
+ * for files whose names match the given pattern. The filename is tested
+ * using the given implementation of {@link com.opensymphony.xwork2.util.PatternMatcher} by default it 
+ * uses {@link com.opensymphony.xwork2.util.WildcardHelper}
+ *
+ * @version $Rev$ $Date$
+ */
+public class ClassPathFinder {
+	
+	/**
+     * The String pattern to test against.
+     */
+	private String pattern ;
+	
+	private int[] compiledPattern ;
+	
+	/**
+     * The PatternMatcher implementation to use
+     */
+	private PatternMatcher<int[]> patternMatcher = new WildcardHelper();
+
+	private Vector<String> compared = new Vector<>();
+	
+	/**
+	 * retrieves the pattern in use
+	 */
+	public String getPattern() {
+		return pattern;
+	}
+
+	/**
+	 * sets the String pattern for comparing filenames
+	 * @param pattern
+	 */
+	public void setPattern(String pattern) {
+		this.pattern = pattern;
+	}
+
+	/**
+     * Builds a {@link java.util.Vector} containing Strings which each name a file
+     * who's name matches the pattern set by setPattern(String). The classpath is 
+     * searched recursively, so use with caution.
+     *
+     * @return Vector<String> containing matching filenames
+     */
+	public Vector<String> findMatches() {
+		Vector<String> matches = new Vector<>();
+		URLClassLoader cl = getURLClassLoader();
+		if (cl == null ) {
+			throw new XWorkException("unable to attain an URLClassLoader") ;
+		}
+		URL[] parentUrls = cl.getURLs();
+		compiledPattern = patternMatcher.compilePattern(pattern);
+		for (URL url : parentUrls) {
+			if (!"file".equals(url.getProtocol())) {
+				continue ;
+			}
+			URI entryURI ;
+			try {
+				entryURI = url.toURI();
+			} catch (URISyntaxException e) {
+				continue;
+			}
+			File entry = new File(entryURI) ;
+			Vector<String> results = checkEntries(entry.list(), entry, "");
+			if (results != null ) {
+				matches.addAll(results);
+			}
+		}
+		return matches;
+	}
+	
+	private Vector<String> checkEntries(String[] entries, File parent, String prefix) {
+		
+		if (entries == null ) {
+			return null;
+		}
+
+		Vector<String> matches = new Vector<>();
+		for (String listEntry : entries) {
+			File tempFile ;
+			if (!"".equals(prefix) ) {
+				tempFile = new File(parent, prefix + "/" + listEntry);
+			}
+			else {
+				tempFile = new File(parent, listEntry);
+			}
+			if (tempFile.isDirectory() && 
+					!(".".equals(listEntry) || "..".equals(listEntry)) ) {
+				if	(!"".equals(prefix) ) {
+					matches.addAll(checkEntries(tempFile.list(), parent, prefix + "/" + listEntry));
+				}
+				else {
+					matches.addAll(checkEntries(tempFile.list(), parent, listEntry));
+				}
+			}
+			else {
+				
+				String entryToCheck ;
+				if ("".equals(prefix)) {
+					entryToCheck = listEntry ;
+				}
+				else {
+					entryToCheck = prefix + "/" + listEntry ;
+				}
+				
+				if (compared.contains(entryToCheck) ) {
+					continue;
+				}
+				else {
+					compared.add(entryToCheck) ;
+				}
+				
+				boolean doesMatch = patternMatcher.match(new HashMap<String,String>(), entryToCheck, compiledPattern);
+				if (doesMatch) {
+					matches.add(entryToCheck);
+				}
+			}
+		}
+		return matches ;
+	}
+
+	/**
+	 * sets the PatternMatcher implementation to use when comparing filenames
+	 * @param patternMatcher
+	 */
+	public void setPatternMatcher(PatternMatcher<int[]> patternMatcher) {
+		this.patternMatcher = patternMatcher;
+	}
+
+	private URLClassLoader getURLClassLoader() {
+		URLClassLoader ucl = null;
+		ClassLoader loader = Thread.currentThread().getContextClassLoader();
+		
+		if(! (loader instanceof URLClassLoader)) {
+			loader = ClassPathFinder.class.getClassLoader();
+			if (loader instanceof URLClassLoader) {
+				ucl = (URLClassLoader) loader ;
+			}
+		}
+		else {
+			ucl = (URLClassLoader) loader;
+		}
+		
+		return ucl ;
+	}
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/ClearableValueStack.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/ClearableValueStack.java b/core/src/main/java/com/opensymphony/xwork2/util/ClearableValueStack.java
new file mode 100644
index 0000000..e4d129e
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/ClearableValueStack.java
@@ -0,0 +1,29 @@
+/*
+ * $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;
+
+/**
+ * ValueStacks implementing this interface provide a way to remove values from
+ * their contexts.
+ */
+public interface ClearableValueStack {
+    /**
+     * Remove all values from the context
+     */
+    void clearContextValues();
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/CompoundRoot.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/CompoundRoot.java b/core/src/main/java/com/opensymphony/xwork2/util/CompoundRoot.java
new file mode 100644
index 0000000..9abade0
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/CompoundRoot.java
@@ -0,0 +1,53 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * A Stack that is implemented using a List.
+ * 
+ * @author plightbo
+ * @version $Revision$
+ */
+public class CompoundRoot extends ArrayList {
+
+    public CompoundRoot() {
+    }
+
+    public CompoundRoot(List list) {
+        super(list);
+    }
+
+
+    public CompoundRoot cutStack(int index) {
+        return new CompoundRoot(subList(index, size()));
+    }
+
+    public Object peek() {
+        return get(0);
+    }
+
+    public Object pop() {
+        return remove(0);
+    }
+
+    public void push(Object o) {
+        add(0, o);
+    }
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/CreateIfNull.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/CreateIfNull.java b/core/src/main/java/com/opensymphony/xwork2/util/CreateIfNull.java
new file mode 100644
index 0000000..050ec2a
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/CreateIfNull.java
@@ -0,0 +1,77 @@
+/*
+ * 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 java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <!-- START SNIPPET: description -->
+ * <p/>Sets the CreateIfNull for type conversion.
+ * <!-- END SNIPPET: description -->
+ *
+ * <p/> <u>Annotation usage:</u>
+ *
+ * <!-- START SNIPPET: usage -->
+ * <p/>The CreateIfNull annotation must be applied at field or method level.
+ * <!-- END SNIPPET: usage -->
+ * <p/> <u>Annotation parameters:</u>
+ *
+ * <!-- START SNIPPET: parameters -->
+ * <table>
+ * <thead>
+ * <tr>
+ * <th>Parameter</th>
+ * <th>Required</th>
+ * <th>Default</th>
+ * <th>Description</th>
+ * </tr>
+ * </thead>
+ * <tbody>
+ * <tr>
+ * <td>value</td>
+ * <td>no</td>
+ * <td>false</td>
+ * <td>The CreateIfNull property value.</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ * <!-- END SNIPPET: parameters -->
+ *
+ * <p/> <u>Example code:</u>
+ * <pre>
+ * <!-- START SNIPPET: example -->
+ * &#64;CreateIfNull( value = true )
+ * private List<User> users;
+ * <!-- END SNIPPET: example -->
+ * </pre>
+ *
+ * @author Rainer Hermanns
+ * @version $Id$
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.METHOD})
+public @interface CreateIfNull {
+
+    /**
+     * The CreateIfNull value.
+     * Defaults to <tt>true</tt>.
+     */
+    boolean value() default true;
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/DomHelper.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/DomHelper.java b/core/src/main/java/com/opensymphony/xwork2/util/DomHelper.java
new file mode 100644
index 0000000..47d86e1
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/DomHelper.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright 1999-2005 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.ObjectFactory;
+import com.opensymphony.xwork2.XWorkException;
+import com.opensymphony.xwork2.util.location.Location;
+import com.opensymphony.xwork2.util.location.LocationAttributes;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.xml.sax.*;
+import org.xml.sax.helpers.DefaultHandler;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TransformerHandler;
+import java.util.Map;
+
+/**
+ * Helper class to create and retrieve information from location-enabled
+ * DOM-trees.
+ *
+ * @since 1.2
+ */
+public class DomHelper {
+
+    private static final Logger LOG = LogManager.getLogger(DomHelper.class);
+    
+    public static final String XMLNS_URI = "http://www.w3.org/2000/xmlns/";
+
+    public static Location getLocationObject(Element element) {
+        return LocationAttributes.getLocation(element);
+    }
+
+    
+    /**
+     * Creates a W3C Document that remembers the location of each element in
+     * the source file. The location of element nodes can then be retrieved
+     * using the {@link #getLocationObject(Element)} method.
+     *
+     * @param inputSource the inputSource to read the document from
+     */
+    public static Document parse(InputSource inputSource) {
+        return parse(inputSource, null);
+    }
+    
+    
+    /**
+     * Creates a W3C Document that remembers the location of each element in
+     * the source file. The location of element nodes can then be retrieved
+     * using the {@link #getLocationObject(Element)} method.
+     *
+     * @param inputSource the inputSource to read the document from
+     * @param dtdMappings a map of DTD names and public ids
+     */
+    public static Document parse(InputSource inputSource, Map<String, String> dtdMappings) {
+                
+        SAXParserFactory factory = null;
+        String parserProp = System.getProperty("xwork.saxParserFactory");
+        if (parserProp != null) {
+            try {
+                Class clazz = ObjectFactory.getObjectFactory().getClassInstance(parserProp);
+                factory = (SAXParserFactory) clazz.newInstance();
+            } catch (Exception e) {
+                LOG.error("Unable to load saxParserFactory set by system property 'xwork.saxParserFactory': {}", parserProp, e);
+            }
+        }
+
+        if (factory == null) {
+            factory = SAXParserFactory.newInstance();
+        }
+
+        factory.setValidating((dtdMappings != null));
+        factory.setNamespaceAware(true);
+
+        SAXParser parser;
+        try {
+            parser = factory.newSAXParser();
+        } catch (Exception ex) {
+            throw new XWorkException("Unable to create SAX parser", ex);
+        }
+        
+        
+        DOMBuilder builder = new DOMBuilder();
+
+        // Enhance the sax stream with location information
+        ContentHandler locationHandler = new LocationAttributes.Pipe(builder);
+        
+        try {
+            parser.parse(inputSource, new StartHandler(locationHandler, dtdMappings));
+        } catch (Exception ex) {
+            throw new XWorkException(ex);
+        }
+        
+        return builder.getDocument();
+    }
+    
+    /**
+     * The <code>DOMBuilder</code> is a utility class that will generate a W3C
+     * DOM Document from SAX events.
+     *
+     * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a>
+     */
+    static public class DOMBuilder implements ContentHandler {
+    
+        /** The default transformer factory shared by all instances */
+        protected static SAXTransformerFactory FACTORY;
+    
+        /** The transformer factory */
+        protected SAXTransformerFactory factory;
+    
+        /** The result */
+        protected DOMResult result;
+    
+        /** The parentNode */
+        protected Node parentNode;
+        
+        protected ContentHandler nextHandler;
+    
+        static {
+            String parserProp = System.getProperty("xwork.saxTransformerFactory");
+            if (parserProp != null) {
+                try {
+                    Class clazz = ObjectFactory.getObjectFactory().getClassInstance(parserProp);
+                    FACTORY = (SAXTransformerFactory) clazz.newInstance();
+                } catch (Exception e) {
+                    LOG.error("Unable to load SAXTransformerFactory set by system property 'xwork.saxTransformerFactory': {}", parserProp, e);
+                }
+            }
+
+            if (FACTORY == null) {
+                 FACTORY = (SAXTransformerFactory) TransformerFactory.newInstance();
+            }
+        }
+
+        /**
+         * Construct a new instance of this DOMBuilder.
+         */
+        public DOMBuilder() {
+            this((Node) null);
+        }
+    
+        /**
+         * Construct a new instance of this DOMBuilder.
+         */
+        public DOMBuilder(SAXTransformerFactory factory) {
+            this(factory, null);
+        }
+    
+        /**
+         * Constructs a new instance that appends nodes to the given parent node.
+         */
+        public DOMBuilder(Node parentNode) {
+            this(null, parentNode);
+        }
+    
+        /**
+         * Construct a new instance of this DOMBuilder.
+         */
+        public DOMBuilder(SAXTransformerFactory factory, Node parentNode) {
+            this.factory = factory == null? FACTORY: factory;
+            this.parentNode = parentNode;
+            setup();
+        }
+    
+        /**
+         * Setup this instance transformer and result objects.
+         */
+        private void setup() {
+            try {
+                TransformerHandler handler = this.factory.newTransformerHandler();
+                nextHandler = handler;
+                if (this.parentNode != null) {
+                    this.result = new DOMResult(this.parentNode);
+                } else {
+                    this.result = new DOMResult();
+                }
+                handler.setResult(this.result);
+            } catch (javax.xml.transform.TransformerException local) {
+                throw new XWorkException("Fatal-Error: Unable to get transformer handler", local);
+            }
+        }
+    
+        /**
+         * Return the newly built Document.
+         */
+        public Document getDocument() {
+            if (this.result == null || this.result.getNode() == null) {
+                return null;
+            } else if (this.result.getNode().getNodeType() == Node.DOCUMENT_NODE) {
+                return (Document) this.result.getNode();
+            } else {
+                return this.result.getNode().getOwnerDocument();
+            }
+        }
+    
+        public void setDocumentLocator(Locator locator) {
+            nextHandler.setDocumentLocator(locator);
+        }
+        
+        public void startDocument() throws SAXException {
+            nextHandler.startDocument();
+        }
+        
+        public void endDocument() throws SAXException {
+            nextHandler.endDocument();
+        }
+    
+        public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException {
+            nextHandler.startElement(uri, loc, raw, attrs);
+        }
+    
+        public void endElement(String arg0, String arg1, String arg2) throws SAXException {
+            nextHandler.endElement(arg0, arg1, arg2);
+        }
+    
+        public void startPrefixMapping(String arg0, String arg1) throws SAXException {
+            nextHandler.startPrefixMapping(arg0, arg1);
+        }
+    
+        public void endPrefixMapping(String arg0) throws SAXException {
+            nextHandler.endPrefixMapping(arg0);
+        }
+    
+        public void characters(char[] arg0, int arg1, int arg2) throws SAXException {
+            nextHandler.characters(arg0, arg1, arg2);
+        }
+    
+        public void ignorableWhitespace(char[] arg0, int arg1, int arg2) throws SAXException {
+            nextHandler.ignorableWhitespace(arg0, arg1, arg2);
+        }
+    
+        public void processingInstruction(String arg0, String arg1) throws SAXException {
+            nextHandler.processingInstruction(arg0, arg1);
+        }
+    
+        public void skippedEntity(String arg0) throws SAXException {
+            nextHandler.skippedEntity(arg0);
+        }
+    }
+    
+    public static class StartHandler extends DefaultHandler {
+        
+        private ContentHandler nextHandler;
+        private Map<String, String> dtdMappings;
+        
+        /**
+         * Create a filter that is chained to another handler.
+         * @param next the next handler in the chain.
+         */
+        public StartHandler(ContentHandler next, Map<String, String> dtdMappings) {
+            nextHandler = next;
+            this.dtdMappings = dtdMappings;
+        }
+
+        @Override
+        public void setDocumentLocator(Locator locator) {
+            nextHandler.setDocumentLocator(locator);
+        }
+        
+        @Override
+        public void startDocument() throws SAXException {
+            nextHandler.startDocument();
+        }
+        
+        @Override
+        public void endDocument() throws SAXException {
+            nextHandler.endDocument();
+        }
+
+        @Override
+        public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException {
+            nextHandler.startElement(uri, loc, raw, attrs);
+        }
+
+        @Override
+        public void endElement(String arg0, String arg1, String arg2) throws SAXException {
+            nextHandler.endElement(arg0, arg1, arg2);
+        }
+
+        @Override
+        public void startPrefixMapping(String arg0, String arg1) throws SAXException {
+            nextHandler.startPrefixMapping(arg0, arg1);
+        }
+
+        @Override
+        public void endPrefixMapping(String arg0) throws SAXException {
+            nextHandler.endPrefixMapping(arg0);
+        }
+
+        @Override
+        public void characters(char[] arg0, int arg1, int arg2) throws SAXException {
+            nextHandler.characters(arg0, arg1, arg2);
+        }
+
+        @Override
+        public void ignorableWhitespace(char[] arg0, int arg1, int arg2) throws SAXException {
+            nextHandler.ignorableWhitespace(arg0, arg1, arg2);
+        }
+
+        @Override
+        public void processingInstruction(String arg0, String arg1) throws SAXException {
+            nextHandler.processingInstruction(arg0, arg1);
+        }
+
+        @Override
+        public void skippedEntity(String arg0) throws SAXException {
+            nextHandler.skippedEntity(arg0);
+        }
+        
+        @Override
+        public InputSource resolveEntity(String publicId, String systemId) {
+            if (dtdMappings != null && dtdMappings.containsKey(publicId)) {
+                String dtdFile = dtdMappings.get(publicId);
+                return new InputSource(ClassLoaderUtil.getResourceAsStream(dtdFile, DomHelper.class));
+            } else {
+                LOG.warn("Local DTD is missing for publicID: {} - defined mappings: {}", publicId, dtdMappings);
+            }
+            return null;
+        }
+        
+        @Override
+        public void warning(SAXParseException exception) {
+        }
+
+        @Override
+        public void error(SAXParseException exception) throws SAXException {
+            LOG.error("{} at ({}:{}:{})", exception.getMessage(), exception.getPublicId(), exception.getLineNumber(), exception.getColumnNumber(), exception);
+            throw exception;
+        }
+
+        @Override
+        public void fatalError(SAXParseException exception) throws SAXException {
+            LOG.fatal("{} at ({}:{}:{})", exception.getMessage(), exception.getPublicId(), exception.getLineNumber(), exception.getColumnNumber(), exception);
+            throw exception;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/Element.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/Element.java b/core/src/main/java/com/opensymphony/xwork2/util/Element.java
new file mode 100644
index 0000000..30903d2
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/Element.java
@@ -0,0 +1,81 @@
+/*
+ * 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 java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <!-- START SNIPPET: description -->
+ * <p/>Sets the Element for type conversion.
+ * <!-- END SNIPPET: description -->
+ *
+ * <p/> <u>Annotation usage:</u>
+ *
+ * <!-- START SNIPPET: usage -->
+ * <p/>The Element annotation must be applied at field or method level.
+ * <!-- END SNIPPET: usage -->
+ * <p/> <u>Annotation parameters:</u>
+ *
+ * <!-- START SNIPPET: parameters -->
+ * <table>
+ * <thead>
+ * <tr>
+ * <th>Parameter</th>
+ * <th>Required</th>
+ * <th>Default</th>
+ * <th>Description</th>
+ * </tr>
+ * </thead>
+ * <tbody>
+ * <tr>
+ * <td>value</td>
+ * <td>no</td>
+ * <td>java.lang.Object.class</td>
+ * <td>The element property value.</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ * <!-- END SNIPPET: parameters -->
+ *
+ * <p/> <u>Example code:</u>
+ * <pre>
+ * <!-- START SNIPPET: example -->
+ * // The key property for User objects within the users collection is the <code>userName</code> attribute.
+ * &#64;Element( value = com.acme.User )
+ * private Map<Long, User> userMap;
+ *
+ * &#64;Element( value = com.acme.User )
+ * public List<User> userList;
+ * <!-- END SNIPPET: example -->
+ * </pre>
+ *
+ * @author Rainer Hermanns
+ * @version $Id$
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.METHOD})
+public @interface Element {
+
+    /**
+     * The Element value.
+     * Defaults to <tt>java.lang.Object.class</tt>.
+     */
+    Class value() default java.lang.Object.class;
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/Key.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/Key.java b/core/src/main/java/com/opensymphony/xwork2/util/Key.java
new file mode 100644
index 0000000..c1b0fc8
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/Key.java
@@ -0,0 +1,78 @@
+/*
+ * 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 java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <!-- START SNIPPET: description -->
+ * <p/>Sets the Key for type conversion.
+ * <!-- END SNIPPET: description -->
+ *
+ * <p/> <u>Annotation usage:</u>
+ *
+ * <!-- START SNIPPET: usage -->
+ * <p/>The Key annotation must be applied at field or method level.
+ * <!-- END SNIPPET: usage -->
+ * <p/> <u>Annotation parameters:</u>
+ *
+ * <!-- START SNIPPET: parameters -->
+ * <table>
+ * <thead>
+ * <tr>
+ * <th>Parameter</th>
+ * <th>Required</th>
+ * <th>Default</th>
+ * <th>Description</th>
+ * </tr>
+ * </thead>
+ * <tbody>
+ * <tr>
+ * <td>value</td>
+ * <td>no</td>
+ * <td>java.lang.Object.class</td>
+ * <td>The key property value.</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ * <!-- END SNIPPET: parameters -->
+ *
+ * <p/> <u>Example code:</u>
+ * <pre>
+ * <!-- START SNIPPET: example -->
+ * // The key property for User objects within the users collection is the <code>userName</code> attribute.
+ * &#64;Key( value = java.lang.Long.class )
+ * private Map<Long, User> userMap;
+ * <!-- END SNIPPET: example -->
+ * </pre>
+ *
+ * @author Rainer Hermanns
+ * @version $Id$
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.METHOD})
+public @interface Key {
+
+    /**
+     * The Key value.
+     * Defaults to <tt>java.lang.Object.class</tt>.
+     */
+    Class value() default java.lang.Object.class;
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/KeyProperty.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/KeyProperty.java b/core/src/main/java/com/opensymphony/xwork2/util/KeyProperty.java
new file mode 100644
index 0000000..8832bee
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/KeyProperty.java
@@ -0,0 +1,79 @@
+/*
+ * 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 java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <!-- START SNIPPET: description -->
+ * <p/>Sets the KeyProperty for type conversion.
+ * <!-- END SNIPPET: description -->
+ *
+ * <p/> <u>Annotation usage:</u>
+ *
+ * <!-- START SNIPPET: usage -->
+ * <p/>The KeyProperty annotation must be applied at field or method level.
+ * <p/>This annotation should be used with Generic types, if the key property of the key element needs to be specified.
+ * <!-- END SNIPPET: usage -->
+ * <p/> <u>Annotation parameters:</u>
+ *
+ * <!-- START SNIPPET: parameters -->
+ * <table>
+ * <thead>
+ * <tr>
+ * <th>Parameter</th>
+ * <th>Required</th>
+ * <th>Default</th>
+ * <th>Description</th>
+ * </tr>
+ * </thead>
+ * <tbody>
+ * <tr>
+ * <td>value</td>
+ * <td>no</td>
+ * <td>id</td>
+ * <td>The key property value.</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ * <!-- END SNIPPET: parameters -->
+ *
+ * <p/> <u>Example code:</u>
+ * <pre>
+ * <!-- START SNIPPET: example -->
+ * // The key property for User objects within the users collection is the <code>userName</code> attribute.
+ * &#64;KeyProperty( value = "userName" )
+ * protected List<User> users = null;
+ * <!-- END SNIPPET: example -->
+ * </pre>
+ *
+ * @author Patrick Lightbody
+ * @author Rainer Hermanns
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.METHOD})
+public @interface KeyProperty {
+
+    /**
+     * The KeyProperty value.
+     * Defaults to the <tt>id</tt> attribute. 
+     */
+    String value() default "id";
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/LocalizedTextUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/LocalizedTextUtil.java b/core/src/main/java/com/opensymphony/xwork2/util/LocalizedTextUtil.java
new file mode 100644
index 0000000..2d9afa2
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/LocalizedTextUtil.java
@@ -0,0 +1,942 @@
+/*
+ * $Id$
+ *
+ * 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 com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.ModelDriven;
+import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
+import com.opensymphony.xwork2.util.reflection.ReflectionProviderFactory;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+
+/**
+ * Provides support for localization in XWork.
+ * <p/>
+ * <!-- START SNIPPET: searchorder -->
+ * Resource bundles are searched in the following order:<p/>
+ * <p/>
+ * <ol>
+ * <li>ActionClass.properties</li>
+ * <li>Interface.properties (every interface and sub-interface)</li>
+ * <li>BaseClass.properties (all the way to Object.properties)</li>
+ * <li>ModelDriven's model (if implements ModelDriven), for the model object repeat from 1</li>
+ * <li>package.properties (of the directory where class is located and every parent directory all the way to the root directory)</li>
+ * <li>search up the i18n message key hierarchy itself</li>
+ * <li>global resource properties</li>
+ * </ol>
+ * <p/>
+ * <!-- END SNIPPET: searchorder -->
+ * <p/>
+ * <!-- START SNIPPET: packagenote -->
+ * To clarify #5, while traversing the package hierarchy, Struts 2 will look for a file package.properties:<p/>
+ * com/<br/>
+ * &nbsp; acme/<br/>
+ * &nbsp; &nbsp; package.properties<br/>
+ * &nbsp; &nbsp; actions/<br/>
+ * &nbsp; &nbsp; &nbsp; package.properties<br/>
+ * &nbsp; &nbsp; &nbsp; FooAction.java<br/>
+ * &nbsp; &nbsp; &nbsp; FooAction.properties<br/>
+ * <p/>
+ * If FooAction.properties does not exist, com/acme/action/package.properties will be searched for, if
+ * not found com/acme/package.properties, if not found com/package.properties, etc.
+ * <p/>
+ * <!-- END SNIPPET: packagenote -->
+ * <p/>
+ * <!-- START SNIPPET: globalresource -->
+ * A global resource bundle could be specified programatically, as well as the locale.
+ * <p/>
+ * <!-- END SNIPPET: globalresource -->
+ *
+ * @author Jason Carreira
+ * @author Mark Woon
+ * @author Rainer Hermanns
+ * @author tm_jee
+ * @version $Date$ $Id$
+ */
+public class LocalizedTextUtil {
+
+    private static final Logger LOG = LogManager.getLogger(LocalizedTextUtil.class);
+
+    private static final String TOMCAT_RESOURCE_ENTRIES_FIELD = "resourceEntries";
+
+    private static final ConcurrentMap<Integer, List<String>> classLoaderMap = new ConcurrentHashMap<>();
+
+    private static boolean reloadBundles = false;
+    private static boolean devMode;
+
+    private static final ConcurrentMap<String, ResourceBundle> bundlesMap = new ConcurrentHashMap<>();
+    private static final ConcurrentMap<MessageFormatKey, MessageFormat> messageFormats = new ConcurrentHashMap<>();
+    private static final ConcurrentMap<Integer, ClassLoader> delegatedClassLoaderMap = new ConcurrentHashMap<>();
+
+    private static final String RELOADED = "com.opensymphony.xwork2.util.LocalizedTextUtil.reloaded";
+    private static final String XWORK_MESSAGES_BUNDLE = "com/opensymphony/xwork2/xwork-messages";
+
+    static {
+        clearDefaultResourceBundles();
+    }
+
+
+    /**
+     * Clears the internal list of resource bundles.
+     */
+    public static void clearDefaultResourceBundles() {
+        ClassLoader ccl = getCurrentThreadContextClassLoader();
+        List<String> bundles = new ArrayList<>();
+        classLoaderMap.put(ccl.hashCode(), bundles);
+        bundles.add(0, XWORK_MESSAGES_BUNDLE);
+    }
+
+    /**
+     * Should resorce bundles be reloaded.
+     *
+     * @param reloadBundles reload bundles?
+     */
+    public static void setReloadBundles(boolean reloadBundles) {
+        LocalizedTextUtil.reloadBundles = reloadBundles;
+    }
+
+    public static void setDevMode(boolean devMode) {
+        LocalizedTextUtil.devMode = devMode;
+    }
+
+    /**
+     * Add's the bundle to the internal list of default bundles.
+     * <p/>
+     * If the bundle already exists in the list it will be readded.
+     *
+     * @param resourceBundleName the name of the bundle to add.
+     */
+    public static void addDefaultResourceBundle(String resourceBundleName) {
+        //make sure this doesn't get added more than once
+        ClassLoader ccl;
+        synchronized (XWORK_MESSAGES_BUNDLE) {
+            ccl = getCurrentThreadContextClassLoader();
+            List<String> bundles = classLoaderMap.get(ccl.hashCode());
+            if (bundles == null) {
+                bundles = new ArrayList<String>();
+                classLoaderMap.put(ccl.hashCode(), bundles);
+                bundles.add(XWORK_MESSAGES_BUNDLE);
+            }
+            bundles.remove(resourceBundleName);
+            bundles.add(0, resourceBundleName);
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Added default resource bundle '{}' to default resource bundles for the following classloader '{}'", resourceBundleName, ccl.toString());
+        }
+    }
+
+    /**
+     * Builds a {@link java.util.Locale} from a String of the form en_US_foo into a Locale
+     * with language "en", country "US" and variant "foo". This will parse the output of
+     * {@link java.util.Locale#toString()}.
+     *
+     * @param localeStr     The locale String to parse.
+     * @param defaultLocale The locale to use if localeStr is <tt>null</tt>.
+     * @return requested Locale
+     */
+    public static Locale localeFromString(String localeStr, Locale defaultLocale) {
+        if ((localeStr == null) || (localeStr.trim().length() == 0) || ("_".equals(localeStr))) {
+            if (defaultLocale != null) {
+                return defaultLocale;
+            }
+            return Locale.getDefault();
+        }
+
+        int index = localeStr.indexOf('_');
+        if (index < 0) {
+            return new Locale(localeStr);
+        }
+
+        String language = localeStr.substring(0, index);
+        if (index == localeStr.length()) {
+            return new Locale(language);
+        }
+
+        localeStr = localeStr.substring(index + 1);
+        index = localeStr.indexOf('_');
+        if (index < 0) {
+            return new Locale(language, localeStr);
+        }
+
+        String country = localeStr.substring(0, index);
+        if (index == localeStr.length()) {
+            return new Locale(language, country);
+        }
+
+        localeStr = localeStr.substring(index + 1);
+        return new Locale(language, country, localeStr);
+    }
+
+    /**
+     * Returns a localized message for the specified key, aTextName.  Neither the key nor the
+     * message is evaluated.
+     *
+     * @param aTextName the message key
+     * @param locale    the locale the message should be for
+     * @return a localized message based on the specified key, or null if no localized message can be found for it
+     */
+    public static String findDefaultText(String aTextName, Locale locale) {
+        List<String> localList = classLoaderMap.get(Thread.currentThread().getContextClassLoader().hashCode());
+
+        for (String bundleName : localList) {
+            ResourceBundle bundle = findResourceBundle(bundleName, locale);
+            if (bundle != null) {
+                reloadBundles();
+                try {
+                    return bundle.getString(aTextName);
+                } catch (MissingResourceException e) {
+                	// will be logged when not found in any bundle
+                }
+            }
+        }
+
+        if (devMode) {
+            LOG.warn("Missing key [{}] in bundles [{}]!", aTextName, localList);
+        } else {
+            LOG.debug("Missing key [{}] in bundles [{}]!", aTextName, localList);
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns a localized message for the specified key, aTextName, substituting variables from the
+     * array of params into the message.  Neither the key nor the message is evaluated.
+     *
+     * @param aTextName the message key
+     * @param locale    the locale the message should be for
+     * @param params    an array of objects to be substituted into the message text
+     * @return A formatted message based on the specified key, or null if no localized message can be found for it
+     */
+    public static String findDefaultText(String aTextName, Locale locale, Object[] params) {
+        String defaultText = findDefaultText(aTextName, locale);
+        if (defaultText != null) {
+            MessageFormat mf = buildMessageFormat(defaultText, locale);
+            return formatWithNullDetection(mf, params);
+        }
+        return null;
+    }
+
+    /**
+     * Finds the given resorce bundle by it's name.
+     * <p/>
+     * Will use <code>Thread.currentThread().getContextClassLoader()</code> as the classloader.
+     *
+     * @param aBundleName the name of the bundle (usually it's FQN classname).
+     * @param locale      the locale.
+     * @return the bundle, <tt>null</tt> if not found.
+     */
+    public static ResourceBundle findResourceBundle(String aBundleName, Locale locale) {
+
+        ResourceBundle bundle = null;
+
+        ClassLoader classLoader = getCurrentThreadContextClassLoader();
+        String key = createMissesKey(String.valueOf(classLoader.hashCode()), aBundleName, locale);
+        try {
+            if (!bundlesMap.containsKey(key)) {
+                bundle = ResourceBundle.getBundle(aBundleName, locale, classLoader);
+                bundlesMap.putIfAbsent(key, bundle);
+            } else {
+                bundle = bundlesMap.get(key);
+            }
+        } catch (MissingResourceException ex) {
+            if (delegatedClassLoaderMap.containsKey(classLoader.hashCode())) {
+                try {
+                    if (!bundlesMap.containsKey(key)) {
+                        bundle = ResourceBundle.getBundle(aBundleName, locale, delegatedClassLoaderMap.get(classLoader.hashCode()));
+                        bundlesMap.putIfAbsent(key, bundle);
+                    } else {
+                        bundle = bundlesMap.get(key);
+                    }
+                } catch (MissingResourceException e) {
+                    LOG.debug("Missing resource bundle [{}]!", aBundleName, e);
+                }
+            }
+        }
+        return bundle;
+    }
+
+    /**
+     * Sets a {@link ClassLoader} to look up the bundle from if none can be found on the current thread's classloader
+     */
+    public static void setDelegatedClassLoader(final ClassLoader classLoader) {
+        synchronized (bundlesMap) {
+            delegatedClassLoaderMap.put(getCurrentThreadContextClassLoader().hashCode(), classLoader);
+        }
+    }
+
+    /**
+     * Removes the bundle from any cached "misses"
+     */
+    public static void clearBundle(final String bundleName) {
+        bundlesMap.remove(getCurrentThreadContextClassLoader().hashCode() + bundleName);
+    }
+
+
+    /**
+     * Creates a key to used for lookup/storing in the bundle misses cache.
+     *
+     * @param prefix      the prefix for the returning String - it is supposed to be the ClassLoader hash code.
+     * @param aBundleName the name of the bundle (usually it's FQN classname).
+     * @param locale      the locale.
+     * @return the key to use for lookup/storing in the bundle misses cache.
+     */
+    private static String createMissesKey(String prefix, String aBundleName, Locale locale) {
+        return prefix + aBundleName + "_" + locale.toString();
+    }
+
+    /**
+     * Calls {@link #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args)}
+     * with aTextName as the default message.
+     *
+     * @see #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args)
+     */
+    public static String findText(Class aClass, String aTextName, Locale locale) {
+        return findText(aClass, aTextName, locale, aTextName, new Object[0]);
+    }
+
+    /**
+     * Finds a localized text message for the given key, aTextName. Both the key and the message
+     * itself is evaluated as required.  The following algorithm is used to find the requested
+     * message:
+     * <p/>
+     * <ol>
+     * <li>Look for message in aClass' class hierarchy.
+     * <ol>
+     * <li>Look for the message in a resource bundle for aClass</li>
+     * <li>If not found, look for the message in a resource bundle for any implemented interface</li>
+     * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li>
+     * </ol></li>
+     * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in
+     * the model's class hierarchy (repeat sub-steps listed above).</li>
+     * <li>If not found, look for message in child property.  This is determined by evaluating
+     * the message key as an OGNL expression.  For example, if the key is
+     * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an
+     * object.  If so, repeat the entire process fromthe beginning with the object's class as
+     * aClass and "address.state" as the message key.</li>
+     * <li>If not found, look for the message in aClass' package hierarchy.</li>
+     * <li>If still not found, look for the message in the default resource bundles.</li>
+     * <li>Return defaultMessage</li>
+     * </ol>
+     * <p/>
+     * When looking for the message, if the key indexes a collection (e.g. user.phone[0]) and a
+     * message for that specific key cannot be found, the general form will also be looked up
+     * (i.e. user.phone[*]).
+     * <p/>
+     * If a message is found, it will also be interpolated.  Anything within <code>${...}</code>
+     * will be treated as an OGNL expression and evaluated as such.
+     *
+     * @param aClass         the class whose name to use as the start point for the search
+     * @param aTextName      the key to find the text message for
+     * @param locale         the locale the message should be for
+     * @param defaultMessage the message to be returned if no text message can be found in any
+     *                       resource bundle
+     * @return the localized text, or null if none can be found and no defaultMessage is provided
+     */
+    public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args) {
+        ValueStack valueStack = ActionContext.getContext().getValueStack();
+        return findText(aClass, aTextName, locale, defaultMessage, args, valueStack);
+
+    }
+
+    /**
+     * Finds a localized text message for the given key, aTextName. Both the key and the message
+     * itself is evaluated as required.  The following algorithm is used to find the requested
+     * message:
+     * <p/>
+     * <ol>
+     * <li>Look for message in aClass' class hierarchy.
+     * <ol>
+     * <li>Look for the message in a resource bundle for aClass</li>
+     * <li>If not found, look for the message in a resource bundle for any implemented interface</li>
+     * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li>
+     * </ol></li>
+     * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in
+     * the model's class hierarchy (repeat sub-steps listed above).</li>
+     * <li>If not found, look for message in child property.  This is determined by evaluating
+     * the message key as an OGNL expression.  For example, if the key is
+     * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an
+     * object.  If so, repeat the entire process fromthe beginning with the object's class as
+     * aClass and "address.state" as the message key.</li>
+     * <li>If not found, look for the message in aClass' package hierarchy.</li>
+     * <li>If still not found, look for the message in the default resource bundles.</li>
+     * <li>Return defaultMessage</li>
+     * </ol>
+     * <p/>
+     * When looking for the message, if the key indexes a collection (e.g. user.phone[0]) and a
+     * message for that specific key cannot be found, the general form will also be looked up
+     * (i.e. user.phone[*]).
+     * <p/>
+     * If a message is found, it will also be interpolated.  Anything within <code>${...}</code>
+     * will be treated as an OGNL expression and evaluated as such.
+     * <p/>
+     * If a message is <b>not</b> found a WARN log will be logged.
+     *
+     * @param aClass         the class whose name to use as the start point for the search
+     * @param aTextName      the key to find the text message for
+     * @param locale         the locale the message should be for
+     * @param defaultMessage the message to be returned if no text message can be found in any
+     *                       resource bundle
+     * @param valueStack     the value stack to use to evaluate expressions instead of the
+     *                       one in the ActionContext ThreadLocal
+     * @return the localized text, or null if none can be found and no defaultMessage is provided
+     */
+    public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args,
+                                  ValueStack valueStack) {
+        String indexedTextName = null;
+        if (aTextName == null) {
+        	LOG.warn("Trying to find text with null key!");
+            aTextName = "";
+        }
+        // calculate indexedTextName (collection[*]) if applicable
+        if (aTextName.contains("[")) {
+            int i = -1;
+
+            indexedTextName = aTextName;
+
+            while ((i = indexedTextName.indexOf("[", i + 1)) != -1) {
+                int j = indexedTextName.indexOf("]", i);
+                String a = indexedTextName.substring(0, i);
+                String b = indexedTextName.substring(j);
+                indexedTextName = a + "[*" + b;
+            }
+        }
+
+        // search up class hierarchy
+        String msg = findMessage(aClass, aTextName, indexedTextName, locale, args, null, valueStack);
+
+        if (msg != null) {
+            return msg;
+        }
+
+        if (ModelDriven.class.isAssignableFrom(aClass)) {
+            ActionContext context = ActionContext.getContext();
+            // search up model's class hierarchy
+            ActionInvocation actionInvocation = context.getActionInvocation();
+
+            // ActionInvocation may be null if we're being run from a Sitemesh filter, so we won't get model texts if this is null
+            if (actionInvocation != null) {
+                Object action = actionInvocation.getAction();
+                if (action instanceof ModelDriven) {
+                    Object model = ((ModelDriven) action).getModel();
+                    if (model != null) {
+                        msg = findMessage(model.getClass(), aTextName, indexedTextName, locale, args, null, valueStack);
+                        if (msg != null) {
+                            return msg;
+                        }
+                    }
+                }
+            }
+        }
+
+        // nothing still? alright, search the package hierarchy now
+        for (Class clazz = aClass;
+             (clazz != null) && !clazz.equals(Object.class);
+             clazz = clazz.getSuperclass()) {
+
+            String basePackageName = clazz.getName();
+            while (basePackageName.lastIndexOf('.') != -1) {
+                basePackageName = basePackageName.substring(0, basePackageName.lastIndexOf('.'));
+                String packageName = basePackageName + ".package";
+                msg = getMessage(packageName, locale, aTextName, valueStack, args);
+
+                if (msg != null) {
+                    return msg;
+                }
+
+                if (indexedTextName != null) {
+                    msg = getMessage(packageName, locale, indexedTextName, valueStack, args);
+
+                    if (msg != null) {
+                        return msg;
+                    }
+                }
+            }
+        }
+
+        // see if it's a child property
+        int idx = aTextName.indexOf(".");
+
+        if (idx != -1) {
+            String newKey = null;
+            String prop = null;
+
+            if (aTextName.startsWith(XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX)) {
+                idx = aTextName.indexOf(".", XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX.length());
+
+                if (idx != -1) {
+                    prop = aTextName.substring(XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX.length(), idx);
+                    newKey = XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX + aTextName.substring(idx + 1);
+                }
+            } else {
+                prop = aTextName.substring(0, idx);
+                newKey = aTextName.substring(idx + 1);
+            }
+
+            if (prop != null) {
+                Object obj = valueStack.findValue(prop);
+                try {
+                    Object actionObj = ReflectionProviderFactory.getInstance().getRealTarget(prop, valueStack.getContext(), valueStack.getRoot());
+                    if (actionObj != null) {
+                        PropertyDescriptor propertyDescriptor = ReflectionProviderFactory.getInstance().getPropertyDescriptor(actionObj.getClass(), prop);
+
+                        if (propertyDescriptor != null) {
+                            Class clazz = propertyDescriptor.getPropertyType();
+
+                            if (clazz != null) {
+                                if (obj != null) {
+                                    valueStack.push(obj);
+                                }
+                                msg = findText(clazz, newKey, locale, null, args);
+                                if (obj != null) {
+                                    valueStack.pop();
+                                }
+                                if (msg != null) {
+                                    return msg;
+                                }
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    LOG.debug("unable to find property {}", prop, e);
+                }
+            }
+        }
+
+        // get default
+        GetDefaultMessageReturnArg result;
+        if (indexedTextName == null) {
+            result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);
+        } else {
+            result = getDefaultMessage(aTextName, locale, valueStack, args, null);
+            if (result != null && result.message != null) {
+                return result.message;
+            }
+            result = getDefaultMessage(indexedTextName, locale, valueStack, args, defaultMessage);
+        }
+
+        // could we find the text, if not log a warn
+        if (unableToFindTextForKey(result) && LOG.isDebugEnabled()) {
+            String warn = "Unable to find text for key '" + aTextName + "' ";
+            if (indexedTextName != null) {
+                warn += " or indexed key '" + indexedTextName + "' ";
+            }
+            warn += "in class '" + aClass.getName() + "' and locale '" + locale + "'";
+            LOG.debug(warn);
+        }
+
+        return result != null ? result.message : null;
+    }
+
+    /**
+     * Determines if we found the text in the bundles.
+     *
+     * @param result the result so far
+     * @return <tt>true</tt> if we could <b>not</b> find the text, <tt>false</tt> if the text was found (=success).
+     */
+    private static boolean unableToFindTextForKey(GetDefaultMessageReturnArg result) {
+        if (result == null || result.message == null) {
+            return true;
+        }
+
+        // did we find it in the bundle, then no problem?
+        if (result.foundInBundle) {
+            return false;
+        }
+
+        // not found in bundle
+        return true;
+    }
+
+    /**
+     * Finds a localized text message for the given key, aTextName, in the specified resource bundle
+     * with aTextName as the default message.
+     * <p/>
+     * If a message is found, it will also be interpolated.  Anything within <code>${...}</code>
+     * will be treated as an OGNL expression and evaluated as such.
+     *
+     * @see #findText(java.util.ResourceBundle, String, java.util.Locale, String, Object[])
+     */
+    public static String findText(ResourceBundle bundle, String aTextName, Locale locale) {
+        return findText(bundle, aTextName, locale, aTextName, new Object[0]);
+    }
+
+    /**
+     * Finds a localized text message for the given key, aTextName, in the specified resource
+     * bundle.
+     * <p/>
+     * If a message is found, it will also be interpolated.  Anything within <code>${...}</code>
+     * will be treated as an OGNL expression and evaluated as such.
+     * <p/>
+     * If a message is <b>not</b> found a WARN log will be logged.
+     *
+     * @param bundle         the bundle
+     * @param aTextName      the key
+     * @param locale         the locale
+     * @param defaultMessage the default message to use if no message was found in the bundle
+     * @param args           arguments for the message formatter.
+     */
+    public static String findText(ResourceBundle bundle, String aTextName, Locale locale, String defaultMessage, Object[] args) {
+        ValueStack valueStack = ActionContext.getContext().getValueStack();
+        return findText(bundle, aTextName, locale, defaultMessage, args, valueStack);
+    }
+
+    /**
+     * Finds a localized text message for the given key, aTextName, in the specified resource
+     * bundle.
+     * <p/>
+     * If a message is found, it will also be interpolated.  Anything within <code>${...}</code>
+     * will be treated as an OGNL expression and evaluated as such.
+     * <p/>
+     * If a message is <b>not</b> found a WARN log will be logged.
+     *
+     * @param bundle         the bundle
+     * @param aTextName      the key
+     * @param locale         the locale
+     * @param defaultMessage the default message to use if no message was found in the bundle
+     * @param args           arguments for the message formatter.
+     * @param valueStack     the OGNL value stack.
+     */
+    public static String findText(ResourceBundle bundle, String aTextName, Locale locale, String defaultMessage, Object[] args,
+                                  ValueStack valueStack) {
+        try {
+            reloadBundles(valueStack.getContext());
+
+            String message = TextParseUtil.translateVariables(bundle.getString(aTextName), valueStack);
+            MessageFormat mf = buildMessageFormat(message, locale);
+
+            return formatWithNullDetection(mf, args);
+        } catch (MissingResourceException ex) {
+            if (devMode) {
+                LOG.warn("Missing key [{}] in bundle [{}]!", aTextName, bundle);
+            } else {
+                LOG.debug("Missing key [{}] in bundle [{}]!", aTextName, bundle);
+            }
+        }
+
+        GetDefaultMessageReturnArg result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);
+        if (unableToFindTextForKey(result)) {
+            LOG.warn("Unable to find text for key '{}' in ResourceBundles for locale '{}'", aTextName, locale);
+        }
+        return result != null ? result.message : null;
+    }
+
+    /**
+     * Gets the default message.
+     */
+    private static GetDefaultMessageReturnArg getDefaultMessage(String key, Locale locale, ValueStack valueStack, Object[] args,
+                                                                String defaultMessage) {
+        GetDefaultMessageReturnArg result = null;
+        boolean found = true;
+
+        if (key != null) {
+            String message = findDefaultText(key, locale);
+
+            if (message == null) {
+                message = defaultMessage;
+                found = false; // not found in bundles
+            }
+
+            // defaultMessage may be null
+            if (message != null) {
+                MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);
+
+                String msg = formatWithNullDetection(mf, args);
+                result = new GetDefaultMessageReturnArg(msg, found);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Gets the message from the named resource bundle.
+     */
+    private static String getMessage(String bundleName, Locale locale, String key, ValueStack valueStack, Object[] args) {
+        ResourceBundle bundle = findResourceBundle(bundleName, locale);
+        if (bundle == null) {
+            return null;
+        }
+            reloadBundles(valueStack.getContext());
+        try {
+            String message = TextParseUtil.translateVariables(bundle.getString(key), valueStack);
+            MessageFormat mf = buildMessageFormat(message, locale);
+            return formatWithNullDetection(mf, args);
+        } catch (MissingResourceException e) {
+            if (devMode) {
+                LOG.warn("Missing key [{}] in bundle [{}]!", key, bundleName);
+            } else {
+                LOG.debug("Missing key [{}] in bundle [{}]!", key, bundleName);
+            }
+            return null;
+        }
+    }
+
+    private static String formatWithNullDetection(MessageFormat mf, Object[] args) {
+        String message = mf.format(args);
+        if ("null".equals(message)) {
+            return null;
+        } else {
+            return message;
+        }
+    }
+
+    private static MessageFormat buildMessageFormat(String pattern, Locale locale) {
+        MessageFormatKey key = new MessageFormatKey(pattern, locale);
+        MessageFormat format = messageFormats.get(key);
+        if (format == null) {
+            format = new MessageFormat(pattern);
+            format.setLocale(locale);
+            format.applyPattern(pattern);
+            messageFormats.put(key, format);
+        }
+
+        return format;
+    }
+
+    /**
+     * Traverse up class hierarchy looking for message.  Looks at class, then implemented interface,
+     * before going up hierarchy.
+     */
+    private static String findMessage(Class clazz, String key, String indexedKey, Locale locale, Object[] args, Set<String> checked,
+                                      ValueStack valueStack) {
+        if (checked == null) {
+            checked = new TreeSet<String>();
+        } else if (checked.contains(clazz.getName())) {
+            return null;
+        }
+
+        // look in properties of this class
+        String msg = getMessage(clazz.getName(), locale, key, valueStack, args);
+
+        if (msg != null) {
+            return msg;
+        }
+
+        if (indexedKey != null) {
+            msg = getMessage(clazz.getName(), locale, indexedKey, valueStack, args);
+
+            if (msg != null) {
+                return msg;
+            }
+        }
+
+        // look in properties of implemented interfaces
+        Class[] interfaces = clazz.getInterfaces();
+
+        for (Class anInterface : interfaces) {
+            msg = getMessage(anInterface.getName(), locale, key, valueStack, args);
+
+            if (msg != null) {
+                return msg;
+            }
+
+            if (indexedKey != null) {
+                msg = getMessage(anInterface.getName(), locale, indexedKey, valueStack, args);
+
+                if (msg != null) {
+                    return msg;
+                }
+            }
+        }
+
+        // traverse up hierarchy
+        if (clazz.isInterface()) {
+            interfaces = clazz.getInterfaces();
+
+            for (Class anInterface : interfaces) {
+                msg = findMessage(anInterface, key, indexedKey, locale, args, checked, valueStack);
+
+                if (msg != null) {
+                    return msg;
+                }
+            }
+        } else {
+            if (!clazz.equals(Object.class) && !clazz.isPrimitive()) {
+                return findMessage(clazz.getSuperclass(), key, indexedKey, locale, args, checked, valueStack);
+            }
+        }
+
+        return null;
+    }
+
+    private static void reloadBundles() {
+        reloadBundles(ActionContext.getContext() != null ? ActionContext.getContext().getContextMap() : null);
+    }
+
+    private static void reloadBundles(Map<String, Object> context) {
+        if (reloadBundles) {
+            try {
+                Boolean reloaded;
+                if (context != null) {
+                    reloaded = (Boolean) ObjectUtils.defaultIfNull(context.get(RELOADED), Boolean.FALSE);
+                }else {
+                    reloaded = Boolean.FALSE;
+                }
+                if (!reloaded) {
+                    bundlesMap.clear();
+                    try {
+                        clearMap(ResourceBundle.class, null, "cacheList");
+                    } catch (NoSuchFieldException e) {
+                        // happens in IBM JVM, that has a different ResourceBundle impl
+                        // it has a 'cache' member
+                        clearMap(ResourceBundle.class, null, "cache");
+                    }
+
+                    // now, for the true and utter hack, if we're running in tomcat, clear
+                    // it's class loader resource cache as well.
+                    clearTomcatCache();
+                    if(context!=null) {
+                        context.put(RELOADED, true);
+                    }
+                    LOG.debug("Resource bundles reloaded");
+                }
+            } catch (Exception e) {
+                LOG.error("Could not reload resource bundles", e);
+            }
+        }
+    }
+
+
+    private static void clearTomcatCache() {
+        ClassLoader loader = getCurrentThreadContextClassLoader();
+        // no need for compilation here.
+        Class cl = loader.getClass();
+
+        try {
+            if ("org.apache.catalina.loader.WebappClassLoader".equals(cl.getName())) {
+                clearMap(cl, loader, TOMCAT_RESOURCE_ENTRIES_FIELD);
+            } else {
+                LOG.debug("Class loader {} is not tomcat loader.", cl.getName());
+            }
+        } catch (NoSuchFieldException nsfe) {
+            if ("org.apache.catalina.loader.WebappClassLoaderBase".equals(cl.getSuperclass().getName())) {
+                LOG.debug("Base class {} doesn't contain '{}' field, trying with parent!", cl.getName(), TOMCAT_RESOURCE_ENTRIES_FIELD, nsfe);
+                try {
+                    clearMap(cl.getSuperclass(), loader, TOMCAT_RESOURCE_ENTRIES_FIELD);
+                } catch (Exception e) {
+                    LOG.warn("Couldn't clear tomcat cache using {}", cl.getSuperclass().getName(), e);
+                }
+            }
+        } catch (Exception e) {
+      	    LOG.warn("Couldn't clear tomcat cache", cl.getName(), e);
+        }
+    }
+
+
+    private static void clearMap(Class cl, Object obj, String name)
+            throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
+
+        Field field = cl.getDeclaredField(name);
+        field.setAccessible(true);
+
+        Object cache = field.get(obj);
+
+        synchronized (cache) {
+            Class ccl = cache.getClass();
+            Method clearMethod = ccl.getMethod("clear");
+            clearMethod.invoke(cache);
+        }
+    }
+
+    /**
+     * Clears all the internal lists.
+     */
+    public static void reset() {
+        clearDefaultResourceBundles();
+        bundlesMap.clear();
+        messageFormats.clear();
+    }
+
+    static class MessageFormatKey {
+        String pattern;
+        Locale locale;
+
+        MessageFormatKey(String pattern, Locale locale) {
+            this.pattern = pattern;
+            this.locale = locale;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof MessageFormatKey)) return false;
+
+            final MessageFormatKey messageFormatKey = (MessageFormatKey) o;
+
+            if (locale != null ? !locale.equals(messageFormatKey.locale) : messageFormatKey.locale != null)
+                return false;
+            if (pattern != null ? !pattern.equals(messageFormatKey.pattern) : messageFormatKey.pattern != null)
+                return false;
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result;
+            result = (pattern != null ? pattern.hashCode() : 0);
+            result = 29 * result + (locale != null ? locale.hashCode() : 0);
+            return result;
+        }
+    }
+
+    private static ClassLoader getCurrentThreadContextClassLoader() {
+        return Thread.currentThread().getContextClassLoader();
+    }
+
+    static class GetDefaultMessageReturnArg {
+        String message;
+        boolean foundInBundle;
+
+        public GetDefaultMessageReturnArg(String message, boolean foundInBundle) {
+            this.message = message;
+            this.foundInBundle = foundInBundle;
+        }
+    }
+
+    private static class EmptyResourceBundle extends ResourceBundle {
+        @Override
+        public Enumeration<String> getKeys() {
+            return null; // dummy
+        }
+
+        @Override
+        protected Object handleGetObject(String key) {
+            return null; // dummy
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/MemberAccessValueStack.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/MemberAccessValueStack.java b/core/src/main/java/com/opensymphony/xwork2/util/MemberAccessValueStack.java
new file mode 100644
index 0000000..51f4e48
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/MemberAccessValueStack.java
@@ -0,0 +1,16 @@
+package com.opensymphony.xwork2.util;
+
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * ValueStacks implementing this interface provide a way to remove block or allow access
+ * to properties using regular expressions
+ */
+public interface MemberAccessValueStack {
+
+    void setExcludeProperties(Set<Pattern> excludeProperties);
+
+    void setAcceptProperties(Set<Pattern> acceptedProperties);
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/NamedVariablePatternMatcher.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/NamedVariablePatternMatcher.java b/core/src/main/java/com/opensymphony/xwork2/util/NamedVariablePatternMatcher.java
new file mode 100644
index 0000000..8d197c6
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/NamedVariablePatternMatcher.java
@@ -0,0 +1,146 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An implementation of a pattern matcher that uses simple named wildcards.  The named wildcards are defined using the
+ * <code>{VARIABLE_NAME}</code> syntax and will match any characters that aren't '/'.  Internally, the pattern is
+ * converted into a regular expression where the named wildcard will be translated into <code>([^/]+)</code> so that
+ * at least one character must match in order for the wildcard to be matched successfully.  Matched values will be
+ * available in the variable map, indexed by the name they were given in the pattern.
+ *
+ * <p>For example, the following patterns will be processed as so:
+ * </p>
+ * <table>
+ * <tr>
+ *  <th>Pattern</th>
+ *  <th>Example</th>
+ *  <th>Variable Map Contents</th>
+ * </tr>
+ * <tr>
+ *  <td><code>/animals/{animal}</code</td>
+ *  <td><code>/animals/dog</code></td>
+ *  <td>{animal -> dog}</td>
+ * </tr>
+ * <tr>
+ *  <td><code>/animals/{animal}/tag/No{id}</code</td>
+ *  <td><code>/animals/dog/tag/No23</code></td>
+ *  <td>{animal -> dog, id -> 23}</td>
+ * </tr>
+ * <tr>
+ *  <td><code>/{language}</code</td>
+ *  <td><code>/en</code></td>
+ *  <td>{language -> en}</td>
+ * </tr>
+ * </table>
+ *
+ * <p>
+ * Excaping hasn't been implemented since the intended use of these patterns will be in matching URLs.
+ * </p>
+ *
+ * @Since 2.1
+ */
+public class NamedVariablePatternMatcher implements PatternMatcher<NamedVariablePatternMatcher.CompiledPattern> {
+
+    public boolean isLiteral(String pattern) {
+        return (pattern == null || pattern.indexOf('{') == -1);
+    }
+
+    /**
+     * Compiles the pattern.
+     *
+     * @param data The pattern, must not be null or empty
+     * @return The compiled pattern, null if the pattern was null or empty
+     */
+    public CompiledPattern compilePattern(String data) {
+        StringBuilder regex = new StringBuilder();
+        if (data != null && data.length() > 0) {
+            List<String> varNames = new ArrayList<>();
+            StringBuilder varName = null;
+            for (int x=0; x<data.length(); x++) {
+                char c = data.charAt(x);
+                switch (c) {
+                    case '{' :  varName = new StringBuilder(); break;
+                    case '}' :  if (varName == null) {
+                                    throw new IllegalArgumentException("Mismatched braces in pattern");
+                                }
+                                varNames.add(varName.toString());
+                                regex.append("([^/]+)");
+                                varName = null;
+                                break;
+                    default  :  if (varName == null) {
+                                    regex.append(c);
+                                } else {
+                                    varName.append(c);
+                                }
+                }
+            }
+            return new CompiledPattern(Pattern.compile(regex.toString()), varNames);
+        }
+        return null;
+    }
+
+    /**
+     * Tries to process the data against the compiled expression.  If successful, the map will contain
+     * the matched data, using the specified variable names in the original pattern.
+     *
+     * @param map The map of variables
+     * @param data The data to match
+     * @param expr The compiled pattern
+     * @return True if matched, false if not matched, the data was null, or the data was an empty string
+     */
+    public boolean match(Map<String, String> map, String data, CompiledPattern expr) {
+
+        if (data != null && data.length() > 0) {
+            Matcher matcher = expr.getPattern().matcher(data);
+            if (matcher.matches()) {
+                for (int x=0; x<expr.getVariableNames().size(); x++)  {
+                    map.put(expr.getVariableNames().get(x), matcher.group(x+1));
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Stores the compiled pattern and the variable names matches will correspond to.
+     */
+    public static class CompiledPattern {
+        private Pattern pattern;
+        private List<String> variableNames;
+
+
+        public CompiledPattern(Pattern pattern, List<String> variableNames) {
+            this.pattern = pattern;
+            this.variableNames = variableNames;
+        }
+
+        public Pattern getPattern() {
+            return pattern;
+        }
+
+        public List<String> getVariableNames() {
+            return variableNames;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/OgnlTextParser.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/OgnlTextParser.java b/core/src/main/java/com/opensymphony/xwork2/util/OgnlTextParser.java
new file mode 100644
index 0000000..899c375
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/OgnlTextParser.java
@@ -0,0 +1,83 @@
+package com.opensymphony.xwork2.util;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * OGNL implementation of {@link TextParser}
+ */
+public class OgnlTextParser implements TextParser {
+
+    public Object evaluate(char[] openChars, String expression, TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount) {
+        // deal with the "pure" expressions first!
+        //expression = expression.trim();
+        Object result = expression = (expression == null) ? "" : expression;
+        int pos = 0;
+
+        for (char open : openChars) {
+            int loopCount = 1;
+            //this creates an implicit StringBuffer and shouldn't be used in the inner loop
+            final String lookupChars = open + "{";
+
+            while (true) {
+                int start = expression.indexOf(lookupChars, pos);
+                if (start == -1) {
+                    loopCount++;
+                    start = expression.indexOf(lookupChars);
+                }
+                if (loopCount > maxLoopCount) {
+                    // translateVariables prevent infinite loop / expression recursive evaluation
+                    break;
+                }
+                int length = expression.length();
+                int x = start + 2;
+                int end;
+                char c;
+                int count = 1;
+                while (start != -1 && x < length && count != 0) {
+                    c = expression.charAt(x++);
+                    if (c == '{') {
+                        count++;
+                    } else if (c == '}') {
+                        count--;
+                    }
+                }
+                end = x - 1;
+
+                if ((start != -1) && (end != -1) && (count == 0)) {
+                    String var = expression.substring(start + 2, end);
+
+                    Object o = evaluator.evaluate(var);
+
+                    String left = expression.substring(0, start);
+                    String right = expression.substring(end + 1);
+                    String middle = null;
+                    if (o != null) {
+                        middle = o.toString();
+                        if (StringUtils.isEmpty(left)) {
+                            result = o;
+                        } else {
+                            result = left.concat(middle);
+                        }
+
+                        if (StringUtils.isNotEmpty(right)) {
+                            result = result.toString().concat(right);
+                        }
+
+                        expression = left.concat(middle).concat(right);
+                    } else {
+                        // the variable doesn't exist, so don't display anything
+                        expression = left.concat(right);
+                        result = expression;
+                    }
+                    pos = (left != null && left.length() > 0 ? left.length() - 1: 0) +
+                            (middle != null && middle.length() > 0 ? middle.length() - 1: 0) +
+                            1;
+                    pos = Math.max(pos, 1);
+                } else {
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/PatternMatcher.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/PatternMatcher.java b/core/src/main/java/com/opensymphony/xwork2/util/PatternMatcher.java
new file mode 100644
index 0000000..f472893
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/PatternMatcher.java
@@ -0,0 +1,57 @@
+/*
+ * $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;
+
+/**
+ * Compiles and matches a pattern against a value
+ * 
+ * @since 2.1
+ */
+public interface PatternMatcher<E extends Object> {
+
+    /**
+     * Determines if the pattern is a simple literal string or contains wildcards that will need to be processed
+     * @param pattern The string pattern
+     * @return True if the pattern doesn't contain processing elements, false otherwise
+     */
+    boolean isLiteral(String pattern);
+
+    /**
+     * <p> Translate the given <code>String</code> into an object
+     * representing the pattern matchable by this class. 
+     *
+     * @param data The string to translate.
+     * @return The encoded string 
+     * @throws NullPointerException If data is null.
+     */
+    E compilePattern(String data);
+
+    /**
+     * Match a pattern against a string 
+     *
+     * @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
+     */
+    boolean match(Map<String,String> map, String data, E expr);
+    
+}
\ No newline at end of file