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