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; } } }