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