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