Commits

musachy  committed 9d51abb

XW-640 Support multiple unknown handlers

git-svn-id: http://svn.opensymphony.com/svn/xwork/trunk@1886e221344d-f017-0410-9bd5-d282ab1896d7

  • Participants
  • Parent commits 0d45d87

Comments (0)

Files changed (19)

File src/java/com/opensymphony/xwork2/DefaultActionInvocation.java

  */
 package com.opensymphony.xwork2;
 
+import com.opensymphony.xwork2.config.Configuration;
 import com.opensymphony.xwork2.config.ConfigurationException;
 import com.opensymphony.xwork2.config.entities.ActionConfig;
 import com.opensymphony.xwork2.config.entities.InterceptorMapping;
     protected ActionEventListener actionEventListener;
     protected ValueStackFactory valueStackFactory;
     protected Container container;
-    protected UnknownHandler unknownHandler;
+    private Configuration configuration;
+    protected UnknownHandlerManager unknownHandlerManager;
 
     public DefaultActionInvocation(final Map<String, Object> extraContext, final boolean pushAction) {
         DefaultActionInvocation.this.extraContext = extraContext;
     }
 
     @Inject
+    public void setUnknownHandlerManager(UnknownHandlerManager unknownHandlerManager) {
+        this.unknownHandlerManager = unknownHandlerManager;
+    }
+
+    @Inject
     public void setValueStackFactory(ValueStackFactory fac) {
         this.valueStackFactory = fac;
     }
 
     @Inject
+    public void setConfiguration(Configuration configuration) {
+        this.configuration = configuration;
+    }
+
+    @Inject
     public void setObjectFactory(ObjectFactory fac) {
         this.objectFactory = fac;
     }
         this.container = cont;
     }
 
-    @Inject(required = false)
-    public void setUnknownHandler(UnknownHandler hand) {
-        this.unknownHandler = hand;
-    }
-
-    @Inject(required = false)
+    @Inject(required=false)
     public void setActionEventListener(ActionEventListener listener) {
         this.actionEventListener = listener;
     }
         if (explicitResult != null) {
             Result ret = explicitResult;
             explicitResult = null;
-            
+
             return ret;
         }
         ActionConfig config = proxy.getConfig();
                 LOG.error("There was an exception while instantiating the result of type " + resultConfig.getClassName(), e);
                 throw new XWorkException(e, resultConfig);
             }
