com.google.code.rees.scope.struts2.StrutsActionProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.google.code.rees.scope.struts2.StrutsActionProvider.java

Source

/*******************************************************************************
 * 
 *  Struts2-Conversation-Plugin - An Open Source Conversation- and Flow-Scope Solution for Struts2-based Applications
 *  =================================================================================================================
 * 
 *  Copyright (C) 2012 by Rees Byars
 *  http://code.google.com/p/struts2-conversation/
 * 
 * **********************************************************************************************************************
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 *  the License. You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 *  an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 *  specific language governing permissions and limitations under the License.
 * 
 * **********************************************************************************************************************
 * 
 *  $Id: StrutsActionProvider.java reesbyars $
 ******************************************************************************/
package com.google.code.rees.scope.struts2;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.StrutsConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.code.rees.scope.ActionProvider;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.FileManager;
import com.opensymphony.xwork2.FileManagerFactory;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.classloader.ReloadingClassLoader;
import com.opensymphony.xwork2.util.finder.ClassFinder;
import com.opensymphony.xwork2.util.finder.ClassFinder.ClassInfo;
import com.opensymphony.xwork2.util.finder.ClassLoaderInterface;
import com.opensymphony.xwork2.util.finder.ClassLoaderInterfaceDelegate;
import com.opensymphony.xwork2.util.finder.Test;
import com.opensymphony.xwork2.util.finder.UrlSet;

/**
 * Struts2 implementation of the {@link ActionProvider}.
 * 
 * @author rees.byars with code from the struts2 convention package
 */
public class StrutsActionProvider implements ActionProvider {

    private static final long serialVersionUID = 6728107973559862449L;

    private static final Logger LOG = LoggerFactory.getLogger(StrutsActionProvider.class);
    private static final boolean EXTRACT_BASE_INTERFACES = true;

    private transient Set<Class<?>> actionClasses;
    private String[] actionPackages;
    private String[] packageLocators;
    private String[] includeJars;
    private boolean disablePackageLocatorsScanning;
    private boolean checkImplementsAction;
    private String packageLocatorsBasePackage;
    private String actionSuffix;
    private Set<String> fileProtocols;
    private boolean devMode;
    private ReloadingClassLoader reloadingClassLoader;
    private boolean reload;
    private boolean excludeParentClassLoader;
    private boolean requireFollowsConvention;
    private transient FileManager fileManager;
    private transient Container container;

    public Set<Class<?>> getActionClasses() throws Exception {
        try {
            if (this.fileManager == null) {
                //retrieve this way instead of by injection in order to catch an handle errors/exceptions with older versions of struts2
                this.fileManager = this.container.getInstance(FileManagerFactory.class).getFileManager();
            }
            if (actionClasses == null) {
                initReloadClassLoader();
                actionClasses = this.findActions();
            }
            return this.actionClasses;
            //This is a hack to make sure that changes to Struts2 classes used below don't blow up the whole plugin
        } catch (Throwable t) {
            throw new Exception(
                    "Could not load action classes on startup.  Configuration caches will be built on-demand.");
        }

    }

    @Inject
    public void setContainer(Container container) {
        this.container = container;
    }

    @Inject(StrutsConstants.STRUTS_DEVMODE)
    public void setDevMode(String mode) {
        this.devMode = "true".equals(mode);
    }

    /**
     * Reload configuration when classes change. Defaults to "false" and should
     * not be used in production.
     */
    @Inject(ConventionConstants.RELOAD_CLASSES)
    public void setReload(String reload) {
        this.reload = "true".equals(reload);
    }

    /**
     * Exclude URLs found by the parent class loader. Defaults to "true", set to
     * true for JBoss
     */
    @Inject(ConventionConstants.EXCLUDE_PARENT_CLASS_LOADER)
    public void setExcludeParentClassLoader(String exclude) {
        this.excludeParentClassLoader = "true".equals(exclude);
    }

    /**
     * File URLs whose protocol are in these list will be processed as jars
     * containing classes
     * 
     * @param fileProtocols
     *        Comma separated list of file protocols that will be considered
     *        as jar files and scanned
     */
    @Inject(ConventionConstants.FILE_PROTOCOLS)
    public void setFileProtocols(String fileProtocols) {
        if (StringUtils.isNotBlank(fileProtocols)) {
            this.fileProtocols = TextParseUtil.commaDelimitedStringToSet(fileProtocols);
        }
    }

