Merge pull request #912 from atlassian-forks/issue/WW-5408-add-option-to-not-fallback-to-empty-namespace-when-unresolved

WW-5408 add option to not fallback to empty namespace when unresolved
diff --git a/core/src/main/java/com/opensymphony/xwork2/XWorkTestCase.java b/core/src/main/java/com/opensymphony/xwork2/XWorkTestCase.java
index 88790fc..8a4695e 100644
--- a/core/src/main/java/com/opensymphony/xwork2/XWorkTestCase.java
+++ b/core/src/main/java/com/opensymphony/xwork2/XWorkTestCase.java
@@ -36,6 +36,8 @@
 import java.util.Locale;
 import java.util.Map;
 
+import static java.util.Collections.singletonMap;
+
 /**
  * Base JUnit TestCase to extend for XWork specific JUnit tests. Uses
  * the generic test setup for logic.
@@ -56,9 +58,7 @@
     @Override
     protected void setUp() throws Exception {
         configurationManager = XWorkTestCaseHelper.setUp();
-        configuration = configurationManager.getConfiguration();
-        container = configuration.getContainer();
-        actionProxyFactory = container.getInstance(ActionProxyFactory.class);
+        reloadConfiguration(configurationManager);
     }
 
     @Override
@@ -66,13 +66,17 @@
         XWorkTestCaseHelper.tearDown(configurationManager);
     }
 
-    protected void loadConfigurationProviders(ConfigurationProvider... providers) {
-        configurationManager = XWorkTestCaseHelper.loadConfigurationProviders(configurationManager, providers);
+    private void reloadConfiguration(ConfigurationManager configurationManager) {
         configuration = configurationManager.getConfiguration();
         container = configuration.getContainer();
         actionProxyFactory = container.getInstance(ActionProxyFactory.class);
     }
 
+    protected void loadConfigurationProviders(ConfigurationProvider... providers) {
+        configurationManager = XWorkTestCaseHelper.loadConfigurationProviders(configurationManager, providers);
+        reloadConfiguration(configurationManager);
+    }
+
     protected void loadButSet(Map<String, ?> properties) {
         loadConfigurationProviders(new StubConfigurationProvider() {
             @Override
@@ -115,4 +119,25 @@
             .getContextMap();
     }
 
+    protected void setStrutsConstant(String constant, String value) {
+        setStrutsConstant(singletonMap(constant, value));
+    }
+
+    protected void setStrutsConstant(final Map<String, String> overwritePropeties) {
+        configurationManager.addContainerProvider(new StubConfigurationProvider() {
+            @Override
+            public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException {
+                for (Map.Entry<String, String> stringStringEntry : overwritePropeties.entrySet()) {
+                    props.setProperty(stringStringEntry.getKey(), stringStringEntry.getValue(), null);
+                }
+            }
+
+            @Override
+            public void destroy() {
+            }
+        });
+
+        configurationManager.reload();
+        reloadConfiguration(configurationManager);
+    }
 }
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 7bf0e7c..7c725e1 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
@@ -120,6 +120,7 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
@@ -459,9 +460,12 @@
         boolean appendNamedParameters = Boolean.parseBoolean(
                 container.getInstance(String.class, StrutsConstants.STRUTS_MATCHER_APPEND_NAMED_PARAMETERS)
         );
+        boolean fallbackToEmptyNamespace = Boolean.parseBoolean(
+                Optional.ofNullable(container.getInstance(String.class, StrutsConstants.STRUTS_ACTION_CONFIG_FALLBACK_TO_EMPTY_NAMESPACE)).orElse("true")
+        );
 
         return new RuntimeConfigurationImpl(Collections.unmodifiableMap(namespaceActionConfigs),
-                Collections.unmodifiableMap(namespaceConfigs), matcher, appendNamedParameters);
+                Collections.unmodifiableMap(namespaceConfigs), matcher, appendNamedParameters, fallbackToEmptyNamespace);
     }
 
     private void setDefaultResults(Map<String, ResultConfig> results, PackageConfig packageContext) {
@@ -536,14 +540,17 @@
         private final Map<String, ActionConfigMatcher> namespaceActionConfigMatchers;
         private final NamespaceMatcher namespaceMatcher;
         private final Map<String, String> namespaceConfigs;
+        private final boolean fallbackToEmptyNamespace;
 
         public RuntimeConfigurationImpl(Map<String, Map<String, ActionConfig>> namespaceActionConfigs,
                                         Map<String, String> namespaceConfigs,
                                         PatternMatcher<int[]> matcher,
-                                        boolean appendNamedParameters)
+                                        boolean appendNamedParameters,
+                                        boolean fallbackToEmptyNamespace)
         {
             this.namespaceActionConfigs = namespaceActionConfigs;
             this.namespaceConfigs = namespaceConfigs;
+            this.fallbackToEmptyNamespace = fallbackToEmptyNamespace;
 
             this.namespaceActionConfigMatchers = new LinkedHashMap<>();
             this.namespaceMatcher = new NamespaceMatcher(matcher, namespaceActionConfigs.keySet(), appendNamedParameters);
@@ -583,14 +590,17 @@
             }
 
             // fail over to empty namespace
-            if (config == null && StringUtils.isNotBlank(namespace)) {
+            if (config == null && shouldFallbackToEmptyNamespace(namespace)) {
                 config = findActionConfigInNamespace("", name);
             }
 
-
             return config;
         }
 
+        private boolean shouldFallbackToEmptyNamespace(String namespace) {
+            return StringUtils.isNotBlank(namespace) && ("/".equals(namespace) || fallbackToEmptyNamespace);
+        }
+
         private ActionConfig findActionConfigInNamespace(String namespace, String name) {
             ActionConfig config = null;
             if (namespace == null) {
diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index 10d9fa5..91b8eb2 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -230,6 +230,8 @@
     public static final String STRUTS_XWORKCONVERTER = "struts.xworkConverter";
 
     public static final String STRUTS_ALWAYS_SELECT_FULL_NAMESPACE = "struts.mapper.alwaysSelectFullNamespace";
+    /** Fallback to empty namespace when request namespace didn't match any in action configuration */
+    public static final String STRUTS_ACTION_CONFIG_FALLBACK_TO_EMPTY_NAMESPACE = "struts.actionConfig.fallbackToEmptyNamespace";
 
     /** The {@link com.opensymphony.xwork2.LocaleProviderFactory} implementation class */
     public static final String STRUTS_LOCALE_PROVIDER_FACTORY = "struts.localeProviderFactory";
