1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.felix.bundleplugin;
20
21
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileNotFoundException;
25 import java.io.IOError;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.io.Writer;
30 import java.nio.charset.StandardCharsets;
31 import java.nio.file.Files;
32 import java.nio.file.Path;
33 import java.nio.file.Paths;
34 import java.util.Iterator;
35 import java.util.LinkedHashMap;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Map.Entry;
39 import java.util.Properties;
40 import java.util.Set;
41 import java.util.jar.Manifest;
42 import java.util.stream.Collectors;
43 import java.util.stream.Stream;
44 import java.util.zip.ZipEntry;
45 import java.util.zip.ZipFile;
46
47 import aQute.bnd.osgi.Analyzer;
48 import aQute.bnd.osgi.Builder;
49 import aQute.bnd.osgi.Instructions;
50 import aQute.bnd.osgi.Jar;
51 import aQute.bnd.osgi.Resource;
52 import aQute.lib.collections.ExtList;
53 import org.apache.maven.artifact.Artifact;
54 import org.apache.maven.plugin.MojoExecutionException;
55 import org.apache.maven.plugin.MojoFailureException;
56 import org.apache.maven.plugin.logging.Log;
57 import org.apache.maven.plugins.annotations.Component;
58 import org.apache.maven.plugins.annotations.LifecyclePhase;
59 import org.apache.maven.plugins.annotations.Mojo;
60 import org.apache.maven.plugins.annotations.Parameter;
61 import org.apache.maven.plugins.annotations.ResolutionScope;
62 import org.apache.maven.project.MavenProject;
63 import org.codehaus.plexus.util.Scanner;
64 import org.osgi.service.metatype.MetaTypeService;
65 import org.sonatype.plexus.build.incremental.BuildContext;
66
67
68
69
70
71 @Mojo( name = "manifest", requiresDependencyResolution = ResolutionScope.TEST,
72 threadSafe = true,
73 defaultPhase = LifecyclePhase.PROCESS_CLASSES)
74 public class ManifestPlugin extends BundlePlugin
75 {
76
77
78
79 @Parameter( property = "rebuildBundle" )
80 protected boolean rebuildBundle;
81
82
83
84
85
86
87 @Parameter( property = "supportIncrementalBuild" )
88 private boolean supportIncrementalBuild;
89
90 @Component
91 private BuildContext buildContext;
92
93 @Override
94 protected void execute( Map<String, String> instructions, ClassPathItem[] classpath )
95 throws MojoExecutionException
96 {
97
98 if (supportIncrementalBuild && isUpToDate(project)) {
99 return;
100 }
101
102
103 if (buildContext.isIncremental() && !(supportIncrementalBuild && anyJavaSourceFileTouchedSinceLastBuild())) {
104 getLog().debug("Skipping manifest generation because no java source file was added, updated or removed since last build.");
105 return;
106 }
107
108 Analyzer analyzer;
109 try
110 {
111 analyzer = getAnalyzer(project, instructions, classpath);
112
113 if (supportIncrementalBuild) {
114 writeIncrementalInfo(project);
115 }
116 }
117 catch ( FileNotFoundException e )
118 {
119 throw new MojoExecutionException( "Cannot find " + e.getMessage()
120 + " (manifest goal must be run after compile phase)", e );
121 }
122 catch ( IOException e )
123 {
124 throw new MojoExecutionException( "Error trying to generate Manifest", e );
125 }
126 catch ( MojoFailureException e )
127 {
128 getLog().error( e.getLocalizedMessage() );
129 throw new MojoExecutionException( "Error(s) found in manifest configuration", e );
130 }
131 catch ( Exception e )
132 {
133 getLog().error( "An internal error occurred", e );
134 throw new MojoExecutionException( "Internal error in maven-bundle-plugin", e );
135 }
136
137 File outputFile = new File( manifestLocation, "MANIFEST.MF" );
138
139 try
140 {
141 writeManifest( analyzer, outputFile, niceManifest, exportScr, scrLocation, buildContext, getLog() );
142 }
143 catch ( Exception e )
144 {
145 throw new MojoExecutionException( "Error trying to write Manifest to file " + outputFile, e );
146 }
147 finally
148 {
149 try
150 {
151 analyzer.close();
152 }
153 catch ( IOException e )
154 {
155 throw new MojoExecutionException( "Error trying to write Manifest to file " + outputFile, e );
156 }
157 }
158 }
159
160
161
162
163 private boolean anyJavaSourceFileTouchedSinceLastBuild() {
164 @SuppressWarnings("unchecked")
165 List<String> sourceDirectories = project.getCompileSourceRoots();
166 for (String sourceDirectory : sourceDirectories) {
167 File directory = new File(sourceDirectory);
168 Scanner scanner = buildContext.newScanner(directory);
169 Scanner deleteScanner = buildContext.newDeleteScanner(directory);
170 if (containsJavaFile(scanner) || containsJavaFile(deleteScanner)) {
171 return true;
172 }
173 }
174 return false;
175 }
176 private boolean containsJavaFile(Scanner scanner) {
177 String[] includes = new String[] { "**/*.java" };
178 scanner.setIncludes(includes);
179 scanner.scan();
180 return scanner.getIncludedFiles().length > 0;
181 }
182
183 public Manifest getManifest( MavenProject project, ClassPathItem[] classpath ) throws IOException, MojoFailureException,
184 MojoExecutionException, Exception
185 {
186 return getManifest( project, new LinkedHashMap<String, String>(), classpath, buildContext );
187 }
188
189
190 public Manifest getManifest( MavenProject project, Map<String, String> instructions, ClassPathItem[] classpath,
191 BuildContext buildContext ) throws IOException, MojoFailureException, MojoExecutionException, Exception
192 {
193 Analyzer analyzer = getAnalyzer(project, instructions, classpath);
194
195 Jar jar = analyzer.getJar();
196 Manifest manifest = jar.getManifest();
197
198 if (exportScr)
199 {
200 exportScr(analyzer, jar, scrLocation, buildContext, getLog() );
201 }
202
203
204 analyzer.close();
205
206 return manifest;
207 }
208
209 private static void exportScr(Analyzer analyzer, Jar jar, File scrLocation, BuildContext buildContext, Log log ) throws Exception {
210 log.debug("Export SCR metadata to: " + scrLocation.getPath());
211 scrLocation.mkdirs();
212
213
214 Map<String, Resource> scrDir = jar.getDirectories().get("OSGI-INF");
215 if (scrDir != null) {
216 for (Map.Entry<String, Resource> entry : scrDir.entrySet()) {
217 String path = entry.getKey();
218 Resource resource = entry.getValue();
219 writeSCR(resource, new File(scrLocation, path), buildContext,
220 log);
221 }
222 }
223
224
225 Map<String,Resource> metatypeDir = jar.getDirectories().get(MetaTypeService.METATYPE_DOCUMENTS_LOCATION);
226 if (metatypeDir != null) {
227 for (Map.Entry<String, Resource> entry : metatypeDir.entrySet())
228 {
229 String path = entry.getKey();
230 Resource resource = entry.getValue();
231 writeSCR(resource, new File(scrLocation, path), buildContext, log);
232 }
233 }
234
235 }
236
237 private static void writeSCR(Resource resource, File destination, BuildContext buildContext, Log log ) throws Exception
238 {
239 log.debug("Write SCR file: " + destination.getPath());
240 destination.getParentFile().mkdirs();
241 OutputStream os = buildContext.newFileOutputStream(destination);
242 try
243 {
244 resource.write(os);
245 }
246 finally
247 {
248 os.close();
249 }
250 }
251
252 protected Analyzer getAnalyzer( MavenProject project, ClassPathItem[] classpath ) throws IOException, MojoExecutionException,
253 Exception
254 {
255 return getAnalyzer( project, new LinkedHashMap<>(), classpath );
256 }
257
258
259 protected Analyzer getAnalyzer( MavenProject project, Map<String, String> instructions, ClassPathItem[] classpath )
260 throws IOException, MojoExecutionException, Exception
261 {
262 if ( rebuildBundle && supportedProjectTypes.contains( project.getArtifact().getType() ) )
263 {
264 return buildOSGiBundle( project, instructions, classpath );
265 }
266
267 File file = getOutputDirectory();
268 if ( file == null )
269 {
270 file = project.getArtifact().getFile();
271 }
272
273 if ( !file.exists() )
274 {
275 if ( file.equals( getOutputDirectory() ) )
276 {
277 file.mkdirs();
278 }
279 else
280 {
281 throw new FileNotFoundException( file.getPath() );
282 }
283 }
284
285 Builder analyzer = getOSGiBuilder( project, instructions, classpath );
286
287 analyzer.setJar( file );
288
289
290
291
292
293 boolean isOutputDirectory = file.equals( getOutputDirectory() );
294
295 if ( analyzer.getProperty( Analyzer.EXPORT_PACKAGE ) == null
296 && analyzer.getProperty( Analyzer.EXPORT_CONTENTS ) == null
297 && analyzer.getProperty( Analyzer.PRIVATE_PACKAGE ) == null && !isOutputDirectory )
298 {
299 String export = calculateExportsFromContents( analyzer.getJar() );
300 analyzer.setProperty( Analyzer.EXPORT_PACKAGE, export );
301 }
302
303 addMavenInstructions( project, analyzer );
304
305
306 if ( analyzer.getProperty( DependencyEmbedder.EMBED_DEPENDENCY ) != null && isOutputDirectory )
307 {
308 analyzer.build();
309 }
310 else
311 {
312 analyzer.mergeManifest( analyzer.getJar().getManifest() );
313 analyzer.getJar().setManifest( analyzer.calcManifest() );
314 }
315
316 mergeMavenManifest( project, analyzer );
317
318 boolean hasErrors = reportErrors( "Manifest " + project.getArtifact(), analyzer );
319 if ( hasErrors )
320 {
321 String failok = analyzer.getProperty( "-failok" );
322 if ( null == failok || "false".equalsIgnoreCase( failok ) )
323 {
324 throw new MojoFailureException( "Error(s) found in manifest configuration" );
325 }
326 }
327
328 Jar jar = analyzer.getJar();
329
330 if ( unpackBundle )
331 {
332 File outputFile = getOutputDirectory();
333 for ( Entry<String, Resource> entry : jar.getResources().entrySet() )
334 {
335 File entryFile = new File( outputFile, entry.getKey() );
336 if ( !entryFile.exists() || entry.getValue().lastModified() == 0 )
337 {
338 entryFile.getParentFile().mkdirs();
339 OutputStream os = buildContext.newFileOutputStream( entryFile );
340 entry.getValue().write( os );
341 os.close();
342 }
343 }
344 }
345
346 return analyzer;
347 }
348
349 private void writeIncrementalInfo(MavenProject project) throws MojoExecutionException {
350 try {
351 Path cacheData = getIncrementalDataPath(project);
352 String curdata = getIncrementalData();
353 Files.createDirectories(cacheData.getParent());
354 try (Writer w = Files.newBufferedWriter(cacheData)) {
355 w.append(curdata);
356 }
357 } catch (IOException e) {
358 throw new MojoExecutionException("Error checking manifest uptodate status", e);
359 }
360 }
361
362 private boolean isUpToDate(MavenProject project) throws MojoExecutionException {
363 try {
364 Path cacheData = getIncrementalDataPath(project);
365 String prvdata;
366 if (Files.isRegularFile(cacheData)) {
367 prvdata = new String(Files.readAllBytes(cacheData), StandardCharsets.UTF_8);
368 } else {
369 prvdata = null;
370 }
371 String curdata = getIncrementalData();
372 if (curdata.equals(prvdata)) {
373 long lastmod = Files.getLastModifiedTime(cacheData).toMillis();
374 Set<String> stale = Stream.concat(Stream.of(new File(project.getBuild().getOutputDirectory())),
375 project.getArtifacts().stream().map(Artifact::getFile))
376 .flatMap(f -> newer(lastmod, f))
377 .collect(Collectors.toSet());
378 if (!stale.isEmpty()) {
379 getLog().info("Stale files detected, re-generating manifest.");
380 if (getLog().isDebugEnabled()) {
381 getLog().debug("Stale files: " + stale.stream()
382 .collect(Collectors.joining(", ")));
383 }
384 } else {
385
386 getLog().info("Skipping manifest generation, everything is up to date.");
387 return true;
388 }
389 } else {
390 if (prvdata == null) {
391 getLog().info("No previous run data found, generating manifest.");
392 } else {
393 getLog().info("Configuration changed, re-generating manifest.");
394 }
395 }
396 } catch (IOException e) {
397 throw new MojoExecutionException("Error checking manifest uptodate status", e);
398 }
399 return false;
400 }
401
402 private String getIncrementalData() {
403 return getInstructions().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue())
404 .collect(Collectors.joining("\n", "", "\n"));
405 }
406
407 private Path getIncrementalDataPath(MavenProject project) {
408 return Paths.get(project.getBuild().getDirectory(), "maven-bundle-plugin",
409 "org.apache.felix_maven-bundle-plugin_manifest_xx");
410 }
411
412 private long lastmod(Path p) {
413 try {
414 return Files.getLastModifiedTime(p).toMillis();
415 } catch (IOException e) {
416 return 0;
417 }
418 }
419
420 private Stream<String> newer(long lastmod, File file) {
421 try {
422 if (file.isDirectory()) {
423 return Files.walk(file.toPath())
424 .filter(Files::isRegularFile)
425 .filter(p -> lastmod(p) > lastmod)
426 .map(Path::toString);
427 } else if (file.isFile()) {
428 if (lastmod(file.toPath()) > lastmod) {
429 if (file.getName().endsWith(".jar")) {
430 try (ZipFile zf = new ZipFile(file)) {
431 return zf.stream()
432 .filter(ze -> !ze.isDirectory())
433 .filter(ze -> ze.getLastModifiedTime().toMillis() > lastmod)
434 .map(ze -> file.toString() + "!" + ze.getName())
435 .collect(Collectors.toList())
436 .stream();
437 }
438 } else {
439 return Stream.of(file.toString());
440 }
441 } else {
442 return Stream.empty();
443 }
444 } else {
445 return Stream.empty();
446 }
447 } catch (IOException e) {
448 throw new IOError(e);
449 }
450 }
451
452
453 public static void writeManifest( Analyzer analyzer, File outputFile, boolean niceManifest,
454 boolean exportScr, File scrLocation, BuildContext buildContext, Log log ) throws Exception
455 {
456 Properties properties = analyzer.getProperties();
457 Jar jar = analyzer.getJar();
458 Manifest manifest = jar.getManifest();
459 if ( outputFile.exists() && properties.containsKey( "Merge-Headers" ) )
460 {
461 Manifest analyzerManifest = manifest;
462 manifest = new Manifest();
463 InputStream inputStream = new FileInputStream( outputFile );
464 try
465 {
466 manifest.read( inputStream );
467 }
468 finally
469 {
470 inputStream.close();
471 }
472 Instructions instructions = new Instructions( ExtList.from( analyzer.getProperty("Merge-Headers") ) );
473 mergeManifest( instructions, manifest, analyzerManifest );
474 }
475 else
476 {
477 File parentFile = outputFile.getParentFile();
478 parentFile.mkdirs();
479 }
480 writeManifest( manifest, outputFile, niceManifest, buildContext, log );
481
482 if (exportScr)
483 {
484 exportScr(analyzer, jar, scrLocation, buildContext, log);
485 }
486 }
487
488
489 public static void writeManifest( Manifest manifest, File outputFile, boolean niceManifest,
490 BuildContext buildContext, Log log ) throws IOException
491 {
492 log.debug("Write manifest to " + outputFile.getPath());
493 outputFile.getParentFile().mkdirs();
494
495 OutputStream os = buildContext.newFileOutputStream( outputFile );
496 try
497 {
498 ManifestWriter.outputManifest( manifest, os, niceManifest );
499 }
500 finally
501 {
502 try
503 {
504 os.close();
505 }
506 catch ( IOException e )
507 {
508
509 }
510 }
511 }
512
513
514
515
516
517 public static String calculateExportsFromContents( Jar bundle )
518 {
519 String ddel = "";
520 StringBuffer sb = new StringBuffer();
521 Map<String, Map<String, Resource>> map = bundle.getDirectories();
522 for ( Iterator<Entry<String, Map<String, Resource>>> i = map.entrySet().iterator(); i.hasNext(); )
523 {
524
525
526
527 Entry<String, Map<String, Resource>> entry = i.next();
528 if ( entry.getValue() == null || entry.getValue().isEmpty() )
529 continue;
530
531 String directory = entry.getKey();
532 if ( directory.equals( "META-INF" ) || directory.startsWith( "META-INF/" ) )
533 continue;
534 if ( directory.equals( "OSGI-OPT" ) || directory.startsWith( "OSGI-OPT/" ) )
535 continue;
536 if ( directory.equals( "/" ) )
537 continue;
538
539 if ( directory.endsWith( "/" ) )
540 directory = directory.substring( 0, directory.length() - 1 );
541
542 directory = directory.replace( '/', '.' );
543 sb.append( ddel );
544 sb.append( directory );
545 ddel = ",";
546 }
547 return sb.toString();
548 }
549 }