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