You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by cz...@apache.org on 2020/11/10 08:18:16 UTC

[sling-org-apache-sling-feature] branch master updated: SLING-9887 : Record feature origins for configurations

This is an automated email from the ASF dual-hosted git repository.

cziegeler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature.git


The following commit(s) were added to refs/heads/master by this push:
     new c91f514  SLING-9887 : Record feature origins for configurations
c91f514 is described below

commit c91f51489b02346a0c04cdd3f7f843b66159454b
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Tue Nov 10 09:17:55 2020 +0100

    SLING-9887 : Record feature origins for configurations
---
 .../org/apache/sling/feature/Configuration.java    |  46 ++++++
 .../apache/sling/feature/builder/BuilderUtil.java  | 117 +++++++-------
 .../sling/feature/builder/FeatureBuilder.java      |   9 +-
 .../org/apache/sling/feature/package-info.java     |   2 +-
 .../apache/sling/feature/ConfigurationTest.java    |  36 +++++
 .../sling/feature/builder/BuilderUtilTest.java     | 104 ++++++++-----
 .../sling/feature/builder/FeatureBuilderTest.java  | 169 ++++++++++++++++++++-
 7 files changed, 388 insertions(+), 95 deletions(-)

diff --git a/src/main/java/org/apache/sling/feature/Configuration.java b/src/main/java/org/apache/sling/feature/Configuration.java
index b44e9a6..97133c5 100644
--- a/src/main/java/org/apache/sling/feature/Configuration.java
+++ b/src/main/java/org/apache/sling/feature/Configuration.java
@@ -16,11 +16,16 @@
  */
 package org.apache.sling.feature;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Dictionary;
 import java.util.Enumeration;
 import java.util.Hashtable;
+import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.felix.cm.json.Configurations;
+import org.osgi.util.converter.Converters;
 
 
 /**
@@ -52,6 +57,13 @@ public class Configuration
      */
     public static final String PROP_ARTIFACT_ID = PROP_PREFIX + "service.bundleLocation";
 
+    /**
+     * This optional configuration property stores the artifact ids (array) of the
+     * features this configuration has been specified.
+     * @since 1.6
+     */
+    public static final String PROP_FEATURE_ORIGINS = PROP_PREFIX + "origins";
+
     /** The pid or name for factory pids. */
     private final String pid;
 
