blob: 8ef49b5ea81f9de367a88f120c989cce3930b41b [file] [log] [blame]
/*
* 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.FileWriter;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.util.Map;
import java.util.Map.Entry;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
import org.codehaus.plexus.util.xml.XMLWriter;
import org.sonatype.plexus.build.incremental.BuildContext;
import aQute.bnd.differ.Baseline.Info;
import aQute.bnd.service.diff.Delta;
import aQute.bnd.service.diff.Diff;
import aQute.bnd.service.diff.Type;
import aQute.bnd.version.Version;
/**
* BND Baseline check between two bundles.
*
* @goal baseline
* @phase verify
* @requiresDependencyResolution test
* @threadSafe true
* @since 2.4.1
*/
public final class BaselinePlugin
extends AbstractBaselinePlugin
{
private static final String TABLE_PATTERN = "%s %-50s %-10s %-10s %-10s %-10s %-10s";
/**
* An XML output file to render to <code>${project.build.directory}/baseline.xml</code>.
*
* @parameter expression="${project.build.directory}/baseline.xml"
*/
private File xmlOutputFile;
/**
* Whether to log the results to the console or not, true by default.
*
* @parameter expression="${logResults}" default-value="true"
*/
private boolean logResults;
/**
* @component
*/
private BuildContext buildContext;
private static final class Context {
public FileWriter writer;
public XMLWriter xmlWriter;
}
@Override
protected Object init(final Object noContext)
{
if ( xmlOutputFile != null )
{
xmlOutputFile.getParentFile().mkdirs();
try
{
final Context ctx = new Context();
ctx.writer = new FileWriter( xmlOutputFile );
ctx.xmlWriter = new PrettyPrintXMLWriter( ctx.writer );
return ctx;
}
catch ( IOException e )
{
getLog().warn( "No XML report will be produced, cannot write data to " + xmlOutputFile, e );
}
}
return null;
}
@Override
protected void close(final Object writer)
{
if ( writer != null )
{
try {
((Context)writer).writer.close();
}
catch (IOException e)
{
// ignore
}
}
}
@Override
protected void startBaseline( Object context,
String generationDate,
String bundleName,
String currentVersion,
String previousVersion )
{
final XMLWriter xmlWriter = context == null ? null : ((Context)context).xmlWriter;
if ( isLoggingResults() )
{
log( "Baseline Report - Generated by Apache Felix Maven Bundle Plugin on %s based on Bnd - see http://www.aqute.biz/Bnd/Bnd",
generationDate );
log( "Comparing bundle %s version %s to version %s", bundleName, currentVersion, previousVersion );
log( "" );
log( TABLE_PATTERN,
" ",
"PACKAGE_NAME",
"DELTA",
"CUR_VER",
"BASE_VER",
"REC_VER",
"WARNINGS",
"ATTRIBUTES" );
log( TABLE_PATTERN,
"=",
"==================================================",
"==========",
"==========",
"==========",
"==========",
"==========",
"==========" );
}
if ( xmlWriter != null )
{
xmlWriter.startElement( "baseline" );
xmlWriter.addAttribute( "version", "1.0.0" );
xmlWriter.addAttribute( "vendor", "The Apache Software Foundation" );
xmlWriter.addAttribute( "vendorURL", "http://www.apache.org/" );
xmlWriter.addAttribute( "generator", "Apache Felix Maven Bundle Plugin" );
xmlWriter.addAttribute( "generatorURL", "http://felix.apache.org/site/apache-felix-maven-bundle-plugin-bnd.html" );
xmlWriter.addAttribute( "analyzer", "Bnd" );
xmlWriter.addAttribute( "analyzerURL", "http://www.aqute.biz/Bnd/Bnd" );
xmlWriter.addAttribute( "generatedOn", generationDate );
xmlWriter.addAttribute( "bundleName", bundleName );
xmlWriter.addAttribute( "currentVersion", currentVersion );
xmlWriter.addAttribute( "previousVersion", previousVersion );
}
}
@Override
protected void startPackage( Object context,
boolean mismatch,
String name,
String shortDelta,
String delta,
Version newerVersion,
Version olderVersion,
Version suggestedVersion,
DiffMessage diffMessage,
Map<String,String> attributes )
{
final XMLWriter xmlWriter = context == null ? null : ((Context)context).xmlWriter;
if ( isLoggingResults() )
{
log( TABLE_PATTERN,
mismatch ? '*' : shortDelta,
name,
delta,
newerVersion,
olderVersion,
suggestedVersion,
diffMessage != null ? diffMessage : '-',
attributes );
}
if ( xmlWriter != null )
{
xmlWriter.startElement( "package" );
xmlWriter.addAttribute( "name", name );
xmlWriter.addAttribute( "delta", delta );
simpleElement( xmlWriter, "mismatch", String.valueOf( mismatch ) );
simpleElement( xmlWriter, "newerVersion", newerVersion.toString() );
simpleElement( xmlWriter, "olderVersion", olderVersion.toString() );
if ( suggestedVersion != null )
{
simpleElement( xmlWriter, "suggestedVersion", suggestedVersion.toString() );
}
if ( diffMessage != null )
{
simpleElement( xmlWriter, diffMessage.getType().name(), diffMessage.getMessage() );
}
xmlWriter.startElement( "attributes" );
if (attributes != null)
{
for (Entry<String, String> attribute : attributes.entrySet())
{
String attributeName = attribute.getKey();
if (':' == attributeName.charAt(attributeName.length() - 1))
{
attributeName = attributeName.substring(0, attributeName.length() - 1);
}
String attributeValue = attribute.getValue();
xmlWriter.startElement(attributeName);
xmlWriter.writeText(attributeValue);
xmlWriter.endElement();
}
}
xmlWriter.endElement();
}
}
@Override
protected void startDiff( Object context, int depth, String type, String name, String delta, String shortDelta )
{
final XMLWriter xmlWriter = context == null ? null : ((Context)context).xmlWriter;
if ( isLoggingResults() )
{
log( "%-" + (depth * 4) + "s %s %s %s",
"",
shortDelta,
type,
name );
}
if ( xmlWriter != null )
{
xmlWriter.startElement( type );
xmlWriter.addAttribute( "name", name );
xmlWriter.addAttribute( "delta", delta );
}
}
@Override
protected void endDiff( Object context, int depth )
{
final XMLWriter xmlWriter = context == null ? null : ((Context)context).xmlWriter;
if ( xmlWriter != null )
{
xmlWriter.endElement();
}
}
@Override
protected void endPackage(Object context)
{
final XMLWriter xmlWriter = context == null ? null : ((Context)context).xmlWriter;
if ( isLoggingResults() )
{
log( "-----------------------------------------------------------------------------------------------------------" );
}
if ( xmlWriter != null )
{
xmlWriter.endElement();
}
}
@Override
protected void endBaseline(Object context)
{
final XMLWriter xmlWriter = context == null ? null : ((Context)context).xmlWriter;
if ( xmlWriter != null )
{
xmlWriter.endElement();
}
}
private boolean isLoggingResults()
{
return logResults && getLog().isInfoEnabled();
}
private void log( String format, Object...args )
{
getLog().info( String.format( format, args ) );
}
private void simpleElement( XMLWriter xmlWriter, String name, String value )
{
xmlWriter.startElement( name );
xmlWriter.writeText( value );
xmlWriter.endElement();
}
@Override
protected void reportErrors(final Info[] infos)
throws IOException
{
for ( final Info info : infos )
{
// clear previous messages
final FileMarker packageMarker = findMarkerLocationForPackage(info.packageName);
if ( info.suggestedVersion != null )
{
if ( info.newerVersion.compareTo( info.suggestedVersion ) > 0 )
{
final String msg = "Excessive version increase: detected " + info.newerVersion + ", suggested " + info.suggestedVersion;
this.buildContext.addMessage(packageMarker.file, packageMarker.line, packageMarker.col, msg, BuildContext.SEVERITY_WARNING, null);
}
else if ( info.newerVersion.compareTo( info.suggestedVersion ) < 0 )
{
final String msg = "Version increase required: detected " + info.newerVersion + ", suggested " + info.suggestedVersion;
this.buildContext.addMessage(packageMarker.file, packageMarker.line, packageMarker.col, msg, BuildContext.SEVERITY_ERROR, null);
}
}
if ( info.packageDiff.getDelta() == Delta.UNCHANGED && info.newerVersion.compareTo( info.suggestedVersion ) != 0 )
{
final String msg = "Version has been increased but analysis detected no changes: detected " + info.newerVersion + ", suggested " + info.suggestedVersion;
this.buildContext.addMessage(packageMarker.file, packageMarker.line, packageMarker.col, msg, BuildContext.SEVERITY_WARNING, null);
}
generateStructuralChangeMarkers(info);
}
}
private File getPackageFile(final String packageName)
{
final String sourceDir = this.project.getBuild().getSourceDirectory();
final File packageDir = new File((sourceDir + '/' + packageName.replace('.', '/')).replace('/', File.separatorChar));
return packageDir;
}
private File getSourceFile(final String className)
{
final String sourceDir = this.project.getBuild().getSourceDirectory();
final File packageDir = new File((sourceDir + '/' + className.replace('.', '/') + ".java").replace('/', File.separatorChar));
return packageDir;
}
private static final class FileMarker
{
public File file;
public int line;
public int col;
}
/**
* Find marker location for a package
* and clear old markers.
*/
private FileMarker findMarkerLocationForPackage(final String packageName)
throws IOException
{
final File packageDir = getPackageFile(packageName);
this.buildContext.removeMessages(packageDir);
final File packageInfoFile = new File(packageDir, "packageinfo");
this.buildContext.removeMessages(packageInfoFile);
final File packageInfoJavaFile = new File(packageDir, "package-info.java");
this.buildContext.removeMessages(packageInfoJavaFile);
// package-info.java has precedence
if ( packageInfoJavaFile != null && packageInfoJavaFile.exists() )
{
return findVersionLocationInPackageInfoJava(packageInfoJavaFile);
}
// packageinfo next
if ( packageInfoFile != null && packageInfoFile.exists() )
{
return findVersionLocationInPackageInfo(packageInfoFile);
}
final FileMarker defaultMarker = new FileMarker();
defaultMarker.file = packageDir;
return defaultMarker;
}
private FileMarker findVersionLocationInPackageInfo(final File file)
throws IOException
{
final FileMarker marker = new FileMarker();
marker.file = file;
final String contents = FileUtils.fileRead(file);
final LineNumberReader reader = new LineNumberReader(new StringReader(contents));
try
{
String line;
while ( (line = reader.readLine()) != null )
{
final int pos = line.indexOf("version ");
if ( pos > -1 )
{
marker.col = pos;
marker.line = reader.getLineNumber();
break;
}
}
}
finally
{
IOUtil.close(reader);
}
return marker;
}
private FileMarker findVersionLocationInPackageInfoJava(final File file)
throws IOException
{
final FileMarker marker = new FileMarker();
marker.file = file;
final String contents = FileUtils.fileRead(file);
final LineNumberReader reader = new LineNumberReader(new StringReader(contents));
try
{
String line;
while ( (line = reader.readLine()) != null )
{
final int pos = line.indexOf("@Version");
if ( pos > -1 )
{
marker.col = pos;
marker.line = reader.getLineNumber();
break;
}
}
}
finally
{
IOUtil.close(reader);
}
return marker;
}
private void generateStructuralChangeMarkers(final Info baselineInfo)
{
final Delta packageDelta = baselineInfo.packageDiff.getDelta();
// Iterate into the package member diffs
for (final Diff pkgMemberDiff : baselineInfo.packageDiff.getChildren())
{
// only deal with interfaces and classes
if ( pkgMemberDiff.getType() != Type.INTERFACE && pkgMemberDiff.getType() != Type.CLASS )
{
continue;
}
// clear old marker
final String className = pkgMemberDiff.getName();
final File classFile = this.getSourceFile(className);
this.buildContext.removeMessages(classFile);
// Skip deltas that have lesser significance than the overall package delta
if (pkgMemberDiff.getDelta().ordinal() < packageDelta.ordinal())
{
continue;
}
if (Delta.ADDED == pkgMemberDiff.getDelta())
{
// TODO
}
else if (Delta.REMOVED == pkgMemberDiff.getDelta())
{
// TODO
}
else
{
// Iterate into the class member diffs
for (final Diff classMemberDiff : pkgMemberDiff.getChildren())
{
// Skip deltas that have lesser significance than the overall package delta (again)
if (classMemberDiff.getDelta().ordinal() < packageDelta.ordinal())
continue;
if (Delta.ADDED == classMemberDiff.getDelta())
{
addAddedMarker(classFile, classMemberDiff);
}
else if (Delta.REMOVED == classMemberDiff.getDelta())
{
addRemovedMarker(classFile, classMemberDiff);
}
else if (Delta.CHANGED == classMemberDiff.getDelta())
{
addChangedMarker(classFile, classMemberDiff);
}
}
}
}
}
private void addAddedMarker(final File classFile, final Diff diff)
{
this.buildContext.addMessage(classFile, 0, 0, diff.getType() + " " + diff.getName() + " has been added.", BuildContext.SEVERITY_ERROR, null);
}
private void addRemovedMarker(final File classFile, final Diff diff)
{
this.buildContext.addMessage(classFile, 0, 0, diff.getType() + " " + diff.getName() + " has been removed.", BuildContext.SEVERITY_ERROR, null);
}
private void addChangedMarker(final File classFile, final Diff diff)
{
this.buildContext.addMessage(classFile, 0, 0, diff.getType() + " " + diff.getName() + " has been changed.", BuildContext.SEVERITY_ERROR, null);
}
}