org.microtitan.diffusive.launcher.DiffusiveLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.microtitan.diffusive.launcher.DiffusiveLoader.java

Source

/*
 * Copyright 2012 Robert Philipp
 * 
 * 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.
 */
package org.microtitan.diffusive.launcher;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.Loader;
import javassist.NotFoundException;
import javassist.Translator;

import org.apache.log4j.Logger;
import org.microtitan.diffusive.Constants;
import org.microtitan.diffusive.annotations.DiffusiveConfiguration;

/**
 * Class loader that provides the ability to load additional classes with the same class loader that is
 * used to load the and run the application with the {@link #run(String[])} or {@link #resolveClass(Class)} method.
 * For example, suppose you want to load a singleton as part of a set-up or configuration process, then you
 * can specify a set of classes and their associated static methods that will be loaded with the same class loader
 * that will be used to load and run the application.
 * 
 * Additionally, you can also specify prefixes of class names (package + class name) for which loading will be 
 * delegated to the parent class loader. This is useful, for example, to handle things like Apache's log4j configuration.
 * 
 * @author Robert Philipp
 */
public class DiffusiveLoader extends Loader {

    private static final Logger LOGGER = Logger.getLogger(DiffusiveLoader.class);

    //   private final List< String > configurationClasses;
    private final Map<String, Object[]> configurationClasses;
    private final List<String> delegationPrefixes;

    // hold a reference to this, because we need it to overload the javassist Loader
    private ClassPool classPool;
    private Translator translator;

    /**
      * Creates a new class loader using the specified parent class loader for delegation. Also
      * provides a mechanism for running additional configuration that will be loaded by the
      * same class loader that is loading the application with the diffuser annotations.
     * @param configClasses A {@link List} containing the names of configuration classes that are 
     * used for configuration. Because these need to be loaded by this class loader, they must all 
     * be static methods (i.e. the class shouldn't have already been loaded) and they must be annotated
     * with the @{@link DiffusiveConfiguration} annotation. Associated with each configuration class is
     * an {@code {@link Object}[]} containing any arguments the configuration method may need.
     * @param delegationPrefixes The list of prefixes to the fully qualified class name. Classes whose fully qualified class
     * names start with one of these prefixes are loaded by the parent class loader instead of this one.
     * @param parentLoader the parent class loader of this class loader
     * @param classPool the source of the class files
     */
    public DiffusiveLoader(final Map<String, Object[]> configClasses, final List<String> delegationPrefixes,
            final ClassLoader parentLoader, final ClassPool classPool) {
        super(parentLoader, classPool);
        configurationClasses = new LinkedHashMap<>(configClasses);
        this.delegationPrefixes = new ArrayList<>(delegationPrefixes);

        this.classPool = classPool;
    }

    /**
      * Creates a new class loader using the specified parent class loader for delegation. Also
      * provides a mechanism for running additional configuration that will be loaded by the
      * same class loader that is loading the application with the diffuser annotations.
     * @param configClasses A {@link List} containing the names of configuration classes that are 
     * used for configuration. Because these need to be loaded by this class loader, they must all 
     * be static methods (i.e. the class shouldn't have already been loaded) and they must be annotated
     * with the @{@link DiffusiveConfiguration} annotation. Associated with each configuration class is
     * an {@code {@link Object}[]} containing any arguments the configuration method may need.
     * @param parentLoader the parent class loader of this class loader
     * @param classPool the source of the class files
     */
    public DiffusiveLoader(final Map<String, Object[]> configClasses, final ClassLoader parentLoader,
            final ClassPool classPool) {
        this(configClasses, createDefaultDelegationPrefixes(), parentLoader, classPool);
    }

    /**
      * Creates a new class loader using the specified parent class loader for delegation.
     * @param parentLoader the parent class loader of this class loader
     * @param classPool the source of the class files
     */
    public DiffusiveLoader(final ClassLoader parentLoader, final ClassPool classPool) {
        this(new LinkedHashMap<String, Object[]>(), parentLoader, classPool);
    }