-        } else if (resultCode != null && !Action.NONE.equals(resultCode) && unknownHandler != null) {
-            return unknownHandler.handleUnknownResult(invocationContext, proxy.getActionName(), proxy.getConfig(), resultCode);
+        } else if (resultCode != null && !Action.NONE.equals(resultCode) && unknownHandlerManager.hasUnknownHandlers()) {
+            return unknownHandlerManager.handleUnknownResult(invocationContext, proxy.getActionName(), proxy.getConfig(), resultCode);
         }
         return null;
     }
                 String interceptorMsg = "interceptor: " + interceptor.getName();
                 UtilTimerStack.push(interceptorMsg);
                 try {
-                    resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
-                }
+                                resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
+                            }
                 finally {
                     UtilTimerStack.pop(interceptorMsg);
                 }
                     method = getAction().getClass().getMethod(altMethodName, new Class[0]);
                 } catch (NoSuchMethodException e1) {
                     // well, give the unknown handler a shot
-                    if (unknownHandler != null) {
+                    if (unknownHandlerManager.hasUnknownHandlers()) {
                         try {
-                            methodResult = unknownHandler.handleUnknownActionMethod(action, methodName);
+                            methodResult = unknownHandlerManager.handleUnknownMethod(action, methodName);
                             methodCalled = true;
                         } catch (NoSuchMethodException e2) {
                             // throw the original one

File src/java/com/opensymphony/xwork2/DefaultUnknownHandlerManager.java

+/*
+ * Copyright (c) 2002-2006 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork2;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.Result;
+import com.opensymphony.xwork2.UnknownHandler;
+import com.opensymphony.xwork2.UnknownHandlerManager;
+import com.opensymphony.xwork2.config.Configuration;
+import com.opensymphony.xwork2.config.entities.ActionConfig;
+import com.opensymphony.xwork2.config.entities.UnknownHandlerConfig;
+import com.opensymphony.xwork2.inject.Container;
+import com.opensymphony.xwork2.inject.Inject;
+
+/**
+ * Default implementation of UnknownHandlerManager
+ *
+ * @see com.opensymphony.xwork2.UnknownHandlerManager
+ */
+public class DefaultUnknownHandlerManager implements UnknownHandlerManager {
+    protected ArrayList<UnknownHandler> unknownHandlers;
+    private Configuration configuration;
+    private Container container;
+
+    @Inject
+    public void setConfiguration(Configuration configuration) {
+        this.configuration = configuration;
+        build();
+    }
+
+    @Inject
+    public void setContainer(Container container) {
+        this.container = container;
+        build();
+    }
+
+    /**
+     * Builds a list of UnknowHandlers in the order specified by the configured "unknown-handler-stack".
+     * If "unknown-handler-stack" was not configured, all UnknowHandlers will be returned, in no specific order
+     */
+    protected void build() {
+        if (configuration != null && container != null) {
+            List<UnknownHandlerConfig> unkownHandlerStack = configuration.getUnknownHandlerStack();
+            unknownHandlers = new ArrayList<UnknownHandler>();
+
+            if (unkownHandlerStack != null && !unkownHandlerStack.isEmpty()) {
+                //get UnknownHandlers in the specified order
+                for (UnknownHandlerConfig unknownHandlerConfig : unkownHandlerStack) {
+                    UnknownHandler uh = container.getInstance(UnknownHandler.class, unknownHandlerConfig.getName());
+                    unknownHandlers.add(uh);
+                }
+            } else {
+                //add all available UnknownHandlers
+                Set<String> unknowHandlerNames = container.getInstanceNames(UnknownHandler.class);
+                if (unknowHandlerNames != null) {
+                    for (String unknowHandlerName : unknowHandlerNames) {
+                        UnknownHandler uh = container.getInstance(UnknownHandler.class, unknowHandlerName);
+                        unknownHandlers.add(uh);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Iterate over UnknownHandlers and return the result of the first one that can handle it
+     */
+    public Result handleUnknownResult(ActionContext actionContext, String actionName, ActionConfig actionConfig, String resultCode) {
+        for (UnknownHandler unknownHandler : unknownHandlers) {
+            Result result = unknownHandler.handleUnknownResult(actionContext, actionName, actionConfig, resultCode);
+            if (result != null)
+                return result;
+        }
+
+        return null;
+    }
+
+    /**
+     * Iterate over UnknownHandlers and return the result of the first one that can handle it
+     *
+     * @throws NoSuchMethodException
+     */
+    public Object handleUnknownMethod(Object action, String methodName) throws NoSuchMethodException {
+        for (UnknownHandler unknownHandler : unknownHandlers) {
+            Object result = unknownHandler.handleUnknownActionMethod(action, methodName);
+            if (result != null)
+                return result;
+        }
+
+        return null;
+    }
+
+    /**
+     * Iterate over UnknownHandlers and return the result of the first one that can handle it
+     *
+     * @throws NoSuchMethodException
+     */
+    public ActionConfig handleUnknownAction(String namespace, String actionName) {
+        for (UnknownHandler unknownHandler : unknownHandlers) {
+            ActionConfig result = unknownHandler.handleUnknownAction(namespace, actionName);
+            if (result != null)
+                return result;
+        }
+
+        return null;
+    }
+
+    public boolean hasUnknownHandlers() {
+        return unknownHandlers != null && !unknownHandlers.isEmpty();
+    }
+
+    public List<UnknownHandler> getUnknownHandlers() {
+        return unknownHandlers;
+    }
+}

File src/java/com/opensymphony/xwork2/TestNGXWorkTestCase.java

     protected void setUp() throws Exception {
         configurationManager = XWorkTestCaseHelper.setUp();
         configuration = new MockConfiguration();
+        ((MockConfiguration)configuration).selfRegister();
         container = configuration.getContainer();
         actionProxyFactory = container.getInstance(ActionProxyFactory.class);
     }

File src/java/com/opensymphony/xwork2/UnknownHandlerManager.java

+/*
+ * Copyright (c) 2002-2006 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork2;
+
+import com.opensymphony.xwork2.config.entities.ActionConfig;
+
+import java.util.List;
+
+/**
+ * An unknown handler manager contains a list of UnknownHandler and iterates on them by order
+ *
+ * @see com.opensymphony.xwork2.DefaultUnknownHandlerManager
+ */
+public interface UnknownHandlerManager {
+    Result handleUnknownResult(ActionContext actionContext, String actionName, ActionConfig actionConfig, String resultCode);
+
+    Object handleUnknownMethod(Object action, String methodName) throws NoSuchMethodException;
+
+    ActionConfig handleUnknownAction(String namespace, String actionName);
+
+    boolean hasUnknownHandlers();
+
+    List<UnknownHandler> getUnknownHandlers();
+}

File src/java/com/opensymphony/xwork2/config/Configuration.java

 package com.opensymphony.xwork2.config;
 
 import com.opensymphony.xwork2.config.entities.PackageConfig;
+import com.opensymphony.xwork2.config.entities.UnknownHandlerConfig;
 import com.opensymphony.xwork2.inject.Container;
 
 import java.io.Serializable;
  * @author Mike
  */
 public interface Configuration extends Serializable {
-    
+
     void rebuildRuntimeConfiguration();
 
     PackageConfig getPackageConfig(String name);
     Container getContainer();
 
     Set<String> getLoadedFileNames();
+
+    /**
+     * @since 2.1
+     * @return list of unknown handlers
+     */
+    List<UnknownHandlerConfig> getUnknownHandlerStack();
+
+    /**
+     * @since 2.1
+     * @param unknownHandlerStack
+     */
+    void setUnknownHandlerStack(List<UnknownHandlerConfig> unknownHandlerStack);
 }

File src/java/com/opensymphony/xwork2/config/entities/UnknownHandlerConfig.java

+/*
+ * Copyright (c) 2002-2006 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork2.config.entities;
+
+public class UnknownHandlerConfig {
+    private String name;
+
+    public UnknownHandlerConfig(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}

File src/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java

     protected Container container;
     protected String defaultFrameworkBeanName;
     protected Set<String> loadedFileNames = new TreeSet<String>();
+    protected List<UnknownHandlerConfig> unknownHandlerStack;
 
 
     ObjectFactory objectFactory;
     public DefaultConfiguration() {
         this("xwork");
     }
-    
+
     public DefaultConfiguration(String defaultBeanName) {
         this.defaultFrameworkBeanName = defaultBeanName;
     }
         return packageContexts.get(name);
     }
 
+    public List<UnknownHandlerConfig> getUnknownHandlerStack() {
+        return unknownHandlerStack;
+    }
+
+    public void setUnknownHandlerStack(List<UnknownHandlerConfig> unknownHandlerStack) {
+        this.unknownHandlerStack = unknownHandlerStack;
+    }
+
     public Set<String> getPackageConfigNames() {
         return packageContexts.keySet();
     }
     public Map<String, PackageConfig> getPackageConfigs() {
         return packageContexts;
     }
-    
+
     public Set<String> getLoadedFileNames() {
         return loadedFileNames;
     }
     public RuntimeConfiguration getRuntimeConfiguration() {
         return runtimeConfiguration;
     }
-    
+
     /**
      * @return the container
      */
             if (check.getLocation() != null && packageContext.getLocation() != null
                     && check.getLocation().equals(packageContext.getLocation())) {
                 if (LOG.isDebugEnabled()) {
-                    LOG.debug("The package name '" + name 
-                    + "' is already been loaded by the same location and could be removed: " 
+                    LOG.debug("The package name '" + name
+                    + "' is already been loaded by the same location and could be removed: "
                     + packageContext.getLocation());
-                } 
+                }
             } else {
-                throw new ConfigurationException("The package name '" + name 
+                throw new ConfigurationException("The package name '" + name
                         + "' at location "+packageContext.getLocation()
                         + " is already been used by another package at location " + check.getLocation(),
                         packageContext);
     public void rebuildRuntimeConfiguration() {
         runtimeConfiguration = buildRuntimeConfiguration();
     }
-    
+
     /**
      * Calls the ConfigurationProviderFactory.getConfig() to tell it to reload the configuration and then calls
      * buildRuntimeConfiguration().
      * @throws ConfigurationException
      */
     public synchronized void reload(List<ConfigurationProvider> providers) throws ConfigurationException {
-        
+
         // Silly copy necessary due to lack of ability to cast generic lists
         List<ContainerProvider> contProviders = new ArrayList<ContainerProvider>();
         contProviders.addAll(providers);
-        
+
         reloadContainer(contProviders);
     }
 
             containerProvider.register(builder, props);
         }
         props.setConstants(builder);
-        
+
         builder.factory(Configuration.class, new Factory<Configuration>() {
             public Configuration create(Context context) throws Exception {
                 return DefaultConfiguration.this;
                     packageProviders.add((PackageProvider)containerProvider);
                 }
             }
-            
+
             // Then process any package providers from the plugins
             Set<String> packageProviderNames = container.getInstanceNames(PackageProvider.class);
             if (packageProviderNames != null) {
                     packageProviders.add(provider);
                 }
             }
-    
+
             rebuildRuntimeConfiguration();
         } finally {
             if (oldContext == null) {
         }
         return packageProviders;
     }
-    
+
     protected ActionContext setContext(Container cont) {
         ActionContext context = ActionContext.getContext();
         if (context == null) {
                     ActionConfig baseConfig = actionConfigs.get(actionName);
                     configs.put(actionName, buildFullActionConfig(packageConfig, baseConfig));
                 }
-                
-                
-                
+
+
+
                 namespaceActionConfigs.put(namespace, configs);
                 if (packageConfig.getFullDefaultActionRef() != null) {
                     namespaceConfigs.put(namespace, packageConfig.getFullDefaultActionRef());
             }
         }
 
-        
-        
+
+
         return new ActionConfig.Builder(baseConfig)
             .addParams(params)
             .addResultConfigs(results)
             this.namespaceConfigs = namespaceConfigs;
 
             PatternMatcher<int[]> matcher = container.getInstance(PatternMatcher.class);
-            
+
             this.namespaceActionConfigMatchers = new LinkedHashMap<String, ActionConfigMatcher>();
             this.namespaceMatcher = new NamespaceMatcher(matcher, namespaceActionConfigs.keySet());
 
             return buff.toString();
         }
     }
-    
+
     class ContainerProperties extends LocatableProperties {
         private static final long serialVersionUID = -7320625750836896089L;
 
         public void setConstants(ContainerBuilder builder) {
             for (Object keyobj : keySet()) {
                 String key = (String)keyobj;
-                builder.factory(String.class, key, 
+                builder.factory(String.class, key,
                         new LocatableConstantFactory<String>(getProperty(key), getPropertyLocation(key)));
             }
         }

File src/java/com/opensymphony/xwork2/config/impl/MockConfiguration.java

 
 import com.opensymphony.xwork2.config.*;
 import com.opensymphony.xwork2.config.entities.PackageConfig;
+import com.opensymphony.xwork2.config.entities.UnknownHandlerConfig;
 import com.opensymphony.xwork2.config.providers.XWorkConfigurationProvider;
 import com.opensymphony.xwork2.inject.Container;
 import com.opensymphony.xwork2.inject.ContainerBuilder;
+import com.opensymphony.xwork2.inject.Scope;
 import com.opensymphony.xwork2.util.location.LocatableProperties;
 
 import java.util.*;
     private Map<String, PackageConfig> packages = new HashMap<String, PackageConfig>();
     private Set<String> loadedFiles = new HashSet<String>();
     private Container container;
-    
+    protected List<UnknownHandlerConfig> unknownHandlerStack;
+    private ContainerBuilder builder;
+
     public MockConfiguration() {
-        ContainerBuilder builder = new ContainerBuilder();
+        builder = new ContainerBuilder();
+    }
+
+    public void selfRegister() {
+        //this cannot be done in the constructor, as it causes an infinite loop
+        builder.factory(Configuration.class, MockConfiguration.class, Scope.SINGLETON);
         LocatableProperties props = new LocatableProperties();
         new XWorkConfigurationProvider().register(builder, props);
         builder.constant("devMode", "false");
             throws ConfigurationException {
         throw new UnsupportedOperationException();
     }
+
+    public List<UnknownHandlerConfig> getUnknownHandlerStack() {
+        return unknownHandlerStack;
+    }
+
+    public void setUnknownHandlerStack(List<UnknownHandlerConfig> unknownHandlerStack) {
+        this.unknownHandlerStack = unknownHandlerStack;
+    }
 }

File src/java/com/opensymphony/xwork2/config/providers/XWorkConfigurationProvider.java

 import com.opensymphony.xwork2.ActionProxyFactory;
 import com.opensymphony.xwork2.DefaultActionProxyFactory;
 import com.opensymphony.xwork2.DefaultTextProvider;
+import com.opensymphony.xwork2.DefaultUnknownHandlerManager;
 import com.opensymphony.xwork2.TextProvider;
+import com.opensymphony.xwork2.UnknownHandlerManager;
 import com.opensymphony.xwork2.config.Configuration;
 import com.opensymphony.xwork2.config.ConfigurationException;
 import com.opensymphony.xwork2.config.ConfigurationProvider;
 import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
 import com.opensymphony.xwork2.inject.ContainerBuilder;
 import com.opensymphony.xwork2.inject.Scope;
-import com.opensymphony.xwork2.ognl.*;
-import com.opensymphony.xwork2.ognl.accessor.*;
+import com.opensymphony.xwork2.ognl.ObjectProxy;
+import com.opensymphony.xwork2.ognl.OgnlReflectionContextFactory;
+import com.opensymphony.xwork2.ognl.OgnlReflectionProvider;
+import com.opensymphony.xwork2.ognl.OgnlUtil;
+import com.opensymphony.xwork2.ognl.OgnlValueStackFactory;
+import com.opensymphony.xwork2.ognl.accessor.CompoundRootAccessor;
+import com.opensymphony.xwork2.ognl.accessor.ObjectAccessor;
+import com.opensymphony.xwork2.ognl.accessor.ObjectProxyPropertyAccessor;
+import com.opensymphony.xwork2.ognl.accessor.XWorkCollectionPropertyAccessor;
+import com.opensymphony.xwork2.ognl.accessor.XWorkEnumerationAccessor;
+import com.opensymphony.xwork2.ognl.accessor.XWorkIteratorPropertyAccessor;
+import com.opensymphony.xwork2.ognl.accessor.XWorkListPropertyAccessor;
+import com.opensymphony.xwork2.ognl.accessor.XWorkMapPropertyAccessor;
+import com.opensymphony.xwork2.ognl.accessor.XWorkMethodAccessor;
 import com.opensymphony.xwork2.util.CompoundRoot;
 import com.opensymphony.xwork2.util.PatternMatcher;
 import com.opensymphony.xwork2.util.ValueStackFactory;
 import com.opensymphony.xwork2.util.location.LocatableProperties;
 import com.opensymphony.xwork2.util.reflection.ReflectionContextFactory;
 import com.opensymphony.xwork2.util.reflection.ReflectionProvider;
-import com.opensymphony.xwork2.validator.*;
+import com.opensymphony.xwork2.validator.ActionValidatorManager;
+import com.opensymphony.xwork2.validator.AnnotationActionValidatorManager;
+import com.opensymphony.xwork2.validator.DefaultActionValidatorManager;
+import com.opensymphony.xwork2.validator.DefaultValidatorFactory;
+import com.opensymphony.xwork2.validator.DefaultValidatorFileParser;
+import com.opensymphony.xwork2.validator.ValidatorFactory;
+import com.opensymphony.xwork2.validator.ValidatorFileParser;
 import ognl.MethodAccessor;
 import ognl.PropertyAccessor;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 public class XWorkConfigurationProvider implements ConfigurationProvider {
 
                .factory(PropertyAccessor.class, Object.class.getName(), ObjectAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, Iterator.class.getName(), XWorkIteratorPropertyAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, Enumeration.class.getName(), XWorkEnumerationAccessor.class, Scope.SINGLETON)
+               .factory(UnknownHandlerManager.class, DefaultUnknownHandlerManager.class, Scope.SINGLETON)
                
                // silly workarounds for ognl since there is no way to flush its caches
                .factory(PropertyAccessor.class, List.class.getName(), XWorkListPropertyAccessor.class, Scope.SINGLETON)

File src/java/com/opensymphony/xwork2/config/providers/XmlConfigurationProvider.java

 import com.opensymphony.xwork2.config.ConfigurationProvider;
 import com.opensymphony.xwork2.config.ConfigurationUtil;
 import com.opensymphony.xwork2.config.entities.*;
+import com.opensymphony.xwork2.config.entities.UnknownHandlerConfig;
 import com.opensymphony.xwork2.config.impl.LocatableFactory;
 import com.opensymphony.xwork2.inject.Container;
 import com.opensymphony.xwork2.inject.ContainerBuilder;
         this.errorIfMissing = errorIfMissing;
 
         Map<String, String> mappings = new HashMap<String, String>();
+        mappings.put("-//OpenSymphony Group//XWork 2.1//EN", "xwork-2.1.dtd");
         mappings.put("-//OpenSymphony Group//XWork 2.0//EN", "xwork-2.0.dtd");
         mappings.put("-//OpenSymphony Group//XWork 1.1.1//EN", "xwork-1.1.1.dtd");
         mappings.put("-//OpenSymphony Group//XWork 1.1//EN", "xwork-1.1.dtd");
                         String name = child.getAttribute("name");
                         String value = child.getAttribute("value");
                         props.setProperty(name, value, childNode);
+                    } else if (nodeName.equals("unknown-handler-stack")) {
+                        List<UnknownHandlerConfig> unknownHandlerStack = new ArrayList<UnknownHandlerConfig>();
+                        NodeList unknownHandlers = child.getElementsByTagName("unknown-handler-ref");
+                        int unknownHandlersSize = unknownHandlers.getLength();
+
+                        for (int k = 0; k < unknownHandlersSize; k++) {
+                            Element unknownHandler = (Element) unknownHandlers.item(k);
+                            unknownHandlerStack.add(new UnknownHandlerConfig(unknownHandler.getAttribute("name")));
+                        }
+
+                        if (!unknownHandlerStack.isEmpty())
+                            configuration.setUnknownHandlerStack(unknownHandlerStack);
                     }
                 }
             }

File src/java/xwork-2.1.dtd

+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- START SNIPPET: xworkDtd -->
+
+<!--
+   XWork configuration DTD.
+   Use the following DOCTYPE
+
+   <!DOCTYPE xwork PUBLIC
+	"-//OpenSymphony Group//XWork 2.1//EN"
+	"http://www.opensymphony.com/xwork/xwork-2.1.dtd">
+-->
+
+<!ELEMENT xwork ((package|include|bean|constant)*, unknown-handler-stack?)>
+
+<!ELEMENT package (result-types?, interceptors?, default-interceptor-ref?, default-action-ref?, default-class-ref?, global-results?, global-exception-mappings?, action*)>
+<!ATTLIST package
+    name CDATA #REQUIRED
+    extends CDATA #IMPLIED
+    namespace CDATA #IMPLIED
+    abstract CDATA #IMPLIED
+>
+
+<!ELEMENT result-types (result-type+)>
+
+<!ELEMENT result-type (param*)>
+<!ATTLIST result-type
+    name CDATA #REQUIRED
+    class CDATA #REQUIRED
+    default (true|false) "false"
+>
+
+<!ELEMENT interceptors (interceptor|interceptor-stack)+>
+
+<!ELEMENT interceptor (param*)>
+<!ATTLIST interceptor
+    name CDATA #REQUIRED
+    class CDATA #REQUIRED
+>
+
+<!ELEMENT interceptor-stack (interceptor-ref*)>
+<!ATTLIST interceptor-stack
+    name CDATA #REQUIRED
+>
+
+<!ELEMENT interceptor-ref (param*)>
+<!ATTLIST interceptor-ref
+    name CDATA #REQUIRED
+>
+
+<!ELEMENT default-interceptor-ref (#PCDATA)>
+<!ATTLIST default-interceptor-ref
+    name CDATA #REQUIRED
+>
+
+<!ELEMENT default-action-ref (#PCDATA)>
+<!ATTLIST default-action-ref
+    name CDATA #REQUIRED
+>
+
+<!ELEMENT default-class-ref (#PCDATA)>
+<!ATTLIST default-class-ref
+   class CDATA #REQUIRED
+>
+
+<!ELEMENT global-results (result+)>
+
+<!ELEMENT global-exception-mappings (exception-mapping+)>
+
+<!ELEMENT action (param|result|interceptor-ref|exception-mapping)*>
+<!ATTLIST action
+    name CDATA #REQUIRED
+    class CDATA #IMPLIED
+    method CDATA #IMPLIED
+    converter CDATA #IMPLIED
+>
+
+<!ELEMENT param (#PCDATA)>
+<!ATTLIST param
+    name CDATA #REQUIRED
+>
+
+<!ELEMENT result (#PCDATA|param)*>
+<!ATTLIST result
+    name CDATA #IMPLIED
+    type CDATA #IMPLIED
+>
+
+<!ELEMENT exception-mapping (#PCDATA|param)*>
+<!ATTLIST exception-mapping
+    name CDATA #IMPLIED
+    exception CDATA #REQUIRED
+    result CDATA #REQUIRED
+>
+
+<!ELEMENT include (#PCDATA)>
+<!ATTLIST include
+    file CDATA #REQUIRED
+>
+
+<!ELEMENT bean (#PCDATA)>
+<!ATTLIST bean
+    type CDATA #IMPLIED
+    name CDATA #IMPLIED
+    class CDATA #REQUIRED
+    scope CDATA #IMPLIED
+    static CDATA #IMPLIED
+    optional CDATA #IMPLIED
+>
+
+<!ELEMENT constant (#PCDATA)>
+<!ATTLIST constant
+    name CDATA #REQUIRED
+    value CDATA #REQUIRED
+>
+
+<!ELEMENT unknown-handler-stack (unknown-handler-ref*)>
+<!ELEMENT unknown-handler-ref (#PCDATA)>
+<!ATTLIST unknown-handler-ref
+    name CDATA #REQUIRED
+>
+
+<!-- END SNIPPET: xworkDtd -->
+

File src/test/com/opensymphony/xwork2/ActionInvocationTest.java

 
 import com.opensymphony.xwork2.config.entities.ActionConfig;
 import com.opensymphony.xwork2.config.providers.XmlConfigurationProvider;
+import com.mockobjects.dynamic.Mock;
 
 import java.util.HashMap;
+import java.util.Arrays;
 
 
 /**
                 "baz", "myCommand", null, null);
         assertEquals(SimpleAction.COMMAND_RETURN_CODE, commandActionProxy.execute());
     }
-    
+
     public void testCommandInvocationDoMethod() throws Exception {
         ActionProxy baseActionProxy = actionProxyFactory.createActionProxy(
                 "baz", "doMethodTest", null, null);
         assertEquals("input", baseActionProxy.execute());
     }
-    
+
     public void testCommandInvocationUnknownHandler() throws Exception {
-    	
+
         DefaultActionProxy baseActionProxy = (DefaultActionProxy) actionProxyFactory.createActionProxy(
                 "baz", "unknownMethodTest", "unknownmethod", null);
-        ((DefaultActionInvocation)baseActionProxy.getInvocation()).unknownHandler = new UnknownHandler() {
+        UnknownHandler unknownHandler = new UnknownHandler() {
 			public ActionConfig handleUnknownAction(String namespace, String actionName) throws XWorkException { return null;}
 			public Result handleUnknownResult(ActionContext actionContext, String actionName, ActionConfig actionConfig, String resultCode) throws XWorkException {
 				return null;
 				}
 			}
         };
+
+        UnknownHandlerManagerMock uhm = new UnknownHandlerManagerMock();
+        uhm.addUnknownHandler(unknownHandler);
+        ((DefaultActionInvocation)baseActionProxy.getInvocation()).setUnknownHandlerManager(uhm);
+
         assertEquals("found", baseActionProxy.execute());
     }
-    
+
     public void testResultReturnInvocationAndWired() throws Exception {
         ActionProxy baseActionProxy = actionProxyFactory.createActionProxy(
                 "baz", "resultAction", null, null);

File src/test/com/opensymphony/xwork2/UnknownHandlerManagerMock.java

+package com.opensymphony.xwork2;
+
+import com.opensymphony.xwork2.DefaultUnknownHandlerManager;
+
+import java.util.ArrayList;
+
+/*
+ * Utility class for testing DefaultUnknownHandlerManager, which does not allow to add
+ * UnknownHandlers directly
+ */
+public class UnknownHandlerManagerMock extends DefaultUnknownHandlerManager {
+    public void addUnknownHandler(UnknownHandler uh) {
+        if (this.unknownHandlers == null)
+            this.unknownHandlers = new ArrayList<UnknownHandler>();
+        this.unknownHandlers.add(uh);
+    }
+}

File src/test/com/opensymphony/xwork2/config/providers/ConfigurationTestBase.java

 
     protected ConfigurationProvider buildConfigurationProvider(final String filename) {
         configuration = new MockConfiguration();
+        ((MockConfiguration)configuration).selfRegister();
         container = configuration.getContainer();
-        
+
         XmlConfigurationProvider prov = new XmlConfigurationProvider(filename, true);
         prov.setObjectFactory(container.getInstance(ObjectFactory.class));
         prov.init(configuration);

File src/test/com/opensymphony/xwork2/config/providers/SomeUnknownHandler.java

+/*
+ * Copyright (c) 2002-2006 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork2.config.providers;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.Result;
+import com.opensymphony.xwork2.UnknownHandler;
+import com.opensymphony.xwork2.XWorkException;
+import com.opensymphony.xwork2.config.entities.ActionConfig;
+
+public class SomeUnknownHandler implements UnknownHandler{
+    private ActionConfig actionConfig;
+    private String actionMethodResult;
+
+    public ActionConfig handleUnknownAction(String namespace, String actionName) throws XWorkException {
+        return actionConfig;
+    }
+
+    public Object handleUnknownActionMethod(Object action, String methodName) throws NoSuchMethodException {
+        return actionMethodResult;
+    }
+
+    public Result handleUnknownResult(ActionContext actionContext, String actionName, ActionConfig actionConfig,
+            String resultCode) throws XWorkException {
+        return null;
+    }
+
+    public void setActionConfig(ActionConfig actionConfig) {
+        this.actionConfig = actionConfig;
+    }
+
+    public void setActionMethodResult(String actionMethodResult) {
+        this.actionMethodResult = actionMethodResult;
+    }
+}

File src/test/com/opensymphony/xwork2/config/providers/XmlConfigurationProviderUnknownHandlerStackTest.java

+package com.opensymphony.xwork2.config.providers;
+
+import com.opensymphony.xwork2.UnknownHandlerManager;
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.config.ConfigurationProvider;
+import com.opensymphony.xwork2.config.entities.UnknownHandlerConfig;
+import com.opensymphony.xwork2.DefaultUnknownHandlerManager;
+
+import java.util.List;
+
+public class XmlConfigurationProviderUnknownHandlerStackTest extends ConfigurationTestBase {
+
+    public void testStackWithElements() throws ConfigurationException {
+        final String filename = "com/opensymphony/xwork2/config/providers/xwork-unknownhandler-stack.xml";
+        ConfigurationProvider provider = buildConfigurationProvider(filename);
+        loadConfigurationProviders(provider);
+        configurationManager.reload();
+
+        List<UnknownHandlerConfig> unknownHandlerStack = configuration.getUnknownHandlerStack();
+        assertNotNull(unknownHandlerStack);
+        assertEquals(2, unknownHandlerStack.size());
+
+        assertEquals("uh1", unknownHandlerStack.get(0).getName());
+        assertEquals("uh2", unknownHandlerStack.get(1).getName());
+
+        UnknownHandlerManager unknownHandlerManager = new DefaultUnknownHandlerManager();
+        container.inject(unknownHandlerManager);
+        assertTrue(unknownHandlerManager.hasUnknownHandlers());
+    }
+
+    public void testEmptyStack() throws ConfigurationException {
+        final String filename = "com/opensymphony/xwork2/config/providers/xwork-unknownhandler-stack-empty.xml";
+        ConfigurationProvider provider = buildConfigurationProvider(filename);
+        loadConfigurationProviders(provider);
+        configurationManager.reload();
+
+        List<UnknownHandlerConfig> unknownHandlerStack = configuration.getUnknownHandlerStack();
+        assertNull(unknownHandlerStack);
+    }
+}

File src/test/com/opensymphony/xwork2/config/providers/xwork-unknownhandler-stack-empty.xml

+<!DOCTYPE xwork PUBLIC
+    "-//OpenSymphony Group//XWork 2.1//EN"
+    "http://www.opensymphony.com/xwork/xwork-2.1.dtd"
+ >
+
+<xwork>
+    <bean type="com.opensymphony.xwork2.UnknownHandler" name="uh1" class="com.opensymphony.xwork2.config.providers.SomeUnknownHandler"/>
+    <bean type="com.opensymphony.xwork2.UnknownHandler" name="uh2" class="com.opensymphony.xwork2.config.providers.SomeUnknownHandler"/>
+
+    <unknown-handler-stack>
+    </unknown-handler-stack>
+</xwork>

File src/test/com/opensymphony/xwork2/config/providers/xwork-unknownhandler-stack.xml

+<!DOCTYPE xwork PUBLIC
+    "-//OpenSymphony Group//XWork 2.1//EN"
+    "http://www.opensymphony.com/xwork/xwork-2.1.dtd"
+ >
+
+<xwork>
+    <bean type="com.opensymphony.xwork2.UnknownHandler" name="uh1" class="com.opensymphony.xwork2.config.providers.SomeUnknownHandler"/>
+    <bean type="com.opensymphony.xwork2.UnknownHandler" name="uh2" class="com.opensymphony.xwork2.config.providers.SomeUnknownHandler"/>
+
+    <unknown-handler-stack>
+        <unknown-handler-ref name="uh1" />
+        <unknown-handler-ref name="uh2" />
+    </unknown-handler-stack>
+</xwork>

File src/test/com/opensymphony/xwork2/util/UnknownHandlerManagerTest.java

+package com.opensymphony.xwork2.util;
+
+import java.util.List;
+
+import com.opensymphony.xwork2.UnknownHandler;
+import com.opensymphony.xwork2.UnknownHandlerManager;
+import com.opensymphony.xwork2.UnknownHandlerManagerMock;
+import com.opensymphony.xwork2.DefaultUnknownHandlerManager;
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.config.ConfigurationProvider;
+import com.opensymphony.xwork2.config.providers.ConfigurationTestBase;
+import com.opensymphony.xwork2.config.providers.SomeUnknownHandler;
+
+/**
+ * Test UnknownHandlerUtil
+ */
+public class UnknownHandlerManagerTest extends ConfigurationTestBase {
+
+    public void testStack() throws ConfigurationException {
+        final String filename = "com/opensymphony/xwork2/config/providers/xwork-unknownhandler-stack.xml";
+        ConfigurationProvider provider = buildConfigurationProvider(filename);
+        loadConfigurationProviders(provider);
+        configurationManager.reload();
+
+        UnknownHandlerManager unknownHandlerManager = new DefaultUnknownHandlerManager();
+        container.inject(unknownHandlerManager);
+        List<UnknownHandler> unknownHandlers = unknownHandlerManager.getUnknownHandlers();
+
+        assertNotNull(unknownHandlers);
+        assertEquals(2, unknownHandlers.size());
+
+        UnknownHandler uh1 = unknownHandlers.get(0);
+        UnknownHandler uh2 = unknownHandlers.get(1);
+
+        assertTrue(uh1 instanceof SomeUnknownHandler);
+        assertTrue(uh2 instanceof SomeUnknownHandler);
+    }
+
+    public void testEmptyStack() throws ConfigurationException {
+        final String filename = "com/opensymphony/xwork2/config/providers/xwork-unknownhandler-stack-empty.xml";
+        ConfigurationProvider provider = buildConfigurationProvider(filename);
+        loadConfigurationProviders(provider);
+        configurationManager.reload();
+
+        UnknownHandlerManager unknownHandlerManager = new DefaultUnknownHandlerManager();
+        container.inject(unknownHandlerManager);
+        List<UnknownHandler> unknownHandlers = unknownHandlerManager.getUnknownHandlers();
+
+        assertNotNull(unknownHandlers);
+        assertEquals(2, unknownHandlers.size());
+
+        UnknownHandler uh1 = unknownHandlers.get(0);
+        UnknownHandler uh2 = unknownHandlers.get(1);
+
+        assertTrue(uh1 instanceof SomeUnknownHandler);
+        assertTrue(uh2 instanceof SomeUnknownHandler);
+    }
+
+    public void testInvocationOrder() throws ConfigurationException, NoSuchMethodException {
+        SomeUnknownHandler uh1 = new SomeUnknownHandler();
+        uh1.setActionMethodResult("uh1");
+
+        SomeUnknownHandler uh2 = new SomeUnknownHandler();
+        uh2.setActionMethodResult("uh2");
+
+        UnknownHandlerManagerMock uhm = new UnknownHandlerManagerMock();
+        uhm.addUnknownHandler(uh1);
+        uhm.addUnknownHandler(uh2);
+
+        //should pick the first one
+        assertEquals("uh1", uhm.handleUnknownMethod(null, null));
+
+        //should pick the second one
+        uh1.setActionMethodResult(null);
+        assertEquals("uh2", uhm.handleUnknownMethod(null, null));
+
+        //should not pick any
+        uh1.setActionMethodResult(null);
+        uh2.setActionMethodResult(null);
+        assertEquals(null, uhm.handleUnknownMethod(null, null));
+    }
+}