org.apache.felix.bundleplugin.baseline.AbstractBaselinePlugin.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.felix.bundleplugin.baseline.AbstractBaselinePlugin.java

Source

/*
 * 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.felix.bundleplugin.baseline;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

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.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
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.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.StringUtils;

import aQute.bnd.differ.Baseline;
import aQute.bnd.differ.Baseline.Info;
import aQute.bnd.differ.DiffPluginImpl;
import aQute.bnd.osgi.Instructions;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Processor;
import aQute.bnd.service.diff.Delta;
import aQute.bnd.service.diff.Diff;
import aQute.bnd.version.Version;
import aQute.service.reporter.Reporter;

/**
 * Abstract BND Baseline check between two bundles.
 */
abstract class AbstractBaselinePlugin extends AbstractMojo {

    /**
     * Flag to easily skip execution.
     */
    @Parameter(property = "baseline.skip", defaultValue = "false")
    protected boolean skip;

    /**
     * Whether to fail on errors.
     */
    @Parameter(property = "baseline.failOnError", defaultValue = "true")
    protected boolean failOnError;

    /**
     * Whether to fail on warnings.
     */
    @Parameter(property = "baseline.failOnWarning", defaultValue = "false")
    protected boolean failOnWarning;

    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    protected MavenProject project;

    @Parameter(defaultValue = "${session}", readonly = true, required = true)
    protected MavenSession session;

