org.codehaus.mojo.clirr.AbstractClirrMojo.java Source code

Java tutorial

Introduction

Here is the source code for org.codehaus.mojo.clirr.AbstractClirrMojo.java

Source

package org.codehaus.mojo.clirr;

/*
 * Copyright 2006 The Apache Software Foundation.
 *
 * 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.
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import net.sf.clirr.core.Checker;
import net.sf.clirr.core.CheckerException;
import net.sf.clirr.core.ClassFilter;
import net.sf.clirr.core.PlainDiffListener;
import net.sf.clirr.core.Severity;
import net.sf.clirr.core.XmlDiffListener;
import net.sf.clirr.core.internal.bcel.BcelJavaType;
import net.sf.clirr.core.internal.bcel.BcelTypeArrayBuilder;
import net.sf.clirr.core.spi.JavaType;

import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.util.ClassLoaderRepository;
import org.apache.bcel.util.Repository;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.OverConstrainedVersionException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.artifact.InvalidDependencyVersionException;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.IOUtil;

/**
 * Base parameters for Clirr check and report.
 *
 * @author <a href="mailto:brett@apache.org">Brett Porter</a>
 * @todo i18n exceptions, log messages
 * @requiresDependencyResolution compile
 */
public abstract class AbstractClirrMojo extends AbstractMojo {
    /**
     * Flag to easily skip execution.
     *
     * @parameter expression="${clirr.skip}" default-value="false"
     */
    protected boolean skip;

    /**
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    protected MavenProject project;

    /**
     * @component
     */
    protected ArtifactResolver resolver;

    /**
     * @component
     */
    protected ArtifactFactory factory;

    /**
     * @parameter default-value="${localRepository}"
     * @required
     * @readonly
     */
    protected ArtifactRepository localRepository;

    /**
     * @component
     */
    private ArtifactMetadataSource metadataSource;

    /**
     * @component
     */
    private MavenProjectBuilder mavenProjectBuilder;

    /**
     * The classes of this project to compare the last release against.
     *
     * @parameter default-value="${project.build.outputDirectory}
     */
    protected File classesDirectory;

    /**
     * Version to compare the current code against.
     *
     * @parameter expression="${comparisonVersion}" default-value="(,${project.version})"
     */
    protected String comparisonVersion;

    /**
     * List of artifacts to compare the current code against. This
     * overrides <code>comparisonVersion</code>, if present.
     * Each <code>comparisonArtifact</code> is made of a <code>groupId</code>, an <code>artifactId</code> and
     * a <code>version</code> number. Optionally it may have a <code>classifier</code>
     * (default null) and a <code>type</code> (default "jar").
     * @parameter
     */
    protected ArtifactSpecification[] comparisonArtifacts;

    /**
     * Show only messages of this severity or higher. Valid values are
     * <code>info</code>, <code>warning</code> and <code>error</code>.
     *
     * @parameter expression="${minSeverity}" default-value="warning"
     */
    protected String minSeverity;

    /**
     * A text output file to render to. If omitted, no output is rendered to a text file.
     *
     * @parameter expression="${textOutputFile}"
     */
    protected File textOutputFile;

    /**
     * An XML file to render to. If omitted, no output is rendered to an XML file.
     *
     * @parameter expression="${xmlOutputFile}"
     */
    protected File xmlOutputFile;

    /**
     * A list of classes to include. Anything not included is excluded. If omitted, all are assumed to be included.
     * Values are specified in path pattern notation, e.g. <code>org/codehaus/mojo/**</code>.
     *
     * @parameter
     */
    protected String[] includes;

    /**
     * A list of classes to exclude. These classes are excluded from the list of classes that are included.
     * Values are specified in path pattern notation, e.g. <code>org/codehaus/mojo/**</code>.
     *
     * @parameter
     */
    protected String[] excludes;

    /**
     * Whether to log the results to the console or not.
     *
     * @parameter expression="${logResults}" default-value="false"
     */
    protected boolean logResults;

    private static final URL[] EMPTY_URL_ARRAY = new URL[0];

    public void execute() throws MojoExecutionException, MojoFailureException {
        if (skip) {
            getLog().info("Skipping execution");
        } else {
            doExecute();
        }
    }

    protected abstract void doExecute() throws MojoExecutionException, MojoFailureException;

    public ClirrDiffListener executeClirr() throws MojoExecutionException, MojoFailureException {
        return executeClirr(null);
    }