    /**
      * Creates a new class loader. Also provides a mechanism for running additional configuration that 
      * will be loaded by the same class loader that is loading the application with the diffuser annotations.
     * @param configClasses A {@link List} containing the names of configuration classes that are 
     * used for configuration. Because these need to be loaded by this class loader, they must all 
     * be static methods (i.e. the class shouldn't have already been loaded) and they must be annotated
     * with the @{@link DiffusiveConfiguration} annotation. Associated with each configuration class is
     * an {@code {@link Object}[]} containing any arguments the configuration method may need.
     * @param classPool the source of the class files
     */
    public DiffusiveLoader(final Map<String, Object[]> configClasses, final ClassPool classPool) {
        super(classPool);
        configurationClasses = new LinkedHashMap<>(configClasses);
        delegationPrefixes = createDefaultDelegationPrefixes();

        this.classPool = classPool;
    }

    /**
      * Creates a new class loader.
     * @param classPool the source of the class files
     */
    public DiffusiveLoader(final ClassPool classPool) {
        this(new LinkedHashMap<String, Object[]>(), classPool);
    }

    /**
      * Creates a new class loader. Also provides a mechanism for running additional configuration that 
      * will be loaded by the same class loader that is loading the application with the diffuser annotations.
     * @param configClasses A {@link List} containing the names of configuration classes that are 
     * used for configuration. Because these need to be loaded by this class loader, they must all 
     * be static methods (i.e. the class shouldn't have already been loaded) and they must be annotated
     * with the @{@link DiffusiveConfiguration} annotation. Associated with each configuration class is
     * an {@code {@link Object}[]} containing any arguments the configuration method may need.
     */
    public DiffusiveLoader(final Map<String, Object[]> configClasses) {
        super();
        configurationClasses = new LinkedHashMap<>(configClasses);
        delegationPrefixes = createDefaultDelegationPrefixes();
    }

    /**
      * Creates a new class loader.
     */
    public DiffusiveLoader() {
        this(new LinkedHashMap<String, Object[]>());
    }

    /**
     * Adds the name of a class that contains configuration items. A configuration item is
     * represented by a method the has a @{@link DiffusiveConfiguration} annotation.
     * @param className The name of a class containing configuration items.
     * @return true if the class name was added to the list of configuration classes; false otherwise
     * @see #invokeConfigurationClasses()
     */
    public Object[] addConfigurationClass(final String className, final Object[] parameters) {
        if (configurationClasses.containsKey(className)) {
            final StringBuilder message = new StringBuilder();
            message.append("Replaced parameters of the configuration class: ").append(Constants.NEW_LINE)
                    .append("  Configuration Class: ").append(className).append(Constants.NEW_LINE)
                    .append("  Parameters: ").append(parameters.toString());
            LOGGER.info(message.toString());
        }
        return configurationClasses.put(className, parameters);
    }

    /**
     * Removes the name of a class that contains configuration items. A configuration item is
     * represented by a method the has a @{@link DiffusiveConfiguration} annotation.
     * @param className The name of a class containing configuration items.
     * @return true if the class name was removed to the list of configuration classes; false otherwise
     * @see #invokeConfigurationClasses()
     */
    public Object[] removeConfigurationClass(final String className) {
        return configurationClasses.remove(className);
    }

    /**
     * Removes all the name of a class that contains configuration items. A configuration item is
     * represented by a method the has a @{@link DiffusiveConfiguration} annotation.
     * @see #invokeConfigurationClasses()
     */
    public void clearConfigurationItems() {
        configurationClasses.clear();
    }

    /**
     * @return Creates and returns the default list of delegation prefixes. These are the
     * prefixes of fully qualified class names that should be loaded by the parent class
     * loader, instead of this class loader.
     * @see #loadClassByDelegation(String)
     */
    protected static List<String> createDefaultDelegationPrefixes() {
        final List<String> prefixes = new ArrayList<>();
        prefixes.add("org.apache.log4j.");
        prefixes.add("org.apache.commons.logging.");
        prefixes.add("org.apache.abdera.");

        // we want to make sure that the DiffusiveConfiguration annotation is loaded
        // by the default app class loader, and NOT this one.
        prefixes.add(DiffusiveConfiguration.class.getName());

        return prefixes;
    }

