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:39 UTC

[sling-slingfeature-maven-plugin] branch SLING-8174 created (now dc02dec)

This is an automated email from the ASF dual-hosted git repository.

simonetripodi pushed a change to branch SLING-8174
in repository https://gitbox.apache.org/repos/asf/sling-slingfeature-maven-plugin.git.


      at dc02dec  SLING-8174 - New MOJO implementation which creates a bundle for each api-region containing related APIs

This branch includes the following new commits:

     new dc02dec  SLING-8174 - New MOJO implementation which creates a bundle for each api-region containing related APIs

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[sling-slingfeature-maven-plugin] 01/01: SLING-8174 - New MOJO implementation which creates a bundle for each api-region containing related APIs

Posted by si...@apache.org.
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;
+    }
+
+}