| /* |
| * 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 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; |
| |
| 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; |
| |
| /** |
| * 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().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 ) ); |
| } |
| } |
| } |