@@ -168,6 +180,40 @@ public class Configuration
     }
 
     /**
+     * Get the feature origins - if recorded
+     * 
+     * @return A immutable list of feature artifact ids - list might be empty
+     * @since 1.6
+     * @throws IllegalArgumentException If the stored values are not valid artifact ids
+     */
+    public List<ArtifactId> getFeatureOrigins() {
+        final List<ArtifactId> list = new ArrayList<>();
+        final Object origins = this.properties.get(PROP_FEATURE_ORIGINS);
+        if ( origins != null ) {
+            final String[] values = Converters.standardConverter().convert(origins).to(String[].class);
+            for(final String v : values) {
+                list.add(ArtifactId.parse(v));
+            }
+        }
+        return Collections.unmodifiableList(list);
+    }
+
+    /**
+     * Set the feature origins
+     * @param featureOrigins the list of artifact ids or null to remove the info from this object
+     * @since 1.6
+     */
+    public void setFeatureOrigins(final List<ArtifactId> featureOrigins) {
+        if ( featureOrigins == null || featureOrigins.isEmpty() ) {
+            this.properties.remove(PROP_FEATURE_ORIGINS);
+        } else {
+            final List<String> list = featureOrigins.stream().map(ArtifactId::toMvnId).collect(Collectors.toList());
+            final String[] values = Converters.standardConverter().convert(list).to(String[].class);
+            this.properties.put(PROP_FEATURE_ORIGINS, values);
+        }
+    }
+    
+    /**
      * Get the configuration properties of the configuration. This configuration
      * properties are all properties minus properties used to manage the
      * configuration. Managing properties have to start with
diff --git a/src/main/java/org/apache/sling/feature/builder/BuilderUtil.java b/src/main/java/org/apache/sling/feature/builder/BuilderUtil.java
index 9298f36..1be3bbd 100644
--- a/src/main/java/org/apache/sling/feature/builder/BuilderUtil.java
+++ b/src/main/java/org/apache/sling/feature/builder/BuilderUtil.java
@@ -472,68 +472,83 @@ class BuilderUtil {
     }
 
     // configurations - merge / override
-    static void mergeConfigurations(final Configurations target, final Configurations source, final Map<String, String> overrides) {
-        for(final Configuration cfg : source) {
-            boolean found = false;
-            for(int c = 0; c < target.size();c++) {
-                final Configuration current = target.get(c);
+    static void mergeConfigurations(final Configurations target, 
+        final Configurations source, 
+        final Map<String, String> overrides,
+        final ArtifactId sourceFeatureId) {
 
-                if ( current.compareTo(cfg) == 0 ) {
-                    found = true;
+        for(final Configuration cfg : source) {
+            final List<ArtifactId> sourceOrigins = cfg.getFeatureOrigins().isEmpty() ? Collections.singletonList(sourceFeatureId) : cfg.getFeatureOrigins();
+            Configuration found = target.getConfiguration(cfg.getPid());
 
-                    boolean handled = false;
-                    outer:
-                    for (Map.Entry<String, String> override : overrides.entrySet()) {
-                        if (match(cfg, override.getKey())) {
-                            if (BuilderContext.CONFIG_USE_LATEST.equals(override.getValue())) {
-                                int idx = target.indexOf(current);
-                                target.remove(current);
-                                target.add(idx, cfg.copy(cfg.getPid()));
-                                handled = true;
-                            }
-                            else if (BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH.equals(override.getValue())){
-                                for (Enumeration<String> i = cfg.getProperties().keys(); i.hasMoreElements(); ) {
-                                    final String key = i.nextElement();
-                                    if (current.getProperties().get(key) != null) {
-                                        break outer;
-                                    }
-                                    else {
-                                        current.getProperties().put(key, cfg.getProperties().get(key));
-                                    }
-                                }
-                                handled = true;
-                            }
-                            else if (BuilderContext.CONFIG_MERGE_LATEST.equals(override.getValue())) {
-                                for (Enumeration<String> i = cfg.getProperties().keys(); i.hasMoreElements(); ) {
-                                    final String key = i.nextElement();
-                                    current.getProperties().put(key, cfg.getProperties().get(key));
+            if ( found != null ) {
+                boolean handled = false;
+                outer:
+                for (Map.Entry<String, String> override : overrides.entrySet()) {
+                    if (match(cfg, override.getKey())) {
+                        if (BuilderContext.CONFIG_USE_LATEST.equals(override.getValue())) {
+                            int idx = target.indexOf(found);
+                            target.remove(found);
+                            found = cfg.copy(cfg.getPid());
+                            target.add(idx, found);
+                            handled = true;
+                        } else if (BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH.equals(override.getValue())){
+                            final List<ArtifactId> origins = found.getFeatureOrigins();
+                            for (Enumeration<String> i = cfg.getProperties().keys(); i.hasMoreElements(); ) {
+                                final String key = i.nextElement();
+                                if (found.getProperties().get(key) != null) {
+                                    break outer;
+                                } else {
+                                    found.getProperties().put(key, cfg.getProperties().get(key));
                                 }
-                                handled = true;
                             }
-                            else if (BuilderContext.CONFIG_USE_FIRST.equals(override.getValue())) {
-                                handled = true;
+                            // restore origin
+                            found.setFeatureOrigins(origins);
+                            handled = true;
+                        } else if (BuilderContext.CONFIG_MERGE_LATEST.equals(override.getValue())) {
+                            final List<ArtifactId> origins = found.getFeatureOrigins();
+                            for (Enumeration<String> i = cfg.getProperties().keys(); i.hasMoreElements(); ) {
+                                final String key = i.nextElement();
+                                found.getProperties().put(key, cfg.getProperties().get(key));
                             }
-                            else if (BuilderContext.CONFIG_MERGE_FIRST.equals(override.getValue())) {
-                                for (Enumeration<String> i = cfg.getProperties().keys(); i.hasMoreElements(); ) {
-                                    final String key = i.nextElement();
-                                    if (current.getProperties().get(key) == null) {
-                                        current.getProperties().put(key, cfg.getProperties().get(key));
-                                    }
+                            // restore origin
+                            found.setFeatureOrigins(origins);
+                            handled = true;
+                        } else if (BuilderContext.CONFIG_USE_FIRST.equals(override.getValue())) {
+                            handled = true;
+                            found = null;
+                        } else if (BuilderContext.CONFIG_MERGE_FIRST.equals(override.getValue())) {
+                            final List<ArtifactId> origins = found.getFeatureOrigins();
+                            for (Enumeration<String> i = cfg.getProperties().keys(); i.hasMoreElements(); ) {
+                                final String key = i.nextElement();
+                                if (found.getProperties().get(key) == null) {
+                                    found.getProperties().put(key, cfg.getProperties().get(key));
                                 }
-                                handled = true;
                             }
-                            break outer;
+                            // restore origin
+                            found.setFeatureOrigins(origins);
+                            handled = true;
                         }
-                    }
-                    if (!handled) {
-                        throw new IllegalStateException("Configuration override rule required to select between configurations for " +
-                            cfg.getPid());
+                        break outer;
                     }
                 }
+                if (!handled) {
+                    throw new IllegalStateException("Configuration override rule required to select between configurations for " +
+                        cfg.getPid());
+                }
+            } else {
+                // create new configuration
+                found = cfg.copy(cfg.getPid());
+                target.add(found);
+                if ( !found.getFeatureOrigins().isEmpty() ) {
+                    found = null;
+                }
             }
-            if ( !found ) {
-                final Configuration newCfg = cfg.copy(cfg.getPid());
-                target.add(newCfg);
+            if ( found != null ) {
+                // update origin
+                final List<ArtifactId> origins = new ArrayList<>(found.getFeatureOrigins());
+                origins.addAll(sourceOrigins);
+                found.setFeatureOrigins(origins);    
             }
         }
     }
diff --git a/src/main/java/org/apache/sling/feature/builder/FeatureBuilder.java b/src/main/java/org/apache/sling/feature/builder/FeatureBuilder.java
index 21e08b1..f582a64 100644
--- a/src/main/java/org/apache/sling/feature/builder/FeatureBuilder.java
+++ b/src/main/java/org/apache/sling/feature/builder/FeatureBuilder.java
@@ -356,6 +356,13 @@ public abstract class FeatureBuilder {
                     }
                 }
             }
+            // correct feature origins
+            for(final Configuration cfg : result.getConfigurations()) {
+                final List<ArtifactId> origins = cfg.getFeatureOrigins();
+                if ( origins.size() == 1 && origins.contains(feature.getId())) {
+                    cfg.setFeatureOrigins(null);
+                }
+            }
         }
 
         result.setAssembled(true);
@@ -373,7 +380,7 @@ public abstract class FeatureBuilder {
             final String originKey) {
         BuilderUtil.mergeVariables(target.getVariables(), source.getVariables(), context);
         BuilderUtil.mergeArtifacts(target.getBundles(), source.getBundles(), source, artifactOverrides, originKey);
-        BuilderUtil.mergeConfigurations(target.getConfigurations(), source.getConfigurations(), configOverrides);
+        BuilderUtil.mergeConfigurations(target.getConfigurations(), source.getConfigurations(), configOverrides, source.getId());
         BuilderUtil.mergeFrameworkProperties(target.getFrameworkProperties(), source.getFrameworkProperties(), context);
         BuilderUtil.mergeRequirements(target.getRequirements(), source.getRequirements());
         BuilderUtil.mergeCapabilities(target.getCapabilities(), source.getCapabilities());
diff --git a/src/main/java/org/apache/sling/feature/package-info.java b/src/main/java/org/apache/sling/feature/package-info.java
index 6ed34c0..9d1cc7f 100644
--- a/src/main/java/org/apache/sling/feature/package-info.java
+++ b/src/main/java/org/apache/sling/feature/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@org.osgi.annotation.versioning.Version("1.5.0")
+@org.osgi.annotation.versioning.Version("1.6.0")
 package org.apache.sling.feature;
 
 
diff --git a/src/test/java/org/apache/sling/feature/ConfigurationTest.java b/src/test/java/org/apache/sling/feature/ConfigurationTest.java
index dacdb10..2e28cb3 100644
--- a/src/test/java/org/apache/sling/feature/ConfigurationTest.java
+++ b/src/test/java/org/apache/sling/feature/ConfigurationTest.java
@@ -16,12 +16,17 @@
  */
 package org.apache.sling.feature;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.util.Arrays;
