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 -->
+ * @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.
+ * @Element( value = com.acme.User )
+ * private Map<Long, User> userMap;
+ *
+ * @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.
+ * @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.
+ * @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/>
+ * acme/<br/>
+ * package.properties<br/>
+ * actions/<br/>
+ * package.properties<br/>
+ * FooAction.java<br/>
+ * 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