de.icongmbh.oss.maven.plugin.javassist.JavassistTransformerExecutor.java Source code

Java tutorial

Introduction

Here is the source code for de.icongmbh.oss.maven.plugin.javassist.JavassistTransformerExecutor.java

Source

/*
 * Copyright 2013 https://github.com/barthel
 *
 * 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 de.icongmbh.oss.maven.plugin.javassist;

import java.io.File;
import java.io.IOException;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Iterator;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
import javassist.CtField.Initializer;
import javassist.build.IClassTransformer;
import javassist.build.JavassistBuildException;
import javassist.bytecode.AccessFlag;
import javassist.bytecode.ClassFile;

/**
 * Executor to perform the transformation by a list of {@link IClassTransformer} instances.
 *
 * @since 1.1.0
 */
public class JavassistTransformerExecutor {

    /**
     * Stamp field name prefix.
     */
    static final String STAMP_FIELD_NAME = "__TRANSFORMED_BY_JAVASSIST_MAVEN_PLUGIN__";

    private IClassTransformer[] transformerInstances = new IClassTransformer[0];

    private String inputDirectory;

    private String outputDirectory;

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

    public JavassistTransformerExecutor() {
        super();
    }

    /**
     * Configure class transformer instances for use with this executor.
     *
     * @param transformerInstances must not be {@code null}
     *
     * @throws NullPointerException if passed {@code transformerInstances} is {@code null}
     */
    public void setTransformerClasses(final IClassTransformer... transformerInstances) {
        this.transformerInstances = transformerInstances.clone();
    }

    /**
     * Sets the output directory where the transformed classes will stored.
     * <p>
     * The configured input directory will used if this directory is {@code null} or empty.
     * </p>
     *
     * @param outputDirectory could be {@code null} or empty.
     *
     * @see #setInputDirectory(String)
     */
    public void setOutputDirectory(final String outputDirectory) {
        this.outputDirectory = outputDirectory;
    }

    /**
     * Returns the output directory where the transformed classes will stored.
     *
     * @return maybe {@code null} or empty
     */
    protected String getOutputDirectory() {
        return outputDirectory;
    }

    /**
     * Sets the input directory where the classes to transform will selected from.
     * <p>
     * Nothing will transformed if this directory is {@code null} or empty.
     * </p>
     *
     * @param inputDirectory could be {@code null} or empty.
     */
    public void setInputDirectory(final String inputDirectory) {
        this.inputDirectory = inputDirectory;
    }

    /**
     * Returns the input directory where the the classes to transform will selected from.
     *
     * @return maybe {@code null} or empty
     */
    protected String getInputDirectory() {
        return inputDirectory;
    }

    /**
     * Executes all configured {@link IClassTransformer}.
     *
     * @see #setTransformerClasses(IClassTransformer...)
     * @see #execute(IClassTransformer)
     */
    public void execute() {
        for (final IClassTransformer transformer : transformerInstances) {
            execute(transformer);
        }
    }

    /**
     * Execute transformation with passed {@link IClassTransformer}.
     *
     * <p>
     * This method uses {@link #getInputDirectory() } and {@link #getOutputDirectory() } and calls
     * {@link #transform(IClassTransformer, String, String)}.
     * </p>
     * <p>
     * If the passed {@code transformer} is {@code null} nothing will transformed.
     * </p>
     *
     * @param transformer the transformer that will apply transformations could be {@code null}.
     *
     * @see #getInputDirectory()
     * @see #getOutputDirectory()
     * @see #transform(IClassTransformer, String, String)
     */
    protected void execute(final IClassTransformer transformer) {
        transform(transformer, getInputDirectory(), getOutputDirectory());
    }

    /**
     * Search for class files on the passed directory name ({@link #iterateClassnames(String)}) and
     * apply transformation to each one (
     * {@link #transform(IClassTransformer, String, String, Iterator)}).
     *
     * <p>
     * <strong>Limitation:</strong> do not search inside .jar files.
     * </p>
     * <p>
     * If the passed {@code transformer} is {@code null} or the passed {@code directory} is
     * {@code null} or empty nothing will transformed.
     * </p>
     *
     * @param transformer the transformer that will apply transformations could be {@code null}.
     * @param directory could be {@code null} or empty. The input and output directory are the same.
     *
     * @see #iterateClassnames(String)
     * @see #transform(IClassTransformer, String, String, Iterator)
     *
     */
    public final void transform(final IClassTransformer transformer, final String directory) {
        transform(transformer, directory, directory, iterateClassnames(directory));
    }

