You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by da...@apache.org on 2018/11/21 09:53:54 UTC

[sling-org-apache-sling-feature] branch master updated: SLING-8104 Avoid magic when merging features

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

davidb 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 1f4369b  SLING-8104 Avoid magic when merging features
1f4369b is described below

commit 1f4369bdbda933588c3e093a7b53f357aab65bdb
Author: David Bosschaert <bo...@adobe.com>
AuthorDate: Wed Nov 21 09:47:14 2018 +0000

    SLING-8104 Avoid magic when merging features
    
    When merging artifacts/bundles, they need to be selected from a provided
    override list if the artifact versions are not the same. The list has
    the following syntax:
      groupid1:artifactid1:<resolution>
      groupid2:artifactid2:<resolution>
    To apply the same override rule for all clashes, a wildcard using '*' for
    groupID and artifactID can be used:
      *:*:<resolution>
    means always select the same resolution in case of a clash.
    
    Where <resolution> is one of the following:
      ALL - select all the artifacts
      HIGHEST - select only the artifact with the highest version number
      LATEST - select only the artifact provided latest
      <version> - select this specific version
    When comparing version numbers these are converted to OSGi version
    numbers and the OSGi version number ordering is applied.
    
    When merging includes artifacts specified in the target feature
    override all artifacts with the same group ID and artifact ID in the
    included feature.
    Both the included as well the target feature can have multiple artifacts
    with the same group ID and artifact ID but different versions.
    
    Additionally variables and framework properties overwrites are now
    called overrides.
    
    This closes #9
---
 .../sling/feature/builder/BuilderContext.java      |  56 +++----
 .../apache/sling/feature/builder/BuilderUtil.java  | 160 ++++++++++++++-----
 .../sling/feature/builder/FeatureBuilder.java      |  56 +++++--
 .../sling/feature/builder/BuilderUtilTest.java     | 101 ++++++++++--
 .../sling/feature/builder/FeatureBuilderTest.java  | 177 +++++++++++++++++----
 5 files changed, 420 insertions(+), 130 deletions(-)

