]> git.argeo.org Git - cc0/argeo-build.git/blob - src/org/argeo/build/Repackage.java
1b2d6d1d8e7af3d440236dca2c431b37fb8ae42b
[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.util.jar.Attributes.Name.MANIFEST_VERSION;
10 import static org.argeo.build.Repackage.ManifestConstants.ARGEO_ORIGIN_M2;
11 import static org.argeo.build.Repackage.ManifestConstants.ARGEO_ORIGIN_M2_REPO;
12 import static org.argeo.build.Repackage.ManifestConstants.BUNDLE_LICENSE;
13 import static org.argeo.build.Repackage.ManifestConstants.BUNDLE_SYMBOLICNAME;
14 import static org.argeo.build.Repackage.ManifestConstants.BUNDLE_VERSION;
15 import static org.argeo.build.Repackage.ManifestConstants.ECLIPSE_SOURCE_BUNDLE;
16 import static org.argeo.build.Repackage.ManifestConstants.EXPORT_PACKAGE;
17 import static org.argeo.build.Repackage.ManifestConstants.IMPORT_PACKAGE;
18 import static org.argeo.build.Repackage.ManifestConstants.SPDX_LICENSE_IDENTIFIER;
19
20 import java.io.File;
21 import java.io.FileNotFoundException;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.lang.System.Logger;
26 import java.net.MalformedURLException;
27 import java.net.URL;
28 import java.nio.charset.StandardCharsets;
29 import java.nio.file.DirectoryStream;
30 import java.nio.file.FileSystem;
31 import java.nio.file.FileSystems;
32 import java.nio.file.FileVisitResult;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.nio.file.PathMatcher;
36 import java.nio.file.Paths;
37 import java.nio.file.SimpleFileVisitor;
38 import java.nio.file.StandardOpenOption;
39 import java.nio.file.attribute.BasicFileAttributes;
40 import java.util.ArrayList;
41 import java.util.HashMap;
42 import java.util.Iterator;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Objects;
46 import java.util.Properties;
47 import java.util.Set;
48 import java.util.TreeMap;
49 import java.util.TreeSet;
50 import java.util.concurrent.CompletableFuture;
51 import java.util.jar.Attributes;
52 import java.util.jar.JarEntry;
53 import java.util.jar.JarInputStream;
54 import java.util.jar.JarOutputStream;
55 import java.util.jar.Manifest;
56 import java.util.zip.Deflater;
57
58 import aQute.bnd.osgi.Analyzer;
59 import aQute.bnd.osgi.Jar;
60
61 /**
62 * Simple tool repackaging existing jar files into OSGi bundles in an A2
63 * repository.
64 */
65 public class Repackage {
66 final static Logger logger = System.getLogger(Repackage.class.getName());
67
68 /**
69 * Environment variable on whether sources should be packaged separately or
70 * integrated in the bundles.
71 */
72 final static String ENV_SOURCE_BUNDLES = "SOURCE_BUNDLES";
73
74 /** Whethere repackaging should run in parallel or sequentially. */
75 final static boolean parallel = true;
76
77 // cache
78 final static Map<String, Set<String>> licensesUsed = new TreeMap<>();
79
80 /** Main entry point. */
81 public static void main(String[] args) {
82 if (args.length < 2) {
83 System.err.println("Usage: <path to a2 output dir> <category1> <category2> ...");
84 System.exit(1);
85 }
86 Path a2Base = Paths.get(args[0]).toAbsolutePath().normalize();
87 Path descriptorsBase = Paths.get(".").toAbsolutePath().normalize();
88 Repackage factory = new Repackage(a2Base, descriptorsBase);
89
90 List<CompletableFuture<Void>> toDos = new ArrayList<>();
91 for (int i = 1; i < args.length; i++) {
92 Path p = Paths.get(args[i]);
93 if (parallel)
94 toDos.add(CompletableFuture.runAsync(() -> factory.processCategory(p)));
95 else
96 factory.processCategory(p);
97 }
98 CompletableFuture.allOf(toDos.toArray(new CompletableFuture[toDos.size()])).join();
99
100 // Summary
101 StringBuilder sb = new StringBuilder();
102 for (String licenseId : licensesUsed.keySet())
103 for (String name : licensesUsed.get(licenseId))
104 sb.append(licenseId + "\t" + name + "\n");
105 logger.log(INFO, "# License summary:\n" + sb);
106 }
107
108 final static String COMMON_BND = "common.bnd";
109 final static String MERGE_BND = "merge.bnd";
110
111 /** Directory where to download archives */
112 Path originBase;
113 /** Directory where to download Maven artifacts */
114 Path mavenBase;
115
116 /** A2 repository base for binary bundles */
117 Path a2Base;
118 /** A2 repository base for source bundles */
119 Path a2SrcBase;
120 /** A2 base for native components */
121 Path a2LibBase;
122 /** Location of the descriptors driving the packaging */
123 Path descriptorsBase;
124 /** URIs of archives to download */
125 Properties uris = new Properties();
126 /** Mirrors for archive download. Key is URI prefix, value list of base URLs */
127 Map<String, List<String>> mirrors = new HashMap<String, List<String>>();
128
129 /** Whether sources should be packaged separately */
130 final boolean sourceBundles;
131
132 /** Constructor initialises the various variables */
133 public Repackage(Path a2Base, Path descriptorsBase) {
134 sourceBundles = Boolean.parseBoolean(System.getenv(ENV_SOURCE_BUNDLES));
135 if (sourceBundles)
136 logger.log(INFO, "Sources will be packaged separately");
137
138 Objects.requireNonNull(a2Base);
139 Objects.requireNonNull(descriptorsBase);
140 this.originBase = Paths.get(System.getProperty("user.home"), ".cache", "argeo/build/origin");
141 this.mavenBase = Paths.get(System.getProperty("user.home"), ".m2", "repository");
142
143 // TODO define and use a build base
144 this.a2Base = a2Base;
145 this.a2SrcBase = a2Base.getParent().resolve(a2Base.getFileName() + ".src");
146 this.a2LibBase = a2Base.resolve("lib");
147 this.descriptorsBase = descriptorsBase;
148 if (!Files.exists(this.descriptorsBase))
149 throw new IllegalArgumentException(this.descriptorsBase + " does not exist");
150
151 // URIs mapping
152 Path urisPath = this.descriptorsBase.resolve("uris.properties");
153 if (Files.exists(urisPath)) {
154 try (InputStream in = Files.newInputStream(urisPath)) {
155 uris.load(in);
156 } catch (IOException e) {
157 throw new IllegalStateException("Cannot load " + urisPath, e);
158 }
159 }
160
161 // Eclipse mirrors
162 Path eclipseMirrorsPath = this.descriptorsBase.resolve("eclipse.mirrors.txt");
163 List<String> eclipseMirrors = new ArrayList<>();
164 if (Files.exists(eclipseMirrorsPath)) {
165 try {
166 eclipseMirrors = Files.readAllLines(eclipseMirrorsPath, StandardCharsets.UTF_8);
167 } catch (IOException e) {
168 throw new IllegalStateException("Cannot load " + eclipseMirrorsPath, e);
169 }
170 for (Iterator<String> it = eclipseMirrors.iterator(); it.hasNext();) {
171 String value = it.next();
172 if (value.strip().equals(""))
173 it.remove();
174 }
175 }
176 mirrors.put("http://www.eclipse.org/downloads", eclipseMirrors);
177 }
178
179 /*
180 * MAVEN ORIGIN
181 */
182 /** Process a whole category/group id. */
183 void processCategory(Path categoryRelativePath) {
184 try {
185 Path targetCategoryBase = descriptorsBase.resolve(categoryRelativePath);
186 DirectoryStream<Path> bnds = Files.newDirectoryStream(targetCategoryBase,
187 (p) -> p.getFileName().toString().endsWith(".bnd") && !p.getFileName().toString().equals(COMMON_BND)
188 && !p.getFileName().toString().equals(MERGE_BND));
189 for (Path p : bnds) {
190 processSingleM2ArtifactDistributionUnit(p);
191 }
192
193 DirectoryStream<Path> dus = Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p));
194 for (Path duDir : dus) {
195 if (duDir.getFileName().toString().startsWith("eclipse-")) {
196 processEclipseArchive(duDir);
197 } else {
198 processM2BasedDistributionUnit(duDir);
199 }
200 }
201 } catch (IOException e) {
202 throw new RuntimeException("Cannot process category " + categoryRelativePath, e);
203 }
204 }
205
206 /** Process a standalone Maven artifact. */
207 void processSingleM2ArtifactDistributionUnit(Path bndFile) {
208 try {
209 Path categoryRelativePath = descriptorsBase.relativize(bndFile.getParent());
210 Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
211
212 Properties fileProps = new Properties();
213 try (InputStream in = Files.newInputStream(bndFile)) {
214 fileProps.load(in);
215 }
216 String repoStr = fileProps.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
217 ? fileProps.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
218 : null;
219
220 if (!fileProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
221 // use file name as symbolic name
222 String symbolicName = bndFile.getFileName().toString();
223 symbolicName = symbolicName.substring(0, symbolicName.length() - ".bnd".length());
224 fileProps.put(BUNDLE_SYMBOLICNAME.toString(), symbolicName);
225 }
226
227 String m2Coordinates = fileProps.getProperty(ARGEO_ORIGIN_M2.toString());
228 if (m2Coordinates == null)
229 throw new IllegalArgumentException("No M2 coordinates available for " + bndFile);
230 M2Artifact artifact = new M2Artifact(m2Coordinates);
231 URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
232 Path downloaded = downloadMaven(url, artifact);
233
234 Path targetBundleDir = processBndJar(downloaded, targetCategoryBase, fileProps, artifact);
235 downloadAndProcessM2Sources(repoStr, artifact, targetBundleDir);
236
237 createJar(targetBundleDir);
238 } catch (Exception e) {
239 throw new RuntimeException("Cannot process " + bndFile, e);
240 }
241 }
242
243 /** Process multiple Maven artifacts. */
244 void processM2BasedDistributionUnit(Path duDir) {
245 try {
246 Path categoryRelativePath = descriptorsBase.relativize(duDir.getParent());
247 Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
248
249 Path mergeBnd = duDir.resolve(MERGE_BND);
250 if (Files.exists(mergeBnd)) // merge
251 mergeM2Artifacts(mergeBnd);
252
253 Path commonBnd = duDir.resolve(COMMON_BND);
254 if (!Files.exists(commonBnd))
255 return;
256
257 Properties commonProps = new Properties();
258 try (InputStream in = Files.newInputStream(commonBnd)) {
259 commonProps.load(in);
260 }
261
262 String m2Version = commonProps.getProperty(ARGEO_ORIGIN_M2.toString());
263 if (m2Version == null) {
264 logger.log(WARNING, "Ignoring " + duDir + " as it is not an M2-based distribution unit");
265 return;// ignore, this is probably an Eclipse archive
266 }
267 if (!m2Version.startsWith(":")) {
268 throw new IllegalStateException("Only the M2 version can be specified: " + m2Version);
269 }
270 m2Version = m2Version.substring(1);
271
272 DirectoryStream<Path> ds = Files.newDirectoryStream(duDir,
273 (p) -> p.getFileName().toString().endsWith(".bnd") && !p.getFileName().toString().equals(COMMON_BND)
274 && !p.getFileName().toString().equals(MERGE_BND));
275 for (Path p : ds) {
276 Properties fileProps = new Properties();
277 try (InputStream in = Files.newInputStream(p)) {
278 fileProps.load(in);
279 }
280 String m2Coordinates = fileProps.getProperty(ARGEO_ORIGIN_M2.toString());
281 M2Artifact artifact = new M2Artifact(m2Coordinates);
282 artifact.setVersion(m2Version);
283
284 // prepare manifest entries
285 Properties mergeProps = new Properties();
286 mergeProps.putAll(commonProps);
287
288 fileEntries: for (Object key : fileProps.keySet()) {
289 if (ManifestConstants.ARGEO_ORIGIN_M2.toString().equals(key))
290 continue fileEntries;
291 String value = fileProps.getProperty(key.toString());
292 Object previousValue = mergeProps.put(key.toString(), value);
293 if (previousValue != null) {
294 logger.log(WARNING,
295 commonBnd + ": " + key + " was " + previousValue + ", overridden with " + value);
296 }
297 }
298 mergeProps.put(ManifestConstants.ARGEO_ORIGIN_M2.toString(), artifact.toM2Coordinates());
299 if (!mergeProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
300 // use file name as symbolic name
301 String symbolicName = p.getFileName().toString();
302 symbolicName = symbolicName.substring(0, symbolicName.length() - ".bnd".length());
303 mergeProps.put(BUNDLE_SYMBOLICNAME.toString(), symbolicName);
304 }
305
306 String repoStr = mergeProps.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
307 ? mergeProps.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
308 : null;
309
310 // download
311 URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
312 Path downloaded = downloadMaven(url, artifact);
313
314 Path targetBundleDir = processBndJar(downloaded, targetCategoryBase, mergeProps, artifact);
315 downloadAndProcessM2Sources(repoStr, artifact, targetBundleDir);
316 createJar(targetBundleDir);
317 }
318 } catch (IOException e) {
319 throw new RuntimeException("Cannot process " + duDir, e);
320 }
321 }
322
323 /** Merge multiple Maven artifacts. */
324 void mergeM2Artifacts(Path mergeBnd) throws IOException {
325 Path duDir = mergeBnd.getParent();
326 String category = duDir.getParent().getFileName().toString();
327 Path targetCategoryBase = a2Base.resolve(category);
328
329 Properties mergeProps = new Properties();
330 try (InputStream in = Files.newInputStream(mergeBnd)) {
331 mergeProps.load(in);
332 }
333
334 String m2Version = mergeProps.getProperty(ARGEO_ORIGIN_M2.toString());
335 if (m2Version == null) {
336 logger.log(WARNING, "Ignoring " + duDir + " as it is not an M2-based distribution unit");
337 return;// ignore, this is probably an Eclipse archive
338 }
339 if (!m2Version.startsWith(":")) {
340 throw new IllegalStateException("Only the M2 version can be specified: " + m2Version);
341 }
342 m2Version = m2Version.substring(1);
343 mergeProps.put(ManifestConstants.BUNDLE_VERSION.toString(), m2Version);
344
345 String artifactsStr = mergeProps.getProperty(ManifestConstants.ARGEO_ORIGIN_M2_MERGE.toString());
346 if (artifactsStr == null)
347 throw new IllegalArgumentException(
348 mergeBnd + ": " + ManifestConstants.ARGEO_ORIGIN_M2_MERGE + " must be set");
349
350 String repoStr = mergeProps.containsKey(ARGEO_ORIGIN_M2_REPO.toString())
351 ? mergeProps.getProperty(ARGEO_ORIGIN_M2_REPO.toString())
352 : null;
353
354 String bundleSymbolicName = mergeProps.getProperty(ManifestConstants.BUNDLE_SYMBOLICNAME.toString());
355 if (bundleSymbolicName == null)
356 throw new IllegalArgumentException("Bundle-SymbolicName must be set in " + mergeBnd);
357 CategoryNameVersion nameVersion = new M2Artifact(category + ":" + bundleSymbolicName + ":" + m2Version);
358
359 Path targetBundleDir = targetCategoryBase.resolve(bundleSymbolicName + "." + nameVersion.getBranch());
360
361 String[] artifacts = artifactsStr.split(",");
362 artifacts: for (String str : artifacts) {
363 String m2Coordinates = str.trim();
364 if ("".equals(m2Coordinates))
365 continue artifacts;
366 M2Artifact artifact = new M2Artifact(m2Coordinates.trim());
367 if (artifact.getVersion() == null)
368 artifact.setVersion(m2Version);
369 URL url = M2ConventionsUtils.mavenRepoUrl(repoStr, artifact);
370 Path downloaded = downloadMaven(url, artifact);
371 JarEntry entry;
372 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(downloaded), false)) {
373 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
374 if (entry.isDirectory())
375 continue entries;
376 else if (entry.getName().endsWith(".RSA") || entry.getName().endsWith(".SF"))
377 continue entries;
378 else if (entry.getName().startsWith("META-INF/versions/"))
379 continue entries;
380 else if (entry.getName().startsWith("META-INF/maven/"))
381 continue entries;
382 else if (entry.getName().equals("module-info.class"))
383 continue entries;
384 else if (entry.getName().equals("META-INF/NOTICE"))
385 continue entries;
386 else if (entry.getName().equals("META-INF/NOTICE.txt"))
387 continue entries;
388 else if (entry.getName().equals("META-INF/LICENSE"))
389 continue entries;
390 else if (entry.getName().equals("META-INF/LICENSE.md"))
391 continue entries;
392 else if (entry.getName().equals("META-INF/LICENSE-notice.md"))
393 continue entries;
394 else if (entry.getName().equals("META-INF/DEPENDENCIES"))
395 continue entries;
396 if (entry.getName().startsWith(".cache/")) // Apache SSHD
397 continue entries;
398 Path target = targetBundleDir.resolve(entry.getName());
399 Files.createDirectories(target.getParent());
400 if (!Files.exists(target)) {
401 Files.copy(jarIn, target);
402 } else {
403 if (entry.getName().startsWith("META-INF/services/")) {
404 try (OutputStream out = Files.newOutputStream(target, StandardOpenOption.APPEND)) {
405 out.write("\n".getBytes());
406 jarIn.transferTo(out);
407 logger.log(DEBUG, artifact.getArtifactId() + " - Appended " + entry.getName());
408 }
409 } else if (entry.getName().startsWith("org/apache/batik/")) {
410 logger.log(TRACE, "Skip " + entry.getName());
411 continue entries;
412 } else {
413 throw new IllegalStateException("File " + target + " from " + artifact + " already exists");
414 }
415 }
416 logger.log(TRACE, () -> "Copied " + target);
417 }
418
419 }
420 downloadAndProcessM2Sources(repoStr, artifact, targetBundleDir);
421 }
422
423 // additional service files
424 Path servicesDir = duDir.resolve("services");
425 if (Files.exists(servicesDir)) {
426 for (Path p : Files.newDirectoryStream(servicesDir)) {
427 Path target = targetBundleDir.resolve("META-INF/services/").resolve(p.getFileName());
428 try (InputStream in = Files.newInputStream(p);
429 OutputStream out = Files.newOutputStream(target, StandardOpenOption.APPEND);) {
430 out.write("\n".getBytes());
431 in.transferTo(out);
432 logger.log(DEBUG, "Appended " + p);
433 }
434 }
435 }
436
437 Map<String, String> entries = new TreeMap<>();
438 try (Analyzer bndAnalyzer = new Analyzer()) {
439 bndAnalyzer.setProperties(mergeProps);
440 Jar jar = new Jar(targetBundleDir.toFile());
441 bndAnalyzer.setJar(jar);
442 Manifest manifest = bndAnalyzer.calcManifest();
443
444 keys: for (Object key : manifest.getMainAttributes().keySet()) {
445 Object value = manifest.getMainAttributes().get(key);
446
447 switch (key.toString()) {
448 case "Tool":
449 case "Bnd-LastModified":
450 case "Created-By":
451 continue keys;
452 }
453 if ("Require-Capability".equals(key.toString())
454 && value.toString().equals("osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.1))\""))
455 continue keys;// hack for very old classes
456 entries.put(key.toString(), value.toString());
457 }
458 } catch (Exception e) {
459 throw new RuntimeException("Cannot process " + mergeBnd, e);
460 }
461
462 Manifest manifest = new Manifest();
463 Path manifestPath = targetBundleDir.resolve("META-INF/MANIFEST.MF");
464 Files.createDirectories(manifestPath.getParent());
465 for (String key : entries.keySet()) {
466 String value = entries.get(key);
467 manifest.getMainAttributes().putValue(key, value);
468 }
469
470 try (OutputStream out = Files.newOutputStream(manifestPath)) {
471 manifest.write(out);
472 }
473 createJar(targetBundleDir);
474 }
475
476 /** Generate MANIFEST using BND. */
477 Path processBndJar(Path downloaded, Path targetCategoryBase, Properties fileProps, M2Artifact artifact) {
478
479 try {
480 Map<String, String> additionalEntries = new TreeMap<>();
481 boolean doNotModify = Boolean.parseBoolean(fileProps
482 .getOrDefault(ManifestConstants.ARGEO_ORIGIN_MANIFEST_NOT_MODIFIED.toString(), "false").toString());
483
484 // we always force the symbolic name
485
486 if (doNotModify) {
487 fileEntries: for (Object key : fileProps.keySet()) {
488 if (ManifestConstants.ARGEO_ORIGIN_M2.toString().equals(key))
489 continue fileEntries;
490 String value = fileProps.getProperty(key.toString());
491 additionalEntries.put(key.toString(), value);
492 }
493 } else {
494 if (artifact != null) {
495 if (!fileProps.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
496 fileProps.put(BUNDLE_SYMBOLICNAME.toString(), artifact.getName());
497 }
498 if (!fileProps.containsKey(BUNDLE_VERSION.toString())) {
499 fileProps.put(BUNDLE_VERSION.toString(), artifact.getVersion());
500 }
501 }
502
503 if (!fileProps.containsKey(EXPORT_PACKAGE.toString())) {
504 fileProps.put(EXPORT_PACKAGE.toString(),
505 "*;version=\"" + fileProps.getProperty(BUNDLE_VERSION.toString()) + "\"");
506 }
507
508 try (Analyzer bndAnalyzer = new Analyzer()) {
509 bndAnalyzer.setProperties(fileProps);
510 Jar jar = new Jar(downloaded.toFile());
511 bndAnalyzer.setJar(jar);
512 Manifest manifest = bndAnalyzer.calcManifest();
513
514 keys: for (Object key : manifest.getMainAttributes().keySet()) {
515 Object value = manifest.getMainAttributes().get(key);
516
517 switch (key.toString()) {
518 case "Tool":
519 case "Bnd-LastModified":
520 case "Created-By":
521 continue keys;
522 }
523 if ("Require-Capability".equals(key.toString())
524 && value.toString().equals("osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.1))\""))
525 continue keys;// !! hack for very old classes
526 additionalEntries.put(key.toString(), value.toString());
527 }
528 }
529 }
530 Path targetBundleDir = processBundleJar(downloaded, targetCategoryBase, additionalEntries);
531 logger.log(DEBUG, () -> "Processed " + downloaded);
532 return targetBundleDir;
533 } catch (Exception e) {
534 throw new RuntimeException("Cannot BND process " + downloaded, e);
535 }
536
537 }
538
539 /** Download and integrates sources for a single Maven artifact. */
540 void downloadAndProcessM2Sources(String repoStr, M2Artifact artifact, Path targetBundleDir) throws IOException {
541 try {
542 M2Artifact sourcesArtifact = new M2Artifact(artifact.toM2Coordinates(), "sources");
543 URL sourcesUrl = M2ConventionsUtils.mavenRepoUrl(repoStr, sourcesArtifact);
544 Path sourcesDownloaded = downloadMaven(sourcesUrl, artifact, true);
545 processM2SourceJar(sourcesDownloaded, targetBundleDir);
546 logger.log(TRACE, () -> "Processed source " + sourcesDownloaded);
547 } catch (Exception e) {
548 logger.log(ERROR, () -> "Cannot download source for " + artifact);
549 }
550
551 }
552
553 /** Integrate sources from a downloaded jar file. */
554 void processM2SourceJar(Path file, Path targetBundleDir) throws IOException {
555 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
556 Path targetSourceDir = sourceBundles
557 ? targetBundleDir.getParent().resolve(targetBundleDir.toString() + ".src")
558 : targetBundleDir.resolve("OSGI-OPT/src");
559
560 Files.createDirectories(targetSourceDir);
561 JarEntry entry;
562 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
563 if (entry.isDirectory())
564 continue entries;
565 if (entry.getName().startsWith("META-INF"))// skip META-INF entries
566 continue entries;
567 if (entry.getName().startsWith("module-info.java"))// skip META-INF entries
568 continue entries;
569 if (entry.getName().startsWith("/")) // absolute paths
570 continue entries;
571 Path target = targetSourceDir.resolve(entry.getName());
572 Files.createDirectories(target.getParent());
573 if (!Files.exists(target)) {
574 Files.copy(jarIn, target);
575 logger.log(TRACE, () -> "Copied source " + target);
576 } else {
577 logger.log(TRACE, () -> target + " already exists, skipping...");
578 }
579 }
580 }
581
582 }
583
584 /** Download a Maven artifact. */
585 Path downloadMaven(URL url, M2Artifact artifact) throws IOException {
586 return downloadMaven(url, artifact, false);
587 }
588
589 /** Download a Maven artifact. */
590 Path downloadMaven(URL url, M2Artifact artifact, boolean sources) throws IOException {
591 return download(url, mavenBase, artifact.getGroupId().replace(".", "/") //
592 + '/' + artifact.getArtifactId() + '/' + artifact.getVersion() //
593 + '/' + artifact.getArtifactId() + "-" + artifact.getVersion() + (sources ? "-sources" : "") + ".jar");
594 }
595
596 /*
597 * ECLIPSE ORIGIN
598 */
599 /** Process an archive in Eclipse format. */
600 void processEclipseArchive(Path duDir) {
601 try {
602 Path categoryRelativePath = descriptorsBase.relativize(duDir.getParent());
603 Path targetCategoryBase = a2Base.resolve(categoryRelativePath);
604 Files.createDirectories(targetCategoryBase);
605 // first delete all directories from previous builds
606 for (Path dir : Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p)))
607 deleteDirectory(dir);
608
609 Files.createDirectories(originBase);
610
611 Path commonBnd = duDir.resolve(COMMON_BND);
612 Properties commonProps = new Properties();
613 try (InputStream in = Files.newInputStream(commonBnd)) {
614 commonProps.load(in);
615 }
616 String url = commonProps.getProperty(ManifestConstants.ARGEO_ORIGIN_URI.toString());
617 if (url == null) {
618 url = uris.getProperty(duDir.getFileName().toString());
619 if (url == null)
620 throw new IllegalStateException("No url available for " + duDir);
621 commonProps.put(ManifestConstants.ARGEO_ORIGIN_URI.toString(), url);
622 }
623 Path downloaded = tryDownloadArchive(url, originBase);
624
625 FileSystem zipFs = FileSystems.newFileSystem(downloaded, (ClassLoader) null);
626
627 // filters
628 List<PathMatcher> includeMatchers = new ArrayList<>();
629 Properties includes = new Properties();
630 try (InputStream in = Files.newInputStream(duDir.resolve("includes.properties"))) {
631 includes.load(in);
632 }
633 for (Object pattern : includes.keySet()) {
634 PathMatcher pathMatcher = zipFs.getPathMatcher("glob:/" + pattern);
635 includeMatchers.add(pathMatcher);
636 }
637
638 List<PathMatcher> excludeMatchers = new ArrayList<>();
639 Path excludeFile = duDir.resolve("excludes.properties");
640 if (Files.exists(excludeFile)) {
641 Properties excludes = new Properties();
642 try (InputStream in = Files.newInputStream(excludeFile)) {
643 excludes.load(in);
644 }
645 for (Object pattern : excludes.keySet()) {
646 PathMatcher pathMatcher = zipFs.getPathMatcher("glob:/" + pattern);
647 excludeMatchers.add(pathMatcher);
648 }
649 }
650
651 Files.walkFileTree(zipFs.getRootDirectories().iterator().next(), new SimpleFileVisitor<Path>() {
652
653 @Override
654 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
655 includeMatchers: for (PathMatcher includeMatcher : includeMatchers) {
656 if (includeMatcher.matches(file)) {
657 for (PathMatcher excludeMatcher : excludeMatchers) {
658 if (excludeMatcher.matches(file)) {
659 logger.log(TRACE, "Skipping excluded " + file);
660 return FileVisitResult.CONTINUE;
661 }
662 }
663 if (file.getFileName().toString().contains(".source_")) {
664 processEclipseSourceJar(file, targetCategoryBase);
665 logger.log(DEBUG, () -> "Processed source " + file);
666 } else {
667 Map<String, String> map = new HashMap<>();
668 for (Object key : commonProps.keySet())
669 map.put(key.toString(), commonProps.getProperty(key.toString()));
670 processBundleJar(file, targetCategoryBase, map);
671 logger.log(DEBUG, () -> "Processed " + file);
672 }
673 break includeMatchers;
674 }
675 }
676 return FileVisitResult.CONTINUE;
677 }
678 });
679
680 DirectoryStream<Path> dirs = Files.newDirectoryStream(targetCategoryBase, (p) -> Files.isDirectory(p)
681 && p.getFileName().toString().indexOf('.') >= 0 && !p.getFileName().toString().endsWith(".src"));
682 for (Path dir : dirs) {
683 createJar(dir);
684 }
685 } catch (IOException e) {
686 throw new RuntimeException("Cannot process " + duDir, e);
687 }
688
689 }
690
691 /** Process sources in Eclipse format. */
692 void processEclipseSourceJar(Path file, Path targetBase) throws IOException {
693 try {
694 Path targetBundleDir;
695 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
696 Manifest manifest = jarIn.getManifest();
697
698 String[] relatedBundle = manifest.getMainAttributes().getValue(ECLIPSE_SOURCE_BUNDLE.toString())
699 .split(";");
700 String version = relatedBundle[1].substring("version=\"".length());
701 version = version.substring(0, version.length() - 1);
702 NameVersion nameVersion = new NameVersion(relatedBundle[0], version);
703 targetBundleDir = targetBase.resolve(nameVersion.getName() + "." + nameVersion.getBranch());
704
705 Path targetSourceDir = sourceBundles
706 ? targetBundleDir.getParent().resolve(targetBundleDir.toString() + ".src")
707 : targetBundleDir.resolve("OSGI-OPT/src");
708
709 Files.createDirectories(targetSourceDir);
710 JarEntry entry;
711 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
712 if (entry.isDirectory())
713 continue entries;
714 if (entry.getName().startsWith("META-INF"))// skip META-INF entries
715 continue entries;
716 Path target = targetSourceDir.resolve(entry.getName());
717 Files.createDirectories(target.getParent());
718 Files.copy(jarIn, target);
719 logger.log(TRACE, () -> "Copied source " + target);
720 }
721 }
722 } catch (IOException e) {
723 throw new IllegalStateException("Cannot process " + file, e);
724 }
725 }
726
727 /*
728 * COMMON PROCESSING
729 */
730 /** Normalise a bundle. */
731 Path processBundleJar(Path file, Path targetBase, Map<String, String> entries) throws IOException {
732 NameVersion nameVersion;
733 Path targetBundleDir;
734 try (JarInputStream jarIn = new JarInputStream(Files.newInputStream(file), false)) {
735 Manifest sourceManifest = jarIn.getManifest();
736 Manifest manifest = sourceManifest != null ? new Manifest(sourceManifest) : new Manifest();
737
738 // singleton
739 boolean isSingleton = false;
740 String rawSourceSymbolicName = manifest.getMainAttributes()
741 .getValue(ManifestConstants.BUNDLE_SYMBOLICNAME.toString());
742 if (rawSourceSymbolicName != null) {
743 // make sure there is no directive
744 String[] arr = rawSourceSymbolicName.split(";");
745 for (int i = 1; i < arr.length; i++) {
746 if (arr[i].trim().equals("singleton:=true"))
747 isSingleton = true;
748 logger.log(DEBUG, file.getFileName() + " is a singleton");
749 }
750 }
751 // remove problematic entries in MANIFEST
752 manifest.getEntries().clear();
753
754 String ourSymbolicName = entries.get(BUNDLE_SYMBOLICNAME.toString());
755 String ourVersion = entries.get(BUNDLE_VERSION.toString());
756
757 if (ourSymbolicName != null && ourVersion != null) {
758 nameVersion = new NameVersion(ourSymbolicName, ourVersion);
759 } else {
760 nameVersion = nameVersionFromManifest(manifest);
761 if (ourVersion != null && !nameVersion.getVersion().equals(ourVersion)) {
762 logger.log(WARNING,
763 "Original version is " + nameVersion.getVersion() + " while new version is " + ourVersion);
764 entries.put(BUNDLE_VERSION.toString(), ourVersion);
765 }
766 if (ourSymbolicName != null) {
767 // we always force our symbolic name
768 nameVersion.setName(ourSymbolicName);
769 }
770 }
771 targetBundleDir = targetBase.resolve(nameVersion.getName() + "." + nameVersion.getBranch());
772
773 // force Java 9 module name
774 entries.put(ManifestConstants.AUTOMATIC_MODULE_NAME.toString(), nameVersion.getName());
775
776 boolean isNative = false;
777 String os = null;
778 String arch = null;
779 if (targetBundleDir.startsWith(a2LibBase)) {
780 isNative = true;
781 Path libRelativePath = a2LibBase.relativize(targetBundleDir);
782 os = libRelativePath.getName(0).toString();
783 arch = libRelativePath.getName(1).toString();
784 }
785
786 // copy entries
787 JarEntry entry;
788 entries: while ((entry = jarIn.getNextJarEntry()) != null) {
789 if (entry.isDirectory())
790 continue entries;
791 if (entry.getName().endsWith(".RSA") || entry.getName().endsWith(".SF"))
792 continue entries;
793 if (entry.getName().endsWith("module-info.class")) // skip Java 9 module info
794 continue entries;
795 if (entry.getName().startsWith("META-INF/versions/")) // skip multi-version
796 continue entries;
797 // skip file system providers as they cause issues with native image
798 if (entry.getName().startsWith("META-INF/services/java.nio.file.spi.FileSystemProvider"))
799 continue entries;
800 if (entry.getName().startsWith("OSGI-OPT/src/")) // skip embedded sources
801 continue entries;
802 Path target = targetBundleDir.resolve(entry.getName());
803 Files.createDirectories(target.getParent());
804 Files.copy(jarIn, target);
805
806 // native libraries
807 if (isNative && (entry.getName().endsWith(".so") || entry.getName().endsWith(".dll")
808 || entry.getName().endsWith(".jnilib"))) {
809 Path categoryDir = targetBundleDir.getParent();
810 boolean copyDll = false;
811 Path targetDll = categoryDir.resolve(targetBundleDir.relativize(target));
812 if (nameVersion.getName().equals("com.sun.jna")) {
813 if (arch.equals("x86_64"))
814 arch = "x86-64";
815 if (os.equals("macosx"))
816 os = "darwin";
817 if (target.getParent().getFileName().toString().equals(os + "-" + arch)) {
818 copyDll = true;
819 }
820 targetDll = categoryDir.resolve(target.getFileName());
821 } else {
822 copyDll = true;
823 }
824 if (copyDll) {
825 Files.createDirectories(targetDll.getParent());
826 if (Files.exists(targetDll))
827 Files.delete(targetDll);
828 Files.copy(target, targetDll);
829 }
830 Files.delete(target);
831 }
832 logger.log(TRACE, () -> "Copied " + target);
833 }
834
835 // copy MANIFEST
836 Path manifestPath = targetBundleDir.resolve("META-INF/MANIFEST.MF");
837 Files.createDirectories(manifestPath.getParent());
838
839 if (isSingleton && entries.containsKey(BUNDLE_SYMBOLICNAME.toString())) {
840 entries.put(BUNDLE_SYMBOLICNAME.toString(),
841 entries.get(BUNDLE_SYMBOLICNAME.toString()) + ";singleton:=true");
842 }
843
844 // Final MANIFEST decisions
845 // This also where we check the original OSGi metadata and compare with our
846 // changes
847 for (String key : entries.keySet()) {
848 String value = entries.get(key);
849 String previousValue = manifest.getMainAttributes().getValue(key);
850 boolean wasDifferent = previousValue != null && !previousValue.equals(value);
851 boolean keepPrevious = false;
852 if (wasDifferent) {
853 if (SPDX_LICENSE_IDENTIFIER.toString().equals(key) && previousValue != null)
854 keepPrevious = true;
855 else if (BUNDLE_VERSION.toString().equals(key) && wasDifferent)
856 if (previousValue.equals(value + ".0")) // typically a Maven first release
857 keepPrevious = true;
858
859 if (keepPrevious) {
860 if (logger.isLoggable(TRACE))
861 logger.log(TRACE, file.getFileName() + ": " + key + " was NOT modified, value kept is "
862 + previousValue + ", not overriden with " + value);
863 value = previousValue;
864 }
865 }
866
867 manifest.getMainAttributes().putValue(key, value);
868 if (wasDifferent && !keepPrevious) {
869 if (IMPORT_PACKAGE.toString().equals(key) || EXPORT_PACKAGE.toString().equals(key))
870 logger.log(TRACE, () -> file.getFileName() + ": " + key + " was modified");
871 else
872 logger.log(WARNING, file.getFileName() + ": " + key + " was " + previousValue
873 + ", overridden with " + value);
874 }
875
876 // !! hack to remove unresolvable
877 if (key.equals("Provide-Capability") || key.equals("Require-Capability"))
878 if (nameVersion.getName().equals("osgi.core") || nameVersion.getName().equals("osgi.cmpn")) {
879 manifest.getMainAttributes().remove(key);
880 }
881 }
882
883 // last checks
884 String spdxLicenceId = manifest.getMainAttributes().getValue(SPDX_LICENSE_IDENTIFIER.toString());
885 String bundleLicense = manifest.getMainAttributes().getValue(BUNDLE_LICENSE.toString());
886 if (spdxLicenceId == null) {
887 logger.log(ERROR, file.getFileName() + ": " + SPDX_LICENSE_IDENTIFIER + " not available, "
888 + BUNDLE_LICENSE + " is " + bundleLicense);
889 } else {
890 if (!licensesUsed.containsKey(spdxLicenceId))
891 licensesUsed.put(spdxLicenceId, new TreeSet<>());
892 licensesUsed.get(spdxLicenceId).add(file.getFileName().toString());
893 }
894
895 try (OutputStream out = Files.newOutputStream(manifestPath)) {
896 manifest.write(out);
897 }
898 }
899 return targetBundleDir;
900 }
901
902 /*
903 * UTILITIES
904 */
905 /** Recursively deletes a directory. */
906 static void deleteDirectory(Path path) throws IOException {
907 if (!Files.exists(path))
908 return;
909 Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
910 @Override
911 public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
912 if (e != null)
913 throw e;
914 Files.delete(directory);
915 return CONTINUE;
916 }
917
918 @Override
919 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
920 Files.delete(file);
921 return CONTINUE;
922 }
923 });
924 }
925
926 /** Extract name/version from a MANIFEST. */
927 NameVersion nameVersionFromManifest(Manifest manifest) {
928 Attributes attrs = manifest.getMainAttributes();
929 // symbolic name
930 String symbolicName = attrs.getValue(ManifestConstants.BUNDLE_SYMBOLICNAME.toString());
931 if (symbolicName == null)
932 return null;
933 // make sure there is no directive
934 symbolicName = symbolicName.split(";")[0];
935
936 String version = attrs.getValue(ManifestConstants.BUNDLE_VERSION.toString());
937 return new NameVersion(symbolicName, version);
938 }
939
940 /** Try to download from an URI. */
941 Path tryDownloadArchive(String uri, Path dir) throws IOException {
942 // find mirror
943 List<String> urlBases = null;
944 String uriPrefix = null;
945 uriPrefixes: for (String uriPref : mirrors.keySet()) {
946 if (uri.startsWith(uriPref)) {
947 if (mirrors.get(uriPref).size() > 0) {
948 urlBases = mirrors.get(uriPref);
949 uriPrefix = uriPref;
950 break uriPrefixes;
951 }
952 }
953 }
954 if (urlBases == null)
955 try {
956 return downloadArchive(new URL(uri), dir);
957 } catch (FileNotFoundException e) {
958 throw new FileNotFoundException("Cannot find " + uri);
959 }
960
961 // try to download
962 for (String urlBase : urlBases) {
963 String relativePath = uri.substring(uriPrefix.length());
964 URL url = new URL(urlBase + relativePath);
965 try {
966 return downloadArchive(url, dir);
967 } catch (FileNotFoundException e) {
968 logger.log(WARNING, "Cannot download " + url + ", trying another mirror");
969 }
970 }
971 throw new FileNotFoundException("Cannot find " + uri);
972 }
973
974 /**
975 * Effectively download. Synchronised in order to avoid downloading twice in
976 * parallel.
977 */
978 synchronized Path downloadArchive(URL url, Path dir) throws IOException {
979 return download(url, dir, (String) null);
980 }
981
982 /** Effectively download. */
983 Path download(URL url, Path dir, String name) throws IOException {
984
985 Path dest;
986 if (name == null) {
987 // We use also use parent directory in case the archive itself has a fixed name
988 String[] segments = url.getPath().split("/");
989 name = segments.length > 1 ? segments[segments.length - 2] + '-' + segments[segments.length - 1]
990 : segments[segments.length - 1];
991 }
992
993 dest = dir.resolve(name);
994 if (Files.exists(dest)) {
995 logger.log(TRACE, () -> "File " + dest + " already exists for " + url + ", not downloading again");
996 return dest;
997 } else {
998 Files.createDirectories(dest.getParent());
999 }
1000
1001 try (InputStream in = url.openStream()) {
1002 Files.copy(in, dest);
1003 logger.log(DEBUG, () -> "Downloaded " + dest + " from " + url);
1004 }
1005 return dest;
1006 }
1007
1008 /** Create a JAR file from a directory. */
1009 Path createJar(Path bundleDir) throws IOException {
1010 // Create the jar
1011 Path jarPath = bundleDir.getParent().resolve(bundleDir.getFileName() + ".jar");
1012 Path manifestPath = bundleDir.resolve("META-INF/MANIFEST.MF");
1013 Manifest manifest;
1014 try (InputStream in = Files.newInputStream(manifestPath)) {
1015 manifest = new Manifest(in);
1016 }
1017 try (JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(jarPath), manifest)) {
1018 jarOut.setLevel(Deflater.DEFAULT_COMPRESSION);
1019 Files.walkFileTree(bundleDir, new SimpleFileVisitor<Path>() {
1020
1021 @Override
1022 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1023 if (file.getFileName().toString().equals("MANIFEST.MF"))
1024 return super.visitFile(file, attrs);
1025 JarEntry entry = new JarEntry(
1026 bundleDir.relativize(file).toString().replace(File.separatorChar, '/'));
1027 jarOut.putNextEntry(entry);
1028 Files.copy(file, jarOut);
1029 return super.visitFile(file, attrs);
1030 }
1031
1032 });
1033 }
1034 deleteDirectory(bundleDir);
1035
1036 if (sourceBundles) {
1037 Path bundleCategoryDir = bundleDir.getParent();
1038 Path sourceDir = bundleCategoryDir.resolve(bundleDir.toString() + ".src");
1039 if (!Files.exists(sourceDir)) {
1040 logger.log(WARNING, sourceDir + " does not exist, skipping...");
1041 return jarPath;
1042
1043 }
1044
1045 Path relPath = a2Base.relativize(bundleCategoryDir);
1046 Path srcCategoryDir = a2SrcBase.resolve(relPath);
1047 Path srcJarP = srcCategoryDir.resolve(sourceDir.getFileName() + ".jar");
1048 Files.createDirectories(srcJarP.getParent());
1049
1050 String bundleSymbolicName = manifest.getMainAttributes().getValue("Bundle-SymbolicName").toString();
1051 // in case there are additional directives
1052 bundleSymbolicName = bundleSymbolicName.split(";")[0];
1053 Manifest srcManifest = new Manifest();
1054 srcManifest.getMainAttributes().put(MANIFEST_VERSION, "1.0");
1055 srcManifest.getMainAttributes().putValue(BUNDLE_SYMBOLICNAME.toString(), bundleSymbolicName + ".src");
1056 srcManifest.getMainAttributes().putValue(BUNDLE_VERSION.toString(),
1057 manifest.getMainAttributes().getValue(BUNDLE_VERSION.toString()).toString());
1058 srcManifest.getMainAttributes().putValue(ECLIPSE_SOURCE_BUNDLE.toString(), bundleSymbolicName
1059 + ";version=\"" + manifest.getMainAttributes().getValue(BUNDLE_VERSION.toString()));
1060
1061 try (JarOutputStream srcJarOut = new JarOutputStream(Files.newOutputStream(srcJarP), srcManifest)) {
1062 srcJarOut.setLevel(Deflater.BEST_COMPRESSION);
1063 Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {
1064
1065 @Override
1066 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1067 if (file.getFileName().toString().equals("MANIFEST.MF"))
1068 return super.visitFile(file, attrs);
1069 JarEntry entry = new JarEntry(
1070 sourceDir.relativize(file).toString().replace(File.separatorChar, '/'));
1071 srcJarOut.putNextEntry(entry);
1072 Files.copy(file, srcJarOut);
1073 return super.visitFile(file, attrs);
1074 }
1075
1076 });
1077 }
1078 deleteDirectory(sourceDir);
1079 }
1080
1081 return jarPath;
1082 }
1083
1084 /** MANIFEST headers. */
1085 enum ManifestConstants {
1086 // OSGi
1087 /** OSGi bundle symbolic name. */
1088 BUNDLE_SYMBOLICNAME("Bundle-SymbolicName"), //
1089 /** OSGi bundle version. */
1090 BUNDLE_VERSION("Bundle-Version"), //
1091 /** OSGi bundle license. */
1092 BUNDLE_LICENSE("Bundle-License"), //
1093 /** OSGi exported packages list. */
1094 EXPORT_PACKAGE("Export-Package"), //
1095 /** OSGi imported packages list. */
1096 IMPORT_PACKAGE("Import-Package"), //
1097 // Java
1098 /** Java module name. */
1099 AUTOMATIC_MODULE_NAME("Automatic-Module-Name"), //
1100 // Eclipse
1101 /** Eclipse source bundle. */
1102 ECLIPSE_SOURCE_BUNDLE("Eclipse-SourceBundle"), //
1103 // SPDX
1104 /**
1105 * SPDX license identifier.
1106 *
1107 * @see https://spdx.org/licenses/
1108 */
1109 SPDX_LICENSE_IDENTIFIER("SPDX-License-Identifier"), //
1110 // Argeo Origin
1111 /**
1112 * Maven coordinates of the origin, possibly partial when using common.bnd or
1113 * merge.bnd.
1114 */
1115 ARGEO_ORIGIN_M2("Argeo-Origin-M2"), //
1116 /** List of Maven coordinates to merge. */
1117 ARGEO_ORIGIN_M2_MERGE("Argeo-Origin-M2-Merge"), //
1118 /** Maven repository if not the default one. */
1119 ARGEO_ORIGIN_M2_REPO("Argeo-Origin-M2-Repo"), //
1120 /**
1121 * Do not perform BND analysis of the origin component. Typically IMport_package
1122 * and Export-Package will be kept untouched.
1123 */
1124 ARGEO_ORIGIN_MANIFEST_NOT_MODIFIED("Argeo-Origin-ManifestNotModified"), //
1125 /**
1126 * Origin (non-Maven) URI of the component. It may be anything (jar, archive,
1127 * etc.).
1128 */
1129 ARGEO_ORIGIN_URI("Argeo-Origin-URI"), //
1130 ;
1131
1132 final String value;
1133
1134 private ManifestConstants(String value) {
1135 this.value = value;
1136 }
1137
1138 @Override
1139 public String toString() {
1140 return value;
1141 }
1142 }
1143 }
1144
1145 /** Simple representation of an M2 artifact. */
1146 class M2Artifact extends CategoryNameVersion {
1147 private String classifier;
1148
1149 M2Artifact(String m2coordinates) {
1150 this(m2coordinates, null);
1151 }
1152
1153 M2Artifact(String m2coordinates, String classifier) {
1154 String[] parts = m2coordinates.split(":");
1155 setCategory(parts[0]);
1156 setName(parts[1]);
1157 if (parts.length > 2) {
1158 setVersion(parts[2]);
1159 }
1160 this.classifier = classifier;
1161 }
1162
1163 String getGroupId() {
1164 return super.getCategory();
1165 }
1166
1167 String getArtifactId() {
1168 return super.getName();
1169 }
1170
1171 String toM2Coordinates() {
1172 return getCategory() + ":" + getName() + (getVersion() != null ? ":" + getVersion() : "");
1173 }
1174
1175 String getClassifier() {
1176 return classifier != null ? classifier : "";
1177 }
1178
1179 String getExtension() {
1180 return "jar";
1181 }
1182 }
1183
1184 /** Utilities around Maven (conventions based). */
1185 class M2ConventionsUtils {
1186 final static String MAVEN_CENTRAL_BASE_URL = "https://repo1.maven.org/maven2/";
1187
1188 /** The file name of this artifact when stored */
1189 static String artifactFileName(M2Artifact artifact) {
1190 return artifact.getArtifactId() + '-' + artifact.getVersion()
1191 + (artifact.getClassifier().equals("") ? "" : '-' + artifact.getClassifier()) + '.'
1192 + artifact.getExtension();
1193 }
1194
1195 /** Absolute path to the file */
1196 static String artifactPath(String artifactBasePath, M2Artifact artifact) {
1197 return artifactParentPath(artifactBasePath, artifact) + '/' + artifactFileName(artifact);
1198 }
1199
1200 /** Absolute path to the file */
1201 static String artifactUrl(String repoUrl, M2Artifact artifact) {
1202 if (repoUrl.endsWith("/"))
1203 return repoUrl + artifactPath("/", artifact).substring(1);
1204 else
1205 return repoUrl + artifactPath("/", artifact);
1206 }
1207
1208 /** Absolute path to the file */
1209 static URL mavenRepoUrl(String repoBase, M2Artifact artifact) {
1210 String url = artifactUrl(repoBase == null ? MAVEN_CENTRAL_BASE_URL : repoBase, artifact);
1211 try {
1212 return new URL(url);
1213 } catch (MalformedURLException e) {
1214 // it should not happen
1215 throw new IllegalStateException(e);
1216 }
1217 }
1218
1219 /** Absolute path to the directories where the files will be stored */
1220 static String artifactParentPath(String artifactBasePath, M2Artifact artifact) {
1221 return artifactBasePath + (artifactBasePath.endsWith("/") ? "" : "/") + artifactParentPath(artifact);
1222 }
1223
1224 /** Relative path to the directories where the files will be stored */
1225 static String artifactParentPath(M2Artifact artifact) {
1226 return artifact.getGroupId().replace('.', '/') + '/' + artifact.getArtifactId() + '/' + artifact.getVersion();
1227 }
1228
1229 /** Singleton */
1230 private M2ConventionsUtils() {
1231 }
1232 }
1233
1234 /** Combination of a category, a name and a version. */
1235 class CategoryNameVersion extends NameVersion {
1236 private String category;
1237
1238 CategoryNameVersion() {
1239 }
1240
1241 CategoryNameVersion(String category, String name, String version) {
1242 super(name, version);
1243 this.category = category;
1244 }
1245
1246 CategoryNameVersion(String category, NameVersion nameVersion) {
1247 super(nameVersion);
1248 this.category = category;
1249 }
1250
1251 String getCategory() {
1252 return category;
1253 }
1254
1255 void setCategory(String category) {
1256 this.category = category;
1257 }
1258
1259 @Override
1260 public String toString() {
1261 return category + ":" + super.toString();
1262 }
1263
1264 }
1265
1266 /** Combination of a name and a version. */
1267 class NameVersion implements Comparable<NameVersion> {
1268 private String name;
1269 private String version;
1270
1271 NameVersion() {
1272 }
1273
1274 /** Interprets string in OSGi-like format my.module.name;version=0.0.0 */
1275 NameVersion(String nameVersion) {
1276 int index = nameVersion.indexOf(";version=");
1277 if (index < 0) {
1278 setName(nameVersion);
1279 setVersion(null);
1280 } else {
1281 setName(nameVersion.substring(0, index));
1282 setVersion(nameVersion.substring(index + ";version=".length()));
1283 }
1284 }
1285
1286 NameVersion(String name, String version) {
1287 this.name = name;
1288 this.version = version;
1289 }
1290
1291 NameVersion(NameVersion nameVersion) {
1292 this.name = nameVersion.getName();
1293 this.version = nameVersion.getVersion();
1294 }
1295
1296 String getName() {
1297 return name;
1298 }
1299
1300 void setName(String name) {
1301 this.name = name;
1302 }
1303
1304 String getVersion() {
1305 return version;
1306 }
1307
1308 void setVersion(String version) {
1309 this.version = version;
1310 }
1311
1312 String getBranch() {
1313 String[] parts = getVersion().split("\\.");
1314 if (parts.length < 2)
1315 throw new IllegalStateException("Version " + getVersion() + " cannot be interpreted as branch.");
1316 return parts[0] + "." + parts[1];
1317 }
1318
1319 @Override
1320 public boolean equals(Object obj) {
1321 if (obj instanceof NameVersion) {
1322 NameVersion nameVersion = (NameVersion) obj;
1323 return name.equals(nameVersion.getName()) && version.equals(nameVersion.getVersion());
1324 } else
1325 return false;
1326 }
1327
1328 @Override
1329 public int hashCode() {
1330 return name.hashCode();
1331 }
1332
1333 @Override
1334 public String toString() {
1335 return name + ":" + version;
1336 }
1337
1338 public int compareTo(NameVersion o) {
1339 if (o.getName().equals(name))
1340 return version.compareTo(o.getVersion());
1341 else
1342 return name.compareTo(o.getName());
1343 }
1344 }