diff --git a/support-maven/bundle-auto-version/pom.xml b/support-maven/bundle-auto-version/pom.xml new file mode 100644 index 0000000..79cd140 --- /dev/null +++ b/support-maven/bundle-auto-version/pom.xml @@ -0,0 +1,84 @@ + + + + + ddf.support + support-maven + 2.3.18-SNAPSHOT + + 4.0.0 + maven-plugin + + bundle-auto-version + + Bundle Import Auto Versioning Plugin + Utility for enforcing OSGi bundle import versions automatically + + + 1.8 + 1.8 + 3.6.0 + 3.6.0 + 3.6.0 + 3.5 + + + + + org.apache.maven + maven-plugin-api + ${maven.plugin.api.version} + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${maven.plugin.annotations.version} + + + org.apache.maven + maven-core + ${maven.core.version} + + + junit + junit + 4.12 + test + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + ${maven.plugin.plugin.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + + org.apache.maven.surefire + surefire-junit47 + ${maven.surefire.plugin.version} + + + + + + + \ No newline at end of file diff --git a/support-maven/bundle-auto-version/src/main/java/org/codice/bundle/auto/version/BundleAutoVersionPlugin.java b/support-maven/bundle-auto-version/src/main/java/org/codice/bundle/auto/version/BundleAutoVersionPlugin.java new file mode 100644 index 0000000..b051070 --- /dev/null +++ b/support-maven/bundle-auto-version/src/main/java/org/codice/bundle/auto/version/BundleAutoVersionPlugin.java @@ -0,0 +1,244 @@ +/** + * Copyright (c) Codice Foundation + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General private License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General private License for more details. A copy of the GNU Lesser General private + * License is distributed along with this program and can be found at + * . + */ +package org.codice.bundle.auto.version; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.jar.Manifest; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.apache.maven.model.Build; +import org.apache.maven.model.Model; +import org.apache.maven.model.Plugin; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.model.io.xpp3.MavenXpp3Writer; +import org.apache.maven.plugin.AbstractMojo; +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.project.MavenProject; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; + +@Mojo( + name = "bundle-auto-version", + defaultPhase = LifecyclePhase.PREPARE_PACKAGE, + threadSafe = true) +public class BundleAutoVersionPlugin extends AbstractMojo { + + private static final String COPYRIGHT_NOTICE = + ""; + + private static final Pattern IMPORT_VALUE_PATTERN = + Pattern.compile("(?:[^,\\\"]+|(?:\\\"[^\\\"]*\\\"))+|[^,]+"); + + private static final String IMPORT_PACKAGE_PROP = "Import-Package"; + private static final String MAVEN_BUNDLE_PLUGIN_ARTIFACT_ID = "maven-bundle-plugin"; + private static final String MAVEN_CONFIG_INSTRUCTIONS = "instructions"; + + @Parameter(defaultValue = "${project}", required = true, readonly = true) + private MavenProject mavenProject; + + @Parameter(property = "excludeModules", defaultValue = "${}") + private List excludeModules; + + @Override + public void execute() { + updateAndSaveModulePom(mavenProject.getModel(), mavenProject.getModel().getProjectDirectory()); + } + + private void updateAndSaveModulePom(Model model, File basePath) { + if (excludeModules.contains(model.getArtifactId())) { + getLog().info("Skipping bundle version update for excluded module " + model.getArtifactId()); + return; + } + + // Does this model have the maven-bundle-plugin + Plugin mavenBundlePlugin = getMavenBundlePlugin(model); + Consumer updateSubFunction = subModel -> updateAndSaveModulePom(subModel, basePath); + + if (null == mavenBundlePlugin) { + getLog() + .warn( + "No " + + MAVEN_BUNDLE_PLUGIN_ARTIFACT_ID + + " configuration found for " + + model.getName()); + getSubModuleModels(model, basePath).stream().forEach(updateSubFunction); + return; + } + + List manifestImportsList = importStringToList(getManifestPackageImports(model)); + + if (null == manifestImportsList || manifestImportsList.isEmpty()) { + getLog() + .warn("No " + IMPORT_PACKAGE_PROP + " directive was found in 'MANIFEST.MF', skipping"); + + return; + } + + try (FileReader pomFileReader = new FileReader(model.getPomFile())) { + model = new MavenXpp3Reader().read(pomFileReader); + } catch (IOException | XmlPullParserException e) { + getLog().error("Error parsing model for Maven project", e); + } + + // The model loaded from pom is the one we're manipulating that's why we're re-getting the + // plugin configuration... + // to get a reference to the correct one + Xpp3Dom configInstructions = getPluginConfiguration(getMavenBundlePlugin(model)); + + if (configInstructions == null) + throw new RuntimeException("Unable to locate configuration for " + mavenBundlePlugin); + + // Is there an `Import-Package` directive? If not, skip + if (null == configInstructions.getChild(IMPORT_PACKAGE_PROP)) { + getLog() + .info( + "No " + + IMPORT_PACKAGE_PROP + + " found in " + + MAVEN_BUNDLE_PLUGIN_ARTIFACT_ID + + " configuration, skipping"); + return; + } + + List pomImportsList = + importStringToList(configInstructions.getChild(IMPORT_PACKAGE_PROP).getValue()); + + if (pomImportsList.equals(manifestImportsList)) { + getLog().info("Package imports between pom.xml and MANIFEST.MF match, skipping"); + return; + } + + String manifestImportsString = manifestImportsList.stream().collect(Collectors.joining(",\n")); + configInstructions.getChild(IMPORT_PACKAGE_PROP).setValue(manifestImportsString); + + saveProjectModel(model, mavenProject.getModel().getPomFile()); + + getSubModuleModels(model, basePath).stream().forEach(updateSubFunction); + } + + private List getSubModuleModels(Model parentModel, File parentDirectory) { + return parentModel.getModules().stream() + .map(submodule -> new SubModel(parentDirectory.getAbsolutePath(), submodule, getLog())) + .map(SubModel::getModel) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private List importStringToList(String packageImport) { + return StreamSupport.stream( + new MatchIterator(IMPORT_VALUE_PATTERN.matcher(packageImport)), false) + .sorted() + .collect(Collectors.toList()); + } + + private void saveProjectModel(Model model, File pomFile) { + try (FileWriter pomFileWriter = new FileWriter(pomFile)) { + new MavenXpp3Writer().write(pomFileWriter, model); + addCopyrightNotice(pomFile); + } catch (IOException e) { + getLog().error("Error saving project model to pom file", e); + } + } + + private void addCopyrightNotice(File pomFile) throws IOException { + List pomFileLines = Files.lines(pomFile.toPath()).collect(Collectors.toList()); + + pomFileLines.set(0, appendCopyrightNotice(pomFileLines.get(0))); + + Files.write(pomFile.toPath(), pomFileLines, Charset.forName("UTF-8")); + } + + private String appendCopyrightNotice(String line) { + return line + "\n" + COPYRIGHT_NOTICE; + } + + private Model readProjectPom(File pomFile) { + try (FileReader pomFileReader = new FileReader(pomFile)) { + return new MavenXpp3Reader().read(pomFileReader); + } catch (IOException | XmlPullParserException e) { + getLog().error("Error reading project model from pom file", e); + } + + return null; + } + + private String getManifestPackageImports(Model model) { + Path manifestFilePath = + Paths.get(model.getBuild().getOutputDirectory() + "/META-INF/MANIFEST.MF"); + + if (!manifestFilePath.toFile().exists()) return ""; + + Manifest manifest = null; + + try (FileInputStream manifestInputStream = new FileInputStream(manifestFilePath.toString())) { + manifest = new Manifest(manifestInputStream); + } catch (IOException e) { + getLog().error("Error reading 'MANIFEST.MF' for the project", e); + } + + if (manifest == null) throw new RuntimeException("Unable to locate generated 'MANIFEST.MF'"); + + return Optional.ofNullable(manifest) + .map(Manifest::getMainAttributes) + .map(attributes -> attributes.getValue(IMPORT_PACKAGE_PROP)) + .orElse(""); + } + + private Plugin getMavenBundlePlugin(Model model) { + return Optional.ofNullable(model.getBuild()).map(Build::getPlugins) + .orElse(Collections.emptyList()).stream() + .filter(plugin -> MAVEN_BUNDLE_PLUGIN_ARTIFACT_ID.equals(plugin.getArtifactId())) + .findFirst() + .orElse(null); + } + + private Xpp3Dom getPluginConfiguration(Plugin plugin) { + Xpp3Dom configuration = + Optional.ofNullable(plugin.getConfiguration()).map(Xpp3Dom.class::cast).orElse(null); + + return Arrays.stream(configuration.getChildren()) + .filter(entry -> MAVEN_CONFIG_INSTRUCTIONS.equals(entry.getName())) + .findFirst() + .orElse(null); + } +} diff --git a/support-maven/bundle-auto-version/src/main/java/org/codice/bundle/auto/version/MatchIterator.java b/support-maven/bundle-auto-version/src/main/java/org/codice/bundle/auto/version/MatchIterator.java new file mode 100644 index 0000000..f7167ae --- /dev/null +++ b/support-maven/bundle-auto-version/src/main/java/org/codice/bundle/auto/version/MatchIterator.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) Codice Foundation + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General private License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General private License for more details. A copy of the GNU Lesser General private + * License is distributed along with this program and can be found at + * . + */ +package org.codice.bundle.auto.version; + +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.regex.Matcher; + +class MatchIterator extends Spliterators.AbstractSpliterator { + private final Matcher matcher; + + MatchIterator(Matcher m) { + super(m.regionEnd() - m.regionStart(), ORDERED | NONNULL); + matcher = m; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (!matcher.find()) return false; + action.accept(matcher.group()); + return true; + } +} diff --git a/support-maven/bundle-auto-version/src/main/java/org/codice/bundle/auto/version/SubModel.java b/support-maven/bundle-auto-version/src/main/java/org/codice/bundle/auto/version/SubModel.java new file mode 100644 index 0000000..39acbf3 --- /dev/null +++ b/support-maven/bundle-auto-version/src/main/java/org/codice/bundle/auto/version/SubModel.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) Codice Foundation + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General private License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General private License for more details. A copy of the GNU Lesser General private + * License is distributed along with this program and can be found at + * . + */ +package org.codice.bundle.auto.version; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.plugin.logging.Log; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; + +public class SubModel { + + private final String name; + private final Path path; + private final Log log; + + SubModel(String parentPath, String submoduleName, Log log) { + this.path = Paths.get(parentPath, submoduleName); + this.name = submoduleName; + this.log = log; + } + + Model getModel() { + try { + readProjectModel(); + } catch (IOException | XmlPullParserException e) { + log.error("Unable to read model for " + name, e); + } + + return null; + } + + private Model readProjectModel() throws XmlPullParserException, IOException { + FileReader pomFileReader = new FileReader(new File(path.toFile(), "pom.xml")); + return new MavenXpp3Reader().read(pomFileReader); + } +} diff --git a/support-maven/pom.xml b/support-maven/pom.xml index 0a2b8c5..3a93592 100644 --- a/support-maven/pom.xml +++ b/support-maven/pom.xml @@ -28,6 +28,7 @@ version-validation-plugin artifact-size-enforcer bundle-validation-plugin + bundle-auto-version \ No newline at end of file