    @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true)
    private File buildDirectory;

    @Parameter(defaultValue = "${project.build.finalName}", readonly = true, required = true)
    private String finalName;

    @Component
    protected ArtifactResolver resolver;

    @Component
    protected ArtifactFactory factory;

    @Component
    private ArtifactMetadataSource metadataSource;

    /**
     * Group id to compare the current code against.
     */
    @Parameter(defaultValue = "${project.groupId}", property = "comparisonGroupId")
    protected String comparisonGroupId;

    /**
     * Artifact to compare the current code against.
     */
    @Parameter(defaultValue = "${project.artifactId}", property = "comparisonArtifactId")
    protected String comparisonArtifactId;

    /**
     * Version to compare the current code against.
     */
    @Parameter(defaultValue = "(,${project.version})", property = "comparisonVersion")
    protected String comparisonVersion;

    /**
     * Artifact to compare the current code against.
     */
    @Parameter(defaultValue = "${project.packaging}", property = "comparisonPackaging")
    protected String comparisonPackaging;

    /**
     * Classifier for the artifact to compare the current code against.
     */
    @Parameter(property = "comparisonClassifier")
    protected String comparisonClassifier;

    /**
     * A list of packages filter, if empty the whole bundle will be traversed. Values are specified in OSGi package
     * instructions notation, e.g. <code>!org.apache.felix.bundleplugin</code>.
     */
    @Parameter
    private String[] filters;

    /**
     * Project types which this plugin supports.
     */
    @Parameter
    protected List<String> supportedProjectTypes = Arrays.asList(new String[] { "jar", "bundle" });

    public final void execute() throws MojoExecutionException, MojoFailureException {
        this.execute(null);
    }

    protected void execute(Object context) throws MojoExecutionException, MojoFailureException {
        if (skip) {
            getLog().info("Skipping Baseline execution");
            return;
        }

        if (!supportedProjectTypes.contains(project.getArtifact().getType())) {
            getLog().info("Skipping Baseline (project type " + project.getArtifact().getType() + " not supported)");
            return;
        }

        // get the bundles that have to be compared

        final Jar currentBundle = getCurrentBundle();
        if (currentBundle == null) {
            getLog().info("Not generating Baseline report as there is no bundle generated by the project");
            return;
        }

        final Artifact previousArtifact = getPreviousArtifact();

        final Jar previousBundle;
        if (previousArtifact != null) {
            previousBundle = openJar(previousArtifact.getFile());
        } else {
            previousBundle = null;
        }

        if (previousBundle == null) {
            getLog().info(
                    "Not generating Baseline report as there is no previous version of the library to compare against");
            return;
        }

        // preparing the filters

        final Instructions packageFilters;
        if (filters == null || filters.length == 0) {
            packageFilters = new Instructions();
        } else {
            packageFilters = new Instructions(Arrays.asList(filters));
        }

        String generationDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'").format(new Date());
        final Reporter reporter = new Processor();

        final Info[] infos;
        try {
            final Set<Info> infoSet = new Baseline(reporter, new DiffPluginImpl()).baseline(currentBundle,
                    previousBundle, packageFilters);
            infos = infoSet.toArray(new Info[infoSet.size()]);
            Arrays.sort(infos, new InfoComparator());
        } catch (final Exception e) {
            throw new MojoExecutionException("Impossible to calculate the baseline", e);
        } finally {
            closeJars(currentBundle, previousBundle);
        }

        try {
            // go!
            context = this.init(context);
            startBaseline(context, generationDate, project.getArtifactId(), project.getVersion(),
                    previousArtifact.getVersion());

            for (final Info info : infos) {
                DiffMessage diffMessage = null;

                if (info.suggestedVersion != null) {
                    if (info.newerVersion.compareTo(info.suggestedVersion) > 0) {
                        diffMessage = new DiffMessage("Excessive version increase", DiffMessage.Type.warning);
                        reporter.warning("%s: %s; detected %s, suggested %s", info.packageName, diffMessage,
                                info.newerVersion, info.suggestedVersion);
                    } else if (info.newerVersion.compareTo(info.suggestedVersion) < 0) {
                        diffMessage = new DiffMessage("Version increase required", DiffMessage.Type.error);
                        reporter.error("%s: %s; detected %s, suggested %s", info.packageName, diffMessage,
                                info.newerVersion, info.suggestedVersion);
                    }
                }

                switch (info.packageDiff.getDelta()) {
                case UNCHANGED:
                    if (info.newerVersion.compareTo(info.suggestedVersion) != 0) {
                        diffMessage = new DiffMessage("Version has been increased but analysis detected no changes",
                                DiffMessage.Type.warning);
                        reporter.warning("%s: %s; detected %s, suggested %s", info.packageName, diffMessage,
                                info.newerVersion, info.suggestedVersion);
                    }
                    break;

                case REMOVED:
                    diffMessage = new DiffMessage("Package removed", DiffMessage.Type.info);
                    reporter.trace("%s: %s ", info.packageName, diffMessage);
                    break;

                case CHANGED:
                case MICRO:
                case MINOR:
                case MAJOR:
                case ADDED:
                default:
                    // ok
                    break;
                }

                startPackage(context, info.mismatch, info.packageName, getShortDelta(info.packageDiff.getDelta()),
                        StringUtils.lowerCase(String.valueOf(info.packageDiff.getDelta())), info.newerVersion,
                        info.olderVersion, info.suggestedVersion, diffMessage, info.attributes);

                if (Delta.REMOVED != info.packageDiff.getDelta()) {
                    doPackageDiff(context, info.packageDiff);
                }

                endPackage(context);
            }

            endBaseline(context);
        } finally {
            this.close(context);
        }

        // check if it has to fail if some error has been detected

        boolean fail = false;

        if (!reporter.isOk()) {
            for (String errorMessage : reporter.getErrors()) {
                getLog().error(errorMessage);
            }

            if (failOnError) {
                fail = true;
            }
        }

        // check if it has to fail if some warning has been detected

        if (!reporter.getWarnings().isEmpty()) {
            for (String warningMessage : reporter.getWarnings()) {
                getLog().warn(warningMessage);
            }

            if (failOnWarning) {
                fail = true;
            }
        }

        getLog().info(String.format("Baseline analysis complete, %s error(s), %s warning(s)",
                reporter.getErrors().size(), reporter.getWarnings().size()));

        if (fail) {
            throw new MojoFailureException("Baseline failed, see generated report");
        }
    }

    private void doPackageDiff(Object context, Diff diff) {
        int depth = 1;

        for (Diff curDiff : diff.getChildren()) {
            if (Delta.UNCHANGED != curDiff.getDelta()) {
                doDiff(context, curDiff, depth);
            }
        }
    }

    private void doDiff(Object context, Diff diff, int depth) {
        String type = StringUtils.lowerCase(String.valueOf(diff.getType()));
        String shortDelta = getShortDelta(diff.getDelta());
        String delta = StringUtils.lowerCase(String.valueOf(diff.getDelta()));
        String name = diff.getName();

        startDiff(context, depth, type, name, delta, shortDelta);

        for (Diff curDiff : diff.getChildren()) {
            if (Delta.UNCHANGED != curDiff.getDelta()) {
                doDiff(context, curDiff, depth + 1);
            }
        }

        endDiff(context, depth);
    }

    // extensions APIs

    protected abstract Object init(final Object initialContext);

    protected abstract void close(final Object context);

    protected abstract void startBaseline(final Object context, String generationDate, String bundleName,
            String currentVersion, String previousVersion);

    protected abstract void startPackage(final Object context, boolean mismatch, String name, String shortDelta,
            String delta, Version newerVersion, Version olderVersion, Version suggestedVersion,
            DiffMessage diffMessage, Map<String, String> attributes);

    protected abstract void startDiff(final Object context, int depth, String type, String name, String delta,
            String shortDelta);

    protected abstract void endDiff(final Object context, int depth);

    protected abstract void endPackage(final Object context);

    protected abstract void endBaseline(final Object context);

    // internals

    private Jar getCurrentBundle() throws MojoExecutionException {
        /*
         * Resolving the aQute.bnd.osgi.Jar via the produced artifact rather than what is produced in the target/classes
         * directory would make the Mojo working also in projects where the bundle-plugin is used just to generate the
         * manifest file and the final jar is assembled via the jar-plugin
         */
        File currentBundle = new File(buildDirectory, getBundleName());
        if (!currentBundle.exists()) {
            getLog().debug("Produced bundle not found: " + currentBundle);
            return null;
        }

        return openJar(currentBundle);
    }

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

        final Artifact previousArtifact;
        try {
            previousArtifact = factory.createDependencyArtifact(comparisonGroupId, comparisonArtifactId, range,
                    comparisonPackaging, comparisonClassifier, Artifact.SCOPE_COMPILE);

            if (!previousArtifact.getVersionRange().isSelectedVersionKnown(previousArtifact)) {
                getLog().debug("Searching for versions in range: " + previousArtifact.getVersionRange());
                @SuppressWarnings("unchecked")
                // type is konwn
                List<ArtifactVersion> availableVersions = metadataSource.retrieveAvailableVersions(previousArtifact,
                        session.getLocalRepository(), project.getRemoteArtifactRepositories());
                filterSnapshots(availableVersions);
                ArtifactVersion version = range.matchVersion(availableVersions);
                if (version != null) {
                    previousArtifact.selectVersion(version.toString());
                }
            }
        } catch (OverConstrainedVersionException ocve) {
            throw new MojoFailureException("Invalid comparison version: " + ocve.getMessage());
        } catch (ArtifactMetadataRetrievalException amre) {
            throw new MojoExecutionException("Error determining previous version: " + amre.getMessage(), amre);
        }

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

        try {
            resolver.resolve(previousArtifact, project.getRemoteArtifactRepositories(),
                    session.getLocalRepository());
        } catch (ArtifactResolutionException are) {
            throw new MojoExecutionException(
                    "Artifact " + previousArtifact + " cannot be resolved : " + are.getMessage(), are);
        } catch (ArtifactNotFoundException anfe) {
            throw new MojoExecutionException(
                    "Artifact " + previousArtifact + " does not exist on local/remote repositories", anfe);
        }

        return previousArtifact;
    }

    private void filterSnapshots(List<ArtifactVersion> versions) {
        for (Iterator<ArtifactVersion> versionIterator = versions.iterator(); versionIterator.hasNext();) {
            ArtifactVersion version = versionIterator.next();
            if (version.getQualifier() != null && version.getQualifier().endsWith("SNAPSHOT")) {
                versionIterator.remove();
            }
        }
    }

    private static Jar openJar(final File file) throws MojoExecutionException {
        try {
            return new Jar(file);
        } catch (final IOException e) {
            throw new MojoExecutionException("An error occurred while opening JAR directory: " + file, e);
        }
    }

    private static void closeJars(final Jar... jars) {
        for (Jar jar : jars) {
            jar.close();
        }
    }

    private String getBundleName() {
        String extension;
        try {
            extension = project.getArtifact().getArtifactHandler().getExtension();
        } catch (Throwable e) {
            extension = project.getArtifact().getType();
        }

        if (StringUtils.isEmpty(extension) || "bundle".equals(extension) || "pom".equals(extension)) {
            extension = "jar"; // just in case maven gets confused
        }

        String classifier = this.comparisonClassifier != null ? this.comparisonClassifier
                : project.getArtifact().getClassifier();
        if (null != classifier && classifier.trim().length() > 0) {
            return finalName + '-' + classifier + '.' + extension;
        }

        return finalName + '.' + extension;
    }

    private static String getShortDelta(Delta delta) {
        switch (delta) {
        case ADDED:
            return "+";

        case CHANGED:
            return "~";

        case MAJOR:
            return ">";

        case MICRO:
            return "0xB5";

        case MINOR:
            return "<";

        case REMOVED:
            return "-";

        case UNCHANGED:
            return " ";

        default:
            String deltaString = delta.toString();
            return String.valueOf(deltaString.charAt(0));
        }
    }
}