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] 01/22: Move feature model to whiteboard git
Date Fri, 27 Apr 2018 09:51:21 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 9da66950b8f99f3e57b854c98b7cd4e0812998f4
Author: Carsten Ziegeler <cziegele@adobe.com>
AuthorDate: Fri Nov 3 15:06:50 2017 +0100

    Move feature model to whiteboard git
---
 pom.xml                                            |  86 +++++
 readme.md                                          | 242 ++++++++++++
 .../java/org/apache/sling/feature/Application.java | 117 ++++++
 .../java/org/apache/sling/feature/Artifact.java    |  89 +++++
 .../java/org/apache/sling/feature/ArtifactId.java  | 414 +++++++++++++++++++++
 .../java/org/apache/sling/feature/Bundles.java     | 240 ++++++++++++
 .../java/org/apache/sling/feature/Capability.java  | 104 ++++++
 .../org/apache/sling/feature/Configuration.java    | 154 ++++++++
 .../org/apache/sling/feature/Configurations.java   |  58 +++
 .../java/org/apache/sling/feature/Extension.java   | 190 ++++++++++
 .../org/apache/sling/feature/ExtensionType.java    |  27 ++
 .../java/org/apache/sling/feature/Extensions.java  |  41 ++
 .../java/org/apache/sling/feature/Feature.java     | 408 ++++++++++++++++++++
 .../java/org/apache/sling/feature/Include.java     | 115 ++++++
 .../java/org/apache/sling/feature/KeyValueMap.java | 125 +++++++
 .../java/org/apache/sling/feature/Requirement.java | 113 ++++++
 .../org/apache/sling/feature/package-info.java     |  23 ++
 .../sling/feature/process/ApplicationBuilder.java  | 161 ++++++++
 .../sling/feature/process/BuilderContext.java      |  67 ++++
 .../apache/sling/feature/process/BuilderUtil.java  | 261 +++++++++++++
 .../sling/feature/process/FeatureBuilder.java      | 291 +++++++++++++++
 .../feature/process/FeatureExtensionHandler.java   |  55 +++
 .../sling/feature/process/FeatureProvider.java     |  30 ++
 .../apache/sling/feature/process/package-info.java |  23 ++
 .../org/apache/sling/feature/ArtifactIdTest.java   | 144 +++++++
 .../java/org/apache/sling/feature/BundlesTest.java |  45 +++
 .../sling/feature/process/BuilderUtilTest.java     | 133 +++++++
 .../sling/feature/process/FeatureBuilderTest.java  | 277 ++++++++++++++
 28 files changed, 4033 insertions(+)

diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..5cb1614
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+    <!--
+        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.
+    -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>32</version>
+        <relativePath />
+    </parent>
+
+    <artifactId>org.apache.sling.feature</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+
+    <name>Apache Sling Feature</name>
+    <description>
+        A feature describes an OSGi system
+    </description>
+
+    <properties>
+        <sling.java.version>8</sling.java.version>
+    </properties>
+
+    <scm>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/tooling/support/feature</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/tooling/support/feature</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/tooling/support/feature</url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>readme.md</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.geronimo.specs</groupId>
+            <artifactId>geronimo-json_1.0_spec</artifactId>
+            <version>1.0-alpha-1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+
+      <!-- Testing -->
+        <dependency>
+        	    <groupId>junit</groupId>
+        	    <artifactId>junit</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.johnzon</groupId>
+            <artifactId>johnzon-core</artifactId>
+            <version>1.0.0</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..2fbf1f4
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,242 @@
+# Prototype for a new Provisioning Model - Configuration Model for OSGi based applications
+
+The current model to describe OSGi applications is based on Apache Sling's provisioning model (see https://sling.apache.org/documentation/development/slingstart.html)
+
+## Short description of Sling's provisioning model:
+
+* Text based file format, defining features (several in a single file)
+* A feature can have a name and version (both optional)
+* A feature consists of sections, well defined ones like the run modes and user defined sections
+* A run mode has artifacts (with start levels), configurations and settings (framework properties)
+* Variables can be used throughout a feature
+* Inheritance is supported on a feature base through artifacts
+* Configuration merging is possible
+
+## Advantages of the provisioning model
+
+* Well known by Sling developers, has been introduced some years ago. Some tooling around it
+* Very concise, especially for defining artifacts
+* Extensible, custom sections can be added, e.g used by Sling for repoinit, subsystem definitions, content package definitions
+* Easy diff
+* Special API with semantics related to the prov model (not a general purpose config API)
+
+## Disadvantages of the provisioning model
+
+* Single file can contain more than one feature
+* Custom DSL - no standard format (like JSON)
+* Inheritance and custom artifacts (content packages) are mixed with bundles, which makes processing and understanding more complicated
+* Adding additional info to artifacts looks strange
+* Two formats for configurations and now there is an official JSON based format defined through OSGi R7
+* Strange object relationship between feature and run modes
+* API (object relation) differs from text file (to make the text format easier)
+* Tooling only available as maven plugins, not separate usable
+* Run mode handling is complicating the feature files and processing of those
+* Tightly coupled with the way Sling's launchpad works, therefore no independent OSGi format
+
+# Design Criteria for a model
+
+* A feature is a separate file with a required name and version
+* A feature can include other features
+* No support for run modes in the model - run modes can be modeled through separate features
+* OSGi JSON format for configurations
+* Support for standard OSGi artifacts as well as custom artifacts (like content packages)
+* Support OSGi requirements and capabilities for dependency handling
+
+# Prototype
+
+The prototype uses JSON as a well defined and understood format. This fits nicely with the new OSGi R7 JSON format for configurations.
+
+A model file describes a feature. A feature consists of:
+* A unique id and version (see Feature Identity below)
+* A list of bundles described through maven coordinates
+  * Grouped by start level (required)
+  * Additional metadata like a hash etc. (optional)
+  * Configurations (optional)
+* A set of global configurations
+* A set of framework properties
+* A list of provided capabilities
+* A list of required capabilities
+* A set of includes (of other features) described through maven coordinates
+  * Modifications (removals) of the includes (optional)
+* Extensions (optional)
+  * A list of repoinit instructions
+  * A set of content packages described through maven coordinates
+    * Additional metadata like a hash etc. (optional)
+    * Configurations (optional)
+
+# Feature Identity
+
+A feature is uniquely identified through maven coordinates, it has a group id, an artifact id and a version. In addition it might have a classifier.
+
+TBD We need to define a common type for such a feature. It could be "osgifeature" (but is this a good type? slingfeature, slingstart are taken, osgifeature might be too general)
+
+# Maven coordinates
+
+Maven coordinates are used to describe artifacts, e.g. bundles, content packages or includes. In these cases either the short notation (as described here: https://maven.apache.org/pom.html#Maven_Coordinates) can be used or the long version as a JSON object with an id property.
+
+# Requirements and Capabilities vs Dependencies
+
+In order to avoid a concept like "Require-Bundle" a feature does not explicitely declare dependencies to other features. These are declared by the required capabilities, either explicit or implicit. The implicit requirements are calculated by inspecting the contained bundles (and potentially other artifacts like content packages ).
+
+Once a feature is processed by tooling, the tooling might create a full list of requirements and capabilities and add this information in a special section to the final feature. This information can be used by tooling to validate an instance (see below) and avoids rescanning the binary artifacts. However this "cached" information is optional and tooling must work without it (which means it needs access to the binaries in that case). TBD the name and format of this information.
+
+# Includes
+
+Includes allow an aggregation of features and a modification of the included feature: each entity listed in the included feature can be removed, e.g a configuration or a bundle. The list of includes must not contain duplicates (not comparing the version of the includes). If there are duplicates, the feature is invalid.
+
+Once a feature is processed, included references are removed and the content of the included features becomes part of the current feature. The following algorithm applies:
+
+* Includes are processed in the order they are defined in the model. The current feature (containing the includes) is used last which means the algorithm starts with the first included feature.
+* Removal instructions for an include are handled first
+* A clash of bundles or content packages is resolved by picking the latest version (not the highest!)
+* Configurations will be merged by default, later ones potentially overriding newer ones:
+  * If the same property is declared in more than one feature, the last one wins - in case of an array value, this requires redeclaring all values (if they are meant to be kept)
+  * Configurations can be bound to a bundle. When two features are merged, all cases can occur: both might be bound to the same bundle (symbolic name), both might not be bound, they might be bound to different bundles (symbolic name), or one might be bound and the other one might not. As configurations are handled as a set regardless of whether they are bound to a bundle or not, the information of the belonging bundle is handled like a property in the configuration. This means:
+    * If the last configuration belongs to a bundle, this relationship is kept
+    * If the last configuration does not belong to a bundle and has no property removal instruction, the relationship from the first bundle is used (if there is one)
+    * If the last configuration has a property removal instruction for the bundle relationship, the resulting configuration is unbound
+* Later framework properties overwrite newer ones
+* Capabilities and requirements are appended - this might result in duplicates, but that doesn't really hurt in practice.
+* Extensions are handled in an extension specific way:
+    * repoinit is just aggregated (appended)
+    * artifact extensions are handled like bundles
+
+While includes must not be used for assembling an application, they provide an important concept for manipulating existing features. For example to replace a bundle in an existing feature and deliver this modified feature.
+
+# Extensions
+
+An extension has a unique name and a type which can either be text, JSON or artifacts. Depending on the type, inheritance is performed like this:
+* For type text: simple appended
+* For type JSON: merging of the JSON structure, later arriving properties overriding existing ones
+* For type artifacts: merging of the artifacts, higher version wins
+
+# Handling of Environments
+
+A feature itself has no special support for environments (prod, test, dev). In practice it is very unlikely that a single file exists containing configurations for all environments, especially as the configuration might contain secrets, credentials, urls for production services etc which are not meant to be given out in public (or to the dev department). Instead, a separate feature for an environment can be written and maintained by the different share holders which adds the environment  [...]
+
+# Bundles and start levels
+
+For bundles there is no default start level - a default start level is not defined in the OSGi spec. And in addition, it is a little bit confusing when looking at the model when there is a list of artifacts without a start level. Which start level do these have? Its better to be explicit.
+
+In the current PoC, a bundle needs to be explicitely assigned to a start level. This seems to be only working if you know all the features in advance and how they are structured. On the other hand there needs to be a way to define the start order of bundles within a feature. Therefore we can use the start level information as an ordering information for the bundles within a feature. Bundles within the same start level are started in any order.
+
+Proposal: We use the format as it is today, but interpret the start level value different: instead of directly mapping it to a start level in the OSGi framework, it defines just the startup order of bundles within a feature. Features are then started in respect of their dependency information. Even if a feature has no requirement with respect to start ordering of their bundles, it has to define a start level (to act as a container for the bundles). It can use any positive number, suggest [...]
+
+# Configurations belonging to Bundles
+
+In most cases, configurations belong to a bundle. The most common use case is a configuration for a (DS) component. Therefore instead of having a separate configurations section, it is more intuitiv to specify configurations as part of a bundle. The benefit of this approach is, that it can easily be decided if a configuration is to be used: if exactly that bundle is used, the configurations are used; otherwise they are not.
+
+However, there might be situations where it is not clear to which bundle a configuration belongs or the configuration might be a cross cutting concern spawning across multiple bundles. Therefore it is still possible to have configurations not related to a particular bundle.
+
+In fact, configurations - whether they are declared as part of a bundle or not - are all managed in a single set for a feature. See above for how includes etc. are handled.
+
+# Example
+
+This is a feature example:
+
+    {
+      "id" : "org.apache.sling:my.app:feature:optional:1.0",
+
+      "includes" : [
+         {
+             "id" : "org.apache.sling:sling:9",
+             "removals" : {
+                 "configurations" : [
+                 ],
+                 "bundles": [
+                 ],
+                 "framework-properties" : [
+                 ]
+             }
+         }
+      ],
+      "requirements" : [
+          {
+              "namespace" : "osgi.contract",
+              "directives" : {
+                  "filter" : "(&(osgi.contract=JavaServlet)(version=3.1))"
+              }
+          }
+      ],
+      "capabilities" : [
+        {
+             "namespace" : "osgi.implementation",
+             "attributes" : {
+                   "osgi.implementation" : "osgi.http",
+                   "version:Version" : "1.1"
+             },
+             "directives" : {
+                   "uses" : "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard"
+             }
+        },
+        {
+             "namespace" : osgi.service",
+             "attributes" : {
+                  "objectClass:List<String>" : "org.osgi.service.http.runtime.HttpServiceRuntime"
+             },
+             "directives" {
+                  "uses" : "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto"
+             }
+        }
+      ],
+      "framework-properties" {
+        "foo" : 1,
+        "brave" : "something",
+        "org.apache.felix.scr.directory" : "launchpad/scr"
+      },
+      "bundles" : {
+        "1" : [
+            {
+              "id" : "org.apache.sling:security-server:2.2.0",
+              "hash" : "4632463464363646436"
+            },
+            "org.apache.sling:application-bundle:2.0.0",
+            "org.apache.sling:another-bundle:2.1.0"
+          ],
+        "2" : [
+            "org.apache.sling:foo-xyz:1.2.3"
+          ]
+      },
+      "configurations" {
+        "my.pid" {
+           "foo" : 5,
+           "bar" : "test",
+           "number:Integer" : 7
+        },
+        "my.factory.pid~name" {
+           "a.value" : "yeah"
+        }
+    }
+
+# Relation to OBR
+
+TODO
+
+# Provisioning Applications
+
+An application jar can contain a set of features (including the listed artifacts).
+
+An optional application configuration further defines the possibilites:
+
+    {
+         "features" : [
+             "org.apache.sling:org.apache.sling.launchpad:10"
+         ],
+         "options" : [
+             "org.apache.sling:org.apache.sling.scripting.jsp:1.0.0",
+             {
+                 "id" : "org.apache.sling:org.apache.sling.scripting.htl:1.0.0",
+                 "tag": "htl"
+             }
+         ],
+         "defaults" : {
+             "auto-add-options": true,
+             "tags" : ["htl"]
+         },
+         "framework" : {
+             "id" : "org.apache.felix:org.apache.felix.framework:5.6.4"
+         }
+    }
+
+Such a configuration is required for an application, at least one feature needs to be listed in either the features or the options section.
+All features listed in the features section will be added to the application, the ones listed in options are optional and depending on the settings and user input will either be added or left out. In addition all available features of an application will be used to make the application runnable (resolvable).
diff --git a/src/main/java/org/apache/sling/feature/Application.java b/src/main/java/org/apache/sling/feature/Application.java
new file mode 100644
index 0000000..7ca58e4
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Application.java
@@ -0,0 +1,117 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An application consists of
+ * <ul>
+ *   <li>Framework
+ *   <li>Bundles
+ *   <li>Configurations
+ *   <li>Framework properties
+ *   <li>Extensions
+ *   <li>Feature ids (of the features making up this application)
+ * </ul>
+ */
+public class Application {
+
+    /** Container for bundles. */
+    private final Bundles bundles = new Bundles();
+
+    /** List of configurations. */
+    private final Configurations configurations = new Configurations();
+
+    /** Map of framework properties. */
+    private final KeyValueMap frameworkProperties = new KeyValueMap();
+
+    /** List of extensions. */
+    private final Extensions extensions = new Extensions();
+
+    /** List of features. */
+    private final List<ArtifactId> features = new ArrayList<>();
+
+    /** The framework id. */
+    private ArtifactId framework;
+
+    /**
+     * Get the bundles
+     * @return The bundles object.
+     */
+    public Bundles getBundles() {
+        return this.bundles;
+    }
+
+    /**
+     * Get the configurations
+     * The list is modifiable.
+     * @return The list of configurations
+     */
+    public Configurations getConfigurations() {
+        return this.configurations;
+    }
+
+    /**
+     * Get the framework properties
+     * The map is modifiable
+     * @return The map of properties
+     */
+    public KeyValueMap getFrameworkProperties() {
+        return this.frameworkProperties;
+    }
+
+    /**
+     * Get the list of extensions
+     * The list is modifiable
+     * @return The list of extension
+     */
+    public Extensions getExtensions() {
+        return this.extensions;
+    }
+
+    /**
+     * Get the list of used features to build this application
+     * @return The list of features
+     */
+    public List<ArtifactId> getFeatureIds() {
+        return this.features;
+    }
+
+    /**
+     * Get the framework id
+     * @return The framework id or {@code null}
+     */
+    public ArtifactId getFramework() {
+        return framework;
+    }
+
+    /**
+     * Set the framework id
+     * @param framework The framework id
+     */
+    public void setFramework(final ArtifactId framework) {
+        this.framework = framework;
+    }
+
+    @Override
+    public String toString() {
+        return "Application [features=" + this.features
+                + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/Artifact.java b/src/main/java/org/apache/sling/feature/Artifact.java
new file mode 100644
index 0000000..4fada27
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Artifact.java
@@ -0,0 +1,89 @@
+/*
+ * 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;
+
+/**
+ * An artifact consists of
+ * <ul>
+ *   <li>An id
+ *   <li>metadata
+ * </ul>
+ */
+public class Artifact implements Comparable<Artifact> {
+
+    /** The artifact id. */
+    private final ArtifactId id;
+
+    /** Artifact metadata. */
+    private final KeyValueMap metadata = new KeyValueMap();
+
+    /**
+     * Construct a new artifact
+     * @param id The id of the artifact.
+     * @throws IllegalArgumentException If id is {@code null}.
+     */
+    public Artifact(final ArtifactId id) {
+        if ( id == null ) {
+            throw new IllegalArgumentException("id must not be null.");
+        }
+        this.id = id;
+    }
+
+    /**
+     * Get the id of the artifact.
+     * @return The id.
+     */
+    public ArtifactId getId() {
+        return this.id;
+    }
+
+    /**
+     * Get the metadata of the artifact.
+     * The metadata can be modified.
+     * @return The metadata.
+     */
+    public KeyValueMap getMetadata() {
+        return this.metadata;
+    }
+
+    @Override
+    public int compareTo(final Artifact o) {
+        return this.id.compareTo(o.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return this.id.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        return this.id.equals(((Artifact)obj).id);
+    }
+
+    @Override
+    public String toString() {
+        return "Artifact [id=" + id.toMvnId()
+                + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/ArtifactId.java b/src/main/java/org/apache/sling/feature/ArtifactId.java
new file mode 100644
index 0000000..a7231c8
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/ArtifactId.java
@@ -0,0 +1,414 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.osgi.framework.Version;
+
+/**
+ * An artifact identifier.
+ *
+ * An artifact is described by it's Apache Maven coordinates consisting of group id,
+ * artifact id, and version. In addition, the classifier and type can be specified.
+ * If no type is specified, {@code jar} is assumed.
+ */
+public class ArtifactId implements Comparable<ArtifactId> {
+
+    /** The required group id. */
+    private final String groupId;
+
+    /** The required artifact id. */
+    private final String artifactId;
+
+    /** The required version. */
+    private final String version;
+
+    /** The optional classifier. */
+    private final String classifier;
+
+    /** The required type. Defaults to jar. */
+    private final String type;
+
+    /**
+     * Create a new artifact object
+     * @param groupId   The group id (required)
+     * @param artifactId   The artifact id (required)
+     * @param version The version (required)
+     * @param classifier The classifier (optional)
+     * @param type The type/extension (optional, defaults to jar)
+     * @throws IllegalArgumentException If group id, artifact id or version are {@code null} or if
+     *         the version is not a valid version.
+     */
+    public ArtifactId(final String groupId,
+            final String artifactId,
+            final String version,
+            final String classifier,
+            final String type) {
+        if ( groupId == null || artifactId == null || version == null ) {
+            throw new IllegalArgumentException("Argument must not be null");
+        }
+        this.groupId = groupId;
+        this.artifactId = artifactId;
+        this.version = version;
+        this.getOSGiVersion();
+        if ( "bundle".equals(type) || type == null || type.isEmpty() ) {
+            this.type = "jar";
+        } else {
+            this.type = type;
+        }
+        if ( classifier != null && classifier.isEmpty() ) {
+            this.classifier = null;
+        } else {
+            this.classifier = classifier;
+        }
+    }
+
+    /**
+     * Create a new artifact id from a string, the string must either be a
+     * mvn url or a mvn id (= coordinates)
+     * @param s The string to parse
+     * @return The artifact id
+     * @throws IllegalArgumentException if the string can't be parsed to a valid artifact id.
+     */
+    public static ArtifactId parse(final String s) {
+        if ( s.contains(":") ) {
+            return ArtifactId.fromMvnId(s);
+        } else if ( s.contains("/") ) {
+            return ArtifactId.fromMvnUrl(s);
+        }
+        throw new IllegalArgumentException("Unable to parse mvn coordinates/url: " + s);
+    }
+
+    /**
+     * Create a new artifact id from a maven url,
+     * 'mvn:' group-id '/' artifact-id [ '/' [version] [ '/' [type] [ '/' classifier ] ] ] ]
+     * @param url The url
+     * @return A new artifact id
+     * @throws IllegalArgumentException If the url is not valid
+     */
+    public static ArtifactId fromMvnUrl(final String url) {
+        if ( url == null || (url.indexOf(':') != -1 && !url.startsWith("mvn:")) ) {
+            throw new IllegalArgumentException("Invalid mvn url: " + url);
+        }
+        // throw if repository url is included
+        if ( url.indexOf('!') != -1 ) {
+            throw new IllegalArgumentException("Repository url is not supported for Maven artifacts at the moment.");
+        }
+        final String coordinates = url.startsWith("mvn:") ? url.substring(4) : url;
+        String gId = null;
+        String aId = null;
+        String version = null;
+        String type = null;
+        String classifier = null;
+        int part = 0;
+        String value = coordinates;
+        while ( value != null ) {
+            final int pos = value.indexOf('/');
+            final String current;
+            if ( pos == -1 ) {
+                current = value;
+                value = null;
+            } else {
+                if ( pos == 0 ) {
+                    current = null;
+                } else {
+                    current = value.substring(0, pos);
+                }
+                value = value.substring(pos + 1);
+            }
+            if ( current != null ) {
+                if ( part == 0 ) {
+                    gId = current;
+                } else if ( part == 1 ) {
+                    aId = current;
+                } else if ( part == 2 ) {
+                    version = current;
+                } else if ( part == 3 ) {
+                    type = current;
+                } else if ( part == 4 ) {
+                    classifier = current;
+                }
+            }
+            part++;
+        }
+        return new ArtifactId(gId, aId, version, classifier, type);
+    }
+
+    /**
+     * Create a new artifact id from maven coordinates/id
+     * groupId:artifactId[:packaging[:classifier]]:version
+     * @param coordinates The coordinates as outlined above
+     * @return A new artifact id
+     * @throws IllegalArgumentException If the id is not valid
+     */
+    public static ArtifactId fromMvnId(final String coordinates) {
+        final String[] parts = coordinates.split(":");
+        if ( parts.length < 3 || parts.length > 5) {
+            throw new IllegalArgumentException("Invalid mvn coordinates: " + coordinates);
+        }
+        final String gId = parts[0];
+        final String aId = parts[1];
+        final String version = parts[parts.length - 1];
+        final String type = parts.length > 3 ? parts[2] : null;
+        final String classifier = parts.length > 4 ? parts[3] : null;
+
+        return new ArtifactId(gId, aId, version, classifier, type);
+    }
+
+    /**
+     * Return a mvn url
+     * @return A mvn url
+     * @see #fromMvnUrl(String)
+     */
+    public String toMvnUrl() {
+        final StringBuilder sb = new StringBuilder("mvn:");
+        sb.append(this.groupId);
+        sb.append('/');
+        sb.append(this.artifactId);
+        sb.append('/');
+        sb.append(version);
+        if ( this.classifier != null || !"jar".equals(this.type)) {
+            sb.append('/');
+            sb.append(this.type);
+            if ( this.classifier != null ) {
+                sb.append('/');
+                sb.append(this.classifier);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Return a mvn id
+     * @return The mvn id
+     * #see {@link #fromMvnId(String)}
+     */
+    public String toMvnId() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(this.groupId);
+        sb.append(':');
+        sb.append(this.artifactId);
+        sb.append(':');
+        sb.append(version);
+        if ( this.classifier != null || !"jar".equals(this.type)) {
+            sb.append(':');
+            sb.append(this.type);
+            if ( this.classifier != null ) {
+                sb.append(':');
+                sb.append(this.classifier);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Return the group id.
+     * @return The group id.
+     */
+    public String getGroupId() {
+        return groupId;
+    }
+
+    /**
+     * Return the artifact id.
+     * @return The artifact id.
+     */
+    public String getArtifactId() {
+        return artifactId;
+    }
+
+    /**
+     * Return the optional classifier.
+     * @return The classifier or {@code null}.
+     */
+    public String getClassifier() {
+        return classifier;
+    }
+
+    /**
+     * Return the type.
+     * @return The type.
+     */
+    public String getType() {
+        return type;
+    }
+
+    /**
+     * Return the version.
+     * @return The version.
+     */
+    public String getVersion() {
+        return version;
+    }
+
+    /**
+     * Test whether the artifact id is pointing to the same artifact but potentially a different version
+     * @param id The artifact id
+     * @return {@code true} if group id, artifact id, type and classifier equal
+     */
+    public boolean isSame(final ArtifactId id) {
+        if ( this.groupId.equals(id.groupId)
+             && this.artifactId.equals(id.artifactId)
+             && this.type.equals(id.type) ) {
+            if (this.classifier == null && id.classifier == null ) {
+                return true;
+            }
+            if ( this.classifier != null ) {
+                return this.classifier.equals(id.classifier);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return the OSGi version
+     * @return The OSGi version
+     */
+    public Version getOSGiVersion() {
+        String parts[] = version.split("\\.");
+
+        if ( parts.length < 4) {
+
+            int pos = parts[parts.length - 1].indexOf('-');
+            if ( pos != -1 ) {
+                final String[] newParts = new String[4];
+                newParts[0] = parts.length > 1 ? parts[0] : parts[0].substring(0, pos);
+                newParts[1] = parts.length > 2 ? parts[1] : (parts.length > 1 ? parts[1].substring(0, pos) : "0");
+                newParts[2] = parts.length > 3 ? parts[2] : (parts.length > 2 ? parts[2].substring(0, pos) : "0");
+                newParts[3] = parts[parts.length - 1].substring(pos + 1);
+                parts = newParts;
+            }
+            else {
+                // special case for strange versions like NUMBER_NUMBER
+                for (int i = 0; i < parts.length; i++) {
+                    for (pos = parts[i].indexOf('_'); pos != -1 && pos < parts[i].length() - 1; pos = parts[i].indexOf('_')) {
+                        List<String> newParts = new ArrayList<>(Arrays.asList(parts));
+                        newParts.remove(i);
+                        newParts.add(i, parts[i].substring(0, pos));
+                        newParts.add(i + 1, parts[i].substring(pos + 1));
+                        parts = newParts.toArray(new String[0]);
+                    }
+                }
+            }
+        }
+        if ( parts.length >= 4 ) {
+            final int pos = parts[2].indexOf('-');
+            if ( pos != -1 ) {
+                parts[3] = parts[2].substring(pos + 1) + "." + parts[3];
+                parts[2] = parts[2].substring(0, pos);
+            }
+        }
+        if ( parts.length > 4 ) {
+            final StringBuilder sb = new StringBuilder(parts[3]);
+            for(int i=4; i<parts.length;i++) {
+                sb.append('.');
+                sb.append(parts[i]);
+            }
+            parts[3] = sb.toString();
+        }
+        if ( parts.length > 3 && parts[3] != null ) {
+            final StringBuilder sb = new StringBuilder();
+            for ( int i = 0; i < parts[3].length(); i++ )
+            {
+                final char c = parts[3].charAt( i );
+                if ( ( c >= '0' && c <= '9' ) || ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || c == '_'
+                    || c == '-' ) {
+                    sb.append( c );
+                } else {
+                    sb.append( '_' );
+                }
+            }
+            parts[3] = sb.toString();
+        }
+        final int majorVersion = parseInt(parts[0], version);
+        final int minorVersion;
+        final int microVersion;
+        if ( parts.length > 1 ) {
+            minorVersion = parseInt(parts[1], version);
+        } else {
+            minorVersion = 0;
+        }
+        if ( parts.length > 2 ) {
+            microVersion = parseInt(parts[2], version);
+        } else {
+            microVersion = 0;
+        }
+        final String qualifier = (parts.length > 3 ? parts[3] : "");
+        return new Version(majorVersion, minorVersion, microVersion, qualifier);
+    }
+
+    /**
+     * Create a Maven like relative repository path.
+     * @return A relative repository path. The path does not start with a slash.
+     */
+    public String toMvnPath() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(groupId.replace('.', '/'));
+        sb.append('/');
+        sb.append(artifactId);
+        sb.append('/');
+        sb.append(version);
+        sb.append('/');
+        sb.append(artifactId);
+        sb.append('-');
+        sb.append(version);
+        if ( classifier != null ) {
+            sb.append('-');
+            sb.append(classifier);
+        }
+        sb.append('.');
+        sb.append(type);
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return toMvnUrl().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if(o == null) return false;
+        if(!(o instanceof ArtifactId)) return false;
+        return toMvnUrl().equals(((ArtifactId)o).toMvnUrl());
+    }
+
+    @Override
+    public int compareTo(final ArtifactId o) {
+        if(o == null) return 1;
+        return toMvnUrl().compareTo(o.toMvnUrl());
+    }
+
+    @Override
+    public String toString() {
+        return toMvnId();
+    }
+
+    /**
+     * Parse an integer.
+     */
+    private static int parseInt(final String value, final String version) {
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("Invalid version " + version);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/Bundles.java b/src/main/java/org/apache/sling/feature/Bundles.java
new file mode 100644
index 0000000..a359575
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Bundles.java
@@ -0,0 +1,240 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.NoSuchElementException;
+import java.util.TreeMap;
+
+/**
+ * Bundles groups bundle {@code Artifact}s by start level.
+ */
+public class Bundles implements Iterable<Map.Entry<Integer, Artifact>> {
+
+    /** Map of bundles grouped by start level */
+    private final Map<Integer, List<Artifact>> startLevelMap = new TreeMap<>();
+
+    /**
+     * Get the map of all bundles sorted by start level. The map is sorted
+     * and iterating over the keys is done in start level order.
+     * @return The map of bundles. The map is unmodifiable.
+     */
+    public Map<Integer, List<Artifact>> getBundlesByStartLevel() {
+        return Collections.unmodifiableMap(this.startLevelMap);
+    }
+
+    /**
+     * Add an artifact in the given start level.
+     * @param startLevel The start level
+     * @param bundle The bundle
+     */
+    public void add(final int startLevel, final Artifact bundle) {
+        List<Artifact> list = this.startLevelMap.get(startLevel);
+        if ( list == null ) {
+            list = new ArrayList<>();
+            this.startLevelMap.put(startLevel, list);
+        }
+        list.add(bundle);
+    }
+
+    /**
+     * Remove the exact artifact.
+     * All start levels are searched for such an artifact. The first one found is removed.
+     * @param id The artifact id
+     * @return {@code true} if the artifact has been removed
+     */
+    public boolean removeExact(final ArtifactId id) {
+        for(final Map.Entry<Integer, List<Artifact>> entry : this.startLevelMap.entrySet()) {
+            for(final Artifact artifact : entry.getValue()) {
+                if ( artifact.getId().equals(id)) {
+                    entry.getValue().remove(artifact);
+                    if ( entry.getValue().isEmpty() ) {
+                        this.startLevelMap.remove(entry.getKey());
+                    }
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Remove the same artifact, neglecting the version.
+     * All start levels are searched for such an artifact. The first one found is removed.
+     * @param id The artifact id
+     * @return {@code true} if the artifact has been removed
+     */
+    public boolean removeSame(final ArtifactId id) {
+        for(final Map.Entry<Integer, List<Artifact>> entry : this.startLevelMap.entrySet()) {
+            for(final Artifact artifact : entry.getValue()) {
+                if ( artifact.getId().isSame(id)) {
+                    entry.getValue().remove(artifact);
+                    if ( entry.getValue().isEmpty() ) {
+                        this.startLevelMap.remove(entry.getKey());
+                    }
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Clear the bundles map.
+     */
+    public void clear() {
+        this.startLevelMap.clear();
+    }
+
+    /**
+     * Get start level and artifact for the given id, neglecting the version
+     * @param id The artifact id
+     * @return A map entry with start level and artifact, {@code null} otherwise
+     */
+    public Map.Entry<Integer, Artifact> getSame(final ArtifactId id) {
+        for(final Map.Entry<Integer, Artifact> entry : this) {
+            if ( entry.getValue().getId().isSame(id)) {
+                return new Map.Entry<Integer, Artifact>() {
+
+                    @Override
+                    public Integer getKey() {
+                        return entry.getKey();
+                    }
+
+                    @Override
+                    public Artifact getValue() {
+                        return entry.getValue();
+                    }
+
+                    @Override
+                    public Artifact setValue(final Artifact value) {
+                        throw new IllegalStateException();
+                    }
+                };
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Checks whether the exact artifact is available
+     * @param id The artifact id.
+     * @return {@code true} if the artifact exists
+     */
+    public boolean containsExact(final ArtifactId id) {
+        for(final Map.Entry<Integer, Artifact> entry : this) {
+            if ( entry.getValue().getId().equals(id)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks whether the same artifact is available, neglecting the version
+     * @param id The artifact id.
+     * @return {@code true} if the artifact exists
+     */
+    public boolean containsSame(final ArtifactId id) {
+        for(final Map.Entry<Integer, Artifact> entry : this) {
+            if ( entry.getValue().getId().isSame(id)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Iterate over all bundles
+     */
+    @Override
+    public Iterator<Map.Entry<Integer, Artifact>> iterator() {
+        final Iterator<Map.Entry<Integer, List<Artifact>>> mainIter = this.startLevelMap.entrySet().iterator();
+        return new Iterator<Map.Entry<Integer,Artifact>>() {
+
+            private Map.Entry<Integer, Artifact> next = seek();
+
+            private Integer level;
+
+            private Iterator<Artifact> innerIter;
+
+            private Map.Entry<Integer, Artifact> seek() {
+                Map.Entry<Integer, Artifact> entry = null;
+                while ( this.innerIter != null || mainIter.hasNext() ) {
+                    if ( innerIter != null ) {
+                        if ( innerIter.hasNext() ) {
+                            final Artifact a = innerIter.next();
+                            final Integer l = this.level;
+                            entry = new Map.Entry<Integer, Artifact>() {
+
+                                @Override
+                                public Integer getKey() {
+                                    return l;
+                                }
+
+                                @Override
+                                public Artifact getValue() {
+                                    return a;
+                                }
+
+                                @Override
+                                public Artifact setValue(Artifact value) {
+                                    throw new UnsupportedOperationException();
+                                }
+                            };
+                            break;
+                        } else {
+                            innerIter = null;
+                        }
+                    } else {
+                        final Map.Entry<Integer, List<Artifact>> e = mainIter.next();
+                        this.level = e.getKey();
+                        this.innerIter = e.getValue().iterator();
+                    }
+                }
+                return entry;
+            }
+
+            @Override
+            public boolean hasNext() {
+                return this.next != null;
+            }
+
+            @Override
+            public Entry<Integer, Artifact> next() {
+                final Entry<Integer, Artifact> result = next;
+                if ( result == null ) {
+                    throw new NoSuchElementException();
+                }
+                this.next = seek();
+                return result;
+            }
+
+        };
+    }
+
+    @Override
+    public String toString() {
+        return "Bundles [" + this.startLevelMap
+                + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/Capability.java b/src/main/java/org/apache/sling/feature/Capability.java
new file mode 100644
index 0000000..53c9c3b
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Capability.java
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A capability of a feature.
+ * The capability is modeled after an OSGi capability: it
+ * belongs to a namespace and might have attributes and / or
+ * directives.
+ */
+public class Capability {
+
+    /** The namspace. */
+    private final String namespace;
+
+    /** Map of attributes. */
+    private final Map<String, Object> attributes = new ConcurrentHashMap<>();
+
+    /** Map of directives. */
+    private final Map<String, Object> directives = new ConcurrentHashMap<>();
+
+    /**
+     * Create a new Capability.
+     * @param namespace The namespace
+     * @throws IllegalArgumentException If namespace is {@code null}.
+     */
+    public Capability(final String namespace) {
+        if ( namespace == null ) {
+            throw new IllegalArgumentException("namespace must not be null.");
+        }
+        this.namespace = namespace;
+    }
+
+    /**
+     * The namespace
+     * @return The namespace
+     */
+    public String getNamespace() {
+        return namespace;
+    }
+
+    /**
+     * Get the map of attributes.
+     * The map is modifiable.
+     * @return The map of attributes.
+     */
+    public Map<String, Object> getAttributes() {
+        return attributes;
+    }
+
+    /**
+     * Get the map of directives.
+     * The map is modifiable.
+     * @return The map of directives.
+     */
+    public Map<String, Object> getDirectives() {
+        return directives;
+    }
+
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + attributes.hashCode();
+        result = prime * result + directives.hashCode();
+        result = prime * result + namespace.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final Capability other = (Capability) obj;
+        if (!attributes.equals(other.attributes)
+            || !directives.equals(other.directives)
+            || !namespace.equals(other.namespace)) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/Configuration.java b/src/main/java/org/apache/sling/feature/Configuration.java
new file mode 100644
index 0000000..721badf
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Configuration.java
@@ -0,0 +1,154 @@
+/*
+ * 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;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+
+/**
+ * A configuration has either
+ * <ul>
+ *   <li>a pid
+ *   <li>or a factory pid and a name
+ * </ul>
+ * and properties.
+ */
+public class Configuration
+    implements Comparable<Configuration> {
+
+    public static final String PROP_ARTIFACT = "service.bundleLocation";
+
+    /** The pid or name for factory pids. */
+    private final String pid;
+
+    /** The factory pid. */
+    private final String factoryPid;
+
+    /** The properties. */
+    private final Dictionary<String, Object> properties = new Hashtable<>();
+
+    /**
+     * Create a new configuration
+     * @param pid The pid
+     * @throws IllegalArgumentException If pid is {@code null}
+     */
+    public Configuration(final String pid) {
+        if ( pid == null ) {
+            throw new IllegalArgumentException("pid must not be null");
+        }
+        this.pid = pid;
+        this.factoryPid = null;
+    }
+
+    /**
+     * Create a new factor configuration
+     * @param factoryPid The factory pid
+     * @param name The name of the factory pid
+     * @throws IllegalArgumentException If factoryPid or name is {@code null}
+     */
+    public Configuration(final String factoryPid, final String name) {
+        if ( factoryPid == null || name == null ) {
+            throw new IllegalArgumentException("factoryPid and/or name must not be null");
+        }
+        this.pid = name;
+        this.factoryPid = factoryPid;
+    }
+
+    private int compareString(final String a, final String b) {
+        if ( a == null ) {
+            if ( b == null ) {
+                return 0;
+            }
+            return -1;
+        }
+        if ( b == null ) {
+            return 1;
+        }
+        return a.compareTo(b);
+    }
+
+    @Override
+    public int compareTo(final Configuration o) {
+        int result = compareString(this.factoryPid, o.factoryPid);
+        if ( result == 0 ) {
+            result = compareString(this.pid, o.pid);
+        }
+        return result;
+    }
+
+
+    /**
+     * Get the pid.
+     * If this is a factory configuration, it returns {@code null}
+     * @return The pid or {@code null}
+     */
+    public String getPid() {
+        if ( this.isFactoryConfiguration() ) {
+            return null;
+        }
+        return this.pid;
+    }
+
+    /**
+     * Return the factory pid
+     * @return The factory pid or {@code null}.
+     */
+    public String getFactoryPid() {
+        return this.factoryPid;
+    }
+
+    /**
+     * Return the name for a factory configuration.
+     * @return The name or {@code null}.
+     */
+    public String getName() {
+        if ( this.isFactoryConfiguration() ) {
+            return this.pid;
+        }
+        return null;
+    }
+
+    /**
+     * Check whether this is a factory configuration
+     * @return {@code true} if it is a factory configuration
+     */
+    public boolean isFactoryConfiguration() {
+        return this.factoryPid != null;
+    }
+
+    /**
+     * Get all properties of the configuration.
+     * @return The properties
+     */
+    public Dictionary<String, Object> getProperties() {
+        return this.properties;
+    }
+
+    @Override
+    public String toString() {
+        if ( this.isFactoryConfiguration() ) {
+            return "Factory Configuration [factoryPid=" + factoryPid
+                    + ", name=" + pid
+                    + ", properties=" + properties
+                    + "]";
+        }
+        return "Configuration [pid=" + pid
+                + ", properties=" + properties
+                + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/Configurations.java b/src/main/java/org/apache/sling/feature/Configurations.java
new file mode 100644
index 0000000..0a78964
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Configurations.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+
+/**
+ * A container for configurations.
+ */
+public class Configurations extends ArrayList<Configuration> {
+
+    private static final long serialVersionUID = -7243822886707856704L;
+
+    /**
+     * Get the configuration
+     * @param pid The pid of the configuration
+     * @return The configuration or {@code null}
+     */
+    public Configuration getConfiguration(final String pid) {
+        for(final Configuration cfg : this) {
+            if ( !cfg.isFactoryConfiguration() && pid.equals(cfg.getPid())) {
+                return cfg;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get the factory configuration
+     * @param factoryPid The factoryPid of the configuration
+     * @param name The name of the configuration
+     * @return The factory configuration or {@code null}
+     */
+    public Configuration getFactoryConfiguration(final String factoryPid, final String name) {
+        for(final Configuration cfg : this) {
+            if ( cfg.isFactoryConfiguration()
+                    && factoryPid.equals(cfg.getFactoryPid())
+                    && name.equals(cfg.getName())) {
+                return cfg;
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/Extension.java b/src/main/java/org/apache/sling/feature/Extension.java
new file mode 100644
index 0000000..a3e8082
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Extension.java
@@ -0,0 +1,190 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An Extension can either be of type
+ * <ul>
+ *   <li>Artifacts : it contains a list of artifacts
+ *   <li>Text : it contains text
+ *   <li>JSON : it contains a blob of JSON
+ * </ul>
+ *
+ * @see ExtensionType
+ */
+public class Extension {
+
+    /** Common extension name to specify the repoinit part for Apache Sling. */
+    public static final String NAME_REPOINIT = "repoinit";
+
+    /** Common extension name to specify the content packages for Apache Sling. */
+    public static final String NAME_CONTENT_PACKAGES = "content-packages";
+
+    /** The extension type */
+    private final ExtensionType type;
+
+    /** The extension name. */
+    private final String name;
+
+    /** The list of artifacts (if type artifacts) */
+    private final List<Artifact> artifacts;
+
+    /** The text or json (if corresponding type) */
+    private String text;
+
+    /** Whether the artifact is required. */
+    private final boolean required;
+
+    /**
+     * Create a new extension
+     * @param t The type of the extension
+     * @param name The name of the extension
+     * @param required Whether the extension is required or optional
+     * @throws IllegalArgumentException If name or t are {@code null}
+     */
+    public Extension(final ExtensionType t,
+            final String name,
+            final boolean required) {
+        if ( t == null || name == null ) {
+            throw new IllegalArgumentException("Argument must not be null");
+        }
+        this.type = t;
+        this.name = name;
+        this.required = required;
+        if ( t == ExtensionType.ARTIFACTS ) {
+            this.artifacts = new ArrayList<>();
+        } else {
+            this.artifacts = null;
+        }
+    }
+
+    /**
+     * Get the extension type
+     * @return The type
+     */
+    public ExtensionType getType() {
+        return this.type;
+    }
+
+    /**
+     * Get the extension name
+     * @return The name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Return whether the extension is required or optional
+     * @return Return {@code true} if the extension is required.
+     */
+    public boolean isRequired() {
+        return this.required;
+    }
+
+    /**
+     * Return whether the extension is required or optional
+     * @return Return {@code true} if the extension is optional.
+     */
+    public boolean isOptional() {
+        return !this.isRequired();
+    }
+
+    /**
+     * Get the text of the extension
+     * @return The text
+     * @throws IllegalStateException if the type is not {@code ExtensionType#TEXT}
+     */
+    public String getText() {
+        if ( type != ExtensionType.TEXT ) {
+            throw new IllegalStateException();
+        }
+        return text;
+    }
+
+    /**
+     * Set the text of the extension
+     * @param text The text
+     * @throws IllegalStateException if the type is not {@code ExtensionType#TEXT}
+     */
+    public void setText(final String text) {
+        if ( type != ExtensionType.TEXT ) {
+            throw new IllegalStateException();
+        }
+        this.text = text;
+    }
+
+    /**
+     * Get the JSON of the extension
+     * @return The JSON
+     * @throws IllegalStateException if the type is not {@code ExtensionType#JSON}
+     */
+    public String getJSON() {
+        if ( type != ExtensionType.JSON ) {
+            throw new IllegalStateException();
+        }
+        return text;
+    }
+
+    /**
+     * Set the JSON of the extension
+     * @param text The JSON
+     * @throws IllegalStateException if the type is not {@code ExtensionType#JSON}
+     */
+    public void setJSON(String text) {
+        if ( type != ExtensionType.JSON ) {
+            throw new IllegalStateException();
+        }
+        this.text = text;
+    }
+
+    /**
+     * Get the artifacts of the extension
+     * @return The artifacts
+     * @throws IllegalStateException if the type is not {@code ExtensionType#ARTIFACTS}
+     */
+    public List<Artifact> getArtifacts() {
+        if ( type != ExtensionType.ARTIFACTS ) {
+            throw new IllegalStateException();
+        }
+        return artifacts;
+    }
+
+    @Override
+    public int hashCode() {
+        return name.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        return name.equals(((Extension)obj).name);
+    }
+
+    @Override
+    public String toString() {
+        return "Extension [type=" + type + ", name=" + name + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/ExtensionType.java b/src/main/java/org/apache/sling/feature/ExtensionType.java
new file mode 100644
index 0000000..2b8b221
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/ExtensionType.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+/**
+ * Enumeration for {@link Extension} types.
+ */
+public enum ExtensionType {
+
+    ARTIFACTS,
+    TEXT,
+    JSON
+}
diff --git a/src/main/java/org/apache/sling/feature/Extensions.java b/src/main/java/org/apache/sling/feature/Extensions.java
new file mode 100644
index 0000000..703eac2
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Extensions.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+
+/**
+ * A container for extensions
+ */
+public class Extensions extends ArrayList<Extension> {
+
+    private static final long serialVersionUID = -3850006820840607498L;
+
+    /**
+     * Get an extension by name
+     * @param name The name
+     * @return The {@link Extension} or {@code null}
+     */
+    public Extension getByName(final String name) {
+        for(final Extension ext : this) {
+            if ( ext.getName().equals(name) ) {
+                return ext;
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/Feature.java b/src/main/java/org/apache/sling/feature/Feature.java
new file mode 100644
index 0000000..3665689
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Feature.java
@@ -0,0 +1,408 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A feature consists of
+ * <ul>
+ *   <li>A unique id {@link ArtifactId}
+ *   <li>Bundles
+ *   <li>Configurations
+ *   <li>Framework properties
+ *   <li>Requirements and capabilities
+ *   <li>Includes
+ *   <li>Extensions
+ * </ul>
+ */
+public class Feature implements Comparable<Feature> {
+
+    private final ArtifactId id;
+
+    private final Bundles bundles = new Bundles();
+
+    private final Configurations configurations = new Configurations();
+
+    private final KeyValueMap frameworkProperties = new KeyValueMap();
+
+    private final List<Requirement> requirements = new ArrayList<>();
+
+    private final List<Capability> capabilities = new ArrayList<>();
+
+    private final List<Include> includes = new ArrayList<>();
+
+    private final Extensions extensions = new Extensions();
+
+    /** The optional location. */
+    private volatile String location;
+
+    /** The optional title. */
+    private volatile String title;
+
+    /** The optional description. */
+    private volatile String description;
+
+    /** The optional vendor. */
+    private volatile String vendor;
+
+    /** The optional license. */
+    private volatile String license;
+
+    /** Is this an upgrade of another feature? */
+    private volatile ArtifactId upgradeOf;
+
+    /** Flag indicating whether this is an assembled feature */
+    private volatile boolean assembled = false;
+
+    /** Contained upgrades (this is usually only set for assembled features*/
+    private final List<ArtifactId> upgrades = new ArrayList<>();
+
+    /**
+     * Construct a new feature.
+     * @param id The id of the feature.
+     * @throws IllegalArgumentException If id is {@code null}.
+     */
+    public Feature(final ArtifactId id) {
+        if ( id == null ) {
+            throw new IllegalArgumentException("id must not be null.");
+        }
+        this.id = id;
+    }
+
+    /**
+     * Get the id of the artifact.
+     * @return The id.
+     */
+    public ArtifactId getId() {
+        return this.id;
+    }
+
+    /**
+     * Get the location.
+     * The location might be the location of the feature file or any other
+     * means identifying where the object is defined.
+     * @return The location or {@code null}.
+     */
+    public String getLocation() {
+        return this.location;
+    }
+
+    /**
+     * Set the location.
+     * @param value The new location.
+     */
+    public void setLocation(final String value) {
+        this.location = value;
+    }
+
+    /**
+     * Get the bundles.
+     * @return The bundles object.
+     */
+    public Bundles getBundles() {
+        return this.bundles;
+    }
+
+    /**
+     * Get the configurations.
+     * The returned object is modifiable.
+     * @return The configurations
+     */
+    public Configurations getConfigurations() {
+        return this.configurations;
+    }
+
+    /**
+     * Get the framework properties
+     * The returned object is modifiable.
+     * @return The framework properties
+     */
+    public KeyValueMap getFrameworkProperties() {
+        return this.frameworkProperties;
+    }
+
+    /**
+     * Get the list of requirements.
+     * The returned object is modifiable.
+     * @return The list of requirements
+     */
+    public List<Requirement> getRequirements() {
+        return requirements;
+    }
+
+    /**
+     * Get the list of capabilities.
+     * The returned object is modifiable.
+     * @return The list of capabilities
+     */
+    public List<Capability> getCapabilities() {
+        return capabilities;
+    }
+
+    /**
+     * Get the list of includes.
+     * The returned object is modifiable.
+     * @return The list of includes
+     */
+    public List<Include> getIncludes() {
+        return includes;
+    }
+
+    /**
+     * Get the list of extensions.
+     * The returned object is modifiable.
+     * @return The list of extensions
+     */
+    public Extensions getExtensions() {
+        return this.extensions;
+    }
+
+    /**
+     * Get the title
+     * @return The title or {@code null}
+     */
+    public String getTitle() {
+        return title;
+    }
+
+    /**
+     * Set the title
+     * @param title The title
+     */
+    public void setTitle(final String title) {
+        this.title = title;
+    }
+
+    /**
+     * Get the description
+     * @return The description or {@code null}
+     */
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * Set the description
+     * @param description The description
+     */
+    public void setDescription(final String description) {
+        this.description = description;
+    }
+
+    /**
+     * Get the vendor
+     * @return The vendor or {@code null}
+     */
+    public String getVendor() {
+        return vendor;
+    }
+
+    /**
+     * Set the vendor
+     * @param vendor The vendor
+     */
+    public void setVendor(final String vendor) {
+        this.vendor = vendor;
+    }
+
+    /**
+     * Get the license
+     * @return The license or {@code null}
+     */
+    public String getLicense() {
+        return license;
+    }
+
+    /**
+     * Set the vendor
+     * @param license The license
+     */
+    public void setLicense(final String license) {
+        this.license = license;
+    }
+
+    /**
+     * Set the upgrade of information
+     * @param id The artifact id
+     */
+    public void setUpgradeOf(final ArtifactId id) {
+        this.upgradeOf = id;
+    }
+
+    /**
+     * Get the artifact id of the upgrade of information
+     * @return The artifact id or {@code null}
+     */
+    public ArtifactId getUpgradeOf() {
+        return this.upgradeOf;
+    }
+
+    /**
+     * Get the list of upgrades applied to this feature
+     * The returned object is modifiable.
+     * @return The list of upgrades
+     */
+    public List<ArtifactId> getUpgrades() {
+        return this.upgrades;
+    }
+
+    /**
+     * Check whether the feature is already assembled
+     * @return {@code true} if it is assembled, {@code false} if it needs to be assembled
+     */
+    public boolean isAssembled() {
+        return assembled;
+    }
+
+    /**
+     * Set the assembled flag
+     * @param flag The flag
+     */
+    public void setAssembled(final boolean flag) {
+        this.assembled = flag;
+    }
+
+    /**
+     * Create a copy of the feature
+     * @return A copy of the feature
+     */
+    public Feature copy() {
+        return copy(this.getId());
+    }
+
+    /**
+     * Create a copy of the feature with a different id
+     * @param id The new id
+     * @return The copy of the feature with the new id
+     */
+    public Feature copy(final ArtifactId id) {
+        final Feature result = new Feature(id);
+
+        // metadata
+        result.setLocation(this.getLocation());
+        result.setTitle(this.getTitle());
+        result.setDescription(this.getDescription());
+        result.setVendor(this.getVendor());
+        result.setLicense(this.getLicense());
+        result.setAssembled(this.isAssembled());
+
+        // bundles
+        for(final Map.Entry<Integer, Artifact> entry : this.getBundles()) {
+            final Artifact c = new Artifact(entry.getValue().getId());
+            c.getMetadata().putAll(entry.getValue().getMetadata());
+
+            result.getBundles().add(entry.getKey(), c);
+        }
+
+        // configurations
+        for(final Configuration cfg : this.getConfigurations()) {
+            final Configuration c = cfg.isFactoryConfiguration() ? new Configuration(cfg.getFactoryPid(), cfg.getName()) : new Configuration(cfg.getPid());
+            final Enumeration<String> keyEnum = cfg.getProperties().keys();
+            while ( keyEnum.hasMoreElements() ) {
+                final String key = keyEnum.nextElement();
+                c.getProperties().put(key, cfg.getProperties().get(key));
+            }
+            result.getConfigurations().add(c);
+        }
+
+        // framework properties
+        result.getFrameworkProperties().putAll(this.getFrameworkProperties());
+
+        // requirements
+        for(final Requirement r : this.getRequirements()) {
+            final Requirement c = new Requirement(r.getNamespace());
+            c.getAttributes().putAll(r.getAttributes());
+            c.getDirectives().putAll(r.getDirectives());
+            result.getRequirements().add(c);
+        }
+
+        // capabilities
+        for(final Capability r : this.getCapabilities()) {
+            final Capability c = new Capability(r.getNamespace());
+            c.getAttributes().putAll(r.getAttributes());
+            c.getDirectives().putAll(r.getDirectives());
+            result.getCapabilities().add(c);
+        }
+
+        // includes
+        for(final Include i : this.getIncludes()) {
+            final Include c = new Include(i.getId());
+
+            c.getBundleRemovals().addAll(i.getBundleRemovals());
+            c.getConfigurationRemovals().addAll(i.getConfigurationRemovals());
+            c.getExtensionRemovals().addAll(i.getExtensionRemovals());
+            c.getFrameworkPropertiesRemovals().addAll(i.getFrameworkPropertiesRemovals());
+            c.getArtifactExtensionRemovals().putAll(c.getArtifactExtensionRemovals());
+
+            result.getIncludes().add(c);
+        }
+
+        // extensions
+        for(final Extension e : this.getExtensions()) {
+            final Extension c = new Extension(e.getType(), e.getName(), e.isRequired());
+            switch ( c.getType() ) {
+                case ARTIFACTS : for(final Artifact a : e.getArtifacts()) {
+                                     final Artifact x = new Artifact(a.getId());
+                                     x.getMetadata().putAll(a.getMetadata());
+                                     c.getArtifacts().add(x);
+                                 }
+                                 break;
+                case JSON : c.setJSON(e.getJSON());
+                            break;
+                case TEXT : c.setText(e.getText());
+                            break;
+            }
+            result.getExtensions().add(c);
+        }
+
+        return result;
+    }
+
+    @Override
+    public int compareTo(final Feature o) {
+        return this.id.compareTo(o.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return this.id.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        return this.id.equals(((Feature)obj).id);
+    }
+
+    @Override
+    public String toString() {
+        return (this.isAssembled() ? "Assembled Feature" : "Feature") +
+                " [id=" + this.getId().toMvnId()
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+                + "]";
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/feature/Include.java b/src/main/java/org/apache/sling/feature/Include.java
new file mode 100644
index 0000000..eb28293
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Include.java
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A include is an inclusion of a feature with optional removals of
+ * <ul>
+ *   <li>Configurations / configuration properties
+ *   <li>Bundles
+ *   <li>Framework properties
+ *   <li>Extensions or artifacts from extensions
+ * </ul>
+ *
+ *  TODO - requirement, capabilities
+ */
+public class Include implements Comparable<Include> {
+
+    private final ArtifactId id;
+
+    private final List<String> configurationRemovals = new ArrayList<>();
+
+    private final List<ArtifactId> bundleRemovals = new ArrayList<>();
+
+    private final List<String> frameworkPropertiesRemovals = new ArrayList<>();
+
+    private final List<String> extensionRemovals = new ArrayList<>();
+
+    private final Map<String, List<ArtifactId>> artifactExtensionRemovals = new HashMap<>();
+
+    /**
+     * Construct a new Include.
+     * @param id The id of the feature.
+     * @throws IllegalArgumentException If id is {@code null}.
+     */
+    public Include(final ArtifactId id) {
+        if ( id == null ) {
+            throw new IllegalArgumentException("id must not be null.");
+        }
+        this.id = id;
+    }
+
+    /**
+     * Get the id of the artifact.
+     * @return The id.
+     */
+    public ArtifactId getId() {
+        return this.id;
+    }
+
+    public List<String> getConfigurationRemovals() {
+        return configurationRemovals;
+    }
+
+    public List<ArtifactId> getBundleRemovals() {
+        return bundleRemovals;
+    }
+
+    public List<String> getFrameworkPropertiesRemovals() {
+        return frameworkPropertiesRemovals;
+    }
+
+    public List<String> getExtensionRemovals() {
+        return extensionRemovals;
+    }
+
+    public Map<String, List<ArtifactId>> getArtifactExtensionRemovals() {
+        return artifactExtensionRemovals;
+    }
+
+    @Override
+    public int compareTo(final Include o) {
+        return this.id.compareTo(o.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return this.id.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        return this.id.equals(((Include)obj).id);
+    }
+
+    @Override
+    public String toString() {
+        return "Include [id=" + id.toMvnId()
+                + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/KeyValueMap.java b/src/main/java/org/apache/sling/feature/KeyValueMap.java
new file mode 100644
index 0000000..ce8c1c8
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/KeyValueMap.java
@@ -0,0 +1,125 @@
+/*
+ * 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;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * Helper class to hold key value pairs.
+ */
+public class KeyValueMap
+    implements Iterable<Map.Entry<String, String>> {
+
+    /** The map holding the actual key value pairs. */
+    private final Map<String, String> properties = new TreeMap<>();
+
+    /**
+     * Get an item from the map.
+     * @param key The key of the item.
+     * @return The item or {@code null}.
+     */
+    public String get(final String key) {
+        return this.properties.get(key);
+    }
+
+    /**
+     * Put an item in the map
+     * @param key The key of the item.
+     * @param value The value
+     */
+    public void put(final String key, final String value) {
+        this.properties.put(key, value);
+    }
+
+    /**
+     * Remove an item from the map
+     * @param key The key of the item.
+     * @return The previously stored value for the key or {@code null}.
+     */
+    public String remove(final String key) {
+        return this.properties.remove(key);
+    }
+
+    /**
+     * Put all items from the other map in this map
+     * @param map The other map
+     */
+    public void putAll(final KeyValueMap map) {
+        this.properties.putAll(map.properties);
+    }
+
+    @Override
+    public Iterator<Entry<String, String>> iterator() {
+        return this.properties.entrySet().iterator();
+    }
+
+    /**
+     * Check whether this map is empty.
+     * @return {@code true} if the map is empty.
+     */
+    public boolean isEmpty() {
+        return this.properties.isEmpty();
+    }
+
+    @Override
+    public String toString() {
+        return properties.toString();
+    }
+
+    /**
+     * Get the size of the map.
+     * @return The size of the map.
+     */
+    public int size() {
+        return this.properties.size();
+    }
+
+    /**
+     * Clear the map
+     */
+    public void clear() {
+        this.properties.clear();
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((properties == null) ? 0 : properties.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        KeyValueMap other = (KeyValueMap) obj;
+        if (properties == null) {
+            if (other.properties != null)
+                return false;
+        } else if (!properties.equals(other.properties))
+            return false;
+        return true;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/Requirement.java b/src/main/java/org/apache/sling/feature/Requirement.java
new file mode 100644
index 0000000..3409e51
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/Requirement.java
@@ -0,0 +1,113 @@
+/*
+ * 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;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A requirement for a feature.
+ * The requirement is modeled after an OSGi requirement: it
+ * belongs to a namespace and might have attributes and / or
+ * directives.
+ */
+public class Requirement {
+
+    /*public static final String RESOLUTION_OPTIONAL = "optional";
+
+    public static final String	RESOLUTION_DIRECTIVE = "resolution";*/
+
+    /** The namspace. */
+    private final String namespace;
+
+    /** Map of attributes. */
+    private final Map<String, Object> attributes = new ConcurrentHashMap<>();
+
+    /** Map of directives. */
+    private final Map<String, Object> directives = new ConcurrentHashMap<>();
+
+    /**
+     * Create a new Requirement.
+     * @param namespace The namespace
+     * @throws IllegalArgumentException If namespace is {@code null}.
+     */
+    public Requirement(final String namespace) {
+        if ( namespace == null ) {
+            throw new IllegalArgumentException("namespace must not be null.");
+        }
+        this.namespace = namespace;
+    }
+
+    /**
+     * The namespace
+     * @return The namespace
+     */
+    public String getNamespace() {
+        return namespace;
+    }
+
+    /**
+     * Get the map of attributes.
+     * The map is modifiable.
+     * @return The map of attributes.
+     */
+    public Map<String,Object> getAttributes() {
+        return attributes;
+    }
+
+    /**
+     * Get the map of directives.
+     * The map is modifiable.
+     * @return The map of directives.
+     */
+    public Map<String,Object> getDirectives() {
+        return directives;
+    }
+
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + attributes.hashCode();
+        result = prime * result + directives.hashCode();
+        result = prime * result + namespace.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final Requirement other = (Requirement) obj;
+        if (!attributes.equals(other.attributes)
+            || !directives.equals(other.directives)
+            || !namespace.equals(other.namespace)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "[Requirement namespace='" + namespace + "' attributes=" + attributes + " directives=" + directives + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/package-info.java b/src/main/java/org/apache/sling/feature/package-info.java
new file mode 100644
index 0000000..0ec26d7
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/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;
+
+
diff --git a/src/main/java/org/apache/sling/feature/process/ApplicationBuilder.java b/src/main/java/org/apache/sling/feature/process/ApplicationBuilder.java
new file mode 100644
index 0000000..cc0f235
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/process/ApplicationBuilder.java
@@ -0,0 +1,161 @@
+/*
+ * 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.process;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.feature.Application;
+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.
+     *
+     * Upgrade features are only applied if the provided feature list
+     * contains the feature to be upgraded. Otherwise the upgrade feature
+     * is ignored.
+     *
+     * @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.
+     *
+     * Upgrade features are only applied if the provided feature list
+     * contains the feature to be upgraded. Otherwise the upgrade feature
+     * is 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();
+        }
+
+        // detect upgrades and created sorted feature list
+        final Map<Feature, List<Feature>> upgrades = new HashMap<>();
+        final List<Feature> sortedFeatureList = new ArrayList<>();
+        for(final Feature f : features) {
+            if ( f.getUpgradeOf() != null ) {
+                for(final Feature i : features) {
+                    if ( i.getId().equals(f.getUpgradeOf()) ) {
+                        List<Feature> u = upgrades.get(i);
+                        if ( u == null ) {
+                            u = new ArrayList<>();
+                            upgrades.put(i, u);
+                        }
+                        u.add(f);
+                        app.getFeatureIds().add(f.getId());
+                        break;
+                    }
+                }
+            } else {
+                app.getFeatureIds().add(f.getId());
+                sortedFeatureList.add(f);
+            }
+        }
+
+        // process upgrades first
+        for(final Map.Entry<Feature, List<Feature>> entry : upgrades.entrySet()) {
+            final Feature assembled = FeatureBuilder.assemble(entry.getKey(),
+                    entry.getValue(),
+                    context);
+            // update feature to assembled feature
+            sortedFeatureList.remove(entry.getKey());
+            sortedFeatureList.add(assembled);
+        }
+
+        // sort
+        Collections.sort(sortedFeatureList);
+
+        // assemble
+        for(final Feature f : sortedFeatureList) {
+            final Feature assembled = FeatureBuilder.assemble(f, context.clone(new FeatureProvider() {
+
+                @Override
+                public Feature provide(final ArtifactId id) {
+                    for(final Feature f : upgrades.keySet()) {
+                        if ( f.getId().equals(id) ) {
+                            return f;
+                        }
+                    }
+                    for(final Feature f : features) {
+                        if ( f.getId().equals(id) ) {
+                            return f;
+                        }
+                    }
+                    return context.getFeatureProvider().provide(id);
+                }
+            }));
+
+            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/process/BuilderContext.java b/src/main/java/org/apache/sling/feature/process/BuilderContext.java
new file mode 100644
index 0000000..024076d
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/process/BuilderContext.java
@@ -0,0 +1,67 @@
+/*
+ * 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.process;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Builder context holds services used by {@link ApplicationBuilder}
+ * and {@link FeatureBuilder}.
+ */
+public class BuilderContext {
+
+    private final FeatureProvider provider;
+
+    private final List<FeatureExtensionHandler> featureExtensionHandlers = new ArrayList<>();
+
+    /**
+     * Assemble the full feature by processing all includes.
+     *
+     * @param feature The feature to start
+     * @param provider A provider providing the included features
+     * @param extensionMergers Optional feature mergers
+     * @return The assembled feature.
+     * @throws IllegalArgumentException If feature or provider is {@code null}
+     * @throws IllegalStateException If an included feature can't be provided or merged.
+     */
+    public BuilderContext(final FeatureProvider provider) {
+        if ( provider == null ) {
+            throw new IllegalArgumentException("Provider must not be null");
+        }
+        this.provider = provider;
+    }
+
+    FeatureProvider getFeatureProvider() {
+        return this.provider;
+    }
+
+    List<FeatureExtensionHandler> getFeatureExtensionHandlers() {
+        return this.featureExtensionHandlers;
+    }
+
+    public BuilderContext add(final FeatureExtensionHandler handler) {
+        featureExtensionHandlers.add(handler);
+        return this;
+    }
+
+    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/process/BuilderUtil.java b/src/main/java/org/apache/sling/feature/process/BuilderUtil.java
new file mode 100644
index 0000000..19c606c
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/process/BuilderUtil.java
@@ -0,0 +1,261 @@
+/*
+ * 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.process;
+
+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.Capability;
+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.apache.sling.feature.Requirement;
+
+/**
+ * Utility methods for the builders
+ */
+class BuilderUtil {
+
+    public 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.getBundlesByStartLevel().entrySet()) {
+            for(final Artifact a : entry.getValue()) {
+                // version handling - use provided algorithm
+                boolean replace = true;
+                if ( artifactMergeAlg == ArtifactMerge.HIGHEST ) {
+                    final Map.Entry<Integer, Artifact> existing = target.getSame(a.getId());
+                    if ( existing != null && existing.getValue().getId().getOSGiVersion().compareTo(a.getId().getOSGiVersion()) > 0 ) {
+                        replace = false;
+                    }
+                }
+                if ( replace ) {
+                    target.removeSame(a.getId());
+                    target.add(entry.getKey(), 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);
+            }
+        }
+    }
+
+    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/process/FeatureBuilder.java b/src/main/java/org/apache/sling/feature/process/FeatureBuilder.java
new file mode 100644
index 0000000..7208f00
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/process/FeatureBuilder.java
@@ -0,0 +1,291 @@
+/*
+ * 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.process;
+
+import java.util.ArrayList;
+import java.util.Collections;
+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);
+    }
+
+    /**
+     * Assemble the final feature and apply upgrades
+     *
+     * If the list of upgrades contains upgrade features not intended for the
+     * provided feature, this is not considered an error situation. But the
+     * provided upgrade is ignored.
+     *
+     * @param feature The feature to start
+     * @param upgrades The list of upgrades. If this is {@code null} or empty, this method
+     *     behaves like {@link #assemble(Feature, FeatureProvider)}.
+     * @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
+     */
+    public static Feature assemble(final Feature feature,
+            final List<Feature> upgrades,
+            final BuilderContext context) {
+        if ( feature == null || context == null ) {
+            throw new IllegalArgumentException("Feature and/or context must not be null");
+        }
+
+        // check upgrades
+        List<Feature> useUpdates = null;
+        if ( upgrades != null && !upgrades.isEmpty() ) {
+            useUpdates = new ArrayList<>();
+            for(final Feature uf : upgrades) {
+                if ( !feature.getId().equals(uf.getUpgradeOf()) ) {
+                    continue;
+                }
+                boolean found = false;
+                for(final Feature i : useUpdates) {
+                    if ( i.getId().isSame(uf.getId()) ) {
+                        if ( uf.getId().getOSGiVersion().compareTo(i.getId().getOSGiVersion()) > 0 ) {
+                            useUpdates.remove(i);
+                        } else {
+                            found = true;
+                        }
+                        break;
+                    }
+                }
+                if ( !found ) {
+                    // we add a copy as we manipulate the upgrade below
+                    useUpdates.add(uf.copy());
+                }
+            }
+            Collections.sort(useUpdates);
+            if ( useUpdates.isEmpty() ) {
+                useUpdates = null;
+            }
+        }
+
+        // assemble feature without upgrades
+        final Feature assembledFeature = internalAssemble(new ArrayList<>(), feature, context);
+
+        // handle upgrades
+        if ( useUpdates != null ) {
+            for(final Feature uf : useUpdates) {
+                Include found = null;
+                for(final Include inc : uf.getIncludes() ) {
+                    if ( inc.getId().equals(assembledFeature.getId()) ) {
+                        found = inc;
+                        break;
+                    }
+                }
+                if ( found != null ) {
+                    uf.getIncludes().remove(found);
+
+                    // process include instructions
+                    include(assembledFeature, found);
+                }
+
+                // now assemble upgrade, but without considering the base
+                uf.setUpgradeOf(null);
+                assembledFeature.getUpgrades().add(uf.getId());
+                final Feature auf = assemble(uf, context);
+
+                // merge
+                merge(assembledFeature, auf, context);
+            }
+        }
+
+        return assembledFeature;
+    }
+
+    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;
+
+        if ( feature.getUpgradeOf() != null ) {
+            Include found = null;
+            for(final Include inc : feature.getIncludes()) {
+                if ( inc.getId().equals(feature.getUpgradeOf()) ) {
+                    found = inc;
+                    break;
+                }
+            }
+
+            result = feature.copy(feature.getUpgradeOf());
+
+            // add base as the first include
+            if ( found == null ) {
+                result.getIncludes().add(0, new Include(feature.getUpgradeOf()));
+            } else {
+                result.getIncludes().remove(found);
+                result.getIncludes().add(0, found);
+            }
+            result.getUpgrades().add(feature.getId());
+        } else {
+            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/process/FeatureExtensionHandler.java b/src/main/java/org/apache/sling/feature/process/FeatureExtensionHandler.java
new file mode 100644
index 0000000..c2a9460
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/process/FeatureExtensionHandler.java
@@ -0,0 +1,55 @@
+/*
+ * 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.process;
+
+import org.apache.sling.feature.Feature;
+
+/**
+ * A feature extension handler can merge a feature of a particular type
+ * and also post process the final assembled feature.
+ */
+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
+     * @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/process/FeatureProvider.java b/src/main/java/org/apache/sling/feature/process/FeatureProvider.java
new file mode 100644
index 0000000..8d3c35a
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/process/FeatureProvider.java
@@ -0,0 +1,30 @@
+/*
+ * 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.process;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+
+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/process/package-info.java b/src/main/java/org/apache/sling/feature/process/package-info.java
new file mode 100644
index 0000000..f6d563a
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/process/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.process;
+
+
diff --git a/src/test/java/org/apache/sling/feature/ArtifactIdTest.java b/src/test/java/org/apache/sling/feature/ArtifactIdTest.java
new file mode 100644
index 0000000..958e117
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/ArtifactIdTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.osgi.framework.Version;
+
+public class ArtifactIdTest {
+
+    private static final String G = "g";
+    private static final String A = "a";
+
+    @Test
+    public void testSameVersion() {
+        final String v1 = "1";
+        final String v10 = "1.0";
+        final String v100 = "1.0.0";
+
+        final Version ve1 = new ArtifactId(G, A, v1, null, null).getOSGiVersion();
+        final Version ve10 = new ArtifactId(G, A, v10, null, null).getOSGiVersion();
+        final Version ve100 = new ArtifactId(G, A, v100, null, null).getOSGiVersion();
+
+        assertEquals(0, ve1.compareTo(ve10));
+        assertEquals(0, ve10.compareTo(ve100));
+        assertEquals(0, ve1.compareTo(ve100));
+        assertEquals(0, ve10.compareTo(ve1));
+        assertEquals(0, ve100.compareTo(ve10));
+        assertEquals(0, ve100.compareTo(ve1));
+    }
+
+    @Test
+    public void testVersions() {
+        final String v1 = "1";
+        final String v20 = "2.0";
+        final String v150 = "1.5.0";
+
+        final Version ve1 = new ArtifactId(G, A, v1, null, null).getOSGiVersion();
+        final Version ve20 = new ArtifactId(G, A, v20, null, null).getOSGiVersion();
+        final Version ve150 = new ArtifactId(G, A, v150, null, null).getOSGiVersion();
+
+        assertTrue(ve1.compareTo(ve20) < 0);
+        assertTrue(ve20.compareTo(ve150) > 0);
+        assertTrue(ve1.compareTo(ve150) < 0);
+        assertTrue(ve20.compareTo(ve1) > 0);
+        assertTrue(ve150.compareTo(ve20) < 0);
+        assertTrue(ve150.compareTo(ve1) > 0);
+    }
+
+    @Test
+    public void testSnapshotQualifier() {
+        final Version v1 = new ArtifactId(G, A, "1", null, null).getOSGiVersion();
+        final Version v1snapshot = new ArtifactId(G, A, "1-SNAPSHOT", null, null).getOSGiVersion();
+        final Version v1a = new ArtifactId(G, A, "1-A", null, null).getOSGiVersion();
+
+        // snapshot in OSGi is higher than the corresponding version
+        assertTrue(v1.compareTo(v1snapshot) < 0);
+        assertTrue(v1snapshot.compareTo(v1) > 0);
+
+        // qualifier is higher than the version
+        assertTrue(v1a.compareTo(v1) > 0);
+        assertTrue(v1.compareTo(v1a) < 0);
+
+        // qualifier in OSGi is lower than snapshot (A is lower than SNAPSHOT)
+        assertTrue(v1a.compareTo(v1snapshot) < 0);
+        assertTrue(v1snapshot.compareTo(v1a) > 0);
+    }
+
+    @Test
+    public void testQualifiers() {
+        final Version va = new ArtifactId(G, A, "1-A", null, null).getOSGiVersion();
+        final Version vb = new ArtifactId(G, A, "1-B", null, null).getOSGiVersion();
+        assertTrue(va.compareTo(vb) < 0);
+        assertTrue(vb.compareTo(va) > 0);
+
+        final Version vc = new ArtifactId(G, A, "0.11.14.1.0010", null, null).getOSGiVersion();
+        assertEquals(0, vc.getMajor());
+        assertEquals(11, vc.getMinor());
+        assertEquals(14, vc.getMicro());
+        assertEquals("1_0010", vc.getQualifier());
+    }
+
+    @Test
+    public void testOSGiVersion() {
+        final Version v = new ArtifactId(G, A, "1.5.2.SNAPSHOT", null, null).getOSGiVersion();
+        assertEquals(1, v.getMajor());
+        assertEquals(5, v.getMinor());
+        assertEquals(2, v.getMicro());
+        assertEquals("SNAPSHOT", v.getQualifier());
+    }
+
+    @Test
+    public void testStrangeVersions() {
+        final Version v = new ArtifactId(G, A, "3.0.3-20170712.062549-4", null, null).getOSGiVersion();
+        assertEquals(3, v.getMajor());
+        assertEquals(0, v.getMinor());
+        assertEquals(3, v.getMicro());
+        assertEquals("20170712_062549-4", v.getQualifier());
+    }
+
+    @Test public void testCoordinatesGAV() {
+        final ArtifactId id = ArtifactId.fromMvnId("group.a:artifact.b:1.0");
+        assertEquals("group.a", id.getGroupId());
+        assertEquals("artifact.b", id.getArtifactId());
+        assertEquals("1.0", id.getVersion());
+        assertEquals("jar", id.getType());
+        assertNull(id.getClassifier());
+    }
+
+    @Test public void testCoordinatesGAVP() {
+        final ArtifactId id = ArtifactId.fromMvnId("group.a:artifact.b:zip:1.0");
+        assertEquals("group.a", id.getGroupId());
+        assertEquals("artifact.b", id.getArtifactId());
+        assertEquals("1.0", id.getVersion());
+        assertEquals("zip", id.getType());
+        assertNull(id.getClassifier());
+    }
+
+    @Test public void testCoordinatesGAVPC() {
+        final ArtifactId id = ArtifactId.fromMvnId("group.a:artifact.b:zip:foo:1.0");
+        assertEquals("group.a", id.getGroupId());
+        assertEquals("artifact.b", id.getArtifactId());
+        assertEquals("1.0", id.getVersion());
+        assertEquals("zip", id.getType());
+        assertEquals("foo", id.getClassifier());
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/BundlesTest.java b/src/test/java/org/apache/sling/feature/BundlesTest.java
new file mode 100644
index 0000000..3346f19
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/BundlesTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Map;
+
+import org.junit.Test;
+
+public class BundlesTest {
+
+    @Test
+    public void testIterator() {
+        final Bundles bundles = new Bundles();
+        bundles.add(1, new Artifact(ArtifactId.parse("1/a/1")));
+        bundles.add(5, new Artifact(ArtifactId.parse("5/a/5")));
+        bundles.add(5, new Artifact(ArtifactId.parse("5/b/6")));
+        bundles.add(2, new Artifact(ArtifactId.parse("2/b/2")));
+        bundles.add(2, new Artifact(ArtifactId.parse("2/a/3")));
+        bundles.add(4, new Artifact(ArtifactId.parse("4/x/4")));
+
+        int index = 1;
+        for(final Map.Entry<Integer, Artifact> entry : bundles) {
+            assertEquals(entry.getKey().toString(), entry.getValue().getId().getGroupId());
+            assertEquals(index, entry.getValue().getId().getOSGiVersion().getMajor());
+            index++;
+        }
+        assertEquals(7, index);
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/process/BuilderUtilTest.java b/src/test/java/org/apache/sling/feature/process/BuilderUtilTest.java
new file mode 100644
index 0000000..a33ef95
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/process/BuilderUtilTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.process;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+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.Bundles;
+import org.apache.sling.feature.process.BuilderUtil.ArtifactMerge;
+import org.junit.Test;
+
+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, Artifact> entry : f) {
+            result.add(entry);
+        }
+        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(1, new Artifact(ArtifactId.parse("g/a/1.0")));
+        target.add(2, new Artifact(ArtifactId.parse("g/b/2.0")));
+        target.add(3, new Artifact(ArtifactId.parse("g/c/2.5")));
+
+        final Bundles source = new Bundles();
+        source.add(1, new Artifact(ArtifactId.parse("g/a/1.1")));
+        source.add(2, new Artifact(ArtifactId.parse("g/b/1.9")));
+        source.add(3, new Artifact(ArtifactId.parse("g/c/2.5")));
+
+        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(1, new Artifact(ArtifactId.parse("g/a/1.0")));
+        target.add(2, new Artifact(ArtifactId.parse("g/b/2.0")));
+        target.add(3, new Artifact(ArtifactId.parse("g/c/2.5")));
+
+        final Bundles source = new Bundles();
+        source.add(1, new Artifact(ArtifactId.parse("g/a/1.1")));
+        source.add(2, new Artifact(ArtifactId.parse("g/b/1.9")));
+        source.add(3, new Artifact(ArtifactId.parse("g/c/2.5")));
+
+        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(1, new Artifact(ArtifactId.parse("g/a/1.0")));
+
+        final Bundles source = new Bundles();
+        source.add(2, new Artifact(ArtifactId.parse("g/a/1.1")));
+
+        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(1, new Artifact(ArtifactId.parse("g/a/1.0")));
+        target.add(2, new Artifact(ArtifactId.parse("g/b/2.0")));
+        target.add(3, new Artifact(ArtifactId.parse("g/c/2.5")));
+
+        final Bundles source = new Bundles();
+        source.add(1, new Artifact(ArtifactId.parse("g/d/1.1")));
+        source.add(2, new Artifact(ArtifactId.parse("g/e/1.9")));
+        source.add(3, new Artifact(ArtifactId.parse("g/f/2.5")));
+
+        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"));
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/process/FeatureBuilderTest.java b/src/test/java/org/apache/sling/feature/process/FeatureBuilderTest.java
new file mode 100644
index 0000000..5ef1a62
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/process/FeatureBuilderTest.java
@@ -0,0 +1,277 @@
+/*
+ * 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.process;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+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.Capability;
+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.apache.sling.feature.Requirement;
+import org.junit.Test;
+
+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(3, new Artifact(ArtifactId.parse("org.apache.sling/foo-bar/4.5.6")));
+        f1.getBundles().add(5, new Artifact(ArtifactId.parse("group/testnewversion_low/2")));
+        f1.getBundles().add(5, new Artifact(ArtifactId.parse("group/testnewversion_high/2")));
+        f1.getBundles().add(5, new Artifact(ArtifactId.parse("group/testnewstartlevel/1")));
+        f1.getBundles().add(5, new Artifact(ArtifactId.parse("group/testnewstartlevelandversion/1")));
+
+        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, Artifact> entry : f.getBundles()) {
+            result.add(entry);
+        }
+        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());
+        assertEquals(expected.getUpgradeOf(), actuals.getUpgradeOf());
+        assertEquals(expected.getUpgrades(), actuals.getUpgrades());
+
+        // 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 Requirement("osgi.contract");
+        r1.getDirectives().put("filter", "(&(osgi.contract=JavaServlet)(version=3.1))");
+        base.getRequirements().add(r1);
+
+        final Capability c1 = new Capability("osgi.implementation");
+        c1.getAttributes().put("osgi.implementation", "osgi.http");
+        c1.getAttributes().put("version:Version", "1.1");
+        c1.getDirectives().put("uses", "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard");
+        base.getCapabilities().add(c1);
+        final Capability c2 = new Capability("osgi.service");
+        c2.getAttributes().put("objectClass:List<String>", "org.osgi.service.http.runtime.HttpServiceRuntime");
+        c2.getDirectives().put("uses", "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto");
+        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("hash", "4632463464363646436");
+        base.getBundles().add(1, a1);
+        base.getBundles().add(1,  new Artifact(ArtifactId.parse("org.apache.sling/application-bundle/2.0.0")));
+        base.getBundles().add(1,  new Artifact(ArtifactId.parse("org.apache.sling/another-bundle/2.1.0")));
+        base.getBundles().add(2,  new Artifact(ArtifactId.parse("org.apache.sling/foo-xyz/1.2.3")));
+
+        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 Requirement("osgi.contract");
+        r1.getDirectives().put("filter", "(&(osgi.contract=JavaServlet)(version=3.1))");
+        base.getRequirements().add(r1);
+
+        final Capability c1 = new Capability("osgi.implementation");
+        c1.getAttributes().put("osgi.implementation", "osgi.http");
+        c1.getAttributes().put("version:Version", "1.1");
+        c1.getDirectives().put("uses", "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard");
+        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("hash", "4632463464363646436");
+        base.getBundles().add(1, a1);
+        base.getBundles().add(1,  new Artifact(ArtifactId.parse("org.apache.sling/application-bundle/2.0.0")));
+        base.getBundles().add(1,  new Artifact(ArtifactId.parse("org.apache.sling/another-bundle/2.1.0")));
+        base.getBundles().add(2,  new Artifact(ArtifactId.parse("org.apache.sling/foo-xyz/1.2.3")));
+        base.getBundles().add(5,  new Artifact(ArtifactId.parse("group/testnewversion_low/1")));
+        base.getBundles().add(5,  new Artifact(ArtifactId.parse("group/testnewversion_high/5")));
+        base.getBundles().add(10,  new Artifact(ArtifactId.parse("group/testnewstartlevel/1")));
+        base.getBundles().add(10,  new Artifact(ArtifactId.parse("group/testnewstartlevelandversion/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());
+
+        // create the expected result
+        final Feature result = base.copy();
+        result.getIncludes().remove(0);
+        result.getFrameworkProperties().put("bar", "X");
+        result.getBundles().add(3,  new Artifact(ArtifactId.parse("org.apache.sling/foo-bar/4.5.6")));
+        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