+import java.util.Collections;
+
 import org.junit.Test;
 
 public class ConfigurationTest {
@@ -67,4 +72,35 @@ public class ConfigurationTest {
         assertNull(Configuration.getFactoryPid(pid));
         assertNull(Configuration.getName(pid));
     }
+
+    @Test
+    public void testFeatureOrigins() {
+        final Configuration cfg = new Configuration("foo");
+        assertTrue(cfg.getFeatureOrigins().isEmpty());
+        assertNull(cfg.getConfigurationProperties().get(Configuration.PROP_FEATURE_ORIGINS));
+        assertNull(cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS));
+
+        // single id
+        final ArtifactId id = ArtifactId.parse("g:a:1");
+        cfg.setFeatureOrigins(Collections.singletonList(id));
+        assertEquals(1, cfg.getFeatureOrigins().size());
+        assertEquals(id, cfg.getFeatureOrigins().get(0));
+
+        assertNull(cfg.getConfigurationProperties().get(Configuration.PROP_FEATURE_ORIGINS));
+        assertNotNull(cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS));
+        final String[] array = (String[]) cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS);
+        assertArrayEquals(new String[] {id.toMvnId()}, array);
+
+        // add another id
+        final ArtifactId id2 = ArtifactId.parse("g:b:2");
+        cfg.setFeatureOrigins(Arrays.asList(id, id2));
+        assertEquals(2, cfg.getFeatureOrigins().size());
+        assertEquals(id, cfg.getFeatureOrigins().get(0));
+        assertEquals(id2, cfg.getFeatureOrigins().get(1));
+
+        assertNull(cfg.getConfigurationProperties().get(Configuration.PROP_FEATURE_ORIGINS));
+        assertNotNull(cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS));
+        final String[] array2 = (String[]) cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS);
+        assertArrayEquals(new String[] {id.toMvnId(), id2.toMvnId()}, array2);
+    }
 }