    /**
     * @param includeJars
     *        Comma separated list of regular expressions of jars to be
     *        included.
     */
    @Inject(value = ConventionConstants.INCLUDE_JARS, required = false)
    public void setIncludeJars(String includeJars) {
        if (StringUtils.isNotEmpty(includeJars))
            this.includeJars = includeJars.split("\\s*[,]\\s*");
    }

    /**
     * @param disablePackageLocatorsScanning
     *        If set to true, only the named packages will be scanned
     */
    @Inject(value = ConventionConstants.PACKAGE_LOCATORS_DISABLE, required = false)
    public void setDisablePackageLocatorsScanning(String disablePackageLocatorsScanning) {
        this.disablePackageLocatorsScanning = "true".equals(disablePackageLocatorsScanning);
    }

    /**
     * @param actionPackages
     *        (Optional) An optional list of action packages that this
     *        should create configuration for.
     */
    @Inject(value = ConventionConstants.ACTION_PACKAGES, required = false)
    public void setActionPackages(String actionPackages) {
        if (StringUtils.isNotBlank(actionPackages)) {
            this.actionPackages = actionPackages.split("\\s*[,]\\s*");
        }
    }

    /**
     * @param checkImplementsAction
     *        (Optional) Map classes that implement
     *        com.opensymphony.xwork2.Action as actions
     */
    @Inject(value = ConventionConstants.CHECK_IMPLEMENTS_ACTION, required = false)
    public void setCheckImplementsAction(String checkImplementsAction) {
        this.checkImplementsAction = "true".equals(checkImplementsAction);
    }

    /**
     * @param actionSuffix
     *        (Optional) Classes that end with these value will be mapped as
     *        actions (defaults to "Action")
     */
    @Inject(value = ConventionConstants.ACTION_SUFFIX, required = false)
    public void setActionSuffix(String actionSuffix) {
        if (StringUtils.isNotBlank(actionSuffix)) {
            this.actionSuffix = actionSuffix;
        }
    }

    /**
     * @param packageLocators
     *        (Optional) A list of names used to find action packages.
     */
    @Inject(value = ConventionConstants.PACKAGE_LOCATORS, required = false)
    public void setPackageLocators(String packageLocators) {
        this.packageLocators = packageLocators.split("\\s*[,]\\s*");
    }

    /**
     * @param packageLocatorsBasePackage
     *        (Optional) If set, only packages that start with this name
     *        will be scanned for actions.
     */
    @Inject(value = ConventionConstants.PACKAGE_LOCATORS_BASE_PACKAGE, required = false)
    public void setPackageLocatorsBase(String packageLocatorsBasePackage) {
        this.packageLocatorsBasePackage = packageLocatorsBasePackage;
    }

    /**
     * @param requireFollowsConvention
     *        If true, only classes that follow the convention for action
     *        classes will
     *        be scanned for the scope annotations.
     */
    @Inject(value = StrutsScopeConstants.REQUIRE_FOLLOWS_CONVENTION)
    public void setRequireFollowsConvention(String requireFollowsConvention) {
        this.requireFollowsConvention = "true".equals(requireFollowsConvention);
    }

    /**
     * Note that we can't include the test for {@link #actionSuffix} here
     * because a class is included if its name ends in {@link #actionSuffix} OR
     * it implements {@link com.opensymphony.xwork2.Action}. Since the whole
     * goal is to avoid loading the class if we don't have to, the (actionSuffix
     * || implements Action) test will have to remain until later. See
     * {@link #getActionClassTest()} for the test performed on the loaded
     * {@link ClassInfo} structure.
     * 
     * @param className
     *        the name of the class to test
     * @return true if the specified class should be included in the
     *         package-based action scan
     */
    protected boolean includeClassNameInActionScan(String className) {

        String classPackageName = StringUtils.substringBeforeLast(className, ".");

        if (actionPackages != null) {
            for (String packageName : actionPackages) {
                String strictPackageName = packageName + ".";
                if (classPackageName.equals(packageName) || classPackageName.startsWith(strictPackageName))
                    return true;
            }
        }

        if (packageLocators != null && !disablePackageLocatorsScanning) {
            for (String packageLocator : packageLocators) {
                if (classPackageName.length() > 0 && (packageLocatorsBasePackage == null
                        || classPackageName.startsWith(packageLocatorsBasePackage))) {
                    String[] splitted = classPackageName.split("\\.");

                    if (contains(splitted, packageLocator, false))
                        return true;
                }
            }
        }

        return false;
    }

