diff --git a/biz.aQute.bndlib.tests/test/test/AttrsTest.java b/biz.aQute.bndlib.tests/test/test/AttrsTest.java index a61de99206..369d61b76f 100644 --- a/biz.aQute.bndlib.tests/test/test/AttrsTest.java +++ b/biz.aQute.bndlib.tests/test/test/AttrsTest.java @@ -1,5 +1,6 @@ package test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.Arrays; @@ -8,6 +9,7 @@ import org.junit.jupiter.api.Test; import aQute.bnd.header.Attrs; +import aQute.bnd.header.OSGiHeader; import aQute.bnd.version.Version; public class AttrsTest { @@ -65,4 +67,25 @@ public void testVersion() { assertEquals("version:Version=\"1.2.3\";versions:List=\"1.2.3,2.1.0\"", attr.toString()); } + @Test + public void testSorting() { + assertThat(attrs("b=1;a=2;c=3;d=4;$:=0").sort() + .toString()).isEqualTo("$:=0;a=2;b=1;c=3;d=4"); + } + + @Test + public void testComparing() { + assertThat(attrs("a=1").compareTo(attrs("a=1"))).isEqualTo(0); + assertThat(attrs("a=1").compareTo(attrs("a=2"))).isEqualTo(-1); + assertThat(attrs("a=2").compareTo(attrs("a=1"))).isEqualTo(1); + + assertThat(attrs("a=1").compareTo(attrs("a=1;b=1"))).isEqualTo(-1); + assertThat(attrs("a=1;b=1").compareTo(attrs("a=1"))).isEqualTo(1); + } + + private Attrs attrs(String string) { + return OSGiHeader.parseHeader("x;" + string) + .get("x"); + } + } diff --git a/biz.aQute.bndlib.tests/test/test/ProcessorTest.java b/biz.aQute.bndlib.tests/test/test/ProcessorTest.java index 0095b91ccc..5290379da0 100644 --- a/biz.aQute.bndlib.tests/test/test/ProcessorTest.java +++ b/biz.aQute.bndlib.tests/test/test/ProcessorTest.java @@ -25,6 +25,7 @@ import aQute.bnd.osgi.resource.RequirementBuilder; import aQute.bnd.osgi.resource.ResourceBuilder; import aQute.bnd.osgi.resource.ResourceUtils; +import aQute.bnd.service.parameters.ConsolidateParameters; import aQute.lib.collections.ExtList; import aQute.lib.strings.Strings; import aQute.service.reporter.Reporter.SetLocation; @@ -529,4 +530,48 @@ public void testMergAndSuffixes() throws IOException { } } + + @Test + public void testClauses() throws IOException { + try (Processor p = new Processor()) { + assertThat(p.getParameters("foo")).isNotNull() + .isEmpty(); + + p.addClause("FOO", "abc", new Attrs()); + p.addClause("FOO", "def", new Attrs()); + p.addClause("FOO", "abc", new Attrs()); + assertThat(p.getParameters("FOO")).hasSize(3); + assertThat(p.getParameters("FOO") + .toString()).isEqualTo("abc,def,abc"); + assertThat(p.getProperty("FOO")).isEqualTo("abc,def,abc"); + p.addClause("FOO", "abc", new Attrs()); + assertThat(p.getProperty("FOO")).isEqualTo("abc,def,abc,abc"); + + p.setProperty("FOO", "nothing"); + assertThat(p.getParameters("FOO") + .toString()).isEqualTo("nothing"); + + assertThat(p.getParameters("FOO")).isNotNull() + .containsKey("nothing"); + + p.addBasicPlugin((ConsolidateParameters) (k, pars) -> { + if (!k.equals("FOO")) + return null; + return pars.sort(); + }); + + p.addClause("FOO", "def", new Attrs().set("a", "2")); + p.addClause("FOO", "abc", new Attrs().set("a", "3")); + p.addClause("FOO", "def", new Attrs().set("a", "1")); + p.addClause("FOO", "def", new Attrs().set("a", "1") + .set("b", "1")); + + assertThat(p.getParameters("FOO") + .toString()).isEqualTo("nothing,def;a=2,abc;a=3,def;a=1,def;a=1;b=1"); + + assertThat(p.getProperty("FOO")).isEqualTo("abc;a=3,def;a=1,def;a=1;b=1,def;a=2,nothing"); + + } + } + } diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/build/package-info.java index 370d8caa13..c2000bf5d5 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/build/package-info.java +++ b/biz.aQute.bndlib/src/aQute/bnd/build/package-info.java @@ -1,6 +1,6 @@ /** */ -@Version("4.3.0") +@Version("4.4.0") package aQute.bnd.build; import org.osgi.annotation.versioning.Version; diff --git a/biz.aQute.bndlib/src/aQute/bnd/header/Attrs.java b/biz.aQute.bndlib/src/aQute/bnd/header/Attrs.java index 9280b1a504..eb3777af12 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/header/Attrs.java +++ b/biz.aQute.bndlib/src/aQute/bnd/header/Attrs.java @@ -4,7 +4,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -71,6 +73,8 @@ public interface DataType { public static final DataType> LIST_DOUBLE = () -> Type.DOUBLES; public static final DataType> LIST_VERSION = () -> Type.VERSIONS; + public static Comparator COMPARATOR = Attrs::compareTo; + /** * Pattern for List with list type */ @@ -87,6 +91,7 @@ private Attrs(Map map, Map types) { this.types = types; } + public Attrs() { this(new LinkedHashMap<>(), new HashMap<>()); } @@ -634,4 +639,53 @@ public Attrs select(Predicate predicate) { }); return attrs; } + + /** + * Return a new Attributes that is sorted by key + * + * @return a sorted attributes + */ + public Attrs sort() { + Attrs attrs = new Attrs(); + this.entrySet() + .stream() + .sorted((a, b) -> a.getKey() + .compareTo(b.getKey())) + .forEach(e -> attrs.put(e.getKey(), e.getValue())); + return attrs; + } + + public Attrs set(String k, String v) { + put(k, v); + return this; + } + + public int compareTo(Attrs b) { + Attrs a = this; + Iterator> ia = a.entrySet() + .iterator(); + Iterator> ib = b.entrySet() + .iterator(); + while (ia.hasNext()) { + if (!ib.hasNext()) + return 1; + + Entry ea = ia.next(); + Entry eb = ib.next(); + int n = ea.getKey() + .compareTo(eb.getKey()); + if (n != 0) + return n; + + n = ea.getValue() + .compareTo(eb.getValue()); + if (n != 0) + return n; + } + if (ib.hasNext()) { + return -1; + } + return 0; + } + } diff --git a/biz.aQute.bndlib/src/aQute/bnd/header/Parameters.java b/biz.aQute.bndlib/src/aQute/bnd/header/Parameters.java index 8a36901f50..71d04e6381 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/header/Parameters.java +++ b/biz.aQute.bndlib/src/aQute/bnd/header/Parameters.java @@ -8,9 +8,11 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collector; import aQute.bnd.stream.MapStream; +import aQute.lib.collections.MultiMap; import aQute.service.reporter.Reporter; public class Parameters implements Map { @@ -268,4 +270,38 @@ private static Parameters combiner(Parameters t, Parameters u) { public Map> toBasic() { return (Map) this; } + + /** + * A Parameters can contain the same key multiple times. This maps the + * clauses in the Parameters to a multi map with a clean key and a number of + * attributes. + * + * @return a fresh multi map + */ + public Map> flatten() { + MultiMap mmap = new MultiMap<>(); + for (Map.Entry e : map.entrySet()) { + mmap.add(removeDuplicateMarker(e.getKey()), e.getValue()); + } + return mmap; + } + + /** + * Return a sorted Parameters that has the same keys and attributes but that + * is sorted by key and attributes. + * + * @return a sorted Parameters (this is a fresh instance) + */ + public Parameters sort() { + Map> flatten = flatten(); + Parameters result = new Parameters(); + for (String s : new TreeSet<>(flatten.keySet())) { + flatten.get(s) + .stream() + .map(a -> a.sort()) + .sorted(Attrs.COMPARATOR) + .forEach(attrs -> result.add(s, attrs)); + } + return result; + } } diff --git a/biz.aQute.bndlib/src/aQute/bnd/header/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/header/package-info.java index 92c5faf8e4..41148d51f3 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/header/package-info.java +++ b/biz.aQute.bndlib/src/aQute/bnd/header/package-info.java @@ -1,4 +1,4 @@ -@Version("2.5.0") +@Version("2.6.0") package aQute.bnd.header; import org.osgi.annotation.versioning.Version; diff --git a/biz.aQute.bndlib/src/aQute/bnd/junit/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/junit/package-info.java index 4ef9e3462a..6c649521aa 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/junit/package-info.java +++ b/biz.aQute.bndlib/src/aQute/bnd/junit/package-info.java @@ -1,2 +1,2 @@ -@org.osgi.annotation.versioning.Version("2.1.0") +@org.osgi.annotation.versioning.Version("2.2.0") package aQute.bnd.junit; diff --git a/biz.aQute.bndlib/src/aQute/bnd/maven/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/maven/package-info.java index 5b86647b0c..e2c2921adb 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/maven/package-info.java +++ b/biz.aQute.bndlib/src/aQute/bnd/maven/package-info.java @@ -1,6 +1,6 @@ /** */ -@Version("3.3.0") +@Version("3.4.0") package aQute.bnd.maven; import org.osgi.annotation.versioning.Version; diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/ClauseManager.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/ClauseManager.java new file mode 100644 index 0000000000..c400b9e076 --- /dev/null +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/ClauseManager.java @@ -0,0 +1,77 @@ +package aQute.bnd.osgi; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import aQute.bnd.header.Attrs; +import aQute.bnd.header.Parameters; +import aQute.bnd.service.parameters.ConsolidateParameters; + +class ClauseManager { + final static Logger logger = LoggerFactory.getLogger(ClauseManager.class); + final Processor processor; + + final Map parameters = new HashMap<>(); + + ClauseManager(Processor processor) { + this.processor = processor; + } + + void addClause(String key, String name, Attrs ps) { + Parameters p = getParameters(key); + p.add(name, ps); + } + + Parameters getParameters(String key) { + return parameters.computeIfAbsent(key, k -> { + String property = processor.getProperty(key); + return new Parameters(property); + }); + } + + Optional getOptionalParameters(String key) { + return Optional.ofNullable(parameters.get(key)); + } + + void reset(String key) { + if (parameters.remove(key) != null) { + logger.info( + "a clause was set but then cleared due a property overwrite. This might indicate that not all code treats the header {} the same", + key); + } + } + + void reset() { + parameters.clear(); + } + + void consolidate(String key) { + Parameters parameters = this.parameters.get(key); + if (parameters == null) + return; + + try { + for (ConsolidateParameters cp : processor.getPlugins(ConsolidateParameters.class)) { + Parameters consolidated = cp.consolidate(key, parameters); + if (consolidated != null) { + processor.setProperty(key, consolidated.toString()); + return; + } + } + processor.setProperty(key, parameters.toString()); + } finally { + assert !this.parameters.containsKey(key) : "should be cleared by processor"; + } + } + + void consolidate() { + for (String key : new ArrayList<>(parameters.keySet())) { + consolidate(key); + } + } +} diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java index 44a3c47ba7..b97a18cacc 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java @@ -135,6 +135,7 @@ public class Processor extends Domain implements Reporter, Registry, Constants, Collection filter; Boolean strict; boolean fixupMessages; + final ClauseManager clauses = new ClauseManager(this); public static class FileLine { public static final FileLine DUMMY = new FileLine(null, 0, 0); @@ -940,6 +941,7 @@ public void forceRefresh() { } public void propertiesChanged() { + clauses.reset(); Processor p = getParent(); if (p != null) { updateModified(p.lastModified(), "propertiesChanged"); @@ -1068,6 +1070,8 @@ private String getLiteralProperty(String key, String deflt, Processor source, bo String value = null; // Use the key as is first, if found ok + clauses.consolidate(key); + for (Processor proc = source; proc != null; proc = proc.getParent()) { Object raw = proc.getProperties() .get(key); @@ -1292,7 +1296,9 @@ public long lastModified() { * @param value */ public void setProperty(String key, String value) { - getProperties().put(normalizeKey(key), value); + String normalizeKey = normalizeKey(key); + getProperties().put(normalizeKey, value); + clauses.reset(normalizeKey); } /** @@ -2630,4 +2636,27 @@ private List getSelfAndAncestors(List l) { public void setPropertiesFile(File source) { this.propertiesFile = source; } + + /** + * Add a clause to a Parameters like header + * + * @param key the header key of the Parameters, e.g. Require-Capability + * @param name the name of the clause inside the parameters + * @param attrs the set of attributes + */ + + public void addClause(String key, String name, Attrs attrs) { + clauses.addClause(key, name, attrs); + } + + /** + * Get the parameters for a property. + * + * @param key the header key of the Parameters, e.g. Require-Capability + */ + + @Override + public Parameters getParameters(String key) { + return clauses.getParameters(key); + } } diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/repository/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/repository/package-info.java index 2bf5f9e3d3..5d49631c5e 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/repository/package-info.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/repository/package-info.java @@ -1,6 +1,6 @@ /** */ -@Version("3.0.0") +@Version("3.1.0") package aQute.bnd.osgi.repository; import org.osgi.annotation.versioning.Version; diff --git a/biz.aQute.bndlib/src/aQute/bnd/print/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/print/package-info.java index e5f819bfaf..9fa77110fa 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/print/package-info.java +++ b/biz.aQute.bndlib/src/aQute/bnd/print/package-info.java @@ -1,4 +1,4 @@ -@Version("2.0.0") +@Version("2.1.0") package aQute.bnd.print; import org.osgi.annotation.versioning.Version; diff --git a/biz.aQute.bndlib/src/aQute/bnd/service/generate/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/service/generate/package-info.java index ce1517f3bb..0a009bec34 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/service/generate/package-info.java +++ b/biz.aQute.bndlib/src/aQute/bnd/service/generate/package-info.java @@ -1,2 +1,2 @@ -@org.osgi.annotation.versioning.Version("2.0.0") +@org.osgi.annotation.versioning.Version("2.1.0") package aQute.bnd.service.generate; diff --git a/biz.aQute.bndlib/src/aQute/bnd/service/parameters/ConsolidateParameters.java b/biz.aQute.bndlib/src/aQute/bnd/service/parameters/ConsolidateParameters.java new file mode 100644 index 0000000000..a7646b7664 --- /dev/null +++ b/biz.aQute.bndlib/src/aQute/bnd/service/parameters/ConsolidateParameters.java @@ -0,0 +1,26 @@ +package aQute.bnd.service.parameters; + +import aQute.bnd.header.Parameters; + +/** + * This interface is a Processor plugin that is consolidating a Parameters. In + * OSGi most headers follow Parameters syntax. However, different parts of the + * system might contribute one or more clauses to the same header. The addClause + * method in Processor can be used to add a single clause. This interface is + * used to consolidate the aggregate. Some headers need the removal of + * duplicates and and other headers require sorting to ensure they are + * consistent between builds. The default ordering of the Parameters is in order + * of insert. Note that the Parameters can handle duplicate keys by suffixing + * them with a `~`. + */ +public interface ConsolidateParameters { + /** + * Consolidate a Parameters. If the key is not recognized, return null. + * + * @param key the key/header, for example Require-Capability + * @param parameters the parameters to consolidate. Do not modify it. + * @return null if unknown key, otherwise a consolidated Parameters + */ + Parameters consolidate(String key, Parameters parameters); + +} diff --git a/biz.aQute.bndlib/src/aQute/bnd/service/parameters/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/service/parameters/package-info.java new file mode 100644 index 0000000000..4283cfbfd7 --- /dev/null +++ b/biz.aQute.bndlib/src/aQute/bnd/service/parameters/package-info.java @@ -0,0 +1,2 @@ +@org.osgi.annotation.versioning.Version("1.0.0") +package aQute.bnd.service.parameters;