Merge pull request #397 from apache/action-context-boost

[WW-4789] [WW-3788] ActionContext refactoring
diff --git a/apps/showcase/src/main/java/org/apache/struts2/showcase/fileupload/FileUploadAction.java b/apps/showcase/src/main/java/org/apache/struts2/showcase/fileupload/FileUploadAction.java
index 6005306..ed8a11b 100644
--- a/apps/showcase/src/main/java/org/apache/struts2/showcase/fileupload/FileUploadAction.java
+++ b/apps/showcase/src/main/java/org/apache/struts2/showcase/fileupload/FileUploadAction.java
@@ -84,4 +84,12 @@
 	public void setCaption(String caption) {
 		this.caption = caption;
 	}
+
+        public long getUploadSize() {
+            if (upload != null) {
+                return upload.length();
+            } else {
+                return 0;
+            }
+        }
 }
diff --git a/apps/showcase/src/main/resources/org/apache/struts2/showcase/fileupload/FileUploadAction-validation.xml b/apps/showcase/src/main/resources/org/apache/struts2/showcase/fileupload/FileUploadAction-validation.xml
index df2dd55..cfd4178 100644
--- a/apps/showcase/src/main/resources/org/apache/struts2/showcase/fileupload/FileUploadAction-validation.xml
+++ b/apps/showcase/src/main/resources/org/apache/struts2/showcase/fileupload/FileUploadAction-validation.xml
@@ -26,7 +26,7 @@
 <validators>
 	<field name="upload">
 		<field-validator type="fieldexpression">
-			<param name="expression"><![CDATA[upload.length() > 0]]></param>
+			<param name="expression"><![CDATA[getUploadSize() > 0]]></param>
 			<message>File cannot be empty</message>
 		</field-validator>
 	</field>