    /**
     * Construct a {@link Test} object that determines if a specified class name
     * should be included in the package scan based on the clazz's package name.
     * Note that the goal is to avoid loading the class, so the test should only
     * rely on information in the class name itself. The default implementation
     * is to return the result of {@link #includeClassNameInActionScan(String)}.
     * 
     * @return a {@link Test} object that returns true if the specified class
     *         name should be included in the package scan
     */
    protected Test<String> getClassPackageTest() {
        return new Test<String>() {
            public boolean test(String className) {
                return includeClassNameInActionScan(className);
            }
        };
    }

    /**
     * Construct a {@link Test} Object that determines if a specified class
     * should be included in the package scan based on the full
     * {@link ClassInfo} of the class. At this point, the class has been loaded,
     * so it's ok to perform tests such as checking annotations or looking at
     * interfaces or super-classes of the specified class.
     * 
     * @return a {@link Test} object that returns true if the specified class
     *         should be included in the package scan
     */
    protected Test<ClassFinder.ClassInfo> getActionClassTest() {
        return new Test<ClassFinder.ClassInfo>() {
            public boolean test(ClassFinder.ClassInfo classInfo) {

                // Why do we call includeClassNameInActionScan here, when it's
                // already been called to in the initial call to ClassFinder?
                // When some action class passes our package filter in that
                // step,
                // ClassFinder automatically includes parent classes of that
                // action,
                // such as com.opensymphony.xwork2.ActionSupport. We repeat the
                // package filter here to filter out such results.
                boolean inPackage = includeClassNameInActionScan(classInfo.getName());
                boolean nameMatches = classInfo.getName().endsWith(actionSuffix) || !requireFollowsConvention;

                try {
                    return inPackage && (nameMatches || (checkImplementsAction
                            && com.opensymphony.xwork2.Action.class.isAssignableFrom(classInfo.get())));
                } catch (ClassNotFoundException ex) {
                    if (LOG.isErrorEnabled())
                        LOG.error("Unable to load class [#0]", ex, classInfo.getName());
                    return false;
                }
            }
        };
    }

    protected Set<Class<?>> findActions() {
        Set<Class<?>> classes = new HashSet<Class<?>>();
        try {
            if (actionPackages != null || (packageLocators != null && !disablePackageLocatorsScanning)) {

                // By default, ClassFinder scans EVERY class in the specified
                // url set, which can produce spurious warnings for non-action
                // classes that can't be loaded. We pass a package filter that
                // only considers classes that match the action packages
                // specified by the user
                Test<String> classPackageTest = getClassPackageTest();
                List<URL> urls = readUrls();
                ClassFinder finder = new ClassFinder(getClassLoaderInterface(), urls, EXTRACT_BASE_INTERFACES,
                        fileProtocols, classPackageTest);

                Test<ClassFinder.ClassInfo> test = getActionClassTest();
                for (Class<?> clazz : finder.findClasses(test)) {
                    classes.add(clazz);
                }
            }
        } catch (Exception ex) {
            if (LOG.isErrorEnabled())
                LOG.error("Unable to scan named packages", ex);
        }

        return classes;
    }

    private List<URL> readUrls() throws IOException {
        List<URL> resourceUrls = new ArrayList<URL>();
        // Usually the "classes" dir.
        ArrayList<URL> classesList = Collections.list(getClassLoaderInterface().getResources(""));
        for (URL url : classesList) {
            resourceUrls.addAll(fileManager.getAllPhysicalUrls(url));
        }
        return buildUrlSet(resourceUrls).getUrls();
    }

    protected boolean isReloadEnabled() {
        return devMode && reload;
    }

    protected void initReloadClassLoader() {
        // when the configuration is reloaded, a new classloader will be setup
        if (isReloadEnabled() && reloadingClassLoader == null)
            reloadingClassLoader = new ReloadingClassLoader(getClassLoader());
    }

