]> git.argeo.org Git - cc0/argeo-build.git/blob - Repackage.java
310c6ca186bc974eecf72ba1c9ace6adc5269f86
[cc0/argeo-build.git] / Repackage.java
1 package org.argeo.build;
2
3 import static java.lang.System.Logger.Level.DEBUG;
4 import static java.lang.System.Logger.Level.ERROR;
5 import static java.lang.System.Logger.Level.INFO;
6 import static java.lang.System.Logger.Level.TRACE;
7 import static java.lang.System.Logger.Level.WARNING;
8 import static java.nio.file.FileVisitResult.CONTINUE;
9 import static java.nio.file.StandardOpenOption.APPEND;
10 import static java.nio.file.StandardOpenOption.CREATE;
11 import static java.util.jar.Attributes.Name.MANIFEST_VERSION;
12 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_DO_NOT_MODIFY;
13 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_M2;
14 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_M2_MERGE;
15 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_M2_REPO;
16 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_NO_METADATA_GENERATION;
17 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_SOURCES_URI;
18 import static org.argeo.build.Repackage.ManifestHeader.ARGEO_ORIGIN_URI;
19 import static org.argeo.build.Repackage.ManifestHeader.AUTOMATIC_MODULE_NAME;
20 import static org.argeo.build.Repackage.ManifestHeader.BUNDLE_LICENSE;
21 import static org.argeo.build.Repackage.ManifestHeader.BUNDLE_SYMBOLICNAME;
22 import static org.argeo.build.Repackage.ManifestHeader.BUNDLE_VERSION;
23 import static org.argeo.build.Repackage.ManifestHeader.ECLIPSE_SOURCE_BUNDLE;
24 import static org.argeo.build.Repackage.ManifestHeader.EXPORT_PACKAGE;
25 import static org.argeo.build.Repackage.ManifestHeader.IMPORT_PACKAGE;
26 import static org.argeo.build.Repackage.ManifestHeader.SPDX_LICENSE_IDENTIFIER;
27
28 import java.io.BufferedWriter;
29 import java.io.File;
30 import java.io.FileNotFoundException;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.OutputStream;
34 import java.lang.System.Logger;
35 import java.net.MalformedURLException;
36 import java.net.URL;
37 import java.nio.charset.StandardCharsets;
38 import java.nio.file.DirectoryStream;
39 import java.nio.file.FileSystem;
40 import java.nio.file.FileSystems;
41 import java.nio.file.FileVisitResult;
42 import java.nio.file.Files;
43 import java.nio.file.Path;
44 import java.nio.file.PathMatcher;
45 import java.nio.file.Paths;
46 import java.nio.file.SimpleFileVisitor;
47 import java.nio.file.StandardCopyOption;
48 import java.nio.file.StandardOpenOption;
49 import java.nio.file.attribute.BasicFileAttributes;
50 import java.util.ArrayList;
51 import java.util.HashMap;
52 import java.util.Iterator;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Objects;
56 import java.util.Properties;
57 import java.util.Set;
58 import java.util.StringJoiner;
59 import java.util.TreeMap;
60 import java.util.TreeSet;
61 import java.util.concurrent.CompletableFuture;
62 import java.util.jar.Attributes;
63 import java.util.jar.JarEntry;
64 import java.util.jar.JarInputStream;
65 import java.util.jar.JarOutputStream;
66 import java.util.jar.Manifest;
67 import java.util.zip.Deflater;
68
69 import aQute.bnd.osgi.Analyzer;
70 import aQute.bnd.osgi.Jar;
71
72 /** Repackages existing jar files into OSGi bundles in an A2 repository. */
73 public class Repackage {
74 final static Logger logger = System.getLogger(Repackage.class.getName());
75
76 /**
77 * Environment variable on whether sources should be packaged separately or
78 * integrated in the bundles.
79 */
80 final static String ENV_SOURCE_BUNDLES = "SOURCE_BUNDLES";
81 /** Environment variable on whether operations should be parallelised. */
82 final static String ENV_ARGEO_BUILD_SEQUENTIAL = "ARGEO_BUILD_SEQUENTIAL";
83
84 /** Whether repackaging should run in parallel (default) or sequentially. */
85 final static boolean sequential = Boolean.parseBoolean(System.getenv(ENV_ARGEO_BUILD_SEQUENTIAL));
86
87 /** Main entry point. */
88 public static void main(String[] args) {
89 if (sequential)
90 logger.log(INFO, "Build will be sequential");
91 if (args.length < 2) {
92 System.err.println("Usage: <path to a2 output dir> <category1> <category2> ...");
93 System.exit(1);
94 }
95 Path a2Base = Paths.get(args[0]).toAbsolutePath().normalize();
96 Path descriptorsBase = Paths.get(".").toAbsolutePath().normalize();
97 Repackage factory = new Repackage(a2Base, descriptorsBase);
98
99 List<CompletableFuture<Void>> toDos = new ArrayList<>();
100 for (int i = 1; i < args.length; i++) {
101 Path categoryPath = Paths.get(args[i]);
102 cleanPreviousFailedBuild(categoryPath);
103 if (sequential) // sequential processing happens here
104 factory.processCategory(categoryPath);
105 else
106 toDos.add(CompletableFuture.runAsync(() -> factory.processCategory(categoryPath)));
107 }
108 if (!sequential)// parallel processing
109 CompletableFuture.allOf(toDos.toArray(new CompletableFuture[toDos.size()])).join();
110
111 // Summary
112 StringBuilder sb = new StringBuilder();
113 for (String licenseId : licensesUsed.keySet())
114 for (String name : licensesUsed.get(licenseId))
115 sb.append((licenseId.equals("") ? "Proprietary" : licenseId) + "\t\t" + name + "\n");
116 logger.log(INFO, "# License summary:\n" + sb);
117 }
118
119 /** Deletes remaining sub directories. */
120 static void cleanPreviousFailedBuild(Path categoryPath) {
121 // clean previous failed build
122 try {
123 for (Path subDir : Files.newDirectoryStream(categoryPath, (d) -> Files.isDirectory(d))) {
124 if (Files.exists(subDir)) {
125 logger.log(WARNING, "Bundle dir " + subDir
126 + " already exists, probably from a previous failed build, deleting it...");
127 deleteDirectory(subDir);
128 }
129 }
130 } catch (IOException e) {
131 logger.log(ERROR, "Cannot clean previous build", e);
132 }
133 }
134
135 /** MANIFEST headers. */
136 enum ManifestHeader {
137 // OSGi
138 /** OSGi bundle symbolic name. */
139 BUNDLE_SYMBOLICNAME("Bundle-SymbolicName"), //
140 /** OSGi bundle version. */
141 BUNDLE_VERSION("Bundle-Version"), //
142 /** OSGi bundle license. */
143 BUNDLE_LICENSE("Bundle-License"), //
144 /** OSGi exported packages list. */
145 EXPORT_PACKAGE("Export-Package"), //
146 /** OSGi imported packages list. */
147 IMPORT_PACKAGE("Import-Package"), //
148 /** OSGi path to embedded jar. */
149 BUNDLE_CLASSPATH("Bundle-Classpath"), //
150 // Java
151 /** Java module name. */
152 AUTOMATIC_MODULE_NAME("Automatic-Module-Name"), //
153 // Eclipse
154 /** Eclipse source bundle. */
155 ECLIPSE_SOURCE_BUNDLE("Eclipse-SourceBundle"), //
156 // SPDX
157 /**
158 * SPDX license identifier.
159 *
160 * @see https://spdx.org/licenses/
161 */
162 SPDX_LICENSE_IDENTIFIER("SPDX-License-Identifier"), //
163 // Argeo Origin
164 /**
165 * Maven coordinates of the origin, possibly partial when using common.bnd or
166 * merge.bnd.
167 */
168 ARGEO_ORIGIN_M2("Argeo-Origin-M2"), //
169 /** List of Maven coordinates to merge. */
170 ARGEO_ORIGIN_M2_MERGE("Argeo-Origin-M2-Merge"), //
171 /** Maven repository, if not the default one. */
172 ARGEO_ORIGIN_M2_REPO("Argeo-Origin-M2-Repo"), //
173 /**
174 * Do not perform BND analysis of the origin component. Typically Import_package
175 * and Export-Package will be kept untouched.
176 */
177 ARGEO_ORIGIN_NO_METADATA_GENERATION("Argeo-Origin-NoMetadataGeneration"), //
178 // /**
179 // * Embed the original jar without modifying it (may be required by some
180 // * proprietary licenses, such as JCR Day License).
181 // */
182 // ARGEO_ORIGIN_EMBED("Argeo-Origin-Embed"), //
183 /**
184 * Do not modify original jar (may be required by some proprietary licenses,
185 * such as JCR Day License).
186 */
187 ARGEO_DO_NOT_MODIFY("Argeo-Origin-Do-Not-Modify"), //
188 /**
189 * Origin (non-Maven) URI of the component. It may be anything (jar, archive,
190 * etc.).
191 */
192 ARGEO_ORIGIN_URI("Argeo-Origin-URI"), //
193 /**
194 * Origin (non-Maven) URI of the source of the component. It may be anything
195 * (jar, archive, code repository, etc.).
196 */
197 ARGEO_ORIGIN_SOURCES_URI("Argeo-Origin-Sources-URI"), //
198 ;
199
200 final String headerName;
201
202 private ManifestHeader(String headerName) {
203 this.headerName = headerName;
204 }
205
206 @Override
207 public String toString() {
208 return headerName;
209 }
210
211 /** Get the value from either a {@link Manifest} or a {@link Properties}. */
212 String get(Object map) {
213 if (map instanceof Manifest manifest)
214 return manifest.getMainAttributes().getValue(headerName);
215 else if (map instanceof Properties props)
216 return props.getProperty(headerName);
217 else
218 throw new IllegalArgumentException("Unsupported mapping " + map.getClass());
219 }
220
221 /** Put the value into either a {@link Manifest} or a {@link Properties}. */
222 void put(Object map, String value) {
223 if (map instanceof Manifest manifest)
224 manifest.getMainAttributes().putValue(headerName, value);
225 else if (map instanceof Properties props)
226 props.setProperty(headerName, value);
227 else
228 throw new IllegalArgumentException("Unsupported mapping " + map.getClass());
229 }
230 }
231
232 /** Name of the file centralising information for multiple M2 artifacts. */
233 final static String COMMON_BND = "common.bnd";
234 /** Name of the file centralising information for mergin M2 artifacts. */
235 final static String MERGE_BND = "merge.bnd";
236 /**
237 * Subdirectory of the jar file where origin informations (changes, legal
238 * notices etc. are stored)
239 */
240 final static String ARGEO_ORIGIN = "ARGEO-ORIGIN";
241 /** File detailing modifications to the original component. */
242 final static String CHANGES = ARGEO_ORIGIN + "/changes";
243 /**
244 * Name of the file at the root of the repackaged jar, which prominently
245 * notifies that the component has be repackaged.
246 */
247 final static String README_REPACKAGED = "README.repackaged";
248
249 // cache
250 /** Summary of all license seen during the repackaging. */
251 final static Map<String, Set<String>> licensesUsed = new TreeMap<>();
252
253 /** Directory where to download archives */
254 final Path originBase;
255 /** Directory where to download Maven artifacts */
256 final Path mavenBase;
257
258 /** A2 repository base for binary bundles */
259 final Path a2Base;
260 /** A2 repository base for source bundles */
261 final Path a2SrcBase;
262 /** A2 base for native components */
263 final Path a2LibBase;
264 /** Location of the descriptors driving the packaging */
265 final Path descriptorsBase;
266 /** URIs of archives to download */
267 final Properties uris = new Properties();
268 /** Mirrors for archive download. Key is URI prefix, value list of base URLs */
269 final Map<String, List<String>> mirrors = new HashMap<String, List<String>>();
270
271 /** Whether sources should be packaged separately */
272 final boolean separateSources;
273
274 /** Constructor initialises the various variables */
275 public Repackage(Path a2Base, Path descriptorsBase) {
276 separateSources = Boolean.parseBoolean(System.getenv(ENV_SOURCE_BUNDLES));
277 if (separateSources)
278 logger.log(INFO, "Sources will be packaged separately");
279
280 Objects.requireNonNull(a2Base);
281 Objects.requireNonNull(descriptorsBase);
282 this.originBase = Paths.get(System.getProperty("user.home"), ".cache", "argeo/build/origin");
283 this.mavenBase = Paths.get(System.getProperty("user.home"), ".m2", "repository");
284
285 // TODO define and use a build base
286 this.a2Base = a2Base;
287 this.a2SrcBase = separateSources ? a2Base.getParent().resolve(a2Base.getFileName() + ".src") : a2Base;
288 this.a2LibBase = a2Base.resolve("lib");
289 this.descriptorsBase = descriptorsBase;
290 if (!Files.exists(this.descriptorsBase))
291 throw new IllegalArgumentException(this.descriptorsBase + " does not exist");
292
293 // URIs mapping
294 Path urisPath = this.descriptorsBase.resolve("uris.properties");
295 if (Files.exists(urisPath)) {
296 try (InputStream in = Files.newInputStream(urisPath)) {
297 uris.load(in);
298 } catch (IOException e) {
299 throw new IllegalStateException("Cannot load " + urisPath, e);
300 }
301 }
302
303 // Eclipse mirrors
304 Path eclipseMirrorsPath = this.descriptorsBase.resolve("eclipse.mirrors.txt");
305 List<String> eclipseMirrors = new ArrayList<>();
306 if (Files.exists(eclipseMirrorsPath)) {
307 try {
308 eclipseMirrors = Files.readAllLines(eclipseMirrorsPath, StandardCharsets.UTF_8);
309 } catch (IOException e) {
310 throw new IllegalStateException("Cannot load " + eclipseMirrorsPath, e);
311 }
312 for (Iterator<String> it = eclipseMirrors.iterator(); it.hasNext();) {
313 String value = it.next();
314 if (value.strip().equals(""))
315 it.remove();
316 }
317 }
318 mirrors.put("http://www.eclipse.org/downloads", eclipseMirrors);
319 }
320
321 /*
322 * MAVEN ORIGIN
323 */
324 /** Process a whole category/group id. */
325 void processCategory(Path categoryRelativePath) {
326 try {
327 Path targetCategoryBase = descriptorsBase.resolve(categoryRelativePath);
328 DirectoryStream<Path> bnds = Files.newDirectoryStream(targetCategoryBase,
329 (p) -> p.getFileName().toString().endsWith(".bnd") && !p.getFileName().toString().equals(COMMON_BND)
330 && !p.getFileName().toString().equals(MERGE_BND));
331 for (Path p : bnds) {
332 processSingleM2ArtifactDistributionUnit(p);
333 }
334
335 DirectoryStream<Path> dus = Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p));
336 for (Path duDir : dus) {
337 if (duDir.getFileName().toString().startsWith("eclipse-")) {
338 processEclipseArchive(duDir);
339 } else {
340 processM2BasedDistributionUnit(duDir);
341 }
342 }
343 } catch (IOException e) {
344 throw new RuntimeException("Cannot process category " + categoryRelativePath, e);
345 }
346 }
347
348 /** Process a standalone Maven artifact. */
349 void processSingleM2ArtifactDistributionUnit(Path bndFile) {
350 try {
351 Path categoryRelativePath = descriptorsBase.relativize(bndFile.getParent());
352 Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
353
354 Properties fileProps = new Properties();
355 try (InputStream in = Files.newInputStream(bndFile)) {
356 fileProps.load(in);
357 }
358 // use file name as symbolic name
359 if (!fileProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
360 String symbolicName = bndFile.getFileName().toString();
361 symbolicName = symbolicName.substring(0, symbolicName.length() - ".bnd".length());
362 fileProps.put(BUNDLE_SYMBOLICNAME.toString(), symbolicName);
363 }
364
365 String m2Coordinates = fileProps.getProperty(ARGEO_ORIGIN_M2.toString());
366 if (m2Coordinates == null)
367 throw new IllegalArgumentException("No M2 coordinates available for " + bndFile);
368 M2Artifact artifact = new M2Artifact(m2Coordinates);
369
370 Path downloaded = downloadMaven(fileProps, artifact);
371
372 boolean doNotModify = Boolean
373 .parseBoolean(fileProps.getOrDefault(ARGEO_DO_NOT_MODIFY.toString(), "false").toString());
374 if (doNotModify) {
375 processNotModified(targetCategoryBase, downloaded, fileProps, artifact);
376 return;
377 }
378
379 // regular processing
380 A2Origin origin = new A2Origin();
381 Path bundleDir = processBndJar(downloaded, targetCategoryBase, fileProps, artifact, origin);
382 downloadAndProcessM2Sources(fileProps, artifact, bundleDir, false, false);
383 createJar(bundleDir, origin);
384 } catch (Exception e) {
385 throw new RuntimeException("Cannot process " + bndFile, e);
386 }
387 }
388
389 /**
390 * Process multiple Maven artifacts coming from a same project and therefore
391 * with information in common (typically the version), generating single bundles
392 * or merging them if necessary.
393 *
394 * @see #COMMON_BND
395 * @see #MERGE_BND
396 */
397 void processM2BasedDistributionUnit(Path duDir) {
398 try {
399 Path categoryRelativePath = descriptorsBase.relativize(duDir.getParent());
400 Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
401
402 Path mergeBnd = duDir.resolve(MERGE_BND);
403 if (Files.exists(mergeBnd)) // merge
404 mergeM2Artifacts(mergeBnd);
405
406 Path commonBnd = duDir.resolve(COMMON_BND);
407 if (!Files.exists(commonBnd))
408 return;
409
410 Properties commonProps = new Properties();
411 try (InputStream in = Files.newInputStream(commonBnd)) {
412 commonProps.load(in);
413 }
414
415 String m2Version = commonProps.getProperty(ARGEO_ORIGIN_M2.toString());
416 if (m2Version == null) {
417 logger.log(WARNING, "Ignoring " + duDir + " as it is not an M2-based distribution unit");
418 return;// ignore, this is probably an Eclipse archive
419 }
420 if (!m2Version.startsWith(":")) {
421 throw new IllegalStateException("Only the M2 version can be specified: " + m2Version);
422 }
423 m2Version = m2Version.substring(1);
424
425 DirectoryStream<Path> ds = Files.newDirectoryStream(duDir,
426 (p) -> p.getFileName().toString().endsWith(".bnd") && !p.getFileName().toString().equals(COMMON_BND)
427 && !p.getFileName().toString().equals(MERGE_BND));
428 for (Path p : ds) {
429 Properties fileProps = new Properties();
430 try (InputStream in = Files.newInputStream(p)) {
431 fileProps.load(in);
432 }
433 String m2Coordinates = fileProps.getProperty(ARGEO_ORIGIN_M2.toString());
434 M2Artifact artifact = new M2Artifact(m2Coordinates);
435 if (artifact.getVersion() == null) {
436 artifact.setVersion(m2Version);
437 } else {
438 logger.log(WARNING, p.getFileName() + " : Using version " + artifact.getVersion()
439 + " specified in descriptor rather than " + m2Version + " specified in " + COMMON_BND);
440 }
441
442 // prepare manifest entries
443 Properties mergedProps = new Properties();
444 mergedProps.putAll(commonProps);
445
446 fileEntries: for (Object key : fileProps.keySet()) {
447 if (ARGEO_ORIGIN_M2.toString().equals(key))
448 continue fileEntries;
449 String value = fileProps.getProperty(key.toString());
450 Object previousValue = mergedProps.put(key.toString(), value);
451 if (previousValue != null) {
452 logger.log(WARNING,
453 commonBnd + ": " + key + " was " + previousValue + ", overridden with " + value);
454 }
455 }
456 mergedProps.put(ARGEO_ORIGIN_M2.toString(), artifact.toM2Coordinates());
457 if (!mergedProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
458 // use file name as symbolic name
459 String symbolicName = p.getFileName().toString();
460 symbolicName = symbolicName.substring(0, symbolicName.length() - ".bnd".length());
461 mergedProps.put(BUNDLE_SYMBOLICNAME.toString(), symbolicName);
462 }
463
464 // download
465 Path downloaded = downloadMaven(mergedProps, artifact);
466
467 boolean doNotModify = Boolean
468 .parseBoolean(mergedProps.getOrDefault(ARGEO_DO_NOT_MODIFY.toString(), "false").toString());
469 if (doNotModify) {
470 processNotModified(targetCategoryBase, downloaded, mergedProps, artifact);
471 } else {
472 A2Origin origin = new A2Origin();
473 Path targetBundleDir = processBndJar(downloaded, targetCategoryBase, mergedProps, artifact, origin);
474 downloadAndProcessM2Sources(mergedProps, artifact, targetBundleDir, false, false);
475 createJar(targetBundleDir, origin);
476 }
477 }
478 } catch (IOException e) {
479 throw new RuntimeException("Cannot process " + duDir, e);
480 }
481 }
482
483 /** Merge multiple Maven artifacts. */
484 void mergeM2Artifacts(Path mergeBnd) throws IOException {
485 Path duDir = mergeBnd.getParent();
486 String category = duDir.getParent().getFileName().toString();
487 Path targetCategoryBase = a2Base.resolve(category);
488
489 Properties mergeProps = new Properties();
490 // first, load common properties
491 Path commonBnd = duDir.resolve(COMMON_BND);
492 if (Files.exists(commonBnd))
493 try (InputStream in = Files.newInputStream(commonBnd)) {
494 mergeProps.load(in);
495 }
496 // then, the merge properties themselves
497 try (InputStream in = Files.newInputStream(mergeBnd)) {
498 mergeProps.load(in);
499 }
500
501 String m2Version = mergeProps.getProperty(ARGEO_ORIGIN_M2.toString());
502 if (m2Version == null) {
503 logger.log(WARNING, "Ignoring merging in " + duDir + " as it is not an M2-based distribution unit");
504 return;// ignore, this is probably an Eclipse archive
505 }
506 if (!m2Version.startsWith(":")) {
507 throw new IllegalStateException("Only the M2 version can be specified: " + m2Version);
508 }
509 m2Version = m2Version.substring(1);
510 mergeProps.put(BUNDLE_VERSION.toString(), m2Version);
511
512 String artifactsStr = mergeProps.getProperty(ARGEO_ORIGIN_M2_MERGE.toString());
513 if (artifactsStr == null)
514 throw new IllegalArgumentException(mergeBnd + ": " + ARGEO_ORIGIN_M2_MERGE + " must be set");
515
516 String bundleSymbolicName = mergeProps.getProperty(BUNDLE_SYMBOLICNAME.toString());
517 if (bundleSymbolicName == null)
518 throw new IllegalArgumentException("Bundle-SymbolicName must be set in " + mergeBnd);
519 CategoryNameVersion nameVersion = new M2Artifact(category + ":" + bundleSymbolicName + ":" + m2Version);
520
521 A2Origin origin = new A2Origin();
522 Path bundleDir = targetCategoryBase.resolve(bundleSymbolicName + "." + nameVersion.getBranch());
523
524 StringJoiner originDesc = new StringJoiner(",");
525 String[] artifacts = artifactsStr.split(",");
526 artifacts: for (String str : artifacts) {
527 String m2Coordinates = str.trim();
528 if ("".equals(m2Coordinates))
529 continue artifacts;
530 M2Artifact artifact = new M2Artifact(m2Coordinates.trim());
531 if (artifact.getVersion() == null)
532 artifact.setVersion(m2Version);
533 originDesc.add(artifact.toString());
534 Path downloaded = downloadMaven(mergeProps, artifact);
535 JarEntry entry;
536 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(downloaded), false)) {
537 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
538 if (entry.isDirectory())
539 continue entries;
540 if (entry.getName().endsWith(".RSA") || entry.getName().endsWith(".DSA")
541 || entry.getName().endsWith(".SF")) {
542 origin.deleted.add("cryptographic signatures from " + artifact);
543 continue entries;
544 }
545 if (entry.getName().endsWith("module-info.class")) { // skip Java 9 module info
546 origin.deleted.add("Java module information (module-info.class) from " + artifact);
547 continue entries;
548 }
549 if (entry.getName().startsWith("META-INF/versions/")) { // skip multi-version
550 origin.deleted.add("additional Java versions (META-INF/versions) from " + artifact);
551 continue entries;
552 }
553 if (entry.getName().startsWith("META-INF/maven/")) {
554 origin.deleted.add("Maven information (META-INF/maven) from " + artifact);
555 continue entries;
556 }
557 if (entry.getName().startsWith(".cache/")) { // Apache SSHD
558 origin.deleted.add("cache directory (.cache) from " + artifact);
559 continue entries;
560 }
561 if (entry.getName().equals("META-INF/DEPENDENCIES")) {
562 origin.deleted.add("Dependencies (META-INF/DEPENDENCIES) from " + artifact);
563 continue entries;
564 }
565 if (entry.getName().equals("META-INF/MANIFEST.MF")) {
566 Path originalManifest = bundleDir.resolve(ARGEO_ORIGIN).resolve(artifact.getGroupId())
567 .resolve(artifact.getArtifactId()).resolve("MANIFEST.MF");
568 Files.createDirectories(originalManifest.getParent());
569 try (OutputStream out = Files.newOutputStream(originalManifest)) {
570 Files.copy(jarIn, originalManifest);
571 }
572 origin.added.add(
573 "original MANIFEST (" + bundleDir.relativize(originalManifest) + ") from " + artifact);
574 continue entries;
575 }
576
577 if (entry.getName().endsWith("NOTICE") || entry.getName().endsWith("NOTICE.txt")
578 || entry.getName().endsWith("LICENSE") || entry.getName().endsWith("LICENSE.md")
579 || entry.getName().endsWith("LICENSE-notice.md") || entry.getName().endsWith("COPYING")
580 || entry.getName().endsWith("COPYING.LESSER")) {
581 Path artifactOriginDir = bundleDir.resolve(ARGEO_ORIGIN).resolve(artifact.getGroupId())
582 .resolve(artifact.getArtifactId());
583 Path target = artifactOriginDir.resolve(entry.getName());
584 Files.createDirectories(target.getParent());
585 Files.copy(jarIn, target);
586 origin.moved.add(entry.getName() + " in " + artifact + " to " + bundleDir.relativize(target));
587 continue entries;
588 }
589 Path target = bundleDir.resolve(entry.getName());
590 Files.createDirectories(target.getParent());
591 if (!Files.exists(target)) {
592 Files.copy(jarIn, target);
593 } else {
594 if (entry.getName().startsWith("META-INF/services/")) {
595 try (OutputStream out = Files.newOutputStream(target, StandardOpenOption.APPEND)) {
596 out.write("\n".getBytes());
597 jarIn.transferTo(out);
598 logger.log(DEBUG, artifact.getArtifactId() + " - Appended " + entry.getName());
599 }
600 origin.modified.add(entry.getName() + ", merging from " + artifact);
601 } else if (entry.getName().startsWith("org/apache/batik/")) {
602 logger.log(TRACE, "Skip " + entry.getName());
603 continue entries;
604 } else if (entry.getName().startsWith("META-INF/NOTICE")) {
605 logger.log(WARNING, "Skip " + entry.getName() + " from " + artifact);
606 // TODO merge them?
607 continue entries;
608 } else {
609 throw new IllegalStateException("File " + target + " from " + artifact + " already exists");
610 }
611 }
612 logger.log(TRACE, () -> "Copied " + target);
613 }
614 }
615 origin.added.add("binary content of " + artifact);
616
617 // process sources
618 downloadAndProcessM2Sources(mergeProps, artifact, bundleDir, true, false);
619 }
620
621 // additional service files
622 Path servicesDir = duDir.resolve("services");
623 if (Files.exists(servicesDir)) {
624 for (Path p : Files.newDirectoryStream(servicesDir)) {
625 Path target = bundleDir.resolve("META-INF/services/").resolve(p.getFileName());
626 try (InputStream in = Files.newInputStream(p);
627 OutputStream out = Files.newOutputStream(target, StandardOpenOption.APPEND);) {
628 out.write("\n".getBytes());
629 in.transferTo(out);
630 logger.log(DEBUG, "Appended " + p);
631 }
632 origin.added.add(bundleDir.relativize(target).toString());
633 }
634 }
635
636 // BND analysis
637 Map<String, String> entries = new TreeMap<>();
638 try (Analyzer bndAnalyzer = new Analyzer()) {
639 bndAnalyzer.setProperties(mergeProps);
640 Jar jar = new Jar(bundleDir.toFile());
641 bndAnalyzer.setJar(jar);
642 Manifest manifest = bndAnalyzer.calcManifest();
643
644 keys: for (Object key : manifest.getMainAttributes().keySet()) {
645 Object value = manifest.getMainAttributes().get(key);
646
647 switch (key.toString()) {
648 case "Tool":
649 case "Bnd-LastModified":
650 case "Created-By":
651 continue keys;
652 }
653 if ("Require-Capability".equals(key.toString())
654 && value.toString().equals("osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.1))\"")) {
655 origin.deleted.add("MANIFEST header " + key);
656 continue keys;// hack for very old classes
657 }
658 entries.put(key.toString(), value.toString());
659 }
660 } catch (Exception e) {
661 throw new RuntimeException("Cannot process " + mergeBnd, e);
662 }
663
664 Manifest manifest = new Manifest();
665 Path manifestPath = bundleDir.resolve("META-INF/MANIFEST.MF");
666 Files.createDirectories(manifestPath.getParent());
667 for (String key : entries.keySet()) {
668 String value = entries.get(key);
669 manifest.getMainAttributes().putValue(key, value);
670 }
671 manifest.getMainAttributes().putValue(ARGEO_ORIGIN_M2.toString(), originDesc.toString());
672
673 processLicense(bundleDir, manifest);
674
675 // write MANIFEST
676 try (OutputStream out = Files.newOutputStream(manifestPath)) {
677 manifest.write(out);
678 }
679 createJar(bundleDir, origin);
680 }
681
682 /** Generates MANIFEST using BND. */
683 Path processBndJar(Path downloaded, Path targetCategoryBase, Properties fileProps, M2Artifact artifact,
684 A2Origin origin) {
685 try {
686 Map<String, String> additionalEntries = new TreeMap<>();
687 boolean doNotModifyManifest = Boolean.parseBoolean(
688 fileProps.getOrDefault(ARGEO_ORIGIN_NO_METADATA_GENERATION.toString(), "false").toString());
689
690 // Note: we always force the symbolic name
691 if (doNotModifyManifest) {
692 for (Object key : fileProps.keySet()) {
693 String value = fileProps.getProperty(key.toString());
694 additionalEntries.put(key.toString(), value);
695 }
696 } else {
697 if (artifact != null) {
698 if (!fileProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
699 fileProps.put(BUNDLE_SYMBOLICNAME.toString(), artifact.getName());
700 }
701 if (!fileProps.containsKey(BUNDLE_VERSION.toString())) {
702 fileProps.put(BUNDLE_VERSION.toString(), artifact.getVersion());
703 }
704 }
705
706 if (!fileProps.containsKey(EXPORT_PACKAGE.toString())) {
707 fileProps.put(EXPORT_PACKAGE.toString(), "*");
708 }
709
710 // BND analysis
711 try (Analyzer bndAnalyzer = new Analyzer()) {
712 bndAnalyzer.setProperties(fileProps);
713 Jar jar = new Jar(downloaded.toFile());
714 bndAnalyzer.setJar(jar);
715 Manifest manifest = bndAnalyzer.calcManifest();
716
717 keys: for (Object key : manifest.getMainAttributes().keySet()) {
718 Object value = manifest.getMainAttributes().get(key);
719
720 switch (key.toString()) {
721 case "Tool":
722 case "Bnd-LastModified":
723 case "Created-By":
724 continue keys;
725 }
726 if ("Require-Capability".equals(key.toString())
727 && value.toString().equals("osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.1))\"")) {
728 origin.deleted.add("MANIFEST header " + key);
729 continue keys;// !! hack for very old classes
730 }
731 additionalEntries.put(key.toString(), value.toString());
732 }
733 }
734 }
735 Path targetBundleDir = processBundleJar(downloaded, targetCategoryBase, additionalEntries, origin);
736 logger.log(DEBUG, () -> "Processed " + downloaded);
737 return targetBundleDir;
738 } catch (Exception e) {
739 throw new RuntimeException("Cannot BND process " + downloaded, e);
740 }
741
742 }
743
744 /** Process an artifact that should not be modified. */
745 void processNotModified(Path targetCategoryBase, Path downloaded, Properties fileProps, M2Artifact artifact)
746 throws IOException {
747 // Some proprietary or signed artifacts do not allow any modification
748 // When releasing (with separate sources), we just copy it
749 Path unmodifiedTarget = targetCategoryBase
750 .resolve(fileProps.getProperty(BUNDLE_SYMBOLICNAME.toString()) + "." + artifact.getBranch() + ".jar");
751 Files.createDirectories(unmodifiedTarget.getParent());
752 Files.copy(downloaded, unmodifiedTarget, StandardCopyOption.REPLACE_EXISTING);
753 Path bundleDir = targetCategoryBase
754 .resolve(fileProps.getProperty(BUNDLE_SYMBOLICNAME.toString()) + "." + artifact.getBranch());
755 downloadAndProcessM2Sources(fileProps, artifact, bundleDir, false, true);
756 Manifest manifest;
757 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(unmodifiedTarget))) {
758 manifest = jarIn.getManifest();
759 }
760 createSourceJar(bundleDir, manifest, fileProps);
761 }
762
763 /** Download and integrates sources for a single Maven artifact. */
764 void downloadAndProcessM2Sources(Properties props, M2Artifact artifact, Path targetBundleDir, boolean merging,
765 boolean unmodified) throws IOException {
766 try {
767 String repoStr = props.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
768 ? props.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
769 : null;
770 String alternateUri = props.getProperty(ARGEO_ORIGIN_SOURCES_URI.toString());
771 M2Artifact sourcesArtifact = new M2Artifact(artifact.toM2Coordinates(), "sources");
772 URL sourcesUrl = alternateUri != null ? new URL(alternateUri)
773 : M2ConventionsUtils.mavenRepoUrl(repoStr, sourcesArtifact);
774 Path sourcesDownloaded = downloadMaven(sourcesUrl, sourcesArtifact);
775 processM2SourceJar(sourcesDownloaded, targetBundleDir, merging ? artifact : null, unmodified);
776 logger.log(TRACE, () -> "Processed source " + sourcesDownloaded);
777 } catch (Exception e) {
778 logger.log(ERROR, () -> "Cannot download source for " + artifact);
779 }
780
781 }
782
783 /** Integrate sources from a downloaded jar file. */
784 void processM2SourceJar(Path file, Path bundleDir, M2Artifact mergingFrom, boolean unmodified) throws IOException {
785 A2Origin origin = new A2Origin();
786 Path sourceDir = separateSources || unmodified ? bundleDir.getParent().resolve(bundleDir.toString() + ".src")
787 : bundleDir.resolve("OSGI-OPT/src");
788 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
789
790 String mergingMsg = "";
791 if (mergingFrom != null)
792 mergingMsg = " of " + mergingFrom;
793
794 Files.createDirectories(sourceDir);
795 JarEntry entry;
796 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
797 String relPath = entry.getName();
798 if (entry.isDirectory())
799 continue entries;
800 if (entry.getName().equals("META-INF/MANIFEST.MF")) {// skip META-INF entries
801 origin.deleted.add("MANIFEST.MF from the sources" + mergingMsg);
802 continue entries;
803 }
804 if (!unmodified) {
805 if (entry.getName().startsWith("module-info.java")) {// skip Java module information
806 origin.deleted.add("Java module information from the sources (module-info.java)" + mergingMsg);
807 continue entries;
808 }
809 if (entry.getName().startsWith("/")) { // absolute paths
810 int metaInfIndex = entry.getName().indexOf("META-INF");
811 if (metaInfIndex >= 0) {
812 relPath = entry.getName().substring(metaInfIndex);
813 origin.moved.add(" to " + relPath + " entry with absolute path " + entry.getName());
814 } else {
815 logger.log(WARNING, entry.getName() + " has an absolute path");
816 origin.deleted.add(entry.getName() + " from the sources" + mergingMsg);
817 }
818 continue entries;
819 }
820 }
821 Path target = sourceDir.resolve(relPath);
822 Files.createDirectories(target.getParent());
823 if (!Files.exists(target)) {
824 Files.copy(jarIn, target);
825 logger.log(TRACE, () -> "Copied source " + target);
826 } else {
827 logger.log(TRACE, () -> target + " already exists, skipping...");
828 }
829 }
830 }
831 // write the changes
832 if (separateSources || unmodified) {
833 origin.appendChanges(sourceDir);
834 } else {
835 origin.added.add("source code under OSGI-OPT/src");
836 origin.appendChanges(bundleDir);
837 }
838 }
839
840 /** Download a Maven artifact. */
841 Path downloadMaven(Properties props, M2Artifact artifact) throws IOException {
842 String repoStr = props.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
843 ? props.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
844 : null;
845 String alternateUri = props.getProperty(ARGEO_ORIGIN_URI.toString());
846 URL url = alternateUri != null ? new URL(alternateUri) : M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
847 return downloadMaven(url, artifact);
848 }
849
850 /** Download a Maven artifact. */
851 Path downloadMaven(URL url, M2Artifact artifact) throws IOException {
852 return download(url, mavenBase, M2ConventionsUtils.artifactPath("", artifact));
853 }
854
855 /*
856 * ECLIPSE ORIGIN
857 */
858 /** Process an archive in Eclipse format. */
859 void processEclipseArchive(Path duDir) {
860 try {
861 Path categoryRelativePath = descriptorsBase.relativize(duDir.getParent());
862 Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
863 Files.createDirectories(targetCategoryBase);
864 // first delete all directories from previous builds
865 for (Path dir : Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p)))
866 deleteDirectory(dir);
867
868 Files.createDirectories(originBase);
869
870 Path commonBnd = duDir.resolve(COMMON_BND);
871 Properties commonProps = new Properties();
872 try (InputStream in = Files.newInputStream(commonBnd)) {
873 commonProps.load(in);
874 }
875 String url = commonProps.getProperty(ARGEO_ORIGIN_URI.toString());
876 if (url == null) {
877 url = uris.getProperty(duDir.getFileName().toString());
878 if (url == null)
879 throw new IllegalStateException("No url available for " + duDir);
880 commonProps.put(ARGEO_ORIGIN_URI.toString(), url);
881 }
882 Path downloaded = tryDownloadArchive(url, originBase);
883
884 FileSystem zipFs = FileSystems.newFileSystem(downloaded, (ClassLoader) null);
885
886 // filters
887 List<PathMatcher> includeMatchers = new ArrayList<>();
888 Properties includes = new Properties();
889 try (InputStream in = Files.newInputStream(duDir.resolve("includes.properties"))) {
890 includes.load(in);
891 }
892 for (Object pattern : includes.keySet()) {
893 PathMatcher pathMatcher = zipFs.getPathMatcher("glob:/" + pattern);
894 includeMatchers.add(pathMatcher);
895 }
896
897 List<PathMatcher> excludeMatchers = new ArrayList<>();
898 Path excludeFile = duDir.resolve("excludes.properties");
899 if (Files.exists(excludeFile)) {
900 Properties excludes = new Properties();
901 try (InputStream in = Files.newInputStream(excludeFile)) {
902 excludes.load(in);
903 }
904 for (Object pattern : excludes.keySet()) {
905 PathMatcher pathMatcher = zipFs.getPathMatcher("glob:/" + pattern);
906 excludeMatchers.add(pathMatcher);
907 }
908 }
909
910 // keys are the bundle directories
911 Map<Path, A2Origin> origins = new HashMap<>();
912 Files.walkFileTree(zipFs.getRootDirectories().iterator().next(), new SimpleFileVisitor<Path>() {
913
914 @Override
915 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
916 includeMatchers: for (PathMatcher includeMatcher : includeMatchers) {
917 if (includeMatcher.matches(file)) {
918 for (PathMatcher excludeMatcher : excludeMatchers) {
919 if (excludeMatcher.matches(file)) {
920 logger.log(TRACE, "Skipping excluded " + file);
921 return FileVisitResult.CONTINUE;
922 }
923 }
924 if (file.getFileName().toString().contains(".source_")) {
925 processEclipseSourceJar(file, targetCategoryBase);
926 logger.log(DEBUG, () -> "Processed source " + file);
927 } else {
928 Map<String, String> map = new HashMap<>();
929 for (Object key : commonProps.keySet())
930 map.put(key.toString(), commonProps.getProperty(key.toString()));
931 A2Origin origin = new A2Origin();
932 Path bundleDir = processBundleJar(file, targetCategoryBase, map, origin);
933 origins.put(bundleDir, origin);
934 logger.log(DEBUG, () -> "Processed " + file);
935 }
936 break includeMatchers;
937 }
938 }
939 return FileVisitResult.CONTINUE;
940 }
941 });
942
943 DirectoryStream<Path> dirs = Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p)
944 && p.getFileName().toString().indexOf('.') >= 0 && !p.getFileName().toString().endsWith(".src"));
945 for (Path bundleDir : dirs) {
946 A2Origin origin = origins.get(bundleDir);
947 Objects.requireNonNull(origin, "No A2 origin found for " + bundleDir);
948 createJar(bundleDir, origin);
949 }
950 } catch (IOException e) {
951 throw new RuntimeException("Cannot process " + duDir, e);
952 }
953
954 }
955
956 /** Process sources in Eclipse format. */
957 void processEclipseSourceJar(Path file, Path targetBase) throws IOException {
958 try {
959 A2Origin origin = new A2Origin();
960 Path bundleDir;
961 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
962 Manifest manifest = jarIn.getManifest();
963
964 String[] relatedBundle = manifest.getMainAttributes().getValue(ECLIPSE_SOURCE_BUNDLE.toString())
965 .split(";");
966 String version = relatedBundle[1].substring("version=\"".length());
967 version = version.substring(0, version.length() - 1);
968 NameVersion nameVersion = new NameVersion(relatedBundle[0], version);
969 bundleDir = targetBase.resolve(nameVersion.getName() + "." + nameVersion.getBranch());
970
971 Path sourceDir = separateSources ? bundleDir.getParent().resolve(bundleDir.toString() + ".src")
972 : bundleDir.resolve("OSGI-OPT/src");
973
974 Files.createDirectories(sourceDir);
975 JarEntry entry;
976 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
977 if (entry.isDirectory())
978 continue entries;
979 if (entry.getName().startsWith("META-INF"))// skip META-INF entries
980 continue entries;
981 Path target = sourceDir.resolve(entry.getName());
982 Files.createDirectories(target.getParent());
983 Files.copy(jarIn, target);
984 logger.log(TRACE, () -> "Copied source " + target);
985 }
986
987 // write the changes
988 if (separateSources) {
989 origin.appendChanges(sourceDir);
990 } else {
991 origin.added.add("source code under OSGI-OPT/src");
992 origin.appendChanges(bundleDir);
993 }
994 }
995 } catch (IOException e) {
996 throw new IllegalStateException("Cannot process " + file, e);
997 }
998 }
999
1000 /*
1001 * COMMON PROCESSING
1002 */
1003 /** Normalise a single (that is, non-merged) bundle. */
1004 Path processBundleJar(Path file, Path targetBase, Map<String, String> entries, A2Origin origin) throws IOException {
1005 // boolean embed = Boolean.parseBoolean(entries.getOrDefault(ARGEO_ORIGIN_EMBED.toString(), "false").toString());
1006 boolean doNotModify = Boolean
1007 .parseBoolean(entries.getOrDefault(ManifestHeader.ARGEO_DO_NOT_MODIFY.toString(), "false").toString());
1008 NameVersion nameVersion;
1009 Path bundleDir;
1010 // singleton
1011 boolean isSingleton = false;
1012 Manifest manifest;
1013 Manifest sourceManifest;
1014 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
1015 sourceManifest = jarIn.getManifest();
1016 manifest = sourceManifest != null ? new Manifest(sourceManifest) : new Manifest();
1017
1018 String rawSourceSymbolicName = manifest.getMainAttributes().getValue(BUNDLE_SYMBOLICNAME.toString());
1019 if (rawSourceSymbolicName != null) {
1020 // make sure there is no directive
1021 String[] arr = rawSourceSymbolicName.split(";");
1022 for (int i = 1; i < arr.length; i++) {
1023 if (arr[i].trim().equals("singleton:=true"))
1024 isSingleton = true;
1025 logger.log(DEBUG, file.getFileName() + " is a singleton");
1026 }
1027 }
1028 // remove problematic entries in MANIFEST
1029 manifest.getEntries().clear();
1030
1031 String ourSymbolicName = entries.get(BUNDLE_SYMBOLICNAME.toString());
1032 String ourVersion = entries.get(BUNDLE_VERSION.toString());
1033
1034 if (ourSymbolicName != null && ourVersion != null) {
1035 nameVersion = new NameVersion(ourSymbolicName, ourVersion);
1036 } else {
1037 nameVersion = nameVersionFromManifest(manifest);
1038 if (ourVersion != null && !nameVersion.getVersion().equals(ourVersion)) {
1039 logger.log(WARNING,
1040 "Original version is " + nameVersion.getVersion() + " while new version is " + ourVersion);
1041 entries.put(BUNDLE_VERSION.toString(), ourVersion);
1042 }
1043 if (ourSymbolicName != null) {
1044 // we always force our symbolic name
1045 nameVersion.setName(ourSymbolicName);
1046 }
1047 }
1048
1049 bundleDir = targetBase.resolve(nameVersion.getName() + "." + nameVersion.getBranch());
1050
1051 // copy original MANIFEST
1052 if (sourceManifest != null) {
1053 Path originalManifest = bundleDir.resolve(ARGEO_ORIGIN).resolve("MANIFEST.MF");
1054 Files.createDirectories(originalManifest.getParent());
1055 try (OutputStream out = Files.newOutputStream(originalManifest)) {
1056 sourceManifest.write(out);
1057 }
1058 origin.moved.add("original MANIFEST to " + bundleDir.relativize(originalManifest));
1059 }
1060
1061 // force Java 9 module name
1062 entries.put(ManifestHeader.AUTOMATIC_MODULE_NAME.toString(), nameVersion.getName());
1063
1064 boolean isNative = false;
1065 String os = null;
1066 String arch = null;
1067 if (bundleDir.startsWith(a2LibBase)) {
1068 isNative = true;
1069 Path libRelativePath = a2LibBase.relativize(bundleDir);
1070 os = libRelativePath.getName(0).toString();
1071 arch = libRelativePath.getName(1).toString();
1072 }
1073
1074 // copy entries
1075 JarEntry entry;
1076 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
1077 if (entry.isDirectory())
1078 continue entries;
1079 if (!doNotModify) {
1080 if (entry.getName().endsWith(".RSA") || entry.getName().endsWith(".DSA")
1081 || entry.getName().endsWith(".SF")) {
1082 origin.deleted.add("cryptographic signatures");
1083 continue entries;
1084 }
1085 if (entry.getName().endsWith("module-info.class")) { // skip Java 9 module info
1086 origin.deleted.add("Java module information (module-info.class)");
1087 continue entries;
1088 }
1089 if (entry.getName().startsWith("META-INF/versions/")) { // skip multi-version
1090 origin.deleted.add("additional Java versions (META-INF/versions)");
1091 continue entries;
1092 }
1093 if (entry.getName().startsWith("META-INF/maven/")) {
1094 origin.deleted.add("Maven information (META-INF/maven)");
1095 continue entries;
1096 }
1097 // skip file system providers as they cause issues with native image
1098 if (entry.getName().startsWith("META-INF/services/java.nio.file.spi.FileSystemProvider")) {
1099 origin.deleted
1100 .add("file system providers (META-INF/services/java.nio.file.spi.FileSystemProvider)");
1101 continue entries;
1102 }
1103 }
1104 if (entry.getName().startsWith("OSGI-OPT/src/")) { // skip embedded sources
1105 origin.deleted.add("embedded sources");
1106 continue entries;
1107 }
1108 Path target = bundleDir.resolve(entry.getName());
1109 Files.createDirectories(target.getParent());
1110 Files.copy(jarIn, target);
1111
1112 // native libraries
1113 if (isNative && (entry.getName().endsWith(".so") || entry.getName().endsWith(".dll")
1114 || entry.getName().endsWith(".jnilib"))) {
1115 Path categoryDir = bundleDir.getParent();
1116 boolean copyDll = false;
1117 Path targetDll = categoryDir.resolve(bundleDir.relativize(target));
1118 if (nameVersion.getName().equals("com.sun.jna")) {
1119 if (arch.equals("x86_64"))
1120 arch = "x86-64";
1121 if (os.equals("macosx"))
1122 os = "darwin";
1123 if (target.getParent().getFileName().toString().equals(os + "-" + arch)) {
1124 copyDll = true;
1125 }
1126 targetDll = categoryDir.resolve(target.getFileName());
1127 } else {
1128 copyDll = true;
1129 }
1130 if (copyDll) {
1131 Files.createDirectories(targetDll.getParent());
1132 if (Files.exists(targetDll))
1133 Files.delete(targetDll);
1134 Files.copy(target, targetDll);
1135 }
1136 Files.delete(target);
1137 origin.deleted.add(bundleDir.relativize(target).toString());
1138 }
1139 logger.log(TRACE, () -> "Copied " + target);
1140 }
1141 }
1142
1143 // copy MANIFEST
1144 Path manifestPath = bundleDir.resolve("META-INF/MANIFEST.MF");
1145 Files.createDirectories(manifestPath.getParent());
1146
1147 if (isSingleton && entries.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
1148 entries.put(BUNDLE_SYMBOLICNAME.toString(),
1149 entries.get(BUNDLE_SYMBOLICNAME.toString()) + ";singleton:=true");
1150 }
1151
1152 // Final MANIFEST decisions
1153 // We also check the original OSGi metadata and compare with our changes
1154 for (String key : entries.keySet()) {
1155 String value = entries.get(key);
1156 String previousValue = manifest.getMainAttributes().getValue(key);
1157 boolean wasDifferent = previousValue != null && !previousValue.equals(value);
1158 boolean keepPrevious = false;
1159 if (wasDifferent) {
1160 if (SPDX_LICENSE_IDENTIFIER.toString().equals(key) && previousValue != null)
1161 keepPrevious = true;
1162 else if (BUNDLE_VERSION.toString().equals(key) && wasDifferent)
1163 if (previousValue.equals(value + ".0")) // typically a Maven first release
1164 keepPrevious = true;
1165
1166 if (keepPrevious) {
1167 if (logger.isLoggable(DEBUG))
1168 logger.log(DEBUG, file.getFileName() + ": " + key + " was NOT modified, value kept is "
1169 + previousValue + ", not overriden with " + value);
1170 value = previousValue;
1171 }
1172 }
1173
1174 manifest.getMainAttributes().putValue(key, value);
1175 if (wasDifferent && !keepPrevious) {
1176 if (IMPORT_PACKAGE.toString().equals(key) || EXPORT_PACKAGE.toString().equals(key))
1177 logger.log(TRACE, () -> file.getFileName() + ": " + key + " was modified");
1178 else if (BUNDLE_SYMBOLICNAME.toString().equals(key) || AUTOMATIC_MODULE_NAME.toString().equals(key))
1179 logger.log(DEBUG,
1180 file.getFileName() + ": " + key + " was " + previousValue + ", overridden with " + value);
1181 else
1182 logger.log(WARNING,
1183 file.getFileName() + ": " + key + " was " + previousValue + ", overridden with " + value);
1184 origin.modified.add("MANIFEST header " + key);
1185 }
1186
1187 // !! hack to remove unresolvable
1188 if (key.equals("Provide-Capability") || key.equals("Require-Capability"))
1189 if (nameVersion.getName().equals("osgi.core") || nameVersion.getName().equals("osgi.cmpn")) {
1190 manifest.getMainAttributes().remove(key);
1191 origin.deleted.add("MANIFEST header " + key);
1192 }
1193 }
1194
1195 // de-pollute MANIFEST
1196 for (Iterator<Map.Entry<Object, Object>> manifestEntries = manifest.getMainAttributes().entrySet()
1197 .iterator(); manifestEntries.hasNext();) {
1198 Map.Entry<Object, Object> manifestEntry = manifestEntries.next();
1199 switch (manifestEntry.getKey().toString()) {
1200 case "Archiver-Version":
1201 case "Build-By":
1202 case "Created-By":
1203 case "Originally-Created-By":
1204 case "Tool":
1205 case "Bnd-LastModified":
1206 manifestEntries.remove();
1207 origin.deleted.add("MANIFEST header " + manifestEntry.getKey());
1208 break;
1209 default:
1210 if (sourceManifest != null && !sourceManifest.getMainAttributes().containsKey(manifestEntry.getKey()))
1211 origin.added.add("MANIFEST header " + manifestEntry.getKey());
1212 }
1213 }
1214
1215 processLicense(bundleDir, manifest);
1216
1217 origin.modified.add("MANIFEST (META-INF/MANIFEST.MF)");
1218 // write the MANIFEST
1219 try (OutputStream out = Files.newOutputStream(manifestPath)) {
1220 manifest.write(out);
1221 }
1222 return bundleDir;
1223 }
1224
1225 /** Process SPDX license identifier. */
1226 void processLicense(Path bundleDir, Manifest manifest) {
1227 String spdxLicenceId = manifest.getMainAttributes().getValue(SPDX_LICENSE_IDENTIFIER.toString());
1228 String bundleLicense = manifest.getMainAttributes().getValue(BUNDLE_LICENSE.toString());
1229 if (spdxLicenceId == null) {
1230 logger.log(ERROR, bundleDir.getFileName() + ": " + SPDX_LICENSE_IDENTIFIER + " not available, "
1231 + BUNDLE_LICENSE + " is " + bundleLicense);
1232 } else {
1233 // only use the first licensing option
1234 int orIndex = spdxLicenceId.indexOf(" OR ");
1235 if (orIndex >= 0)
1236 spdxLicenceId = spdxLicenceId.substring(0, orIndex).trim();
1237
1238 String bundleDirName = bundleDir.getFileName().toString();
1239 // force licenses of some well-known components
1240 // even if we say otherwise (typically because from an Eclipse archive)
1241 if (bundleDirName.startsWith("org.apache."))
1242 spdxLicenceId = "Apache-2.0";
1243 if (bundleDirName.startsWith("com.sun.jna."))
1244 spdxLicenceId = "Apache-2.0";
1245 if (bundleDirName.startsWith("com.ibm.icu."))
1246 spdxLicenceId = "ICU";
1247 if (bundleDirName.startsWith("javax.annotation."))
1248 spdxLicenceId = "GPL-2.0-only WITH Classpath-exception-2.0";
1249 if (bundleDirName.startsWith("javax.inject."))
1250 spdxLicenceId = "Apache-2.0";
1251 if (bundleDirName.startsWith("org.osgi."))
1252 spdxLicenceId = "Apache-2.0";
1253
1254 manifest.getMainAttributes().putValue(SPDX_LICENSE_IDENTIFIER.toString(), spdxLicenceId);
1255 if (!licensesUsed.containsKey(spdxLicenceId))
1256 licensesUsed.put(spdxLicenceId, new TreeSet<>());
1257 licensesUsed.get(spdxLicenceId).add(bundleDir.getParent().getFileName() + "/" + bundleDir.getFileName());
1258 }
1259 }
1260
1261 /*
1262 * UTILITIES
1263 */
1264 /** Recursively deletes a directory. */
1265 static void deleteDirectory(Path path) throws IOException {
1266 if (!Files.exists(path))
1267 return;
1268 Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
1269 @Override
1270 public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
1271 if (e != null)
1272 throw e;
1273 Files.delete(directory);
1274 return CONTINUE;
1275 }
1276
1277 @Override
1278 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1279 Files.delete(file);
1280 return CONTINUE;
1281 }
1282 });
1283 }
1284
1285 /** Extract name/version from a MANIFEST. */
1286 NameVersion nameVersionFromManifest(Manifest manifest) {
1287 Attributes attrs = manifest.getMainAttributes();
1288 // symbolic name
1289 String symbolicName = attrs.getValue(ManifestHeader.BUNDLE_SYMBOLICNAME.toString());
1290 if (symbolicName == null)
1291 return null;
1292 // make sure there is no directive
1293 symbolicName = symbolicName.split(";")[0];
1294
1295 String version = attrs.getValue(ManifestHeader.BUNDLE_VERSION.toString());
1296 return new NameVersion(symbolicName, version);
1297 }
1298
1299 /** Try to download from an URI. */
1300 Path tryDownloadArchive(String uri, Path dir) throws IOException {
1301 // find mirror
1302 List<String> urlBases = null;
1303 String uriPrefix = null;
1304 uriPrefixes: for (String uriPref : mirrors.keySet()) {
1305 if (uri.startsWith(uriPref)) {
1306 if (mirrors.get(uriPref).size() > 0) {
1307 urlBases = mirrors.get(uriPref);
1308 uriPrefix = uriPref;
1309 break uriPrefixes;
1310 }
1311 }
1312 }
1313 if (urlBases == null)
1314 try {
1315 return downloadArchive(new URL(uri), dir);
1316 } catch (FileNotFoundException e) {
1317 throw new FileNotFoundException("Cannot find " + uri);
1318 }
1319
1320 // try to download
1321 for (String urlBase : urlBases) {
1322 String relativePath = uri.substring(uriPrefix.length());
1323 URL url = new URL(urlBase + relativePath);
1324 try {
1325 return downloadArchive(url, dir);
1326 } catch (FileNotFoundException e) {
1327 logger.log(WARNING, "Cannot download " + url + ", trying another mirror");
1328 }
1329 }
1330 throw new FileNotFoundException("Cannot find " + uri);
1331 }
1332
1333 /**
1334 * Effectively download. Synchronised in order to avoid downloading twice in
1335 * parallel.
1336 */
1337 synchronized Path downloadArchive(URL url, Path dir) throws IOException {
1338 return download(url, dir, (String) null);
1339 }
1340
1341 /** Effectively download. */
1342 Path download(URL url, Path dir, String name) throws IOException {
1343
1344 Path dest;
1345 if (name == null) {
1346 // We use also use parent directory in case the archive itself has a fixed name
1347 String[] segments = url.getPath().split("/");
1348 name = segments.length > 1 ? segments[segments.length - 2] + '-' + segments[segments.length - 1]
1349 : segments[segments.length - 1];
1350 }
1351
1352 dest = dir.resolve(name);
1353 if (Files.exists(dest)) {
1354 logger.log(TRACE, () -> "File " + dest + " already exists for " + url + ", not downloading again");
1355 return dest;
1356 } else {
1357 Files.createDirectories(dest.getParent());
1358 }
1359
1360 try (InputStream in = url.openStream()) {
1361 Files.copy(in, dest);
1362 logger.log(DEBUG, () -> "Downloaded " + dest + " from " + url);
1363 }
1364 return dest;
1365 }
1366
1367 /** Create a JAR file from a directory. */
1368 Path createJar(Path bundleDir, A2Origin origin) throws IOException {
1369 Path manifestPath = bundleDir.resolve("META-INF/MANIFEST.MF");
1370 Manifest manifest;
1371 try (InputStream in = Files.newInputStream(manifestPath)) {
1372 manifest = new Manifest(in);
1373 }
1374 // legal requirements
1375 origin.appendChanges(bundleDir);
1376 createReadMe(bundleDir, manifest);
1377
1378 // create the jar
1379 Path jarPath = bundleDir.getParent().resolve(bundleDir.getFileName() + ".jar");
1380 try (JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(jarPath), manifest)) {
1381 jarOut.setLevel(Deflater.DEFAULT_COMPRESSION);
1382 Files.walkFileTree(bundleDir, new SimpleFileVisitor<Path>() {
1383
1384 @Override
1385 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1386 if (file.getFileName().toString().equals("MANIFEST.MF"))
1387 return super.visitFile(file, attrs);
1388 JarEntry entry = new JarEntry(
1389 bundleDir.relativize(file).toString().replace(File.separatorChar, '/'));
1390 jarOut.putNextEntry(entry);
1391 Files.copy(file, jarOut);
1392 return super.visitFile(file, attrs);
1393 }
1394
1395 });
1396 }
1397 deleteDirectory(bundleDir);
1398
1399 if (separateSources)
1400 createSourceJar(bundleDir, manifest, null);
1401
1402 return jarPath;
1403 }
1404
1405 /** Package sources separately, in the Eclipse-SourceBundle format. */
1406 void createSourceJar(Path bundleDir, Manifest manifest, Properties props) throws IOException {
1407 boolean unmodified = props != null;
1408 Path bundleCategoryDir = bundleDir.getParent();
1409 Path sourceDir = bundleCategoryDir.resolve(bundleDir.toString() + ".src");
1410 if (!Files.exists(sourceDir)) {
1411 logger.log(WARNING, sourceDir + " does not exist, skipping...");
1412 return;
1413 }
1414
1415 Path relPath = a2Base.relativize(bundleCategoryDir);
1416 Path srcCategoryDir = a2SrcBase.resolve(relPath);
1417 Path srcJarP = srcCategoryDir.resolve(sourceDir.getFileName() + ".jar");
1418 Files.createDirectories(srcJarP.getParent());
1419
1420 String bundleSymbolicName = manifest.getMainAttributes().getValue("Bundle-SymbolicName").toString();
1421 // in case there are additional directives
1422 bundleSymbolicName = bundleSymbolicName.split(";")[0];
1423 Manifest srcManifest = new Manifest();
1424 srcManifest.getMainAttributes().put(MANIFEST_VERSION, "1.0");
1425 BUNDLE_SYMBOLICNAME.put(srcManifest, bundleSymbolicName + ".src");
1426 BUNDLE_VERSION.put(srcManifest, BUNDLE_VERSION.get(manifest));
1427 ECLIPSE_SOURCE_BUNDLE.put(srcManifest,
1428 bundleSymbolicName + ";version=\"" + BUNDLE_VERSION.get(manifest) + "\"");
1429
1430 // metadata
1431 createReadMe(sourceDir, unmodified ? props : manifest);
1432 // create jar
1433 try (JarOutputStream srcJarOut = new JarOutputStream(Files.newOutputStream(srcJarP), srcManifest)) {
1434 // srcJarOut.setLevel(Deflater.BEST_COMPRESSION);
1435 Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {
1436
1437 @Override
1438 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1439 if (file.getFileName().toString().equals("MANIFEST.MF"))
1440 return super.visitFile(file, attrs);
1441 JarEntry entry = new JarEntry(
1442 sourceDir.relativize(file).toString().replace(File.separatorChar, '/'));
1443 srcJarOut.putNextEntry(entry);
1444 Files.copy(file, srcJarOut);
1445 return super.visitFile(file, attrs);
1446 }
1447
1448 });
1449 }
1450 deleteDirectory(sourceDir);
1451 }
1452
1453 /**
1454 * Generate a readme clarifying and prominently notifying of the repackaging and
1455 * modifications.
1456 */
1457 void createReadMe(Path jarDir, Object mapping) throws IOException {
1458 // write repackaged README
1459 try (BufferedWriter writer = Files.newBufferedWriter(jarDir.resolve(README_REPACKAGED))) {
1460 boolean merged = ARGEO_ORIGIN_M2_MERGE.get(mapping) != null;
1461 if (merged)
1462 writer.append("This component is a merging of third party components"
1463 + " in order to comply with A2 packaging standards.\n");
1464 else
1465 writer.append("This component is a repackaging of a third party component"
1466 + " in order to comply with A2 packaging standards.\n");
1467
1468 // license
1469 String spdxLicenseId = SPDX_LICENSE_IDENTIFIER.get(mapping);
1470 if (spdxLicenseId == null)
1471 throw new IllegalStateException("An SPDX license id must have beend defined at this stage.");
1472 writer.append("\nIt is redistributed under the following license:\n\n");
1473 writer.append("SPDX-Identifier: " + spdxLicenseId + "\n\n");
1474
1475 if (!spdxLicenseId.startsWith("LicenseRef")) {// standard
1476 int withIndex = spdxLicenseId.indexOf(" WITH ");
1477 if (withIndex >= 0) {
1478 String simpleId = spdxLicenseId.substring(0, withIndex).trim();
1479 String exception = spdxLicenseId.substring(withIndex + " WITH ".length());
1480 writer.append("which are available here: https://spdx.org/licenses/" + simpleId
1481 + "\nand here: https://spdx.org/licenses/" + exception + "\n");
1482 } else {
1483 writer.append("which is available here: https://spdx.org/licenses/" + spdxLicenseId + "\n");
1484 }
1485 } else {
1486 String url = BUNDLE_LICENSE.get(mapping);
1487 if (url != null) {
1488 writer.write("which is available here: " + url + "\n");
1489 } else {
1490 logger.log(ERROR, "No licence URL for " + jarDir);
1491 }
1492 }
1493
1494 // origin
1495 String originDesc = ARGEO_ORIGIN_URI.get(mapping);
1496 if (originDesc != null)
1497 writer.append("\nThe original component comes from " + originDesc + ".\n");
1498 else {
1499 String m2Repo = ARGEO_ORIGIN_M2_REPO.get(mapping);
1500 originDesc = ARGEO_ORIGIN_M2.get(mapping);
1501 if (originDesc != null)
1502 writer.append("\nThe original component has M2 coordinates:\n" + originDesc.replace(',', '\n')
1503 + "\n" + (m2Repo != null ? "\nin M2 repository " + m2Repo + "\n" : ""));
1504 else
1505 logger.log(ERROR, "Cannot find origin information in " + jarDir);
1506 }
1507 String originSources = ARGEO_ORIGIN_SOURCES_URI.get(mapping);
1508 if (originSources != null)
1509 writer.append("\nThe original sources come from " + originSources + ".\n");
1510
1511 if (Files.exists(jarDir.resolve(CHANGES)))
1512 writer.append("\nA detailed list of changes is available under " + CHANGES + ".\n");
1513
1514 if (!jarDir.getFileName().toString().endsWith(".src")) {// binary archive
1515 if (separateSources)
1516 writer.append("Corresponding sources are available in the related archive named "
1517 + jarDir.toString() + ".src.jar.\n");
1518 else
1519 writer.append("Corresponding sources are available under OSGI-OPT/src.\n");
1520 }
1521 }
1522 }
1523
1524 /**
1525 * Gathers modifications performed on the original binaries and sources,
1526 * especially in order to comply with their license requirements.
1527 */
1528 class A2Origin {
1529 A2Origin() {
1530
1531 }
1532
1533 Set<String> modified = new TreeSet<>();
1534 Set<String> deleted = new TreeSet<>();
1535 Set<String> added = new TreeSet<>();
1536 Set<String> moved = new TreeSet<>();
1537
1538 /** Append changes to the A2-ORIGIN/changes file. */
1539 void appendChanges(Path baseDirectory) throws IOException {
1540 if (modified.isEmpty() && deleted.isEmpty() && added.isEmpty() && moved.isEmpty())
1541 return; // no changes
1542 Path changesFile = baseDirectory.resolve(CHANGES);
1543 Files.createDirectories(changesFile.getParent());
1544 try (BufferedWriter writer = Files.newBufferedWriter(changesFile, APPEND, CREATE)) {
1545 for (String msg : added)
1546 writer.write("- Added " + msg + ".\n");
1547 for (String msg : modified)
1548 writer.write("- Modified " + msg + ".\n");
1549 for (String msg : moved)
1550 writer.write("- Moved " + msg + ".\n");
1551 for (String msg : deleted)
1552 writer.write("- Deleted " + msg + ".\n");
1553 }
1554 }
1555 }
1556 }
1557
1558 /** Simple representation of an M2 artifact. */
1559 class M2Artifact extends CategoryNameVersion {
1560 private String classifier;
1561
1562 M2Artifact(String m2coordinates) {
1563 this(m2coordinates, null);
1564 }
1565
1566 M2Artifact(String m2coordinates, String classifier) {
1567 String[] parts = m2coordinates.split(":");
1568 setCategory(parts[0]);
1569 setName(parts[1]);
1570 if (parts.length > 2) {
1571 setVersion(parts[2]);
1572 }
1573 this.classifier = classifier;
1574 }
1575
1576 String getGroupId() {
1577 return super.getCategory();
1578 }
1579
1580 String getArtifactId() {
1581 return super.getName();
1582 }
1583
1584 String toM2Coordinates() {
1585 return getCategory() + ":" + getName() + (getVersion() != null ? ":" + getVersion() : "");
1586 }
1587
1588 String getClassifier() {
1589 return classifier != null ? classifier : "";
1590 }
1591
1592 String getExtension() {
1593 return "jar";
1594 }
1595 }
1596
1597 /** Utilities around Maven (conventions based). */
1598 class M2ConventionsUtils {
1599 final static String MAVEN_CENTRAL_BASE_URL = "https://repo1.maven.org/maven2/";
1600
1601 /** The file name of this artifact when stored */
1602 static String artifactFileName(M2Artifact artifact) {
1603 return artifact.getArtifactId() + '-' + artifact.getVersion()
1604 + (artifact.getClassifier().equals("") ? "" : '-' + artifact.getClassifier()) + '.'
1605 + artifact.getExtension();
1606 }
1607
1608 /** Absolute path to the file */
1609 static String artifactPath(String artifactBasePath, M2Artifact artifact) {
1610 return artifactParentPath(artifactBasePath, artifact) + '/' + artifactFileName(artifact);
1611 }
1612
1613 /** Absolute path to the file */
1614 static String artifactUrl(String repoUrl, M2Artifact artifact) {
1615 if (repoUrl.endsWith("/"))
1616 return repoUrl + artifactPath("/", artifact).substring(1);
1617 else
1618 return repoUrl + artifactPath("/", artifact);
1619 }
1620
1621 /** Absolute path to the file */
1622 static URL mavenRepoUrl(String repoBase, M2Artifact artifact) {
1623 String url = artifactUrl(repoBase == null ? MAVEN_CENTRAL_BASE_URL : repoBase, artifact);
1624 try {
1625 return new URL(url);
1626 } catch (MalformedURLException e) {
1627 // it should not happen
1628 throw new IllegalStateException(e);
1629 }
1630 }
1631
1632 /** Absolute path to the directories where the files will be stored */
1633 static String artifactParentPath(String artifactBasePath, M2Artifact artifact) {
1634 return artifactBasePath + (artifactBasePath.endsWith("/") || artifactBasePath.equals("") ? "" : "/")
1635 + artifactParentPath(artifact);
1636 }
1637
1638 /** Relative path to the directories where the files will be stored */
1639 static String artifactParentPath(M2Artifact artifact) {
1640 return artifact.getGroupId().replace('.', '/') + '/' + artifact.getArtifactId() + '/' + artifact.getVersion();
1641 }
1642
1643 /** Singleton */
1644 private M2ConventionsUtils() {
1645 }
1646 }
1647
1648 /** Combination of a category, a name and a version. */
1649 class CategoryNameVersion extends NameVersion {
1650 private String category;
1651
1652 CategoryNameVersion() {
1653 }
1654
1655 CategoryNameVersion(String category, String name, String version) {
1656 super(name, version);
1657 this.category = category;
1658 }
1659
1660 CategoryNameVersion(String category, NameVersion nameVersion) {
1661 super(nameVersion);
1662 this.category = category;
1663 }
1664
1665 String getCategory() {
1666 return category;
1667 }
1668
1669 void setCategory(String category) {
1670 this.category = category;
1671 }
1672
1673 @Override
1674 public String toString() {
1675 return category + ":" + super.toString();
1676 }
1677
1678 }
1679
1680 /** Combination of a name and a version. */
1681 class NameVersion implements Comparable<NameVersion> {
1682 private String name;
1683 private String version;
1684
1685 NameVersion() {
1686 }
1687
1688 /** Interprets string in OSGi-like format my.module.name;version=0.0.0 */
1689 NameVersion(String nameVersion) {
1690 int index = nameVersion.indexOf(";version=");
1691 if (index < 0) {
1692 setName(nameVersion);
1693 setVersion(null);
1694 } else {
1695 setName(nameVersion.substring(0, index));
1696 setVersion(nameVersion.substring(index + ";version=".length()));
1697 }
1698 }
1699
1700 NameVersion(String name, String version) {
1701 this.name = name;
1702 this.version = version;
1703 }
1704
1705 NameVersion(NameVersion nameVersion) {
1706 this.name = nameVersion.getName();
1707 this.version = nameVersion.getVersion();
1708 }
1709
1710 String getName() {
1711 return name;
1712 }
1713
1714 void setName(String name) {
1715 this.name = name;
1716 }
1717
1718 String getVersion() {
1719 return version;
1720 }
1721
1722 void setVersion(String version) {
1723 this.version = version;
1724 }
1725
1726 String getBranch() {
1727 String[] parts = getVersion().split("\\.");
1728 if (parts.length < 2)
1729 throw new IllegalStateException("Version " + getVersion() + " cannot be interpreted as branch.");
1730 return parts[0] + "." + parts[1];
1731 }
1732
1733 @Override
1734 public boolean equals(Object obj) {
1735 if (obj instanceof NameVersion) {
1736 NameVersion nameVersion = (NameVersion) obj;
1737 return name.equals(nameVersion.getName()) && version.equals(nameVersion.getVersion());
1738 } else
1739 return false;
1740 }
1741
1742 @Override
1743 public int hashCode() {
1744 return name.hashCode();
1745 }
1746
1747 @Override
1748 public String toString() {
1749 return name + ":" + version;
1750 }
1751
1752 public int compareTo(NameVersion o) {
1753 if (o.getName().equals(name))
1754 return version.compareTo(o.getVersion());
1755 else
1756 return name.compareTo(o.getName());
1757 }
1758 }