diff --git a/core/src/main/java/com/opensymphony/xwork2/config/impl/AbstractMatcher.java b/core/src/main/java/com/opensymphony/xwork2/config/impl/AbstractMatcher.java
index 3b3b74b..7fe4254 100644
--- a/core/src/main/java/com/opensymphony/xwork2/config/impl/AbstractMatcher.java
+++ b/core/src/main/java/com/opensymphony/xwork2/config/impl/AbstractMatcher.java
@@ -36,10 +36,8 @@
  * @since 2.1
  */
 public abstract class AbstractMatcher<E> implements Serializable {
-    /**
-     * <p> The logging instance </p>
-     */
-    private static final Logger log = LogManager.getLogger(AbstractMatcher.class);
+
+    private static final Logger LOG = LogManager.getLogger(AbstractMatcher.class);
 
     /**
      * <p> Handles all wildcard pattern matching. </p>
@@ -50,10 +48,34 @@
      * <p> The compiled patterns and their associated target objects </p>
      */
     List<Mapping<E>> compiledPatterns = new ArrayList<>();
-    ;
+
+    /**
+     * This flag controls if passed named params should be appended
+     * to the map in {@link #replaceParameters(Map, Map)}
+     * and will be accessible in {@link com.opensymphony.xwork2.config.entities.ResultConfig}.
+     * If set to false, the named parameters won't be appended.
+     *
+     * This behaviour is controlled by {@link org.apache.struts2.StrutsConstants#STRUTS_MATCHER_APPEND_NAMED_PARAMETERS}
+     *
+     * @since 2.5.23
+     * See WW-5065
+     */
+    private final boolean appendNamedParameters;
     
-    public AbstractMatcher(PatternMatcher<?> helper) {
+    public AbstractMatcher(PatternMatcher<?> helper, boolean appendNamedParameters) {
         this.wildcard = (PatternMatcher<Object>) helper;
+        this.appendNamedParameters = appendNamedParameters;
+    }
+
+    /**
+     * Creates a matcher with {@link #appendNamedParameters} set to true to keep backward compatibility
+     *
+     * @param helper an instance of {@link PatternMatcher}
+     * @deprecated use @{link {@link AbstractMatcher(PatternMatcher, boolean)} instead
+     */
+    @Deprecated
+    public AbstractMatcher(PatternMatcher<?> helper) {
+        this(helper, true);
     }
 
     /**
@@ -84,17 +106,17 @@
                 name = name.substring(1);
             }
 
-            log.debug("Compiling pattern '{}'", name);
+            LOG.debug("Compiling pattern '{}'", name);
 
             pattern = wildcard.compilePattern(name);
-            compiledPatterns.add(new Mapping<E>(name, pattern, target));
+            compiledPatterns.add(new Mapping<>(name, pattern, target));
 
             if (looseMatch) {
                 int lastStar = name.lastIndexOf('*');
                 if (lastStar > 1 && lastStar == name.length() - 1) {
                     if (name.charAt(lastStar - 1) != '*') {
                         pattern = wildcard.compilePattern(name.substring(0, lastStar - 1));
-                        compiledPatterns.add(new Mapping<E>(name, pattern, target));
+                        compiledPatterns.add(new Mapping<>(name, pattern, target));
                     }
                 }
             }
@@ -115,12 +137,12 @@
         E config = null;
 
         if (compiledPatterns.size() > 0) {
-            log.debug("Attempting to match '{}' to a wildcard pattern, {} available", potentialMatch, compiledPatterns.size());
+            LOG.debug("Attempting to match '{}' to a wildcard pattern, {} available", potentialMatch, compiledPatterns.size());
 
-            Map<String,String> vars = new LinkedHashMap<String,String>();
+            Map<String,String> vars = new LinkedHashMap<>();
             for (Mapping<E> m : compiledPatterns) {
                 if (wildcard.match(vars, potentialMatch, m.getPattern())) {
-                    log.debug("Value matches pattern '{}'", m.getOriginalPattern());
+                    LOG.debug("Value matches pattern '{}'", m.getOriginalPattern());
                     config = convert(potentialMatch, m.getTarget(), vars);
                     break;
                 }
@@ -152,20 +174,23 @@
      */
     protected Map<String,String> replaceParameters(Map<String, String> orig, Map<String,String> vars) {
         Map<String, String> map = new LinkedHashMap<>();
-        
+
         //this will set the group index references, like {1}
         for (Map.Entry<String,String> entry : orig.entrySet()) {
             map.put(entry.getKey(), convertParam(entry.getValue(), vars));
         }
-        
-        //the values map will contain entries like name->"Lex Luthor" and 1->"Lex Luthor"
-        //now add the non-numeric values
-        for (Map.Entry<String,String> entry: vars.entrySet()) {
-            if (!NumberUtils.isCreatable(entry.getKey())) {
-                map.put(entry.getKey(), entry.getValue());
+
+        if (appendNamedParameters) {
+            LOG.debug("Appending named parameters to the result map");
+            //the values map will contain entries like name->"Lex Luthor" and 1->"Lex Luthor"
+            //now add the non-numeric values
+            for (Map.Entry<String,String> entry: vars.entrySet()) {
+                if (!NumberUtils.isCreatable(entry.getKey())) {
+                    map.put(entry.getKey(), entry.getValue());
+                }
             }
         }
-        
+
         return map;
     }
 
@@ -192,7 +217,7 @@
             c = val.charAt(x);
             if (x < len - 2 && 
                     c == '{' && '}' == val.charAt(x+2)) {
-                varVal = (String)vars.get(String.valueOf(val.charAt(x + 1)));
+                varVal = vars.get(String.valueOf(val.charAt(x + 1)));
                 if (varVal != null) {
                     ret.append(varVal);
                 } 
@@ -213,18 +238,18 @@
         /**
          * <p> The original pattern. </p>
          */
-        private String original;
+        private final String original;
 
         
         /**
          * <p> The compiled pattern. </p>
          */
-        private Object pattern;
+        private final Object pattern;
 
         /**
          * <p> The original object. </p>
          */
-        private E config;
+        private final E config;
 
         /**
          * <p> Contructs a read-only Mapping instance. </p>
diff --git a/core/src/main/java/com/opensymphony/xwork2/config/impl/ActionConfigMatcher.java b/core/src/main/java/com/opensymphony/xwork2/config/impl/ActionConfigMatcher.java
index b94fff6..344339e 100644
--- a/core/src/main/java/com/opensymphony/xwork2/config/impl/ActionConfigMatcher.java
+++ b/core/src/main/java/com/opensymphony/xwork2/config/impl/ActionConfigMatcher.java
@@ -58,7 +58,33 @@
     public ActionConfigMatcher(PatternMatcher<?> patternMatcher,
             Map<String, ActionConfig> configs,
             boolean looseMatch) {
-        super(patternMatcher);
+        this(patternMatcher, configs, looseMatch, true);
+    }
+
+    /**
+     * <p> Finds and precompiles the wildcard patterns from the ActionConfig
+     * "path" attributes. ActionConfig's will be evaluated in the order they
+     * exist in the config file. Only paths that actually contain a
+     * wildcard will be compiled. </p>
+     *
+     * <p>Patterns can optionally be matched "loosely".  When
+     * the end of the pattern matches \*[^*]\*$ (wildcard, no wildcard,
+     * wildcard), if the pattern fails, it is also matched as if the
+     * last two characters didn't exist.  The goal is to support the
+     * legacy "*!*" syntax, where the "!*" is optional.</p>
+     *
+     * @param patternMatcher pattern matcher
+     * @param configs An array of ActionConfig's to process
+     * @param looseMatch To loosely match wildcards or not
+     * @param appendNamedParameters To append named parameters or not
+     *
+     * @since 2.5.23
+     * See WW-5065
+     */
+    public ActionConfigMatcher(PatternMatcher<?> patternMatcher,
+            Map<String, ActionConfig> configs,
+            boolean looseMatch, boolean appendNamedParameters) {
+        super(patternMatcher, appendNamedParameters);
         for (Map.Entry<String, ActionConfig> entry : configs.entrySet()) {
             addPattern(entry.getKey(), entry.getValue(), looseMatch);
         }
diff --git a/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java b/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java
index 3a714b8..38d250a 100644
--- a/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java
+++ b/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java
@@ -290,6 +290,8 @@
         builder.constant(StrutsConstants.STRUTS_CONFIGURATION_XML_RELOAD, "false");
         builder.constant(StrutsConstants.STRUTS_I18N_RELOAD, "false");
 
+        builder.constant(StrutsConstants.STRUTS_MATCHER_APPEND_NAMED_PARAMETERS, "true");
+
         return builder.create(true);
     }
 
@@ -339,8 +341,12 @@
         }
 
         PatternMatcher<int[]> matcher = container.getInstance(PatternMatcher.class);
+        boolean appendNamedParameters = Boolean.parseBoolean(
+                container.getInstance(String.class, StrutsConstants.STRUTS_MATCHER_APPEND_NAMED_PARAMETERS)
+        );
+
         return new RuntimeConfigurationImpl(Collections.unmodifiableMap(namespaceActionConfigs),
-                Collections.unmodifiableMap(namespaceConfigs), matcher);
+                Collections.unmodifiableMap(namespaceConfigs), matcher, appendNamedParameters);
     }
 
     private void setDefaultResults(Map<String, ResultConfig> results, PackageConfig packageContext) {
@@ -418,15 +424,18 @@
 
         public RuntimeConfigurationImpl(Map<String, Map<String, ActionConfig>> namespaceActionConfigs,
                                         Map<String, String> namespaceConfigs,
-                                        PatternMatcher<int[]> matcher) {
+                                        PatternMatcher<int[]> matcher,
+                                        boolean appendNamedParameters)
+        {
             this.namespaceActionConfigs = namespaceActionConfigs;
             this.namespaceConfigs = namespaceConfigs;
 
             this.namespaceActionConfigMatchers = new LinkedHashMap<>();
-            this.namespaceMatcher = new NamespaceMatcher(matcher, namespaceActionConfigs.keySet());
+            this.namespaceMatcher = new NamespaceMatcher(matcher, namespaceActionConfigs.keySet(), appendNamedParameters);
 
             for (Map.Entry<String, Map<String, ActionConfig>> entry : namespaceActionConfigs.entrySet()) {
-                namespaceActionConfigMatchers.put(entry.getKey(), new ActionConfigMatcher(matcher, entry.getValue(), true));
+                ActionConfigMatcher configMatcher = new ActionConfigMatcher(matcher, entry.getValue(), true, appendNamedParameters);
+                namespaceActionConfigMatchers.put(entry.getKey(), configMatcher);
             }
         }
 
diff --git a/core/src/main/java/com/opensymphony/xwork2/config/impl/NamespaceMatcher.java b/core/src/main/java/com/opensymphony/xwork2/config/impl/NamespaceMatcher.java
index 01decd3..b24fc75 100644
--- a/core/src/main/java/com/opensymphony/xwork2/config/impl/NamespaceMatcher.java
+++ b/core/src/main/java/com/opensymphony/xwork2/config/impl/NamespaceMatcher.java
@@ -29,9 +29,23 @@
  * @since 2.1
  */
 public class NamespaceMatcher extends AbstractMatcher<NamespaceMatch> {
-     public NamespaceMatcher(PatternMatcher<?> patternMatcher,
-            Set<String> namespaces) {
-        super(patternMatcher);
+
+    public NamespaceMatcher(PatternMatcher<?> patternMatcher, Set<String> namespaces) {
+        this(patternMatcher, namespaces, true);
+    }
+
+    /**
+     * Matches namespace strings against a wildcard pattern matcher
+     *
+     * @param patternMatcher pattern matcher
+     * @param namespaces A set of namespaces to process
+     * @param appendNamedParameters To append named parameters or not
+     *
+     * @since 2.5.23
+     * See WW-5065
+     */
+    public NamespaceMatcher(PatternMatcher<?> patternMatcher, Set<String> namespaces, boolean appendNamedParameters) {
+        super(patternMatcher, appendNamedParameters);
         for (String name : namespaces) {
             if (!patternMatcher.isLiteral(name)) {
                 addPattern(name, new NamespaceMatch(name, null), false);
diff --git a/core/src/main/java/com/opensymphony/xwork2/config/providers/XWorkConfigurationProvider.java b/core/src/main/java/com/opensymphony/xwork2/config/providers/XWorkConfigurationProvider.java
index cffb451..63d8fab 100644
--- a/core/src/main/java/com/opensymphony/xwork2/config/providers/XWorkConfigurationProvider.java
+++ b/core/src/main/java/com/opensymphony/xwork2/config/providers/XWorkConfigurationProvider.java
@@ -225,6 +225,7 @@
         props.setProperty(StrutsConstants.STRUTS_CONFIGURATION_XML_RELOAD, Boolean.FALSE.toString());
         props.setProperty(StrutsConstants.STRUTS_ALLOW_STATIC_METHOD_ACCESS, Boolean.FALSE.toString());
         props.setProperty(StrutsConstants.STRUTS_ALLOW_STATIC_FIELD_ACCESS, Boolean.TRUE.toString());
+        props.setProperty(StrutsConstants.STRUTS_MATCHER_APPEND_NAMED_PARAMETERS, Boolean.TRUE.toString());
     }
 
 }
diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index a55ed62..b48b8be 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -346,4 +346,7 @@
     public static final String STRUTS_DISALLOW_PROXY_MEMBER_ACCESS = "struts.disallowProxyMemberAccess";
 
     public static final String STRUTS_OGNL_AUTO_GROWTH_COLLECTION_LIMIT = "struts.ognl.autoGrowthCollectionLimit";
+
+    /** See {@link com.opensymphony.xwork2.config.impl.AbstractMatcher#appendNamedParameters */
+    public static final String STRUTS_MATCHER_APPEND_NAMED_PARAMETERS = "struts.matcher.appendNamedParameters";
 }
diff --git a/core/src/test/java/com/opensymphony/xwork2/config/impl/ActionConfigMatcherTest.java b/core/src/test/java/com/opensymphony/xwork2/config/impl/ActionConfigMatcherTest.java
index 7e0a60c..ac52256 100644
--- a/core/src/test/java/com/opensymphony/xwork2/config/impl/ActionConfigMatcherTest.java
+++ b/core/src/test/java/com/opensymphony/xwork2/config/impl/ActionConfigMatcherTest.java
@@ -24,6 +24,7 @@
 import com.opensymphony.xwork2.config.entities.InterceptorMapping;
 import com.opensymphony.xwork2.config.entities.ResultConfig;
 import com.opensymphony.xwork2.util.WildcardHelper;
+import org.apache.struts2.util.RegexPatternMatcher;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -142,6 +143,79 @@
         
     }
 
+    /**
+     * Test to make sure the {@link AbstractMatcher#replaceParameters(Map, Map)} method isn't adding values to the
+     * return value.
+     */
+    public void testReplaceParametersWithNoAppendingParams() {
+        Map<String, ActionConfig> map = new HashMap<>();
+
+        HashMap<String, String> params = new HashMap<>();
+        params.put("first", "{1}");
+
+        ActionConfig config = new ActionConfig.Builder("package", "foo/{one}/{two}/{three}", "foo.bar.Action")
+                .addParams(params)
+                .addExceptionMapping(new ExceptionMappingConfig.Builder("foo{1}", "java.lang.{2}Exception", "success{1}")
+                        .addParams(new HashMap<>(params))
+                        .build())
+                .addResultConfig(new ResultConfig.Builder("success{1}", "foo.{2}").addParams(params).build())
+                .setStrictMethodInvocation(false)
+                .build();
+        map.put("foo/{one}/{two}/{three}", config);
+        ActionConfigMatcher replaceMatcher = new ActionConfigMatcher(new RegexPatternMatcher(), map, false, false);
+        ActionConfig matched = replaceMatcher.match("foo/paramOne/paramTwo/paramThree");
+        assertNotNull("ActionConfig should be matched", matched);
+
+        // Verify all The ActionConfig, ExceptionConfig, and ResultConfig have the correct number of params
+        assertEquals("The ActionConfig should have the correct number of params", 1, matched.getParams().size());
+        assertEquals("The ExceptionMappingConfigs should have the correct number of params", 1, matched.getExceptionMappings().get(0).getParams().size());
+        assertEquals("The ResultConfigs should have the correct number of params", 1, matched.getResults().get("successparamOne").getParams().size());
+
+        // Verify the params are still getting their values replaced correctly
+        assertEquals("The ActionConfig params have replaced values", "paramOne", matched.getParams().get("first"));
+        assertEquals("The ActionConfig params have replaced values", "paramOne", matched.getExceptionMappings().get(0).getParams().get("first"));
+        assertEquals("The ActionConfig params have replaced values", "paramOne", matched.getResults().get("successparamOne").getParams().get("first"));
+    }
+
+    /**
+     * Test to make sure the {@link AbstractMatcher#replaceParameters(Map, Map)} method is adding values to the
+     * return value.
+     */
+    public void testReplaceParametersWithAppendingParams() {
+        Map<String, ActionConfig> map = new HashMap<>();
+
+        HashMap<String, String> params = new HashMap<>();
+        params.put("first", "{1}");
+
+        ActionConfig config = new ActionConfig.Builder("package", "foo/{one}/{two}/{three}", "foo.bar.Action")
+                .addParams(params)
+                .addExceptionMapping(new ExceptionMappingConfig.Builder("foo{1}", "java.lang.{2}Exception", "success{1}")
+                        .addParams(new HashMap<>(params))
+                        .build())
+                .addResultConfig(new ResultConfig.Builder("success{1}", "foo.{2}").addParams(params).build())
+                .setStrictMethodInvocation(false)
+                .build();
+        map.put("foo/{one}/{two}/{three}", config);
+        ActionConfigMatcher replaceMatcher = new ActionConfigMatcher(new RegexPatternMatcher(), map, false, true);
+        ActionConfig matched = replaceMatcher.match("foo/paramOne/paramTwo/paramThree");
+        assertNotNull("ActionConfig should be matched", matched);
+
+        assertEquals(4, matched.getParams().size());
+        assertEquals(4, matched.getExceptionMappings().get(0).getParams().size());
+        assertEquals(4, matched.getResults().get("successparamOne").getParams().size());
+
+        // Verify the params are still getting their values replaced correctly
+        assertEquals("paramOne", matched.getParams().get("first"));
+        assertEquals("paramOne", matched.getParams().get("one"));
+        assertEquals("paramTwo", matched.getParams().get("two"));
+        assertEquals("paramThree", matched.getParams().get("three"));
+        assertEquals("paramOne", matched.getExceptionMappings().get(0).getParams().get("first"));
+        assertEquals("paramOne", matched.getExceptionMappings().get(0).getParams().get("one"));
+        assertEquals("paramTwo", matched.getExceptionMappings().get(0).getParams().get("two"));
+        assertEquals("paramThree", matched.getExceptionMappings().get(0).getParams().get("three"));
+        assertEquals("paramOne", matched.getResults().get("successparamOne").getParams().get("first"));
+    }
+
     private Map<String,ActionConfig> buildActionConfigMap() {
         Map<String, ActionConfig> map = new HashMap<>();
 
diff --git a/plugins/portlet/pom.xml b/plugins/portlet/pom.xml
index 31d35c5..0b6576b 100644
--- a/plugins/portlet/pom.xml
+++ b/plugins/portlet/pom.xml
@@ -102,6 +102,15 @@
         </dependency>
 
         <dependency>
+            <!-- Requires ASM 3.3.1 for testing, as jmock-cglib apparently overrides a method made final in later ASM versions
+                 (and apparently jmock-cglib still requires ASM, despite no visible tree dependency). -->
+            <groupId>asm</groupId>
+            <artifactId>asm</artifactId>
+            <version>3.3.1</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
             <groupId>mockobjects</groupId>
             <artifactId>mockobjects-core</artifactId>
             <scope>test</scope>
diff --git a/pom.xml b/pom.xml
index a551c0d..4c83307 100644
--- a/pom.xml
+++ b/pom.xml
@@ -751,6 +751,21 @@
                 <artifactId>asm-commons</artifactId>
                 <version>${asm.version}</version>
             </dependency>
+
+            <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-digester3</artifactId>
+                <optional>true</optional>
+                <!-- Prevent inclusion of ASM 3.3.1 via transitive dependency from velocity-tools-view/commons-digester3/cglib 2.2.2
+                     (its presence creates classpath conflicts / duplicate ASM jars due to ASM groupId changing after 3.x).  -->
+                <exclusions>
+                    <exclusion>
+                        <groupId>asm</groupId>
+                        <artifactId>asm</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
             <dependency>
                 <groupId>junit</groupId>
                 <artifactId>junit</artifactId>