    /**
     * Add a delegation prefix to the list of prefixes used to determine what classes get loaded by
     * the parent class loader instead of this one. Useful for loggers and configuration classes. 
     * The following conditions apply: 
     * <ul>
     *    <li>If the specified prefix already exists, then nothing is added.</li>
     *    <li>If the specified prefix is more specific than an existing one, then it isn't added. For example, 
     *       if {@code "org.microtitan."} already exists, and {@code "org.microtitan.testing."} is specified, 
     *       then the specified prefix is unnecessary, and therefore, isn't added.</li> 
     *    <li>And if the specified prefix is more general than an existing one, then the specified
     *       one is added, and the more specific one is removed because it becomes superfluous.</li>
     * @param prefix The prefix to the fully qualified class name. Classes whose fully qualified class
     * names start with one of these prefixes are loaded by the parent class loader instead of this one.
     * @return true of the delegation prefix was added; false otherwise.
     * @see #loadClassByDelegation(String)
     */
    public final boolean addDelegationPrefix(final String prefix) {
        boolean isAdded = false;
        if (delegationPrefixes.contains(prefix)) {
            isAdded = true;
        } else {
            // loop through a copy of the delegation prefixes
            for (String delegationPrefix : new ArrayList<>(delegationPrefixes)) {
                if (prefix.startsWith(delegationPrefix)) {
                    final StringBuilder message = new StringBuilder();
                    message.append(
                            "Attempting to a delegation prefix that is more specific than an existing delegation prefix.")
                            .append(Constants.NEW_LINE)
                            .append("Specified prefix not added, but behavior continues as expected.")
                            .append(Constants.NEW_LINE).append("  Specified Prefix: ").append(prefix)
                            .append(Constants.NEW_LINE).append("  Existing Prefix: ").append(delegationPrefix);
                    LOGGER.warn(message.toString());

                    // should act as if the delegation prefix was added, because the behavior is the same
                    isAdded = true;

                    // we're done with the loop
                    break;
                } else if (delegationPrefix.startsWith(prefix)) {
                    final StringBuilder message = new StringBuilder();
                    message.append(
                            "Attempting to a delegation prefix that is more general than an existing delegation prefix.")
                            .append(Constants.NEW_LINE)
                            .append("Specified prefix will be added, but the more specific one will be removed.")
                            .append(Constants.NEW_LINE).append("  Specified Prefix: ").append(prefix)
                            .append(Constants.NEW_LINE).append("  Existing Prefix: ").append(delegationPrefix);
                    LOGGER.warn(message.toString());

                    // should act as if the delegation prefix was added, because the behavior is the same
                    isAdded = delegationPrefixes.add(prefix);
                    if (isAdded) {
                        delegationPrefixes.remove(delegationPrefix);
                    }

                    // we're done with the loop
                    break;
                }
            }
            if (!isAdded) {
                isAdded = delegationPrefixes.add(prefix);
            }
        }

        return isAdded;
    }

    /**
     * Removes the specified prefix from the list of delegation prefixes. These are the
     * prefixes of fully qualified class names that should be loaded by the parent class
     * loader, instead of this class loader.
     * @param prefix The prefix to remove from the list of delegation prefixes.
     * @return true if the prefix was removed from the list of delegation prefixes; false otherwise
     * @see #loadClassByDelegation(String)
     */
    public boolean removeDelegationPrefix(final String prefix) {
        return delegationPrefixes.remove(prefix);
    }

    /**
     * Removes all the prefixes from the list of delegation prefixes. These are the
     * prefixes of fully qualified class names that should be loaded by the parent class
     * loader, instead of this class loader.
     * @see #loadClassByDelegation(String)
     */
    public void clearDelegationPrefixes() {
        delegationPrefixes.clear();
    }

    /*
     * (non-Javadoc)
     * @see javassist.Loader#setClassPool(javassist.ClassPool)
     */
    @Override
    public final void setClassPool(final ClassPool classPool) {
        super.setClassPool(classPool);
        this.classPool = classPool;
    }

    /**
     * @return The source of the class files
     */
    protected final ClassPool getClassPool() {
        return classPool;
    }