    protected ClirrDiffListener executeClirr(Severity minSeverity)
            throws MojoExecutionException, MojoFailureException {
        ClirrDiffListener listener = new ClirrDiffListener();

        ClassFilter classFilter = new ClirrClassFilter(includes, excludes);

        JavaType[] origClasses = resolvePreviousReleaseClasses(classFilter);

        JavaType[] currentClasses = resolveCurrentClasses(classFilter);

        // Create a Clirr checker and execute
        Checker checker = new Checker();

        List listeners = new ArrayList();

        listeners.add(listener);

        if (xmlOutputFile != null) {
            try {
                listeners.add(new XmlDiffListener(xmlOutputFile.getAbsolutePath()));
            } catch (IOException e) {
                throw new MojoExecutionException(
                        "Error adding '" + xmlOutputFile + "' for output: " + e.getMessage(), e);
            }
        }

        if (textOutputFile != null) {
            try {
                listeners.add(new PlainDiffListener(textOutputFile.getAbsolutePath()));
            } catch (IOException e) {
                throw new MojoExecutionException(
                        "Error adding '" + textOutputFile + "' for output: " + e.getMessage(), e);
            }
        }

        if (logResults) {
            listeners.add(new LogDiffListener(getLog()));
        }

        checker.addDiffListener(new DelegatingListener(listeners, minSeverity));

        reportDiffs(checker, origClasses, currentClasses);

        return listener;
    }

    private JavaType[] resolveCurrentClasses(ClassFilter classFilter) throws MojoExecutionException {
        try {
            ClassLoader currentDepCL = createClassLoader(project.getArtifacts(), null);
            return createClassSet(classesDirectory, currentDepCL, classFilter);
        } catch (MalformedURLException e) {
            throw new MojoExecutionException("Error creating classloader for current classes", e);
        }
    }

    private JavaType[] resolvePreviousReleaseClasses(ClassFilter classFilter)
            throws MojoFailureException, MojoExecutionException {
        final Set previousArtifacts;
        final Artifact firstPreviousArtifact;
        if (comparisonArtifacts == null) {
            firstPreviousArtifact = getComparisonArtifact();
            comparisonVersion = firstPreviousArtifact.getVersion();
            getLog().info("Comparing to version: " + comparisonVersion);
            previousArtifacts = Collections.singleton(firstPreviousArtifact);
        } else {
            previousArtifacts = resolveArtifacts(comparisonArtifacts);
            Artifact a = null;
            for (Iterator iter = previousArtifacts.iterator(); iter.hasNext();) {
                Artifact artifact = (Artifact) iter.next();
                if (a == null) {
                    a = artifact;
                }
                getLog().debug("Comparing to " + artifact.getGroupId() + ":" + artifact.getArtifactId() + ":"
                        + artifact.getVersion() + ":" + artifact.getClassifier() + ":" + artifact.getType());
            }
            firstPreviousArtifact = a;
        }

        try {
            for (Iterator iter = previousArtifacts.iterator(); iter.hasNext();) {
                Artifact artifact = (Artifact) iter.next();
                resolver.resolve(artifact, project.getRemoteArtifactRepositories(), localRepository);
            }

            final List dependencies = getTransitiveDependencies(previousArtifacts);

            ClassLoader origDepCL = createClassLoader(dependencies, previousArtifacts);
            final Set files = new HashSet();
            for (Iterator iter = previousArtifacts.iterator(); iter.hasNext();) {
                Artifact artifact = (Artifact) iter.next();
                // clirr expects jar files, so let's not pass other artifact files.
                if ("jar".equals(artifact.getType())) {
                    files.add(new File(localRepository.getBasedir(), localRepository.pathOf(artifact)));
                }
            }
            return BcelTypeArrayBuilder.createClassSet((File[]) files.toArray(new File[files.size()]), origDepCL,
                    classFilter);
        } catch (ProjectBuildingException e) {
            throw new MojoExecutionException("Failed to build project for previous artifact: " + e.getMessage(), e);
        } catch (InvalidDependencyVersionException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } catch (ArtifactResolutionException e) {
            throw new MissingPreviousException("Error resolving previous version: " + e.getMessage(), e);
        } catch (ArtifactNotFoundException e) {
            getLog().warn("Impossible to find previous version");
            return new JavaType[0];
            //throw new MojoExecutionException( "Error finding previous version: " + e.getMessage(), e );
        } catch (MalformedURLException e) {
            throw new MojoExecutionException("Error creating classloader for previous version's classes", e);
        }
    }

