diff --git a/biz.aQute.bndlib.tests/test/test/ProcessorTest.java b/biz.aQute.bndlib.tests/test/test/ProcessorTest.java index c2abbeb457..2d86beb083 100644 --- a/biz.aQute.bndlib.tests/test/test/ProcessorTest.java +++ b/biz.aQute.bndlib.tests/test/test/ProcessorTest.java @@ -22,6 +22,7 @@ import aQute.bnd.osgi.Constants; import aQute.bnd.osgi.OSInformation; import aQute.bnd.osgi.Processor; +import aQute.bnd.osgi.Processor.PropertyKey; import aQute.bnd.osgi.resource.RequirementBuilder; import aQute.bnd.osgi.resource.ResourceBuilder; import aQute.bnd.osgi.resource.ResourceUtils; @@ -623,6 +624,27 @@ public void testMergAndSuffixes() throws IOException { } + @Test + public void testPropertyKeys() throws IOException { + try (Processor top = new Processor()) { + top.setProperty("foo+.1", "x,y,z"); + top.setProperty("foo+.2", "x,y,z"); + top.setProperty("foo++", "d,e,f"); + try (Processor bottom = new Processor(top)) { + bottom.setProperty("foo+", "a,b,c"); + bottom.setProperty("foo+.2", "x,y,z"); + List keys = bottom.getMergePropertyKeys("foo+"); + assertThat(keys).hasSize(4); + assertThat(keys).containsExactly(// + new PropertyKey(bottom, "foo+", 0), // + new PropertyKey(top, "foo+.1", 1), // + new PropertyKey(bottom, "foo+.2", 0), // + new PropertyKey(top, "foo+.2", 1)); + } + } + + } + @Test public void testIncludeItself() throws IOException { File foo = IO.getFile("generated/foo.bnd"); diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/Project.java b/biz.aQute.bndlib/src/aQute/bnd/build/Project.java index fb147c90e7..39c68a90ae 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/build/Project.java +++ b/biz.aQute.bndlib/src/aQute/bnd/build/Project.java @@ -3656,4 +3656,40 @@ private List parseBuildResources() { } return result; } + + /** + * Find a processor that is inheriting from a project. This is either the + * bnd.bnd file or a sub bnd. + * + * @param file the file that contains the properties + * @return a processor properly setup for the Workspace inheritance or empty + */ + public Optional findProcessor(File file) { + + if (file.equals(getPropertiesFile())) + return Optional.of(this); + + File projectDir = file.getParentFile(); + if (!projectDir.equals(getBase())) + return Optional.empty(); + + try { + if (file.getName() + .endsWith(".bndrun")) + return Optional.of(Run.createRun(getWorkspace(), file)); + + try (ProjectBuilder builder = getBuilder(null)) { + for (Builder b : builder.getSubBuilders()) { + if (file.equals(b.getPropertiesFile())) { + Processor sub = new Processor(this); + sub.setProperties(file); + return Optional.of(sub); + } + } + } + return Optional.empty(); + } catch (Exception e) { + throw Exceptions.duck(e); + } + } } diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/Workspace.java b/biz.aQute.bndlib/src/aQute/bnd/build/Workspace.java index c26a138ea9..63820ffe13 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/build/Workspace.java +++ b/biz.aQute.bndlib/src/aQute/bnd/build/Workspace.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.SortedSet; @@ -123,6 +124,7 @@ public class Workspace extends Processor { public static final String EXT = "ext"; public static final String BUILDFILE = "build.bnd"; public static final String CNFDIR = "cnf"; + public static final String CNF_BUILD_BND = CNFDIR + "/" + BUILDFILE; public static final String CACHEDIR = "cache/" + About.CURRENT; public static final String STANDALONE_REPO_CLASS = "aQute.bnd.repository.osgi.OSGiRepository"; @@ -2009,4 +2011,27 @@ protected Properties magicBnd(File file) throws IOException { } } + /** + * Find the Processor that has the give file as properties. + * + * @param file the file that should match the Project or Workspace + * @return an optional Processor + */ + public Optional findProcessor(File file) { + File cnf = getFile(CNF_BUILD_BND); + if (cnf.equals(file)) + return Optional.of(this); + + File projectDir = file.getParentFile(); + if (projectDir.isDirectory()) { + File wsDir = projectDir.getParentFile(); + if (wsDir.equals(getBase())) { + Project project = getProject(projectDir.getName()); + if (project != null) { + return project.findProcessor(file); + } + } + } + return Optional.empty(); + } } diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/model/BndEditModel.java b/biz.aQute.bndlib/src/aQute/bnd/build/model/BndEditModel.java index a3a28bf328..d4ba0a29c9 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/build/model/BndEditModel.java +++ b/biz.aQute.bndlib/src/aQute/bnd/build/model/BndEditModel.java @@ -114,7 +114,7 @@ public class BndEditModel { private Properties properties = new UTF8Properties(); private final Map objectProperties = new HashMap<>(); private final Map changesToSave = new TreeMap<>(); - private Project project; + private Project bndrun; private volatile boolean dirty; @@ -384,10 +384,10 @@ public BndEditModel(IDocument document) throws IOException { loadFrom(document); } - public BndEditModel(Project project) throws IOException { - this(project.getWorkspace()); - this.project = project; - File propertiesFile = project.getPropertiesFile(); + public BndEditModel(Project bndrun) throws IOException { + this(bndrun.getWorkspace()); + this.bndrun = bndrun; + File propertiesFile = bndrun.getPropertiesFile(); if (propertiesFile.isFile()) this.document = new Document(IO.collect(propertiesFile)); else @@ -395,6 +395,22 @@ public BndEditModel(Project project) throws IOException { loadFrom(this.document); } + /** + * Is either the workspace (when cnf/build.bnd) or a project (when its + * bnd.bnd) or a random bndrun linked to workspace (event if it isn't a + * bndrun). Primary purpose is to walk the inheritance chain implied in the + * Workspace/Project/sub bnd files files + */ + Processor getOwner() { + File propertiesFile = bndrun.getPropertiesFile(); + if (!propertiesFile.getName() + .endsWith(".bnd")) + return bndrun; + + return workspace.findProcessor(propertiesFile) + .orElse(bndrun); + } + public void loadFrom(IDocument document) throws IOException { try (InputStream in = toEscaped(document.get())) { loadFrom(in); @@ -1342,11 +1358,11 @@ private boolean hasIncludeResourceHeaderLikeInstruction() { } public void setProject(Project project) { - this.project = project; + this.bndrun = project; } public Project getProject() { - return project; + return bndrun; } public Workspace getWorkspace() { @@ -1379,10 +1395,10 @@ public Processor getProperties() throws Exception { File source = getBndResource(); Processor parent; - if (project != null) { - parent = project; + if (bndrun != null) { + parent = bndrun; if (source == null) { - source = project.getPropertiesFile(); + source = bndrun.getPropertiesFile(); } } else if (workspace != null && isCnf()) { parent = workspace; @@ -1455,7 +1471,7 @@ public Map getDocumentChanges() { */ public void saveChanges() throws IOException { assert document != null - && project != null : "you can only call saveChanges when you created this edit model with a project"; + && bndrun != null : "you can only call saveChanges when you created this edit model with a project"; saveChangesTo(document); store(document, getProject().getPropertiesFile()); @@ -1472,7 +1488,7 @@ public ResolutionInstructions.ResolveMode getResolveMode() { try { return aQute.lib.converter.Converter.cnv(ResolutionInstructions.ResolveMode.class, resolve); } catch (Exception e) { - project.error("Invalid value for %s: %s. Allowed values are %s", Constants.RESOLVE, resolve, + bndrun.error("Invalid value for %s: %s. Allowed values are %s", Constants.RESOLVE, resolve, ResolutionInstructions.ResolveMode.class.getEnumConstants()); } } @@ -1496,7 +1512,7 @@ public void setDirty(boolean isDirty) { } public void load() throws IOException { - loadFrom(project.getPropertiesFile()); + loadFrom(bndrun.getPropertiesFile()); } /** @@ -1561,5 +1577,4 @@ public > String add(String header, String toAdd) { public long getLastChangedAt() { return lastChangedAt; } - } diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java index e36ee75b1b..b541c2f9dc 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Processor.java @@ -1006,6 +1006,89 @@ private String getProperty(String key, String deflt, String separator, boolean i return getWildcardProperty(deflt, separator, inherit, ins); } + /** + * A Property Key is the pair of a Processor and a key it defines. It also + * defines if this is the firsts definition viewed from this Processor. The + * floor indicates where the property is defined relative to its parents. + * Zero is in the current processor, 1, is its parents, and so on. + */ + public record PropertyKey(Processor processor, String key, int floor) + implements Comparable { + + /** + * Check if this PropertyKey belongs to the given processor + * + * @param p the processor to check + * @return true if our processor is the same as p + */ + public boolean isLocalTo(Processor p) { + return processor == p; + } + + /** + * Get the value of the property key + * + * @return a processed value + */ + public String getValue() { + return processor.getProperty(key); + } + + /** + * Get the raw value of the property key + * + * @return a raw value + */ + public String getRawValue() { + return processor.getProperties() + .getProperty(key); + } + + @Override + public int compareTo(PropertyKey o) { + int n = key.compareTo(o.key); + if ( n != 0) + return n; + return Integer.compare(floor, o.floor); + } + } + + /** + * Return a list of sorted PropertyKey that match the predicate and includes + * the inheritance chain. The intention is to capture the processor that + * defines a key. + * + * @param predicate the predicate to filter the key + * @return new modifiable sorted list of PropertyKey + */ + @SuppressWarnings("resource") + public List getPropertyKeys(Predicate predicate) { + List keys = new ArrayList<>(); + Processor rover = this; + int level = 0; + while( rover != null) { + Processor localRover = rover; + int localLevel = level; + rover.stream(false) // local only + .filter(predicate) + .map(k -> new PropertyKey(localRover, k, localLevel)) + .forEach(keys::add); + rover = rover.getParent(); + level++; + } + Collections.sort(keys); + return keys; + + } + + /** + * Return the merge property keys + */ + public List getMergePropertyKeys(String stem) { + String prefix = stem + "."; + return getPropertyKeys(k -> k.equals(stem) || k.startsWith(prefix)); + } + private String getWildcardProperty(String deflt, String separator, boolean inherit, Instruction ins) { // Handle a wildcard key, make sure they're sorted // for consistency @@ -2144,18 +2227,11 @@ public String mergeProperties(String key) { } public String mergeLocalProperties(String key) { - if (since(About._3_3)) { - return getProperty(makeWildcard(key), null, ",", false); - } else - return mergeProperties(key); + return getProperty(makeWildcard(key), null, ",", false); } public String mergeProperties(String key, String separator) { - if (since(About._2_4)) - return getProperty(makeWildcard(key), null, separator, true); - else - return getProperty(key); - + return getProperty(makeWildcard(key), null, separator, true); } private String makeWildcard(String key) {