    /**
     * Search for class files on the passed input directory ({@link #iterateClassnames(String)}) and
     * apply transformation to each one (
     * {@link #transform(IClassTransformer, String, String, Iterator)}).
     * <p>
     * <strong>Limitation:</strong> do not search inside .jar files.
     * </p>
     *
     * @param transformer The transformer that will apply transformations could be {@code null}.
     * @param inputDir The root directory where the classes to transform will selected from could
     *          be {@code null} or empty. If it is {@code null} or empty nothing will be transformed.
     * @param outputDir The output directory where the transformed classes will stored could be
     *          {@code null} or empty. If it is {@code null} or empty the {@code inputDir} will be
     *          used.
     *
     * @see #iterateClassnames(String)
     * @see #transform(IClassTransformer, String, String, Iterator)
     */
    public void transform(final IClassTransformer transformer, final String inputDir, final String outputDir) {
        transform(transformer, inputDir, outputDir, iterateClassnames(inputDir));
    }

    /**
     * Transform each class passed via {@link Iterator} of class names.
     * <p>
     * Use the passed {@code className} iterator, load each one as {@link CtClass}, filter the valid
     * candidates and apply transformation to each one.
     * </p>
     * <p>
     * <strong>Limitation:</strong> do not search inside .jar files.
     * </p>
     * <p>
     * Any unexpected (internal catched) {@link Exception} will be re-thrown in an
     * {@link RuntimeException}.
     * </p>
     *
     * @param transformer The transformer that will apply transformations could be {@code null}.
     * @param inputDir The root directory where the classes to transform will selected from could
     *          be {@code null} or empty. If it is {@code null} or empty nothing will be transformed.
     * @param outputDir The output directory where the transformed classes will stored could be
     *          {@code null} or empty. If it is {@code null} or empty the {@code inputDir} will be
     *          used.
     * @param classNames could be {@code null} or empty. If it is {@code null} or empty nothing will
     *          be transformed.
     *
     * @see #initializeClass(ClassPool, CtClass)
     * @see IClassTransformer#shouldTransform(CtClass)
     * @see IClassTransformer#applyTransformations(CtClass)
     */
    public final void transform(final IClassTransformer transformer, final String inputDir, final String outputDir,
            final Iterator<String> classNames) {
        if (null == transformer) {
            return;
        }
        if (null == inputDir || inputDir.trim().isEmpty()) {
            return;
        }
        if (null == classNames || !classNames.hasNext()) {
            return;
        }
        final String inDirectory = inputDir.trim();
        try {
            final ClassPool classPool = configureClassPool(buildClassPool(), inDirectory);
            final String outDirectory = evaluateOutputDirectory(outputDir, inDirectory);
            int classCounter = 0;
            while (classNames.hasNext()) {
                final String className = classNames.next();
                if (null == className) {
                    continue;
                }
                try {
                    LOGGER.debug("Got class name {}", className);
                    classPool.importPackage(className);
                    final CtClass candidateClass = classPool.get(className);
                    initializeClass(classPool, candidateClass);
                    if (!hasStamp(candidateClass) && transformer.shouldTransform(candidateClass)) {
                        transformer.applyTransformations(candidateClass);
                        applyStamp(candidateClass);
                        // #48
                        for (final CtClass nestedClass : candidateClass.getNestedClasses()) {
                            if (!nestedClass.isModified() || hasStamp(nestedClass)) {
                                continue;
                            }
                            final CtClass nestedCtClass = classPool.get(nestedClass.getName());
                            initializeClass(classPool, nestedCtClass);
                            applyStamp(nestedCtClass);
                            nestedCtClass.writeFile(outDirectory);
                        }
                        candidateClass.writeFile(outDirectory);
                        LOGGER.debug("Class {} instrumented by {}", className, getClass().getName());
                        ++classCounter;
                    }
                } catch (final NotFoundException e) {
                    LOGGER.warn("Class {} could not be resolved due to dependencies not found on "
                            + "current classpath (usually your class depends on \"provided\""
                            + " scoped dependencies).", className);
                } catch (final IOException ex) { // EOFException ...
                    LOGGER.error("Class {} could not be instrumented due to initialize FAILED.", className, ex);
                } catch (final CannotCompileException ex) {
                    LOGGER.error("Class {} could not be instrumented due to initialize FAILED.", className, ex);
                } catch (final JavassistBuildException ex) {
                    LOGGER.error("Class {} could not be instrumented due to initialize FAILED.", className, ex);
                }
            }
            LOGGER.info("#{} classes instrumented by {}", classCounter, getClass().getName());
        } catch (final NotFoundException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    /**
     * Evaluates and returns the output directory.
     *
     * <p>
     * If the passed {@code outputDir} is {@code null} or empty, the passed {@code inputDir} otherwise
     * the {@code outputDir} will returned.
     *
     * @param outputDir could be {@code null} or empty
     * @param inputDir must not be {@code null}
     *
     * @return never {@code null}
     *
     * @throws NullPointerException if passed {@code inputDir} is {@code null}
     *
     * @since 1.2.0
     */
    protected String evaluateOutputDirectory(final String outputDir, final String inputDir) {
        return outputDir != null && !outputDir.trim().isEmpty() ? outputDir : inputDir.trim();
    }

    /**
     * Creates a new instance of a {@link ClassPool}.
     *
     * @return never {@code null}
     *
     * @since 1.2.0
     */
    protected ClassPool buildClassPool() {
        // create new classpool for transform; don't blow up the default
        return new ClassPool(ClassPool.getDefault());
    }

    /**
     * Configure the passed instance of a {@link ClassPool} and append required class pathes on it.
     *
     * @param classPool must not be {@code null}
     * @param inputDir must not be {@code null}
     *
     * @return never {@code null}
     *
     * @throws NotFoundException if passed {@code classPool} is {@code null} or if passed
     *           {@code inputDir} is a JAR or ZIP and not found.
     * @throws NullPointerException if passed {@code inputDir} is {@code null}
     *
     * @since 1.2.0
     */
    protected ClassPool configureClassPool(final ClassPool classPool, final String inputDir)
            throws NotFoundException {
        classPool.childFirstLookup = true;
        classPool.appendClassPath(inputDir);
        classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
        classPool.appendSystemPath();
        debugClassLoader(classPool);
        return classPool;
    }

    /**
     * Search for class files (file extension: {@code .class}) on the passed {@code directory}.
     * <p>
     * Note: The passed directory name must exist and readable.
     * </p>
     *
     * @param directory must nor be {@code null}
     * @return iterator of full qualified class names and never {@code null}
     *
     * @throws NullPointerException if passed {@code directory} is {@code null}.
     *
     * @see SuffixFileFilter
     * @see TrueFileFilter
     * @see FileUtils#iterateFiles(File, IOFileFilter, IOFileFilter)
     * @see ClassnameExtractor#iterateClassnames(File, Iterator)
     */
    protected Iterator<String> iterateClassnames(final String directory) {
        final File dir = new File(directory);
        final String[] extensions = { ".class" };
        final IOFileFilter fileFilter = new SuffixFileFilter(extensions);
        final IOFileFilter dirFilter = TrueFileFilter.INSTANCE;
        return ClassnameExtractor.iterateClassnames(dir, FileUtils.iterateFiles(dir, fileFilter, dirFilter));
    }

    /**
     * Apply a "stamp" to a class to indicate it has been modified.
     * <p>
     * By default, this method uses a boolean field named {@value #STAMP_FIELD_NAME} as the stamp. Any
     * class overriding this method should also override {@link #hasStamp(CtClass)}.
     * </p>
     *
     * @param candidateClass the class to mark/stamp must not be {@code null}.
     *
     * @throws NullPointerException if passed {@code candidateClass} is {@code null}.
     * @throws CannotCompileException by {@link CtClass#addField(CtField, CtField.Initializer)}
     *
     * @see #createStampField(CtClass)
     * @see CtClass#addField(CtField, CtField.Initializer)
     */
    protected void applyStamp(CtClass candidateClass) throws CannotCompileException {
        candidateClass.addField(createStampField(candidateClass), Initializer.constant(true));
    }

    /**
     * Remove a "stamp" from a class if the "stamp" field is available.
     * <p>
     * By default, this method removes a boolean field named {@value #STAMP_FIELD_NAME}. Any class
     * overriding this method should also override {@link #hasStamp(CtClass)}.
     * </p>
     *
     * @param candidateClass the class to remove the mark/stamp from must not be {@code null}
     *
     * @throws NullPointerException if passed {@code candidateClass} is {@code null}.
     * @throws CannotCompileException by {@link CtClass#removeField(CtField)}
     *
     * @see #createStampField(CtClass)
     * @see CtClass#removeField(CtField)
     */
    protected void removeStamp(CtClass candidateClass) throws CannotCompileException {
        try {
            candidateClass.removeField(createStampField(candidateClass));
        } catch (final NotFoundException e) {
            // ignore; mission accomplished.
        }
    }

    /**
     * Indicates whether a class holds a stamp or not.
     * <p>
     * By default, this method uses a boolean field named {@value #STAMP_FIELD_NAME} as the stamp. Any
     * class overriding this method should also override {@link #applyStamp(CtClass)} and
     * {@link #removeStamp(CtClass) }.
     * </p>
     *
     * @param candidateClass the class to check must not be {@code null}.
     *
     * @return {@code true} if the class owns the stamp, otherwise {@code false}.
     *
     * @throws NullPointerException if passed {@code candidateClass} is {@code null}.
     * @see CtClass#getDeclaredField(String)
     */
    protected boolean hasStamp(CtClass candidateClass) {
        boolean hasStamp;
        try {
            hasStamp = null != candidateClass.getDeclaredField(createStampFieldName());
        } catch (NotFoundException e) {
            hasStamp = false;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Stamp {}{} found in class {}", createStampFieldName(), (hasStamp ? "" : " NOT"),
                    candidateClass.getName());
        }
        return hasStamp;
    }

    /**
     * Creates the name of the stamp field.
     * <p>
     * This implementation appends {@value #STAMP_FIELD_NAME} with the full qualified class name and
     * replaces all non-word characters (like '.') with '_'.
     * </p>
     *
     * @return never {@code null} or empty.
     */
    private String createStampFieldName() {
        return STAMP_FIELD_NAME + getClass().getName().replaceAll("\\W", "_");
    }

    /**
     * Creates a {@link CtField} instance associated with the passed {@code candidateClass}.
     *
     * @param candidateClass must not be {@code null}
     *
     * @return never {@code null}
     *
     * @throws NullPointerException if passed {@code candidateClass} is {@code null}.
     * @throws CannotCompileException field could not created
     *
     * @see CtField
     */
    private CtField createStampField(final CtClass candidateClass) throws CannotCompileException {
        int stampModifiers = AccessFlag.STATIC | AccessFlag.FINAL;
        if (!candidateClass.isInterface()) {
            stampModifiers |= AccessFlag.PRIVATE;
        } else {
            stampModifiers |= AccessFlag.PUBLIC;
        }
        final CtField stampField = new CtField(CtClass.booleanType, createStampFieldName(), candidateClass);
        stampField.setModifiers(stampModifiers);
        return stampField;
    }

    private void initializeClass(final ClassPool classPool, final CtClass candidateClass) throws NotFoundException {
        debugClassFile(candidateClass.getClassFile2());
        // TODO hack to initialize class to avoid further NotFoundException (what's the right way of
        // doing this?)
        candidateClass.subtypeOf(classPool.get(Object.class.getName()));
    }

    private void debugClassFile(final ClassFile classFile) {
        if (!LOGGER.isDebugEnabled()) {
            return;
        }
        LOGGER.debug(" - class: {}", classFile.getName());
        LOGGER.debug(" -- Java version: {}.{}", classFile.getMajorVersion(), classFile.getMinorVersion());
        LOGGER.debug(" -- interface: {} abstract: {} final: {}", classFile.isInterface(), classFile.isAbstract(),
                classFile.isFinal());
        LOGGER.debug(" -- extends class: {}", classFile.getSuperclass());
        LOGGER.debug(" -- implements interfaces: {}", Arrays.deepToString(classFile.getInterfaces()));
    }

    private void debugClassLoader(final ClassPool classPool) {
        if (!LOGGER.isDebugEnabled()) {
            return;
        }
        LOGGER.debug(" - classPool: {}", classPool.toString());
        ClassLoader classLoader = classPool.getClassLoader();
        while (classLoader != null) {
            LOGGER.debug(" -- {}: {}", classLoader.getClass().getName(), classLoader.toString());
            if (classLoader instanceof URLClassLoader) {
                LOGGER.debug(" --- urls: {}", Arrays.deepToString(((URLClassLoader) classLoader).getURLs()));
            }
            classLoader = classLoader.getParent();
        }
    }

}