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