View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2021 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.maven.plugins.slinginitialcontenttransform;
21  
22  import java.io.File;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.nio.file.Files;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.jar.Attributes;
30  import java.util.jar.Manifest;
31  import java.util.stream.Collectors;
32  import java.util.zip.ZipEntry;
33  import java.util.zip.ZipOutputStream;
34  
35  import org.apache.commons.io.FilenameUtils;
36  import org.apache.commons.io.IOUtils;
37  import org.apache.commons.lang3.StringUtils;
38  import org.apache.jackrabbit.vault.packaging.PackageType;
39  import org.apache.maven.plugin.AbstractMojo;
40  import org.apache.maven.plugin.MojoExecutionException;
41  import org.apache.maven.plugin.MojoFailureException;
42  import org.apache.maven.plugins.annotations.Component;
43  import org.apache.maven.plugins.annotations.LifecyclePhase;
44  import org.apache.maven.plugins.annotations.Mojo;
45  import org.apache.maven.plugins.annotations.Parameter;
46  import org.apache.maven.project.MavenProject;
47  import org.apache.maven.project.MavenProjectHelper;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  import io.wcm.tooling.commons.contentpackagebuilder.ContentPackage;
52  import io.wcm.tooling.commons.contentpackagebuilder.ContentPackageBuilder;
53  import io.wcm.tooling.commons.contentpackagebuilder.PackageFilter;
54  
55  /**
56   * Extracts Sling-Initial-Content from an OSGi bundle and attaches two artifacts with classifiers:
57   * <ul>
58   * <li><code>bundle</code>: OSGi bundle without the Sling-Initial-Content</li>
59   * <li><code>content</code>: Content packages with the Sling-Initial-Content transformed to FileVault</li>
60   * </ul>
61   */
62  @Mojo(name = "transform", requiresProject = true, threadSafe = true, defaultPhase = LifecyclePhase.PACKAGE)
63  public class TransformMojo extends AbstractMojo {
64  
65    private static final String CLASSIFIER_CONTENT = "content";
66    private static final String CLASSIFIER_BUNDLE = "bundle";
67    private static final String MANIFEST_FILE = "META-INF/MANIFEST.MF";
68  
69    private static final Logger log = LoggerFactory.getLogger(TransformMojo.class);
70  
71    /**
72     * The name of the OSGi bundle file to process.
73     */
74    @Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}.jar", required = true)
75    private File file;
76  
77    /**
78     * The group of the content package.
79     */
80    @Parameter(defaultValue = "${project.groupId}", required = true)
81    private String group;
82  
83    /**
84     * Generate attached "content" artifact with content package with Sling-Initial-Content.
85     */
86    @Parameter(defaultValue = "true", required = true)
87    private boolean generateContent;
88  
89    /**
90     * Generate attached "bundle" artifact with OSGi bundle without Sling-Initial-Content.
91     */
92    @Parameter(defaultValue = "true", required = true)
93    private boolean generateBundle;
94  
95    /**
96     * Additional XML namespace mappings.
97     */
98    @Parameter
99    private Map<String, String> xmlNamespaces;
100 
101   /**
102    * Allows to skip the plugin execution.
103    */
104   @Parameter(property = "slinginitialcontenttransform.skip", defaultValue = "false")
105   private boolean skip;
106 
107   @Parameter(property = "project", required = true, readonly = true)
108   private MavenProject project;
109   @Component
110   private MavenProjectHelper projectHelper;
111 
112   @Override
113   public void execute() throws MojoExecutionException, MojoFailureException {
114     if (skip) {
115       log.debug("Skipping execution.");
116       return;
117     }
118     if (!StringUtils.equals(project.getPackaging(), "jar")) {
119       log.debug("Skipping execution - not a jar project: {}", project.getPackaging());
120       return;
121     }
122     if (!file.exists()) {
123       log.warn("File does not exist: {}", file.getPath());
124       return;
125     }
126 
127     try (OsgiBundleFile osgiBundle = new OsgiBundleFile(file)) {
128       if (!osgiBundle.hasContent()) {
129         log.debug("Skipping execution - bundle does not contain Sling-Initial-Content.");
130         return;
131       }
132       transformBundle(osgiBundle);
133     }
134     catch (IOException ex) {
135       throw new MojoExecutionException("Unable to transform bundle.", ex);
136     }
137   }
138 
139   /**
140    * Transform OSGi bundle with Sling-Initial-Content to two separate artifacts with classifier "content" and "bundle".
141    * @throws IOException I/O exception
142    */
143   private void transformBundle(OsgiBundleFile osgiBundle) throws IOException {
144     if (generateContent) {
145       File contentPackageFile = createContentPackage(osgiBundle);
146       projectHelper.attachArtifact(project, "zip", CLASSIFIER_CONTENT, contentPackageFile);
147     }
148     if (generateBundle) {
149       File bundleFile = createBundleWithoutContent(osgiBundle);
150       projectHelper.attachArtifact(project, "jar", CLASSIFIER_BUNDLE, bundleFile);
151     }
152   }
153 
154   /**
155    * Extract Sling-Initial-Content to a content package.
156    * @param osgiBundle OSGi bundle
157    * @return Content package file
158    * @throws IOException I/O exception
159    */
160   private File createContentPackage(OsgiBundleFile osgiBundle) throws IOException {
161     String contentPackageName = project.getBuild().getFinalName() + "-" + CLASSIFIER_CONTENT + ".zip";
162     File contentPackageFile = new File(project.getBuild().getDirectory(), contentPackageName);
163     if (contentPackageFile.exists()) {
164       Files.delete(contentPackageFile.toPath());
165     }
166 
167     ContentPackageBuilder contentPackageBuilder = new ContentPackageBuilder()
168         .group(this.group)
169         .name(project.getArtifactId() + "-" + CLASSIFIER_CONTENT)
170         .version(project.getVersion())
171         .packageType(PackageType.APPLICATION.name().toLowerCase());
172     for (ContentMapping mapping : osgiBundle.getContentMappings()) {
173       contentPackageBuilder.filter(new PackageFilter(mapping.getContentPath()));
174     }
175     for (Map.Entry<String, String> namespace : osgiBundle.getNamespaces().entrySet()) {
176       contentPackageBuilder.xmlNamespace(namespace.getKey(), namespace.getValue());
177     }
178     if (xmlNamespaces != null) {
179       for (Map.Entry<String, String> namespace : xmlNamespaces.entrySet()) {
180         contentPackageBuilder.xmlNamespace(namespace.getKey(), namespace.getValue());
181       }
182     }
183     try (ContentPackage contentPackage = contentPackageBuilder.build(contentPackageFile)) {
184       for (ContentMapping mapping : osgiBundle.getContentMappings()) {
185         List<BundleEntry> entries = osgiBundle.getContentEntries(mapping).collect(Collectors.toList());
186 
187         // first collect all paths we do not need to create explicit directories for
188         CollectNonDirectoryPathsProcessor nonDirectoryPaths = new CollectNonDirectoryPathsProcessor();
189         for (BundleEntry entry : entries) {
190           processContent(contentPackage, entry, mapping, nonDirectoryPaths);
191         }
192 
193         // then generate the actual content in content package
194         WriteContentProcessor writeContent = new WriteContentProcessor(nonDirectoryPaths.getPaths());
195         for (BundleEntry entry : entries) {
196           processContent(contentPackage, entry, mapping, writeContent);
197         }
198       }
199     }
200 
201     log.info("Created package with Sling-Initial-Content: {}", contentPackageFile.getName());
202     return contentPackageFile;
203   }
204 
205   /**
206    * Processes a JAR file entry in the OSGi bundle.
207    * @param contentPackage Content package
208    * @param entry Entry
209    * @param mapping Content mapping that is currently processed
210    * @param processor Processor to do the actual work
211    * @throws IOException I/O exception
212    */
213   private void processContent(ContentPackage contentPackage, BundleEntry entry, ContentMapping mapping,
214       BundleEntryProcessor processor) throws IOException {
215     String extension = FilenameUtils.getExtension(entry.getPath());
216     if (entry.isDirectory()) {
217       String path = StringUtils.removeEnd(entry.getPath(), "/");
218       processor.directory(path, contentPackage);
219     }
220     else if (mapping.isJson() && StringUtils.equals(extension, "json")) {
221       String path = StringUtils.substringBeforeLast(entry.getPath(), ".json");
222       processor.jsonContent(path, entry, contentPackage);
223     }
224     else if (mapping.isXml() && StringUtils.equals(extension, "xml")) {
225       String path = StringUtils.substringBeforeLast(entry.getPath(), ".xml");
226       processor.xmlContent(path, entry, contentPackage);
227     }
228     else {
229       String path = entry.getPath();
230       processor.binaryContent(path, entry, contentPackage);
231     }
232   }
233 
234   /**
235    * Create OSGi bundle JAR file without Sling-Initial-Content.
236    * @param osgiBundle OSGi bundle
237    * @return OSGi bundle file
238    * @throws IOException I/O exception
239    */
240   private File createBundleWithoutContent(OsgiBundleFile osgiBundle) throws IOException {
241     String bundleFileName = project.getBuild().getFinalName() + "-" + CLASSIFIER_BUNDLE + ".jar";
242     File bundleFile = new File(project.getBuild().getDirectory(), bundleFileName);
243     if (bundleFile.exists()) {
244       Files.delete(bundleFile.toPath());
245     }
246 
247     try (FileOutputStream fos = new FileOutputStream(bundleFile);
248         ZipOutputStream zos = new ZipOutputStream(fos)) {
249       List<BundleEntry> entries = osgiBundle.getNonContentEntries().collect(Collectors.toList());
250       for (BundleEntry entry : entries) {
251         zos.putNextEntry(new ZipEntry(entry.getPath()));
252         if (!entry.isDirectory()) {
253           try (InputStream is = entry.getInputStream()) {
254             if (StringUtils.equals(entry.getPath(), MANIFEST_FILE)) {
255               Manifest transformedManifest = getManifestWithoutSlingInitialContentHeader(is);
256               transformedManifest.write(zos);
257             }
258             else {
259               IOUtils.copy(is, zos);
260             }
261           }
262         }
263       }
264     }
265 
266     log.info("Created bundle without content: {}", bundleFile.getName());
267     return bundleFile;
268   }
269 
270   /**
271    * Removes Sling-Initial-Content header of manifest.
272    * @param is Inputstream for manifest file
273    * @return Manifest
274    * @throws IOException I/O exception
275    */
276   private Manifest getManifestWithoutSlingInitialContentHeader(InputStream is) throws IOException {
277     Manifest manifest = new Manifest(is);
278     manifest.getMainAttributes().remove(new Attributes.Name(OsgiBundleFile.HEADER_INITIAL_CONTENT));
279     return manifest;
280   }
281 
282 }