Add 'plugins/struts2-codebehind-plugin/' from commit '5b30f3ae145ce4aaf231fea299da20d4e9f90725'

git-subtree-dir: plugins/struts2-codebehind-plugin
git-subtree-mainline: f212c8fc73491de4a08db5a30225a68e924798ca
git-subtree-split: 5b30f3ae145ce4aaf231fea299da20d4e9f90725
diff --git a/plugins/struts2-codebehind-plugin/pom.xml b/plugins/struts2-codebehind-plugin/pom.xml
new file mode 100644
index 0000000..ec347f2
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/pom.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * $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.
+ */
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.struts</groupId>
+        <artifactId>struts2-plugins</artifactId>
+        <version>2.5-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>struts2-codebehind-plugin</artifactId>
+    <packaging>jar</packaging>
+    <name>Struts 2 Codebehind Plugin</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>struts2-junit-plugin</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>mockobjects</groupId>
+            <artifactId>mockobjects-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>jsp-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+   </dependencies>
+    <properties>
+    	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+</project>
diff --git a/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/codebehind/CodebehindUnknownHandler.java b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/codebehind/CodebehindUnknownHandler.java
new file mode 100644
index 0000000..430ec65
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/codebehind/CodebehindUnknownHandler.java
@@ -0,0 +1,281 @@
+/*
+ * $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 org.apache.struts2.codebehind;
+
+import com.opensymphony.xwork2.Action;
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.Result;
+import com.opensymphony.xwork2.UnknownHandler;
+import com.opensymphony.xwork2.XWorkException;
+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.InterceptorLocator;
+import com.opensymphony.xwork2.config.entities.PackageConfig;
+import com.opensymphony.xwork2.config.entities.ResultConfig;
+import com.opensymphony.xwork2.config.entities.ResultTypeConfig;
+import com.opensymphony.xwork2.config.providers.InterceptorBuilder;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.util.ClassLoaderUtil;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+
+import javax.servlet.ServletContext;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Uses code-behind conventions to solve the two unknown problems.  
+ */
+public class CodebehindUnknownHandler implements UnknownHandler {
+
+    protected String defaultPackageName;
+    protected ServletContext servletContext;
+    protected Map<String,ResultTypeConfig> resultsByExtension;
+    protected String templatePathPrefix;
+    protected Configuration configuration;
+    protected ObjectFactory objectFactory;
+    
+    protected static final Logger LOG = LoggerFactory.getLogger(CodebehindUnknownHandler.class);
+
+    @Inject
+    public CodebehindUnknownHandler(@Inject("struts.codebehind.defaultPackage") String defaultPackage, 
+                                    @Inject Configuration configuration) {
+
+        this.configuration = configuration;
+        this.defaultPackageName = defaultPackage;
+        resultsByExtension = new LinkedHashMap<String,ResultTypeConfig>();
+        PackageConfig parentPackage = configuration.getPackageConfig(defaultPackageName);
+        if (parentPackage == null) {
+            throw new ConfigurationException("Unknown parent package: "+parentPackage);
+        }    
+        Map<String,ResultTypeConfig> results = parentPackage.getAllResultTypeConfigs();
+        
+        resultsByExtension.put("jsp", results.get("dispatcher"));
+        resultsByExtension.put("vm", results.get("velocity"));
+        resultsByExtension.put("ftl", results.get("freemarker"));
+       
+    }                                
+
+    @Inject("struts.codebehind.pathPrefix")
+    public void setPathPrefix(String prefix) {
+        this.templatePathPrefix=prefix;
+    }
+
+    @Inject
+    public void setServletContext(ServletContext servletContext) {
+        this.servletContext = servletContext;
+    }
+    
+    @Inject
+    public void setObjectFactory(ObjectFactory objectFactory) {
+        this.objectFactory = objectFactory;
+    }
+    
+    public ActionConfig handleUnknownAction(String namespace, String actionName)
+            throws XWorkException {
+        String pathPrefix = determinePath(templatePathPrefix, namespace);
+        ActionConfig actionConfig = null;
+        for (String ext : resultsByExtension.keySet()) {
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Trying to locate unknown action template with extension ."+ext+" in directory "+pathPrefix);
+            }
+            String path = string(pathPrefix, actionName, "." , ext);
+            try {
+                if (locateTemplate(path) != null) {
+                    actionConfig = buildActionConfig(path, namespace, actionName, resultsByExtension.get(ext));
+                    break;
+                }
+            } catch (MalformedURLException e) {
+                LOG.warn("Unable to parse template path: "+path+", skipping...");
+            }
+        }
+        return actionConfig;
+    }
+
+    /** Create a new ActionConfig in the default package, with the default interceptor stack and a single result */
+    protected ActionConfig buildActionConfig(String path, String namespace, String actionName, ResultTypeConfig resultTypeConfig) {
+        final PackageConfig pkg = configuration.getPackageConfig(defaultPackageName);
+        return new ActionConfig.Builder(defaultPackageName, "execute", pkg.getDefaultClassRef())
+                .addInterceptors(InterceptorBuilder.constructInterceptorReference(new InterceptorLocator() {
+                    public Object getInterceptorConfig(String name) {
+                        return pkg.getAllInterceptorConfigs().get(name); // recurse package hiearchy
+                    }
+                }, pkg.getFullDefaultInterceptorRef(),
+                Collections.EMPTY_MAP, null, objectFactory))
+                .addResultConfig(new ResultConfig.Builder(Action.SUCCESS, resultTypeConfig.getClassName())
+                        .addParams(resultTypeConfig.getParams())
+                        .addParam(resultTypeConfig.getDefaultResultParam(), path)
+                        .build())
+                .build();
+    }
+
+    public Result handleUnknownResult(ActionContext actionContext, String actionName, 
+            ActionConfig actionConfig, String resultCode) throws XWorkException {
+        
+        Result result = null;
+        PackageConfig pkg = configuration.getPackageConfig(actionConfig.getPackageName());
+        String ns = pkg.getNamespace();
+        String pathPrefix = determinePath(templatePathPrefix, ns);
+
+        for (String ext : resultsByExtension.keySet()) {
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Trying to locate result with extension ."+ext+" in directory "+pathPrefix);
+            }
+            String path = string(pathPrefix, actionName, "-", resultCode, "." , ext);
+            try {
+                if (locateTemplate(path) != null) {
+                    result = buildResult(path, resultCode, resultsByExtension.get(ext), actionContext);
+                    break;
+                }
+            } catch (MalformedURLException e) {
+                LOG.warn("Unable to parse template path: "+path+", skipping...");
+            }
+            
+            path = string(pathPrefix, actionName, "." , ext);
+            try {
+                if (locateTemplate(path) != null) {
+                    result = buildResult(path, resultCode, resultsByExtension.get(ext), actionContext);
+                    break;
+                }
+            } catch (MalformedURLException e) {
+                LOG.warn("Unable to parse template path: "+path+", skipping...");
+            }
+        }
+        
+        return result;
+    }
+    
+    protected Result buildResult(String path, String resultCode, ResultTypeConfig config, ActionContext invocationContext) {
+        ResultConfig resultConfig = new ResultConfig.Builder(resultCode, config.getClassName())
+            .addParams(config.getParams())
+            .addParam(config.getDefaultResultParam(), path)
+            .build();
+        try {
+            return objectFactory.buildResult(resultConfig, invocationContext.getContextMap());
+        } catch (Exception e) {
+            throw new XWorkException("Unable to build codebehind result", e, resultConfig);
+        }
+    }
+
+    protected String string(String... parts) {
+        StringBuilder sb = new StringBuilder();
+        for (String part : parts) {
+            sb.append(part);
+        }
+        return sb.toString();
+    }
+
+    protected String joinPaths(boolean leadingSlash, boolean trailingSlash, String... parts) {
+        StringBuilder sb = new StringBuilder();
+        if (leadingSlash) {
+            sb.append("/");
+        }
+        for (String part : parts) {
+            if (sb.length() > 0 && sb.charAt(sb.length()-1) != '/') {
+                sb.append("/");
+            }
+            sb.append(stripSlashes(part));
+        }
+        if (trailingSlash) {
+            if (sb.length() > 0 && sb.charAt(sb.length()-1) != '/') {
+                sb.append("/");
+            }
+        }
+        return sb.toString();
+    }
+
+    protected String determinePath(String prefix, String ns) {        
+        return joinPaths(true, true, prefix, ns);
+    }
+
+    protected String stripLeadingSlash(String path) {
+        String result;
+        if (path != null) {
+            if (path.length() > 0) {
+                if (path.charAt(0) == '/') {
+                    result = path.substring(1);
+                } else {
+                    result = path;
+                }
+            } else {
+                result = path;
+            }
+        } else {
+            result = "";
+        }
+
+        return result;
+    }
+
+    protected String stripTrailingSlash(String path) {
+        String result;
+
+        if (path != null) {
+            if (path.length() > 0) {
+                if (path.charAt(path.length() - 1) == '/') {
+                    result = path.substring(0, path.length()-1);
+                } else {
+                    result = path;
+                }
+            } else {
+                result = path;
+            }
+        } else {
+            result = "";
+        }
+
+        return result;
+    }
+
+    protected String stripSlashes(String path) {
+        return stripLeadingSlash(stripTrailingSlash(path));
+    }
+
+    URL locateTemplate(String path) throws MalformedURLException {
+        URL template = servletContext.getResource(path);
+        if (template != null) {
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Loaded template '" + path + "' from servlet context.");
+            }
+        } else {
+            template = ClassLoaderUtil.getResource(stripLeadingSlash(path), getClass());
+            if (template != null && LOG.isDebugEnabled()) {
+                LOG.debug("Loaded template '" + stripLeadingSlash(path) + "' from class path.");
+            }
+        }
+        return template;
+    }
+
+
+    /**
+     * Not used
+     */
+	public Object handleUnknownActionMethod(Object action, String methodName) throws NoSuchMethodException {
+		throw new NoSuchMethodException();
+	}
+
+}
diff --git a/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Action.java b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Action.java
new file mode 100644
index 0000000..f139202
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Action.java
@@ -0,0 +1,32 @@
+/*
+ * $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 org.apache.struts2.config;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Action {
+    public static final String DEFAULT_NAMESPACE = "__default_namespace__";
+    String namespace() default DEFAULT_NAMESPACE;
+    String name();
+}
diff --git a/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/ClasspathPackageProvider.java b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/ClasspathPackageProvider.java
new file mode 100644
index 0000000..0840e21
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/ClasspathPackageProvider.java
@@ -0,0 +1,761 @@
+/*
+ * $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 org.apache.struts2.config;
+
+import com.opensymphony.xwork2.Action;
+import com.opensymphony.xwork2.config.Configuration;
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.config.PackageProvider;
+import com.opensymphony.xwork2.config.entities.ActionConfig;
+import com.opensymphony.xwork2.config.entities.PackageConfig;
+import com.opensymphony.xwork2.config.entities.ResultConfig;
+import com.opensymphony.xwork2.config.entities.ResultTypeConfig;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.util.ClassLoaderUtil;
+import com.opensymphony.xwork2.util.ResolverUtil;
+import com.opensymphony.xwork2.util.ResolverUtil.ClassTest;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.servlet.ServletContext;
+import java.lang.reflect.Modifier;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * ClasspathPackageProvider loads the configuration
+ * by scanning the classpath or selected packages for Action classes.
+ * <p>
+ * This provider is only invoked if one or more action packages are passed to the dispatcher,
+ * usually from the web.xml.
+ * Configurations are created for objects that either implement Action or have classnames that end with "Action".
+ */
+public class ClasspathPackageProvider implements PackageProvider {
+
+    /**
+     * The default page prefix (or "path").
+     * Some applications may place pages under "/WEB-INF" as an extreme security precaution.
+     */
+    protected static final String DEFAULT_PAGE_PREFIX = "struts.configuration.classpath.defaultPagePrefix";
+
+    /**
+     * The default page prefix (none).
+     */
+    private String defaultPagePrefix = "";
+
+    /**
+     * The default page extension,  to use in place of ".jsp".
+     */
+    protected static final String DEFAULT_PAGE_EXTENSION = "struts.configuration.classpath.defaultPageExtension";
+
+    /**
+     * The defacto default page extension, usually associated with JavaServer Pages.
+     */
+    private String defaultPageExtension = ".jsp";
+
+    /**
+     * A setting to indicate a custom default parent package,
+     * to use in place of "struts-default".
+     */
+    protected static final String DEFAULT_PARENT_PACKAGE = "struts.configuration.classpath.defaultParentPackage";
+
+    /**
+     * A setting to disable action scanning.
+     */
+    protected static final String DISABLE_ACTION_SCANNING = "struts.configuration.classpath.disableActionScanning";
+
+    /**
+     * Name of the framework's default configuration package,
+     * that application configuration packages automatically inherit.
+     */
+    private String defaultParentPackage = "struts-default";
+
+    /**
+     * The default page prefix (or "path").
+     * Some applications may place pages under "/WEB-INF" as an extreme security precaution.
+     */
+    protected static final String FORCE_LOWER_CASE = "struts.configuration.classpath.forceLowerCase";
+
+    /**
+     * Whether to use a lowercase letter as the initial letter of an action.
+     * If false, actions will retain the initial uppercase letter from the Action class.
+     * (<code>view.action</code> (true) versus <code>View.action</code> (false)).
+     */
+    private boolean forceLowerCase = true;
+
+    protected static final String CLASS_SUFFIX = "struts.codebehind.classSuffix";
+    /**
+     * Default suffix that can be used to indicate POJO "Action" classes.
+     */
+    protected String classSuffix = "Action";
+
+    protected static final String CHECK_IMPLEMENTS_ACTION = "struts.codebehind.checkImplementsAction";
+
+    /**
+     * When testing a class, check that it implements Action
+     */
+    protected boolean checkImplementsAction = true;
+
+    protected static final String CHECK_ANNOTATION = "struts.codebehind.checkAnnotation";
+
+    /**
+     * When testing a class, check that it has an @Action annotation
+     */
+    protected boolean checkAnnotation = true;
+
+    /**
+     * Helper class to scan class path for server pages.
+     */
+    private PageLocator pageLocator = new ClasspathPageLocator();
+
+    /**
+     * Flag to indicate the packages have been loaded.
+     *
+     * @see #loadPackages
+     * @see #needsReload
+     */
+    private boolean initialized = false;
+
+    private boolean disableActionScanning = false;
+
+    private PackageLoader packageLoader;
+
+    /**
+     * Logging instance for this class.
+     */
+    private static final Logger LOG = LoggerFactory.getLogger(ClasspathPackageProvider.class);
+
+    /**
+     * The XWork Configuration for this application.
+     *
+     * @see #init
+     */
+    private Configuration configuration;
+
+    private String actionPackages;
+
+    private ServletContext servletContext;
+
+    public ClasspathPackageProvider() {
+    }
+
+    /**
+     * PageLocator defines a locate method that can be used to discover server pages.
+     */
+    public static interface PageLocator {
+        public URL locate(String path);
+    }
+
+    /**
+     * ClasspathPathLocator searches the classpath for server pages.
+     */
+    public static class ClasspathPageLocator implements PageLocator {
+        public URL locate(String path) {
+            return ClassLoaderUtil.getResource(path, getClass());
+        }
+    }
+
+    @Inject("actionPackages")
+    public void setActionPackages(String packages) {
+        this.actionPackages = packages;
+    }
+
+    public void setServletContext(ServletContext ctx) {
+        this.servletContext = ctx;
+    }
+
+    /**
+     * Disables action scanning.
+     *
+     * @param disableActionScanning True to disable
+     */
+    @Inject(value=DISABLE_ACTION_SCANNING, required=false)
+    public void setDisableActionScanning(String disableActionScanning) {
+        this.disableActionScanning = "true".equals(disableActionScanning);
+    }
+
+    /**
+     * Check that the class implements Action
+     *
+     * @param checkImplementsAction True to check
+     */
+    @Inject(value=CHECK_IMPLEMENTS_ACTION, required=false)
+    public void setCheckImplementsAction(String checkImplementsAction) {
+        this.checkImplementsAction = "true".equals(checkImplementsAction);
+    }
+
+    /**
+     * Check that the class has an @Action annotation
+     *
+     * @param checkImplementsAction True to check
+     */
+    @Inject(value=CHECK_ANNOTATION, required=false)
+    public void setCheckAnnotation(String checkAnnotation) {
+        this.checkAnnotation = "true".equals(checkAnnotation);
+    }
+
+    /**
+     * Register a default parent package for the actions.
+     *
+     * @param defaultParentPackage the new defaultParentPackage
+     */
+    @Inject(value=DEFAULT_PARENT_PACKAGE, required=false)
+    public void setDefaultParentPackage(String defaultParentPackage) {
+        this.defaultParentPackage = defaultParentPackage;
+    }
+
+    /**
+     * Register a default page extension to use when locating pages.
+     *
+     * @param defaultPageExtension the new defaultPageExtension
+     */
+    @Inject(value=DEFAULT_PAGE_EXTENSION, required=false)
+    public void setDefaultPageExtension(String defaultPageExtension) {
+        this.defaultPageExtension = defaultPageExtension;
+    }
+
+    /**
+     * Reigster a default page prefix to use when locating pages.
+     *
+     * @param defaultPagePrefix the defaultPagePrefix to set
+     */
+    @Inject(value=DEFAULT_PAGE_PREFIX, required=false)
+    public void setDefaultPagePrefix(String defaultPagePrefix) {
+        this.defaultPagePrefix = defaultPagePrefix;
+    }
+
+    /**
+     * Default suffix that can be used to indicate POJO "Action" classes.
+     *
+     * @param classSuffix the classSuffix to set
+     */
+    @Inject(value=CLASS_SUFFIX, required=false)
+    public void setClassSuffix(String classSuffix) {
+        this.classSuffix = classSuffix;
+    }
+
+    /**
+     * Whether to use a lowercase letter as the initial letter of an action.
+     *
+     * @param force If false, actions will retain the initial uppercase letter from the Action class.
+     * (<code>view.action</code> (true) versus <code>View.action</code> (false)).
+     */
+    @Inject(value=FORCE_LOWER_CASE, required=false)
+    public void setForceLowerCase(String force) {
+        this.forceLowerCase = "true".equals(force);
+    }
+
+    /**
+     * Register a PageLocation to use to scan for server pages.
+     *
+     * @param locator
+     */
+    public void setPageLocator(PageLocator locator) {
+        this.pageLocator = locator;
+    }
+
+    /**
+     * Scan a list of packages for Action classes.
+     *
+     * This method loads classes that implement the Action interface
+     * or have a class name that ends with the letters "Action".
+     *
+     * @param pkgs A list of packages to load
+     * @see #processActionClass
+     */
+    protected void loadPackages(String[] pkgs) {
+
+        packageLoader = new PackageLoader();
+        ResolverUtil<Class> resolver = new ResolverUtil<Class>();
+        resolver.find(createActionClassTest(), pkgs);
+
+        Set<? extends Class<? extends Class>> actionClasses = resolver.getClasses();
+        for (Object obj : actionClasses) {
+           Class cls = (Class) obj;
+           if (!Modifier.isAbstract(cls.getModifiers())) {
+               processActionClass(cls, pkgs);
+           }
+        }
+
+        for (PackageConfig config : packageLoader.createPackageConfigs()) {
+            configuration.addPackageConfig(config.getName(), config);
+        }
+    }
+
+    protected ClassTest createActionClassTest() {
+        return new ClassTest() {
+            // Match Action implementations and classes ending with "Action"
+            public boolean matches(Class type) {
+                // TODO: should also find annotated classes
+                return ((checkImplementsAction && Action.class.isAssignableFrom(type)) ||
+                        type.getSimpleName().endsWith(getClassSuffix()) ||
+                        (checkAnnotation && type.getAnnotation(org.apache.struts2.config.Action.class) != null));
+            }
+
+        };
+    }
+
+    protected String getClassSuffix() {
+        return classSuffix;
+    }
+
+    /**
+     * Create a default action mapping for a class instance.
+     *
+     * The namespace annotation is honored, if found, otherwise
+     * the Java package is converted into the namespace
+     * by changing the dots (".") to slashes ("/").
+     *
+     * @param cls Action or POJO instance to process
+     * @param pkgs List of packages that were scanned for Actions
+     */
+    protected void processActionClass(Class<?> cls, String[] pkgs) {
+        String name = cls.getName();
+        String actionPackage = cls.getPackage().getName();
+        String actionNamespace = null;
+        String actionName = null;
+
+        org.apache.struts2.config.Action actionAnn =
+            (org.apache.struts2.config.Action) cls.getAnnotation(org.apache.struts2.config.Action.class);
+        if (actionAnn != null) {
+            actionName = actionAnn.name();
+            if (actionAnn.namespace().equals(org.apache.struts2.config.Action.DEFAULT_NAMESPACE)) {
+                actionNamespace = "";
+            } else {
+                actionNamespace = actionAnn.namespace();
+            }
+        } else {
+            for (String pkg : pkgs) {
+                if (name.startsWith(pkg)) {
+                    if (LOG.isDebugEnabled()) {
+                        LOG.debug("ClasspathPackageProvider: Processing class "+name);
+                    }
+                    name = name.substring(pkg.length() + 1);
+
+                    actionNamespace = "";
+                    actionName = name;
+                    int pos = name.lastIndexOf('.');
+                    if (pos > -1) {
+                        actionNamespace = "/" + name.substring(0, pos).replace('.','/');
+                        actionName = name.substring(pos+1);
+                    }
+                    break;
+                }
+            }
+            // Truncate Action suffix if found
+            if (actionName.endsWith(getClassSuffix())) {
+                actionName = actionName.substring(0, actionName.length() - getClassSuffix().length());
+            }
+
+            // Force initial letter of action to lowercase, if desired
+            if ((forceLowerCase) && (actionName.length() > 1)) {
+                int lowerPos = actionName.lastIndexOf('/') + 1;
+                StringBuilder sb = new StringBuilder();
+                sb.append(actionName.substring(0, lowerPos));
+                sb.append(Character.toLowerCase(actionName.charAt(lowerPos)));
+                sb.append(actionName.substring(lowerPos + 1));
+                actionName = sb.toString();
+            }
+        }
+
+        PackageConfig.Builder pkgConfig = loadPackageConfig(actionNamespace, actionPackage, cls);
+
+        // In case the package changed due to namespace annotation processing
+        if (!actionPackage.equals(pkgConfig.getName())) {
+            actionPackage = pkgConfig.getName();
+        }
+
+        List<PackageConfig> parents = findAllParentPackages(cls);
+        if (parents.size() > 0) {
+            pkgConfig.addParents(parents);
+
+            // Try to guess the namespace from the first package
+            PackageConfig firstParent = parents.get(0);
+            if (StringUtils.isEmpty(pkgConfig.getNamespace()) && StringUtils.isNotEmpty(firstParent.getNamespace())) {
+                pkgConfig.namespace(firstParent.getNamespace());
+            }
+        }
+
+
+        ResultTypeConfig defaultResultType = packageLoader.getDefaultResultType(pkgConfig);
+        ActionConfig actionConfig = new ActionConfig.Builder(actionPackage, actionName, cls.getName())
+                .addResultConfigs(new ResultMap<String,ResultConfig>(cls, actionName, defaultResultType))
+                .build();
+        pkgConfig.addActionConfig(actionName, actionConfig);
+    }
+
+    /**
+     * Finds all parent packages by first looking at the ParentPackage annotation on the package, then the class
+     * @param cls The action class
+     * @return A list of unique packages to add
+     */
+    private List<PackageConfig> findAllParentPackages(Class<?> cls) {
+
+        List<PackageConfig> parents = new ArrayList<PackageConfig>();
+        // Favor parent package annotations from the package
+        Set<String> parentNames = new LinkedHashSet<String>();
+        ParentPackage annotation = cls.getPackage().getAnnotation(ParentPackage.class);
+        if (annotation != null) {
+            parentNames.addAll(Arrays.asList(annotation.value()));
+        }
+        annotation = cls.getAnnotation(ParentPackage.class);
+        if (annotation != null) {
+            parentNames.addAll(Arrays.asList(annotation.value()));
+        }
+        if (parentNames.size() > 0) {
+            for (String parent : parentNames) {
+                PackageConfig parentPkg = configuration.getPackageConfig(parent);
+                if (parentPkg == null) {
+                    throw new ConfigurationException("ClasspathPackageProvider: Unable to locate parent package "+parent, annotation);
+                }
+                parents.add(parentPkg);
+            }
+        }
+        return parents;
+    }
+
+    /**
+     * Finds or creates the package configuration for an Action class.
+     *
+     * The namespace annotation is honored, if found,
+     * and the namespace is checked for a parent configuration.
+     *
+     * @param actionNamespace The configuration namespace
+     * @param actionPackage The Java package containing our Action classes
+     * @param actionClass The Action class instance
+     * @return PackageConfig object for the Action class
+     */
+    protected PackageConfig.Builder loadPackageConfig(String actionNamespace, String actionPackage, Class actionClass) {
+        PackageConfig.Builder parent = null;
+
+        // Check for the @Namespace annotation
+        if (actionClass != null) {
+            Namespace ns = (Namespace) actionClass.getAnnotation(Namespace.class);
+            if (ns != null) {
+                parent = loadPackageConfig(actionNamespace, actionPackage, null);
+                actionNamespace = ns.value();
+                actionPackage = actionClass.getName();
+
+            // See if the namespace has been overridden by the @Action annotation
+            } else {
+                org.apache.struts2.config.Action actionAnn =
+                    (org.apache.struts2.config.Action) actionClass.getAnnotation(org.apache.struts2.config.Action.class);
+                if (actionAnn != null && !actionAnn.DEFAULT_NAMESPACE.equals(actionAnn.namespace())) {
+                    // we pass null as the namespace in case the parent package hasn't been loaded yet
+                    parent = loadPackageConfig(null, actionPackage, null);
+                    actionPackage = actionClass.getName();
+                }
+            }
+        }
+
+
+        PackageConfig.Builder pkgConfig = packageLoader.getPackage(actionPackage);
+        if (pkgConfig == null) {
+            pkgConfig = new PackageConfig.Builder(actionPackage);
+
+            pkgConfig.namespace(actionNamespace);
+            if (parent == null) {
+                PackageConfig cfg = configuration.getPackageConfig(defaultParentPackage);
+                if (cfg != null) {
+                    pkgConfig.addParent(cfg);
+                } else {
+                    throw new ConfigurationException("ClasspathPackageProvider: Unable to locate default parent package: " +
+                        defaultParentPackage);
+                }
+            }
+
+            packageLoader.registerPackage(pkgConfig);
+
+        // if the parent package was first created by a child, ensure the namespace is correct
+        } else if (pkgConfig.getNamespace() == null) {
+            pkgConfig.namespace(actionNamespace);
+        }
+
+        if (parent != null) {
+            packageLoader.registerChildToParent(pkgConfig, parent);
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("class:"+actionClass+" parent:"+parent+" current:"+(pkgConfig != null ? pkgConfig.getName() : ""));
+        }
+
+        return pkgConfig;
+    }
+
+    /**
+     * Default destructor. Override to provide behavior.
+     */
+    public void destroy() {
+
+    }
+
+    /**
+     * Register this application's configuration.
+     *
+     * @param config The configuration for this application.
+     */
+    public void init(Configuration config) {
+        this.configuration = config;
+    }
+
+    /**
+     * Clears and loads the list of packages registered at construction.
+     *
+     * @throws ConfigurationException
+     */
+    public void loadPackages() throws ConfigurationException {
+        if (actionPackages != null && !disableActionScanning) {
+            String[] names = actionPackages.split("\\s*[,]\\s*");
+            // Initialize the classloader scanner with the configured packages
+            if (names.length > 0) {
+                setPageLocator(new ServletContextPageLocator(servletContext));
+            }
+            loadPackages(names);
+        }
+        initialized = true;
+    }
+
+    /**
+     * Indicates whether the packages have been initialized.
+     *
+     * @return True if the packages have been initialized
+     */
+    public boolean needsReload() {
+        return !initialized;
+    }
+
+    /**
+     * Creates ResultConfig objects from result annotations,
+     * and if a result isn't found, creates it on the fly.
+     */
+    class ResultMap<K,V> extends HashMap<K,V> {
+        private Class actionClass;
+        private String actionName;
+        private ResultTypeConfig defaultResultType;
+
+        public ResultMap(Class actionClass, String actionName, ResultTypeConfig defaultResultType) {
+            this.actionClass = actionClass;
+            this.actionName = actionName;
+            this.defaultResultType = defaultResultType;
+
+            // check if any annotations are around
+            while (!actionClass.getName().equals(Object.class.getName())) {
+                //noinspection unchecked
+                Results results = (Results) actionClass.getAnnotation(Results.class);
+                if (results != null) {
+                    // first check here...
+                    for (int i = 0; i < results.value().length; i++) {
+                        Result result = results.value()[i];
+                        ResultConfig config = createResultConfig(result);
+						if (!containsKey((K)config.getName())) {
+                            put((K)config.getName(), (V)config);
+                        }
+                    }
+                }
+
+                // what about a single Result annotation?
+                Result result = (Result) actionClass.getAnnotation(Result.class);
+                if (result != null) {
+                    ResultConfig config = createResultConfig(result);
+                    if (!containsKey((K)config.getName())) {
+                        put((K)config.getName(), (V)config);
+                    }
+                }
+
+                actionClass = actionClass.getSuperclass();
+            }
+        }
+
+        /**
+         * Extracts result name and value and calls {@link #createResultConfig}.
+         *
+         * @param result Result annotation reference representing result type to create
+         * @return New or cached ResultConfig object for result
+         */
+        protected ResultConfig createResultConfig(Result result) {
+            Class<? extends Object> cls = result.type();
+            if (cls == NullResult.class) {
+                cls = null;
+            }
+            return createResultConfig(result.name(), cls, result.value(), createParameterMap(result.params()));
+        }
+
+        protected Map<String, String> createParameterMap(String[] parms) {
+            Map<String, String> map = new HashMap<String, String>();
+            int subtract = parms.length % 2;
+            if(subtract != 0) {
+                LOG.warn("Odd number of result parameters key/values specified.  The final one will be ignored.");
+            }
+            for (int i = 0; i < parms.length - subtract; i++) {
+                String key = parms[i++];
+                String value = parms[i];
+                map.put(key, value);
+                if(LOG.isDebugEnabled()) {
+                    LOG.debug("Adding parmeter["+key+":"+value+"] to result.");
+                }
+            }
+            return map;
+        }
+
+        /**
+         * Creates a default ResultConfig,
+         * using either the resultClass or the default ResultType for configuration package
+         * associated this ResultMap class.
+         *
+         * @param key The result type name
+         * @param resultClass The class for the result type
+         * @param location Path to the resource represented by this type
+         * @return A ResultConfig for key mapped to location
+         */
+        private ResultConfig createResultConfig(Object key, Class<? extends Object> resultClass,
+                                                String location,
+                                                Map<? extends Object,? extends Object > configParams) {
+            if (resultClass == null) {
+                configParams = defaultResultType.getParams();
+                String className = defaultResultType.getClassName();
+                try {
+                    resultClass = ClassLoaderUtil.loadClass(className, getClass());
+                } catch (ClassNotFoundException ex) {
+                    throw new ConfigurationException("ClasspathPackageProvider: Unable to locate result class "+className, actionClass);
+                }
+            }
+
+            String defaultParam;
+            try {
+                defaultParam = (String) resultClass.getField("DEFAULT_PARAM").get(null);
+            } catch (Exception e) {
+                // not sure why this happened, but let's just use a sensible choice
+                defaultParam = "location";
+            }
+
+            HashMap params = new HashMap();
+            if (configParams != null) {
+                params.putAll(configParams);
+            }
+
+            params.put(defaultParam, location);
+            return new ResultConfig.Builder((String) key, resultClass.getName()).addParams(params).build();
+        }
+    }
+
+    /**
+     * Search classpath for a page.
+     */
+    private final class ServletContextPageLocator implements PageLocator {
+        private final ServletContext context;
+        private ClasspathPageLocator classpathPageLocator = new ClasspathPageLocator();
+
+        private ServletContextPageLocator(ServletContext context) {
+            this.context = context;
+        }
+
+        public URL locate(String path) {
+            URL url = null;
+            try {
+                url = context.getResource(path);
+                if (url == null) {
+                    url = classpathPageLocator.locate(path);
+                }
+            } catch (MalformedURLException e) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug("Unable to resolve path "+path+" against the servlet context");
+                }
+            }
+            return url;
+        }
+    }
+
+    private static class PackageLoader {
+
+        /**
+         * The package configurations for scanned Actions.
+         */
+        private Map<String,PackageConfig.Builder> packageConfigBuilders = new HashMap<String,PackageConfig.Builder>();
+
+        private Map<PackageConfig.Builder,PackageConfig.Builder> childToParent = new HashMap<PackageConfig.Builder,PackageConfig.Builder>();
+
+        public PackageConfig.Builder getPackage(String name) {
+            return packageConfigBuilders.get(name);
+        }
+
+        public void registerChildToParent(PackageConfig.Builder child, PackageConfig.Builder parent) {
+            childToParent.put(child, parent);
+        }
+
+        public void registerPackage(PackageConfig.Builder builder) {
+            packageConfigBuilders.put(builder.getName(), builder);
+        }
+
+        public Collection<PackageConfig> createPackageConfigs() {
+            Map<String, PackageConfig> configs = new HashMap<String, PackageConfig>();
+
+            Set<PackageConfig.Builder> builders;
+            while ((builders = findPackagesWithNoParents()).size() > 0) {
+                for (PackageConfig.Builder parent : builders) {
+                    PackageConfig config = parent.build();
+                    configs.put(config.getName(), config);
+                    packageConfigBuilders.remove(config.getName());
+
+                    for (Iterator<Map.Entry<PackageConfig.Builder,PackageConfig.Builder>> i = childToParent.entrySet().iterator(); i.hasNext(); ) {
+                        Map.Entry<PackageConfig.Builder,PackageConfig.Builder> entry = i.next();
+                        if (entry.getValue() == parent) {
+                            entry.getKey().addParent(config);
+                            i.remove();
+                        }
+                    }
+                }
+            }
+            return configs.values();
+        }
+
+        Set<PackageConfig.Builder> findPackagesWithNoParents() {
+            Set<PackageConfig.Builder> builders = new HashSet<PackageConfig.Builder>();
+            for (PackageConfig.Builder child : packageConfigBuilders.values()) {
+                if (!childToParent.containsKey(child)) {
+                    builders.add(child);
+                }
+            }
+            return builders;
+        }
+
+        public ResultTypeConfig getDefaultResultType(PackageConfig.Builder pkgConfig) {
+            PackageConfig.Builder parent;
+            PackageConfig.Builder current = pkgConfig;
+
+            while ((parent = childToParent.get(current)) != null) {
+                current = parent;
+            }
+            return current.getResultType(current.getFullDefaultResultType());
+        }
+    }
+}
diff --git a/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Namespace.java b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Namespace.java
new file mode 100644
index 0000000..636f942
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Namespace.java
@@ -0,0 +1,36 @@
+/*
+ * $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 org.apache.struts2.config;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+
+/**
+ * Allows an action class to specify its namespace
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Namespace {
+    String value();
+}
diff --git a/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/ParentPackage.java b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/ParentPackage.java
new file mode 100644
index 0000000..2d23d4a
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/ParentPackage.java
@@ -0,0 +1,36 @@
+/*
+ * $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 org.apache.struts2.config;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+
+/**
+ * Allows an action class or package to specify an xwork package to inherit
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.PACKAGE})
+public @interface ParentPackage {
+    String[] value();
+}
diff --git a/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Result.java b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Result.java
new file mode 100644
index 0000000..fc7c5bd
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Result.java
@@ -0,0 +1,41 @@
+/*
+ * $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 org.apache.struts2.config;
+
+import com.opensymphony.xwork2.Action;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines an XWork Result
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Result {
+    String name() default Action.SUCCESS;
+    Class<? extends com.opensymphony.xwork2.Result> type() default NullResult.class;
+    String value();
+    String[] params() default {};
+}
diff --git a/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Results.java b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Results.java
new file mode 100644
index 0000000..a7d9096
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/main/java/org/apache/struts2/config/Results.java
@@ -0,0 +1,36 @@
+/*
+ * $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 org.apache.struts2.config;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+
+/**
+ * Defines multiple XWork Results
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Results {
+    Result[] value();
+}
diff --git a/plugins/struts2-codebehind-plugin/src/main/resources/LICENSE.txt b/plugins/struts2-codebehind-plugin/src/main/resources/LICENSE.txt
new file mode 100644
index 0000000..dd5b3a5
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/main/resources/LICENSE.txt
@@ -0,0 +1,174 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
diff --git a/plugins/struts2-codebehind-plugin/src/main/resources/NOTICE.txt b/plugins/struts2-codebehind-plugin/src/main/resources/NOTICE.txt
new file mode 100644
index 0000000..bfba90c
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/main/resources/NOTICE.txt
@@ -0,0 +1,5 @@
+Apache Struts
+Copyright 2000-2011 The Apache Software Foundation
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
\ No newline at end of file
diff --git a/plugins/struts2-codebehind-plugin/src/main/resources/struts-plugin.xml b/plugins/struts2-codebehind-plugin/src/main/resources/struts-plugin.xml
new file mode 100644
index 0000000..601ba8c
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/main/resources/struts-plugin.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+/*
+ * $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.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+	"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
+	"http://struts.apache.org/dtds/struts-2.3.dtd">
+
+<struts>
+    <bean type="com.opensymphony.xwork2.UnknownHandler" class="org.apache.struts2.codebehind.CodebehindUnknownHandler" />
+    <bean type="com.opensymphony.xwork2.config.PackageProvider" name="codebehind" class="org.apache.struts2.config.ClasspathPackageProvider" />
+
+    <constant name="struts.codebehind.pathPrefix" value="/"/>
+    <constant name="struts.codebehind.defaultPackage" value="codebehind-default"/>
+    <constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />
+
+    <package name="codebehind-default" extends="struts-default">
+    </package>
+</struts>
diff --git a/plugins/struts2-codebehind-plugin/src/site/site.xml b/plugins/struts2-codebehind-plugin/src/site/site.xml
new file mode 100644
index 0000000..07a667e
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/site/site.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<project name="Struts 2">
+    <skin>
+        <groupId>org.apache.maven.skins</groupId>
+        <artifactId>maven-fluido-skin</artifactId>
+        <version>1.3.1</version>
+    </skin>
+    <bannerLeft>
+        <name>Apache Software Foundation</name>
+        <src>http://www.apache.org/images/asf-logo.gif</src>
+        <href>http://www.apache.org/</href>
+    </bannerLeft>
+    <bannerRight>
+        <name>Apache Struts</name>
+        <src>http://struts.apache.org/img/struts-logo.svg</src>
+        <href>http://struts.apache.org/</href>
+    </bannerRight>
+    <publishDate position="none"/>
+    <version position="none"/>
+    <body>
+        <links>
+            <item name="Apache" href="http://www.apache.org/"/>
+            <item name="Struts" href="http://struts.apache.org/"/>
+        </links>
+
+        <menu ref="parent"/>
+        <menu ref="reports"/>
+
+        <footer>
+            <div class="row span12">
+                Apache Struts, Struts, Apache, the Apache feather logo, and the Apache Struts
+                project logos are trademarks of The Apache Software Foundation.
+            </div>
+        </footer>
+
+    </body>
+</project>
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/codebehind/CodebehindUnknownHandlerTest.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/codebehind/CodebehindUnknownHandlerTest.java
new file mode 100644
index 0000000..e6cff79
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/codebehind/CodebehindUnknownHandlerTest.java
@@ -0,0 +1,137 @@
+/*
+ * $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 org.apache.struts2.codebehind;
+
+import com.mockobjects.dynamic.C;
+import com.mockobjects.dynamic.Mock;
+import com.opensymphony.xwork2.*;
+import com.opensymphony.xwork2.config.entities.ActionConfig;
+import com.opensymphony.xwork2.config.entities.ResultTypeConfig;
+import com.opensymphony.xwork2.util.XWorkTestCaseHelper;
+import org.apache.struts2.StrutsTestCase;
+import org.apache.struts2.dispatcher.ServletDispatcherResult;
+import org.springframework.mock.web.MockServletContext;
+
+import javax.servlet.ServletContext;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.HashMap;
+
+public class CodebehindUnknownHandlerTest extends StrutsTestCase {
+
+    CodebehindUnknownHandler handler;
+    Mock mockServletContext;
+    
+    public void setUp() throws Exception {
+        configurationManager = XWorkTestCaseHelper.setUp();
+        configuration = configurationManager.getConfiguration();
+        container = configuration.getContainer();
+        actionProxyFactory = container.getInstance(ActionProxyFactory.class);
+        servletContext = new MockServletContext();
+        initDispatcher(Collections.singletonMap("actionPackages", "foo.bar"));
+        mockServletContext = new Mock(ServletContext.class);
+        handler = new CodebehindUnknownHandler("codebehind-default", configuration);
+        handler.setPathPrefix("/");
+        handler.setObjectFactory(container.getInstance(ObjectFactory.class));
+        handler.setServletContext((ServletContext)mockServletContext.proxy());
+    }
+
+    public void testBuildResult() {
+        ActionContext ctx = new ActionContext(new HashMap());
+        ResultTypeConfig config = new ResultTypeConfig.Builder("null", SomeResult.class.getName()).defaultResultParam("location").build();
+        
+        Result result = handler.buildResult("/foo.jsp", "success", config, ctx);
+        assertNotNull(result);
+        assertTrue(result instanceof SomeResult);
+        assertEquals("/foo.jsp", ((SomeResult) result).location);
+        
+    }
+
+    public void testString() {
+        assertEquals("foo.bar.jim", handler.string("foo", ".", "bar", ".", "jim"));
+    }
+
+    public void testDeterminePath() {
+        assertEquals("/", handler.determinePath("/", ""));
+        assertEquals("/", handler.determinePath("/", null));
+        assertEquals("/", handler.determinePath("/", "/"));
+        assertEquals("/foo/", handler.determinePath("/", "/foo"));
+        assertEquals("/foo/", handler.determinePath("/", "/foo/"));
+        assertEquals("/foo/", handler.determinePath("/", "foo"));
+        assertEquals("/", handler.determinePath("", ""));
+        assertEquals("/foo/", handler.determinePath("", "foo"));
+        assertEquals("/foo/", handler.determinePath("", "/foo/"));
+    }
+    
+    public void testLocateTemplate() throws MalformedURLException {
+        URL url = new URL("file:/foo.xml");
+        mockServletContext.expectAndReturn("getResource", C.args(C.eq("/foo.xml")), url);
+        assertEquals(url, handler.locateTemplate("/foo.xml"));
+        mockServletContext.verify();
+        
+    }
+    
+    public void testLocateTemplateFromClasspath() throws MalformedURLException {
+        mockServletContext.expectAndReturn("getResource", C.args(C.eq("struts-plugin.xml")), null);
+        URL url = handler.locateTemplate("struts-plugin.xml");
+        assertNotNull(url);
+        assertTrue(url.toString().endsWith("struts-plugin.xml"));
+        mockServletContext.verify();
+    }
+
+    /**
+     * Assert that an unknown action like /foo maps to ActionSupport with a ServletDispatcherResult to /foo.jsp
+     */
+    public void testBuildActionConfigForUnknownAction() throws MalformedURLException {
+        URL url = new URL("file:/foo.jsp");
+        mockServletContext.expectAndReturn("getResource", C.args(C.eq("/foo.jsp")), url);
+        ActionConfig actionConfig = handler.handleUnknownAction("/", "foo");
+        // we need a package
+        assertEquals("codebehind-default", actionConfig.getPackageName());
+        // a non-empty interceptor stack
+        assertTrue(actionConfig.getInterceptors().size() > 0);
+        // ActionSupport as the implementation
+        assertEquals(ActionSupport.class.getName(), actionConfig.getClassName());
+        // with one result
+        assertEquals(1, actionConfig.getResults().size());
+        // named success
+        assertNotNull(actionConfig.getResults().get("success"));
+        // of ServletDispatcherResult type
+        assertEquals(ServletDispatcherResult.class.getName(), actionConfig.getResults().get("success").getClassName());
+        // and finally pointing to foo.jsp!
+        assertEquals("/foo.jsp", actionConfig.getResults().get("success").getParams().get("location"));
+    }
+
+    public static class SomeResult implements Result {
+
+        public String location;
+        public void setLocation(String loc) {
+            this.location = loc;
+        }
+        
+        public void execute(ActionInvocation invocation) throws Exception {
+        }
+        
+    }
+
+}
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/AnnotatedAction.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/AnnotatedAction.java
new file mode 100644
index 0000000..61ee16c
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/AnnotatedAction.java
@@ -0,0 +1,27 @@
+/*
+ * $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 org.apache.struts2.config;
+
+@Action(name="myaction",namespace="/foo")
+public class AnnotatedAction {
+
+}
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/AnotherAnnotatedObject.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/AnotherAnnotatedObject.java
new file mode 100644
index 0000000..2238bb4
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/AnotherAnnotatedObject.java
@@ -0,0 +1,27 @@
+/*
+ * $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 org.apache.struts2.config;
+
+@Action(name="myaction2")
+public class AnotherAnnotatedObject {
+
+}
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/ClasspathPackageProviderTest.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/ClasspathPackageProviderTest.java
new file mode 100644
index 0000000..b25558b
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/ClasspathPackageProviderTest.java
@@ -0,0 +1,166 @@
+/*
+ * $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 org.apache.struts2.config;
+
+import com.opensymphony.xwork2.config.Configuration;
+import com.opensymphony.xwork2.config.entities.ActionConfig;
+import com.opensymphony.xwork2.config.entities.PackageConfig;
+import com.opensymphony.xwork2.config.entities.ResultTypeConfig;
+import com.opensymphony.xwork2.config.impl.DefaultConfiguration;
+import junit.framework.TestCase;
+import org.apache.struts2.dispatcher.ServletDispatcherResult;
+
+import java.util.Map;
+
+public class ClasspathPackageProviderTest extends TestCase {
+
+    ClasspathPackageProvider provider;
+    Configuration config;
+
+    public void setUp() throws Exception {
+        provider = new ClasspathPackageProvider();
+        provider.setActionPackages("org.apache.struts2.config");
+        config = createNewConfiguration();
+        provider.init(config);
+        provider.loadPackages();
+    }
+
+    private Configuration createNewConfiguration() {
+        Configuration config = new DefaultConfiguration();
+        PackageConfig strutsDefault = new PackageConfig.Builder("struts-default")
+                .addResultTypeConfig(new ResultTypeConfig.Builder("dispatcher", ServletDispatcherResult.class.getName())
+                        .defaultResultParam("location")
+                        .build())
+                .defaultResultType("dispatcher")
+                .build();
+        config.addPackageConfig("struts-default", strutsDefault);
+        PackageConfig customPackage = new PackageConfig.Builder("custom-package")
+            .namespace("/custom")
+            .build();
+        config.addPackageConfig("custom-package", customPackage);
+        return config;
+    }
+
+    public void tearDown() throws Exception {
+        provider = null;
+        config = null;
+    }
+
+    public void testFoundRootPackages() {
+        assertEquals(7, config.getPackageConfigs().size());
+        PackageConfig pkg = config.getPackageConfig("org.apache.struts2.config");
+        assertNotNull(pkg);
+        Map configs = pkg.getActionConfigs();
+        assertNotNull(configs);
+        // assertEquals(1, configs.size());
+        ActionConfig actionConfig = (ActionConfig) configs.get("customParentPackage");
+        assertNotNull(actionConfig);
+    }
+    
+    public void testDisableScanning() {
+        provider = new ClasspathPackageProvider();
+        provider.setActionPackages("org.apache.struts2.config");
+        provider.setDisableActionScanning("true");
+        config = new DefaultConfiguration();
+        provider.init(config);
+        provider.loadPackages();
+        
+        assertEquals(0, config.getPackageConfigs().size());
+    }
+
+    public void testParentPackage() {
+        PackageConfig pkg = config.getPackageConfig("org.apache.struts2.config");
+        // assertEquals(2, pkg.getParents().size());
+        Map configs = pkg.getActionConfigs();
+        ActionConfig config = (ActionConfig) configs.get("customParentPackage");
+        assertNotNull(config);
+        assertEquals("/custom", pkg.getNamespace());
+    }
+
+    public void testParentPackageOnPackage() {
+        provider = new ClasspathPackageProvider();
+        provider.setActionPackages("org.apache.struts2.config.parenttest");
+        provider.init(createNewConfiguration());
+        provider.loadPackages();
+
+
+        PackageConfig pkg = config.getPackageConfig("org.apache.struts2.config.parenttest");
+        // assertEquals(2, pkg.getParents().size());
+        assertNotNull(pkg);
+
+        assertEquals("custom-package", pkg.getParents().get(0).getName());
+        Map configs = pkg.getActionConfigs();
+        ActionConfig config = (ActionConfig) configs.get("some");
+        assertNotNull(config);
+    }
+
+    public void testCustomNamespace() {
+        PackageConfig pkg = config.getPackageConfig("org.apache.struts2.config.CustomNamespaceAction");
+        Map configs = pkg.getAllActionConfigs();
+        // assertEquals(2, configs.size());
+        ActionConfig config = (ActionConfig) configs.get("customNamespace");
+        assertEquals(config.getPackageName(), pkg.getName());
+        assertEquals(1, pkg.getParents().size());
+        assertNotNull(config);
+        assertEquals("/mynamespace", pkg.getNamespace());
+        ActionConfig ac = (ActionConfig) configs.get("customParentPackage");
+        assertNotNull(ac);
+    }
+    
+    public void testCustomActionAnnotation() {
+        PackageConfig pkg = config.getPackageConfig("org.apache.struts2.config.AnnotatedAction");
+        Map configs = pkg.getAllActionConfigs();
+        // assertEquals(2, configs.size());
+        ActionConfig config = (ActionConfig) configs.get("myaction");
+        assertNotNull(config);
+    }
+    
+    public void testCustomActionAnnotationOfAnyName() {
+        PackageConfig pkg = config.getPackageConfig("org.apache.struts2.config");
+        Map configs = pkg.getAllActionConfigs();
+        // assertEquals(2, configs.size());
+        ActionConfig config = (ActionConfig) configs.get("myaction2");
+        assertNotNull(config);
+    }
+    
+    public void testResultAnnotations() {
+        PackageConfig pkg = config.getPackageConfig("org.apache.struts2.config.cltest");
+        assertEquals("/cltest", pkg.getNamespace());
+        ActionConfig acfg = pkg.getActionConfigs().get("twoResult");
+        assertNotNull(acfg);
+        assertEquals(2, acfg.getResults().size());
+        assertEquals("input.jsp", acfg.getResults().get("input").getParams().get("location"));
+        assertEquals("bob", acfg.getResults().get("chain").getParams().get("location"));
+
+        acfg = pkg.getActionConfigs().get("oneResult");
+        assertNotNull(acfg);
+        assertEquals(1, acfg.getResults().size());
+        assertEquals("input-parent.jsp", acfg.getResults().get("input").getParams().get("location"));
+    }
+
+    public void testActionImplementation() {
+        PackageConfig pkg = config.getPackageConfig("org.apache.struts2.config.cltest");
+        assertEquals("/cltest", pkg.getNamespace());
+        ActionConfig acfg = pkg.getActionConfigs().get("actionImpl");
+        assertNotNull(acfg);
+    }
+}
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/CustomNamespaceAction.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/CustomNamespaceAction.java
new file mode 100644
index 0000000..8231bb4
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/CustomNamespaceAction.java
@@ -0,0 +1,27 @@
+/*
+ * $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 org.apache.struts2.config;
+
+@Namespace("/mynamespace")
+public class CustomNamespaceAction {
+
+}
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/CustomParentPackageAction.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/CustomParentPackageAction.java
new file mode 100644
index 0000000..734d2f9
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/CustomParentPackageAction.java
@@ -0,0 +1,27 @@
+/*
+ * $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 org.apache.struts2.config;
+
+@ParentPackage("custom-package")
+public class CustomParentPackageAction {
+
+}
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/cltest/ActionImpl.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/cltest/ActionImpl.java
new file mode 100644
index 0000000..6863e75
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/cltest/ActionImpl.java
@@ -0,0 +1,32 @@
+/*
+ * $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 org.apache.struts2.config.cltest;
+
+import com.opensymphony.xwork2.Action;
+
+public class ActionImpl implements Action {
+
+    public String execute() throws Exception {
+        return null;
+    }
+
+}
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/cltest/OneResultAction.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/cltest/OneResultAction.java
new file mode 100644
index 0000000..72efc3c
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/cltest/OneResultAction.java
@@ -0,0 +1,29 @@
+/*
+ * $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 org.apache.struts2.config.cltest;
+
+import org.apache.struts2.config.Result;
+
+@Result(name="input", value="input-parent.jsp")
+public class OneResultAction {
+
+}
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/cltest/TwoResultAction.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/cltest/TwoResultAction.java
new file mode 100644
index 0000000..a62e02b
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/cltest/TwoResultAction.java
@@ -0,0 +1,35 @@
+/*
+ * $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 org.apache.struts2.config.cltest;
+
+import org.apache.struts2.config.Result;
+import org.apache.struts2.config.Results;
+import org.apache.struts2.dispatcher.ServletDispatcherResult;
+
+
+@Results({
+    @Result(name="chain", value="bob", type=ServletDispatcherResult.class),
+    @Result(name="input", value="input.jsp")
+})
+public class TwoResultAction extends OneResultAction {
+
+}
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/parenttest/SomeAction.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/parenttest/SomeAction.java
new file mode 100644
index 0000000..08263af
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/parenttest/SomeAction.java
@@ -0,0 +1,30 @@
+/*
+ * $Id: ParentPackage.java 651946 2008-04-27 13:41:38Z apetrelli $
+ *
+ * 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 org.apache.struts2.config.parenttest;
+
+import com.opensymphony.xwork2.Action;
+
+public class SomeAction implements Action {
+
+    public String execute() throws Exception {
+        return null;  //To change body of implemented methods use File | Settings | File Templates.
+    }
+}
diff --git a/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/parenttest/package-info.java b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/parenttest/package-info.java
new file mode 100644
index 0000000..fdb5c92
--- /dev/null
+++ b/plugins/struts2-codebehind-plugin/src/test/java/org/apache/struts2/config/parenttest/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * $Id: ParentPackage.java 651946 2008-04-27 13:41:38Z apetrelli $
+ *
+ * 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.
+ */
+@ParentPackage("custom-package")
+package org.apache.struts2.config.parenttest;
+
+import org.apache.struts2.config.ParentPackage;
\ No newline at end of file