diff --git a/src/main/java/org/apache/sling/feature/builder/BuilderContext.java b/src/main/java/org/apache/sling/feature/builder/BuilderContext.java
index 1dd100c..61acba5 100644
--- a/src/main/java/org/apache/sling/feature/builder/BuilderContext.java
+++ b/src/main/java/org/apache/sling/feature/builder/BuilderContext.java
@@ -29,10 +29,6 @@ import java.util.Map;
  */
 public class BuilderContext {
 
-    enum ArtifactMergeAlgorithm {
-        LATEST, HIGHEST
-    }
-
     /** The required feature provider */
     private final FeatureProvider provider;
 
@@ -42,10 +38,10 @@ public class BuilderContext {
     private final Map<String, Map<String,String>> extensionConfiguration = new HashMap<>();
     private final List<MergeHandler> mergeExtensions = new ArrayList<>();
     private final List<PostProcessHandler> postProcessExtensions = new ArrayList<>();
+    private final List<String> artifactsOverrides = new ArrayList<>();
     private final Map<String,String> variables = new HashMap<>();
     private final Map<String,String> frameworkProperties = new HashMap<>();
 
-    private ArtifactMergeAlgorithm merge = ArtifactMergeAlgorithm.HIGHEST;
 
     /**
      * Create a new context
@@ -72,28 +68,39 @@ public class BuilderContext {
     }
 
     /**
-     * Add overwrites for the variables
+     * Add overrides for the variables
      *
-     * @param vars The overwrites
+     * @param vars The overrides
      * @return The builder context
      */
-    public BuilderContext addVariablesOverwrites(final Map<String,String> vars) {
+    public BuilderContext addVariablesOverrides(final Map<String,String> vars) {
         this.variables.putAll(vars);
         return this;
     }
 
     /**
-     * Add overwrites for the framework properties
+     * Add overrides for the framework properties
      *
-     * @param props The overwrites
+     * @param props The overrides
      * @return The builder context
      */
-    public BuilderContext addFrameworkPropertiesOverwrites(final Map<String,String> props) {
+    public BuilderContext addFrameworkPropertiesOverrides(final Map<String,String> props) {
         this.frameworkProperties.putAll(props);
         return this;
     }
 
     /**
+     * Add overrides for artifact clashes
+     *
+     * @param overrides The overrides
+     * @return The builder context
+     */
+    public BuilderContext addArtifactsOverrides(final List<String> overrides) {
+        this.artifactsOverrides.addAll(overrides);
+        return this;
+    }
+
+    /**
      * Add merge extensions
      *
      * @param extensions A list of merge extensions
@@ -116,17 +123,6 @@ public class BuilderContext {
     }
 
     /**
-     * Set the merge algorithm
-     *
-     * @param alg The algorithm
-     * @return The builder context
-     */
-    public BuilderContext setMergeAlgorithm(final ArtifactMergeAlgorithm alg) {
-        this.merge = alg;
-        return this;
-    }
-
-    /**
      * Set a handler configuration
      *
      * @param The name of the handler
@@ -152,11 +148,15 @@ public class BuilderContext {
         return this.artifactProvider;
     }
 
-    Map<String,String> getVariablesOverwrites() {
-        return  this.variables;
+    List<String> getArtifactOverrides() {
+        return this.artifactsOverrides;
+    }
+
+    Map<String,String> getVariablesOverrides() {
+        return this.variables;
     }
 
-    Map<String,String> getFrameworkPropertiesOverwrites() {
+    Map<String,String> getFrameworkPropertiesOverrides() {
         return this.frameworkProperties;
     }
 
@@ -168,10 +168,6 @@ public class BuilderContext {
         return this.provider;
     }
 
-    ArtifactMergeAlgorithm getMergeAlgorithm() {
-        return this.merge;
-    }
-
     /**
      * Get the list of merge extensions
      * @return The list of merge extensions
@@ -196,11 +192,11 @@ public class BuilderContext {
     BuilderContext clone(final FeatureProvider featureProvider) {
         final BuilderContext ctx = new BuilderContext(featureProvider);
         ctx.setArtifactProvider(this.artifactProvider);
+        ctx.artifactsOverrides.addAll(this.artifactsOverrides);
         ctx.variables.putAll(this.variables);
         ctx.frameworkProperties.putAll(this.frameworkProperties);
         ctx.mergeExtensions.addAll(mergeExtensions);
         ctx.postProcessExtensions.addAll(postProcessExtensions);
-        ctx.merge = this.merge;
         return ctx;
     }
 }
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 f74c2b1..be56b0b 100644
--- a/src/main/java/org/apache/sling/feature/builder/BuilderUtil.java
+++ b/src/main/java/org/apache/sling/feature/builder/BuilderUtil.java
@@ -19,6 +19,8 @@ package org.apache.sling.feature.builder;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.List;
@@ -42,7 +44,7 @@ import org.apache.sling.feature.Configurations;
 import org.apache.sling.feature.Extension;
 import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.FeatureConstants;
-import org.apache.sling.feature.builder.BuilderContext.ArtifactMergeAlgorithm;
+import org.osgi.framework.Version;
 import org.osgi.resource.Capability;
 import org.osgi.resource.Requirement;
 
@@ -50,6 +52,17 @@ import org.osgi.resource.Requirement;
  * Utility methods for the builders
  */
 class BuilderUtil {
+    /** Used in override rule to select all candidates. */
+    static final String OVERRIDE_SELECT_ALL = "ALL";
+
+    /** Used in override rule to select the candidate with the highest version (OSGi version comparison rules). */
+    static final String OVERRIDE_SELECT_HIGHEST = "HIGHEST";
+
+    /** Used in override rule to select the last candidate applied. */
+    static final String OVERRIDE_SELECT_LATEST = "LATEST";
+
+    /** Used in override rule to have it apply to all artifacts. */
+    static final String CATCHALL_OVERRIDE = "*:*:";
 
     static boolean contains(String key, Iterable<Map.Entry<String, String>> iterable) {
         if (iterable != null) {
@@ -73,7 +86,7 @@ class BuilderUtil {
         return null;
     }
 
-    private static void mergeWithContextOverwrite(String type, Map<String,String> target, Map<String,String> source, Iterable<Map.Entry<String,String>> context) {
+    private static void mergeWithContextOverride(String type, Map<String,String> target, Map<String,String> source, Iterable<Map.Entry<String,String>> context) {
         Map<String,String> result = new HashMap<>();
         for (Map.Entry<String, String> entry : target.entrySet()) {
             result.put(entry.getKey(), contains(entry.getKey(), context) ? get(entry.getKey(), context) : entry.getValue());
@@ -88,7 +101,7 @@ class BuilderUtil {
                     String targetValue = target.get(entry.getKey());
                     if (targetValue != null) {
                         if (!value.equals(targetValue)) {
-                            throw new IllegalStateException(String.format("Can't merge %s '%s' defined twice (as '%s' v.s. '%s') and not overwritten.", type, entry.getKey(), value, targetValue));
+                            throw new IllegalStateException(String.format("Can't merge %s '%s' defined twice (as '%s' v.s. '%s') and not overridden.", type, entry.getKey(), value, targetValue));
                         }
                     }
                     else {
@@ -106,8 +119,8 @@ class BuilderUtil {
 
     // variables
     static void mergeVariables(Map<String,String> target, Map<String,String> source, BuilderContext context) {
-        mergeWithContextOverwrite("Variable", target, source,
-                (null != context) ? context.getVariablesOverwrites().entrySet() : null);
+        mergeWithContextOverride("Variable", target, source,
+                (null != context) ? context.getVariablesOverrides().entrySet() : null);
     }
 
     /**
@@ -115,32 +128,40 @@ class BuilderUtil {
      *
      * @param target             The target bundles
      * @param source             The source bundles
-     * @param originatingFeature Optional, if set origin will be recorded
+     * @param sourceFeature Optional, if set origin will be recorded
      * @param artifactMergeAlg   Algorithm used to merge the artifacts
      */
     static void mergeBundles(final Bundles target,
         final Bundles source,
-        final Feature originatingFeature,
-            final ArtifactMergeAlgorithm artifactMergeAlg) {
+        final Feature sourceFeature,
+        final List<String> artifactOverrides) {
         for(final Map.Entry<Integer, List<Artifact>> entry : source.getBundlesByStartOrder().entrySet()) {
             for(final Artifact a : entry.getValue()) {
-                // version handling - use provided algorithm
-                boolean replace = true;
-                if (artifactMergeAlg == ArtifactMergeAlgorithm.HIGHEST) {
-                    final Artifact existing = target.getSame(a.getId());
-                    if ( existing != null && existing.getId().getOSGiVersion().compareTo(a.getId().getOSGiVersion()) > 0 ) {
-                        replace = false;
+                Artifact existing = target.getSame(a.getId());
+                List<Artifact> selectedArtifacts = null;
+                if (existing != null) {
+                    if (sourceFeature.getId().toMvnId().equals(
+                            existing.getMetadata().get(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE))) {
+                        // If the source artifact came from the same feature, keep them side-by-side
+                        selectedArtifacts = Arrays.asList(existing, a);
+                    } else {
+                        selectedArtifacts = selectArtifactOverride(existing, a, artifactOverrides);
+                        while(target.removeSame(existing.getId())) {
+                            // Keep executing removeSame() which ignores the version until last one was removed
+                        }
                     }
+                } else {
+                    selectedArtifacts = Collections.singletonList(a);
                 }
-                if ( replace ) {
-                    target.removeSame(a.getId());
+
+                for (Artifact sa : selectedArtifacts) {
                     // create a copy to detach artifact from source
-                    final Artifact cp = a.copy(a.getId());
+                    final Artifact cp = sa.copy(sa.getId());
                     // Record the original feature of the bundle
-                    if (originatingFeature != null
-                            && a.getMetadata().get(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE) == null) {
+                    if (sourceFeature != null && source.contains(sa)
+                            && sa.getMetadata().get(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE) == null) {
                         cp.getMetadata().put(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE,
-                                originatingFeature.getId().toMvnId());
+                                sourceFeature.getId().toMvnId());
                     }
                     target.add(cp);
                 }
@@ -148,6 +169,51 @@ class BuilderUtil {
         }
     }
 
+    static List<Artifact> selectArtifactOverride(Artifact a1, Artifact a2, List<String> artifactOverrides) {
+        if (a1.getId().equals(a2.getId())) {
+            // They're the same so return one of them
+            return Collections.singletonList(a2);
+        }
+
+        String a1gid = a1.getId().getGroupId();
+        String a1aid = a1.getId().getArtifactId();
+        String a2gid = a2.getId().getGroupId();
+        String a2aid = a2.getId().getArtifactId();
+
+        if (!a1gid.equals(a2gid))
+            throw new IllegalStateException("Artifacts must have the same group ID: " + a1 + " and " + a2);
+        if (!a1aid.equals(a2aid))
+            throw new IllegalStateException("Artifacts must have the same artifact ID: " + a1 + " and " + a2);
+
+        String prefix = a1gid + ":" + a1aid + ":";
+        for (String o : artifactOverrides) {
+            if (o.startsWith(prefix) || o.startsWith(CATCHALL_OVERRIDE)) {
+                int idx = o.lastIndexOf(':');
+                if (idx <= 0 || o.length() <= idx)
+                    continue;
+
+                String rule = o.substring(idx+1).trim();
+
+                if (OVERRIDE_SELECT_ALL.equals(rule)) {
+                    return Arrays.asList(a1, a2);
+                } else if (OVERRIDE_SELECT_HIGHEST.equals(rule)) {
+                    Version a1v = a1.getId().getOSGiVersion();
+                    Version a2v = a2.getId().getOSGiVersion();
+                    return a1v.compareTo(a2v) > 0 ? Collections.singletonList(a1) : Collections.singletonList(a2);
+                } else if (OVERRIDE_SELECT_LATEST.equals(rule)) {
+                    return Collections.singletonList(a2);
+                } else if (a1.getId().getVersion().equals(rule)) {
+                    return Collections.singletonList(a1);
+                } else if (a2.getId().getVersion().equals(rule)) {
+                    return Collections.singletonList(a2);
+                }
+                throw new IllegalStateException("Override rule " + o + " not applicable to artifacts " + a1 + " and " + a2);
+            }
+        }
+        throw new IllegalStateException("Artifact override rule required to select between these two artifacts " +
+                a1 + " and " + a2);
+    }
+
     // configurations - merge / override
     static void mergeConfigurations(final Configurations target, final Configurations source, final Feature origin) {
         for(final Configuration cfg : source) {
@@ -176,8 +242,8 @@ class BuilderUtil {
 
     // framework properties (add/merge)
     static void mergeFrameworkProperties(final Map<String,String> target, final Map<String,String> source, BuilderContext context) {
-        mergeWithContextOverwrite("Property", target, source,
-                context != null ? context.getFrameworkPropertiesOverwrites().entrySet() : null);
+        mergeWithContextOverride("Property", target, source,
+                context != null ? context.getFrameworkPropertiesOverrides().entrySet() : null);
     }
 
     // requirements (add)
@@ -203,13 +269,13 @@ class BuilderUtil {
      *
      * @param target             The target extension
      * @param source             The source extension
-     * @param originatingFeature Optional, if set origin will be recorded for
-     *                           artifacts
+     * @param originatingFeature Optional, if set origin will be recorded for artifacts
      * @param artifactMergeAlg   The merge algorithm for artifacts
      */
     static void mergeExtensions(final Extension target,
-            final Extension source, final Feature originatingFeature,
-            final ArtifactMergeAlgorithm artifactMergeAlg) {
+            final Extension source,
+            final Feature sourceFeature,
+            final List<String> artifactOverrides) {
         switch ( target.getType() ) {
             case TEXT : // simply append
                 target.setText(target.getText() + "\n" + source.getText());
@@ -249,25 +315,32 @@ class BuilderUtil {
                 break;
 
         case ARTIFACTS:
-            for (final Artifact a : source.getArtifacts()) {
-                // use artifactMergeAlg
-                boolean replace = true;
-                if (artifactMergeAlg == ArtifactMergeAlgorithm.HIGHEST) {
-                    final Artifact existing = target.getArtifacts().getSame(a.getId());
-                    if (existing != null
-                            && existing.getId().getOSGiVersion().compareTo(a.getId().getOSGiVersion()) > 0) {
-                        replace = false;
+            for(final Artifact a : source.getArtifacts()) {
+                Artifact existing = target.getArtifacts().getSame(a.getId());
+                List<Artifact> selectedArtifacts = null;
+                if (existing != null) {
+                    if (sourceFeature.getId().toMvnId().equals(
+                            existing.getMetadata().get(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE))) {
+                        // If the source artifact came from the same feature, keep them side-by-side
+                        selectedArtifacts = Arrays.asList(existing, a);
+                    } else {
+                        selectedArtifacts = selectArtifactOverride(existing, a, artifactOverrides);
+                        while(target.getArtifacts().removeSame(existing.getId())) {
+                            // Keep executing removeSame() which ignores the version until last one was removed
+                        }
                     }
+                } else {
+                    selectedArtifacts = Collections.singletonList(a);
                 }
-                if (replace) {
-                    target.getArtifacts().removeSame(a.getId());
+
+                for (Artifact sa : selectedArtifacts) {
                     // create a copy to detach artifact from source
-                    final Artifact cp = a.copy(a.getId());
-                    // Record the original feature of the artifact
-                    if (originatingFeature != null
-                            && a.getMetadata().get(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE) == null) {
+                    final Artifact cp = sa.copy(sa.getId());
+                    // Record the original feature of the bundle
+                    if (sourceFeature != null && source.getArtifacts().contains(sa)
+                            && sa.getMetadata().get(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE) == null) {
                         cp.getMetadata().put(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE,
-                                originatingFeature.getId().toMvnId());
+                                sourceFeature.getId().toMvnId());
                     }
                     target.getArtifacts().add(cp);
                 }
@@ -279,7 +352,8 @@ class BuilderUtil {
     // extensions (add/merge)
     static void mergeExtensions(final Feature target,
         final Feature source,
-            final ArtifactMergeAlgorithm artifactMergeAlg, final BuilderContext context, final boolean recordOrigin) {
+        final BuilderContext context,
+        final List<String> artifactOverrides) {
         for(final Extension ext : source.getExtensions()) {
             boolean found = false;
 
@@ -301,7 +375,7 @@ class BuilderUtil {
                     }
                     if ( !handled ) {
                         // default merge
-                        mergeExtensions(current, ext, recordOrigin ? source : null, artifactMergeAlg);
+                        mergeExtensions(current, ext, source, artifactOverrides);
                     }
                 }
             }
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 f844443..b9f8e94 100644
--- a/src/main/java/org/apache/sling/feature/builder/FeatureBuilder.java
+++ b/src/main/java/org/apache/sling/feature/builder/FeatureBuilder.java
@@ -18,6 +18,7 @@ package org.apache.sling.feature.builder;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Dictionary;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -195,7 +196,7 @@ public abstract class FeatureBuilder {
             }
             usedFeatures.add(assembled.getId());
 
-            merge(target, assembled, context, context.getMergeAlgorithm(), true);
+            merge(target, assembled, context, context.getArtifactOverrides());
         }
 
         // append feature list in extension
@@ -323,10 +324,15 @@ public abstract class FeatureBuilder {
             // process include instructions
             processInclude(includedFeature, i);
 
-            // and now merge
-            merge(result, includedFeature, context, BuilderContext.ArtifactMergeAlgorithm.LATEST, true);
+            // and now merge the included feature into the result. No overrides should be needed since the result is empty before
+            merge(result, includedFeature, context, Collections.emptyList());
 
-            merge(result, feature, context, BuilderContext.ArtifactMergeAlgorithm.LATEST, false);
+            // and merge the current feature over the included feature into the result
+            merge(result, feature, context, Collections.singletonList(
+                    BuilderUtil.CATCHALL_OVERRIDE + BuilderUtil.OVERRIDE_SELECT_LATEST));
+
+            // set the origin of the artifacts and configurations included and part of the resulting feature back to null
+            resetOrigins(result, includedFeature);
         }
 
         result.setAssembled(true);
@@ -339,18 +345,14 @@ public abstract class FeatureBuilder {
     private static void merge(final Feature target,
             final Feature source,
             final BuilderContext context,
-            final BuilderContext.ArtifactMergeAlgorithm mergeAlg, final boolean recordOrigin) {
+            final List<String> artifactOverrides) {
         BuilderUtil.mergeVariables(target.getVariables(), source.getVariables(), context);
-        BuilderUtil.mergeBundles(target.getBundles(), source.getBundles(), recordOrigin ? source : null, mergeAlg);
-        BuilderUtil.mergeConfigurations(target.getConfigurations(), source.getConfigurations(),
-                recordOrigin ? source : null);
+        BuilderUtil.mergeBundles(target.getBundles(), source.getBundles(), source, artifactOverrides);
+        BuilderUtil.mergeConfigurations(target.getConfigurations(), source.getConfigurations(), source);
         BuilderUtil.mergeFrameworkProperties(target.getFrameworkProperties(), source.getFrameworkProperties(), context);
         BuilderUtil.mergeRequirements(target.getRequirements(), source.getRequirements());
         BuilderUtil.mergeCapabilities(target.getCapabilities(), source.getCapabilities());
-        BuilderUtil.mergeExtensions(target,
-                source,
-                mergeAlg,
-                context, recordOrigin);
+        BuilderUtil.mergeExtensions(target, source, context, artifactOverrides);
     }
 
     /**
@@ -466,4 +468,34 @@ public abstract class FeatureBuilder {
             }
         }
     }
+
+    // Set the origin of elements coming from an included feature to the target feature
+    private static void resetOrigins(Feature feature, Feature includedFeature) {
+        String currentID = feature.getId().toMvnId();
+        String includedID = includedFeature.getId().toMvnId();
+
+        // As the feature is a prototype, set the origins to the target where it's going to
+        for (Artifact a : feature.getBundles()) {
+            Map<String, String> md = a.getMetadata();
+            if (currentID.equals(md.get(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE))
+                    || includedID.equals(md.get(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE)))
+                md.remove(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE);
+        }
+        for (Configuration c : feature.getConfigurations()) {
+            Dictionary<String, Object> props = c.getProperties();
+            if (currentID.equals(props.get(Configuration.PROP_ORIGINAL__FEATURE))
+                    || includedID.equals(props.get(Configuration.PROP_ORIGINAL__FEATURE)))
+                props.remove(Configuration.PROP_ORIGINAL__FEATURE);
+        }
+        for (Extension e : feature.getExtensions()) {
+            if (ExtensionType.ARTIFACTS == e.getType()) {
+                for (Artifact a : e.getArtifacts()) {
+                    Map<String, String> md = a.getMetadata();
+                    if (currentID.equals(md.get(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE))
+                            || includedID.equals(md.get(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE)))
+                        md.remove(FeatureConstants.ARTIFACT_ATTR_ORIGINAL_FEATURE);
+                }
+            }
+        }
+    }
 }
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 5fd187d..23f3338 100644
--- a/src/test/java/org/apache/sling/feature/builder/BuilderUtilTest.java
+++ b/src/test/java/org/apache/sling/feature/builder/BuilderUtilTest.java
@@ -22,7 +22,6 @@ import org.apache.sling.feature.Bundles;
 import org.apache.sling.feature.Extension;
 import org.apache.sling.feature.ExtensionType;
 import org.apache.sling.feature.Feature;
-import org.apache.sling.feature.builder.BuilderContext.ArtifactMergeAlgorithm;
 import org.junit.Test;
 import org.mockito.Mockito;
 
@@ -30,6 +29,7 @@ import java.io.StringReader;
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -105,7 +105,8 @@ public class BuilderUtilTest {
 
         final Feature orgFeat = new Feature(new ArtifactId("gid", "aid", "123", null, null));
 
-        BuilderUtil.mergeBundles(target, source, orgFeat, ArtifactMergeAlgorithm.HIGHEST);
+        List<String> overrides = Arrays.asList("g:a:HIGHEST", "g:b:HIGHEST");
+        BuilderUtil.mergeBundles(target, source, orgFeat, overrides);
 
         final List<Map.Entry<Integer, Artifact>> result = getBundles(target);
         assertEquals(3, result.size());
@@ -133,7 +134,8 @@ public class BuilderUtilTest {
 
         final Feature orgFeat = new Feature(new ArtifactId("gid", "aid", "123", null, null));
 
-        BuilderUtil.mergeBundles(target, source, orgFeat, ArtifactMergeAlgorithm.LATEST);
+        List<String> overrides = Arrays.asList("g:a:LATEST", "g:b:LATEST");
+        BuilderUtil.mergeBundles(target, source, orgFeat, overrides);
 
         final List<Map.Entry<Integer, Artifact>> result = getBundles(target);
         assertEquals(3, result.size());
@@ -151,7 +153,8 @@ public class BuilderUtilTest {
         source.add(createBundle("g/a/1.1", 2));
 
         final Feature orgFeat = new Feature(new ArtifactId("gid", "aid", "123", null, null));
-        BuilderUtil.mergeBundles(target, source, orgFeat, ArtifactMergeAlgorithm.LATEST);
+        List<String> overrides = Arrays.asList("g:a:LATEST", "g:b:LATEST");
+        BuilderUtil.mergeBundles(target, source, orgFeat, overrides);
 
         final List<Map.Entry<Integer, Artifact>> result = getBundles(target);
         assertEquals(1, result.size());
@@ -171,7 +174,7 @@ public class BuilderUtilTest {
         source.add(createBundle("g/f/2.5", 3));
 
         final Feature orgFeat = new Feature(new ArtifactId("gid", "aid", "123", null, null));
-        BuilderUtil.mergeBundles(target, source, orgFeat, ArtifactMergeAlgorithm.LATEST);
+        BuilderUtil.mergeBundles(target, source, orgFeat, new ArrayList<>());
 
         final List<Map.Entry<Integer, Artifact>> result = getBundles(target);
         assertEquals(6, result.size());
@@ -191,11 +194,11 @@ public class BuilderUtilTest {
         source.add(createBundle("g/b/1.0", 1));
 
         final Feature orgFeat = new Feature(new ArtifactId("gid", "aid", "123", "c1", "slingfeature"));
-        BuilderUtil.mergeBundles(target, source, orgFeat, ArtifactMergeAlgorithm.LATEST);
+        BuilderUtil.mergeBundles(target, source, orgFeat, new ArrayList<>());
 
         final Bundles target2 = new Bundles();
         final Feature orgFeat2 = new Feature(new ArtifactId("g", "a", "1", null, null));
-        BuilderUtil.mergeBundles(target2, target, orgFeat2, ArtifactMergeAlgorithm.LATEST);
+        BuilderUtil.mergeBundles(target2, target, orgFeat2, new ArrayList<>());
 
         List<Entry<Integer, Artifact>> result = getBundles(target2);
         assertEquals(2, result.size());
@@ -214,7 +217,7 @@ public class BuilderUtilTest {
 
         source.setJSON("[\"source1\",\"source2\"]");
 
-        BuilderUtil.mergeExtensions(target, source, null, ArtifactMergeAlgorithm.HIGHEST);
+        BuilderUtil.mergeExtensions(target, source, null, new ArrayList<>());
 
         assertEquals(target.getJSON(), "[\"target1\",\"target2\",\"source1\",\"source2\"]");
 
@@ -306,7 +309,7 @@ public class BuilderUtilTest {
         Feature ft = new Feature(ArtifactId.fromMvnId("g:t:1"));
 
         assertEquals("Precondition", 0, ft.getExtensions().size());
-        BuilderUtil.mergeExtensions(ft, fs, ArtifactMergeAlgorithm.LATEST, ctx, true);
+        BuilderUtil.mergeExtensions(ft, fs, ctx, new ArrayList<>());
         assertEquals(1, ft.getExtensions().size());
 
         Extension actual = ft.getExtensions().get(0);
@@ -330,7 +333,7 @@ public class BuilderUtilTest {
         ft.getExtensions().add(et);
 
         assertEquals("Precondition", 1, ft.getExtensions().size());
-        BuilderUtil.mergeExtensions(ft, fs, ArtifactMergeAlgorithm.LATEST, ctx, true);
+        BuilderUtil.mergeExtensions(ft, fs, ctx, new ArrayList<>());
         assertEquals(1, ft.getExtensions().size());
 
         Extension actual = ft.getExtensions().get(0);
@@ -357,7 +360,7 @@ public class BuilderUtilTest {
         Feature ft = new Feature(ArtifactId.fromMvnId("g:t:1"));
 
         assertEquals("Precondition", 0, ft.getExtensions().size());
-        BuilderUtil.mergeExtensions(ft, fs, ArtifactMergeAlgorithm.LATEST, ctx, true);
+        BuilderUtil.mergeExtensions(ft, fs, ctx, new ArrayList<>());
         assertEquals(1, ft.getExtensions().size());
 
         Extension actual = ft.getExtensions().get(0);
@@ -382,7 +385,7 @@ public class BuilderUtilTest {
         ft.getExtensions().add(et);
 
         assertEquals("Precondition", 1, ft.getExtensions().size());
-        BuilderUtil.mergeExtensions(ft, fs, ArtifactMergeAlgorithm.LATEST, ctx, true);
+        BuilderUtil.mergeExtensions(ft, fs, ctx, new ArrayList<>());
         assertEquals(1, ft.getExtensions().size());
 
         Extension actual = ft.getExtensions().get(0);
@@ -393,6 +396,80 @@ public class BuilderUtilTest {
         assertEquals(er.readObject(), ar.readObject());
     }
 
+    @Test public void testSelectArtifactOverrideAll() {
+        Artifact a1 = new Artifact(ArtifactId.fromMvnId("gid:aid:1"));
+        Artifact a2 = new Artifact(ArtifactId.fromMvnId("gid:aid:2"));
+        List<String> overrides = Arrays.asList("gid:aid2:1", "gid:aid:ALL ");
+        assertEquals(Arrays.asList(a1, a2), BuilderUtil.selectArtifactOverride(a1, a2, overrides));
+    }
+
+    @Test public void testSelectArtifactOverrideIdenticalNeedsNoRule() {
+        Artifact a1 = new Artifact(ArtifactId.fromMvnId("gid:aid:1"));
+        Artifact a2 = new Artifact(ArtifactId.fromMvnId("gid:aid:1"));
+        assertEquals(Collections.singletonList(a1), BuilderUtil.selectArtifactOverride(a1, a2, Collections.emptyList()));
+    }
+
+    @Test public void testSelectArtifactOverride1() {
+        Artifact a1 = new Artifact(ArtifactId.fromMvnId("gid:aid:1"));
+        Artifact a2 = new Artifact(ArtifactId.fromMvnId("gid:aid:2"));
+        List<String> overrides = Collections.singletonList("gid:aid:1");
+        assertEquals(Collections.singletonList(a1), BuilderUtil.selectArtifactOverride(a1, a2, overrides));
+    }
+
+    @Test public void testSelectArtifactOverride2() {
+        Artifact a1 = new Artifact(ArtifactId.fromMvnId("gid:aid:1"));
+        Artifact a2 = new Artifact(ArtifactId.fromMvnId("gid:aid:2"));
+        List<String> overrides = Collections.singletonList("gid:aid:2");
+        assertEquals(Collections.singletonList(a2), BuilderUtil.selectArtifactOverride(a1, a2, overrides));
+    }
+
+    @Test(expected=IllegalStateException.class)
+    public void testSelectArtifactOverride3() {
+        Artifact a1 = new Artifact(ArtifactId.fromMvnId("gid:aid:1"));
+        Artifact a2 = new Artifact(ArtifactId.fromMvnId("gid:aid:2"));
+        List<String> overrides = Collections.singletonList("gid:aid:3");
+        BuilderUtil.selectArtifactOverride(a1, a2, overrides);
+    }
+
+    @Test(expected=IllegalStateException.class)
+    public void testSelectArtifactOverrideDifferentGroupID() {
+        Artifact a1 = new Artifact(ArtifactId.fromMvnId("aid:aid:1"));
+        Artifact a2 = new Artifact(ArtifactId.fromMvnId("gid:aid:2"));
+        List<String> overrides = Collections.singletonList("gid:aid:2");
+        BuilderUtil.selectArtifactOverride(a1, a2, overrides);
+    }
+
+    @Test(expected=IllegalStateException.class)
+    public void testSelectArtifactOverrideDifferentArtifactID() {
+        Artifact a1 = new Artifact(ArtifactId.fromMvnId("gid:gid:1"));
+        Artifact a2 = new Artifact(ArtifactId.fromMvnId("gid:aid:2"));
+        List<String> overrides = Collections.singletonList("gid:aid:2");
+        BuilderUtil.selectArtifactOverride(a1, a2, overrides);
+    }
+
+    @Test(expected=IllegalStateException.class)
+    public void testSelectArtifactOverrideDifferentNoRule() {
+        Artifact a1 = new Artifact(ArtifactId.fromMvnId("gid:aid:1"));
+        Artifact a2 = new Artifact(ArtifactId.fromMvnId("gid:aid:2"));
+        BuilderUtil.selectArtifactOverride(a1, a2, Collections.emptyList());
+    }
+
+    @Test public void testSelectArtifactOverrideHigest() {
+        Artifact a1 = new Artifact(ArtifactId.fromMvnId("gid:aid:1.1"));
+        Artifact a2 = new Artifact(ArtifactId.fromMvnId("gid:aid:2.0.1"));
+        List<String> overrides = Collections.singletonList("gid:aid:HIGHEST");
+        assertEquals(Collections.singletonList(a2), BuilderUtil.selectArtifactOverride(a1, a2, overrides));
+        assertEquals(Collections.singletonList(a2), BuilderUtil.selectArtifactOverride(a2, a1, overrides));
+    }
+
+    @Test public void testSelectArtifactOverrideLatest() {
+        Artifact a1 = new Artifact(ArtifactId.fromMvnId("gid:aid:1.1"));
+        Artifact a2 = new Artifact(ArtifactId.fromMvnId("gid:aid:2.0.1"));
+        List<String> overrides = Collections.singletonList("gid:aid:LATEST");
+        assertEquals(Collections.singletonList(a2), BuilderUtil.selectArtifactOverride(a1, a2, overrides));
+        assertEquals(Collections.singletonList(a1), BuilderUtil.selectArtifactOverride(a2, a1, overrides));
+    }
+
     @SafeVarargs
     public static Artifact createBundle(final String id, final int startOrder, Map.Entry<String, String> ... metadata) {
         final Artifact a = new Artifact(ArtifactId.parse(id));
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 91c6235..b81a7f2 100644
--- a/src/test/java/org/apache/sling/feature/builder/FeatureBuilderTest.java
+++ b/src/test/java/org/apache/sling/feature/builder/FeatureBuilderTest.java
@@ -16,23 +16,6 @@
  */
 package org.apache.sling.feature.builder;
 
-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.AbstractMap;
-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.Set;
-
 import org.apache.felix.utils.resource.CapabilityImpl;
 import org.apache.felix.utils.resource.RequirementImpl;
 import org.apache.sling.feature.Artifact;
@@ -47,6 +30,22 @@ import org.junit.Test;
 import org.osgi.resource.Capability;
 import org.osgi.resource.Requirement;
 
+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.Set;
+
+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;
+
 public class FeatureBuilderTest {
 
     private static final Map<String, Feature> FEATURES = new HashMap<>();
@@ -71,6 +70,25 @@ public class FeatureBuilderTest {
         FEATURES.put(f1.getId().toMvnId(), f1);
     }
 
+    static {
+        final Feature f2 = new Feature(ArtifactId.parse("g/a/2"));
+
+        f2.getBundles().add(BuilderUtilTest.createBundle("group/testmulti/1", 4));
+        f2.getBundles().add(BuilderUtilTest.createBundle("group/testmulti/2", 8));
+        f2.getBundles().add(BuilderUtilTest.createBundle("group/someart/1.2.3", 4));
+
+        FEATURES.put(f2.getId().toMvnId(), f2);
+    }
+
+    static {
+        final Feature f2 = new Feature(ArtifactId.parse("g/a/3"));
+
+        f2.getBundles().add(BuilderUtilTest.createBundle("group/testmulti/2", 8));
+        f2.getBundles().add(BuilderUtilTest.createBundle("group/someart/1.2.3", 4));
+
+        FEATURES.put(f2.getId().toMvnId(), f2);
+    }
+
     private final FeatureProvider provider = new FeatureProvider() {
 
         @Override
@@ -276,16 +294,14 @@ public class FeatureBuilderTest {
         final Artifact a1 = new Artifact(ArtifactId.parse("org.apache.sling/oak-server/1.0.0"));
         a1.getMetadata().put(Artifact.KEY_START_ORDER, "1");
         a1.getMetadata().put("hash", "4632463464363646436");
-        a1.getMetadata().put("org-feature", "org.apache.sling:test-feature:1.1");
         base.getBundles().add(a1);
-        Map.Entry<String, String> md = new AbstractMap.SimpleEntry<String, String>("org-feature", "org.apache.sling:test-feature:1.1");
-        base.getBundles().add(BuilderUtilTest.createBundle("org.apache.sling/application-bundle/2.0.0", 1, md));
-        base.getBundles().add(BuilderUtilTest.createBundle("org.apache.sling/another-bundle/2.1.0", 1, md));
-        base.getBundles().add(BuilderUtilTest.createBundle("org.apache.sling/foo-xyz/1.2.3", 2, md));
-        base.getBundles().add(BuilderUtilTest.createBundle("group/testnewversion_low/1", 5, md));
-        base.getBundles().add(BuilderUtilTest.createBundle("group/testnewversion_high/5", 5, md));
-        base.getBundles().add(BuilderUtilTest.createBundle("group/testnewstartlevel/1", 10, md));
-        base.getBundles().add(BuilderUtilTest.createBundle("group/testnewstartlevelandversion/2", 10, md));
+        base.getBundles().add(BuilderUtilTest.createBundle("org.apache.sling/application-bundle/2.0.0", 1));
+        base.getBundles().add(BuilderUtilTest.createBundle("org.apache.sling/another-bundle/2.1.0", 1));
+        base.getBundles().add(BuilderUtilTest.createBundle("org.apache.sling/foo-xyz/1.2.3", 2));
+        base.getBundles().add(BuilderUtilTest.createBundle("group/testnewversion_low/1", 5));
+        base.getBundles().add(BuilderUtilTest.createBundle("group/testnewversion_high/5", 5));
+        base.getBundles().add(BuilderUtilTest.createBundle("group/testnewstartlevel/1", 10));
+        base.getBundles().add(BuilderUtilTest.createBundle("group/testnewstartlevelandversion/2", 10));
 
         final Configuration co1 = new Configuration("my.pid");
         co1.getProperties().put("foo", 5L);
@@ -304,11 +320,9 @@ public class FeatureBuilderTest {
         result.getVariables().put("varx", "myvalx");
         result.setInclude(null);
         result.getFrameworkProperties().put("bar", "X");
-        Map.Entry<String, String> md2 = new AbstractMap.SimpleEntry<String, String>("org-feature", "g:a:1");
-        result.getBundles().add(BuilderUtilTest.createBundle("org.apache.sling/foo-bar/4.5.6", 3, md2));
+        result.getBundles().add(BuilderUtilTest.createBundle("org.apache.sling/foo-bar/4.5.6", 3));
         final Configuration co3 = new Configuration("org.apache.sling.foo");
         co3.getProperties().put("prop", "value");
-        co3.getProperties().put(Configuration.PROP_ORIGINAL__FEATURE, i1.getId().toMvnId());
         result.getConfigurations().add(co3);
 
         // assemble
@@ -318,6 +332,103 @@ public class FeatureBuilderTest {
         equals(result, assembled);
     }
 
+    @Test public void testSingleIncludeMultiVersion() {
+        Feature base = new Feature(ArtifactId.fromMvnId("g:tgtart:1"));
+        Include i1 = new Include(ArtifactId.fromMvnId("g:a:3"));
+        base.setInclude(i1);
+        base.getBundles().add(new Artifact(ArtifactId.fromMvnId("g:myart:1")));
+        base.getBundles().add(new Artifact(ArtifactId.fromMvnId("group:testmulti:1")));
+        base.getBundles().add(new Artifact(ArtifactId.fromMvnId("group:testmulti:3")));
+
+        BuilderContext builderContext = new BuilderContext(provider);
+        Feature assembled = FeatureBuilder.assemble(base, builderContext);
+
+        Feature result = new Feature(ArtifactId.parse("g:tgtart:1"));
+        Artifact b0 = new Artifact(ArtifactId.fromMvnId("g:myart:1"));
+        result.getBundles().add(b0);
+        Artifact b1 = new Artifact(ArtifactId.fromMvnId("group:testmulti:1"));
+        result.getBundles().add(b1);
+        Artifact b2 = new Artifact(ArtifactId.fromMvnId("group:testmulti:3"));
+        result.getBundles().add(b2);
+        Artifact b3 = new Artifact(ArtifactId.fromMvnId("group:someart:1.2.3"));
+        b3.setStartOrder(4);
+        result.getBundles().add(b3);
+
+        equals(result, assembled);
+    }
+
+    @Test public void testSingleIncludeMultiVersion2() {
+        Feature base = new Feature(ArtifactId.fromMvnId("g:tgtart:1"));
+        Include i1 = new Include(ArtifactId.fromMvnId("g:a:2"));
+        base.setInclude(i1);
+        base.getBundles().add(new Artifact(ArtifactId.fromMvnId("g:myart:1")));
+
+        BuilderContext builderContext = new BuilderContext(provider);
+        Feature assembled = FeatureBuilder.assemble(base, builderContext);
+
+        Feature result = new Feature(ArtifactId.parse("g:tgtart:1"));
+        Artifact b0 = new Artifact(ArtifactId.fromMvnId("g:myart:1"));
+        result.getBundles().add(b0);
+        Artifact b1 = new Artifact(ArtifactId.fromMvnId("group:testmulti:1"));
+        b1.setStartOrder(4);
+        result.getBundles().add(b1);
+        Artifact b2 = new Artifact(ArtifactId.fromMvnId("group:testmulti:2"));
+        b2.setStartOrder(8);
+        result.getBundles().add(b2);
+        Artifact b3 = new Artifact(ArtifactId.fromMvnId("group:someart:1.2.3"));
+        b3.setStartOrder(4);
+        result.getBundles().add(b3);
+
+        equals(result, assembled);
+    }
+
+    @Test public void testSingleIncludeMultiVersion3() {
+        Feature base = new Feature(ArtifactId.fromMvnId("g:tgtart:1"));
+        Include i1 = new Include(ArtifactId.fromMvnId("g:a:2"));
+        base.setInclude(i1);
+        base.getBundles().add(new Artifact(ArtifactId.fromMvnId("g:myart:1")));
+        base.getBundles().add(new Artifact(ArtifactId.fromMvnId("group:testmulti:1")));
+
+        BuilderContext builderContext = new BuilderContext(provider);
+        Feature assembled = FeatureBuilder.assemble(base, builderContext);
+
+        Feature result = new Feature(ArtifactId.parse("g:tgtart:1"));
+        Artifact b0 = new Artifact(ArtifactId.fromMvnId("g:myart:1"));
+        result.getBundles().add(b0);
+        Artifact b1 = new Artifact(ArtifactId.fromMvnId("group:testmulti:1"));
+        result.getBundles().add(b1);
+        Artifact b3 = new Artifact(ArtifactId.fromMvnId("group:someart:1.2.3"));
+        b3.setStartOrder(4);
+        result.getBundles().add(b3);
+
+        equals(result, assembled);
+    }
+
+    @Test public void testSingleIncludeMultiVersion4() {
+        Feature base = new Feature(ArtifactId.fromMvnId("g:tgtart:1"));
+        Include i1 = new Include(ArtifactId.fromMvnId("g:a:2"));
+        base.setInclude(i1);
+        base.getBundles().add(new Artifact(ArtifactId.fromMvnId("g:myart:1")));
+        base.getBundles().add(new Artifact(ArtifactId.fromMvnId("group:testmulti:1")));
+        base.getBundles().add(new Artifact(ArtifactId.fromMvnId("group:testmulti:3")));
+
+        BuilderContext builderContext = new BuilderContext(provider);
+        Feature assembled = FeatureBuilder.assemble(base, builderContext);
+
+        Feature result = new Feature(ArtifactId.parse("g:tgtart:1"));
+        Artifact b0 = new Artifact(ArtifactId.fromMvnId("g:myart:1"));
+        result.getBundles().add(b0);
+        Artifact b1 = new Artifact(ArtifactId.fromMvnId("group:testmulti:1"));
+        result.getBundles().add(b1);
+        Artifact b2 = new Artifact(ArtifactId.fromMvnId("group:testmulti:3"));
+        result.getBundles().add(b2);
+        Artifact b3 = new Artifact(ArtifactId.fromMvnId("group:someart:1.2.3"));
+        b3.setStartOrder(4);
+        result.getBundles().add(b3);
+
+        equals(result, assembled);
+    }
+
     @Test public void testDeduplicationInclude() throws Exception {
         final ArtifactId idA = ArtifactId.fromMvnId("g:a:1.0.0");
         final ArtifactId idB = ArtifactId.fromMvnId("g:b:1.0.0");
@@ -624,7 +735,7 @@ public class FeatureBuilderTest {
             {
                 return null;
             }
-                }).addVariablesOverwrites(override), aFeature, bFeature);
+                }).addVariablesOverrides(override), aFeature, bFeature);
 
         Map<String,String> vars = new HashMap<>();
         vars.putAll(kvMapA);
@@ -645,7 +756,7 @@ public class FeatureBuilderTest {
                 {
                     return null;
                 }
-                    }).addVariablesOverwrites(override), aFeature, bFeature);
+                    }).addVariablesOverrides(override), aFeature, bFeature);
             fail("Excepted merge exception");
         } catch (IllegalStateException expected) {}
 
@@ -658,7 +769,7 @@ public class FeatureBuilderTest {
             {
                 return null;
             }
-                }).addVariablesOverwrites(override), aFeature, bFeature);
+                }).addVariablesOverrides(override), aFeature, bFeature);
 
         vars = new HashMap<>();
         vars.putAll(kvMapA);
@@ -677,7 +788,7 @@ public class FeatureBuilderTest {
             {
                 return null;
             }
-                }).addVariablesOverwrites(override), aFeature, bFeature);
+                }).addVariablesOverrides(override), aFeature, bFeature);
 
         vars.put("var2", null);
         assertTrue(cFeature.getVariables().equals(vars));