diff --git a/src/test/java/org/apache/sling/feature/builder/BuilderUtilTest.java b/src/test/java/org/apache/sling/feature/builder/BuilderUtilTest.java
index dd86386..06a06bf 100644
--- a/src/test/java/org/apache/sling/feature/builder/BuilderUtilTest.java
+++ b/src/test/java/org/apache/sling/feature/builder/BuilderUtilTest.java
@@ -27,7 +27,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -598,6 +597,8 @@ public class BuilderUtilTest {
         assertEquals(0, cfg.size());
     }
 
+    private static final ArtifactId SOURCE_ID = ArtifactId.parse("source:source:1");
+
     @Test public void testMergeConfigurations() {
         Configurations target = new Configurations();
         Configurations source = new Configurations();
@@ -607,10 +608,13 @@ public class BuilderUtilTest {
         Configuration bar = new Configuration("bar");
         bar.getProperties().put("barKey", "valueBAR");
         source.add(bar);
-        BuilderUtil.mergeConfigurations(target, source, Collections.emptyMap());
+        BuilderUtil.mergeConfigurations(target, source, Collections.emptyMap(), SOURCE_ID);
         assertEquals(2, target.size());
-        assertEquals(target.getConfiguration("foo").getProperties(), foo.getProperties());
-        assertEquals(target.getConfiguration("bar").getProperties(), bar.getProperties());
+        assertEquals(target.getConfiguration("foo").getConfigurationProperties(), foo.getConfigurationProperties());
+        assertTrue(target.getConfiguration("foo").getFeatureOrigins().isEmpty());
+        assertEquals(target.getConfiguration("bar").getConfigurationProperties(), bar.getConfigurationProperties());
+        assertEquals(1, target.getConfiguration("bar").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("bar").getFeatureOrigins().get(0));
     }
 
     @Test public void testMergeConfigurationsCLASH() {
@@ -619,7 +623,7 @@ public class BuilderUtilTest {
         target.add(new Configuration("foo"));
         source.add(new Configuration("foo"));
         try {
-            BuilderUtil.mergeConfigurations(target, source, Collections.emptyMap());
+            BuilderUtil.mergeConfigurations(target, source, Collections.emptyMap(), SOURCE_ID);
             fail();
         } catch (IllegalStateException ex) {
 
@@ -635,13 +639,15 @@ public class BuilderUtilTest {
         Configuration foo2 = new Configuration("foo");
         foo2.getProperties().put("barKey", "valueBAR");
         source.add(foo2);
-        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH));
+        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH), SOURCE_ID);
 
-        assertEquals("valueFOO", target.getConfiguration("foo").getProperties().get("fooKey"));
-        assertEquals("valueBAR", target.getConfiguration("foo").getProperties().get("barKey"));
+        assertEquals("valueFOO", target.getConfiguration("foo").getConfigurationProperties().get("fooKey"));
+        assertEquals("valueBAR", target.getConfiguration("foo").getConfigurationProperties().get("barKey"));
+        assertEquals(1, target.getConfiguration("foo").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("foo").getFeatureOrigins().get(0));
 
         try {
-            BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH));
+            BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH), SOURCE_ID);
             fail();
         } catch (IllegalStateException ex) {
 
@@ -657,10 +663,11 @@ public class BuilderUtilTest {
         Configuration foo2 = new Configuration("foo");
         foo2.getProperties().put("barKey", "valueBAR");
         source.add(foo2);
-        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_USE_FIRST));
+        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_USE_FIRST), SOURCE_ID);
 
-        assertEquals("valueFOO", target.getConfiguration("foo").getProperties().get("fooKey"));
-        assertNull(target.getConfiguration("foo").getProperties().get("barKey"));
+        assertEquals("valueFOO", target.getConfiguration("foo").getConfigurationProperties().get("fooKey"));
+        assertNull(target.getConfiguration("foo").getConfigurationProperties().get("barKey"));
+        assertTrue(target.getConfiguration("foo").getFeatureOrigins().isEmpty());
     }
 
     @Test public void testMergeConfigurationsUSELATEST() {
@@ -672,10 +679,12 @@ public class BuilderUtilTest {
         Configuration foo2 = new Configuration("foo");
         foo2.getProperties().put("barKey", "valueBAR");
         source.add(foo2);
-        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_USE_LATEST));
+        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_USE_LATEST), SOURCE_ID);
 
-        assertEquals("valueBAR", target.getConfiguration("foo").getProperties().get("barKey"));
-        assertNull(target.getConfiguration("foo").getProperties().get("fooKey"));
+        assertEquals("valueBAR", target.getConfiguration("foo").getConfigurationProperties().get("barKey"));
+        assertNull(target.getConfiguration("foo").getConfigurationProperties().get("fooKey"));
+        assertEquals(1, target.getConfiguration("foo").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("foo").getFeatureOrigins().get(0));
     }
 
     @Test public void testMergeConfigurationsMERGELATEST() {
@@ -687,9 +696,11 @@ public class BuilderUtilTest {
         Configuration foo2 = new Configuration("foo");
         foo2.getProperties().put("fooKey", "valueBAR");
         source.add(foo2);
-        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_MERGE_LATEST));
+        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_MERGE_LATEST), SOURCE_ID);
 
