You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by si...@apache.org on 2018/12/12 13:49:40 UTC
[sling-slingfeature-maven-plugin] 01/01: SLING-8174 - New MOJO
implementation which creates a bundle for each api-region containing
related APIs
This is an automated email from the ASF dual-hosted git repository.
simonetripodi pushed a commit to branch SLING-8174
in repository https://gitbox.apache.org/repos/asf/sling-slingfeature-maven-plugin.git
commit dc02decaf778c5167fe37d55cf731226d536661a
Author: Simo Tripodi <st...@adobe.com>
AuthorDate: Wed Dec 12 14:49:12 2018 +0100
SLING-8174 - New MOJO implementation which creates a bundle for each
api-region containing related APIs
initial checking
---
pom.xml | 11 +
.../sling/feature/maven/mojos/ApisJarMojo.java | 505 +++++++++++++++++++++
2 files changed, 516 insertions(+)
diff --git a/pom.xml b/pom.xml
index 900d52f..655dd54 100644
--- a/pom.xml
+++ b/pom.xml
@@ -191,6 +191,11 @@
<version>${maven.version}</version>
</dependency>
<dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-archiver</artifactId>
+ <version>3.3.0</version>
+ </dependency>
+ <dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.5</version>
@@ -247,6 +252,12 @@
<artifactId>jackson-core</artifactId>
<version>2.2.3</version>
</dependency>
+ <!-- APIs JARs -->
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.utils</artifactId>
+ <version>1.11.0</version>
+ </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/ApisJarMojo.java b/src/main/java/org/apache/sling/feature/maven/mojos/ApisJarMojo.java
new file mode 100644
index 0000000..c73d1fb
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/ApisJarMojo.java
@@ -0,0 +1,505 @@
+/*
+ * 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.sling.feature.maven.mojos;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Formatter;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.Manifest;
+
+import javax.json.Json;
+import javax.json.stream.JsonParser;
+import javax.json.stream.JsonParser.Event;
+
+import org.apache.felix.utils.manifest.Clause;
+import org.apache.felix.utils.manifest.Parser;
+import org.apache.maven.archiver.MavenArchiveConfiguration;
+import org.apache.maven.archiver.MavenArchiver;
+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.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.shared.utils.StringUtils;
+import org.apache.maven.shared.utils.io.DirectoryScanner;
+import org.apache.maven.shared.utils.logging.MessageUtils;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.Extensions;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.ArtifactProvider;
+import org.apache.sling.feature.maven.ProjectHelper;
+import org.codehaus.plexus.archiver.UnArchiver;
+import org.codehaus.plexus.archiver.jar.JarArchiver;
+import org.codehaus.plexus.archiver.manager.ArchiverManager;
+import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
+import org.osgi.framework.Constants;
+
+import edu.emory.mathcs.backport.java.util.Arrays;
+
+/**
+ * Generates the APIs JARs for each selected Feature file.
+ */
+@Mojo(name = "apis-jar",
+ defaultPhase = LifecyclePhase.PACKAGE,
+ requiresDependencyResolution = ResolutionScope.TEST,
+ threadSafe = true
+)
+public class ApisJarMojo extends AbstractIncludingFeatureMojo {
+
+ private static final String API_REGIONS_KEY = "api-regions";
+
+ private static final String NAME_KEY = "name";
+
+ private static final String EXPORTS_KEY = "exports";
+
+ private static final String APIS = "apis";
+
+ private static final String SOURCES = "sources";
+
+ private static final String JAVADOC = "javadoc";
+
+ private static final String[] OUT_DIRS = { APIS, SOURCES, JAVADOC };
+
+ @Parameter
+ private FeatureSelectionConfig selection;
+
+ @Parameter(defaultValue = "${project.build.directory}/apis-jars", readonly = true)
+ private File mainOutputDir;
+
+ @Parameter
+ private String[] includeResources;
+
+ @Parameter
+ private Set<String> excludeRegions;
+
+ //@Component
+ //private ScmManager scmManager;
+
+ @Component
+ private ArchiverManager archiverManager;
+
+ private ArtifactProvider artifactProvider;
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ ProjectHelper.checkPreprocessorRun(this.project);
+
+ artifactProvider = new ArtifactProvider() {
+
+ @Override
+ public File provide(final ArtifactId id) {
+ return ProjectHelper.getOrResolveArtifact(project, mavenSession, artifactHandlerManager, artifactResolver, id).getFile();
+ }
+
+ };
+
+ getLog().debug("Retrieving Feature files...");
+ final Collection<Feature> features = this.getSelectedFeatures(selection).values();
+
+ if (features.isEmpty()) {
+ getLog().debug("There are no assciated Feature files to current project, plugin execution will be interrupted");
+ return;
+ }
+
+ getLog().debug("Starting APIs JARs creation...");
+
+ for (final Feature feature : features) {
+ onFeature(feature);
+ }
+ }
+
+ private void onFeature(Feature feature) throws MojoExecutionException {
+ getLog().debug(MessageUtils.buffer().a("Creating APIs JARs for Feature ").strong(feature.getId().toMvnId())
+ .a(" ...").toString());
+
+ Extensions extensions = feature.getExtensions();
+ Extension apiRegionsExtension = extensions.getByName(API_REGIONS_KEY);
+ if (apiRegionsExtension == null) {
+ getLog().debug("Feature file " + feature.getId().toMvnId() + " does not declare '" + API_REGIONS_KEY + "' extension, no API JAR will be created");
+ return;
+ }
+
+ String jsonRepresentation = apiRegionsExtension.getJSON();
+ if (jsonRepresentation == null || jsonRepresentation.isEmpty()) {
+ getLog().debug("Feature file " + feature.getId().toMvnId() + " declares an empty '" + API_REGIONS_KEY + "' extension, no API JAR will be created");
+ return;
+ }
+
+ // first output level is the aggregated feature
+ File featureDir = new File(mainOutputDir, feature.getId().getClassifier());
+
+ File deflatedDir = new File(featureDir, "deflated-bin");
+ deflatedDir.mkdirs();
+
+ // calculate all api-regions first, taking the inheritance in account
+ List<ApiRegion> apiRegions = fromJson(feature, jsonRepresentation);
+
+ // setup all output directories
+ for (ApiRegion apiRegion : apiRegions) {
+ File regionDir = new File(featureDir, apiRegion.getName());
+ regionDir.mkdirs();
+
+ for (String dirName : OUT_DIRS) {
+ new File(regionDir, dirName).mkdir();
+ }
+ }
+
+ // for each artifact included in the feature file:
+ for (Artifact artifact : feature.getBundles()) {
+ ArtifactId artifactId = artifact.getId();
+
+ // deflate all bundles first, in order to copy APIs and resources later, depending to the region
+ File deflatedBundleDirectory = deflate(deflatedDir, artifactId);
+
+ // renaming potential name-collapsing resources
+ renameResources(deflatedBundleDirectory, artifactId);
+
+ // calculate the exported versioned packages in the manifest file for each region
+ computeExportPackage(apiRegions, deflatedBundleDirectory, artifactId);
+
+ // TODO download the -sources artifact
+ // TODO collect the -sources artifact
+ }
+
+ // recollect and package stuff
+ for (ApiRegion apiRegion : apiRegions) {
+ recollect(feature.getId(), apiRegion, deflatedDir, featureDir);
+ }
+
+ getLog().debug(MessageUtils.buffer().a("APIs JARs for Feature ").debug(feature.getId().toMvnId())
+ .a(" succesfully created").toString());
+ }
+
+ private File deflate(File deflatedDir, ArtifactId artifactId) throws MojoExecutionException {
+ getLog().debug("Retrieving bundle " + artifactId + "...");
+
+ File sourceFile = artifactProvider.provide(artifactId);
+
+ getLog().debug("Bundle " + artifactId + " successfully retrieved");
+
+ getLog().debug("Deflating bundle " + sourceFile + "...");
+
+ File destDirectory = new File(deflatedDir, sourceFile.getName());
+ destDirectory.mkdirs();
+
+ // unarchive the bundle first
+
+ try {
+ UnArchiver unArchiver = archiverManager.getUnArchiver(sourceFile);
+ unArchiver.setSourceFile(sourceFile);
+ unArchiver.setDestDirectory(destDirectory);
+ unArchiver.extract();
+ } catch (NoSuchArchiverException e) {
+ throw new MojoExecutionException("An error occurred while deflating file "
+ + sourceFile
+ + " to directory "
+ + destDirectory, e);
+ }
+
+ getLog().debug("Bundle " + sourceFile + " successfully deflated");
+
+ return destDirectory;
+ }
+
+ private void renameResources(File destDirectory, ArtifactId artifactId) throws MojoExecutionException {
+ if (includeResources == null || includeResources.length == 0) {
+ getLog().debug("No configured resources to rename in " + destDirectory);
+ }
+
+ getLog().debug("Renaming " + Arrays.toString(includeResources) + " files in " + destDirectory + "...");
+
+ DirectoryScanner directoryScanner = new DirectoryScanner();
+ directoryScanner.setBasedir(destDirectory);
+ directoryScanner.setIncludes(includeResources);
+ directoryScanner.scan();
+
+ if (directoryScanner.getIncludedFiles().length == 0) {
+ getLog().debug("No " + Arrays.toString(includeResources) + " resources in " + destDirectory + " to be renamed renamed.");
+ return;
+ }
+
+ for (String resourceName : directoryScanner.getIncludedFiles()) {
+ File resource = new File(destDirectory, resourceName);
+ File renamed = new File(resource.getParentFile(), artifactId.getGroupId() + "-" + artifactId.getArtifactId() + "-" + resource.getName());
+
+ getLog().debug("Renaming resource " + resource + " to " + renamed + "...");
+
+ if (resource.renameTo(renamed)) {
+ getLog().debug("Resource renamed to " + renamed);
+ } else {
+ getLog().warn("Impossible to rename resource " + resource + " to " + renamed + ", please check the current user has enough rights on the File System");
+ }
+ }
+
+ getLog().debug(Arrays.toString(includeResources) + " resources in " + destDirectory + " successfully renamed");
+ }
+
+ private void computeExportPackage(List<ApiRegion> apiRegions, File destDirectory, ArtifactId artifactId) throws MojoExecutionException {
+ File manifestFile = new File(destDirectory, "META-INF/MANIFEST.MF");
+
+ getLog().debug("Reading Manifest headers from file " + manifestFile);
+
+ if (!manifestFile.exists()) {
+ throw new MojoExecutionException("Manifest file "
+ + manifestFile
+ + " does not exist, make sure "
+ + destDirectory
+ + " contains the valid deflated "
+ + artifactId
+ + " bundle");
+ }
+
+ try (FileInputStream input = new FileInputStream(manifestFile)) {
+ Manifest manifest = new Manifest(input);
+ String exportPackageHeader = manifest.getMainAttributes().getValue(Constants.EXPORT_PACKAGE);
+ Clause[] exportPackages = Parser.parseHeader(exportPackageHeader);
+
+ // filter for each region
+ for (Clause exportPackage : exportPackages) {
+ String api = exportPackage.getName();
+
+ for (ApiRegion apiRegion : apiRegions) {
+ if (apiRegion.containsApi(api)) {
+ apiRegion.addExportPackage(exportPackage);
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new MojoExecutionException("An error occurred while reading " + manifestFile + " file", e);
+ }
+ }
+
+ private void recollect(ArtifactId featureId, ApiRegion apiRegion, File deflatedDir, File featureDir) throws MojoExecutionException {
+ DirectoryScanner directoryScanner = new DirectoryScanner();
+ directoryScanner.setBasedir(deflatedDir);
+
+ // for each region, include both APIs and resources
+ String[] includes = concatenate(apiRegion.getFilteringApis(), includeResources);
+ directoryScanner.setIncludes(includes);
+ directoryScanner.scan();
+
+ JarArchiver jarArchiver = new JarArchiver();
+ for (String includedFile : directoryScanner.getIncludedFiles()) {
+ // first level is always the filename...
+ jarArchiver.addFile(new File(deflatedDir, includedFile), includedFile.substring(includedFile.indexOf(File.separator) + 1));
+ }
+
+ // APIs need OSGi Manifest entry
+ MavenArchiveConfiguration archiveConfiguration = new MavenArchiveConfiguration();
+ archiveConfiguration.addManifestEntry("Export-Package", StringUtils.join(apiRegion.getExportPackage(), ","));
+ archiveConfiguration.addManifestEntry("Bundle-Description", project.getDescription());
+ archiveConfiguration.addManifestEntry("Bundle-Version", featureId.getOSGiVersion().toString());
+ archiveConfiguration.addManifestEntry("Bundle-ManifestVersion", "2");
+ String symbolicName = String.format("%s.%s-%s-%s",
+ featureId.getArtifactId(),
+ featureId.getClassifier(),
+ apiRegion.getName(),
+ APIS);
+ archiveConfiguration.addManifestEntry("Bundle-SymbolicName", symbolicName);
+ String bundleName = String.format("%s-%s-%s-%s",
+ featureId.getArtifactId(),
+ featureId.getClassifier(),
+ apiRegion.getName(),
+ APIS);
+ archiveConfiguration.addManifestEntry("Bundle-Name", bundleName);
+ archiveConfiguration.addManifestEntry("Bundle-Vendor", project.getOrganization().getName());
+ archiveConfiguration.addManifestEntry("Specification-Version", featureId.getVersion());
+ archiveConfiguration.addManifestEntry("Implementation-Title", bundleName);
+
+ String targetName = String.format("%s-%s.jar",
+ bundleName,
+ featureId.getVersion());
+ File target = new File(mainOutputDir, targetName);
+ MavenArchiver archiver = new MavenArchiver();
+ archiver.setArchiver(jarArchiver);
+ archiver.setOutputFile(target);
+
+ try {
+ archiver.createArchive(mavenSession, project, archiveConfiguration);
+ projectHelper.attachArtifact(project, "jar", featureId.getClassifier() + APIS, target);
+ } catch (Exception e) {
+ throw new MojoExecutionException("An error occurred while creating APIs "
+ + target
+ +" archive", e);
+ }
+ }
+
+ private List<ApiRegion> fromJson(Feature feature, String jsonRepresentation) throws MojoExecutionException {
+ List<ApiRegion> apiRegions = new ArrayList<>();
+
+ // pointers
+ Event event;
+ ApiRegion apiRegion;
+
+ JsonParser parser = Json.createParser(new StringReader(jsonRepresentation));
+ if (Event.START_ARRAY != parser.next()) {
+ throw new MojoExecutionException("Expected 'api-region' element to start with an Array in Feature "
+ + feature.getId().toMvnId()
+ + ": "
+ + parser.getLocation());
+ }
+
+ while (Event.END_ARRAY != (event = parser.next())) {
+ if (Event.START_OBJECT != event) {
+ throw new MojoExecutionException("Expected 'api-region' data to start with an Object in Feature "
+ + feature.getId().toMvnId()
+ + ": "
+ + parser.getLocation());
+ }
+
+ apiRegion = new ApiRegion();
+
+ // inherit all APIs from previous region, if any
+ if (apiRegions.size() > 1) {
+ apiRegion.doInherit(apiRegions.get(apiRegions.size() - 1));
+ }
+
+ while (Event.END_OBJECT != (event = parser.next())) {
+ if (Event.KEY_NAME == event) {
+ switch (parser.getString()) {
+ case NAME_KEY:
+ parser.next();
+ apiRegion.setName(parser.getString());
+ break;
+
+ case EXPORTS_KEY:
+ // start array
+ parser.next();
+
+ while (parser.hasNext() && Event.VALUE_STRING == parser.next()) {
+ String api = parser.getString();
+ // skip comments
+ if ('#' != api.charAt(0)) {
+ apiRegion.addApi(api);
+ }
+ }
+
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ if (excludeRegions != null
+ && !excludeRegions.isEmpty()
+ && excludeRegions.contains(apiRegion.getName())) {
+ getLog().debug("API Region " + apiRegion.getName() + " will not processed since it is in the exclude list");
+ } else {
+ apiRegions.add(apiRegion);
+ }
+ }
+
+ return apiRegions;
+ }
+
+ private static final class ApiRegion {
+
+ private final Set<String> apis = new TreeSet<>();
+
+ private final Set<String> filteringApis = new TreeSet<>();
+
+ private final List<Clause> exportPackage = new ArrayList<>();
+
+ private String name;
+
+ private String inherits;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Iterable<String> getApis() {
+ return apis;
+ }
+
+ public String[] getFilteringApis() {
+ return filteringApis.toArray(new String[filteringApis.size()]);
+ }
+
+ public void addApi(String api) {
+ apis.add(api);
+ filteringApis.add("**/" + api.replace('.', '/') + "/*");
+ }
+
+ public boolean containsApi(String api) {
+ return apis.contains(api);
+ }
+
+ public void addExportPackage(Clause exportPackage) {
+ this.exportPackage.add(exportPackage);
+ }
+
+ public Iterator<Clause> getExportPackage() {
+ return exportPackage.iterator();
+ }
+
+ public void doInherit(ApiRegion parent) {
+ inherits = parent.getName();
+ for (String api : parent.getApis()) {
+ addApi(api);
+ }
+ }
+
+ @Override
+ public String toString() {
+ Formatter formatter = new Formatter();
+ formatter.format("Region '%s'", name);
+
+ if (inherits != null && !inherits.isEmpty()) {
+ formatter.format(" inherits from '%s'", inherits);
+ }
+
+ formatter.format(":%n");
+
+ for (String api : apis) {
+ formatter.format(" * %s%n", api);
+ }
+
+ String toString = formatter.toString();
+ formatter.close();
+
+ return toString;
+ }
+
+ }
+
+ private static String[] concatenate(String[] a, String[] b) {
+ String[] result = new String[a.length + b.length];
+ System.arraycopy(a, 0, result, 0, a.length);
+ System.arraycopy(b, 0, result, a.length, b.length);
+ return result;
+ }
+
+}