package aQute.bnd.maven.plugin;
/*
* Copyright (c) Paremus and others (2015, 2016). All Rights Reserved.
*
* 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 static aQute.lib.io.IO.getFile;
import java.io.File;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Settings;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonatype.plexus.build.incremental.BuildContext;
import aQute.bnd.build.Project;
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.FileResource;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Processor;
import aQute.bnd.osgi.Resource;
import aQute.bnd.version.MavenVersion;
import aQute.bnd.version.Version;
import aQute.lib.io.IO;
import aQute.lib.strings.Strings;
import aQute.lib.utf8properties.UTF8Properties;
import aQute.service.reporter.Report.Location;
@Mojo(name = "bnd-process", defaultPhase = LifecyclePhase.PROCESS_CLASSES, requiresDependencyResolution = ResolutionScope.COMPILE)
public class BndMavenPlugin extends AbstractMojo {
private static final Logger logger = LoggerFactory
.getLogger(BndMavenPlugin.class);
private static final String MANIFEST_LAST_MODIFIED = "aQute.bnd.maven.plugin.BndMavenPlugin.manifestLastModified";
private static final String MARKED_FILES = "aQute.bnd.maven.plugin.BndMavenPlugin.markedFiles";
private static final String PACKAGING_POM = "pom";
private static final String TSTAMP = "${tstamp}";
@Parameter(defaultValue = "${project.build.directory}", readonly = true)
private File targetDir;
@Parameter(defaultValue = "${project.build.sourceDirectory}", readonly = true)
private File sourceDir;
@Parameter(defaultValue = "${project.build.resources}", readonly = true)
private List<org.apache.maven.model.Resource> resources;
@Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true)
private File classesDir;
@Parameter(defaultValue = "${project.build.outputDirectory}/META-INF/MANIFEST.MF", readonly = true)
private File manifestPath;
@Parameter(defaultValue = "${project}", required = true, readonly = true)
private MavenProject project;
@Parameter(defaultValue = "${settings}", readonly = true)
private Settings settings;
@Parameter(defaultValue = "${mojoExecution}", readonly = true)
private MojoExecution mojoExecution;
@Parameter(property = "bnd.skip", defaultValue = "false")
private boolean skip;
/**
* File path to a bnd file containing bnd instructions for this project.
* Defaults to {@code bnd.bnd}. The file path can be an absolute or relative
* to the project directory.
* <p>
* The bnd instructions for this project are merged with the bnd
* instructions, if any, for the parent project.
*/
@Parameter(defaultValue = Project.BNDFILE)
// This is not used and is for doc only; see loadProjectProperties
@SuppressWarnings("unused")
private String bndfile;
/**
* Bnd instructions for this project specified directly in the pom file.
* This is generally be done using a {@code <![CDATA[]]>} section. If the
* projects has a {@link #bndfile bnd file}, then this configuration element
* is ignored.
* <p>
* The bnd instructions for this project are merged with the bnd
* instructions, if any, for the parent project.
*/
@Parameter
// This is not used and is for doc only; see loadProjectProperties
@SuppressWarnings("unused")
private String bnd;
@Component
private BuildContext buildContext;
private File propertiesFile;
public void execute() throws MojoExecutionException {
if (skip) {
logger.debug("skip project as configured");
return;
}
// Exit without generating anything if this is a pom-packaging project.
// Probably it's just a parent project.
if (PACKAGING_POM.equals(project.getPackaging())) {
logger.info("skip project with packaging=pom");
return;
}
Properties beanProperties = new BeanProperties();
beanProperties.put("project", project);
beanProperties.put("settings", settings);
Properties mavenProperties = new Properties(beanProperties);
mavenProperties.putAll(project.getProperties());
try (Builder builder = new Builder(new Processor(mavenProperties, false))) {
builder.setTrace(logger.isDebugEnabled());
builder.setBase(project.getBasedir());
propertiesFile = loadProjectProperties(builder, project);
builder.setProperty("project.output", targetDir.getCanonicalPath());
// If no bundle to be built, we have nothing to do
if (Builder.isTrue(builder.getProperty(Constants.NOBUNDLES))) {
logger.debug(Constants.NOBUNDLES + ": true");
return;
}
// Reject sub-bundle projects
List<Builder> subs = builder.getSubBuilders();
if ((subs.size() != 1) || !builder.equals(subs.get(0))) {
throw new MojoExecutionException("Sub-bundles not permitted in a maven build");
}
// Reject wab projects
if (builder.getProperty(Constants.WAB) != null) {
throw new MojoExecutionException(Constants.WAB + " not supported in a maven build");
}
if (builder.getProperty(Constants.WABLIB) != null) {
throw new MojoExecutionException(Constants.WABLIB + " not supported in a maven build");
}
// Include local project packages automatically
if (classesDir.isDirectory()) {
Jar classesDirJar = new Jar(project.getName(), classesDir);
classesDirJar.setManifest(new Manifest());
builder.setJar(classesDirJar);
}
// Compute bnd classpath
Set<Artifact> artifacts = project.getArtifacts();
List<Object> buildpath = new ArrayList<Object>(artifacts.size());
for (Artifact artifact : artifacts) {
File cpe = artifact.getFile().getCanonicalFile();
if (!cpe.exists()) {
logger.debug("dependency {} does not exist", cpe);
continue;
}
if (cpe.isDirectory()) {
Jar cpeJar = new Jar(cpe);
builder.addClose(cpeJar);
builder.updateModified(cpeJar.lastModified(), cpe.getPath());
buildpath.add(cpeJar);
} else {
if (!artifact.getType().equals("jar")) {
/*
* Check if it is a valid zip file. We don't create a
* Jar object here because we want to avoid the cost of
* creating the Jar object if we decide not to build.
*/
try (ZipFile zip = new ZipFile(cpe)) {
zip.entries();
} catch (ZipException e) {
logger.debug("dependency {} is not a zip", cpe);
continue;
}
}
builder.updateModified(cpe.lastModified(), cpe.getPath());
buildpath.add(cpe);
}
}
builder.setProperty("project.buildpath", Strings.join(File.pathSeparator, buildpath));
logger.debug("builder classpath: {}", builder.getProperty("project.buildpath"));
// Compute bnd sourcepath
boolean delta = !buildContext.isIncremental() || manifestOutOfDate();
List<File> sourcepath = new ArrayList<File>();
if (sourceDir.exists()) {
sourcepath.add(sourceDir.getCanonicalFile());
delta |= buildContext.hasDelta(sourceDir);
}
for (org.apache.maven.model.Resource resource : resources) {
File resourceDir = new File(resource.getDirectory());
if (resourceDir.exists()) {
sourcepath.add(resourceDir.getCanonicalFile());
delta |= buildContext.hasDelta(resourceDir);
}
}
builder.setProperty("project.sourcepath", Strings.join(File.pathSeparator, sourcepath));
logger.debug("builder sourcepath: {}", builder.getProperty("project.sourcepath"));
// Set Bundle-SymbolicName
if (builder.getProperty(Constants.BUNDLE_SYMBOLICNAME) == null) {
builder.setProperty(Constants.BUNDLE_SYMBOLICNAME, project.getArtifactId());
}
// Set Bundle-Name
if (builder.getProperty(Constants.BUNDLE_NAME) == null) {
builder.setProperty(Constants.BUNDLE_NAME, project.getName());
}
// Set Bundle-Version
if (builder.getProperty(Constants.BUNDLE_VERSION) == null) {
Version version = MavenVersion.parseString(project.getVersion()).getOSGiVersion();
builder.setProperty(Constants.BUNDLE_VERSION, version.toString());
if (builder.getProperty(Constants.SNAPSHOT) == null) {
builder.setProperty(Constants.SNAPSHOT, TSTAMP);
}
}
logger.debug("builder properties: {}", builder.getProperties());
logger.debug("builder delta: {}", delta);
if (delta || (builder.getJar() == null) || (builder.lastModified() > builder.getJar().lastModified())) {
// Set builder paths
builder.setClasspath(buildpath);
builder.setSourcepath(sourcepath.toArray(new File[0]));
// Build bnd Jar (in memory)
Jar bndJar = builder.build();
// Expand Jar into target/classes
expandJar(bndJar, classesDir);
} else {
logger.debug("No build");
}
// Finally, report
reportErrorsAndWarnings(builder);
} catch (MojoExecutionException e) {
throw e;
} catch (Exception e) {
throw new MojoExecutionException("bnd error: " + e.getMessage(), e);
}
}
private File loadProjectProperties(Builder builder, MavenProject project) throws Exception {
// Load parent project properties first
MavenProject parentProject = project.getParent();
if (parentProject != null) {
loadProjectProperties(builder, parentProject);
}
// Merge in current project properties
Xpp3Dom configuration = project.getGoalConfiguration("biz.aQute.bnd", "bnd-maven-plugin",
mojoExecution.getExecutionId(), mojoExecution.getGoal());
File baseDir = project.getBasedir();
if (baseDir != null) { // file system based pom
File pomFile = project.getFile();
builder.updateModified(pomFile.lastModified(), "POM: " + pomFile);
// check for bnd file
String bndFileName = Project.BNDFILE;
if (configuration != null) {
Xpp3Dom bndfileElement = configuration.getChild("bndfile");
if (bndfileElement != null) {
bndFileName = bndfileElement.getValue();
}
}
File bndFile = IO.getFile(baseDir, bndFileName);
if (bndFile.isFile()) {
logger.debug("loading bnd properties from file: {}", bndFile);
// we use setProperties to handle -include
builder.setProperties(bndFile.getParentFile(), builder.loadProperties(bndFile));
return bndFile;
}
// no bnd file found, so we fall through
}
// check for bnd-in-pom configuration
if (configuration != null) {
Xpp3Dom bndElement = configuration.getChild("bnd");
if (bndElement != null) {
logger.debug("loading bnd properties from bnd element in pom: {}", project);
UTF8Properties properties = new UTF8Properties();
properties.load(bndElement.getValue(), project.getFile(), builder);
if (baseDir != null) {
String here = baseDir.toURI().getPath();
here = Matcher.quoteReplacement(here.substring(0, here.length() - 1));
properties = properties.replaceAll("\\$\\{\\.\\}", here);
}
// we use setProperties to handle -include
builder.setProperties(baseDir, properties);
}
}
return project.getFile();
}
private void reportErrorsAndWarnings(Builder builder) throws MojoExecutionException {
@SuppressWarnings("unchecked")
Collection<File> markedFiles = (Collection<File>) buildContext.getValue(MARKED_FILES);
if (markedFiles == null) {
buildContext.removeMessages(propertiesFile);
markedFiles = builder.getIncluded();
}
if (markedFiles != null) {
for (File f : markedFiles) {
buildContext.removeMessages(f);
}
}
markedFiles = new HashSet<>();
List<String> warnings = builder.getWarnings();
for (String warning : warnings) {
Location location = builder.getLocation(warning);
if (location == null) {
location = new Location();
location.message = warning;
}
File f = location.file == null ? propertiesFile : new File(location.file);
markedFiles.add(f);
buildContext.addMessage(f, location.line, location.length, location.message, BuildContext.SEVERITY_WARNING,
null);
}
List<String> errors = builder.getErrors();
for (String error : errors) {
Location location = builder.getLocation(error);
if (location == null) {
location = new Location();
location.message = error;
}
File f = location.file == null ? propertiesFile : new File(location.file);
markedFiles.add(f);
buildContext.addMessage(f, location.line, location.length, location.message, BuildContext.SEVERITY_ERROR,
null);
}
buildContext.setValue(MARKED_FILES, markedFiles);
if (!builder.isOk()) {
if (errors.size() == 1)
throw new MojoExecutionException(errors.get(0));
else
throw new MojoExecutionException("Errors in bnd processing, see log for details.");
}
}
private void expandJar(Jar jar, File dir) throws Exception {
final long lastModified = jar.lastModified();
if (logger.isDebugEnabled()) {
logger.debug(String.format("Bundle lastModified: %tF %<tT.%<tL", lastModified));
}
dir = dir.getAbsoluteFile();
Files.createDirectories(dir.toPath());
for (Map.Entry<String,Resource> entry : jar.getResources().entrySet()) {
File outFile = getFile(dir, entry.getKey());
Resource resource = entry.getValue();
// Skip the copy if the source and target are the same file
if (resource instanceof FileResource) {
@SuppressWarnings("resource")
FileResource fr = (FileResource) resource;
if (outFile.equals(fr.getFile())) {
continue;
}
}
if (!outFile.exists() || outFile.lastModified() < lastModified) {
if (logger.isDebugEnabled()) {
if (outFile.exists())
logger.debug(String.format("Updating lastModified: %tF %<tT.%<tL '%s'", outFile.lastModified(),
outFile));
else
logger.debug("Creating '{}'", outFile);
}
Files.createDirectories(outFile.toPath().getParent());
try (OutputStream out = buildContext.newFileOutputStream(outFile)) {
IO.copy(resource.openInputStream(), out);
}
}
}
if (manifestOutOfDate() || manifestPath.lastModified() < lastModified) {
if (logger.isDebugEnabled()) {
if (!manifestOutOfDate())
logger.debug(String.format("Updating lastModified: %tF %<tT.%<tL '%s'", manifestPath.lastModified(),
manifestPath));
else
logger.debug("Creating '{}'", manifestPath);
}
Files.createDirectories(manifestPath.toPath().getParent());
try (OutputStream manifestOut = buildContext.newFileOutputStream(manifestPath)) {
jar.writeManifest(manifestOut);
}
buildContext.setValue(MANIFEST_LAST_MODIFIED, manifestPath.lastModified());
}
}
private boolean manifestOutOfDate() {
if (!manifestPath.isFile()) {
return true;
}
long manifestLastModified = 0L;
if (buildContext.getValue(MANIFEST_LAST_MODIFIED) != null) {
manifestLastModified = (Long) buildContext.getValue(MANIFEST_LAST_MODIFIED);
}
return manifestPath.lastModified() != manifestLastModified;
}
private class BeanProperties extends Properties {
private static final long serialVersionUID = 1L;
BeanProperties() {
super();
}
@Override
public String getProperty(String key) {
final int i = key.indexOf('.');
final String name = (i > 0) ? key.substring(0, i) : key;
Object value = get(name);
if ((value != null) && (i > 0)) {
value = getField(value, key.substring(i + 1));
}
if (value == null) {
return null;
}
return value.toString();
}
private Object getField(Object target, String key) {
final int i = key.indexOf('.');
final String fieldName = (i > 0) ? key.substring(0, i) : key;
final String getterSuffix = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
Object value = null;
try {
Class< ? > targetClass = target.getClass();
while (!Modifier.isPublic(targetClass.getModifiers())) {
targetClass = targetClass.getSuperclass();
}
Method getter;
try {
getter = targetClass.getMethod("get" + getterSuffix);
} catch (NoSuchMethodException nsme) {
getter = targetClass.getMethod("is" + getterSuffix);
}
value = getter.invoke(target);
} catch (Exception e) {
logger.debug("Could not find getter method for field: {}", fieldName, e);
}
if ((value != null) && (i > 0)) {
value = getField(value, key.substring(i + 1));
}
return value;
}
}
}