sling-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From dav...@apache.org
Subject [sling-org-apache-sling-feature] 21/22: Move Feature Model Builder into feature module.
Date Fri, 27 Apr 2018 09:51:41 GMT
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

commit ec2a23ee7ae3d57dfbd2698957409959dc11ecbf
Author: David Bosschaert <david.bosschaert@gmail.com>
AuthorDate: Wed Apr 25 10:19:53 2018 +0100

    Move Feature Model Builder into feature module.
---
 .../sling/feature/builder/ApplicationBuilder.java  | 153 +++++++++++
 .../sling/feature/builder/BuilderContext.java      |  81 ++++++
 .../apache/sling/feature/builder/BuilderUtil.java  | 267 ++++++++++++++++++
 .../sling/feature/builder/FeatureBuilder.java      | 182 +++++++++++++
 .../feature/builder/FeatureExtensionHandler.java   |  59 ++++
 .../sling/feature/builder/FeatureProvider.java     |  36 +++
 .../apache/sling/feature/builder/package-info.java |  23 ++
 .../sling/feature/builder/BuilderUtilTest.java     | 160 +++++++++++
 .../sling/feature/builder/FeatureBuilderTest.java  | 303 +++++++++++++++++++++
 9 files changed, 1264 insertions(+)

diff --git a/src/main/java/org/apache/sling/feature/builder/ApplicationBuilder.java b/src/main/java/org/apache/sling/feature/builder/ApplicationBuilder.java
new file mode 100644
index 0000000..e41dc94
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/builder/ApplicationBuilder.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.feature.builder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+
+/**
+ * Build an application based on features.
+ */
+public class ApplicationBuilder {
+
+    /**
+     * Assemble an application based on the provided feature Ids.
+     * The features are processed in the order they are provided.
+     *
+     * @param app The optional application to use as a base.
+     * @param context The builder context
+     * @param featureIds The feature ids
+     * @return The application
+     * throws IllegalArgumentException If context or featureIds is {@code null}
+     * throws IllegalStateException If the provided ids are invalid, or the feature can't be provided
+     */
+    public static Application assemble(final Application app,
+            final BuilderContext context,
+            final String... featureIds) {
+        if ( featureIds == null || context == null ) {
+            throw new IllegalArgumentException("Features and/or context must not be null");
+        }
+
+        final Feature[] features = new Feature[featureIds.length];
+        int index = 0;
+        for(final String id : featureIds) {
+            features[index] = context.getFeatureProvider().provide(ArtifactId.parse(id));
+            if ( features[index] == null ) {
+                throw new IllegalStateException("Unable to find included feature " + id);
+            }
+            index++;
+        }
+        return assemble(app, context, features);
+    }
+
+    /**
+     * Assemble an application based on the provided features.
+     *
+     * The features are processed in the order they are provided.
+     * If the same feature is included more than once only the feature with
+     * the highest version is used. The others are ignored.
+     *
+     * @param app The optional application to use as a base.
+     * @param context The builder context
+     * @param features The features
+     * @return The application
+     * throws IllegalArgumentException If context or featureIds is {@code null}
+     * throws IllegalStateException If a feature can't be provided
+     */
+    public static Application assemble(
+            Application app,
+            final BuilderContext context,
+            final Feature... features) {
+        if ( features == null || context == null ) {
+            throw new IllegalArgumentException("Features and/or context must not be null");
+        }
+
+        if ( app == null ) {
+            app = new Application();
+        }
+
+        // Remove duplicate features by selecting the one with the highest version
+        final List<Feature> featureList = new ArrayList<>();
+        for(final Feature f : features) {
+            Feature found = null;
+            for(final Feature s : featureList) {
+                if ( s.getId().isSame(f.getId()) ) {
+                    found = s;
+                    break;
+                }
+            }
+            boolean add = true;
+            // feature with different version found
+            if ( found != null ) {
+                if ( f.getId().getOSGiVersion().compareTo(found.getId().getOSGiVersion()) <= 0 ) {
+                    // higher version already included
+                    add = false;
+                } else {
+                    // remove lower version, higher version will be added
+                    featureList.remove(found);
+                }
+            }
+            if ( add ) {
+                featureList.add(f);
+            }
+        }
+
+        // assemble
+        int featureStartOrder = 5; // begin with start order a little higher than 0
+        for(final Feature f : featureList) {
+            app.getFeatureIds().add(f.getId());
+            final Feature assembled = FeatureBuilder.assemble(f, context.clone(new FeatureProvider() {
+
+                @Override
+                public Feature provide(final ArtifactId id) {
+                    for(final Feature f : features) {
+                        if ( f.getId().equals(id) ) {
+                            return f;
+                        }
+                    }
+                    return context.getFeatureProvider().provide(id);
+                }
+            }));
+
+            int globalStartOrder = featureStartOrder;
+            for (Artifact a : assembled.getBundles()) {
+                int so = a.getStartOrder() + featureStartOrder;
+                if (so > globalStartOrder)
+                    globalStartOrder = so;
+                a.setStartOrder(so);
+            }
+            // Next feature will have a higher start order than the previous
+            featureStartOrder = globalStartOrder + 1;
+
+            merge(app, assembled);
+        }
+
+        return app;
+    }
+
+    private static void merge(final Application target, final Feature source) {
+        BuilderUtil.mergeBundles(target.getBundles(), source.getBundles(), BuilderUtil.ArtifactMerge.HIGHEST);
+        BuilderUtil.mergeConfigurations(target.getConfigurations(), source.getConfigurations());
+        BuilderUtil.mergeFrameworkProperties(target.getFrameworkProperties(), source.getFrameworkProperties());
+        BuilderUtil.mergeExtensions(target, source, BuilderUtil.ArtifactMerge.HIGHEST);
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/builder/BuilderContext.java b/src/main/java/org/apache/sling/feature/builder/BuilderContext.java
new file mode 100644
index 0000000..ca3d82a
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/builder/BuilderContext.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.feature.builder;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Builder context holds services used by {@link ApplicationBuilder}
+ * and {@link FeatureBuilder}.
+ */
+public class BuilderContext {
+
+    private final FeatureProvider provider;
+
+    private final List<FeatureExtensionHandler> featureExtensionHandlers = new CopyOnWriteArrayList<>();
+
+    /**
+     * Create a new context
+     *
+     * @param provider A provider providing the included features
+     * @throws IllegalArgumentException If feature provider is {@code null}
+     */
+    public BuilderContext(final FeatureProvider provider) {
+        if ( provider == null ) {
+            throw new IllegalArgumentException("Provider must not be null");
+        }
+        this.provider = provider;
+    }
+
+    /**
+     * Add a feature extension handler
+     * @param handler A handler
+     * @return This instance
+     */
+    public BuilderContext add(final FeatureExtensionHandler handler) {
+        featureExtensionHandlers.add(handler);
+        return this;
+    }
+
+    /**
+     * Get the feature provider.
+     * @return The feature provider
+     */
+    FeatureProvider getFeatureProvider() {
+        return this.provider;
+    }
+
+    /**
+     * Get the list of extension handlers
+     * @return The list of handlers
+     */
+    List<FeatureExtensionHandler> getFeatureExtensionHandlers() {
+        return this.featureExtensionHandlers;
+    }
+
+    /**
+     * Clone the context and replace the feature provider
+     * @param featureProvider The new feature provider
+     * @return Cloned context
+     */
+    BuilderContext clone(final FeatureProvider featureProvider) {
+        final BuilderContext ctx = new BuilderContext(featureProvider);
+        ctx.featureExtensionHandlers.addAll(featureExtensionHandlers);
+        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
new file mode 100644
index 0000000..ad512dd
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/builder/BuilderUtil.java
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.feature.builder;
+
+import java.io.StringReader;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import javax.json.JsonStructure;
+import javax.json.JsonValue;
+import javax.json.JsonValue.ValueType;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.KeyValueMap;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+
+/**
+ * Utility methods for the builders
+ */
+class BuilderUtil {
+
+    enum ArtifactMerge {
+        LATEST,
+        HIGHEST
+    };
+
+    // bundles
+    static void mergeBundles(final Bundles target,
+            final Bundles source,
+            final ArtifactMerge artifactMergeAlg) {
+        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 == ArtifactMerge.HIGHEST ) {
+                    final Artifact existing = target.getSame(a.getId());
+                    if ( existing != null && existing.getId().getOSGiVersion().compareTo(a.getId().getOSGiVersion()) > 0 ) {
+                        replace = false;
+                    }
+                }
+                if ( replace ) {
+                    target.removeSame(a.getId());
+                    target.add(a);
+                }
+            }
+        }
+    }
+
+    // configurations - merge / override
+    static void mergeConfigurations(final Configurations target, final Configurations source) {
+        for(final Configuration cfg : source) {
+            boolean found = false;
+            for(final Configuration current : target) {
+                if ( current.compareTo(cfg) == 0 ) {
+                    found = true;
+                    // merge / override properties
+                    final Enumeration<String> i = cfg.getProperties().keys();
+                    while ( i.hasMoreElements() ) {
+                        final String key = i.nextElement();
+                        current.getProperties().put(key, cfg.getProperties().get(key));
+                    }
+                    break;
+                }
+            }
+            if ( !found ) {
+                target.add(cfg);
+            }
+        }
+    }
+
+    // framework properties (add/merge)
+    static void mergeFrameworkProperties(final KeyValueMap target, final KeyValueMap source) {
+        target.putAll(source);
+    }
+
+    // requirements (add)
+    static void mergeRequirements(final List<Requirement> target, final List<Requirement> source) {
+        for(final Requirement req : source) {
+            if ( !target.contains(req) ) {
+                target.add(req);
+            }
+        }
+    }
+
+    // capabilities (add)
+    static void mergeCapabilities(final List<Capability> target, final List<Capability> source) {
+        for(final Capability cap : source) {
+            if ( !target.contains(cap) ) {
+                target.add(cap);
+            }
+        }
+    }
+
+    // default merge for extensions
+    static void mergeExtensions(final Extension target,
+            final Extension source,
+            final ArtifactMerge artifactMergeAlg) {
+        switch ( target.getType() ) {
+            case TEXT : // simply append
+                        target.setText(target.getText() + "\n" + source.getText());
+                        break;
+            case JSON : final JsonStructure struct1;
+                        try ( final StringReader reader = new StringReader(target.getJSON()) ) {
+                            struct1 = Json.createReader(reader).read();
+                        }
+                        final JsonStructure struct2;
+                        try ( final StringReader reader = new StringReader(source.getJSON()) ) {
+                            struct2 = Json.createReader(reader).read();
+                        }
+
+                        if ( struct1.getValueType() != struct2.getValueType() ) {
+                            throw new IllegalStateException("Found different JSON types for extension " + target.getName()
+                                + " : " + struct1.getValueType() + " and " + struct2.getValueType());
+                        }
+                        if ( struct1.getValueType() == ValueType.ARRAY ) {
+                            // array is append
+                            final JsonArray a1 = (JsonArray)struct1;
+                            final JsonArray a2 = (JsonArray)struct2;
+                            for(final JsonValue val : a2) {
+                                a1.add(val);
+                            }
+                        } else {
+                            // object is merge
+                            merge((JsonObject)struct1, (JsonObject)struct2);
+                        }
+                        break;
+
+            case ARTIFACTS : for(final Artifact a : source.getArtifacts()) {
+                                 // use artifactMergeAlg
+                                 boolean add = true;
+                                 for(final Artifact targetArtifact : target.getArtifacts()) {
+                                     if ( targetArtifact.getId().isSame(a.getId()) ) {
+                                         if ( artifactMergeAlg == ArtifactMerge.HIGHEST ) {
+                                             if ( targetArtifact.getId().getOSGiVersion().compareTo(a.getId().getOSGiVersion()) > 0 ) {
+                                                 add = false;
+                                             } else {
+                                                 target.getArtifacts().remove(targetArtifact);
+                                             }
+                                         } else { // latest
+
+                                             target.getArtifacts().remove(targetArtifact);
+                                         }
+                                         break;
+                                     }
+                                 }
+
+                                 if ( add ) {
+                                     target.getArtifacts().add(a);
+                                 }
+
+                             }
+                             break;
+        }
+    }
+
+    // extensions (add/merge)
+    static void mergeExtensions(final Feature target,
+            final Feature source,
+            final ArtifactMerge artifactMergeAlg,
+            final BuilderContext context) {
+        for(final Extension ext : source.getExtensions()) {
+            boolean found = false;
+            for(final Extension current : target.getExtensions()) {
+                if ( current.getName().equals(ext.getName()) ) {
+                    found = true;
+                    if ( current.getType() != ext.getType() ) {
+                        throw new IllegalStateException("Found different types for extension " + current.getName()
+                        + " : " + current.getType() + " and " + ext.getType());
+                    }
+                    boolean handled = false;
+                    for(final FeatureExtensionHandler fem : context.getFeatureExtensionHandlers()) {
+                        if ( fem.canMerge(current.getName()) ) {
+                            fem.merge(target, source, current.getName());
+                            handled = true;
+                            break;
+                        }
+                    }
+                    if ( !handled ) {
+                        // default merge
+                        mergeExtensions(current, ext, artifactMergeAlg);
+                    }
+                }
+            }
+            if ( !found ) {
+                target.getExtensions().add(ext);
+            }
+        }
+        // post processing
+        for(final Extension ext : target.getExtensions()) {
+            for(final FeatureExtensionHandler fem : context.getFeatureExtensionHandlers()) {
+                fem.postProcess(target, ext.getName());
+            }
+        }
+    }
+
+    static void mergeExtensions(final Application target,
+            final Feature source,
+            final ArtifactMerge artifactMergeAlg) {
+        for(final Extension ext : source.getExtensions()) {
+            boolean found = false;
+            for(final Extension current : target.getExtensions()) {
+                if ( current.getName().equals(ext.getName()) ) {
+                    found = true;
+                    if ( current.getType() != ext.getType() ) {
+                        throw new IllegalStateException("Found different types for extension " + current.getName()
+                        + " : " + current.getType() + " and " + ext.getType());
+                    }
+                    // default merge
+                    mergeExtensions(current, ext, artifactMergeAlg);
+                }
+            }
+            if ( !found ) {
+                target.getExtensions().add(ext);
+            }
+        }
+    }
+
+    private static void merge(final JsonObject obj1, final JsonObject obj2) {
+        for(final Map.Entry<String, JsonValue> entry : obj2.entrySet()) {
+            if ( !obj1.containsKey(entry.getKey()) ) {
+                obj1.put(entry.getKey(), entry.getValue());
+            } else {
+                final JsonValue oldValue = obj1.get(entry.getKey());
+                if ( oldValue.getValueType() != entry.getValue().getValueType() ) {
+                    // new type wins
+                    obj1.put(entry.getKey(), entry.getValue());
+                } else if ( oldValue.getValueType() == ValueType.ARRAY ) {
+                    final JsonArray a1 = (JsonArray)oldValue;
+                    final JsonArray a2 = (JsonArray)entry.getValue();
+                    for(final JsonValue val : a2) {
+                        a1.add(val);
+                    }
+
+                } else if ( oldValue.getValueType() == ValueType.OBJECT ) {
+                    merge((JsonObject)oldValue, (JsonObject)entry.getValue());
+                } else {
+                    obj1.put(entry.getKey(), entry.getValue());
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/builder/FeatureBuilder.java b/src/main/java/org/apache/sling/feature/builder/FeatureBuilder.java
new file mode 100644
index 0000000..8713ed4
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/builder/FeatureBuilder.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.feature.builder;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Include;
+
+public class FeatureBuilder {
+
+    /**
+     * Assemble the full feature by processing all includes.
+     *
+     * @param feature The feature to start
+     * @param context The builder context
+     * @return The assembled feature.
+     * @throws IllegalArgumentException If feature or context is {@code null}
+     * @throws IllegalStateException If an included feature can't be provided or merged.
+     */
+    public static Feature assemble(final Feature feature,
+            final BuilderContext context) {
+        if ( feature == null || context == null ) {
+            throw new IllegalArgumentException("Feature and/or context must not be null");
+        }
+        return internalAssemble(new ArrayList<>(), feature, context);
+    }
+
+    private static Feature internalAssemble(final List<String> processedFeatures,
+            final Feature feature,
+            final BuilderContext context) {
+        if ( feature.isAssembled() ) {
+            return feature;
+        }
+        if ( processedFeatures.contains(feature.getId().toMvnId()) ) {
+            throw new IllegalStateException("Recursive inclusion of " + feature.getId().toMvnId() + " via " + processedFeatures);
+        }
+        processedFeatures.add(feature.getId().toMvnId());
+
+        // we copy the feature as we set the assembled flag on the result
+        final Feature result = feature.copy();
+
+        if ( !result.getIncludes().isEmpty() ) {
+
+            final List<Include> includes = new ArrayList<>(result.getIncludes());
+
+            // clear everything in the result, will be added in the process
+            result.getBundles().clear();
+            result.getFrameworkProperties().clear();
+            result.getConfigurations().clear();
+            result.getRequirements().clear();
+            result.getCapabilities().clear();
+            result.getIncludes().clear();
+            result.getExtensions().clear();
+
+            for(final Include i : includes) {
+                final Feature f = context.getFeatureProvider().provide(i.getId());
+                if ( f == null ) {
+                    throw new IllegalStateException("Unable to find included feature " + i.getId());
+                }
+                final Feature af = internalAssemble(processedFeatures, f, context);
+
+                // process include instructions
+                include(af, i);
+
+                // and now merge
+                merge(result, af, context);
+            }
+            merge(result, feature, context);
+        }
+        processedFeatures.remove(feature.getId().toMvnId());
+
+        result.setAssembled(true);
+        return result;
+    }
+
+    private static void merge(final Feature target,
+            final Feature source,
+            final BuilderContext context) {
+        BuilderUtil.mergeBundles(target.getBundles(), source.getBundles(), BuilderUtil.ArtifactMerge.LATEST);
+        BuilderUtil.mergeConfigurations(target.getConfigurations(), source.getConfigurations());
+        BuilderUtil.mergeFrameworkProperties(target.getFrameworkProperties(), source.getFrameworkProperties());
+        BuilderUtil.mergeRequirements(target.getRequirements(), source.getRequirements());
+        BuilderUtil.mergeCapabilities(target.getCapabilities(), source.getCapabilities());
+        BuilderUtil.mergeExtensions(target,
+                source,
+                BuilderUtil.ArtifactMerge.LATEST,
+                context);
+    }
+
+    private static void include(final Feature base, final Include i) {
+        // process removals
+        // bundles
+        for(final ArtifactId a : i.getBundleRemovals()) {
+            base.getBundles().removeExact(a);
+            final Iterator<Configuration> iter = base.getConfigurations().iterator();
+            while ( iter.hasNext() ) {
+                final Configuration cfg = iter.next();
+                final String bundleId = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT);
+                if ( a.toMvnId().equals(bundleId) ) {
+                    iter.remove();
+                }
+            }
+        }
+        // configurations
+        for(final String c : i.getConfigurationRemovals()) {
+            final int attrPos = c.indexOf('@');
+            final String val = (attrPos == -1 ? c : c.substring(0, attrPos));
+            final String attr = (attrPos == -1 ? null : c.substring(attrPos + 1));
+
+            final int sepPos = val.indexOf('~');
+            Configuration found = null;
+            if ( sepPos == -1 ) {
+                found = base.getConfigurations().getConfiguration(val);
+
+            } else {
+                final String factoryPid = val.substring(0, sepPos);
+                final String name = val.substring(sepPos + 1);
+
+                found = base.getConfigurations().getFactoryConfiguration(factoryPid, name);
+            }
+            if ( found != null ) {
+                if ( attr == null ) {
+                    base.getConfigurations().remove(found);
+                } else {
+                    found.getProperties().remove(attr);
+                }
+            }
+        }
+
+        // framework properties
+        for(final String p : i.getFrameworkPropertiesRemovals()) {
+            base.getFrameworkProperties().remove(p);
+        }
+
+        // extensions
+        for(final String name : i.getExtensionRemovals()) {
+            for(final Extension ext : base.getExtensions()) {
+                if ( ext.getName().equals(name) ) {
+                    base.getExtensions().remove(ext);
+                    break;
+                }
+            }
+        }
+        for(final Map.Entry<String, List<ArtifactId>> entry : i.getArtifactExtensionRemovals().entrySet()) {
+            for(final Extension ext : base.getExtensions()) {
+                if ( ext.getName().equals(entry.getKey()) ) {
+                    for(final ArtifactId id : entry.getValue() ) {
+                        for(final Artifact a : ext.getArtifacts()) {
+                            if ( a.getId().equals(id) ) {
+                                ext.getArtifacts().remove(a);
+                                break;
+                            }
+                        }
+                    }
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/builder/FeatureExtensionHandler.java b/src/main/java/org/apache/sling/feature/builder/FeatureExtensionHandler.java
new file mode 100644
index 0000000..2907f9b
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/builder/FeatureExtensionHandler.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.feature.builder;
+
+import org.apache.sling.feature.Feature;
+import org.osgi.annotation.versioning.ConsumerType;
+
+/**
+ * A feature extension handler can merge a feature of a particular type
+ * and also post process the final assembled feature.
+ */
+@ConsumerType
+public interface FeatureExtensionHandler {
+
+    /**
+     * Checks whether this merger can merge extensions with that name
+     * @param extensionName The extension name
+     * @return {@code true} if merger can handle this
+     */
+    boolean canMerge(String extensionName);
+
+    /**
+     * Merge the source extension into the target extension.
+     *
+     * The caller of this method already ensured that both
+     * extensions share the same name and type and that
+     * {@link #canMerge(String)} returned {@code true}.
+     *
+     * @param target The target feature
+     * @param source The source feature
+     * @param extensionName The extension name
+     * @throws IllegalStateException If the extensions can't be merged
+     */
+    void merge(Feature target, Feature source, String extensionName);
+
+    /**
+     * Post process the feature with respect to the extension.
+     * Post processing is invoked after all extensions have been merged.
+     * This method is called regardless whether {@link #canMerge(String)} returned {@code true} or not.
+     * @param feature The feature
+     * @param extensionName The extension name
+     * @throws IllegalStateException If post processing failed
+     */
+    void postProcess(Feature feature, String extensionName);
+}
diff --git a/src/main/java/org/apache/sling/feature/builder/FeatureProvider.java b/src/main/java/org/apache/sling/feature/builder/FeatureProvider.java
new file mode 100644
index 0000000..ace0005
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/builder/FeatureProvider.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.feature.builder;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+import org.osgi.annotation.versioning.ConsumerType;
+
+/**
+ * The feature provider is used to find features while assembling using either
+ * {@link ApplicationBuilder} or {@link FeatureBuilder}.
+ */
+@ConsumerType
+public interface FeatureProvider {
+
+    /**
+     * Provide the feature with the given id.
+     * @param id The feature id
+     * @return The feature or {@code null}
+     */
+    Feature provide(ArtifactId id);
+}
diff --git a/src/main/java/org/apache/sling/feature/builder/package-info.java b/src/main/java/org/apache/sling/feature/builder/package-info.java
new file mode 100644
index 0000000..4ecbe60
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/builder/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.feature.builder;
+
+
diff --git a/src/test/java/org/apache/sling/feature/builder/BuilderUtilTest.java b/src/test/java/org/apache/sling/feature/builder/BuilderUtilTest.java
new file mode 100644
index 0000000..b5d3570
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/builder/BuilderUtilTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.feature.builder;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.builder.BuilderUtil;
+import org.apache.sling.feature.builder.BuilderUtil.ArtifactMerge;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class BuilderUtilTest {
+
+    private List<Map.Entry<Integer, Artifact>> getBundles(final Bundles f) {
+        final List<Map.Entry<Integer, Artifact>> result = new ArrayList<>();
+        for(final Map.Entry<Integer, List<Artifact>> entry : f.getBundlesByStartOrder().entrySet()) {
+            for(final Artifact artifact : entry.getValue()) {
+                result.add(new Map.Entry<Integer, Artifact>() {
+
+                    @Override
+                    public Integer getKey() {
+                        return entry.getKey();
+                    }
+
+                    @Override
+                    public Artifact getValue() {
+                        return artifact;
+                    }
+
+                    @Override
+                    public Artifact setValue(Artifact value) {
+                        return null;
+                    }
+                });
+            }
+        }
+
+        return result;
+    }
+
+    private void assertContains(final List<Map.Entry<Integer, Artifact>> bundles,
+            final int level, final ArtifactId id) {
+        for(final Map.Entry<Integer, Artifact> entry : bundles) {
+            if ( entry.getKey().intValue() == level
+                 && entry.getValue().getId().equals(id) ) {
+                return;
+            }
+        }
+        fail(id.toMvnId());
+    }
+
+    @Test public void testMergeBundlesWithAlgHighest() {
+        final Bundles target = new Bundles();
+
+        target.add(createBundle("g/a/1.0", 1));
+        target.add(createBundle("g/b/2.0", 2));
+        target.add(createBundle("g/c/2.5", 3));
+
+        final Bundles source = new Bundles();
+        source.add(createBundle("g/a/1.1", 1));
+        source.add(createBundle("g/b/1.9", 2));
+        source.add(createBundle("g/c/2.5", 3));
+
+        BuilderUtil.mergeBundles(target, source, ArtifactMerge.HIGHEST);
+
+        final List<Map.Entry<Integer, Artifact>> result = getBundles(target);
+        assertEquals(3, result.size());
+        assertContains(result, 1, ArtifactId.parse("g/a/1.1"));
+        assertContains(result, 2, ArtifactId.parse("g/b/2.0"));
+        assertContains(result, 3, ArtifactId.parse("g/c/2.5"));
+    }
+
+    @Test public void testMergeBundlesWithAlgLatest() {
+        final Bundles target = new Bundles();
+
+        target.add(createBundle("g/a/1.0", 1));
+        target.add(createBundle("g/b/2.0", 2));
+        target.add(createBundle("g/c/2.5", 3));
+
+        final Bundles source = new Bundles();
+        source.add(createBundle("g/a/1.1", 1));
+        source.add(createBundle("g/b/1.9", 2));
+        source.add(createBundle("g/c/2.5", 3));
+
+        BuilderUtil.mergeBundles(target, source, ArtifactMerge.LATEST);
+
+        final List<Map.Entry<Integer, Artifact>> result = getBundles(target);
+        assertEquals(3, result.size());
+        assertContains(result, 1, ArtifactId.parse("g/a/1.1"));
+        assertContains(result, 2, ArtifactId.parse("g/b/1.9"));
+        assertContains(result, 3, ArtifactId.parse("g/c/2.5"));
+    }
+
+    @Test public void testMergeBundlesDifferentStartlevel() {
+        final Bundles target = new Bundles();
+
+        target.add(createBundle("g/a/1.0", 1));
+
+        final Bundles source = new Bundles();
+        source.add(createBundle("g/a/1.1", 2));
+
+        BuilderUtil.mergeBundles(target, source, ArtifactMerge.LATEST);
+
+        final List<Map.Entry<Integer, Artifact>> result = getBundles(target);
+        assertEquals(1, result.size());
+        assertContains(result, 2, ArtifactId.parse("g/a/1.1"));
+    }
+
+    @Test public void testMergeBundles() {
+        final Bundles target = new Bundles();
+
+        target.add(createBundle("g/a/1.0", 1));
+        target.add(createBundle("g/b/2.0", 2));
+        target.add(createBundle("g/c/2.5", 3));
+
+        final Bundles source = new Bundles();
+        source.add(createBundle("g/d/1.1", 1));
+        source.add(createBundle("g/e/1.9", 2));
+        source.add(createBundle("g/f/2.5", 3));
+
+        BuilderUtil.mergeBundles(target, source, ArtifactMerge.LATEST);
+
+        final List<Map.Entry<Integer, Artifact>> result = getBundles(target);
+        assertEquals(6, result.size());
+        assertContains(result, 1, ArtifactId.parse("g/a/1.0"));
+        assertContains(result, 2, ArtifactId.parse("g/b/2.0"));
+        assertContains(result, 3, ArtifactId.parse("g/c/2.5"));
+        assertContains(result, 1, ArtifactId.parse("g/d/1.1"));
+        assertContains(result, 2, ArtifactId.parse("g/e/1.9"));
+        assertContains(result, 3, ArtifactId.parse("g/f/2.5"));
+    }
+
+    public static Artifact createBundle(final String id, final int startOrder) {
+        final Artifact a = new Artifact(ArtifactId.parse(id));
+        a.getMetadata().put(Artifact.KEY_START_ORDER, String.valueOf(startOrder));
+
+        return a;
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/builder/FeatureBuilderTest.java b/src/test/java/org/apache/sling/feature/builder/FeatureBuilderTest.java
new file mode 100644
index 0000000..7b81eab
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/builder/FeatureBuilderTest.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.feature.builder;
+
+import org.apache.felix.utils.resource.CapabilityImpl;
+import org.apache.felix.utils.resource.RequirementImpl;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Include;
+import org.junit.Test;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class FeatureBuilderTest {
+
+    private static final Map<String, Feature> FEATURES = new HashMap<>();
+
+    static {
+        final Feature f1 = new Feature(ArtifactId.parse("g/a/1"));
+
+        f1.getFrameworkProperties().put("foo", "2");
+        f1.getFrameworkProperties().put("bar", "X");
+
+        f1.getBundles().add(BuilderUtilTest.createBundle("org.apache.sling/foo-bar/4.5.6", 3));
+        f1.getBundles().add(BuilderUtilTest.createBundle("group/testnewversion_low/2", 5));
+        f1.getBundles().add(BuilderUtilTest.createBundle("group/testnewversion_high/2", 5));
+        f1.getBundles().add(BuilderUtilTest.createBundle("group/testnewstartlevel/1", 5));
+        f1.getBundles().add(BuilderUtilTest.createBundle("group/testnewstartlevelandversion/1", 5));
+
+        final Configuration c1 = new Configuration("org.apache.sling.foo");
+        c1.getProperties().put("prop", "value");
+        f1.getConfigurations().add(c1);
+
+        FEATURES.put(f1.getId().toMvnId(), f1);
+    }
+
+    private final FeatureProvider provider = new FeatureProvider() {
+
+        @Override
+        public Feature provide(final ArtifactId id) {
+            return FEATURES.get(id.getGroupId() + ":" + id.getArtifactId() + ":" + id.getVersion());
+        }
+    };
+
+    private List<Map.Entry<Integer, Artifact>> getBundles(final Feature f) {
+        final List<Map.Entry<Integer, Artifact>> result = new ArrayList<>();
+        for(final Map.Entry<Integer, List<Artifact>> entry : f.getBundles().getBundlesByStartOrder().entrySet()) {
+            for(final Artifact artifact : entry.getValue()) {
+                result.add(new Map.Entry<Integer, Artifact>() {
+
+                    @Override
+                    public Integer getKey() {
+                        return entry.getKey();
+                    }
+
+                    @Override
+                    public Artifact getValue() {
+                        return artifact;
+                    }
+
+                    @Override
+                    public Artifact setValue(Artifact value) {
+                        return null;
+                    }
+                });
+            }
+        }
+
+        return result;
+    }
+
+    private void equals(final Feature expected, final Feature actuals) {
+        assertFalse(expected.isAssembled());
+        assertTrue(actuals.isAssembled());
+
+        assertEquals(expected.getId(), actuals.getId());
+        assertEquals(expected.getTitle(), actuals.getTitle());
+        assertEquals(expected.getDescription(), actuals.getDescription());
+        assertEquals(expected.getVendor(), actuals.getVendor());
+        assertEquals(expected.getLicense(), actuals.getLicense());
+
+        // bundles
+        final List<Map.Entry<Integer, Artifact>> expectedBundles = getBundles(expected);
+        final List<Map.Entry<Integer, Artifact>> actualsBundles = getBundles(actuals);
+        assertEquals(expectedBundles.size(), actualsBundles.size());
+        for(final Map.Entry<Integer, Artifact> entry : expectedBundles) {
+            boolean found = false;
+            for(final Map.Entry<Integer, Artifact> inner : actualsBundles) {
+                if ( inner.getValue().getId().equals(entry.getValue().getId()) ) {
+                    found = true;
+                    assertEquals("Startlevel of bundle " + entry.getValue(), entry.getKey(), inner.getKey());
+                    assertEquals("Metadata of bundle " + entry.getValue(), entry.getValue().getMetadata(), inner.getValue().getMetadata());
+                    break;
+                }
+            }
+            assertTrue("Bundle " + entry.getValue() + " in level " + entry.getKey(), found);
+        }
+
+        // configurations
+        assertEquals(expected.getConfigurations().size(), actuals.getConfigurations().size());
+        for(final Configuration cfg : expected.getConfigurations()) {
+            final Configuration found = (cfg.isFactoryConfiguration() ? actuals.getConfigurations().getFactoryConfiguration(cfg.getFactoryPid(), cfg.getName())
+                                                                      : actuals.getConfigurations().getConfiguration(cfg.getPid()));
+            assertNotNull("Configuration " + cfg, found);
+            assertEquals("Configuration " + cfg, cfg.getProperties(), found.getProperties());
+        }
+
+        // frameworkProperties
+        assertEquals(expected.getFrameworkProperties(), actuals.getFrameworkProperties());
+
+        // requirements
+        assertEquals(expected.getRequirements().size(), actuals.getRequirements().size());
+        for(final Requirement r : expected.getRequirements()) {
+            boolean found = false;
+            for(final Requirement i : actuals.getRequirements()) {
+                if ( r.equals(i) ) {
+                    found = true;
+                    break;
+                }
+            }
+            assertTrue(found);
+        }
+
+        // capabilities
+        assertEquals(expected.getCapabilities().size(), actuals.getCapabilities().size());
+        for(final Capability r : expected.getCapabilities()) {
+            boolean found = false;
+            for(final Capability i : actuals.getCapabilities()) {
+                if ( r.equals(i) ) {
+                    found = true;
+                    break;
+                }
+            }
+            assertTrue(found);
+        }
+
+        // extensions
+        assertEquals(expected.getExtensions().size(), actuals.getExtensions().size());
+        for(final Extension ext : expected.getExtensions()) {
+            final Extension inner = actuals.getExtensions().getByName(ext.getName());
+            assertNotNull(inner);
+            assertEquals(ext.getType(), inner.getType());
+            switch ( ext.getType()) {
+                case JSON : assertEquals(ext.getJSON(), inner.getJSON());
+                            break;
+                case TEXT : assertEquals(ext.getText(), inner.getText());
+                            break;
+                case ARTIFACTS : assertEquals(ext.getArtifacts().size(), inner.getArtifacts().size());
+                                 for(final Artifact art : ext.getArtifacts()) {
+                                     boolean found = false;
+                                     for(final Artifact i : inner.getArtifacts()) {
+                                         if ( art.getId().equals(i.getId()) ) {
+                                             found = true;
+                                             assertEquals(art.getMetadata(), i.getMetadata());
+                                             break;
+                                         }
+                                     }
+                                     assertTrue(found);
+                                 }
+            }
+        }
+
+        // includes should always be empty
+        assertTrue(actuals.getIncludes().isEmpty());
+    }
+
+    @Test public void testNoIncludesNoUpgrade() throws Exception {
+        final Feature base = new Feature(ArtifactId.parse("org.apache.sling/test-feature/1.1"));
+
+        final Requirement r1 = new RequirementImpl(null, "osgi.contract",
+                Collections.singletonMap("filter", "(&(osgi.contract=JavaServlet)(version=3.1))"), null);
+        base.getRequirements().add(r1);
+
+        Map<String, Object> attrs = new HashMap<>();
+        attrs.put("osgi.implementation", "osgi.http");
+        attrs.put("version:Version", "1.1");
+        final Capability c1 = new CapabilityImpl(null, "osgi.implementation",
+                Collections.singletonMap("uses", "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard"),
+                attrs);
+        base.getCapabilities().add(c1);
+        final Capability c2 = new CapabilityImpl(null, "osgi.service",
+                Collections.singletonMap("uses", "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto"),
+                Collections.singletonMap("objectClass:List<String>", "org.osgi.service.http.runtime.HttpServiceRuntime"));
+        base.getCapabilities().add(c2);
+
+        base.getFrameworkProperties().put("foo", "1");
+        base.getFrameworkProperties().put("brave", "something");
+        base.getFrameworkProperties().put("org.apache.felix.scr.directory", "launchpad/scr");
+
+        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");
+        base.getBundles().add(a1);
+        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));
+
+        final Configuration co1 = new Configuration("my.pid");
+        co1.getProperties().put("foo", 5L);
+        co1.getProperties().put("bar", "test");
+        co1.getProperties().put("number", 7);
+        base.getConfigurations().add(co1);
+
+        final Configuration co2 = new Configuration("my.factory.pid", "name");
+        co2.getProperties().put("a.value", "yeah");
+        base.getConfigurations().add(co2);
+
+        assertFalse(base.isAssembled());
+
+        final Feature assembled = FeatureBuilder.assemble(base, new BuilderContext(provider));
+
+        equals(base, assembled);
+    }
+
+    @Test public void testSingleInclude() throws Exception {
+        final Feature base = new Feature(ArtifactId.parse("org.apache.sling/test-feature/1.1"));
+        final Include i1 = new Include(ArtifactId.parse("g/a/1"));
+        base.getIncludes().add(i1);
+
+        final Requirement r1 = new RequirementImpl(null, "osgi.contract",
+                Collections.singletonMap("filter", "(&(osgi.contract=JavaServlet)(version=3.1))"), null);
+        base.getRequirements().add(r1);
+
+        Map<String, Object> attrs = new HashMap<>();
+        attrs.put("osgi.implementation", "osgi.http");
+        attrs.put("version:Version", "1.1");
+        final Capability c1 = new CapabilityImpl(null, "osgi.implementation",
+                Collections.singletonMap("uses", "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard"),
+                attrs);
+        base.getCapabilities().add(c1);
+
+        base.getFrameworkProperties().put("foo", "1");
+        base.getFrameworkProperties().put("brave", "something");
+        base.getFrameworkProperties().put("org.apache.felix.scr.directory", "launchpad/scr");
+
+        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");
+        base.getBundles().add(a1);
+        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);
+        co1.getProperties().put("bar", "test");
+        co1.getProperties().put("number", 7);
+        base.getConfigurations().add(co1);
+
+        final Configuration co2 = new Configuration("my.factory.pid", "name");
+        co2.getProperties().put("a.value", "yeah");
+        base.getConfigurations().add(co2);
+
+        assertFalse(base.isAssembled());
+
+        // create the expected result
+        final Feature result = base.copy();
+        result.getIncludes().remove(0);
+        result.getFrameworkProperties().put("bar", "X");
+        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");
+        result.getConfigurations().add(co3);
+
+        // assemble
+        final Feature assembled = FeatureBuilder.assemble(base, new BuilderContext(provider));
+
+        // and test
+        equals(result, assembled);
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
davidb@apache.org.

Mime
View raw message