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