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 
115     log.warn("The plugin sling-initial-content-transform plugin is deprecated, it is not longer required. Please remove it from your project.");
116 
117     if (skip) {
118       log.debug("Skipping execution.");
119       return;
120     }
121     if (!StringUtils.equals(project.getPackaging(), "jar")) {
122       log.debug("Skipping execution - not a jar project: {}", project.getPackaging());
123       return;
124     }
125     if (!file.exists()) {
126       log.warn("File does not exist: {}", file.getPath());
127       return;
128     }
129 
130     try (OsgiBundleFile osgiBundle = new OsgiBundleFile(file)) {
131       if (!osgiBundle.hasContent()) {
132         log.debug("Skipping execution - bundle does not contain Sling-Initial-Content.");
133         return;
134       }
135       transformBundle(osgiBundle);
136     }
137     catch (IOException ex) {
138       throw new MojoExecutionException("Unable to transform bundle.", ex);
139     }
140   }
141 
142   /**
143    * Transform OSGi bundle with Sling-Initial-Content to two separate artifacts with classifier "content" and "bundle".
144    * @throws IOException I/O exception
145    */
146   private void transformBundle(OsgiBundleFile osgiBundle) throws IOException {
147     if (generateContent) {
148       File contentPackageFile = createContentPackage(osgiBundle);
149       projectHelper.attachArtifact(project, "zip", CLASSIFIER_CONTENT, contentPackageFile);
150     }
151     if (generateBundle) {
152       File bundleFile = createBundleWithoutContent(osgiBundle);
153       projectHelper.attachArtifact(project, "jar", CLASSIFIER_BUNDLE, bundleFile);
154     }
155   }
156 
157   /**
158    * Extract Sling-Initial-Content to a content package.
159    * @param osgiBundle OSGi bundle
160    * @return Content package file
161    * @throws IOException I/O exception
162    */
163   private File createContentPackage(OsgiBundleFile osgiBundle) throws IOException {
164     String contentPackageName = project.getBuild().getFinalName() + "-" + CLASSIFIER_CONTENT + ".zip";
165     File contentPackageFile = new File(project.getBuild().getDirectory(), contentPackageName);
166     if (contentPackageFile.exists()) {
167       Files.delete(contentPackageFile.toPath());
168     }
169 
170     ContentPackageBuilder contentPackageBuilder = new ContentPackageBuilder()
171         .group(this.group)
172         .name(project.getArtifactId() + "-" + CLASSIFIER_CONTENT)
173         .version(project.getVersion())
174         .packageType(PackageType.APPLICATION.name().toLowerCase());
175     for (ContentMapping mapping : osgiBundle.getContentMappings()) {
176       contentPackageBuilder.filter(new PackageFilter(mapping.getContentPath()));
177     }
178     for (Map.Entry<String, String> namespace : osgiBundle.getNamespaces().entrySet()) {
179       contentPackageBuilder.xmlNamespace(namespace.getKey(), namespace.getValue());
180     }
181     if (xmlNamespaces != null) {
182       for (Map.Entry<String, String> namespace : xmlNamespaces.entrySet()) {
183         contentPackageBuilder.xmlNamespace(namespace.getKey(), namespace.getValue());
184       }
185     }
186     try (ContentPackage contentPackage = contentPackageBuilder.build(contentPackageFile)) {
187       for (ContentMapping mapping : osgiBundle.getContentMappings()) {
188         List<BundleEntry> entries = osgiBundle.getContentEntries(mapping).collect(Collectors.toList());
189 
190         // first collect all paths we do not need to create explicit directories for
191         CollectNonDirectoryPathsProcessor nonDirectoryPaths = new CollectNonDirectoryPathsProcessor();
192         for (BundleEntry entry : entries) {
193           processContent(contentPackage, entry, mapping, nonDirectoryPaths);
194         }
195 
196         // then generate the actual content in content package
197         WriteContentProcessor writeContent = new WriteContentProcessor(nonDirectoryPaths.getPaths());
198         for (BundleEntry entry : entries) {
199           processContent(contentPackage, entry, mapping, writeContent);
200         }
201       }
202     }
203 
204     log.info("Created package with Sling-Initial-Content: {}", contentPackageFile.getName());
205     return contentPackageFile;
206   }
207 
208   /**
209    * Processes a JAR file entry in the OSGi bundle.
210    * @param contentPackage Content package
211    * @param entry Entry
212    * @param mapping Content mapping that is currently processed
213    * @param processor Processor to do the actual work
214    * @throws IOException I/O exception
215    */
216   private void processContent(ContentPackage contentPackage, BundleEntry entry, ContentMapping mapping,
217       BundleEntryProcessor processor) throws IOException {
218     String extension = FilenameUtils.getExtension(entry.getPath());
219     if (entry.isDirectory()) {
220       String path = StringUtils.removeEnd(entry.getPath(), "/");
221       processor.directory(path, contentPackage);
222     }
223     else if (mapping.isJson() && StringUtils.equals(extension, "json")) {
224       String path = StringUtils.substringBeforeLast(entry.getPath(), ".json");
225       processor.jsonContent(path, entry, contentPackage);
226     }
227     else if (mapping.isXml() && StringUtils.equals(extension, "xml")) {
228       String path = StringUtils.substringBeforeLast(entry.getPath(), ".xml");
229       processor.xmlContent(path, entry, contentPackage);
230     }
231     else {
232       String path = entry.getPath();
233       processor.binaryContent(path, entry, contentPackage);
234     }
235   }
236 
237   /**
238    * Create OSGi bundle JAR file without Sling-Initial-Content.
239    * @param osgiBundle OSGi bundle
240    * @return OSGi bundle file
241    * @throws IOException I/O exception
242    */
243   private File createBundleWithoutContent(OsgiBundleFile osgiBundle) throws IOException {
244     String bundleFileName = project.getBuild().getFinalName() + "-" + CLASSIFIER_BUNDLE + ".jar";
245     File bundleFile = new File(project.getBuild().getDirectory(), bundleFileName);
246     if (bundleFile.exists()) {
247       Files.delete(bundleFile.toPath());
248     }
249 
250     try (FileOutputStream fos = new FileOutputStream(bundleFile);
251         ZipOutputStream zos = new ZipOutputStream(fos)) {
252       List<BundleEntry> entries = osgiBundle.getNonContentEntries().collect(Collectors.toList());
253       for (BundleEntry entry : entries) {
254         zos.putNextEntry(new ZipEntry(entry.getPath()));
255         if (!entry.isDirectory()) {
256           try (InputStream is = entry.getInputStream()) {
257             if (StringUtils.equals(entry.getPath(), MANIFEST_FILE)) {
258               Manifest transformedManifest = getManifestWithoutSlingInitialContentHeader(is);
259               transformedManifest.write(zos);
260             }
261             else {
262               IOUtils.copy(is, zos);
263             }
264           }
265         }
266       }
267     }
268 
269     log.info("Created bundle without content: {}", bundleFile.getName());
270     return bundleFile;
271   }
272 
273   /**
274    * Removes Sling-Initial-Content header of manifest.
275    * @param is Inputstream for manifest file
276    * @return Manifest
277    * @throws IOException I/O exception
278    */
279   private Manifest getManifestWithoutSlingInitialContentHeader(InputStream is) throws IOException {
280     Manifest manifest = new Manifest(is);
281     manifest.getMainAttributes().remove(new Attributes.Name(OsgiBundleFile.HEADER_INITIAL_CONTENT));
282     return manifest;
283   }
284 
285 }