-        assertEquals("valueBAR", target.getConfiguration("foo").getProperties().get("fooKey"));
+        assertEquals("valueBAR", target.getConfiguration("foo").getConfigurationProperties().get("fooKey"));
+        assertEquals(1, target.getConfiguration("foo").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("foo").getFeatureOrigins().get(0));
     }
 
     @Test public void testMergeConfigurationsMERGEFIRST() {
@@ -702,10 +713,12 @@ public class BuilderUtilTest {
         foo2.getProperties().put("fooKey", "valueBAR");
         foo2.getProperties().put("barKey", "valueBAR");
         source.add(foo2);
-        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_MERGE_FIRST));
+        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*", BuilderContext.CONFIG_MERGE_FIRST), SOURCE_ID);
 
-        assertEquals("valueFOO", target.getConfiguration("foo").getProperties().get("fooKey"));
-        assertEquals("valueBAR", target.getConfiguration("foo").getProperties().get("barKey"));
+        assertEquals("valueFOO", target.getConfiguration("foo").getConfigurationProperties().get("fooKey"));
+        assertEquals("valueBAR", target.getConfiguration("foo").getConfigurationProperties().get("barKey"));
+        assertEquals(1, target.getConfiguration("foo").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("foo").getFeatureOrigins().get(0));
     }
 
     @Test public void testMergeConfigurationsFactory() {
@@ -717,10 +730,13 @@ public class BuilderUtilTest {
         Configuration bar = new Configuration("bar~bar");
         bar.getProperties().put("barKey", "valueBAR");
         source.add(bar);
-        BuilderUtil.mergeConfigurations(target, source, Collections.emptyMap());
+        BuilderUtil.mergeConfigurations(target, source, Collections.emptyMap(), SOURCE_ID);
         assertEquals(2, target.size());
-        assertEquals(target.getConfiguration("foo~foo").getProperties(), foo.getProperties());
-        assertEquals(target.getConfiguration("bar~bar").getProperties(), bar.getProperties());
+        assertEquals(target.getConfiguration("foo~foo").getConfigurationProperties(), foo.getConfigurationProperties());
+        assertTrue(target.getConfiguration("foo~foo").getFeatureOrigins().isEmpty());
+        assertEquals(target.getConfiguration("bar~bar").getConfigurationProperties(), bar.getConfigurationProperties());
+        assertEquals(1, target.getConfiguration("bar~bar").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("bar~bar").getFeatureOrigins().get(0));
     }
 
     @Test public void testMergeConfigurationsCLASHFactory() {
@@ -729,7 +745,7 @@ public class BuilderUtilTest {
         target.add(new Configuration("foo~foo"));
         source.add(new Configuration("foo~foo"));
         try {
-            BuilderUtil.mergeConfigurations(target, source, Collections.emptyMap());
+            BuilderUtil.mergeConfigurations(target, source, Collections.emptyMap(), SOURCE_ID);
             fail();
         } catch (IllegalStateException ex) {
 
@@ -745,13 +761,15 @@ public class BuilderUtilTest {
         Configuration foo2 = new Configuration("foo~foo");
         foo2.getProperties().put("barKey", "valueBAR");
         source.add(foo2);
-        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*~f*", BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH));
-
-        assertEquals("valueFOO", target.getConfiguration("foo~foo").getProperties().get("fooKey"));
-        assertEquals("valueBAR", target.getConfiguration("foo~foo").getProperties().get("barKey"));
+        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*~f*", BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH), SOURCE_ID);
 
+        assertEquals("valueFOO", target.getConfiguration("foo~foo").getConfigurationProperties().get("fooKey"));
+        assertEquals("valueBAR", target.getConfiguration("foo~foo").getConfigurationProperties().get("barKey"));
+        assertEquals(1, target.getConfiguration("foo~foo").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("foo~foo").getFeatureOrigins().get(0));
+ 
         try {
-            BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*~fo*", BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH));
+            BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*~fo*", BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH), SOURCE_ID);
             fail();
         } catch (IllegalStateException ex) {
 
@@ -767,10 +785,12 @@ public class BuilderUtilTest {
         Configuration foo2 = new Configuration("foo~foo");
         foo2.getProperties().put("barKey", "valueBAR");
         source.add(foo2);
-        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*~f*", BuilderContext.CONFIG_USE_LATEST));
+        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("fo*~f*", BuilderContext.CONFIG_USE_LATEST), SOURCE_ID);
 
-        assertEquals("valueBAR", target.getConfiguration("foo~foo").getProperties().get("barKey"));
-        assertNull(target.getConfiguration("foo~foo").getProperties().get("fooKey"));
+        assertEquals("valueBAR", target.getConfiguration("foo~foo").getConfigurationProperties().get("barKey"));
+        assertNull(target.getConfiguration("foo~foo").getConfigurationProperties().get("fooKey"));
+        assertEquals(1, target.getConfiguration("foo~foo").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("foo~foo").getFeatureOrigins().get(0));
     }
 
     @Test public void testMergeConfigurationsMERGELATESTFactory() {
@@ -782,9 +802,11 @@ public class BuilderUtilTest {
         Configuration foo2 = new Configuration("foo~foo");
         foo2.getProperties().put("fooKey", "valueBAR");
         source.add(foo2);
-        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("foo~foo", BuilderContext.CONFIG_MERGE_LATEST));
+        BuilderUtil.mergeConfigurations(target, source, Collections.singletonMap("foo~foo", BuilderContext.CONFIG_MERGE_LATEST), SOURCE_ID);
 
-        assertEquals("valueBAR", target.getConfiguration("foo~foo").getProperties().get("fooKey"));
+        assertEquals("valueBAR", target.getConfiguration("foo~foo").getConfigurationProperties().get("fooKey"));
+        assertEquals(1, target.getConfiguration("foo~foo").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("foo~foo").getFeatureOrigins().get(0));
     }
 
     @Test public void testMergeConfigurationsMixed() {
@@ -798,17 +820,21 @@ public class BuilderUtilTest {
         source.add(foo4);
         Configuration foo2 = new Configuration("foo");
         foo2.getProperties().put("fooKey", "valueBAR");
-        source.add(foo2);
+        target.add(foo2);
         Configuration foo3 = new Configuration("foo");
         foo2.getProperties().put("fooKey", "valueBAR2");
         source.add(foo3);
         Map<String, String> overrides = new HashMap<>();
         overrides.put("foo", BuilderContext.CONFIG_MERGE_LATEST);
         overrides.put("foo~foo", BuilderContext.CONFIG_USE_LATEST);
-        BuilderUtil.mergeConfigurations(target, source, overrides);
-
-        assertEquals("valueFOO4", target.getConfiguration("foo~foo").getProperties().get("fooKey"));
-        assertEquals("valueBAR2", target.getConfiguration("foo").getProperties().get("fooKey"));
+        BuilderUtil.mergeConfigurations(target, source, overrides, SOURCE_ID);
+
+        assertEquals("valueFOO4", target.getConfiguration("foo~foo").getConfigurationProperties().get("fooKey"));
+        assertEquals("valueBAR2", target.getConfiguration("foo").getConfigurationProperties().get("fooKey"));
+        assertEquals(1, target.getConfiguration("foo").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("foo").getFeatureOrigins().get(0));
+        assertEquals(1, target.getConfiguration("foo~foo").getFeatureOrigins().size());
+        assertEquals(SOURCE_ID, target.getConfiguration("foo~foo").getFeatureOrigins().get(0));
     }
 
     @SafeVarargs
diff --git a/src/test/java/org/apache/sling/feature/builder/FeatureBuilderTest.java b/src/test/java/org/apache/sling/feature/builder/FeatureBuilderTest.java
index d58d472..e32ae42 100644
--- a/src/test/java/org/apache/sling/feature/builder/FeatureBuilderTest.java
+++ b/src/test/java/org/apache/sling/feature/builder/FeatureBuilderTest.java
@@ -131,7 +131,7 @@ public class FeatureBuilderTest {
         for(final Configuration cfg : expected.getConfigurations()) {
             final Configuration found = actuals.getConfigurations().getConfiguration(cfg.getPid());
             assertNotNull("Configuration " + cfg, found);
-            assertEquals("Configuration " + cfg, cfg.getProperties(), found.getProperties());
+            assertEquals("Configuration " + cfg, cfg.getConfigurationProperties(), found.getConfigurationProperties());
         }
 
         // frameworkProperties
@@ -203,7 +203,6 @@ public class FeatureBuilderTest {
         Feature a = new Feature(ArtifactId.fromMvnId("g:a:1"));
         Feature b = new Feature(ArtifactId.fromMvnId("g:b:1"));
 
-
         a.getBundles().add(BuilderUtilTest.createBundle("o/a/1.0.0", 10));
         a.getBundles().add(BuilderUtilTest.createBundle("o/a/2.0.0", 9));
         a.getBundles().add(BuilderUtilTest.createBundle("o/a/3.0.0", 11));
@@ -264,7 +263,6 @@ public class FeatureBuilderTest {
         Feature a = new Feature(ArtifactId.fromMvnId("g:a:1"));
         Feature b = new Feature(ArtifactId.fromMvnId("g:b:1"));
 
-
         a.getBundles().add(BuilderUtilTest.createBundle("o/a/1.0.0", 10));
         a.getBundles().add(BuilderUtilTest.createBundle("o/a/2.0.0", 9));
         a.getBundles().add(BuilderUtilTest.createBundle("o/a/3.0.0", 11));
@@ -1127,6 +1125,171 @@ public class FeatureBuilderTest {
         assertEquals("world", result[1]);
     }
 
+    @Test public void testMergeConfigurationFeatureOrigins() {
+        final Feature f1 = new Feature(ArtifactId.parse("g:a:1"));
+        final Configuration c1 = new Configuration("c1");
+        f1.getConfigurations().add(c1);
+        final Configuration c2 = new Configuration("c2");
+        f1.getConfigurations().add(c2);
+
+        final Feature f2 = new Feature(ArtifactId.parse("g:b:1"));
+        final Configuration c3 = new Configuration("c2");
+        f2.getConfigurations().add(c3);
+        final Configuration c4 = new Configuration("c3");
+        f2.getConfigurations().add(c4);
+
+        final BuilderContext bc = new BuilderContext(provider);
+        bc.addConfigsOverrides(Collections.singletonMap("*", BuilderContext.CONFIG_MERGE_LATEST));
+        final Feature f = FeatureBuilder.assemble(ArtifactId.parse("g:f:1"), bc, f1, f2);
+
+        assertEquals(3, f.getConfigurations().size());
+        final Configuration fc1 = f.getConfigurations().getConfiguration("c1");
+        assertNotNull(fc1);
+        final Configuration fc2 = f.getConfigurations().getConfiguration("c2");
+        assertNotNull(fc2);
+        final Configuration fc3 = f.getConfigurations().getConfiguration("c3");
+        assertNotNull(fc3);
+        assertEquals(1, fc1.getFeatureOrigins().size());
+        assertEquals(f1.getId(), fc1.getFeatureOrigins().get(0));
+        assertEquals(2, fc2.getFeatureOrigins().size());
+        assertEquals(f1.getId(), fc2.getFeatureOrigins().get(0));
+        assertEquals(f2.getId(), fc2.getFeatureOrigins().get(1));
+        assertEquals(1, fc3.getFeatureOrigins().size());
+        assertEquals(f2.getId(), fc3.getFeatureOrigins().get(0));
+
+        // merge with empty feature - this should not change the origins
+        final Feature empty = new Feature(ArtifactId.parse("g:c:1"));
+        final Feature ft1 = FeatureBuilder.assemble(ArtifactId.parse("g:e:1"), bc, empty, f);
+
+        assertEquals(3, ft1.getConfigurations().size());
+        final Configuration cft1_1 = ft1.getConfigurations().getConfiguration("c1");
+        assertNotNull(cft1_1);
+        assertEquals(1, cft1_1.getFeatureOrigins().size());
+        assertEquals(f1.getId(), cft1_1.getFeatureOrigins().get(0));
+
+        final Configuration cft1_2 = ft1.getConfigurations().getConfiguration("c2");
+        assertNotNull(cft1_2);
+        assertEquals(2, cft1_2.getFeatureOrigins().size());
+        assertEquals(f1.getId(), cft1_2.getFeatureOrigins().get(0));
+        assertEquals(f2.getId(), cft1_2.getFeatureOrigins().get(1));
+
+        final Configuration cft1_3 = ft1.getConfigurations().getConfiguration("c3");
+        assertNotNull(cft1_3);
+        assertEquals(1, cft1_3.getFeatureOrigins().size());
+        assertEquals(f2.getId(), cft1_3.getFeatureOrigins().get(0));
+
+        // merge with empty feature, reverse order
+        final Feature ft2 = FeatureBuilder.assemble(ArtifactId.parse("g:e:1"), bc, f, empty);
+        assertEquals(3, ft2.getConfigurations().size());
+        final Configuration cft2_1 = ft2.getConfigurations().getConfiguration("c1");
+        assertNotNull(cft2_1);
+        assertEquals(1, cft2_1.getFeatureOrigins().size());
+        assertEquals(f1.getId(), cft2_1.getFeatureOrigins().get(0));
+
+        final Configuration cft2_2 = ft2.getConfigurations().getConfiguration("c2");
+        assertNotNull(cft2_2);
+        assertEquals(2, cft2_2.getFeatureOrigins().size());
+        assertEquals(f1.getId(), cft2_2.getFeatureOrigins().get(0));
+        assertEquals(f2.getId(), cft2_2.getFeatureOrigins().get(1));
+
+        final Configuration cft2_3 = ft2.getConfigurations().getConfiguration("c3");
+        assertNotNull(cft2_3);
+        assertEquals(1, cft2_3.getFeatureOrigins().size());
+        assertEquals(f2.getId(), cft2_3.getFeatureOrigins().get(0));
+
+        // merge with another feature containing c3
+        final Feature f3 = new Feature(ArtifactId.parse("g:x:1"));
+        final Configuration f3c3 = new Configuration("c3");
+        f3.getConfigurations().add(f3c3);
+        final Feature ft3 = FeatureBuilder.assemble(ArtifactId.parse("g:e:1"), bc, f3, f);
+        assertEquals(3, ft3.getConfigurations().size());
+        final Configuration cft3_1 = ft3.getConfigurations().getConfiguration("c1");
+        assertNotNull(cft3_1);
+        assertEquals(1, cft3_1.getFeatureOrigins().size());
+        assertEquals(f1.getId(), cft3_1.getFeatureOrigins().get(0));
+
+        final Configuration cft3_2 = ft3.getConfigurations().getConfiguration("c2");
+        assertNotNull(cft3_2);
+        assertEquals(2, cft3_2.getFeatureOrigins().size());
+        assertEquals(f1.getId(), cft3_2.getFeatureOrigins().get(0));
+        assertEquals(f2.getId(), cft3_2.getFeatureOrigins().get(1));
+
+        final Configuration cft3_3 = ft3.getConfigurations().getConfiguration("c3");
+        assertNotNull(cft3_3);
+        assertEquals(2, cft3_3.getFeatureOrigins().size());
+        assertEquals(f3.getId(), cft3_3.getFeatureOrigins().get(0));
+        assertEquals(f2.getId(), cft3_3.getFeatureOrigins().get(1));
+
+        // merge with another feature containing c3 - reverse order
+        final Feature ft4 = FeatureBuilder.assemble(ArtifactId.parse("g:e:1"), bc, f, f3);
+        assertEquals(3, ft4.getConfigurations().size());
+        final Configuration cft4_1 = ft4.getConfigurations().getConfiguration("c1");
+        assertNotNull(cft4_1);
+        assertEquals(1, cft4_1.getFeatureOrigins().size());
+        assertEquals(f1.getId(), cft4_1.getFeatureOrigins().get(0));
+
+        final Configuration cft4_2 = ft4.getConfigurations().getConfiguration("c2");
+        assertNotNull(cft4_2);
+        assertEquals(2, cft4_2.getFeatureOrigins().size());
+        assertEquals(f1.getId(), cft4_2.getFeatureOrigins().get(0));
+        assertEquals(f2.getId(), cft4_2.getFeatureOrigins().get(1));
+
+        final Configuration cft4_3 = ft4.getConfigurations().getConfiguration("c3");
+        assertNotNull(cft4_3);
+        assertEquals(2, cft4_3.getFeatureOrigins().size());
+        assertEquals(f2.getId(), cft4_3.getFeatureOrigins().get(0));
+        assertEquals(f3.getId(), cft4_3.getFeatureOrigins().get(1));
+    }
+
+    @Test public void testPrototypeConfigurationFeatureOrigins() {
+        final Feature prototype = new Feature(ArtifactId.parse("g:a:1"));
+        final Configuration c1 = new Configuration("c1");
+        prototype.getConfigurations().add(c1);
+        final Configuration c2 = new Configuration("c2");
+        prototype.getConfigurations().add(c2);
+
+        final Feature feature = new Feature(ArtifactId.parse("g:b:1"));
+        final Configuration c3 = new Configuration("c2");
+        feature.getConfigurations().add(c3);
+        final Configuration c4 = new Configuration("c3");
+        feature.getConfigurations().add(c4);
+
+        feature.setPrototype(new Prototype(prototype.getId()));
+        final BuilderContext bc = new BuilderContext(new FeatureProvider(){
+
+			@Override
+			public Feature provide(final ArtifactId id) {
+				if ( id.equals(prototype.getId()) ) {
+                    return prototype;
+                }
+				return provider.provide(id);
+			}
+            
+        });
+        bc.addConfigsOverrides(Collections.singletonMap("*", BuilderContext.CONFIG_MERGE_LATEST));
+        final Feature assembled = FeatureBuilder.assemble(feature, bc);
+
+        assertEquals(3, assembled.getConfigurations().size());
+
+        // c1 is only in the prototype - origin set to prototype
+        final Configuration fc1 = assembled.getConfigurations().getConfiguration("c1");
+        assertNotNull(fc1);
+        assertEquals(1, fc1.getFeatureOrigins().size());
+        assertEquals(prototype.getId(), fc1.getFeatureOrigins().get(0));
+
+        // c2 is in both - origin set to both
+        final Configuration fc2 = assembled.getConfigurations().getConfiguration("c2");
+        assertNotNull(fc2);
+        assertEquals(2, fc2.getFeatureOrigins().size());
+        assertEquals(prototype.getId(), fc2.getFeatureOrigins().get(0));
+        assertEquals(feature.getId(), fc2.getFeatureOrigins().get(1));
+
+        // c3 is only in the feature - no origins set
+        final Configuration fc3 = assembled.getConfigurations().getConfiguration("c3");
+        assertNotNull(fc3);
+        assertTrue(fc3.getFeatureOrigins().isEmpty());
+    }
+
     private static class MatchingRequirementImpl extends RequirementImpl implements MatchingRequirement {
 
         public MatchingRequirementImpl(Resource res, String ns, Map<String, String> dirs, Map<String, Object> attrs) {