diff --git a/core/src/main/java/org/apache/struts2/config/entities/ConstantConfig.java b/core/src/main/java/org/apache/struts2/config/entities/ConstantConfig.java
index 6106aad..11b30c2 100644
--- a/core/src/main/java/org/apache/struts2/config/entities/ConstantConfig.java
+++ b/core/src/main/java/org/apache/struts2/config/entities/ConstantConfig.java
@@ -90,6 +90,7 @@
     private Boolean freemarkerWrapperAltMap;
     private BeanConfig xworkConverter;
     private Boolean mapperAlwaysSelectFullNamespace;
+    private Boolean actionConfigFallbackToEmptyNamespace;
     private BeanConfig localeProviderFactory;
     private String mapperIdParameterName;
     private Boolean ognlAllowStaticFieldAccess;
@@ -226,6 +227,7 @@
         map.put(StrutsConstants.STRUTS_FREEMARKER_WRAPPER_ALT_MAP, Objects.toString(freemarkerWrapperAltMap, null));
         map.put(StrutsConstants.STRUTS_XWORKCONVERTER, beanConfToString(xworkConverter));
         map.put(StrutsConstants.STRUTS_ALWAYS_SELECT_FULL_NAMESPACE, Objects.toString(mapperAlwaysSelectFullNamespace, null));
+        map.put(StrutsConstants.STRUTS_ACTION_CONFIG_FALLBACK_TO_EMPTY_NAMESPACE, Objects.toString(actionConfigFallbackToEmptyNamespace, null));
         map.put(StrutsConstants.STRUTS_LOCALE_PROVIDER_FACTORY, beanConfToString(localeProviderFactory));
         map.put(StrutsConstants.STRUTS_ID_PARAMETER_NAME, mapperIdParameterName);
         map.put(StrutsConstants.STRUTS_ALLOW_STATIC_FIELD_ACCESS, Objects.toString(ognlAllowStaticFieldAccess, null));
@@ -814,6 +816,14 @@
         this.mapperAlwaysSelectFullNamespace = mapperAlwaysSelectFullNamespace;
     }
 
+    public Boolean getActionConfigFallbackToEmptyNamespace() {
+        return actionConfigFallbackToEmptyNamespace;
+    }
+
+    public void setActionConfigFallbackToEmptyNamespace(Boolean actionConfigFallbackToEmptyNamespace) {
+        this.actionConfigFallbackToEmptyNamespace = actionConfigFallbackToEmptyNamespace;
+    }
+
     public BeanConfig getLocaleProviderFactory() {
         return localeProviderFactory;
     }
diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties
index 96c5459..8b7226a 100644
--- a/core/src/main/resources/org/apache/struts2/default.properties
+++ b/core/src/main/resources/org/apache/struts2/default.properties
@@ -215,6 +215,9 @@
 ### Whether to always select the namespace to be everything before the last slash or not
 struts.mapper.alwaysSelectFullNamespace=false
 