    /*
     * (non-Javadoc)
     * @see javassist.Loader#addTranslator(javassist.ClassPool, javassist.Translator)
     */
    @Override
    public final void addTranslator(final ClassPool classPool, final Translator translator)
            throws NotFoundException, CannotCompileException {
        super.addTranslator(classPool, translator);
        this.classPool = classPool;
        this.translator = translator;
    }

    /**
     * @return the translator that gets called when a class is loaded
     */
    protected final Translator getTranslator() {
        return translator;
    }

    /* 
     * (non-Javadoc)
     * @see javassist.Loader#run(java.lang.String[])
     */
    @Override
    public void run(final String[] args) throws Throwable {
        // invoke the methods of the classes that are used to configure Diffusive
        // For example, we may load the KeyedDiffuserRepository and set the diffuser
        // that we like. Using this method ensures that the loading happens using the
        // same class loader (this one) as is used to load the application that is to be
        // run.
        invokeConfigurationClasses();

        super.run(args);
    }

    /* 
     * (non-Javadoc)
     * @see javassist.Loader#run(java.lang.String, java.lang.String[])
     */
    @Override
    public void run(final String classname, final String[] args) throws Throwable {
        // invoke the methods of the classes that are used to configure Diffusive
        // For example, we may load the KeyedDiffuserRepository and set the diffuser
        // that we like. Using this method ensures that the loading happens using the
        // same class loader (this one) as is used to load the application that is to be
        // run.
        invokeConfigurationClasses();

        // call the Javassist class loader's run method to load and run the code
        super.run(classname, args);
    }

    /**
     * Invokes the methods of the classes specified in the {@link #configurationClasses} list
     * that are annotated with @{@link DiffusiveConfiguration}.
     * 
     * For this method to work, the @{@link DiffusiveConfiguration} class name must be in the list
     * of prefixes ({@link #delegationPrefixes}) that are delegated to the parent class loader. If
     * the annotation does not appear in the list, it may be loaded by this class loader instead
     * of the default app class loader, and this method will not see the configuration method's
     * annotation, and will therefore, NOT call it.
     *  
     * @throws Throwable
     */
    private void invokeConfigurationClasses() throws Throwable {
        // run through the class names, load the classes, and then invoke the configuration methods
        // (that have been annotated with @DiffusiveConfiguration)
        for (Map.Entry<String, Object[]> className : configurationClasses.entrySet()) {
            final Class<?> setupClazz = loadClass(className.getKey());
            Method configurationMethod = null;
            try {
                // grab the methods that have an annotation @DiffusiveConfiguration and invoke them
                for (final Method method : setupClazz.getMethods()) {
                    if (method.isAnnotationPresent(DiffusiveConfiguration.class)) {
                        // hold on the the method in case there is an invocation exception
                        // and to warn the user if no configuration method was found
                        configurationMethod = method;
                        method.invoke(null, className.getValue());
                    }
                }
                if (configurationMethod == null) {
                    final StringBuilder message = new StringBuilder();
                    message.append("Error finding a method annotated with @")
                            .append(DiffusiveConfiguration.class.getSimpleName()).append(Constants.NEW_LINE)
                            .append("  Configuration Class: ").append(className).append(Constants.NEW_LINE);
                    LOGGER.warn(message.toString());
                }
            } catch (InvocationTargetException e) {
                final StringBuilder message = new StringBuilder();
                message.append("Error invoking target method.").append(Constants.NEW_LINE).append("  Class Name: ")
                        .append(className).append(Constants.NEW_LINE).append("  Method Name: ")
                        .append(configurationMethod.getName());
                LOGGER.error(message.toString(), e);
                throw new IllegalArgumentException(message.toString(), e);
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see javassist.Loader#loadClassByDelegation(java.lang.String)
     */
    @Override
    protected Class<?> loadClassByDelegation(final String name) throws ClassNotFoundException {
        // ask the javassist class loader to delegate what it wants to delegate
        Class<?> clazz = super.loadClassByDelegation(name);

        // if not delegated then check if we want to delegate
        if (clazz == null) {
            for (String prefix : delegationPrefixes) {
                if (name.startsWith(prefix)) {
                    clazz = delegateToParent(name);
                    break;
                }
            }
        }
        return clazz;
    }
}