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:[email protected]">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
