diff --git a/biz.aQute.bndlib.tests/test/test/BuilderTest.java b/biz.aQute.bndlib.tests/test/test/BuilderTest.java index 25d390aed2..675fdfede0 100644 --- a/biz.aQute.bndlib.tests/test/test/BuilderTest.java +++ b/biz.aQute.bndlib.tests/test/test/BuilderTest.java @@ -1124,6 +1124,50 @@ public void testNamesection() throws Exception { } + @Test + public void testPackageNamesection() throws Exception { + Builder b = new Builder(); + try { + b.addClasspath(IO.getFile("jar/osgi.jar")); + b.setProperty(Constants.NAMESECTION, + "org/osgi/service/event/;Foo=bar"); + b.setProperty(Constants.PRIVATEPACKAGE, "org.osgi.service.event"); + Jar build = b.build(); + assertOk(b); + assertTrue(b.check()); + Manifest m = build.getManifest(); + m.write(System.err); + + assertNotNull(m.getAttributes( + "org/osgi/service/event/") + .getValue("Foo")); + } finally { + b.close(); + } + + } + + @Test + public void testGlobPackageNamesection() throws Exception { + Builder b = new Builder(); + try { + b.addClasspath(IO.getFile("jar/osgi.jar")); + b.setProperty(Constants.NAMESECTION, "org/osgi/service/*/;Foo=bar"); + b.setProperty(Constants.PRIVATEPACKAGE, "org.osgi.service.event"); + Jar build = b.build(); + assertOk(b); + assertTrue(b.check()); + Manifest m = build.getManifest(); + m.write(System.err); + + assertNotNull(m.getAttributes("org/osgi/service/event/") + .getValue("Foo")); + } finally { + b.close(); + } + + } + /** * Test the digests */ diff --git a/biz.aQute.bndlib/src/aQute/bnd/help/Syntax.java b/biz.aQute.bndlib/src/aQute/bnd/help/Syntax.java index 758f549e1d..ef7e58bd42 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/help/Syntax.java +++ b/biz.aQute.bndlib/src/aQute/bnd/help/Syntax.java @@ -557,8 +557,9 @@ null, null, new Syntax("name", "The display name of the developer", "name='Peter new Syntax(NOEE, "Do not calculate the osgi.ee name space Execution Environment from the class file version.", NOEE + "=true", "true,false", Verifier.TRUEORFALSEPATTERN), new Syntax(NAMESECTION, - "Create a name section (second part of manifest) with optional property expansion and addition of custom attributes.", - NAMESECTION + "=*;baz=true, abc/def/bar/X.class=3", null, null), + "Create a name section (second part of manifest) with optional property expansion and addition of custom attributes. Patterns not ending with \"/\" target resources. Those ending with \"/\" target packages.", + NAMESECTION + "=*;baz=true, abc/def/bar/X.class;bar=3", null, + null), new Syntax(OUTPUT, "Specify the output directory or file.", OUTPUT + "=my_directory", null, null), new Syntax(OUTPUTMASK, "If set, is used a template to calculate the output file. It can use any macro but the ${@bsn} and ${@version} macros refer to the current JAR being saved. The default is bsn + \".jar\".", diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java index 64b1b09438..c582f5b067 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java @@ -1295,6 +1295,14 @@ private void doNamesection(Jar dot, Manifest manifest) { Instructions instructions = new Instructions(namesection); Set resources = new HashSet<>(dot.getResources() .keySet()); + // Support package attributes. See + // https://docs.oracle.com/javase/8/docs/technotes/guides/versioning/spec/versioning2.html#wp89936 + MapStream.of(dot.getDirectories()) + .filterValue(mdir -> Objects.nonNull(mdir) && !mdir.isEmpty()) + .keys() + .map(d -> d.concat( + "/")) + .forEach(resources::add); // // For each instruction, iterator over the resources and filter @@ -1312,8 +1320,12 @@ private void doNamesection(Jar dot, Manifest manifest) { String path = i.next(); // For each resource - if (instr.getKey() - .matches(path)) { + Instruction instruction = instr.getKey(); + + if (path.endsWith("/") && !instruction.toString() + .endsWith("/")) { + // continue + } else if (instruction.matches(path)) { // Instruction matches the resource diff --git a/docs/_instructions/namesection.md b/docs/_instructions/namesection.md index 46437c6e69..3e8e694dde 100644 --- a/docs/_instructions/namesection.md +++ b/docs/_instructions/namesection.md @@ -2,81 +2,63 @@ layout: default class: Builder title: -namesection RESOURCE-SPEC ( ',' RESOURCE-SPEC ) * -summary: Create a name section (second part of manifest) with optional property expansion and addition of custom attributes. +summary: Create a name section (second part of manifest) with optional property expansion and addition of custom attributes. Patterns not ending with \"/\" target resources. Those ending with \"/\" target packages. --- - /** - * Parse the namesection as instructions and then match them against the - * current set of resources For example: - * - *
-	 * 	-namesection: *;baz=true, abc/def/bar/X.class=3
-	 * 
- * - * The raw value of {@link Constants#NAMESECTION} is used but the values of - * the attributes are replaced where @ is set to the resource name. This - * allows macro to operate on the resource - */ +Create a name section (second part of manifest) with optional property expansion and addition of custom attributes. - private void doNamesection(Jar dot, Manifest manifest) { +### Matching +The key of the `-namesection` instruction is an _ant style_ glob. And there are two target groups for matching: - Parameters namesection = parseHeader(getProperties().getProperty(NAMESECTION)); - Instructions instructions = new Instructions(namesection); - Set resources = new HashSet(dot.getResources().keySet()); +* **resources** - the pattern not ending with `/` or is an exact match for a resource path +* **packages** - the pattern ends with `/` or is an exact match for a package path - // - // For each instruction, iterator over the resources and filter - // them. If a resource matches, it must be removed even if the - // instruction is negative. If positive, add a name section - // to the manifest for the given resource name. Then add all - // attributes from the instruction to that name section. - // - for (Map.Entry instr : instructions.entrySet()) { - boolean matched = false; +#### Custom attributes - // For each instruction +The goal of named sections is to provide attributes over a specific subset of resources and paths in the jar file. Attributes are specified using the same syntax used elsewhere (such as package attributes). Attributes can contain properties and macros for expansion and replacement. - for (Iterator i = resources.iterator(); i.hasNext();) { - String path = i.next(); - // For each resource +Each attribute is processed by bnd and the matching value is passed using the `@` property. - if (instr.getKey().matches(path)) { +#### Resources +Resources are targeted by using a glob pattern not ending with `/`. - // Instruction matches the resource +For example, the following instruction sets the content type attribute for `png` files: - matched = true; - if (!instr.getKey().isNegated()) { +```properties +-namesection: com/foo/*.png; Content-Type=image/png +``` - // Positive match, add the attributes +This produces a result like the following: +```properties +Name: org/foo/icon_12x12.png +Content-Type: image/png - Attributes attrs = manifest.getAttributes(path); - if (attrs == null) { - attrs = new Attributes(); - manifest.getEntries().put(path, attrs); - } +Name: org/foo/icon_48x48.png +Content-Type: image/png +``` - // - // Add all the properties from the instruction to the - // name section - // +#### Packages +Packages are targeted by using a glob pattern that ends with `/`. - for (Map.Entry property : instr.getValue().entrySet()) { - setProperty("@", path); - try { - String processed = getReplacer().process(property.getValue()); - attrs.putValue(property.getKey(), processed); - } - finally { - unsetProperty("@"); - } - } - } - i.remove(); - } - } +For example, to produce a Java [Package Version Information](https://docs.oracle.com/javase/tutorial/deployment/jar/packageman.html) section use an instruction like this one: +```properties +-namesection: jakarta/annotation/*/;\ + Specification-Title=Jakarta Annotations;\ + Specification-Version=${annotation.spec.version};\ + Specification-Vendor=Eclipse Foundation;\ + Implementation-Title=jakarta.annotation;\ + Implementation-Version=${annotation.spec.version}.${annotation.revision};\ + Implementation-Vendor=Apache Software Foundation +``` - if (!matched && resources.size() > 0) - warning("The instruction %s in %s did not match any resources", instr.getKey(), NAMESECTION); - } +This produces a result like the following: +```properties +Name: jakarta/annotation/ +Implementation-Title: jakarta.annotation +Implementation-Vendor: Apache Software Foundation +Implementation-Version: 2.0.0-M1 +Specification-Title: Jakarta Annotations +Specification-Vendor: Eclipse Foundation +Specification-Version: 2.0 +``` - }