    protected List getTransitiveDependencies(final Set previousArtifacts) throws ProjectBuildingException,
            InvalidDependencyVersionException, ArtifactResolutionException, ArtifactNotFoundException {
        final List dependencies = new ArrayList();
        for (Iterator iter = previousArtifacts.iterator(); iter.hasNext();) {
            final Artifact a = (Artifact) iter.next();
            final Artifact pomArtifact = factory.createArtifact(a.getGroupId(), a.getArtifactId(), a.getVersion(),
                    a.getScope(), "pom");
            final MavenProject pomProject = mavenProjectBuilder.buildFromRepository(pomArtifact,
                    project.getRemoteArtifactRepositories(), localRepository);
            final Set pomProjectArtifacts = pomProject.createArtifacts(factory, null, null);
            final ArtifactResolutionResult result = resolver.resolveTransitively(pomProjectArtifacts, pomArtifact,
                    localRepository, project.getRemoteArtifactRepositories(), metadataSource, null);
            dependencies.addAll(result.getArtifacts());
        }
        return dependencies;
    }

    private Artifact resolveArtifact(ArtifactSpecification artifactSpec)
            throws MojoFailureException, MojoExecutionException {
        final String groupId = artifactSpec.getGroupId();
        if (groupId == null) {
            throw new MojoFailureException("An artifacts groupId is required.");
        }
        final String artifactId = artifactSpec.getArtifactId();
        if (artifactId == null) {
            throw new MojoFailureException("An artifacts artifactId is required.");
        }
        final String version = artifactSpec.getVersion();
        if (version == null) {
            throw new MojoFailureException("An artifacts version number is required.");
        }
        final VersionRange versionRange = VersionRange.createFromVersion(version);
        String type = artifactSpec.getType();
        if (type == null) {
            type = "jar";
        }

        Artifact artifact = factory.createDependencyArtifact(groupId, artifactId, versionRange, type,
                artifactSpec.getClassifier(), Artifact.SCOPE_COMPILE);
        return artifact;
    }

    protected Set resolveArtifacts(ArtifactSpecification[] artifacts)
            throws MojoFailureException, MojoExecutionException {
        Set artifactSet = new HashSet();
        Artifact[] result = new Artifact[artifacts.length];
        for (int i = 0; i < result.length; i++) {
            artifactSet.add(resolveArtifact(artifacts[i]));
        }
        return artifactSet;
    }

    private Artifact getComparisonArtifact() throws MojoFailureException, MojoExecutionException {
        // Find the previous version JAR and resolve it, and it's dependencies
        VersionRange range;
        try {
            range = VersionRange.createFromVersionSpec(comparisonVersion);
        } catch (InvalidVersionSpecificationException e) {
            throw new MojoFailureException("Invalid comparison version: " + e.getMessage());
        }

        Artifact previousArtifact;
        try {
            previousArtifact = factory.createDependencyArtifact(project.getGroupId(), project.getArtifactId(),
                    range, project.getPackaging(), null, Artifact.SCOPE_COMPILE);

            if (!previousArtifact.getVersionRange().isSelectedVersionKnown(previousArtifact)) {
                getLog().debug("Searching for versions in range: " + previousArtifact.getVersionRange());
                List availableVersions = metadataSource.retrieveAvailableVersions(previousArtifact, localRepository,
                        project.getRemoteArtifactRepositories());
                filterSnapshots(availableVersions);
                ArtifactVersion version = range.matchVersion(availableVersions);
                if (version != null) {
                    previousArtifact.selectVersion(version.toString());
                }
            }
        } catch (OverConstrainedVersionException e1) {
            throw new MojoFailureException("Invalid comparison version: " + e1.getMessage());
        } catch (ArtifactMetadataRetrievalException e11) {
            throw new MojoExecutionException("Error determining previous version: " + e11.getMessage(), e11);
        }

        if (previousArtifact.getVersion() == null) {
            getLog().info("Unable to find a previous version of the project in the repository");
        }

        return previousArtifact;
    }

    private void filterSnapshots(List versions) {
        for (Iterator versionIterator = versions.iterator(); versionIterator.hasNext();) {
            ArtifactVersion version = (ArtifactVersion) versionIterator.next();
            if ("SNAPSHOT".equals(version.getQualifier())) {
                versionIterator.remove();
            }
        }
    }

    public static JavaType[] createClassSet(File classes, ClassLoader thirdPartyClasses, ClassFilter classFilter)
            throws MalformedURLException {
        ClassLoader classLoader = new URLClassLoader(new URL[] { classes.toURI().toURL() }, thirdPartyClasses);

        Repository repository = new ClassLoaderRepository(classLoader);

        List selected = new ArrayList();

        DirectoryScanner scanner = new DirectoryScanner();
        scanner.setBasedir(classes);
        scanner.setIncludes(new String[] { "**/*.class" });
        scanner.scan();

        String[] files = scanner.getIncludedFiles();

        for (int i = 0; i < files.length; i++) {
            File f = new File(classes, files[i]);
            JavaClass clazz = extractClass(f, repository);
            if (classFilter.isSelected(clazz)) {
                selected.add(new BcelJavaType(clazz));
                repository.storeClass(clazz);
            }
        }

        JavaType[] ret = new JavaType[selected.size()];
        selected.toArray(ret);
        return ret;
    }

