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