]> git.argeo.org Git - cc0/argeo-build.git/blob - src/org/argeo/build/Repackage.java
Generate JPM module-info
[cc0/argeo-build.git] / src / org / argeo / build / 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 }
1217
1218 // de-pollute MANIFEST
1219 for (Iterator<Map.Entry<Object, Object>> manifestEntries = manifest.getMainAttributes().entrySet()
1220 .iterator(); manifestEntries.hasNext();) {
1221 Map.Entry<Object, Object> manifestEntry = manifestEntries.next();
1222 String key = manifestEntry.getKey().toString();
1223 // TODO make it more generic
1224 // if (key.equals(REQUIRE_BUNDLE.toString()) && nameVersion.getName().equals("com.sun.jna.platform"))
1225 // manifestEntries.remove();
1226 switch (key) {
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 manifestEntries.remove();
1234 origin.deleted.add("MANIFEST header " + manifestEntry.getKey());
1235 break;
1236 default:
1237 if (sourceManifest != null && !sourceManifest.getMainAttributes().containsKey(manifestEntry.getKey()))
1238 origin.added.add("MANIFEST header " + manifestEntry.getKey());
1239 }
1240 }
1241
1242 processLicense(bundleDir, manifest);
1243
1244 origin.modified.add("MANIFEST (META-INF/MANIFEST.MF)");
1245 // write the MANIFEST
1246 try (OutputStream out = Files.newOutputStream(manifestPath)) {
1247 manifest.write(out);
1248 }
1249 return bundleDir;
1250 }
1251
1252 /** Process SPDX license identifier. */
1253 void processLicense(Path bundleDir, Manifest manifest) {
1254 String spdxLicenceId = manifest.getMainAttributes().getValue(SPDX_LICENSE_IDENTIFIER.toString());
1255 String bundleLicense = manifest.getMainAttributes().getValue(BUNDLE_LICENSE.toString());
1256 if (spdxLicenceId == null) {
1257 logger.log(ERROR, bundleDir.getFileName() + ": " + SPDX_LICENSE_IDENTIFIER + " not available, "
1258 + BUNDLE_LICENSE + " is " + bundleLicense);
1259 } else {
1260 // only use the first licensing option
1261 int orIndex = spdxLicenceId.indexOf(" OR ");
1262 if (orIndex >= 0)
1263 spdxLicenceId = spdxLicenceId.substring(0, orIndex).trim();
1264
1265 String bundleDirName = bundleDir.getFileName().toString();
1266 // force licenses of some well-known components
1267 // even if we say otherwise (typically because from an Eclipse archive)
1268 if (bundleDirName.startsWith("org.apache."))
1269 spdxLicenceId = "Apache-2.0";
1270 if (bundleDirName.startsWith("com.sun.jna."))
1271 spdxLicenceId = "Apache-2.0";
1272 if (bundleDirName.startsWith("com.ibm.icu."))
1273 spdxLicenceId = "ICU";
1274 if (bundleDirName.startsWith("javax.annotation."))
1275 spdxLicenceId = "GPL-2.0-only WITH Classpath-exception-2.0";
1276 if (bundleDirName.startsWith("javax.inject."))
1277 spdxLicenceId = "Apache-2.0";
1278 if (bundleDirName.startsWith("org.osgi."))
1279 spdxLicenceId = "Apache-2.0";
1280
1281 manifest.getMainAttributes().putValue(SPDX_LICENSE_IDENTIFIER.toString(), spdxLicenceId);
1282 if (!licensesUsed.containsKey(spdxLicenceId))
1283 licensesUsed.put(spdxLicenceId, new TreeSet<>());
1284 licensesUsed.get(spdxLicenceId).add(bundleDir.getParent().getFileName() + "/" + bundleDir.getFileName());
1285 }
1286 }
1287
1288 /*
1289 * UTILITIES
1290 */
1291 /** Recursively deletes a directory. */
1292 static void deleteDirectory(Path path) throws IOException {
1293 if (!Files.exists(path))
1294 return;
1295 Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
1296 @Override
1297 public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
1298 if (e != null)
1299 throw e;
1300 Files.delete(directory);
1301 return CONTINUE;
1302 }
1303
1304 @Override
1305 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1306 Files.delete(file);
1307 return CONTINUE;
1308 }
1309 });
1310 }
1311
1312 /** Extract name/version from a MANIFEST. */
1313 NameVersion nameVersionFromManifest(Manifest manifest) {
1314 Attributes attrs = manifest.getMainAttributes();
1315 // symbolic name
1316 String symbolicName = attrs.getValue(ManifestHeader.BUNDLE_SYMBOLICNAME.toString());
1317 if (symbolicName == null)
1318 return null;
1319 // make sure there is no directive
1320 symbolicName = symbolicName.split(";")[0];
1321
1322 String version = attrs.getValue(ManifestHeader.BUNDLE_VERSION.toString());
1323 return new NameVersion(symbolicName, version);
1324 }
1325
1326 /** Try to download from an URI. */
1327 Path tryDownloadArchive(String uriStr, Path dir) throws IOException {
1328 // find mirror
1329 List<String> urlBases = null;
1330 String uriPrefix = null;
1331 uriPrefixes: for (String uriPref : mirrors.keySet()) {
1332 if (uriStr.startsWith(uriPref)) {
1333 if (mirrors.get(uriPref).size() > 0) {
1334 urlBases = mirrors.get(uriPref);
1335 uriPrefix = uriPref;
1336 break uriPrefixes;
1337 }
1338 }
1339 }
1340 if (urlBases == null)
1341 try {
1342 return downloadArchive(new URI(uriStr), dir);
1343 } catch (FileNotFoundException | URISyntaxException e) {
1344 throw new FileNotFoundException("Cannot find " + uriStr);
1345 }
1346
1347 // try to download
1348 for (String urlBase : urlBases) {
1349 String relativePath = uriStr.substring(uriPrefix.length());
1350 String uStr = urlBase + relativePath;
1351 try {
1352 return downloadArchive(new URI(uStr), dir);
1353 } catch (FileNotFoundException | URISyntaxException e) {
1354 logger.log(WARNING, "Cannot download " + uStr + ", trying another mirror");
1355 }
1356 }
1357 throw new FileNotFoundException("Cannot find " + uriStr);
1358 }
1359
1360 /**
1361 * Effectively download an archive.
1362 */
1363 Path downloadArchive(URI uri, Path dir) throws IOException {
1364 return download(uri, dir, (String) null);
1365 }
1366
1367 /**
1368 * Effectively download. Synchronised in order to avoid downloading twice in
1369 * parallel.
1370 */
1371 synchronized Path download(URI uri, Path dir, String name) throws IOException {
1372
1373 Path dest;
1374 if (name == null) {
1375 // We use also use parent directory in case the archive itself has a fixed name
1376 String[] segments = uri.getPath().split("/");
1377 name = segments.length > 1 ? segments[segments.length - 2] + '-' + segments[segments.length - 1]
1378 : segments[segments.length - 1];
1379 }
1380
1381 dest = dir.resolve(name);
1382 if (Files.exists(dest)) {
1383 logger.log(TRACE, () -> "File " + dest + " already exists for " + uri + ", not downloading again");
1384 return dest;
1385 } else {
1386 Files.createDirectories(dest.getParent());
1387 }
1388
1389 try (InputStream in = uri.toURL().openStream()) {
1390 Files.copy(in, dest);
1391 logger.log(DEBUG, () -> "Downloaded " + dest + " from " + uri);
1392 }
1393 return dest;
1394 }
1395
1396 /** Create a JAR file from a directory. */
1397 Path createJar(Path bundleDir, A2Origin origin) throws IOException {
1398 Path manifestPath = bundleDir.resolve("META-INF/MANIFEST.MF");
1399 Manifest manifest;
1400 try (InputStream in = Files.newInputStream(manifestPath)) {
1401 manifest = new Manifest(in);
1402 }
1403 // legal requirements
1404 origin.appendChanges(bundleDir);
1405 createReadMe(bundleDir, manifest);
1406
1407 // create the jar
1408 Path jarPath = bundleDir.getParent().resolve(bundleDir.getFileName() + ".jar");
1409 try (JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(jarPath), manifest)) {
1410 jarOut.setLevel(Deflater.DEFAULT_COMPRESSION);
1411 Files.walkFileTree(bundleDir, new SimpleFileVisitor<Path>() {
1412
1413 @Override
1414 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1415 if (file.getFileName().toString().equals("MANIFEST.MF"))
1416 return super.visitFile(file, attrs);
1417 JarEntry entry = new JarEntry(
1418 bundleDir.relativize(file).toString().replace(File.separatorChar, '/'));
1419 jarOut.putNextEntry(entry);
1420 Files.copy(file, jarOut);
1421 return super.visitFile(file, attrs);
1422 }
1423
1424 });
1425 }
1426 deleteDirectory(bundleDir);
1427
1428 if (separateSources)
1429 createSourceJar(bundleDir, manifest, null);
1430
1431 return jarPath;
1432 }
1433
1434 /** Package sources separately, in the Eclipse-SourceBundle format. */
1435 void createSourceJar(Path bundleDir, Manifest manifest, Properties props) throws IOException {
1436 boolean unmodified = props != null;
1437 Path bundleCategoryDir = bundleDir.getParent();
1438 Path sourceDir = bundleCategoryDir.resolve(bundleDir.toString() + ".src");
1439 if (!Files.exists(sourceDir)) {
1440 logger.log(WARNING, sourceDir + " does not exist, skipping...");
1441 return;
1442 }
1443
1444 Path relPath = a2Base.relativize(bundleCategoryDir);
1445 Path srcCategoryDir = a2SrcBase.resolve(relPath);
1446 Path srcJarP = srcCategoryDir.resolve(sourceDir.getFileName() + ".jar");
1447 Files.createDirectories(srcJarP.getParent());
1448
1449 String bundleSymbolicName = manifest.getMainAttributes().getValue("Bundle-SymbolicName").toString();
1450 // in case there are additional directives
1451 bundleSymbolicName = bundleSymbolicName.split(";")[0];
1452 Manifest srcManifest = new Manifest();
1453 srcManifest.getMainAttributes().put(MANIFEST_VERSION, "1.0");
1454 BUNDLE_SYMBOLICNAME.put(srcManifest, bundleSymbolicName + ".src");
1455 BUNDLE_VERSION.put(srcManifest, BUNDLE_VERSION.get(manifest));
1456 ECLIPSE_SOURCE_BUNDLE.put(srcManifest,
1457 bundleSymbolicName + ";version=\"" + BUNDLE_VERSION.get(manifest) + "\"");
1458
1459 // metadata
1460 createReadMe(sourceDir, unmodified ? props : manifest);
1461 // create jar
1462 try (JarOutputStream srcJarOut = new JarOutputStream(Files.newOutputStream(srcJarP), srcManifest)) {
1463 // srcJarOut.setLevel(Deflater.BEST_COMPRESSION);
1464 Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {
1465
1466 @Override
1467 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1468 if (file.getFileName().toString().equals("MANIFEST.MF"))
1469 return super.visitFile(file, attrs);
1470 JarEntry entry = new JarEntry(
1471 sourceDir.relativize(file).toString().replace(File.separatorChar, '/'));
1472 srcJarOut.putNextEntry(entry);
1473 Files.copy(file, srcJarOut);
1474 return super.visitFile(file, attrs);
1475 }
1476
1477 });
1478 }
1479 deleteDirectory(sourceDir);
1480 }
1481
1482 /**
1483 * Generate a readme clarifying and prominently notifying of the repackaging and
1484 * modifications.
1485 */
1486 void createReadMe(Path jarDir, Object mapping) throws IOException {
1487 // write repackaged README
1488 try (BufferedWriter writer = Files.newBufferedWriter(jarDir.resolve(README_REPACKAGED))) {
1489 boolean merged = ARGEO_ORIGIN_M2_MERGE.get(mapping) != null;
1490 if (merged)
1491 writer.append("This component is a merging of third party components"
1492 + " in order to comply with A2 packaging standards.\n");
1493 else
1494 writer.append("This component is a repackaging of a third party component"
1495 + " in order to comply with A2 packaging standards.\n");
1496
1497 // license
1498 String spdxLicenseId = SPDX_LICENSE_IDENTIFIER.get(mapping);
1499 if (spdxLicenseId == null)
1500 throw new IllegalStateException("An SPDX license id must have beend defined at this stage.");
1501 writer.append("\nIt is redistributed under the following license:\n\n");
1502 writer.append("SPDX-Identifier: " + spdxLicenseId + "\n\n");
1503
1504 if (!spdxLicenseId.startsWith("LicenseRef")) {// standard
1505 int withIndex = spdxLicenseId.indexOf(" WITH ");
1506 if (withIndex >= 0) {
1507 String simpleId = spdxLicenseId.substring(0, withIndex).trim();
1508 String exception = spdxLicenseId.substring(withIndex + " WITH ".length());
1509 writer.append("which are available here: https://spdx.org/licenses/" + simpleId
1510 + "\nand here: https://spdx.org/licenses/" + exception + "\n");
1511 } else {
1512 writer.append("which is available here: https://spdx.org/licenses/" + spdxLicenseId + "\n");
1513 }
1514 } else {
1515 String url = BUNDLE_LICENSE.get(mapping);
1516 if (url != null) {
1517 writer.write("which is available here: " + url + "\n");
1518 } else {
1519 logger.log(ERROR, "No licence URL for " + jarDir);
1520 }
1521 }
1522
1523 // origin
1524 String originDesc = ARGEO_ORIGIN_URI.get(mapping);
1525 if (originDesc != null)
1526 writer.append("\nThe original component comes from " + originDesc + ".\n");
1527 else {
1528 String m2Repo = ARGEO_ORIGIN_M2_REPO.get(mapping);
1529 originDesc = ARGEO_ORIGIN_M2.get(mapping);
1530 if (originDesc != null)
1531 writer.append("\nThe original component has M2 coordinates:\n" + originDesc.replace(',', '\n')
1532 + "\n" + (m2Repo != null ? "\nin M2 repository " + m2Repo + "\n" : ""));
1533 else
1534 logger.log(ERROR, "Cannot find origin information in " + jarDir);
1535 }
1536 String originSources = ARGEO_ORIGIN_SOURCES_URI.get(mapping);
1537 if (originSources != null)
1538 writer.append("\nThe original sources come from " + originSources + ".\n");
1539
1540 if (Files.exists(jarDir.resolve(CHANGES)))
1541 writer.append("\nA detailed list of changes is available under " + CHANGES + ".\n");
1542
1543 if (!jarDir.getFileName().toString().endsWith(".src")) {// binary archive
1544 if (separateSources)
1545 writer.append("Corresponding sources are available in the related archive named "
1546 + jarDir.toString() + ".src.jar.\n");
1547 else
1548 writer.append("Corresponding sources are available under OSGI-OPT/src.\n");
1549 }
1550 }
1551 }
1552
1553 /**
1554 * Gathers modifications performed on the original binaries and sources,
1555 * especially in order to comply with their license requirements.
1556 */
1557 class A2Origin {
1558 A2Origin() {
1559
1560 }
1561
1562 Set<String> modified = new TreeSet<>();
1563 Set<String> deleted = new TreeSet<>();
1564 Set<String> added = new TreeSet<>();
1565 Set<String> moved = new TreeSet<>();
1566
1567 /** Append changes to the A2-ORIGIN/changes file. */
1568 void appendChanges(Path baseDirectory) throws IOException {
1569 if (modified.isEmpty() && deleted.isEmpty() && added.isEmpty() && moved.isEmpty())
1570 return; // no changes
1571 Path changesFile = baseDirectory.resolve(CHANGES);
1572 Files.createDirectories(changesFile.getParent());
1573 try (BufferedWriter writer = Files.newBufferedWriter(changesFile, APPEND, CREATE)) {
1574 for (String msg : added)
1575 writer.write("- Added " + msg + ".\n");
1576 for (String msg : modified)
1577 writer.write("- Modified " + msg + ".\n");
1578 for (String msg : moved)
1579 writer.write("- Moved " + msg + ".\n");
1580 for (String msg : deleted)
1581 writer.write("- Deleted " + msg + ".\n");
1582 }
1583 }
1584 }
1585 }
1586
1587 /** Simple representation of an M2 artifact. */
1588 class M2Artifact extends CategoryNameVersion {
1589 private String classifier;
1590
1591 M2Artifact(String m2coordinates) {
1592 this(m2coordinates, null);
1593 }
1594
1595 M2Artifact(String m2coordinates, String classifier) {
1596 String[] parts = m2coordinates.split(":");
1597 setCategory(parts[0]);
1598 setName(parts[1]);
1599 if (parts.length > 2) {
1600 setVersion(parts[2]);
1601 }
1602 this.classifier = classifier;
1603 }
1604
1605 String getGroupId() {
1606 return super.getCategory();
1607 }
1608
1609 String getArtifactId() {
1610 return super.getName();
1611 }
1612
1613 String toM2Coordinates() {
1614 return getCategory() + ":" + getName() + (getVersion() != null ? ":" + getVersion() : "");
1615 }
1616
1617 String getClassifier() {
1618 return classifier != null ? classifier : "";
1619 }
1620
1621 String getExtension() {
1622 return "jar";
1623 }
1624 }
1625
1626 /** Utilities around Maven (conventions based). */
1627 class M2ConventionsUtils {
1628 final static String MAVEN_CENTRAL_BASE_URL = "https://repo1.maven.org/maven2/";
1629
1630 /** The file name of this artifact when stored */
1631 static String artifactFileName(M2Artifact artifact) {
1632 return artifact.getArtifactId() + '-' + artifact.getVersion()
1633 + (artifact.getClassifier().equals("") ? "" : '-' + artifact.getClassifier()) + '.'
1634 + artifact.getExtension();
1635 }
1636
1637 /** Absolute path to the file */
1638 static String artifactPath(String artifactBasePath, M2Artifact artifact) {
1639 return artifactParentPath(artifactBasePath, artifact) + '/' + artifactFileName(artifact);
1640 }
1641
1642 /** Absolute path to the file */
1643 static String artifactUrl(String repoUrl, M2Artifact artifact) {
1644 if (repoUrl.endsWith("/"))
1645 return repoUrl + artifactPath("/", artifact).substring(1);
1646 else
1647 return repoUrl + artifactPath("/", artifact);
1648 }
1649
1650 /** Absolute path to the file */
1651 static URI mavenRepoUrl(String repoBase, M2Artifact artifact) throws URISyntaxException {
1652 String uri = artifactUrl(repoBase == null ? MAVEN_CENTRAL_BASE_URL : repoBase, artifact);
1653 return new URI(uri);
1654 }
1655
1656 /** Absolute path to the directories where the files will be stored */
1657 static String artifactParentPath(String artifactBasePath, M2Artifact artifact) {
1658 return artifactBasePath + (artifactBasePath.endsWith("/") || artifactBasePath.equals("") ? "" : "/")
1659 + artifactParentPath(artifact);
1660 }
1661
1662 /** Relative path to the directories where the files will be stored */
1663 static String artifactParentPath(M2Artifact artifact) {
1664 return artifact.getGroupId().replace('.', '/') + '/' + artifact.getArtifactId() + '/' + artifact.getVersion();
1665 }
1666
1667 /** Singleton */
1668 private M2ConventionsUtils() {
1669 }
1670 }
1671
1672 /** Combination of a category, a name and a version. */
1673 class CategoryNameVersion extends NameVersion {
1674 private String category;
1675
1676 CategoryNameVersion() {
1677 }
1678
1679 CategoryNameVersion(String category, String name, String version) {
1680 super(name, version);
1681 this.category = category;
1682 }
1683
1684 CategoryNameVersion(String category, NameVersion nameVersion) {
1685 super(nameVersion);
1686 this.category = category;
1687 }
1688
1689 String getCategory() {
1690 return category;
1691 }
1692
1693 void setCategory(String category) {
1694 this.category = category;
1695 }
1696
1697 @Override
1698 public String toString() {
1699 return category + ":" + super.toString();
1700 }
1701
1702 }
1703
1704 /** Combination of a name and a version. */
1705 class NameVersion implements Comparable<NameVersion> {
1706 private String name;
1707 private String version;
1708
1709 NameVersion() {
1710 }
1711
1712 /** Interprets string in OSGi-like format my.module.name;version=0.0.0 */
1713 NameVersion(String nameVersion) {
1714 int index = nameVersion.indexOf(";version=");
1715 if (index < 0) {
1716 setName(nameVersion);
1717 setVersion(null);
1718 } else {
1719 setName(nameVersion.substring(0, index));
1720 setVersion(nameVersion.substring(index + ";version=".length()));
1721 }
1722 }
1723
1724 NameVersion(String name, String version) {
1725 this.name = name;
1726 this.version = version;
1727 }
1728
1729 NameVersion(NameVersion nameVersion) {
1730 this.name = nameVersion.getName();
1731 this.version = nameVersion.getVersion();
1732 }
1733
1734 String getName() {
1735 return name;
1736 }
1737
1738 void setName(String name) {
1739 this.name = name;
1740 }
1741
1742 String getVersion() {
1743 return version;
1744 }
1745
1746 void setVersion(String version) {
1747 this.version = version;
1748 }
1749
1750 String getBranch() {
1751 String[] parts = getVersion().split("\\.");
1752 if (parts.length < 2)
1753 throw new IllegalStateException("Version " + getVersion() + " cannot be interpreted as branch.");
1754 return parts[0] + "." + parts[1];
1755 }
1756
1757 @Override
1758 public boolean equals(Object obj) {
1759 if (obj instanceof NameVersion) {
1760 NameVersion nameVersion = (NameVersion) obj;
1761 return name.equals(nameVersion.getName()) && version.equals(nameVersion.getVersion());
1762 } else
1763 return false;
1764 }
1765
1766 @Override
1767 public int hashCode() {
1768 return name.hashCode();
1769 }
1770
1771 @Override
1772 public String toString() {
1773 return name + ":" + version;
1774 }
1775
1776 public int compareTo(NameVersion o) {
1777 if (o.getName().equals(name))
1778 return version.compareTo(o.getVersion());
1779 else
1780 return name.compareTo(o.getName());
1781 }
1782 }