    private static JavaClass extractClass(File f, Repository repository) throws CheckerException {
        InputStream is = null;
        try {
            is = new FileInputStream(f);

            ClassParser parser = new ClassParser(is, f.getName());
            JavaClass clazz = parser.parse();
            clazz.setRepository(repository);
            return clazz;
        } catch (IOException ex) {
            throw new CheckerException("Cannot read " + f, ex);
        } finally {
            IOUtil.close(is);
        }
    }

    /**
     * Create a ClassLoader, which includes the artifacts in <code>artifacts</code>,
     * but excludes the artifacts in <code>previousArtifacts</code>. The intention is,
     * that we let BCEL inspect the artifacts in the latter set, using a
     * {@link ClassLoader}, which contains the dependencies. However, the
     * {@link ClassLoader} must not contain the jar files, which are being inspected.
     * @param artifacts The artifacts, from which to build a {@link ClassLoader}.
     * @param previousArtifacts The artifacts being inspected, or null, if te
     *   returned {@link ClassLoader} should contain all the elements of
     *   <code>artifacts</code>.
     * @return A {@link ClassLoader} which may be used to inspect the classes in
     *   previousArtifacts.
     * @throws MalformedURLException Failed to convert a file to an URL.
     */
    protected static ClassLoader createClassLoader(Collection artifacts, Set previousArtifacts)
            throws MalformedURLException {
        URLClassLoader cl = null;
        if (!artifacts.isEmpty()) {
            List urls = new ArrayList(artifacts.size());
            for (Iterator i = artifacts.iterator(); i.hasNext();) {
                Artifact artifact = (Artifact) i.next();
                if (previousArtifacts == null || !previousArtifacts.contains(artifact)) {
                    urls.add(artifact.getFile().toURI().toURL());
                }
            }
            if (!urls.isEmpty()) {
                cl = new URLClassLoader((URL[]) urls.toArray(EMPTY_URL_ARRAY));
            }
        }
        return cl;
    }

    protected static Severity convertSeverity(String minSeverity) {
        Severity s;
        if ("info".equals(minSeverity)) {
            s = Severity.INFO;
        } else if ("warning".equals(minSeverity)) {
            s = Severity.WARNING;
        } else if ("error".equals(minSeverity)) {
            s = Severity.ERROR;
        } else {
            s = null;
        }
        return s;
    }

    protected boolean canGenerate() throws MojoFailureException, MojoExecutionException {
        boolean classes = false;

        if (classesDirectory.exists()) {
            classes = true;
        } else {
            getLog().debug("Classes directory not found: " + classesDirectory);
        }

        if (!classes) {
            getLog().info("Not generating Clirr report as there are no classes generated by the project");
            return false;
        }

        if (comparisonArtifacts == null || comparisonArtifacts.length == 0) {
            Artifact previousArtifact = getComparisonArtifact();
            if (previousArtifact.getVersion() == null) {
                getLog().info(
                        "Not generating Clirr report as there is no previous version of the library to compare against");
                return false;
            }
        }

        return true;
    }

    /**
     * Calls {@link Checker#reportDiffs(JavaType[], JavaType[])} and take care of BCEL errors.
     *
     * @param checker not null
     * @param origClasses not null
     * @param currentClasses not null
     * @see Checker#reportDiffs(JavaType[], JavaType[])
     */
    private void reportDiffs(Checker checker, JavaType[] origClasses, JavaType[] currentClasses) {
        try {
            checker.reportDiffs(origClasses, currentClasses);
        } catch (CheckerException e) {
            getLog().error(e.getMessage());

            // remove class with errors
            JavaType[] origClasses2 = new JavaType[origClasses.length - 1];
            int j = 0;
            for (int i = 0; i < origClasses.length; i++) {
                if (!e.getMessage().endsWith(origClasses[i].getName())) {
                    origClasses2[j++] = origClasses[i];
                }
            }
            JavaType[] currentClasses2 = new JavaType[currentClasses.length - 1];
            j = 0;
            for (int i = 0; i < currentClasses.length; i++) {
                if (!e.getMessage().endsWith(currentClasses[i].getName())) {
                    currentClasses2[j++] = currentClasses[i];
                }
            }

            reportDiffs(checker, origClasses2, currentClasses2);
        }
    }
}