    protected ClassLoaderInterface getClassLoaderInterface() {
        if (isReloadEnabled())
            return new ClassLoaderInterfaceDelegate(this.reloadingClassLoader);
        else {
            /*
             * if there is a ClassLoaderInterface in the context, use it,
             * otherwise default to the default ClassLoaderInterface (a wrapper
             * around the current thread classloader) using this, other plugins
             * (like OSGi) can plugin their own classloader for a while and it
             * will be used by Convention (it cannot be a bean, as Convention is
             * likely to be called multiple times, and it needs to use the
             * default ClassLoaderInterface during normal startup)
             */
            ClassLoaderInterface classLoaderInterface = null;
            ActionContext ctx = ActionContext.getContext();
            if (ctx != null)
                classLoaderInterface = (ClassLoaderInterface) ctx.get(ClassLoaderInterface.CLASS_LOADER_INTERFACE);

            return (ClassLoaderInterface) ObjectUtils.defaultIfNull(classLoaderInterface,
                    new ClassLoaderInterfaceDelegate(getClassLoader()));
        }
    }

    protected ClassLoader getClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }

    private UrlSet buildUrlSet(List<URL> resourceUrls) throws IOException {
        ClassLoaderInterface classLoaderInterface = getClassLoaderInterface();
        UrlSet urlSet = new UrlSet(resourceUrls);
        urlSet.include(new UrlSet(classLoaderInterface, this.fileProtocols));

        //excluding the urls found by the parent class loader is desired, but fails in JBoss (all urls are removed)
        if (excludeParentClassLoader) {
            //exclude parent of classloaders
            ClassLoaderInterface parent = classLoaderInterface.getParent();
            //if reload is enabled, we need to step up one level, otherwise the UrlSet will be empty
            //this happens because the parent of the realoding class loader is the web app classloader
            if (parent != null && isReloadEnabled())
                parent = parent.getParent();

            if (parent != null)
                urlSet = urlSet.exclude(parent);

            try {
                // This may fail in some sandboxes, ie GAE
                ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
                urlSet = urlSet.exclude(new ClassLoaderInterfaceDelegate(systemClassLoader.getParent()));

            } catch (SecurityException e) {
                if (LOG.isWarnEnabled())
                    LOG.warn(
                            "Could not get the system classloader due to security constraints, there may be improper urls left to scan");
            }
        }

        //try to find classes dirs inside war files
        urlSet = urlSet.includeClassesUrl(classLoaderInterface, new UrlSet.FileProtocolNormalizer() {
            public URL normalizeToFileProtocol(URL url) {
                return fileManager.normalizeToFileProtocol(url);
            }
        });

        urlSet = urlSet.excludeJavaExtDirs();
        urlSet = urlSet.excludeJavaEndorsedDirs();
        try {
            urlSet = urlSet.excludeJavaHome();
        } catch (NullPointerException e) {
            // This happens in GAE since the sandbox contains no java.home directory
            if (LOG.isWarnEnabled())
                LOG.warn("Could not exclude JAVA_HOME, is this a sandbox jvm?");
        }
        urlSet = urlSet.excludePaths(System.getProperty("sun.boot.class.path", ""));
        urlSet = urlSet.exclude(".*/JavaVM.framework/.*");

        if (includeJars == null) {
            urlSet = urlSet.exclude(".*?\\.jar(!/|/)?");
        } else {
            //jar urls regexes were specified
            List<URL> rawIncludedUrls = urlSet.getUrls();
            Set<URL> includeUrls = new HashSet<URL>();
            boolean[] patternUsed = new boolean[includeJars.length];

            for (URL url : rawIncludedUrls) {
                if (fileProtocols.contains(url.getProtocol())) {
                    //it is a jar file, make sure it macthes at least a url regex
                    for (int i = 0; i < includeJars.length; i++) {
                        String includeJar = includeJars[i];
                        if (Pattern.matches(includeJar, url.toExternalForm())) {
                            includeUrls.add(url);
                            patternUsed[i] = true;
                            break;
                        }
                    }
                } else {
                    //it is not a jar
                    includeUrls.add(url);
                }
            }

            if (LOG.isWarnEnabled()) {
                for (int i = 0; i < patternUsed.length; i++) {
                    if (!patternUsed[i]) {
                        LOG.warn("The includeJars pattern [#0] did not match any jars in the classpath",
                                includeJars[i]);
                    }
                }
            }
            return new UrlSet(includeUrls);
        }

        return urlSet;
    }

    public static boolean contains(String[] strings, String value, boolean ignoreCase) {
        if (strings != null) {
            for (String string : strings) {
                if ((string.equals(value)) || ((ignoreCase) && (string.equalsIgnoreCase(value)))) {
                    return true;
                }
            }
        }
        return false;
    }

}