+### Whether to fallback to empty namespace when request namespace does not match any in configuration
+struts.actionConfig.fallbackToEmptyNamespace=true
+
 ### Whether to allow static field access in OGNL expressions or not
 struts.ognl.allowStaticFieldAccess=true
 
diff --git a/core/src/test/java/com/opensymphony/xwork2/config/ConfigurationTest.java b/core/src/test/java/com/opensymphony/xwork2/config/ConfigurationTest.java
index b52b9d4..520f8c2 100644
--- a/core/src/test/java/com/opensymphony/xwork2/config/ConfigurationTest.java
+++ b/core/src/test/java/com/opensymphony/xwork2/config/ConfigurationTest.java
@@ -31,6 +31,7 @@
 import com.opensymphony.xwork2.mock.MockInterceptor;
 import com.opensymphony.xwork2.test.StubConfigurationProvider;
 import com.opensymphony.xwork2.util.location.LocatableProperties;
+import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.config.StrutsXmlConfigurationProvider;
 import org.apache.struts2.dispatcher.HttpParameters;
 
@@ -239,6 +240,41 @@
         mockContainerProvider.verify();
     }
 
+    public void testGetActionConfigFallbackToEmptyNamespaceWhenNamespaceDontMatchAndEmptyNamespaceFallbackIsEnabled() {
+        // struts.actionConfig.fallbackToEmptyNamespace default to true, so it is enabled
+        RuntimeConfiguration configuration = configurationManager.getConfiguration().getRuntimeConfiguration();
+
+        // check namespace that doesn't match fallback to empty namespace
+        ActionConfig actionConfig = configuration.getActionConfig("/something/that/is/not/in/the/namespace/config", "LazyFoo");
+        assertEquals("default", actionConfig.getPackageName()); // fallback to empty namespace (package name is default)
+        assertEquals("LazyFoo", actionConfig.getName());
+
+        // check non-empty namespace and name in config still matches
+        assertNotNull(configuration.getActionConfig("includeTest", "Foo"));
+
+        // check root namespace and name in config still matches
+        actionConfig = configuration.getActionConfig("/", "LazyFoo");
+        assertEquals("default", actionConfig.getPackageName());
+        assertEquals("LazyFoo", actionConfig.getName());
+    }
+
+    public void testGetActionConfigReturnNullWhenNamespaceDontMatchAndEmptyNamespaceFallbackIsDisabled() {
+        // set the struts.actionConfig.fallbackToEmptyNamespace to false and reload the configuration
+        setStrutsConstant(StrutsConstants.STRUTS_ACTION_CONFIG_FALLBACK_TO_EMPTY_NAMESPACE, "false");
+        RuntimeConfiguration configuration = configurationManager.getConfiguration().getRuntimeConfiguration();
+
+        // check namespace that doesn't match NOT fallback to empty namespace and return null
+        assertNull(configuration.getActionConfig("/something/that/is/not/in/the/namespace/config", "LazyFoo"));
+
+        // check non-empty namespace and name in config still matches
+        assertNotNull(configuration.getActionConfig("includeTest", "Foo"));
+
+        // check root namespace and name in config still matches
+        ActionConfig actionConfig = configuration.getActionConfig("/", "LazyFoo");
+        assertEquals("default", actionConfig.getPackageName());
+        assertEquals("LazyFoo", actionConfig.getName());
+    }
+
     public void testInitForPackageProviders() {
 
         loadConfigurationProviders(new StubConfigurationProvider() {
diff --git a/core/src/test/java/org/apache/struts2/views/jsp/ui/DebugTagTest.java b/core/src/test/java/org/apache/struts2/views/jsp/ui/DebugTagTest.java
index 7f4b545..b7db515 100644
--- a/core/src/test/java/org/apache/struts2/views/jsp/ui/DebugTagTest.java
+++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/DebugTagTest.java
@@ -217,23 +217,9 @@
     /**
      * Overwrite the Struts Constant and reload container
      */
-    private void setStrutsConstant(final Map<String, String> overwritePropeties) {
-        configurationManager.addContainerProvider(new StubConfigurationProvider() {
-            @Override
-            public boolean needsReload() {
-                return true;
-            }
-
-            @Override
-            public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException {
-                for (Map.Entry<String, String> stringStringEntry : overwritePropeties.entrySet()) {
-                    props.setProperty(stringStringEntry.getKey(), stringStringEntry.getValue(), null);
-                }
-            }
-        });
-
-        configurationManager.reload();
-        container = configurationManager.getConfiguration().getContainer();
+    @Override
+    protected void setStrutsConstant(final Map<String, String> overwritePropeties) {
+        super.setStrutsConstant(overwritePropeties);
         stack.getActionContext().withContainer(container);
     }
-}
\ No newline at end of file
+}