You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by cz...@apache.org on 2017/11/03 14:12:54 UTC

[sling-whiteboard] branch master created (now 4199dc7)

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

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


      at 4199dc7  Move feature model to whiteboard git

This branch includes the following new commits:

     new 4199dc7  Move feature model to whiteboard git

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


-- 
To stop receiving notification emails like this one, please contact
['"commits@sling.apache.org" <co...@sling.apache.org>'].

[sling-whiteboard] 01/01: Move feature model to whiteboard git

Posted by cz...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 4199dc777c6d3af124b53ae3cd926a7d25a8ccde
Author: Carsten Ziegeler <cz...@adobe.com>
AuthorDate: Fri Nov 3 15:06:50 2017 +0100

    Move feature model to whiteboard git
---
 .gitignore                                         |    5 +
 LICENSE                                            |  202 ++++
 README.md                                          |    5 +
 featuremodel/example/Example.md                    |    2 +
 featuremodel/example/sling/boot.json               |    1 +
 featuremodel/example/sling/composum-console.json   |    1 +
 featuremodel/example/sling/launchpad.json          |    1 +
 .../example/sling/models-jacksonexporter.json      |    1 +
 featuremodel/example/sling/oak.json                |    1 +
 featuremodel/example/sling/repoinit.json           |    1 +
 featuremodel/example/sling/sling-event.json        |    1 +
 featuremodel/example/sling/sling-validation.json   |    1 +
 featuremodel/example/sling/sling.json              |    1 +
 featuremodel/example/sling/slingshot.json          |    1 +
 featuremodel/example/sling/standalone.json         |    1 +
 featuremodel/feature-analyser/pom.xml              |  145 +++
 .../feature/analyser/ApplicationDescriptor.java    |   39 +
 .../sling/feature/analyser/ArtifactDescriptor.java |   39 +
 .../sling/feature/analyser/BundleDescriptor.java   |   95 ++
 .../feature/analyser/ContainerDescriptor.java      |   70 ++
 .../apache/sling/feature/analyser/Descriptor.java  |   93 ++
 .../sling/feature/analyser/FeatureDescriptor.java  |   28 +
 .../analyser/impl/ApplicationDescriptorImpl.java   |   52 +
 .../analyser/impl/BundleDescriptorImpl.java        |  159 +++
 .../analyser/impl/FeatureDescriptorImpl.java       |   39 +
 .../apache/sling/feature/analyser/main/Main.java   |   74 ++
 .../sling/feature/analyser/package-info.java       |   23 +
 .../sling/feature/analyser/service/Analyser.java   |  124 +++
 .../sling/feature/analyser/service/Scanner.java    |  228 +++++
 .../feature/analyser/service/package-info.java     |   23 +
 .../sling/feature/analyser/task/AnalyserTask.java  |   38 +
 .../feature/analyser/task/AnalyserTaskContext.java |   49 +
 .../task/impl/CheckBundleExportsImports.java       |  218 ++++
 .../task/impl/CheckBundlesForInitialContent.java   |   84 ++
 .../task/impl/CheckBundlesForResources.java        |   81 ++
 .../task/impl/CheckRequirementsCapabilities.java   |  100 ++
 .../sling/feature/analyser/task/package-info.java  |   23 +
 .../apache/sling/feature/resolver/Resolver.java    |   32 +
 .../sling/feature/resolver/ResolverContext.java    |   46 +
 .../sling/feature/resolver/package-info.java       |   23 +
 .../sling/feature/scanner/ExtensionScanner.java    |   48 +
 .../sling/feature/scanner/FrameworkScanner.java    |   44 +
 .../scanner/impl/ContentPackageDescriptor.java     |  142 +++
 .../scanner/impl/ContentPackageScanner.java        |  358 +++++++
 .../impl/ContentPackagesExtensionScanner.java      |   73 ++
 .../scanner/impl/FelixFrameworkScanner.java        |  183 ++++
 .../feature/scanner/impl/RepoInitScanner.java      |   62 ++
 .../apache/sling/feature/scanner/package-info.java |   23 +
 ...apache.sling.feature.analyser.task.AnalyserTask |    5 +
 ...g.apache.sling.feature.scanner.ExtensionScanner |    3 +
 ...g.apache.sling.feature.scanner.FrameworkScanner |    2 +
 .../sling/feature/analyser/AnalyserTest.java       |   69 ++
 .../src/test/resources/feature_complete.json       |   31 +
 .../src/test/resources/feature_incomplete.json     |   33 +
 featuremodel/feature-applicationbuilder/pom.xml    |  127 +++
 .../feature/applicationbuilder/impl/Main.java      |  213 ++++
 featuremodel/feature-karaf/pom.xml                 |   72 ++
 .../sling/feature/karaf/KarafFeatureWriter.java    |  195 ++++
 .../apache/sling/feature/karaf/package-info.java   |   23 +
 featuremodel/feature-launcher/pom.xml              |  128 +++
 .../feature/launcher/impl/FeatureProcessor.java    |  137 +++
 .../sling/feature/launcher/impl/Installation.java  |  137 +++
 .../feature/launcher/impl/LauncherConfig.java      |  122 +++
 .../apache/sling/feature/launcher/impl/Main.java   |  313 ++++++
 .../launcher/impl/launchers/AbstractRunner.java    |  278 ++++++
 .../launcher/impl/launchers/FrameworkLauncher.java |   88 ++
 .../launcher/impl/launchers/FrameworkRunner.java   |   70 ++
 .../sling/feature/launcher/spi/Launcher.java       |   26 +
 .../launcher/spi/LauncherPrepareContext.java       |   32 +
 .../feature/launcher/spi/LauncherRunContext.java   |   58 ++
 featuremodel/feature-modelconverter/pom.xml        |  127 +++
 .../sling/feature/modelconverter/impl/Main.java    |  729 ++++++++++++++
 featuremodel/feature-support/pom.xml               |  118 +++
 .../sling/feature/support/ArtifactHandler.java     |   42 +
 .../sling/feature/support/ArtifactManager.java     |  366 +++++++
 .../feature/support/ArtifactManagerConfig.java     |  126 +++
 .../sling/feature/support/ConfigurationUtil.java   |   90 ++
 .../apache/sling/feature/support/FeatureUtil.java  |  284 ++++++
 .../support/json/ApplicationJSONReader.java        |   92 ++
 .../support/json/ApplicationJSONWriter.java        |   89 ++
 .../support/json/ConfigurationJSONReader.java      |   78 ++
 .../support/json/ConfigurationJSONWriter.java      |   55 ++
 .../feature/support/json/FeatureJSONReader.java    |  332 +++++++
 .../feature/support/json/FeatureJSONWriter.java    |  214 ++++
 .../sling/feature/support/json/JSONConstants.java  |   88 ++
 .../sling/feature/support/json/JSONReaderBase.java |  396 ++++++++
 .../sling/feature/support/json/JSONWriterBase.java |  235 +++++
 .../sling/feature/support/json/package-info.java   |   23 +
 .../apache/sling/feature/support/package-info.java |   23 +
 .../feature/support/spi/ArtifactProvider.java      |   55 ++
 .../support/spi/ArtifactProviderContext.java       |   46 +
 .../sling/feature/support/spi/package-info.java    |   23 +
 .../feature/support/util/CapabilityMatcher.java    |  621 ++++++++++++
 .../sling/feature/support/util/LambdaUtil.java     |   99 ++
 .../sling/feature/support/util/ManifestParser.java | 1038 ++++++++++++++++++++
 .../sling/feature/support/util/ManifestUtil.java   |  174 ++++
 .../sling/feature/support/util/PackageInfo.java    |  104 ++
 .../sling/feature/support/util/SimpleFilter.java   |  647 ++++++++++++
 .../feature/support/util/StringComparator.java     |   71 ++
 .../sling/feature/support/util/SubstVarUtil.java   |  149 +++
 .../sling/feature/support/util/VersionRange.java   |  157 +++
 .../sling/feature/support/ArtifactManagerTest.java |   99 ++
 .../sling/feature/support/FeatureUtilTest.java     |   49 +
 .../support/json/FeatureJSONReaderTest.java        |   58 ++
 .../support/json/FeatureJSONWriterTest.java        |   44 +
 .../org/apache/sling/feature/support/json/U.java   |   82 ++
 .../support/util/CapabilityMatcherTest.java        |   33 +
 .../src/test/resources/features/test.json          |   85 ++
 featuremodel/feature/pom.xml                       |   86 ++
 featuremodel/feature/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 ++++++
 featuremodel/osgifeature-maven-plugin/pom.xml      |  192 ++++
 .../feature/maven/ApplicationProjectConfig.java    |  116 +++
 .../feature/maven/ApplicationProjectInfo.java      |   31 +
 .../apache/sling/feature/maven/Environment.java    |   35 +
 .../sling/feature/maven/FeatureConstants.java      |   32 +
 .../sling/feature/maven/FeatureProjectConfig.java  |  138 +++
 .../sling/feature/maven/FeatureProjectInfo.java    |   32 +
 .../apache/sling/feature/maven/Preprocessor.java   |  515 ++++++++++
 .../apache/sling/feature/maven/ProjectHelper.java  |  319 ++++++
 .../apache/sling/feature/maven/ProjectInfo.java    |   27 +
 .../feature/maven/mojos/AbstractFeatureMojo.java   |  116 +++
 .../sling/feature/maven/mojos/AttachFeature.java   |   77 ++
 .../mojos/DependencyLifecycleParticipant.java      |   89 ++
 .../META-INF/m2e/lifecycle-mapping-metadata.xml    |   32 +
 .../main/resources/META-INF/plexus/components.xml  |  103 ++
 .../src/site/markdown/index.md                     |    6 +
 .../osgifeature-maven-plugin/src/site/site.xml     |   35 +
 featuremodel/pom.xml                               |   69 ++
 154 files changed, 18383 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c1ba99a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.project
+.classpath
+.settings
+target
+bin
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..811d243
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# Apache Sling Whiteboard
+
+This module is part of the [Apache Sling](https://sling.apache.org) project.
+
+This is the whiteboard area of Apache Sling. It contains experimental code that is not intended to be used in production.
diff --git a/featuremodel/example/Example.md b/featuremodel/example/Example.md
new file mode 100644
index 0000000..daa99f6
--- /dev/null
+++ b/featuremodel/example/Example.md
@@ -0,0 +1,2 @@
+java -jar ../feature-applicationbuilder/target/org.apache.sling.feature.applicationbuilder-0.0.1-SNAPSHOT.jar -d sling -u ~/.m2/repository -o sling.json
+java -jar ../feature-launcher/target/org.apache.sling.feature.launcher-0.0.1-SNAPSHOT.jar -a sling.json -I -v
diff --git a/featuremodel/example/sling/boot.json b/featuremodel/example/sling/boot.json
new file mode 100644
index 0000000..6af3979
--- /dev/null
+++ b/featuremodel/example/sling/boot.json
@@ -0,0 +1 @@
+{"id":"generated/:boot/1.0.0","bundles":{"1":["org.apache.aries/org.apache.aries.util/1.1.3","org.apache.felix/org.apache.felix.configadmin/1.9.0-SNAPSHOT","org.apache.felix/org.apache.felix.eventadmin/1.4.10","org.apache.geronimo.specs/geronimo-atinject_1.0_spec/1.0","org.apache.sling/org.apache.sling.commons.log/5.0.2","org.apache.sling/org.apache.sling.commons.logservice/1.0.6","org.apache.sling/org.apache.sling.fragment.transaction/1.0.0","org.apache.sling/org.apache.sling.fragment.w [...]
diff --git a/featuremodel/example/sling/composum-console.json b/featuremodel/example/sling/composum-console.json
new file mode 100644
index 0000000..7726943
--- /dev/null
+++ b/featuremodel/example/sling/composum-console.json
@@ -0,0 +1 @@
+{"id":"generated/composum-console/1.0.0","bundles":{"20":["com.composum.sling.core/composum-sling-core-commons/1.8.2","com.composum.sling.core/composum-sling-core-config/1.8.2","com.composum.sling.core/composum-sling-core-console/1.8.2","com.composum.sling.core/composum-sling-core-jslibs/1.8.2","com.composum.sling.core/composum-sling-package-manager/1.8.2","com.composum.sling.core/composum-sling-user-management/1.8.2","org.apache.jackrabbit.vault/org.apache.jackrabbit.vault/3.1.30"]}}
\ No newline at end of file
diff --git a/featuremodel/example/sling/launchpad.json b/featuremodel/example/sling/launchpad.json
new file mode 100644
index 0000000..7bd778b
--- /dev/null
+++ b/featuremodel/example/sling/launchpad.json
@@ -0,0 +1 @@
+{"id":"generated/:launchpad/1.0.0","framework-properties":{"localIndexDir":"${sling.home}/repository/index","repository.home":"${sling.home}/repository","sling.run.mode.install.options":"oak_tar,oak_mongo"}}
\ No newline at end of file
diff --git a/featuremodel/example/sling/models-jacksonexporter.json b/featuremodel/example/sling/models-jacksonexporter.json
new file mode 100644
index 0000000..4fee125
--- /dev/null
+++ b/featuremodel/example/sling/models-jacksonexporter.json
@@ -0,0 +1 @@
+{"id":"generated/models-jacksonexporter/1.0.0","bundles":{"20":["com.fasterxml.jackson.core/jackson-annotations/2.8.7","com.fasterxml.jackson.core/jackson-core/2.8.7","com.fasterxml.jackson.core/jackson-databind/2.8.7","org.apache.sling/org.apache.sling.models.jacksonexporter/1.0.6"]}}
\ No newline at end of file
diff --git a/featuremodel/example/sling/oak.json b/featuremodel/example/sling/oak.json
new file mode 100644
index 0000000..3ddbabe
--- /dev/null
+++ b/featuremodel/example/sling/oak.json
@@ -0,0 +1 @@
+{"id":"generated/oak/1.0.0","bundles":{"10":["org.apache.felix/org.apache.felix.jaas/1.0.2"],"15":["org.apache.jackrabbit/oak-blob/1.6.4","org.apache.jackrabbit/oak-commons/1.6.4","org.apache.jackrabbit/oak-core/1.6.4","org.apache.jackrabbit/oak-jcr/1.6.4","org.apache.jackrabbit/oak-lucene/1.6.4",{"id":"org.apache.jackrabbit/oak-segment-tar/1.6.4"}],"16":["org.apache.sling/org.apache.sling.jcr.oak.server/1.1.4"]},"configurations":{"org.apache.felix.jaas.ConfigurationSpi":{"jaas.defaultRe [...]
diff --git a/featuremodel/example/sling/repoinit.json b/featuremodel/example/sling/repoinit.json
new file mode 100644
index 0000000..3ed827b
--- /dev/null
+++ b/featuremodel/example/sling/repoinit.json
@@ -0,0 +1 @@
+{"id":"generated/repoinit/1.0.0","bundles":{"20":["org.apache.sling/org.apache.sling.jcr.repoinit/1.1.5-SNAPSHOT","org.apache.sling/org.apache.sling.provisioning.model/1.8.2","org.apache.sling/org.apache.sling.repoinit.parser/1.1.1-SNAPSHOT"]},"repoinit:TEXT|false":"# general\ncreate path (sling:OrderedFolder) /content\nset ACL for everyone\nallow   jcr:read\ton /\nend\n\n# sling-mapping\ncreate service user sling-mapping\n\nset ACL for sling-mapping\nallow   jcr:read    on /\nend\n\n# s [...]
diff --git a/featuremodel/example/sling/sling-event.json b/featuremodel/example/sling/sling-event.json
new file mode 100644
index 0000000..29b0586
--- /dev/null
+++ b/featuremodel/example/sling/sling-event.json
@@ -0,0 +1 @@
+{"id":"generated/sling-event/1.0.0","bundles":{"20":["org.apache.sling/org.apache.sling.discovery.api/1.0.4","org.apache.sling/org.apache.sling.discovery.base/2.0.0","org.apache.sling/org.apache.sling.discovery.commons/1.0.20","org.apache.sling/org.apache.sling.discovery.oak/1.2.18","org.apache.sling/org.apache.sling.discovery.support/1.0.0","org.apache.sling/org.apache.sling.event.dea/1.1.2","org.apache.sling/org.apache.sling.event/4.2.6"]},"configurations":{"org.apache.sling.serviceuse [...]
\ No newline at end of file
diff --git a/featuremodel/example/sling/sling-validation.json b/featuremodel/example/sling/sling-validation.json
new file mode 100644
index 0000000..0a89271
--- /dev/null
+++ b/featuremodel/example/sling/sling-validation.json
@@ -0,0 +1 @@
+{"id":"generated/sling-validation/1.0.0","bundles":{"20":["org.apache.sling/org.apache.sling.caconfig.api/1.1.0","org.apache.sling/org.apache.sling.caconfig.impl/1.4.2","org.apache.sling/org.apache.sling.caconfig.spi/1.3.2","org.apache.sling/org.apache.sling.validation.api/1.0.0","org.apache.sling/org.apache.sling.validation.core/1.0.0"]},"configurations":{"org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~sling-caconfig":{"user.mapping":["org.apache.sling.caconfig.i [...]
\ No newline at end of file
diff --git a/featuremodel/example/sling/sling.json b/featuremodel/example/sling/sling.json
new file mode 100644
index 0000000..d4c45d4
--- /dev/null
+++ b/featuremodel/example/sling/sling.json
@@ -0,0 +1 @@
+{"id":"generated/sling/1.0.0","bundles":{"5":["commons-fileupload/commons-fileupload/1.3.2","commons-io/commons-io/2.5","org.apache.aries.jmx/org.apache.aries.jmx.api/1.1.5","org.apache.aries.jmx/org.apache.aries.jmx.core/1.1.7","org.apache.aries.jmx/org.apache.aries.jmx.whiteboard/1.1.5","org.apache.felix/org.apache.felix.bundlerepository/1.6.4","org.apache.felix/org.apache.felix.inventory/1.0.4","org.apache.felix/org.apache.felix.prefs/1.1.0","org.apache.felix/org.apache.felix.webconso [...]
diff --git a/featuremodel/example/sling/slingshot.json b/featuremodel/example/sling/slingshot.json
new file mode 100644
index 0000000..3d5a54c
--- /dev/null
+++ b/featuremodel/example/sling/slingshot.json
@@ -0,0 +1 @@
+{"id":"generated/slingshot/0.8.0","bundles":{"20":["org.apache.sling/org.apache.sling.sample.slingshot/0.8.0"]},"configurations":{"org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~sling.slingshot":{"user.mapping":["org.apache.sling.sample.slingshot=slingshot-service"]}},"repoinit:TEXT|false":"create service user slingshot-service\ncreate user slingshot1 with password slingshot1\ncreate user slingshot2 with password slingshot2\n\ncreate path (sling:Folder) /slingshot [...]
\ No newline at end of file
diff --git a/featuremodel/example/sling/standalone.json b/featuremodel/example/sling/standalone.json
new file mode 100644
index 0000000..75fdde9
--- /dev/null
+++ b/featuremodel/example/sling/standalone.json
@@ -0,0 +1 @@
+{"id":"generated/standalone/1.0.0","bundles":{"5":[{"id":"org.apache.felix/org.apache.felix.http.jetty/3.4.4"},{"id":"org.apache.felix/org.apache.felix.http.servlet-api/1.1.2"}]}}
diff --git a/featuremodel/feature-analyser/pom.xml b/featuremodel/feature-analyser/pom.xml
new file mode 100644
index 0000000..75533a8
--- /dev/null
+++ b/featuremodel/feature-analyser/pom.xml
@@ -0,0 +1,145 @@
+<?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.analyser</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+
+    <name>Apache Sling Feature Analyser</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-analyser</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/tooling/support/feature-analyser</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/tooling/support/feature-analyser</url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                <execution>
+                    <id>unpack-dependencies</id>
+                    <phase>prepare-package</phase>
+                    <goals>
+                        <goal>unpack-dependencies</goal>
+                    </goals>
+                    <configuration>
+                        <excludes>META-INF/**</excludes>
+                        <outputDirectory>${project.build.directory}/classes</outputDirectory>
+                        <overWriteReleases>false</overWriteReleases>
+                        <overWriteSnapshots>true</overWriteSnapshots>
+                        <includeArtifactIds>org.apache.felix.converter,org.apache.sling.feature,org.apache.sling.feature.support,org.apache.sling.commons.johnzon,org.apache.sling.commons.osgi,osgi.core,slf4j-api,slf4j-simple</includeArtifactIds>
+                    </configuration>
+                </execution>
+            </executions>
+            </plugin>
+                <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.sling.feature.analyser.main.Main</mainClass>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask</exclude>
+                        <exclude>src/main/resources/META-INF/services/org.apache.sling.feature.scanner.ExtensionScanner</exclude>
+                        <exclude>src/main/resources/META-INF/services/org.apache.sling.feature.scanner.FrameworkScanner</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.support</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.osgi</artifactId>
+            <version>2.4.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.johnzon</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+		    <artifactId>org.apache.felix.converter</artifactId>
+		    <version>0.1.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+      <!-- Testing -->
+        <dependency>
+        	<groupId>junit</groupId>
+        	<artifactId>junit</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.configurator</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/ApplicationDescriptor.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/ApplicationDescriptor.java
new file mode 100644
index 0000000..d76f686
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/ApplicationDescriptor.java
@@ -0,0 +1,39 @@
+/*
+ * 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.analyser;
+
+import org.apache.sling.feature.Application;
+
+/**
+ * Information about an application.
+ * This is the aggregated information.
+ */
+public abstract class ApplicationDescriptor extends ContainerDescriptor {
+
+    public abstract Application getApplication();
+
+    public abstract BundleDescriptor getFrameworkDescriptor();
+
+    @Override
+    public void lock() {
+        if ( this.isLocked() ) {
+            return;
+        }
+        this.aggregate(this.getFrameworkDescriptor());
+        super.lock();
+    }
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/ArtifactDescriptor.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/ArtifactDescriptor.java
new file mode 100644
index 0000000..8517d2e
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/ArtifactDescriptor.java
@@ -0,0 +1,39 @@
+/*
+ * 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.analyser;
+
+import java.io.File;
+
+import org.apache.sling.feature.Artifact;
+
+/**
+ * Information about an artifact
+ */
+public abstract class ArtifactDescriptor extends Descriptor {
+
+    /**
+     * Get the artifact file
+     * @return The artifact file
+     */
+    public abstract File getArtifactFile();
+
+    /**
+     * Get the artifact
+     * @return The artifact
+     */
+    public abstract Artifact getArtifact();
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/BundleDescriptor.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/BundleDescriptor.java
new file mode 100644
index 0000000..c186fa5
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/BundleDescriptor.java
@@ -0,0 +1,95 @@
+/*
+ * 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.analyser;
+
+import java.util.jar.Manifest;
+
+import org.apache.sling.feature.analyser.impl.BundleDescriptorImpl;
+import org.apache.sling.feature.support.util.PackageInfo;
+
+/**
+ * Information about a bundle
+ */
+public abstract class BundleDescriptor extends ArtifactDescriptor implements Comparable<BundleDescriptor> {
+
+    /**
+     * Get the bundle symbolic name.
+     * @return The bundle symbolic name
+     */
+    public abstract String getBundleSymbolicName();
+
+    /**
+     * Get the bundle version
+     * @return The bundle version
+     */
+    public abstract String getBundleVersion();
+
+    /**
+     * Get the start level
+     * @return The start level.
+     */
+    public abstract int getBundleStartLevel();
+
+    /**
+     * If the artifact has a manifest, return it
+     * @return The manifest
+     */
+    public abstract Manifest getManifest();
+
+    public boolean isExportingPackage(final String packageName) {
+        for(final PackageInfo i : getExportedPackages()) {
+            if ( i.getName().equals(packageName) ) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean isExportingPackage(final PackageInfo info) {
+        for(final PackageInfo i : getExportedPackages()) {
+            if ( i.getName().equals(info.getName())
+                 && (info.getVersion() == null || info.getPackageVersionRange().includes(i.getPackageVersion()))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if ( obj instanceof BundleDescriptorImpl ) {
+            return this.getBundleSymbolicName().equals(((BundleDescriptorImpl)obj).getBundleSymbolicName()) && this.getBundleVersion().equals(((BundleDescriptorImpl)obj).getBundleVersion());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return (this.getBundleSymbolicName() + ':' + this.getBundleVersion()).hashCode();
+
+    }
+
+    @Override
+    public String toString() {
+        return "BundleInfo [symbolicName=" + getBundleSymbolicName() + ", version=" + this.getBundleVersion() + "]";
+    }
+
+    @Override
+    public int compareTo(final BundleDescriptor o) {
+        return (this.getBundleSymbolicName() + ':' + this.getBundleVersion()).compareTo((o.getBundleSymbolicName() + ':' + o.getBundleVersion()));
+    }
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/ContainerDescriptor.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/ContainerDescriptor.java
new file mode 100644
index 0000000..69efb78
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/ContainerDescriptor.java
@@ -0,0 +1,70 @@
+/*
+ * 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.analyser;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Information about a container (feature/application).
+ * This is the aggregated information.
+ */
+public abstract class ContainerDescriptor extends Descriptor {
+
+    private final Set<BundleDescriptor> bundles = new HashSet<>();
+
+    private final Set<ArtifactDescriptor> artifacts = new HashSet<>();
+
+    /**
+     * Return a set of bundle descriptors.
+     *
+     * The requirements and capabilities of the returned bundles are
+     * available as an aggregate from {@link Descriptor#getCapabilities()},
+     * {@link Descriptor#getRequirements()}, {@link Descriptor#getDynamicImportedPackages()}
+     * {@link Descriptor#getExportedPackages()} and {@link Descriptor#getImportedPackages()}
+     * @return The set of bundle descriptors (might be empty)
+     */
+    public final Set<BundleDescriptor> getBundleDescriptors() {
+        return this.isLocked() ? Collections.unmodifiableSet(bundles) : bundles;
+    }
+
+    /**
+     * Return a set of artifact descriptors
+     * The requirements and capabilities of the returned artifacts are
+     * available as an aggregate from {@link Descriptor#getCapabilities()},
+     * {@link Descriptor#getRequirements()}.
+     * @return The set of artifact descriptors (might be empty)
+     */
+    public final Set<ArtifactDescriptor> getArtifactDescriptors() {
+        return this.isLocked() ? Collections.unmodifiableSet(artifacts) : artifacts;
+    }
+
+    @Override
+    public void lock() {
+        if ( this.isLocked() ) {
+            return;
+        }
+        for(final BundleDescriptor bd : this.bundles) {
+            this.aggregate(bd);
+        }
+        for(final ArtifactDescriptor d : this.artifacts) {
+            this.aggregate(d);
+        }
+        super.lock();
+    }
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/Descriptor.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/Descriptor.java
new file mode 100644
index 0000000..145344d
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/Descriptor.java
@@ -0,0 +1,93 @@
+/*
+ * 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.analyser;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.sling.feature.Capability;
+import org.apache.sling.feature.Requirement;
+import org.apache.sling.feature.support.util.PackageInfo;
+
+/**
+ * A descriptor holds information about requirements and capabilities
+ */
+public abstract class Descriptor  {
+
+    private boolean locked;
+
+    private final Set<PackageInfo> exports = new HashSet<>();
+
+    private final Set<PackageInfo> imports = new HashSet<>();
+
+    private final Set<PackageInfo> dynImports = new HashSet<>();
+
+    private final Set<Requirement> reqs = new HashSet<>();
+
+    private final Set<Capability> caps = new HashSet<>();
+
+    public void lock() {
+        this.locked = true;
+    }
+
+    public final boolean isLocked() {
+        return this.locked;
+    }
+
+    protected void checkLocked() {
+        if (this.locked) {
+            throw new IllegalStateException("Descriptor is locked.");
+        }
+    }
+
+    protected void aggregate(final Descriptor d) {
+        reqs.addAll(d.getRequirements());
+        caps.addAll(d.getCapabilities());
+        dynImports.addAll(d.getDynamicImportedPackages());
+        imports.addAll(d.getImportedPackages());
+        exports.addAll(d.getExportedPackages());
+    }
+
+    public final Set<PackageInfo> getExportedPackages() {
+        return locked ? Collections.unmodifiableSet(exports) : exports;
+    }
+
+    public final Set<PackageInfo> getImportedPackages() {
+        return locked ? Collections.unmodifiableSet(imports) : imports;
+    }
+
+    public final Set<PackageInfo> getDynamicImportedPackages() {
+        return locked ? Collections.unmodifiableSet(dynImports) : dynImports;
+    }
+
+    /**
+     * Return the list of requirements.
+     * @return The list of requirements. The list might be empty.
+     */
+    public final Set<Requirement> getRequirements() {
+        return locked ? Collections.unmodifiableSet(reqs) : reqs;
+    }
+
+    /**
+     * Return the list of capabilities.
+     * @return The list of capabilities. The list might be empty.
+     */
+    public final Set<Capability> getCapabilities() {
+        return locked ?  Collections.unmodifiableSet(caps) : caps;
+    }
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/FeatureDescriptor.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/FeatureDescriptor.java
new file mode 100644
index 0000000..89739ce
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/FeatureDescriptor.java
@@ -0,0 +1,28 @@
+/*
+ * 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.analyser;
+
+import org.apache.sling.feature.Feature;
+
+/**
+ * Information about a feature.
+ * This is the aggregated information.
+ */
+public abstract class FeatureDescriptor extends ContainerDescriptor {
+
+    public abstract Feature getFeature();
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/impl/ApplicationDescriptorImpl.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/impl/ApplicationDescriptorImpl.java
new file mode 100644
index 0000000..0ef8d89
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/impl/ApplicationDescriptorImpl.java
@@ -0,0 +1,52 @@
+/*
+ * 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.analyser.impl;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.analyser.ApplicationDescriptor;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+
+/**
+ * Information about an application.
+ * This is the aggregated information.
+ */
+public class ApplicationDescriptorImpl
+    extends ApplicationDescriptor {
+
+    private BundleDescriptor frameworkDescriptor;
+
+    private final Application app;
+
+    public ApplicationDescriptorImpl(final Application app) {
+        this.app = app;
+    }
+
+    @Override
+    public Application getApplication() {
+        return this.app;
+    }
+
+    @Override
+    public BundleDescriptor getFrameworkDescriptor() {
+        return frameworkDescriptor;
+    }
+
+    public void setFrameworkDescriptor(final BundleDescriptor frameworkDescriptor) {
+        checkLocked();
+        this.frameworkDescriptor = frameworkDescriptor;
+    }
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/impl/BundleDescriptorImpl.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/impl/BundleDescriptorImpl.java
new file mode 100644
index 0000000..1ed0ce2
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/impl/BundleDescriptorImpl.java
@@ -0,0 +1,159 @@
+/*
+ * 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.analyser.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Set;
+import java.util.jar.Manifest;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Capability;
+import org.apache.sling.feature.Requirement;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.support.util.ManifestParser;
+import org.apache.sling.feature.support.util.ManifestUtil;
+import org.apache.sling.feature.support.util.PackageInfo;
+import org.osgi.framework.Constants;
+
+/**
+ * Information about a bundle
+ */
+public class BundleDescriptorImpl
+    extends BundleDescriptor {
+
+    /** The bundle symbolic name. */
+    private String symbolicName;
+
+    /** The bundle version. */
+    private String bundleVersion;
+
+    /** The start level of this artifact. */
+    private final int startLevel;
+
+    /** Manifest */
+    private final Manifest manifest;
+
+    /** The physical file for analyzing. */
+    private final File artifactFile;
+
+    /** The corresponding artifact from the feature. */
+    private final Artifact artifact;
+
+    public BundleDescriptorImpl(final Artifact a,
+            final File file,
+            final int startLevel) throws IOException  {
+        this.artifact = a;
+        this.artifactFile = file;
+        this.startLevel = startLevel;
+
+        this.manifest = ManifestUtil.getManifest(file);
+        if ( this.manifest == null ) {
+            throw new IOException("File has no manifest");
+        }
+        this.analyze();
+        this.lock();
+    }
+
+    public BundleDescriptorImpl(final Artifact artifact,
+            final Set<PackageInfo> pcks,
+            final Set<Requirement> reqs,
+            final Set<Capability> caps) throws IOException {
+        this.artifact = artifact;
+        this.artifactFile = null;
+        this.startLevel = 0;
+
+        this.symbolicName = Constants.SYSTEM_BUNDLE_SYMBOLICNAME;
+        this.bundleVersion = artifact.getId().getOSGiVersion().toString();
+        this.getExportedPackages().addAll(pcks);
+        this.getRequirements().addAll(reqs);
+        this.getCapabilities().addAll(caps);
+        this.manifest = null;
+        this.lock();
+    }
+
+    /**
+     * Get the bundle symbolic name.
+     * @return The bundle symbolic name
+     */
+    @Override
+    public String getBundleSymbolicName() {
+        return symbolicName;
+    }
+
+    /**
+     * Get the bundle version
+     * @return The bundle version
+     */
+    @Override
+    public String getBundleVersion() {
+        return bundleVersion;
+    }
+
+    /**
+     * Get the start level
+     * @return The start level or {@code 0} for the default.
+     */
+    @Override
+    public int getBundleStartLevel() {
+        return startLevel;
+    }
+
+    @Override
+    public File getArtifactFile() {
+        return artifactFile;
+    }
+
+    @Override
+    public Artifact getArtifact() {
+        return artifact;
+    }
+
+    @Override
+    public Manifest getManifest() {
+        return this.manifest;
+    }
+
+    protected void analyze() throws IOException {
+        final String name = this.manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+        if ( name != null ) {
+            final String version = this.manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+            if ( version == null ) {
+                throw new IOException("Unable to get bundle version from artifact " + getArtifact().getId().toMvnId());
+            }
+            this.symbolicName = name;
+            this.bundleVersion = version;
+            final String newBundleName = this.getArtifact().getMetadata().get("bundle:rename-bsn");
+            if (newBundleName != null) {
+                this.symbolicName = newBundleName;
+            }
+
+            this.getExportedPackages().addAll(ManifestUtil.extractExportedPackages(this.manifest));
+            this.getImportedPackages().addAll(ManifestUtil.extractImportedPackages(this.manifest));
+            this.getDynamicImportedPackages().addAll(ManifestUtil.extractDynamicImportedPackages(this.manifest));
+            try {
+                ManifestParser parser = new ManifestParser(this.manifest);
+                this.getCapabilities().addAll(ManifestUtil.extractCapabilities(parser));
+                this.getRequirements().addAll(ManifestUtil.extractRequirements(parser));
+            } catch (Exception ex) {
+                throw new IOException(ex);
+            }
+        } else {
+            throw new IOException("Unable to get bundle symbolic name from artifact " + getArtifact().getId().toMvnId());
+        }
+    }
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/impl/FeatureDescriptorImpl.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/impl/FeatureDescriptorImpl.java
new file mode 100644
index 0000000..d7ac09a
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/impl/FeatureDescriptorImpl.java
@@ -0,0 +1,39 @@
+/*
+ * 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.analyser.impl;
+
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.FeatureDescriptor;
+
+/**
+ * Information about a feature.
+ * This is the aggregated information.
+ */
+public class FeatureDescriptorImpl
+    extends FeatureDescriptor {
+
+    private final Feature feature;
+
+    public FeatureDescriptorImpl(final Feature feature) {
+        this.feature = feature;
+    }
+
+    @Override
+    public Feature getFeature() {
+        return this.feature;
+    }
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/main/Main.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/main/Main.java
new file mode 100644
index 0000000..37c7548
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/main/Main.java
@@ -0,0 +1,74 @@
+/*
+ * 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.analyser.main;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.analyser.service.Analyser;
+import org.apache.sling.feature.analyser.service.Scanner;
+import org.apache.sling.feature.support.ArtifactManagerConfig;
+import org.apache.sling.feature.support.FeatureUtil;
+import org.apache.sling.feature.support.json.ApplicationJSONReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class Main {
+
+    public static void main(final String[] args) {
+        // setup logging
+        System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
+        System.setProperty("org.slf4j.simpleLogger.showThreadName", "false");
+        System.setProperty("org.slf4j.simpleLogger.levelInBrackets", "true");
+        System.setProperty("org.slf4j.simpleLogger.showLogName", "false");
+
+        final Logger logger = LoggerFactory.getLogger("analyser");
+        logger.info("Apache Sling Application Analyser");
+        logger.info("");
+
+        if ( args.length == 0 ) {
+            logger.error("Required argument missing: application file");
+            System.exit(1);
+        }
+        if ( args.length > 1 ) {
+            logger.error("Too many arguments. Only one (application file) is supported");
+            System.exit(1);
+        }
+        final File f = new File(args[0]);
+        Application app = null;
+        try ( final FileReader r = new FileReader(f)) {
+            app = ApplicationJSONReader.read(r);
+        } catch ( final IOException ioe) {
+            logger.error("Unable to read application: {}", f, ioe);
+            System.exit(1);
+        }
+        if ( app.getFramework() == null ) {
+            app.setFramework(FeatureUtil.getFelixFrameworkId(null));
+        }
+
+        try {
+            final Scanner scanner = new Scanner(new ArtifactManagerConfig());
+            final Analyser analyser = new Analyser(scanner);
+            analyser.analyse(app);
+        } catch ( final Exception e) {
+            logger.error("Unable to analyse application: {}", f, e);
+            System.exit(1);
+        }
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/package-info.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/package-info.java
new file mode 100644
index 0000000..3042a56
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/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.analyser;
+
+
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/service/Analyser.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/service/Analyser.java
new file mode 100644
index 0000000..42962fd
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/service/Analyser.java
@@ -0,0 +1,124 @@
+/*
+ * 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.analyser.service;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.analyser.ApplicationDescriptor;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.Set;
+
+public class Analyser {
+
+    private final AnalyserTask[] tasks;
+
+    private final Scanner scanner;
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    public Analyser(final Scanner scanner,
+            final AnalyserTask...tasks)
+    throws IOException {
+        this.tasks = tasks;
+        this.scanner = scanner;
+    }
+
+    public Analyser(final Scanner scanner,
+            final String... taskIds)
+    throws IOException {
+        this(scanner, getTasks(taskIds));
+        if ( this.tasks.length != taskIds.length ) {
+            throw new IOException("Couldn't find all tasks " + taskIds);
+        }
+    }
+
+    public Analyser(final Scanner scanner)
+    throws IOException {
+        this(scanner, getTasks((String[])null));
+    }
+
+    private static AnalyserTask[] getTasks(final String... taskIds) {
+        final Set<String> ids = taskIds == null ? null : new HashSet<>(Arrays.asList(taskIds));
+        final ServiceLoader<AnalyserTask> loader = ServiceLoader.load(AnalyserTask.class);
+        final List<AnalyserTask> list = new ArrayList<>();
+        for(final AnalyserTask task : loader) {
+            if ( ids == null || ids.contains(task.getId()) ) {
+                list.add(task);
+            }
+        }
+        return list.toArray(new AnalyserTask[list.size()]);
+    }
+
+    public void analyse(final Application app)
+    throws Exception {
+        logger.info("Starting application analyzer...");
+
+        final ApplicationDescriptor appDesc = scanner.scan(app);
+
+        final List<String> warnings = new ArrayList<>();
+        final List<String> errors = new ArrayList<>();
+
+        // execute analyser tasks
+        for(final AnalyserTask task : tasks) {
+            logger.info("- Executing {}...", task.getName());
+            task.execute(new AnalyserTaskContext() {
+
+                @Override
+                public Application getApplication() {
+                    return app;
+                }
+
+                @Override
+                public ApplicationDescriptor getDescriptor() {
+                    return appDesc;
+                }
+
+                @Override
+                public void reportWarning(final String message) {
+                    warnings.add(message);
+                }
+
+                @Override
+                public void reportError(final String message) {
+                    errors.add(message);
+                }
+            });
+        }
+
+        for(final String msg : warnings) {
+            logger.warn(msg);
+        }
+        for(final String msg : errors) {
+            logger.error(msg);
+        }
+
+        if ( !errors.isEmpty() ) {
+            throw new Exception("Analyser detected errors. See log output for error messages.");
+        }
+
+        logger.info("Provisioning model analyzer finished");
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/service/Scanner.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/service/Scanner.java
new file mode 100644
index 0000000..4b2e5a6
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/service/Scanner.java
@@ -0,0 +1,228 @@
+/*
+ * 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.analyser.service;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.Extensions;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.ApplicationDescriptor;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.analyser.ContainerDescriptor;
+import org.apache.sling.feature.analyser.FeatureDescriptor;
+import org.apache.sling.feature.analyser.impl.ApplicationDescriptorImpl;
+import org.apache.sling.feature.analyser.impl.BundleDescriptorImpl;
+import org.apache.sling.feature.analyser.impl.FeatureDescriptorImpl;
+import org.apache.sling.feature.scanner.ExtensionScanner;
+import org.apache.sling.feature.scanner.FrameworkScanner;
+import org.apache.sling.feature.support.ArtifactManager;
+import org.apache.sling.feature.support.ArtifactManagerConfig;
+
+/**
+ * The scanner is a service that scans items and provides descriptions for these.
+ * The following items can be scanned individually
+ * <ul>
+ *   <li>A bundle artifact
+ *   <li>An artifact (requires {@link ArtifactScannner}s)
+ *   <li>An extension (requires {@link ExtensionScanner}s)
+ *   <li>A feature (requires {@link ArtifactScannner}s and {@link ExtensionScanner}s)
+ *   <li>A framework (requires {@link FrameworkScanner}s
+ *   <li>An application (requires all scanner types)
+ * </ul>
+ */
+public class Scanner {
+
+    private final ArtifactManager artifactManager;
+
+    private final List<ExtensionScanner> extensionScanners;
+
+    private final List<FrameworkScanner> frameworkScanners;
+
+    /**
+     * Create a new scanner
+     *
+     * @param amConfig The artifact manager configuration
+     * @param artifactScanners A list of artifact scanners
+     * @param extensionScanners A list of extension scanners
+     * @param frameworkScanners A list of framework scanners
+     * @throws IOException If something goes wrong
+     */
+    public Scanner(final ArtifactManagerConfig amConfig,
+            final List<ExtensionScanner> extensionScanners,
+            final List<FrameworkScanner> frameworkScanners)
+    throws IOException {
+        this.artifactManager = ArtifactManager.getArtifactManager(amConfig);
+        this.extensionScanners = extensionScanners == null ? getServices(ExtensionScanner.class) : extensionScanners;
+        this.frameworkScanners = frameworkScanners == null ? getServices(FrameworkScanner.class) : frameworkScanners;
+    }
+
+    /**
+     * Create a new scanner and use the service loader to find the scanners
+     *
+     * @param amConfig The artifact manager configuration
+     * @throws IOException If something goes wrong
+     */
+    public Scanner(final ArtifactManagerConfig amConfig)
+    throws IOException {
+        this(amConfig, null, null);
+    }
+
+    /**
+     * Get services from the service loader
+     *
+     * @param clazz The service class
+     * @return The list of services might be empty.
+     */
+    private static <T> List<T> getServices(final Class<T> clazz) {
+        final ServiceLoader<T> loader = ServiceLoader.load(clazz);
+        final List<T> list = new ArrayList<>();
+        for(final T task : loader) {
+            list.add(task);
+        }
+        return list;
+    }
+
+    /**
+     * Scan a bundle
+     *
+     * @param bundle The bundle artifact
+     * @param startLevel The start level of the bundle
+     * @return The bundle descriptor
+     * @throws IOException If something goes wrong or the provided artifact is not a bundle.
+     */
+    public BundleDescriptor scan(final Artifact bundle, final int startLevel) throws IOException {
+        final File file = artifactManager.getArtifactHandler(bundle.getId().toMvnUrl()).getFile();
+        if ( file == null ) {
+            throw new IOException("Unable to find file for " + bundle.getId());
+        }
+
+        return new BundleDescriptorImpl(bundle, file, startLevel);
+    }
+
+    /**
+     * Get all bundle descriptors for a feature / application
+     * @param bundles The bundles
+     * @param desc The descriptor
+     * @throws IOException If something goes wrong or no suitable scanner is found.
+     */
+    private void getBundleInfos(final Bundles bundles, final ContainerDescriptor desc)
+    throws IOException {
+        for(final Map.Entry<Integer, List<Artifact>> entry : bundles.getBundlesByStartLevel().entrySet()) {
+            for(final Artifact bundle : entry.getValue() ) {
+                final BundleDescriptor bundleDesc = scan(bundle, entry.getKey());
+                desc.getBundleDescriptors().add(bundleDesc);
+            }
+        }
+    }
+
+    private void scan(final Extensions extensions, final ContainerDescriptor desc)
+    throws IOException {
+        for(final Extension ext : extensions) {
+            ContainerDescriptor extDesc = null;
+            for(final ExtensionScanner scanner : this.extensionScanners) {
+                extDesc = scanner.scan(ext, this.artifactManager);
+                if ( extDesc != null ) {
+                    break;
+                }
+            }
+            if ( extDesc == null ) {
+                throw new IOException("No extension scanner found for extension named " + ext.getName() + " of type " + ext.getType().name());
+            }
+            desc.getRequirements().addAll(extDesc.getRequirements());
+            desc.getCapabilities().addAll(extDesc.getCapabilities());
+            desc.getExportedPackages().addAll(extDesc.getExportedPackages());
+            desc.getImportedPackages().addAll(extDesc.getImportedPackages());
+            desc.getDynamicImportedPackages().addAll(extDesc.getDynamicImportedPackages());
+
+            desc.getArtifactDescriptors().addAll(extDesc.getArtifactDescriptors());
+            desc.getBundleDescriptors().addAll(extDesc.getBundleDescriptors());
+        }
+    }
+
+    private void compact(final ContainerDescriptor desc) {
+        // TBD remove all import packages / dynamic import packages which are resolved by this bundle set
+        // same with requirements
+
+    }
+
+    /**
+     * Scan a feature
+     *
+     * @param feature The feature
+     * @return The feature descriptor
+     * @throws IOException If something goes wrong or a scanner is missing
+     */
+    public FeatureDescriptor scan(final Feature feature) throws IOException {
+        final FeatureDescriptorImpl desc = new FeatureDescriptorImpl(feature);
+
+        getBundleInfos(feature.getBundles(), desc);
+        scan(feature.getExtensions(), desc);
+
+        compact(desc);
+
+        desc.lock();
+
+        return desc;
+    }
+
+    /**
+     * Scan an application
+     *
+     * @param app The application
+     * @return The application descriptor
+     * @throws IOException If something goes wrong or a scanner is missing
+     */
+    public ApplicationDescriptor scan(final Application app) throws IOException {
+        final ApplicationDescriptorImpl desc = new ApplicationDescriptorImpl(app);
+
+        getBundleInfos(app.getBundles(), desc);
+        scan(app.getExtensions(), desc);
+
+        compact(desc);
+
+        // framework
+        final File file = artifactManager.getArtifactHandler(app.getFramework().toMvnUrl()).getFile();
+        if ( file == null ) {
+            throw new IOException("Unable to find file for " + app.getFramework());
+        }
+
+        BundleDescriptor fwk = null;
+        for(final FrameworkScanner scanner : this.frameworkScanners) {
+            fwk = scanner.scan(app.getFramework(), file, app.getFrameworkProperties());
+            if ( fwk != null ) {
+                break;
+            }
+        }
+        if ( fwk == null ) {
+            throw new IOException("No scanner found for framework " + app.getFramework().toMvnId());
+        }
+
+        desc.setFrameworkDescriptor(fwk);
+
+        desc.lock();
+        return desc;
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/service/package-info.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/service/package-info.java
new file mode 100644
index 0000000..7d81b73
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/service/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.analyser.service;
+
+
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/AnalyserTask.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/AnalyserTask.java
new file mode 100644
index 0000000..8ef21bb
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/AnalyserTask.java
@@ -0,0 +1,38 @@
+/*
+ * 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.analyser.task;
+
+/**
+ * A analyser task analyses a specific part of the assembled
+ * application. It can report errors and warnings.
+ */
+public interface AnalyserTask {
+
+    /** A unique (short) id. */
+    default String getId() {
+        return getClass().getName();
+    };
+
+    /** A human readable name to identify the task. */
+    default String getName() {
+        return getClass().getSimpleName();
+    };
+
+    /** Execute the task. */
+    void execute(AnalyserTaskContext ctx) throws Exception;
+}
+
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/AnalyserTaskContext.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/AnalyserTaskContext.java
new file mode 100644
index 0000000..870e03a
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/AnalyserTaskContext.java
@@ -0,0 +1,49 @@
+/*
+ * 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.analyser.task;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.analyser.ApplicationDescriptor;
+
+public interface AnalyserTaskContext {
+
+    /**
+     * The assembled application.
+     * @return The application.
+     */
+    Application getApplication();
+
+    /**
+     * The application descriptor
+     */
+    ApplicationDescriptor getDescriptor();
+
+    /**
+     * This method is invoked by a {@link AnalyserTask} to report
+     * a warning.
+     * @param message The message.
+     */
+    void reportWarning(String message);
+
+    /**
+     * This method is invoked by a {@link AnalyserTask} to report
+     * an error.
+     * @param message The message.
+     */
+    void reportError(String message);
+}
+
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckBundleExportsImports.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckBundleExportsImports.java
new file mode 100644
index 0000000..ee0dffd
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckBundleExportsImports.java
@@ -0,0 +1,218 @@
+/*
+ * 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.analyser.task.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.apache.sling.feature.support.util.PackageInfo;
+import org.osgi.framework.Version;
+
+public class CheckBundleExportsImports implements AnalyserTask {
+
+    @Override
+    public String getName() {
+        return "Bundle Import/Export Check";
+    }
+
+    @Override
+    public String getId() {
+        return "bundle-packages";
+    }
+
+    public static final class Report {
+
+        public List<PackageInfo> exportWithoutVersion = new ArrayList<>();
+
+        public List<PackageInfo> exportMatchingSeveral = new ArrayList<>();
+
+        public List<PackageInfo> importWithoutVersion = new ArrayList<>();
+
+        public List<PackageInfo> missingExports = new ArrayList<>();
+
+        public List<PackageInfo> missingExportsWithVersion = new ArrayList<>();
+
+        public List<PackageInfo> missingExportsForOptional = new ArrayList<>();
+    }
+
+    private Report getReport(final Map<BundleDescriptor, Report> reports, final BundleDescriptor info) {
+        Report report = reports.get(info);
+        if ( report == null ) {
+            report = new Report();
+            reports.put(info, report);
+        }
+        return report;
+    }
+
+    private void checkForVersionOnExportedPackages(final AnalyserTaskContext ctx, final Map<BundleDescriptor, Report> reports) {
+        for(final BundleDescriptor info : ctx.getDescriptor().getBundleDescriptors()) {
+            if ( info.getExportedPackages() != null ) {
+                for(final PackageInfo i : info.getExportedPackages()) {
+                    if ( i.getPackageVersion().compareTo(Version.emptyVersion) == 0 ) {
+                        getReport(reports, info).exportWithoutVersion.add(i);
+                    }
+                }
+            }
+        }
+    }
+
+    private void checkForVersionOnImportingPackages(final AnalyserTaskContext ctx, final Map<BundleDescriptor, Report> reports) {
+        for(final BundleDescriptor info : ctx.getDescriptor().getBundleDescriptors()) {
+            if ( info.getImportedPackages() != null ) {
+                for(final PackageInfo i : info.getImportedPackages()) {
+                    if ( i.getVersion() == null ) {
+                        // don't report for javax and org.w3c. packages (TODO)
+                        if ( !i.getName().startsWith("javax.")
+                             && !i.getName().startsWith("org.w3c.")) {
+                            getReport(reports, info).importWithoutVersion.add(i);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+
+    @Override
+    public void execute(final AnalyserTaskContext ctx) throws IOException {
+        // basic checks
+        final Map<BundleDescriptor, Report> reports = new HashMap<>();
+        checkForVersionOnExportedPackages(ctx, reports);
+        checkForVersionOnImportingPackages(ctx, reports);
+
+        final SortedMap<Integer, List<BundleDescriptor>> bundlesMap = new TreeMap<>();
+        for(final BundleDescriptor bi : ctx.getDescriptor().getBundleDescriptors()) {
+            List<BundleDescriptor> list = bundlesMap.get(bi.getBundleStartLevel());
+            if ( list == null ) {
+                list = new ArrayList<>();
+                bundlesMap.put(bi.getBundleStartLevel(), list);
+            }
+            list.add(bi);
+        }
+
+        // add all system packages
+        final List<BundleDescriptor> exportingBundles = new ArrayList<>();
+        exportingBundles.add(ctx.getDescriptor().getFrameworkDescriptor());
+
+        for(final Map.Entry<Integer, List<BundleDescriptor>> entry : bundlesMap.entrySet()) {
+            // first add all exporting bundles
+            for(final BundleDescriptor info : entry.getValue()) {
+                if ( info.getExportedPackages() != null ) {
+                    exportingBundles.add(info);
+                }
+            }
+            // check importing bundles
+            for(final BundleDescriptor info : entry.getValue()) {
+                if ( info.getImportedPackages() != null ) {
+                    for(final PackageInfo pck : info.getImportedPackages() ) {
+                        final List<BundleDescriptor> candidates = getCandidates(exportingBundles, pck);
+                        if ( candidates.isEmpty() ) {
+                            if ( pck.isOptional() ) {
+                                getReport(reports, info).missingExportsForOptional.add(pck);
+                            } else {
+                                getReport(reports, info).missingExports.add(pck);
+                            }
+                        } else {
+                            final List<BundleDescriptor> matchingCandidates = new ArrayList<>();
+                            for(final BundleDescriptor i : candidates) {
+                                if ( i.isExportingPackage(pck) ) {
+                                    matchingCandidates.add(i);
+                                }
+                            }
+                            if ( matchingCandidates.isEmpty() ) {
+                                if ( pck.isOptional() ) {
+                                    getReport(reports, info).missingExportsForOptional.add(pck);
+                                } else {
+                                    getReport(reports, info).missingExportsWithVersion.add(pck);
+                                }
+                            } else if ( matchingCandidates.size() > 1 ) {
+                                getReport(reports, info).exportMatchingSeveral.add(pck);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        for(final Map.Entry<BundleDescriptor, Report> entry : reports.entrySet()) {
+            final String key = "Bundle " + entry.getKey().getArtifact().getId().getArtifactId() + ":" + entry.getKey().getArtifact().getId().getVersion();
+
+            if ( !entry.getValue().importWithoutVersion.isEmpty() ) {
+                ctx.reportWarning(key + " is importing package(s) " + getPackageInfo(entry.getValue().importWithoutVersion, false) + " without specifying a version range.");
+            }
+            if ( !entry.getValue().exportWithoutVersion.isEmpty() ) {
+                ctx.reportWarning(key + " is exporting package(s) " + getPackageInfo(entry.getValue().importWithoutVersion, false) + " without a version.");
+            }
+
+            if ( !entry.getValue().missingExports.isEmpty() ) {
+                ctx.reportError(key + " is importing package(s) " + getPackageInfo(entry.getValue().missingExports, false) + " in start level " +
+                        String.valueOf(entry.getKey().getBundleStartLevel()) + " but no bundle is exporting these for that start level.");
+            }
+            if ( !entry.getValue().missingExportsWithVersion.isEmpty() ) {
+                ctx.reportError(key + " is importing package(s) " + getPackageInfo(entry.getValue().missingExportsWithVersion, true) + " in start level " +
+                        String.valueOf(entry.getKey().getBundleStartLevel()) + " but no bundle is exporting these for that start level in the required version range.");
+            }
+        }
+    }
+
+    private String getPackageInfo(final List<PackageInfo> pcks, final boolean includeVersion) {
+        if ( pcks.size() == 1 ) {
+            if (includeVersion) {
+                return pcks.get(0).toString();
+            } else {
+                return pcks.get(0).getName();
+            }
+        }
+        final StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        sb.append('[');
+        for(final PackageInfo info : pcks) {
+            if ( first ) {
+                first = false;
+            } else {
+                sb.append(", ");
+            }
+            if (includeVersion) {
+                sb.append(info.toString());
+            } else {
+                sb.append(info.getName());
+            }
+        }
+        sb.append(']');
+        return sb.toString();
+    }
+
+    private List<BundleDescriptor> getCandidates(final List<BundleDescriptor> exportingBundles, final PackageInfo pck) {
+        final List<BundleDescriptor> candidates = new ArrayList<>();
+        for(final BundleDescriptor info : exportingBundles) {
+            if ( info.isExportingPackage(pck.getName()) ) {
+                candidates.add(info);
+            }
+        }
+        return candidates;
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckBundlesForInitialContent.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckBundlesForInitialContent.java
new file mode 100644
index 0000000..4dd0100
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckBundlesForInitialContent.java
@@ -0,0 +1,84 @@
+/*
+ * 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.analyser.task.impl;
+
+import org.apache.sling.commons.osgi.ManifestHeader;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.Manifest;
+
+public class CheckBundlesForInitialContent implements AnalyserTask {
+
+    /** The manifest header to specify initial content to be loaded. */
+    private static final String CONTENT_HEADER = "Sling-Initial-Content";
+
+    /**
+     * The path directive specifying the target node where initial content will
+     * be loaded.
+     */
+    private static final String PATH_DIRECTIVE = "path";
+
+    @Override
+    public String getName() {
+        return "Bundle Initial Content Check";
+    }
+
+    @Override
+    public String getId() {
+        return "bundle-content";
+    }
+
+    @Override
+    public void execute(final AnalyserTaskContext ctx) {
+        // check for initial content
+        for(final BundleDescriptor info : ctx.getDescriptor().getBundleDescriptors()) {
+            final List<String> initialContent = extractInitialContent(info.getManifest());
+
+            if ( !initialContent.isEmpty() ) {
+                ctx.reportWarning("Found initial content in " + info.getArtifact() + " : " + initialContent);
+            }
+        }
+    }
+
+    private List<String> extractInitialContent(final Manifest m) {
+        final List<String> initialContent = new ArrayList<>();
+        if ( m != null ) {
+            final String root =  m.getMainAttributes().getValue(CONTENT_HEADER);
+            if (root != null) {
+                final ManifestHeader header = ManifestHeader.parse(root);
+                for (final ManifestHeader.Entry entry : header.getEntries()) {
+
+                    String path = entry.getDirectiveValue(PATH_DIRECTIVE);
+                    if (path == null) {
+                        path = "/";
+                    } else if (!path.startsWith("/")) {
+                        // make relative path absolute
+                        path = "/" + path;
+                    }
+                    initialContent.add(path);
+                }
+            }
+        }
+        return initialContent;
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckBundlesForResources.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckBundlesForResources.java
new file mode 100644
index 0000000..603cfe4
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckBundlesForResources.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.feature.analyser.task.impl;
+
+import org.apache.sling.commons.osgi.ManifestHeader;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.Manifest;
+
+public class CheckBundlesForResources implements AnalyserTask {
+
+    /** The manifest header to specify bundle resources. */
+    private static final String BUNDLE_RESOURCE_ROOTS = "Sling-Bundle-Resources";
+
+    /**
+     * The path directive specifying the target node where initial content will
+     * be loaded.
+     */
+    private static final String PATH_DIRECTIVE = "path";
+
+    @Override
+    public String getName() {
+        return "Bundle Resources Check";
+    }
+
+    @Override
+    public String getId() {
+        return "bundle-resources";
+    }
+
+    @Override
+    public void execute(final AnalyserTaskContext ctx) {
+        // check for initial content
+        for(final BundleDescriptor info : ctx.getDescriptor().getBundleDescriptors()) {
+            final List<String> bundleResources = extractBundleResources(info.getManifest());
+            if ( !bundleResources.isEmpty() ) {
+                ctx.reportWarning("Found bundle resources in " + info.getArtifact() + " : " + bundleResources);
+            }
+        }
+    }
+
+    private List<String> extractBundleResources(final Manifest m) {
+        final List<String> bundleResources = new ArrayList<>();
+        if ( m != null ) {
+            final String root =  m.getMainAttributes().getValue(BUNDLE_RESOURCE_ROOTS);
+            if (root != null) {
+                final ManifestHeader header = ManifestHeader.parse(root);
+                for (final ManifestHeader.Entry entry : header.getEntries()) {
+                    final String resourceRoot = entry.getValue();
+                    final String pathDirective = entry.getDirectiveValue(PATH_DIRECTIVE);
+                    if (pathDirective != null) {
+                        bundleResources.add(resourceRoot + "!" + pathDirective);
+                    } else {
+                        bundleResources.add(resourceRoot);
+                    }
+                }
+            }
+        }
+        return bundleResources;
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRequirementsCapabilities.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRequirementsCapabilities.java
new file mode 100644
index 0000000..0ea2799
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckRequirementsCapabilities.java
@@ -0,0 +1,100 @@
+/*
+ * 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.analyser.task.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import org.apache.sling.feature.Requirement;
+import org.apache.sling.feature.analyser.ArtifactDescriptor;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.apache.sling.feature.support.util.CapabilityMatcher;
+
+public class CheckRequirementsCapabilities implements AnalyserTask {
+    private final String format = "Artifact %s:%s requires %s in start level %d but %s";
+
+    @Override
+    public void execute(AnalyserTaskContext ctx) throws Exception {
+        final SortedMap<Integer, List<ArtifactDescriptor>> artifactsMap = new TreeMap<>();
+        for(final BundleDescriptor bi : ctx.getDescriptor().getBundleDescriptors()) {
+            List<ArtifactDescriptor> list = artifactsMap.get(bi.getBundleStartLevel());
+            if ( list == null ) {
+                list = new ArrayList<>();
+                artifactsMap.put(bi.getBundleStartLevel(), list);
+            }
+            list.add(bi);
+        }
+
+        if (!ctx.getDescriptor().getArtifactDescriptors().isEmpty()) {
+            artifactsMap.put(
+                    (artifactsMap.isEmpty() ? 0 : artifactsMap.lastKey()) + 1,
+                    new ArrayList<>(ctx.getDescriptor().getArtifactDescriptors())
+                    );
+        }
+
+        // add system artifact
+        final List<ArtifactDescriptor> artifacts = new ArrayList<>();
+        artifacts.add(ctx.getDescriptor().getFrameworkDescriptor());
+
+        for(final Map.Entry<Integer, List<ArtifactDescriptor>> entry : artifactsMap.entrySet()) {
+            // first add all providing artifacts
+            for (final ArtifactDescriptor info : entry.getValue()) {
+                if (info.getCapabilities() != null) {
+                    artifacts.add(info);
+                }
+            }
+            // check requiring artifacts
+            for (final ArtifactDescriptor info : entry.getValue()) {
+                if (info.getRequirements() != null)
+                {
+                    for (Requirement requirement : info.getRequirements()) {
+                        List<ArtifactDescriptor> candidates = getCandidates(artifacts, requirement);
+
+                        if (candidates.isEmpty()) {
+                            if ( "osgi.service".equals(requirement.getNamespace())  ){
+                                // osgi.service is special - we don't provide errors or warnings in this case
+                                continue;
+                            }
+                            if (!CapabilityMatcher.isOptional(requirement)) {
+                                ctx.reportError(String.format(format, info.getArtifact().getId().getArtifactId(), info.getArtifact().getId().getVersion(), requirement.toString(), entry.getKey(), "no artifact is providing a matching capability in this start level."));
+                            }
+                            else {
+                                ctx.reportWarning(String.format(format, info.getArtifact().getId().getArtifactId(), info.getArtifact().getId().getVersion(), requirement.toString(), entry.getKey(), "while the requirement is optional no artifact is providing a matching capability in this start level."));
+                            }
+                        }
+                        else if ( candidates.size() > 1 ) {
+                            ctx.reportWarning(String.format(format, info.getArtifact().getId().getArtifactId(), info.getArtifact().getId().getVersion(), requirement.toString(), entry.getKey(), "there is more than one matching capability in this start level."));
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private List<ArtifactDescriptor> getCandidates(List<ArtifactDescriptor> artifactDescriptors, Requirement requirement) {
+        return artifactDescriptors.stream()
+                .filter(artifactDescriptor -> artifactDescriptor.getCapabilities() != null)
+                .filter(artifactDescriptor -> artifactDescriptor.getCapabilities().stream().anyMatch(capability -> CapabilityMatcher.matches(capability, requirement)))
+                .collect(Collectors.toList());
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/package-info.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/package-info.java
new file mode 100644
index 0000000..d265303
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/analyser/task/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.analyser.task;
+
+
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/resolver/Resolver.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/resolver/Resolver.java
new file mode 100644
index 0000000..f18626e
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/resolver/Resolver.java
@@ -0,0 +1,32 @@
+/*
+ * 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.resolver;
+
+import org.apache.sling.feature.Feature;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Resolver {
+
+    public List<Feature> resolve(final ResolverContext ctx) {
+        final List<Feature> result = new ArrayList<>(ctx.getRequiredFeatures());
+
+        return result;
+    }
+}
+
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/resolver/ResolverContext.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/resolver/ResolverContext.java
new file mode 100644
index 0000000..3490525
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/resolver/ResolverContext.java
@@ -0,0 +1,46 @@
+/*
+ * 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.resolver;
+
+import org.apache.sling.feature.Feature;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ResolverContext {
+
+    private final List<Feature> requiredFeatures = new ArrayList<>();
+
+    private final List<Feature> optionalFeatures = new ArrayList<>();
+
+    public List<Feature> getRequiredFeatures() {
+        return requiredFeatures;
+    }
+
+    public List<Feature> getOptionalFeatures() {
+        return optionalFeatures;
+    }
+
+    public void addRequiredFeatures(final List<Feature> features) {
+        this.requiredFeatures.addAll(features);
+    }
+
+    public void addOptionalFeatures(final List<Feature> features) {
+        this.optionalFeatures.addAll(features);
+    }
+}
+
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/resolver/package-info.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/resolver/package-info.java
new file mode 100644
index 0000000..932b891
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/resolver/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.resolver;
+
+
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/ExtensionScanner.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/ExtensionScanner.java
new file mode 100644
index 0000000..ca5f6cb
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/ExtensionScanner.java
@@ -0,0 +1,48 @@
+/*
+ * 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.scanner;
+
+import java.io.IOException;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.analyser.ContainerDescriptor;
+import org.apache.sling.feature.support.ArtifactManager;
+import org.osgi.annotation.versioning.ConsumerType;
+
+/**
+ * The extension scanner scans an extension.
+ */
+@ConsumerType
+public interface ExtensionScanner  {
+
+    /** A unique (short) id. */
+    String getId();
+
+    /** A human readable name to identify the scanner. */
+    String getName();
+
+    /**
+     * Try to scan the extension and return a descriptor
+     *
+     * @param extension The extension
+     * @param manager Artifact manager
+     * @return The descriptor or {@code null} if the scanner does not know the extension
+     * @throws IOException If an error occurs while scanning the extension or the extension is invalid
+     */
+    ContainerDescriptor scan(Extension extension,
+            ArtifactManager manager) throws IOException;
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/FrameworkScanner.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/FrameworkScanner.java
new file mode 100644
index 0000000..8661097
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/FrameworkScanner.java
@@ -0,0 +1,44 @@
+/*
+ * 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.scanner;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.KeyValueMap;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.osgi.annotation.versioning.ConsumerType;
+
+/**
+ * The framework scanner scans the framework
+ */
+@ConsumerType
+public interface FrameworkScanner  {
+
+    /**
+     * Try to scan the artifact
+     * @param framework The framework artifact id
+     * @param file The framework artifact
+     * @param frameworkProps framework properties to launch the framework
+     * @return A descriptor or {@code null}
+     * @throws IOException If an error occurs while scanning the platform or the artifact is invalid
+     */
+    BundleDescriptor scan(ArtifactId framework,
+            File platformFile,
+            KeyValueMap frameworkProps) throws IOException;
+}
\ No newline at end of file
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageDescriptor.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageDescriptor.java
new file mode 100644
index 0000000..7f75dd3
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageDescriptor.java
@@ -0,0 +1,142 @@
+/*
+ * 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.scanner.impl;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.analyser.ArtifactDescriptor;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+
+/**
+ * Information about a content package.
+ */
+public class ContentPackageDescriptor extends ArtifactDescriptor {
+
+    /** The content package name. */
+    private String name;
+
+    /** Bundles in the content package. */
+    public final List<BundleDescriptor> bundles = new ArrayList<>();
+
+    /** Configurations in the content package. */
+    public final List<Configuration> configs = new ArrayList<>();
+
+    private File artifactFile;
+
+    private Artifact artifact;
+
+    /**
+     * Get the artifact file
+     * @return The artifact file
+     */
+    @Override
+    public File getArtifactFile() {
+        return artifactFile;
+    }
+
+    /**
+     * Get the artifact
+     * @return The artifact
+     */
+    @Override
+    public Artifact getArtifact() {
+        return artifact;
+    }
+
+    /**
+     * Set the artifact
+     * @param artifact The artifact
+     */
+    public void setArtifact(Artifact artifact) {
+        checkLocked();
+        this.artifact = artifact;
+    }
+
+    /**
+     * Set the artifact file
+     * @param artifactFile The artifact file
+     */
+    public void setArtifactFile(File artifactFile) {
+        checkLocked();
+        this.artifactFile = artifactFile;
+    }
+
+    /** Optional: the artifact of the content package. */
+    private Artifact contentPackage;
+
+    /** Optional: the path inside of the content package. */
+    private String contentPath;
+
+    /**
+     * Get the content package
+     * @return The content package or {@code null}
+     */
+    public Artifact getContentPackage() {
+        return contentPackage;
+    }
+
+    /**
+     * Get the content path
+     * @return The content path or {@code null}
+     */
+    public String getContentPath() {
+        return this.contentPath;
+    }
+
+    /**
+     * Whether this artifact is embedded in a content package
+     * @return {@code true} if embedded.
+     */
+    public boolean isEmbeddedInContentPackage() {
+        return this.contentPath != null;
+    }
+
+    /**
+     * Set the information about the content package containing this artifact
+     * @param artifact The package
+     * @param path The path inside the package
+     */
+    public void setContentPackageInfo(final Artifact artifact, final String path) {
+        checkLocked();
+        this.contentPackage = artifact;
+        this.contentPath = path;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String value) {
+        checkLocked();
+        this.name = value;
+    }
+
+
+    public boolean hasEmbeddedArtifacts() {
+        return !this.bundles.isEmpty() || !this.configs.isEmpty();
+    }
+
+    @Override
+    public String toString() {
+        return "ContentPackage [" + name + "]";
+    }
+}
+
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java
new file mode 100644
index 0000000..90b84c7
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java
@@ -0,0 +1,358 @@
+/*
+ * 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.scanner.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.analyser.impl.BundleDescriptorImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ContentPackageScanner {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final byte[] buffer = new byte[65536];
+
+    private enum FileType {
+        BUNDLE,
+        CONFIG,
+        PACKAGE
+    }
+
+    public Set<ContentPackageDescriptor> scan(final Artifact desc, final File file) throws IOException {
+        if (!file.getName().endsWith(".zip") ) {
+            throw new IOException("Artifact seems to be no content package (not a zip file): " + desc.getId().toMvnId());
+        }
+
+        final Set<ContentPackageDescriptor> contentPackages = new HashSet<>();
+        final ContentPackageDescriptor cp = new ContentPackageDescriptor();
+        final int lastDot = file.getName().lastIndexOf(".");
+        cp.setName(file.getName().substring(0, lastDot));
+        cp.setArtifact(desc);
+        cp.setArtifactFile(file);
+
+        extractContentPackage(cp, contentPackages, file);
+
+        contentPackages.add(cp);
+        cp.lock();
+
+        return contentPackages;
+    }
+
+    private void extractContentPackage(final ContentPackageDescriptor cp,
+            final Set<ContentPackageDescriptor> infos,
+            final File archive)
+    throws IOException {
+        logger.debug("Analyzing Content Package {}", archive.getName());
+
+        final File tempDir = Files.createTempDirectory(null).toFile();
+        try {
+            final File toDir = new File(tempDir, archive.getName());
+            toDir.mkdirs();
+
+            final List<File> toProcess = new ArrayList<>();
+
+            try (final ZipInputStream zis = new ZipInputStream(new FileInputStream(archive)) ) {
+                boolean done = false;
+                while ( !done ) {
+                    final ZipEntry entry = zis.getNextEntry();
+                    if ( entry == null ) {
+                        done = true;
+                    } else {
+                        final String entryName = entry.getName();
+                        if ( !entryName.endsWith("/") && entryName.startsWith("jcr_root/") ) {
+                            final String contentPath = entryName.substring(8);
+
+                            FileType fileType = null;
+
+                            if ( entryName.endsWith(".zip") ) {
+                                // embedded content package
+                                fileType = FileType.PACKAGE;
+
+                                // check for libs or apps
+                            } else if ( entryName.startsWith("jcr_root/libs/") || entryName.startsWith("jcr_root/apps/") ) {
+
+                                // check if this is an install folder (I)
+                                // install folders are either named:
+                                // "install" or
+                                // "install.{runmode}"
+                                boolean isInstall = entryName.indexOf("/install/") != -1;
+                                if ( !isInstall ) {
+                                    final int pos = entryName.indexOf("/install.");
+                                    if ( pos != -1 ) {
+                                        final int endSlashPos = entryName.indexOf('/', pos + 1);
+                                        if ( endSlashPos != -1 ) {
+                                            isInstall = true;
+                                        }
+                                    }
+                                }
+                                if ( !isInstall ) {
+                                    // check if this is an install folder (II)
+                                    // config folders are either named:
+                                    // "config" or
+                                    // "config.{runmode}"
+                                    isInstall = entryName.indexOf("/config/") != -1;
+                                    if ( !isInstall ) {
+                                        final int pos = entryName.indexOf("/config.");
+                                        if ( pos != -1 ) {
+                                            final int endSlashPos = entryName.indexOf('/', pos + 1);
+                                            if ( endSlashPos != -1 ) {
+                                                isInstall = true;
+                                            }
+                                        }
+                                    }
+                                }
+
+                                if (isInstall ) {
+
+                                   if ( entryName.endsWith(".jar") ) {
+                                       fileType = FileType.BUNDLE;
+                                   } else if ( entryName.endsWith(".xml") || entryName.endsWith(".config") ) {
+                                       fileType = FileType.CONFIG;
+                                   }
+                                }
+                            }
+
+                            if ( fileType != null ) {
+                                logger.debug("- extracting : {}", entryName);
+                                final File newFile = new File(toDir, entryName.replace('/', File.separatorChar));
+                                newFile.getParentFile().mkdirs();
+
+                                try (final FileOutputStream fos = new FileOutputStream(newFile)) {
+                                    int len;
+                                    while ((len = zis.read(buffer)) > -1) {
+                                        fos.write(buffer, 0, len);
+                                    }
+                                }
+
+                                if ( fileType == FileType.BUNDLE ) {
+                                    int startLevel = 20;
+                                    final int lastSlash = contentPath.lastIndexOf('/');
+                                    final int nextSlash = contentPath.lastIndexOf('/', lastSlash - 1);
+                                    final String part = contentPath.substring(nextSlash + 1, lastSlash);
+                                    try {
+                                        startLevel = Integer.valueOf(part);
+                                    } catch ( final NumberFormatException ignore ) {
+                                        // ignore
+                                    }
+
+                                    final Artifact bundle = new Artifact(extractArtifactId(tempDir, newFile));
+                                    final BundleDescriptor info = new BundleDescriptorImpl(bundle, newFile, startLevel);
+                                    bundle.getMetadata().put("content-package", cp.getArtifact().getId().toMvnId());
+                                    bundle.getMetadata().put("content-path", contentPath);
+
+                                    cp.bundles.add(info);
+
+                                } else if ( fileType == FileType.CONFIG ) {
+
+                                    final Configuration configEntry = this.process(newFile, cp.getArtifact(), contentPath);
+                                    if ( configEntry != null ) {
+
+                                        cp.configs.add(configEntry);
+                                    }
+
+                                } else if ( fileType == FileType.PACKAGE ) {
+                                    toProcess.add(newFile);
+                                }
+
+                            }
+
+                        }
+                        zis.closeEntry();
+                    }
+                }
+
+            }
+
+            for(final File f : toProcess) {
+                extractContentPackage(cp, infos, f);
+                final ContentPackageDescriptor i = new ContentPackageDescriptor();
+                final int lastDot = f.getName().lastIndexOf(".");
+                i.setName(f.getName().substring(0, lastDot));
+                i.setArtifactFile(f);
+                i.setContentPackageInfo(cp.getArtifact(), f.getName());
+                infos.add(i);
+
+                i.lock();
+            }
+        } finally {
+            if ( tempDir.exists() ) {
+                tempDir.delete();
+            }
+        }
+    }
+
+    private ArtifactId extractArtifactId(final File tempDir, final File bundleFile)
+    throws IOException {
+        logger.debug("Extracting Bundle {}", bundleFile.getName());
+
+        final File toDir = new File(tempDir, bundleFile.getName());
+        toDir.mkdirs();
+
+        try (final ZipInputStream zis = new ZipInputStream(new FileInputStream(bundleFile)) ) {
+            boolean done = false;
+            while ( !done ) {
+                final ZipEntry entry = zis.getNextEntry();
+                if ( entry == null ) {
+                    done = true;
+                } else {
+                    final String entryName = entry.getName();
+                    if ( !entryName.endsWith("/") && entryName.startsWith("META-INF/maven/") && entryName.endsWith("/pom.properties")) {
+                        logger.debug("- extracting : {}", entryName);
+                        final File newFile = new File(toDir, entryName.replace('/', File.separatorChar));
+                        newFile.getParentFile().mkdirs();
+
+                        try (final FileOutputStream fos = new FileOutputStream(newFile)) {
+                            int len;
+                            while ((len = zis.read(buffer)) > -1) {
+                                fos.write(buffer, 0, len);
+                            }
+                        }
+
+                    }
+                    zis.closeEntry();
+                }
+            }
+
+        }
+
+        // check for maven
+
+        final File metaInfDir = new File(toDir, "META-INF");
+        if ( metaInfDir.exists() ) {
+            final File mavenDir = new File(metaInfDir, "maven");
+            if ( mavenDir.exists() ) {
+                File groupDir = null;
+                for(final File d : mavenDir.listFiles()) {
+                    if ( d.isDirectory() && !d.getName().startsWith(".") ) {
+                        groupDir = d;
+                        break;
+                    }
+                }
+                if ( groupDir != null ) {
+                    File artifactDir = null;
+                    for(final File d : groupDir.listFiles()) {
+                        if ( d.isDirectory() && !d.getName().startsWith(".") ) {
+                            artifactDir = d;
+                            break;
+                        }
+                    }
+                    if ( artifactDir != null ) {
+                        final File propsFile = new File(artifactDir, "pom.properties");
+                        if ( propsFile.exists() ) {
+                            final Properties props = new Properties();
+                            try ( final Reader r = new FileReader(propsFile) ) {
+                                props.load(r);
+                            }
+                            String groupId = props.getProperty("groupId");
+                            String artifactId = props.getProperty("artifactId");
+                            String version = props.getProperty("version");
+                            String classifier = null;
+
+                            // Capture classifier
+                            final int pos = bundleFile.getName().indexOf(version) + version.length();
+                            if ( bundleFile.getName().charAt(pos) == '-') {
+                                classifier = bundleFile.getName().substring(pos + 1, bundleFile.getName().lastIndexOf('.'));
+                            }
+
+                            final String parts[] = version.split("\\.");
+                            if ( parts.length == 4 ) {
+                                final int lastDot = version.lastIndexOf('.');
+                                version = version.substring(0, lastDot) + '-' + version.substring(lastDot + 1);
+                            }
+
+                            if ( groupId != null && artifactId != null && version != null ) {
+                                return new ArtifactId(groupId,
+                                        artifactId,
+                                        version, classifier, null);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        throw new IOException(bundleFile.getName() + " has no maven coordinates!");
+    }
+
+    private Configuration process(final File configFile,
+            final Artifact packageArtifact,
+            final String contentPath)
+    throws IOException {
+
+        boolean isConfig = true;
+        if ( configFile.getName().endsWith(".xml") ) {
+            final String contents = Files.readAllLines(configFile.toPath()).toString();
+            if ( contents.indexOf("jcr:primaryType=\"sling:OsgiConfig\"") == -1 ) {
+                isConfig = false;
+            }
+        }
+
+        if ( isConfig ) {
+            final String id;
+            if ( ".content.xml".equals(configFile.getName()) ) {
+                final int lastSlash = contentPath.lastIndexOf('/');
+                final int previousSlash = contentPath.lastIndexOf('/', lastSlash - 1);
+                id = contentPath.substring(previousSlash + 1, lastSlash);
+            } else {
+                final int lastDot = configFile.getName().lastIndexOf('.');
+                id = configFile.getName().substring(0, lastDot);
+            }
+
+            final String pid, factoryPid;
+            final int slashPos = id.indexOf('-');
+            if ( slashPos == -1 ) {
+                pid = id;
+                factoryPid = null;
+            } else {
+                pid = id.substring(slashPos + 1);
+                factoryPid = id.substring(0, slashPos);
+            }
+
+            final Configuration cfg;
+            if ( factoryPid != null ) {
+                cfg = new Configuration(factoryPid, pid);
+            } else {
+                cfg = new Configuration(pid);
+            }
+            cfg.getProperties().put("content-path", contentPath);
+            cfg.getProperties().put("content-package", packageArtifact.getId().toMvnId());
+
+            return cfg;
+        }
+
+        return null;
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackagesExtensionScanner.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackagesExtensionScanner.java
new file mode 100644
index 0000000..5c91554
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackagesExtensionScanner.java
@@ -0,0 +1,73 @@
+/*
+ * 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.scanner.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Set;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.analyser.ContainerDescriptor;
+import org.apache.sling.feature.scanner.ExtensionScanner;
+import org.apache.sling.feature.support.ArtifactManager;
+
+public class ContentPackagesExtensionScanner implements ExtensionScanner {
+
+    @Override
+    public String getId() {
+        return "content-packages";
+    }
+
+    @Override
+    public String getName() {
+        return "Content Packages Scanner";
+    }
+
+    @Override
+    public ContainerDescriptor scan(final Extension extension,
+            final ArtifactManager artifactManager)
+    throws IOException {
+        if (!Extension.NAME_CONTENT_PACKAGES.equals(extension.getName()) ) {
+            return null;
+        }
+        if ( extension.getType() != ExtensionType.ARTIFACTS ) {
+            return null;
+        }
+
+        final ContentPackageScanner scanner = new ContentPackageScanner();
+        final ContainerDescriptor cd = new ContainerDescriptor() {};
+
+        for(final Artifact a : extension.getArtifacts()) {
+            final File file = artifactManager.getArtifactHandler(a.getId().toMvnUrl()).getFile();
+            if ( file == null ) {
+                throw new IOException("Unable to find file for " + a.getId());
+            }
+
+            final Set<ContentPackageDescriptor> pcks = scanner.scan(a, file);
+            for(final ContentPackageDescriptor desc : pcks) {
+                cd.getArtifactDescriptors().add(desc);
+                cd.getBundleDescriptors().addAll(desc.bundles);
+            }
+        }
+
+        cd.lock();
+
+        return cd;
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java
new file mode 100644
index 0000000..eb2124a
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java
@@ -0,0 +1,183 @@
+/*
+ * 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.scanner.impl;
+
+import static org.apache.sling.feature.support.util.LambdaUtil.rethrowFunction;
+import static org.apache.sling.feature.support.util.ManifestParser.convertProvideCapabilities;
+import static org.apache.sling.feature.support.util.ManifestParser.normalizeCapabilityClauses;
+import static org.apache.sling.feature.support.util.ManifestParser.parseStandardHeader;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.jar.Manifest;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.apache.sling.commons.osgi.ManifestHeader;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Capability;
+import org.apache.sling.feature.KeyValueMap;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.scanner.FrameworkScanner;
+import org.apache.sling.feature.support.util.PackageInfo;
+import org.apache.sling.feature.support.util.SubstVarUtil;
+import org.osgi.framework.Constants;
+
+public class FelixFrameworkScanner implements FrameworkScanner {
+
+
+    @Override
+    public BundleDescriptor scan(final ArtifactId framework,
+            final File platformFile,
+            final KeyValueMap frameworkProps)
+    throws IOException {
+        final KeyValueMap fwkProps = getFrameworkProperties(frameworkProps, platformFile);
+        if ( fwkProps == null ) {
+            return null;
+        }
+        final Set<PackageInfo> pcks = calculateSystemPackages(fwkProps);
+        final Set<Capability> capabilities = calculateSystemCapabilities(fwkProps);
+
+        final BundleDescriptor d = new BundleDescriptor() {
+
+            @Override
+            public String getBundleSymbolicName() {
+                return Constants.SYSTEM_BUNDLE_SYMBOLICNAME;
+            }
+
+            @Override
+            public String getBundleVersion() {
+                return framework.getOSGiVersion().toString();
+            }
+
+            @Override
+            public int getBundleStartLevel() {
+                return 0;
+            }
+
+            @Override
+            public File getArtifactFile() {
+                return platformFile;
+            }
+
+            @Override
+            public Artifact getArtifact() {
+                return new Artifact(framework);
+            }
+
+            @Override
+            public Manifest getManifest() {
+                return new Manifest();
+            }
+        };
+        d.getCapabilities().addAll(capabilities);
+        d.getExportedPackages().addAll(pcks);
+        d.lock();
+        return d;
+    }
+
+    private Set<Capability> calculateSystemCapabilities(final KeyValueMap fwkProps) {
+        return Stream.of(
+                    fwkProps.get(Constants.FRAMEWORK_SYSTEMCAPABILITIES),
+                    fwkProps.get(Constants.FRAMEWORK_SYSTEMCAPABILITIES_EXTRA)
+                )
+                .filter(Objects::nonNull)
+                .flatMap(
+                        rethrowFunction(header ->
+                            convertProvideCapabilities(normalizeCapabilityClauses(parseStandardHeader(header), "2")).stream()
+                )).collect(Collectors.toSet());
+    }
+
+    private Set<PackageInfo> calculateSystemPackages(final KeyValueMap fwkProps) {
+        final String system = fwkProps.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
+        final String extra = fwkProps.get(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA);
+        final Set<PackageInfo> packages = new HashSet<>();
+        for(int i=0;i<2;i++) {
+            final String value = (i == 0 ? system : extra);
+            if ( value != null ) {
+                final ManifestHeader header = ManifestHeader.parse(value);
+                for(final ManifestHeader.Entry entry : header.getEntries()) {
+                    String version = entry.getAttributeValue("version");
+                    if ( version == null ) {
+                        version = "0.0.0";
+                    }
+
+                    final PackageInfo exportedPackageInfo = new PackageInfo(entry.getValue(),
+                            version, false);
+                    packages.add(exportedPackageInfo);
+                }
+            }
+        }
+        return packages;
+    }
+
+    private static final String DEFAULT_PROPERTIES = "default.properties";
+
+    private KeyValueMap getFrameworkProperties(final KeyValueMap appProps, final File framework)
+    throws IOException {
+        final Map<String, Properties> propsMap = new HashMap<>();
+        try (final ZipInputStream zis = new ZipInputStream(new FileInputStream(framework)) ) {
+            boolean done = false;
+            while ( !done ) {
+                final ZipEntry entry = zis.getNextEntry();
+                if ( entry == null ) {
+                    done = true;
+                } else {
+                    final String entryName = entry.getName();
+                    if ( entryName.endsWith(".properties") ) {
+                        final Properties props = new Properties();
+                        props.load(zis);
+
+                        propsMap.put(entryName, props);
+                    }
+                    zis.closeEntry();
+                }
+            }
+        }
+
+        final Properties defaultMap = propsMap.get(DEFAULT_PROPERTIES);
+        if ( defaultMap == null ) {
+            return null;
+        }
+
+        final KeyValueMap frameworkProps = new KeyValueMap();
+        frameworkProps.putAll(appProps);
+
+        // replace variables
+        defaultMap.put("java.specification.version",
+                System.getProperty("java.specification.version", "1.8"));
+        for(final Object name : defaultMap.keySet()) {
+            if ( frameworkProps.get(name.toString()) == null ) {
+                final String value = (String)defaultMap.get(name);
+                final String substValue = SubstVarUtil.substVars(value, name.toString(), null, (Map) defaultMap);
+                frameworkProps.put(name.toString(), substValue);
+            }
+        }
+
+        return frameworkProps;
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/RepoInitScanner.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/RepoInitScanner.java
new file mode 100644
index 0000000..7b4e221
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/impl/RepoInitScanner.java
@@ -0,0 +1,62 @@
+/*
+ * 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.scanner.impl;
+
+import java.io.IOException;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Requirement;
+import org.apache.sling.feature.analyser.ContainerDescriptor;
+import org.apache.sling.feature.scanner.ExtensionScanner;
+import org.apache.sling.feature.support.ArtifactManager;
+
+public class RepoInitScanner implements ExtensionScanner {
+
+    @Override
+    public String getId() {
+        return "repoinit";
+    }
+
+    @Override
+    public String getName() {
+        return "Apache Sling Repoinit Scanner";
+    }
+
+    @Override
+    public ContainerDescriptor scan(final Extension extension,
+            final ArtifactManager artifactManager)
+    throws IOException {
+        if (!Extension.NAME_REPOINIT.equals(extension.getName()) ) {
+            return null;
+        }
+        if ( extension.getType() != ExtensionType.TEXT ) {
+            return null;
+        }
+
+        final ContainerDescriptor cd = new ContainerDescriptor() {};
+
+        final Requirement req = new Requirement("osgi.implementation");
+        req.getDirectives().put("filter",
+                "(&(osgi.implementation=org.apache.sling.jcr.repoinit)(version>=1.0)(!(version>=2.0)))");
+        cd.getRequirements().add(req);
+
+        cd.lock();
+
+        return cd;
+    }
+}
diff --git a/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/package-info.java b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/package-info.java
new file mode 100644
index 0000000..4c65dc3
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/java/org/apache/sling/feature/scanner/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.scanner;
+
+
diff --git a/featuremodel/feature-analyser/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask b/featuremodel/feature-analyser/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
new file mode 100644
index 0000000..dabd8fb
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
@@ -0,0 +1,5 @@
+org.apache.sling.feature.analyser.task.impl.CheckBundleExportsImports
+org.apache.sling.feature.analyser.task.impl.CheckBundlesForInitialContent
+org.apache.sling.feature.analyser.task.impl.CheckBundlesForResources
+org.apache.sling.feature.analyser.task.impl.CheckRequirementsCapabilities
+
diff --git a/featuremodel/feature-analyser/src/main/resources/META-INF/services/org.apache.sling.feature.scanner.ExtensionScanner b/featuremodel/feature-analyser/src/main/resources/META-INF/services/org.apache.sling.feature.scanner.ExtensionScanner
new file mode 100644
index 0000000..643c7cf
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/resources/META-INF/services/org.apache.sling.feature.scanner.ExtensionScanner
@@ -0,0 +1,3 @@
+org.apache.sling.feature.scanner.impl.ContentPackagesExtensionScanner
+org.apache.sling.feature.scanner.impl.RepoInitScanner
+
diff --git a/featuremodel/feature-analyser/src/main/resources/META-INF/services/org.apache.sling.feature.scanner.FrameworkScanner b/featuremodel/feature-analyser/src/main/resources/META-INF/services/org.apache.sling.feature.scanner.FrameworkScanner
new file mode 100644
index 0000000..ad10c99
--- /dev/null
+++ b/featuremodel/feature-analyser/src/main/resources/META-INF/services/org.apache.sling.feature.scanner.FrameworkScanner
@@ -0,0 +1,2 @@
+org.apache.sling.feature.scanner.impl.FelixFrameworkScanner
+
diff --git a/featuremodel/feature-analyser/src/test/java/org/apache/sling/feature/analyser/AnalyserTest.java b/featuremodel/feature-analyser/src/test/java/org/apache/sling/feature/analyser/AnalyserTest.java
new file mode 100644
index 0000000..872ec3c
--- /dev/null
+++ b/featuremodel/feature-analyser/src/test/java/org/apache/sling/feature/analyser/AnalyserTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.analyser;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.service.Analyser;
+import org.apache.sling.feature.analyser.service.Scanner;
+import org.apache.sling.feature.support.ArtifactManager;
+import org.apache.sling.feature.support.ArtifactManagerConfig;
+import org.apache.sling.feature.support.FeatureUtil;
+import org.apache.sling.feature.support.json.FeatureJSONReader;
+import org.junit.Test;
+
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import static junit.framework.TestCase.fail;
+
+public class AnalyserTest {
+    @Test
+    public void testAnalyserWithCompleteFeature() throws Exception {
+        final Scanner scanner = new Scanner(new ArtifactManagerConfig());
+        final Analyser analyser = new Analyser(scanner);
+        try ( final Reader reader = new InputStreamReader(AnalyserTest.class.getResourceAsStream("/feature_complete.json"),
+                "UTF-8") ) {
+            Feature feature = FeatureJSONReader.read(reader, "feature");
+
+            Application app = FeatureUtil.assembleApplication(null, ArtifactManager.getArtifactManager(new ArtifactManagerConfig()), feature);
+
+            analyser.analyse(app);
+        }
+    }
+
+    @Test
+    public void testAnalyserWithInCompleteFeature() throws Exception {
+        final Scanner scanner = new Scanner(new ArtifactManagerConfig());
+        final Analyser analyser = new Analyser(scanner);
+        try ( final Reader reader = new InputStreamReader(AnalyserTest.class.getResourceAsStream("/feature_incomplete.json"),
+                "UTF-8") ) {
+            Feature feature = FeatureJSONReader.read(reader, "feature");
+
+            Application app = FeatureUtil.assembleApplication(null, ArtifactManager.getArtifactManager(new ArtifactManagerConfig()), feature);
+
+            try {
+                analyser.analyse(app);
+
+                fail("Expected an exception");
+            }
+            catch (Exception ex) {
+                // Pass
+            }
+        }
+    }
+}
diff --git a/featuremodel/feature-analyser/src/test/resources/feature_complete.json b/featuremodel/feature-analyser/src/test/resources/feature_complete.json
new file mode 100644
index 0000000..692ddfe
--- /dev/null
+++ b/featuremodel/feature-analyser/src/test/resources/feature_complete.json
@@ -0,0 +1,31 @@
+{
+    "id" : "test/test.complete/0.1",
+
+    "bundles" : {
+      "1" : [
+        "org.apache.sling/org.apache.sling.commons.log/5.0.0",
+        "org.apache.sling/org.apache.sling.commons.logservice/1.0.6",
+        "org.slf4j/jcl-over-slf4j/1.7.21",
+        "org.slf4j/log4j-over-slf4j/1.7.21",
+        "org.slf4j/slf4j-api/1.7.21",
+        "org.apache.felix/org.apache.felix.configadmin/1.8.14"
+      ],
+      "4" : [
+        "org.apache.felix/org.apache.felix.eventadmin/1.4.8",
+        "org.apache.felix/org.apache.felix.metatype/1.1.2",
+        "org.apache.felix/org.apache.felix.scr/2.0.12"
+      ],
+      "5" : [
+        "org.apache.felix/org.apache.felix.http.jetty/3.4.2",
+        "org.apache.felix/org.apache.felix.http.servlet-api/1.1.2",
+        "commons-io/commons-io/2.5",
+        "commons-fileupload/commons-fileupload/1.3.2",
+        "org.apache.felix/org.apache.felix.inventory/1.0.4",
+        "org.apache.felix/org.apache.felix.webconsole.plugins.ds/2.0.6",
+        "org.apache.felix/org.apache.felix.webconsole.plugins.event/1.1.6",
+        "org.apache.felix/org.apache.felix.webconsole.plugins.packageadmin/1.0.4",
+        "org.apache.felix/org.apache.felix.webconsole/4.3.4",
+        "org.apache.sling/org.apache.sling.commons.log.webconsole/1.0.0"
+      ]
+    }
+}
diff --git a/featuremodel/feature-analyser/src/test/resources/feature_incomplete.json b/featuremodel/feature-analyser/src/test/resources/feature_incomplete.json
new file mode 100644
index 0000000..fee0e7f
--- /dev/null
+++ b/featuremodel/feature-analyser/src/test/resources/feature_incomplete.json
@@ -0,0 +1,33 @@
+{
+    "id" : "test/test.incomplete/0.1",
+
+    "bundles" : {
+      "1" : [
+        "org.apache.sling/org.apache.sling.commons.log/5.0.0",
+        "org.apache.sling/org.apache.sling.commons.logservice/1.0.6",
+        "org.slf4j/jcl-over-slf4j/1.7.21",
+        "org.slf4j/log4j-over-slf4j/1.7.21",
+        "org.slf4j/slf4j-api/1.7.21",
+        "org.apache.felix/org.apache.felix.configadmin/1.8.14"
+      ],
+      "4" : [
+        "org.apache.felix/org.apache.felix.eventadmin/1.4.8",
+        "org.apache.felix/org.apache.felix.metatype/1.1.2",
+        "org.apache.felix/org.apache.felix.scr/2.0.12"
+      ],
+      "5" : [
+        "org.apache.felix/org.apache.felix.http.servlet-api/1.1.2",
+        "commons-io/commons-io/2.5",
+        "commons-fileupload/commons-fileupload/1.3.2",
+        "org.apache.felix/org.apache.felix.inventory/1.0.4",
+        "org.apache.felix/org.apache.felix.webconsole.plugins.ds/2.0.6",
+        "org.apache.felix/org.apache.felix.webconsole.plugins.event/1.1.6",
+        "org.apache.felix/org.apache.felix.webconsole.plugins.packageadmin/1.0.4",
+        "org.apache.felix/org.apache.felix.webconsole/4.3.4",
+        "org.apache.sling/org.apache.sling.commons.log.webconsole/1.0.0"
+      ],
+      "6" : [
+        "org.apache.sling/org.apache.sling.i18n/2.5.8"
+      ]
+    }
+}
diff --git a/featuremodel/feature-applicationbuilder/pom.xml b/featuremodel/feature-applicationbuilder/pom.xml
new file mode 100644
index 0000000..c30dc54
--- /dev/null
+++ b/featuremodel/feature-applicationbuilder/pom.xml
@@ -0,0 +1,127 @@
+<?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.applicationbuilder</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+
+    <name>Apache Sling Feature Application Builder</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-applicationbuilder</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/tooling/support/feature-applicationbuilder</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/tooling/support/feature-applicationbuilder</url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                <execution>
+                    <id>unpack-dependencies</id>
+                    <phase>prepare-package</phase>
+                    <goals>
+                        <goal>unpack-dependencies</goal>
+                    </goals>
+                    <configuration>
+                        <excludes>META-INF/**</excludes>
+                        <outputDirectory>${project.build.directory}/classes</outputDirectory>
+                        <overWriteReleases>false</overWriteReleases>
+                        <overWriteSnapshots>true</overWriteSnapshots>
+                        <includeArtifactIds>commons-cli,org.apache.sling.feature,org.apache.sling.feature.support,org.apache.sling.commons.johnzon,slf4j-api,slf4j-simple,osgi.core,org.apache.felix.converter</includeArtifactIds>
+                    </configuration>
+                </execution>
+            </executions>
+            </plugin>
+                <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.sling.feature.applicationbuilder.impl.Main</mainClass>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+             <groupId>commons-cli</groupId>
+             <artifactId>commons-cli</artifactId>
+             <version>1.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.support</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.johnzon</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.converter</artifactId>
+            <version>0.1.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+      <!-- Testing -->
+        <dependency>
+        	<groupId>junit</groupId>
+        	<artifactId>junit</artifactId>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/featuremodel/feature-applicationbuilder/src/main/java/org/apache/sling/feature/applicationbuilder/impl/Main.java b/featuremodel/feature-applicationbuilder/src/main/java/org/apache/sling/feature/applicationbuilder/impl/Main.java
new file mode 100644
index 0000000..80c609d
--- /dev/null
+++ b/featuremodel/feature-applicationbuilder/src/main/java/org/apache/sling/feature/applicationbuilder/impl/Main.java
@@ -0,0 +1,213 @@
+/*
+ * 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.applicationbuilder.impl;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Extensions;
+import org.apache.sling.feature.KeyValueMap;
+import org.apache.sling.feature.support.ArtifactHandler;
+import org.apache.sling.feature.support.ArtifactManager;
+import org.apache.sling.feature.support.ArtifactManagerConfig;
+import org.apache.sling.feature.support.FeatureUtil;
+import org.apache.sling.feature.support.json.ApplicationJSONReader;
+import org.apache.sling.feature.support.json.ApplicationJSONWriter;
+import org.apache.sling.feature.support.json.FeatureJSONWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Main {
+
+    private static Logger LOGGER;
+
+    private static String output;
+
+    private static String filesInput;
+
+    private static String dirsInput;
+
+    private static String repoUrls;
+
+    private static String propsFile;
+
+    /**
+     * Parse the command line parameters and update a configuration object.
+     * @param args Command line parameters
+     * @return Configuration object.
+     */
+    private static void parseArgs(final String[] args) {
+        final Option repoOption =  Option.builder("u").hasArg().argName("Set repository url")
+                .desc("repository url").build();
+
+        final Option filesOption =  new Option("f", true, "Set feature files (comma separated)");
+        final Option dirsOption = new Option("d", true, "Set feature file dirs (comma separated)");
+        final Option propsOption =  new Option("p", true, "sling.properties file");
+
+
+        final Option outputOption = Option.builder("o").hasArg().argName("Set output file")
+                .desc("output file").build();
+
+        final Options options = new Options();
+        options.addOption(repoOption);
+        options.addOption(filesOption);
+        options.addOption(dirsOption);
+        options.addOption(outputOption);
+        options.addOption(propsOption);
+
+        final CommandLineParser parser = new DefaultParser();
+        try {
+            final CommandLine cl = parser.parse(options, args);
+
+            if ( cl.hasOption(repoOption.getOpt()) ) {
+                repoUrls = cl.getOptionValue(repoOption.getOpt());
+            }
+            if ( cl.hasOption(filesOption.getOpt()) ) {
+                filesInput = cl.getOptionValue(filesOption.getOpt());
+            }
+            if ( cl.hasOption(dirsOption.getOpt()) ) {
+                dirsInput = cl.getOptionValue(dirsOption.getOpt());
+            }
+            if ( cl.hasOption(outputOption.getOpt()) ) {
+                output = cl.getOptionValue(outputOption.getOpt());
+            }
+            if ( cl.hasOption(propsOption.getOpt()) ) {
+                propsFile = cl.getOptionValue(propsOption.getOpt());
+            }
+        } catch ( final ParseException pe) {
+            LOGGER.error("Unable to parse command line: {}", pe.getMessage(), pe);
+            System.exit(1);
+        }
+        if ( filesInput == null && dirsInput == null) {
+            LOGGER.error("Required argument missing: model files or directory");
+            System.exit(1);
+        }
+    }
+
+    private static ArtifactManager getArtifactManager() {
+        final ArtifactManagerConfig amConfig = new ArtifactManagerConfig();
+        if ( repoUrls != null ) {
+            amConfig.setRepositoryUrls(repoUrls.split(","));
+        }
+        try {
+            return ArtifactManager.getArtifactManager(amConfig);
+        } catch ( IOException ioe) {
+            LOGGER.error("Unable to create artifact manager " + ioe.getMessage(), ioe);
+            System.exit(1);
+        }
+        // we never reach this, but have to keep the compiler happy
+        return null;
+    }
+
+    public static void main(final String[] args) {
+        // setup logging
+        System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
+        System.setProperty("org.slf4j.simpleLogger.showThreadName", "false");
+        System.setProperty("org.slf4j.simpleLogger.levelInBrackets", "true");
+        System.setProperty("org.slf4j.simpleLogger.showLogName", "false");
+
+        LOGGER = LoggerFactory.getLogger("applicationbuilder");
+
+        LOGGER.info("Apache Sling Feature Application Builder");
+        LOGGER.info("");
+
+        parseArgs(args);
+
+        final ArtifactManagerConfig amConfig = new ArtifactManagerConfig();
+        if ( repoUrls != null ) {
+            amConfig.setRepositoryUrls(repoUrls.split(","));
+        }
+
+        final ArtifactManager am = getArtifactManager();
+
+
+        final String[] files =
+                Stream.concat(
+                    Stream.of(filesInput != null ? filesInput.split(",") : new String[0])
+                        .map(path -> new File(path))
+                        .filter(File::isFile),
+                    Stream.of(dirsInput != null ? dirsInput.split(",") : new String[0])
+                        .map(path -> new File(path))
+                        .filter(File::isDirectory)
+                        .flatMap(dir ->
+                            Stream.of(dir.listFiles())))
+                .filter(file -> !file.getName().startsWith("."))
+                .sorted()
+                .map(File::getAbsolutePath)
+                .toArray(String[]::new);
+
+        if (files.length == 0) {
+            LOGGER.error("No feature files found.");
+            System.exit(1);
+        }
+
+        try {
+            writeApplication(buildApplication(FeatureUtil.assembleApplication(null, am, files)), output == null ? "application.json" : output);
+
+        } catch ( final IOException ioe) {
+            LOGGER.error("Unable to read feature/application files " + ioe.getMessage(), ioe);
+            System.exit(1);
+        }
+    }
+
+    private static Application buildApplication(final Application app) {
+        app.getBundles().add(1, new org.apache.sling.feature.Artifact(ArtifactId.fromMvnId("org.apache.sling/org.apache.sling.launchpad.api/1.2.0")));
+        // sling.properties (TODO)
+        if ( propsFile == null ) {
+            app.getFrameworkProperties().put("org.osgi.framework.bootdelegation", "sun.*,com.sun.*");
+        } else {
+
+        }
+        // felix framework hard coded for now
+        app.setFramework(FeatureUtil.getFelixFrameworkId(null));
+        return app;
+    }
+
+    private static void writeApplication(final Application app, final String out) {
+        LOGGER.info("Writing application...");
+        final File file = new File(out);
+        try ( final FileWriter writer = new FileWriter(file)) {
+            ApplicationJSONWriter.write(writer, app);
+        } catch ( final IOException ioe) {
+            LOGGER.error("Unable to write application to {} : {}", out, ioe.getMessage(), ioe);
+            System.exit(1);
+        }
+    }
+}
diff --git a/featuremodel/feature-karaf/pom.xml b/featuremodel/feature-karaf/pom.xml
new file mode 100644
index 0000000..4591599
--- /dev/null
+++ b/featuremodel/feature-karaf/pom.xml
@@ -0,0 +1,72 @@
+<?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.karaf</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+
+    <name>Apache Sling Feature Karaf</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-karaf</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/tooling/support/feature-karaf</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/tooling/support/feature-karaf</url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+            </plugin>
+        </plugins>
+    </build>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.support</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+            <version>6.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/featuremodel/feature-karaf/src/main/java/org/apache/sling/feature/karaf/KarafFeatureWriter.java b/featuremodel/feature-karaf/src/main/java/org/apache/sling/feature/karaf/KarafFeatureWriter.java
new file mode 100644
index 0000000..7402f04
--- /dev/null
+++ b/featuremodel/feature-karaf/src/main/java/org/apache/sling/feature/karaf/KarafFeatureWriter.java
@@ -0,0 +1,195 @@
+/*
+ * 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.karaf;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+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.support.ArtifactHandler;
+import org.apache.sling.feature.support.ArtifactManager;
+import org.apache.sling.feature.support.ConfigurationUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+
+/**
+ * This writer writes out a Karaf feature XML.
+ *
+ */
+public class KarafFeatureWriter {
+
+    public static void writeKAR(final OutputStream os,
+            final Feature feature,
+            final ArtifactManager artifactManager)
+    throws IOException {
+        // check for repoinit extension
+        final Extension repoinitExt = feature.getExtensions().getByName(Extension.NAME_REPOINIT);
+        File configurationBundleFile = null;
+        Artifact configuratorBundle = null;
+        try {
+            final Configurations configs = new Configurations();
+            configs.addAll(feature.getConfigurations());
+            if ( repoinitExt != null ) {
+                final Configuration cfg = new Configuration("org.apache.sling.jcr.repoinit.RepositoryInitializer",
+                        "feature-" + feature.getId().getArtifactId());
+                cfg.getProperties().put("scripts", repoinitExt.getText());
+                configs.add(cfg);
+            }
+            if ( !configs.isEmpty() ) {
+                configurationBundleFile = Files.createTempFile(null, null).toFile();
+
+                try ( final FileOutputStream fos = new FileOutputStream(configurationBundleFile)) {
+                    final Map<String, String> map;
+                    if ( repoinitExt == null ) {
+                        map = null;
+                    } else {
+                        map = Collections.singletonMap("Require-Capability",
+                                ConfigurationUtil.REQUIRE_REPOINIT_CAPABILITY);
+                    }
+                    ConfigurationUtil.createConfiguratorBundle(os,
+                            configs,
+                            feature.getId().getGroupId() + "." + feature.getId().getArtifactId(),
+                            feature.getId().getOSGiVersion().toString(),
+                            map);
+                    configuratorBundle = new Artifact(new ArtifactId(feature.getId().getGroupId(),
+                            feature.getId().getArtifactId(),
+                            feature.getId().getVersion(), "configurator", null));
+                }
+            }
+
+            try ( final ZipOutputStream jos = new ZipOutputStream(os) ) {
+                // repository/features.xml
+                // repository/{maven-path-to-bundle}
+                final ZipEntry xmlEntry = new ZipEntry("repository/features.xml");
+                jos.putNextEntry(xmlEntry);
+
+                final Writer writer = new OutputStreamWriter(jos);
+                writeFeaturesXML(writer, feature, configuratorBundle);
+                writer.flush();
+
+                jos.closeEntry();
+
+                for(final Map.Entry<Integer, List<Artifact>> entry : feature.getBundles().getBundlesByStartLevel().entrySet()) {
+                    for(final Artifact artifact : entry.getValue()) {
+                        final ArtifactHandler handler = artifactManager.getArtifactHandler(artifact.getId().toMvnUrl());
+
+                        addEntry(jos, artifact, handler.getFile());
+                    }
+                }
+
+                if ( configuratorBundle != null ) {
+                    addEntry(jos, configuratorBundle, configurationBundleFile);
+                }
+            }
+        } finally {
+            if ( configurationBundleFile != null ) {
+                configurationBundleFile.delete();
+            }
+        }
+    }
+
+    private static void addEntry(final ZipOutputStream jos, final Artifact artifact, final File file)
+    throws IOException {
+        final ZipEntry bundleEntry = new ZipEntry("repository/" + artifact.getId().toMvnPath());
+        jos.putNextEntry(bundleEntry);
+
+        final byte[] buffer = new byte[16384];
+        try ( final FileInputStream fis = new FileInputStream(file)) {
+            int l = 0;
+            while ( (l = fis.read(buffer)) > 0 ) {
+                jos.write(buffer, 0, l);
+            }
+        }
+
+        jos.closeEntry();
+    }
+
+    /**
+     * Writes the feature XML to the writer.
+     * The writer is not closed.
+     * @param writer Writer
+     * @param model Model
+     * @throws IOException
+     */
+    private static void writeFeaturesXML(final Writer writer,
+            final Feature feature,
+            final Artifact configuratorBundle)
+    throws IOException {
+        final PrintWriter w = new PrintWriter(writer);
+
+        w.print("<features name=\"");
+        w.print(feature.getId().getArtifactId());
+        w.print("-repo-");
+        w.print(feature.getId().getVersion());
+        w.println("\" xmlns=\"http://karaf.apache.org/xmlns/features/v1.4.0\">");
+
+        write(w, feature, configuratorBundle);
+
+        w.println("</features>");
+        w.flush();
+    }
+
+    private static void write(final PrintWriter w, final Feature feature, final Artifact configuratorBundle)
+    throws IOException {
+        w.print("  <feature name=\"");
+        w.print(feature.getId().getGroupId());
+        w.print('.');
+        w.print(feature.getId().getArtifactId());
+        if ( feature.getId().getClassifier() != null ) {
+            w.print('.');
+            w.print(feature.getId().getClassifier());
+        }
+        w.print("\" version=\"");
+        w.print(feature.getId().getVersion());
+        w.println("\">");
+
+        if ( configuratorBundle != null ) {
+            w.print("     <bundle start-level=\"1\">");
+            w.print(configuratorBundle.getId().toMvnUrl());
+            w.println("</bundle>");
+        }
+
+        // bundles
+        for(final Map.Entry<Integer, List<Artifact>> entry : feature.getBundles().getBundlesByStartLevel().entrySet()) {
+            for(final Artifact artifact : entry.getValue()) {
+                w.print("     <bundle start-level=\"");
+                w.print(entry.getKey().toString());
+                w.print("\">");
+                w.print(artifact.getId().toMvnUrl());
+                w.println("</bundle>");
+            }
+        }
+
+        w.println("  </feature>");
+    }
+}
diff --git a/featuremodel/feature-karaf/src/main/java/org/apache/sling/feature/karaf/package-info.java b/featuremodel/feature-karaf/src/main/java/org/apache/sling/feature/karaf/package-info.java
new file mode 100644
index 0000000..76136a3
--- /dev/null
+++ b/featuremodel/feature-karaf/src/main/java/org/apache/sling/feature/karaf/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.karaf;
+
+
diff --git a/featuremodel/feature-launcher/pom.xml b/featuremodel/feature-launcher/pom.xml
new file mode 100644
index 0000000..6bf9943
--- /dev/null
+++ b/featuremodel/feature-launcher/pom.xml
@@ -0,0 +1,128 @@
+<?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.launcher</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+
+    <name>Apache Sling Feature Launcher</name>
+    <description>
+        A application launcher using Apache Sling Features
+    </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-launcher</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/tooling/support/feature-launcher</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/tooling/support/feature-launcher</url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                <execution>
+                    <id>unpack-dependencies</id>
+                    <phase>prepare-package</phase>
+                    <goals>
+                        <goal>unpack-dependencies</goal>
+                    </goals>
+                    <configuration>
+                        <excludes>META-INF/**</excludes>
+                        <outputDirectory>${project.build.directory}/classes</outputDirectory>
+                        <overWriteReleases>false</overWriteReleases>
+                        <overWriteSnapshots>true</overWriteSnapshots>
+                        <includeArtifactIds>org.apache.sling.feature,org.apache.sling.feature.support,org.apache.sling.commons.johnzon,org.apache.felix.converter,commons-cli,slf4j-api,slf4j-simple,osgi.core</includeArtifactIds>
+                    </configuration>
+                </execution>
+            </executions>
+            </plugin>
+                <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.sling.feature.launcher.impl.Main</mainClass>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.support</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.converter</artifactId>
+            <version>0.1.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+             <groupId>commons-cli</groupId>
+             <artifactId>commons-cli</artifactId>
+             <version>1.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.johnzon</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>2.8.9</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/FeatureProcessor.java b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/FeatureProcessor.java
new file mode 100644
index 0000000..14eaf9c
--- /dev/null
+++ b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/FeatureProcessor.java
@@ -0,0 +1,137 @@
+/*
+ * 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.launcher.impl;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.launcher.impl.LauncherConfig.StartupMode;
+import org.apache.sling.feature.support.ArtifactHandler;
+import org.apache.sling.feature.support.ArtifactManager;
+import org.apache.sling.feature.support.FeatureUtil;
+import org.apache.sling.feature.support.json.ApplicationJSONReader;
+import org.apache.sling.feature.support.json.ApplicationJSONWriter;
+
+public class FeatureProcessor {
+
+    /**
+     * Initialize the launcher
+     * Read the features and prepare the application
+     * @param config The current configuration
+     * @param artifactManager The artifact manager
+     */
+    public static Application createApplication(final LauncherConfig config,
+            final ArtifactManager artifactManager)
+    throws IOException {
+        final Application app;
+        if ( config.getApplicationFile() != null ) {
+            String absoluteArg = config.getApplicationFile();
+            if ( absoluteArg.indexOf(":") < 2 ) {
+                absoluteArg = new File(absoluteArg).getAbsolutePath();
+            }
+            final ArtifactHandler appArtifact = artifactManager.getArtifactHandler(absoluteArg);
+
+            try (final FileReader r = new FileReader(appArtifact.getFile())) {
+                app = ApplicationJSONReader.read(r);
+            }
+
+        } else {
+           app = FeatureUtil.assembleApplication(null, artifactManager, FeatureUtil.getFeatureFiles(config.getHomeDirectory(), config.getFeatureFiles()).toArray(new String[0]));
+        }
+
+        // write application back
+        final File file = new File(config.getHomeDirectory(), "resources" + File.separatorChar + "provisioning" + File.separatorChar + "application.json");
+        file.getParentFile().mkdirs();
+
+        try (final FileWriter writer = new FileWriter(file)) {
+            ApplicationJSONWriter.write(writer, app);
+        } catch ( final IOException ioe) {
+            Main.LOG().error("Error while writing application file: {}", ioe.getMessage(), ioe);
+            System.exit(1);
+        }
+
+        return app;
+    }
+
+    /**
+     * Prepare the launcher
+     * - add all bundles to the bundle map of the installation object
+     * - add all other artifacts to the install directory (only if startup mode is INSTALL)
+     * - process configurations
+     */
+    public static void prepareLauncher(final LauncherConfig config,
+            final ArtifactManager artifactManager,
+            final Application app) throws Exception {
+        for(final Map.Entry<Integer, List<Artifact>> entry : app.getBundles().getBundlesByStartLevel().entrySet()) {
+            for(final Artifact a : entry.getValue()) {
+                final ArtifactHandler handler = artifactManager.getArtifactHandler(":" + a.getId().toMvnPath());
+                final File artifactFile = handler.getFile();
+
+                config.getInstallation().addBundle(entry.getKey(), artifactFile);
+            }
+        }
+        int index = 1;
+        for(final Extension ext : app.getExtensions()) {
+            if ( ext.getType() == ExtensionType.ARTIFACTS ) {
+                for(final Artifact a : ext.getArtifacts() ) {
+                    if ( config.getStartupMode() == StartupMode.PURE ) {
+                        throw new Exception("Artifacts other than bundle are not supported by framework launcher.");
+                    }
+                    final ArtifactHandler handler = artifactManager.getArtifactHandler(":" + a.getId().toMvnPath());
+                    config.getInstallation().addInstallableArtifact(handler.getFile());
+                }
+            } else {
+                if ( ext.getName().equals(Extension.NAME_REPOINIT) ) {
+                    if ( ext.getType() != ExtensionType.TEXT ) {
+                        throw new Exception(Extension.NAME_REPOINIT + " extension must be of type text and not json");
+                    }
+                    final Configuration cfg = new Configuration("org.apache.sling.jcr.repoinit.RepositoryInitializer", "repoinit" + String.valueOf(index));
+                    index++;
+                    cfg.getProperties().put("scripts", ext.getText());
+                    config.getInstallation().addConfiguration(cfg.getName(), cfg.getFactoryPid(), cfg.getProperties());
+                } else {
+                    if ( ext.isRequired() ) {
+                        throw new Exception("Unknown required extension " + ext.getName());
+                    }
+                }
+            }
+        }
+
+        for(final Configuration cfg : app.getConfigurations()) {
+            if ( cfg.isFactoryConfiguration() ) {
+                config.getInstallation().addConfiguration(cfg.getName(), cfg.getFactoryPid(), cfg.getProperties());
+            } else {
+                config.getInstallation().addConfiguration(cfg.getPid(), null, cfg.getProperties());
+            }
+        }
+
+        for(final Map.Entry<String, String> prop : app.getFrameworkProperties()) {
+            if ( !config.getInstallation().getFrameworkProperties().containsKey(prop.getKey()) ) {
+                config.getInstallation().getFrameworkProperties().put(prop.getKey(), prop.getValue());
+            }
+        }
+    }
+}
diff --git a/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/Installation.java b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/Installation.java
new file mode 100644
index 0000000..2dff508
--- /dev/null
+++ b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/Installation.java
@@ -0,0 +1,137 @@
+/*
+ * 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.launcher.impl;
+
+import org.apache.sling.feature.launcher.spi.LauncherRunContext;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class holds the configuration of the launcher.
+ */
+public class Installation implements LauncherRunContext {
+
+    /** The map with the framework properties. */
+    private final Map<String, String> fwkProperties = new HashMap<>();
+
+    /** Bundle map */
+    private final Map<Integer, List<File>> bundleMap = new HashMap<>();
+
+    /** Artifacts to be installed */
+    private final List<File> installables = new ArrayList<>();
+
+    /** Configurations, they are installed on first start. */
+    private final List<Object[]> configurations = new ArrayList<>();
+
+    /** The list of app jars. */
+    private final List<File> appJars = new ArrayList<>();
+
+    /**
+     * Add an application jar.
+     * @param jar The application jar
+     */
+    public void addAppJar(final File jar) {
+        this.appJars.add(jar);
+    }
+
+    /**
+     * Get the list of application jars.
+     * @return The list of app jars
+     */
+    public List<File> getAppJars() {
+        return this.appJars;
+    }
+
+    /**
+     * Add a bundle with the given start level
+     * @param startLevel The start level
+     * @param file The bundle file
+     */
+    public void addBundle(final Integer startLevel, final File file) {
+        List<File> files = bundleMap.get(startLevel);
+        if ( files == null ) {
+            files = new ArrayList<>();
+            bundleMap.put(startLevel, files);
+        }
+        files.add(file);
+    }
+
+    /**
+     * Add an artifact to be installed by the installer
+     * @param file The file
+     */
+    public void addInstallableArtifact(final File file) {
+        this.installables.add(file);
+    }
+
+    /**
+     * Add a configuration
+     * @param pid The pid
+     * @param factoryPid The factory pid
+     * @param properties The propertis
+     */
+    public void addConfiguration(final String pid, final String factoryPid, final Dictionary<String, Object> properties) {
+        this.configurations.add(new Object[] {pid, factoryPid, properties});
+    }
+
+    /**
+     * @see org.apache.sling.feature.launcher.spi.LauncherRunContext#getFrameworkProperties()
+     */
+    @Override
+    public Map<String, String> getFrameworkProperties() {
+        return this.fwkProperties;
+    }
+
+    /**
+     * @see org.apache.sling.feature.launcher.spi.LauncherRunContext#getBundleMap()
+     */
+    @Override
+    public Map<Integer, List<File>> getBundleMap() {
+        return this.bundleMap;
+    }
+
+    /**
+     * @see org.apache.sling.feature.launcher.spi.LauncherRunContext#getConfigurations()
+     */
+    @Override
+    public List<Object[]> getConfigurations() {
+        return this.configurations;
+    }
+
+    /**
+     * @see org.apache.sling.feature.launcher.spi.LauncherRunContext#getInstallableArtifacts()
+     */
+    @Override
+    public List<File> getInstallableArtifacts() {
+        return this.installables;
+    }
+
+    /**
+     * Clear all in-memory objects
+     */
+    public void clear() {
+        this.configurations.clear();
+        this.fwkProperties.clear();
+        this.bundleMap.clear();
+        this.installables.clear();
+    }
+}
diff --git a/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/LauncherConfig.java b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/LauncherConfig.java
new file mode 100644
index 0000000..56095b9
--- /dev/null
+++ b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/LauncherConfig.java
@@ -0,0 +1,122 @@
+/*
+ * 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.launcher.impl;
+
+import org.apache.sling.feature.support.ArtifactManagerConfig;
+import org.apache.sling.feature.support.spi.ArtifactProviderContext;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * This class holds the configuration of the launcher.
+ */
+public class LauncherConfig
+    extends ArtifactManagerConfig
+    implements ArtifactProviderContext {
+
+    public enum StartupMode {
+        INSTALLER,
+        PURE
+    };
+
+    private static final String HOME = "launcher";
+
+    private static final String CACHE_DIR = "cache";
+
+    /** The feature files or directories. */
+    private volatile String[] featureFiles;
+
+    /** The application file. */
+    private volatile String appFile;
+
+    private volatile StartupMode startupMode = StartupMode.PURE;
+
+    private final Installation installation = new Installation();
+
+    /**
+     * Create a new configuration object.
+     * Set the default values
+     */
+    public LauncherConfig() {
+        this.setCacheDirectory(new File(getHomeDirectory(), CACHE_DIR));
+    }
+
+    public void setApplicationFile(final String value) {
+        appFile = value;
+    }
+
+    public String getApplicationFile() {
+        return this.appFile;
+    }
+
+    /**
+     * Set the list of feature files or directories.
+     * @param value The array with the feature file names.
+     */
+    public void setFeatureFiles(final String[] value) {
+        this.featureFiles = value;
+        if ( value != null && value.length == 0 ) {
+            this.featureFiles = null;
+        }
+    }
+
+    /**
+     * Get the list of feature files.
+     * @return The array of names.
+     * @throws IOException
+     */
+    public String[] getFeatureFiles() {
+        return this.featureFiles;
+    }
+
+
+    /**
+     * Get the home directory.
+     * @return The home directory.
+     */
+    public File getHomeDirectory() {
+        return new File(HOME);
+    }
+
+    /**
+     * Get the startup mode.
+     *
+     * @return The current startup mode.
+     */
+    public StartupMode getStartupMode() {
+        return this.startupMode;
+    }
+
+    /**
+     * Sets the startup mode to {@link StartupMode#INSTALLER}.
+     */
+    public void setUseInstaller() {
+        this.startupMode = StartupMode.INSTALLER;
+    }
+
+    public Installation getInstallation() {
+        return this.installation;
+    }
+
+    /**
+     * Clear all in-memory objects
+     */
+    public void clear() {
+        this.installation.clear();
+    }
+}
diff --git a/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/Main.java b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/Main.java
new file mode 100644
index 0000000..31050fe
--- /dev/null
+++ b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/Main.java
@@ -0,0 +1,313 @@
+/*
+ * 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.launcher.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.cli.BasicParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.launcher.impl.launchers.FrameworkLauncher;
+import org.apache.sling.feature.launcher.spi.Launcher;
+import org.apache.sling.feature.launcher.spi.LauncherPrepareContext;
+import org.apache.sling.feature.support.ArtifactHandler;
+import org.apache.sling.feature.support.ArtifactManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is the launcher main class.
+ * It parses command line parameters and prepares the launcher.
+ */
+public class Main {
+
+    private static Logger LOGGER;
+
+    public static Logger LOG() {
+        if ( LOGGER == null ) {
+            LOGGER = LoggerFactory.getLogger("launcher");
+        }
+        return LOGGER;
+    }
+
+    /** Split a string into key and value */
+    private static String[] split(final String val) {
+        final int pos = val.indexOf('=');
+        if ( pos == -1 ) {
+            return new String[] {val, "true"};
+        }
+        return new String[] {val.substring(0, pos), val.substring(pos + 1)};
+    }
+
+    /**
+     * Parse the command line parameters and update a configuration object.
+     * @param args Command line parameters
+     * @return Configuration object.
+     */
+    private static void parseArgs(final LauncherConfig config, final String[] args) {
+        Main.LOG().info("Assembling configuration...");
+        final Options options = new Options();
+
+        final Option repoOption =  new Option("u", true, "Set repository url");
+        final Option modelOption =  new Option("f", true, "Set feature files/directories");
+        final Option appOption =  new Option("a", true, "Set application file");
+        final Option fwkProperties = new Option("D", true, "Set framework properties");
+        fwkProperties.setArgs(20);
+        final Option debugOption = new Option("v", true, "Verbose");
+        debugOption.setArgs(0);
+        final Option installerOption = new Option("I", true, "Use OSGi installer for additional artifacts.");
+        installerOption.setArgs(0);
+        options.addOption(repoOption);
+        options.addOption(appOption);
+        options.addOption(modelOption);
+        options.addOption(fwkProperties);
+        options.addOption(debugOption);
+        options.addOption(installerOption);
+
+        final CommandLineParser clp = new BasicParser();
+        try {
+            final CommandLine cl = clp.parse(options, args);
+
+            if ( cl.hasOption(repoOption.getOpt()) ) {
+                final String value = cl.getOptionValue(repoOption.getOpt());
+                config.setRepositoryUrls(value.split(","));
+            }
+            if ( cl.hasOption(modelOption.getOpt()) ) {
+                final String value = cl.getOptionValue(modelOption.getOpt());
+                config.setFeatureFiles(value.split(","));
+            }
+            if ( cl.hasOption(fwkProperties.getOpt()) ) {
+                for(final String value : cl.getOptionValues(fwkProperties.getOpt())) {
+                    final String[] keyVal = split(value);
+
+                    config.getInstallation().getFrameworkProperties().put(keyVal[0], keyVal[1]);
+                }
+            }
+            if ( cl.hasOption(debugOption.getOpt()) ) {
+                System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
+                LOGGER = null;
+            }
+            if ( cl.hasOption(installerOption.getOpt()) ) {
+                config.setUseInstaller();
+            }
+            if ( cl.hasOption(appOption.getOpt()) ) {
+                config.setApplicationFile(cl.getOptionValue(appOption.getOpt()));
+            }
+        } catch ( final ParseException pe) {
+            Main.LOG().error("Unable to parse command line: {}", pe.getMessage(), pe);
+            System.exit(1);
+        }
+    }
+
+    public static void main(final String[] args) {
+        // setup logging
+        System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
+        System.setProperty("org.slf4j.simpleLogger.showThreadName", "false");
+        System.setProperty("org.slf4j.simpleLogger.levelInBrackets", "true");
+        System.setProperty("org.slf4j.simpleLogger.showLogName", "false");
+        Main.LOG().info("");
+        Main.LOG().info("Apache Sling Application Launcher");
+        Main.LOG().info("---------------------------------");
+
+        // check if launcher has already been created
+        final LauncherConfig launcherConfig = new LauncherConfig();
+        parseArgs(launcherConfig, args);
+
+        ArtifactManager artifactManager = null;
+        try {
+
+            Main.LOG().info("Initializing...");
+            try {
+                artifactManager = ArtifactManager.getArtifactManager(launcherConfig);
+            } catch ( final IOException ioe) {
+                Main.LOG().error("Unable to setup artifact manager: {}", ioe.getMessage(), ioe);
+                System.exit(1);
+            }
+            Main.LOG().info("Artifact Repositories: {}", Arrays.toString(launcherConfig.getRepositoryUrls()));
+            Main.LOG().info("Assembling provisioning model...");
+
+            try {
+                final Launcher launcher = new FrameworkLauncher();
+                final Application app = FeatureProcessor.createApplication(launcherConfig, artifactManager);
+
+                Main.LOG().info("");
+                Main.LOG().info("Assembling launcher...");
+                final ArtifactManager aMgr = artifactManager;
+                final LauncherPrepareContext ctx = new LauncherPrepareContext() {
+
+                    @Override
+                    public File getArtifactFile(final ArtifactId artifact) throws IOException {
+                        final ArtifactHandler handler = aMgr.getArtifactHandler(":" + artifact.toMvnPath());
+                        return handler.getFile();
+                    }
+
+                    @Override
+                    public void addAppJar(final File jar) {
+                        launcherConfig.getInstallation().addAppJar(jar);
+                    }
+                };
+                launcher.prepare(ctx, app);
+
+                FeatureProcessor.prepareLauncher(launcherConfig, artifactManager, app);
+
+            } catch ( final Exception iae) {
+                Main.LOG().error("Error while assembling launcher: {}", iae.getMessage(), iae);
+                System.exit(1);
+            }
+
+            Main.LOG().info("Using {} local artifacts, {} cached artifacts, and {} downloaded artifacts",
+                    launcherConfig.getLocalArtifacts(), launcherConfig.getCachedArtifacts(), launcherConfig.getDownloadedArtifacts());
+        } finally {
+            if ( artifactManager != null ) {
+                artifactManager.shutdown();
+            }
+        }
+
+        try {
+            run(launcherConfig);
+        } catch ( final Exception iae) {
+            Main.LOG().error("Error while running launcher: {}", iae.getMessage(), iae);
+            System.exit(1);
+        }
+    }
+
+    private static final String STORAGE_PROPERTY = "org.osgi.framework.storage";
+
+    private static final String START_LEVEL_PROP = "org.osgi.framework.startlevel.beginning";
+
+    /**
+     * Run launcher.
+     * @param config The configuration
+     * @throws Exception If anything goes wrong
+     */
+    private static void run(final LauncherConfig config) throws Exception {
+        Main.LOG().info("");
+        Main.LOG().info("Starting launcher...");
+        Main.LOG().info("Launcher Home: {}", config.getHomeDirectory().getAbsolutePath());
+        Main.LOG().info("Cache Directory: {}", config.getCacheDirectory().getAbsolutePath());
+        Main.LOG().info("Startup Mode: {}", config.getStartupMode());
+        Main.LOG().info("");
+
+        final Installation installation = config.getInstallation();
+
+        // set sling home, and use separate locations for launchpad and properties
+        installation.getFrameworkProperties().put("sling.home", config.getHomeDirectory().getAbsolutePath());
+        installation.getFrameworkProperties().put("sling.launchpad", config.getHomeDirectory().getAbsolutePath() + "/launchpad");
+        installation.getFrameworkProperties().put("sling.properties", "conf/sling.properties");
+
+
+        // additional OSGi properties
+        // move storage inside launcher
+        if ( installation.getFrameworkProperties().get(STORAGE_PROPERTY) == null ) {
+            installation.getFrameworkProperties().put(STORAGE_PROPERTY, config.getHomeDirectory().getAbsolutePath() + File.separatorChar + "framework");
+        }
+        // set start level to 30
+        if ( installation.getFrameworkProperties().get(START_LEVEL_PROP) == null ) {
+            installation.getFrameworkProperties().put(START_LEVEL_PROP, "30");
+        }
+
+        final Launcher launcher = new FrameworkLauncher();
+        launcher.run(installation, createClassLoader(installation));
+
+        config.clear();
+    }
+
+    /**
+     * Create the class loader.
+     * @param installation The launcher configuration
+     * @throws Exception If anything goes wrong
+     */
+    public static ClassLoader createClassLoader(final Installation installation) throws Exception {
+        final List<URL> list = new ArrayList<>();
+        for(final File f : installation.getAppJars()) {
+            try {
+                list.add(f.toURI().toURL());
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+        list.add(Main.class.getProtectionDomain().getCodeSource().getLocation());
+
+        final URL[] urls = list.toArray(new URL[list.size()]);
+
+        if ( Main.LOG().isDebugEnabled() ) {
+            Main.LOG().debug("App classpath: ");
+            for (int i = 0; i < urls.length; i++) {
+                Main.LOG().debug(" - {}", urls[i]);
+            }
+        }
+
+        // create a paranoid class loader, loading from parent last
+        final ClassLoader cl = new URLClassLoader(urls) {
+            @Override
+            public final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+                // First check if it's already loaded
+                Class<?> clazz = findLoadedClass(name);
+
+                if (clazz == null) {
+
+                    try {
+                        clazz = findClass(name);
+                    } catch (ClassNotFoundException cnfe) {
+                        ClassLoader parent = getParent();
+                        if (parent != null) {
+                            // Ask to parent ClassLoader (can also throw a CNFE).
+                            clazz = parent.loadClass(name);
+                        } else {
+                            // Propagate exception
+                            throw cnfe;
+                        }
+                    }
+                }
+
+                if (resolve) {
+                    resolveClass(clazz);
+                }
+
+                return clazz;
+            }
+
+            @Override
+            public final URL getResource(final String name) {
+
+                URL resource = findResource(name);
+                ClassLoader parent = this.getParent();
+                if (resource == null && parent != null) {
+                    resource = parent.getResource(name);
+                }
+
+                return resource;
+            }
+        };
+
+        Thread.currentThread().setContextClassLoader(cl);
+
+        return cl;
+    }
+}
diff --git a/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/launchers/AbstractRunner.java b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/launchers/AbstractRunner.java
new file mode 100644
index 0000000..0be1804
--- /dev/null
+++ b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/launchers/AbstractRunner.java
@@ -0,0 +1,278 @@
+/*
+ * 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.launcher.impl.launchers;
+
+import org.apache.sling.feature.launcher.impl.Main;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.startlevel.BundleStartLevel;
+import org.osgi.framework.wiring.FrameworkWiring;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Common functionality for the framework start.
+ */
+public class AbstractRunner {
+
+    private volatile ServiceTracker<Object, Object> configAdminTracker;
+
+    private volatile ServiceTracker<Object, Object> installerTracker;
+
+    private final List<Object[]> configurations;
+
+    private final List<File> installables;
+
+    public AbstractRunner(final List<Object[]> configurations, final List<File> installables) {
+        this.configurations = new ArrayList<>(configurations);
+        this.installables = installables;
+    }
+
+    protected void setupFramework(final Framework framework, final Map<Integer, List<File>> bundlesMap)
+    throws BundleException {
+        if ( !configurations.isEmpty() ) {
+            this.configAdminTracker = new ServiceTracker<>(framework.getBundleContext(),
+                    "org.osgi.service.cm.ConfigurationAdmin",
+                    new ServiceTrackerCustomizer<Object, Object>() {
+
+                        @Override
+                        public Object addingService(final ServiceReference<Object> reference) {
+                            // get config admin
+                            final Object cm = framework.getBundleContext().getService(reference);
+                            if ( cm != null ) {
+                                try {
+                                    configure(cm);
+                                } finally {
+                                    framework.getBundleContext().ungetService(reference);
+                                }
+                            }
+                            return null;
+                        }
+
+                        @Override
+                        public void modifiedService(ServiceReference<Object> reference, Object service) {
+                            // nothing to do
+                        }
+
+                        @Override
+                        public void removedService(ServiceReference<Object> reference, Object service) {
+                            // nothing to do
+                        }
+            });
+            this.configAdminTracker.open();
+        }
+        if ( installables != null && !installables.isEmpty() ) {
+            this.installerTracker = new ServiceTracker<>(framework.getBundleContext(),
+                    "org.apache.sling.installer.api.OsgiInstaller",
+                    new ServiceTrackerCustomizer<Object, Object>() {
+
+                        @Override
+                        public Object addingService(final ServiceReference<Object> reference) {
+                            // get installer
+                            final Object installer = framework.getBundleContext().getService(reference);
+                            if ( installer != null ) {
+                                try {
+                                    install(installer);
+                                } finally {
+                                    framework.getBundleContext().ungetService(reference);
+                                }
+                            }
+                            return null;
+                        }
+
+                        @Override
+                        public void modifiedService(ServiceReference<Object> reference, Object service) {
+                            // nothing to do
+                        }
+
+                        @Override
+                        public void removedService(ServiceReference<Object> reference, Object service) {
+                            // nothing to do
+                        }
+            });
+            this.installerTracker.open();
+        }
+        try {
+            this.install(framework, bundlesMap);
+        } catch ( final IOException ioe) {
+            throw new BundleException("Unable to install bundles.", ioe);
+        }
+    }
+
+    private void configure(final Object configAdmin) {
+        try {
+            final Method createConfig = configAdmin.getClass().getDeclaredMethod("getConfiguration", String.class, String.class);
+            final Method createFactoryConfig = configAdmin.getClass().getDeclaredMethod("getFactoryConfiguration", String.class, String.class, String.class);
+
+            Method updateMethod = null;
+            for(final Object[] obj : this.configurations) {
+                final Object cfg;
+                if ( obj[1] != null ) {
+                    cfg = createFactoryConfig.invoke(configAdmin, obj[1], obj[0], null);
+                } else {
+                    cfg = createConfig.invoke(configAdmin, obj[0], null);
+                }
+                if ( updateMethod == null ) {
+                    updateMethod = cfg.getClass().getDeclaredMethod("update", Dictionary.class);
+                }
+                updateMethod.invoke(cfg, obj[2]);
+            }
+        } catch ( final Exception e) {
+            Main.LOG().error("Unable to create configurations", e);
+            throw new RuntimeException(e);
+        }
+        final Thread t = new Thread(() -> { configAdminTracker.close(); configAdminTracker = null; });
+        t.setDaemon(false);
+        t.start();
+        this.configurations.clear();
+    }
+
+    private boolean isSystemBundleFragment(final Bundle installedBundle) {
+        final String fragmentHeader = getFragmentHostHeader(installedBundle);
+        return fragmentHeader != null
+            && fragmentHeader.indexOf(Constants.EXTENSION_DIRECTIVE) > 0;
+    }
+
+    /**
+     * Gets the bundle's Fragment-Host header.
+     */
+    private String getFragmentHostHeader(final Bundle b) {
+        return b.getHeaders().get( Constants.FRAGMENT_HOST );
+    }
+
+    /**
+     * Install the bundles
+     * @param bundleMap The map with the bundles indexed by start level
+     * @throws IOException, BundleException If anything goes wrong.
+     */
+    private void install(final Framework framework, final Map<Integer, List<File>> bundleMap)
+    throws IOException, BundleException {
+        final BundleContext bc = framework.getBundleContext();
+        int defaultStartLevel = getProperty(bc, "felix.startlevel.bundle", 1);
+        for(final Integer startLevel : sortStartLevels(bundleMap.keySet(), defaultStartLevel)) {
+            Main.LOG().debug("Installing bundles with start level {}", startLevel);
+
+            for(final File file : bundleMap.get(startLevel)) {
+                Main.LOG().debug("- {}", file.getName());
+
+                // use reference protocol. This avoids copying the binary to the cache directory
+                // of the framework
+                final Bundle bundle = bc.installBundle("reference:" + file.toURI().toURL(), null);
+                if ( startLevel > 0 ) {
+                    bundle.adapt(BundleStartLevel.class).setStartLevel(startLevel);
+                }
+
+                // fragment?
+                if ( !isSystemBundleFragment(bundle) ) {
+                    final String fragmentHostHeader = getFragmentHostHeader(bundle);
+                    if (fragmentHostHeader != null) {
+                        for (final Bundle b : bc.getBundles()) {
+                            if (fragmentHostHeader.equals(b.getSymbolicName())) {
+                                final FrameworkWiring fw = framework.adapt(FrameworkWiring.class);
+                                fw.refreshBundles(Collections.singleton(b));
+                                break;
+                            }
+                        }
+
+                    } else {
+                        bundle.start();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Sort the start levels in the ascending order. The only exception is the start level
+     * "0", which should be put at the position configured in {@code felix.startlevel.bundle}.
+     *
+     * @param startLevels integer start levels
+     * @return sorted start levels
+     */
+    private static Iterable<Integer> sortStartLevels(final Collection<Integer> startLevels, final int defaultStartLevel) {
+        final List<Integer> result = new ArrayList<>(startLevels);
+        Collections.sort(result, (o1, o2) -> {
+            int i1 = o1 == 0 ? defaultStartLevel : o1;
+            int i2 = o2 == 0 ? defaultStartLevel : o2;
+            return Integer.compare(i1, i2);
+        });
+        return result;
+    }
+
+    private static int getProperty(BundleContext bc, String propName, int defaultValue) {
+        String val = bc.getProperty(propName);
+        if (val == null) {
+            return defaultValue;
+        } else {
+            return Integer.parseInt(val);
+        }
+    }
+
+    private void install(final Object installer) {
+        try {
+            final Class<?> installableResourceClass = installer.getClass().getClassLoader().loadClass("org.apache.sling.installer.api.InstallableResource");
+            final Object resources = Array.newInstance(installableResourceClass, this.installables.size());
+            final Method registerResources = installer.getClass().getDeclaredMethod("registerResources", String.class, resources.getClass());
+            final Constructor<?> constructor = installableResourceClass.getDeclaredConstructor(String.class,
+                    InputStream.class,
+                    Dictionary.class,
+                    String.class,
+                    String.class,
+                    Integer.class);
+
+            for(int i=0; i<this.installables.size();i++) {
+                final File f = this.installables.get(i);
+                final Dictionary<String, Object> dict = new Hashtable<>();
+                dict.put("resource.uri.hint", f.toURI().toString());
+                final Object rsrc = constructor.newInstance(f.getAbsolutePath(),
+                        new FileInputStream(f),
+                        dict,
+                        f.getName(),
+                        "file",
+                        null);
+                Array.set(resources, i, rsrc);
+            }
+            registerResources.invoke(installer, "cloudlauncher", resources);
+        } catch ( final Exception e) {
+            Main.LOG().error("Unable to contact installer and install additional artifacts", e);
+            throw new RuntimeException(e);
+        }
+        final Thread t = new Thread(() -> { installerTracker.close(); installerTracker = null; });
+        t.setDaemon(false);
+        t.start();
+        this.installables.clear();
+    }
+}
diff --git a/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/launchers/FrameworkLauncher.java b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/launchers/FrameworkLauncher.java
new file mode 100644
index 0000000..8bb5fdc
--- /dev/null
+++ b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/launchers/FrameworkLauncher.java
@@ -0,0 +1,88 @@
+/*
+ * 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.launcher.impl.launchers;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.launcher.impl.Main;
+import org.apache.sling.feature.launcher.spi.Launcher;
+import org.apache.sling.feature.launcher.spi.LauncherPrepareContext;
+import org.apache.sling.feature.launcher.spi.LauncherRunContext;
+import org.apache.sling.feature.support.util.SubstVarUtil;
+
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Launcher directly using the OSGi launcher API.
+ */
+public class FrameworkLauncher implements Launcher {
+
+
+    @Override
+    public void prepare(final LauncherPrepareContext context, final Application app) throws Exception {
+        context.addAppJar(context.getArtifactFile(app.getFramework()));
+    }
+
+    /**
+     * Run the launcher
+     * @throws If anything goes wrong
+     */
+    @Override
+    public void run(final LauncherRunContext context, final ClassLoader cl) throws Exception {
+        Map<String, String> properties = new HashMap<>();
+        context.getFrameworkProperties().forEach((key, value) -> {
+            properties.put(key, SubstVarUtil.substVars(value, key,null, context.getFrameworkProperties()));
+        });
+        if ( Main.LOG().isDebugEnabled() ) {
+            Main.LOG().debug("Bundles:");
+            for(final Integer key : context.getBundleMap().keySet()) {
+                Main.LOG().debug("-- Start Level {}", key);
+                for(final File f : context.getBundleMap().get(key)) {
+                    Main.LOG().debug("  - {}", f.getName());
+                }
+            }
+            Main.LOG().debug("Settings: ");
+            for(final Map.Entry<String, String> entry : properties.entrySet()) {
+                Main.LOG().debug("- {}={}", entry.getKey(), entry.getValue());
+            }
+            Main.LOG().debug("Configurations: ");
+            for(final Object[] entry : context.getConfigurations()) {
+                if ( entry[1] != null ) {
+                    Main.LOG().debug("- Factory {} - {}", entry[1], entry[0]);
+                } else {
+                    Main.LOG().debug("- {}", entry[0]);
+                }
+            }
+            Main.LOG().debug("");
+        }
+        long time = System.currentTimeMillis();
+
+        final Class<?> runnerClass = cl.loadClass(this.getClass().getPackage().getName() + ".FrameworkRunner");
+        final Constructor<?> constructor = runnerClass.getDeclaredConstructor(Map.class, Map.class, List.class, List.class);
+        constructor.setAccessible(true);
+        constructor.newInstance(properties,
+                context.getBundleMap(),
+                context.getConfigurations(),
+                context.getInstallableArtifacts());
+
+        Main.LOG().debug("Startup took: " + (System.currentTimeMillis() - time));
+        // nothing else to do, constructor starts everything
+    }
+}
diff --git a/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/launchers/FrameworkRunner.java b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/launchers/FrameworkRunner.java
new file mode 100644
index 0000000..5d95cef
--- /dev/null
+++ b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/impl/launchers/FrameworkRunner.java
@@ -0,0 +1,70 @@
+/*
+ * 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.launcher.impl.launchers;
+
+import org.osgi.framework.BundleException;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+/**
+ * Launcher directly using the OSGi launcher API.
+ */
+public class FrameworkRunner extends AbstractRunner {
+
+    public FrameworkRunner(final Map<String, String> frameworkProperties,
+            final Map<Integer, List<File>> bundlesMap,
+            final List<Object[]> configurations,
+            final List<File> installables) throws Exception {
+        super(configurations, installables);
+
+        final ServiceLoader<FrameworkFactory> loader = ServiceLoader.load(FrameworkFactory.class);
+        FrameworkFactory factory = null;
+        for(FrameworkFactory f : loader) {
+            factory = f;
+            break;
+        }
+        if ( factory == null ) {
+            throw new Exception("Unable to locate framework factory.");
+        }
+
+        // create the framework
+        final Framework framework = factory.newFramework(frameworkProperties);
+        // initialize the framework
+        framework.init();
+
+        Runtime.getRuntime().addShutdownHook(new Thread() {
+            @Override
+            public void run() {
+                try {
+                    framework.stop();
+                } catch (final BundleException e) {
+                    // ignore
+                }
+            }
+        });
+
+        this.setupFramework(framework, bundlesMap);
+
+        // finally start
+        framework.start();
+    }
+}
diff --git a/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/spi/Launcher.java b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/spi/Launcher.java
new file mode 100644
index 0000000..a1ced11
--- /dev/null
+++ b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/spi/Launcher.java
@@ -0,0 +1,26 @@
+/*
+ * 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.launcher.spi;
+
+import org.apache.sling.feature.Application;
+
+public interface Launcher {
+
+    void prepare(LauncherPrepareContext context, Application app) throws Exception;
+
+    void run(LauncherRunContext context, ClassLoader cl) throws Exception;
+}
diff --git a/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/spi/LauncherPrepareContext.java b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/spi/LauncherPrepareContext.java
new file mode 100644
index 0000000..6d6f02b
--- /dev/null
+++ b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/spi/LauncherPrepareContext.java
@@ -0,0 +1,32 @@
+/*
+ * 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.launcher.spi;
+
+import org.apache.sling.feature.ArtifactId;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * This is the context for the launcher
+ */
+public interface LauncherPrepareContext {
+
+    void addAppJar(File jar);
+
+    File getArtifactFile(ArtifactId artifact) throws IOException;
+}
diff --git a/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/spi/LauncherRunContext.java b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/spi/LauncherRunContext.java
new file mode 100644
index 0000000..20c95a9
--- /dev/null
+++ b/featuremodel/feature-launcher/src/main/java/org/apache/sling/feature/launcher/spi/LauncherRunContext.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.launcher.spi;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This is the context for the launcher
+ */
+public interface LauncherRunContext {
+
+    /**
+     * Map of framework properties to be set when the framework is created.
+     * @return The map with the framework properties.
+     */
+    Map<String, String> getFrameworkProperties();
+
+    /**
+     * Bundle map, key is the start level, value is a list of files.
+     * @return The bundle map, might be empty
+     */
+    Map<Integer, List<File>> getBundleMap();
+
+    /**
+     * List of configurations.
+     * The value in each is an object array with three values
+     * <ol>
+     *  <li>The PID
+     *  <li>The factory PID or {@code null}
+     *  <li>The dictionary with the properties
+     * </ol>
+     * We can't use a custom object due to class loading restrictions.
+     * @return The list, might be empty
+     */
+    List<Object[]> getConfigurations();
+
+    /**
+     * List of installable artifacts.
+     * @return The list of files. The list might be empty.
+     */
+    List<File> getInstallableArtifacts();
+}
diff --git a/featuremodel/feature-modelconverter/pom.xml b/featuremodel/feature-modelconverter/pom.xml
new file mode 100644
index 0000000..64dbec0
--- /dev/null
+++ b/featuremodel/feature-modelconverter/pom.xml
@@ -0,0 +1,127 @@
+<?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.modelconverter</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+
+    <name>Apache Sling Feature Model Converter</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-modelconverter</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/tooling/support/feature-modelconverter</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/tooling/support/feature-modelconverter</url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                <execution>
+                    <id>unpack-dependencies</id>
+                    <phase>prepare-package</phase>
+                    <goals>
+                        <goal>unpack-dependencies</goal>
+                    </goals>
+                    <configuration>
+                        <excludes>META-INF/**</excludes>
+                        <outputDirectory>${project.build.directory}/classes</outputDirectory>
+                        <overWriteReleases>false</overWriteReleases>
+                        <overWriteSnapshots>true</overWriteSnapshots>
+                        <includeArtifactIds>commons-cli,org.apache.sling.feature,org.apache.sling.feature.support,org.apache.sling.commons.johnzon,org.apache.sling.provisioning.model,slf4j-api,slf4j-simple,osgi.core</includeArtifactIds>
+                    </configuration>
+                </execution>
+            </executions>
+            </plugin>
+                <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.sling.feature.modelconverter.impl.Main</mainClass>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+             <groupId>commons-cli</groupId>
+             <artifactId>commons-cli</artifactId>
+             <version>1.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.support</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.provisioning.model</artifactId>
+            <version>1.8.2</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.johnzon</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+      <!-- Testing -->
+        <dependency>
+        	<groupId>junit</groupId>
+        	<artifactId>junit</artifactId>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/featuremodel/feature-modelconverter/src/main/java/org/apache/sling/feature/modelconverter/impl/Main.java b/featuremodel/feature-modelconverter/src/main/java/org/apache/sling/feature/modelconverter/impl/Main.java
new file mode 100644
index 0000000..f030367
--- /dev/null
+++ b/featuremodel/feature-modelconverter/src/main/java/org/apache/sling/feature/modelconverter/impl/Main.java
@@ -0,0 +1,729 @@
+/*
+ * 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.modelconverter.impl;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Extensions;
+import org.apache.sling.feature.KeyValueMap;
+import org.apache.sling.feature.support.ArtifactHandler;
+import org.apache.sling.feature.support.ArtifactManager;
+import org.apache.sling.feature.support.ArtifactManagerConfig;
+import org.apache.sling.feature.support.FeatureUtil;
+import org.apache.sling.feature.support.json.ApplicationJSONReader;
+import org.apache.sling.feature.support.json.ApplicationJSONWriter;
+import org.apache.sling.feature.support.json.FeatureJSONWriter;
+import org.apache.sling.provisioning.model.Artifact;
+import org.apache.sling.provisioning.model.ArtifactGroup;
+import org.apache.sling.provisioning.model.Configuration;
+import org.apache.sling.provisioning.model.Feature;
+import org.apache.sling.provisioning.model.MergeUtility;
+import org.apache.sling.provisioning.model.Model;
+import org.apache.sling.provisioning.model.ModelConstants;
+import org.apache.sling.provisioning.model.ModelUtility;
+import org.apache.sling.provisioning.model.ModelUtility.ResolverOptions;
+import org.apache.sling.provisioning.model.ModelUtility.VariableResolver;
+import org.apache.sling.provisioning.model.RunMode;
+import org.apache.sling.provisioning.model.Section;
+import org.apache.sling.provisioning.model.Traceable;
+import org.apache.sling.provisioning.model.io.ModelReader;
+import org.apache.sling.provisioning.model.io.ModelWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Main {
+
+    private static Logger LOGGER;
+
+    private static String runModes;
+
+    private static String output;
+
+    private static String input;
+
+    private static boolean createApp = false;
+
+    private static boolean includeModelInfo = false;
+
+    private static String repoUrls;
+
+    private static String propsFile;
+
+    /**
+     * Parse the command line parameters and update a configuration object.
+     * @param args Command line parameters
+     * @return Configuration object.
+     */
+    private static void parseArgs(final String[] args) {
+        final Option repoOption =  Option.builder("u").hasArg().argName("Set repository url")
+                .desc("repository url").required().build();
+
+        final Option modelOption =  new Option("f", true, "Set feature files/directories");
+        final Option propsOption =  new Option("p", true, "sling.properties file");
+        final Option runModeOption =  new Option("r", true, "Set run modes (comma separated)");
+        final Option createAppOption = new Option("a", false, "If enabled, create application json");
+        createAppOption.setArgs(0);
+        final Option includeModelOption = new Option("i", false, "Include model filename as metadata for artifacts");
+        includeModelOption.setArgs(0);
+
+        final Option outputOption = Option.builder("o").hasArg().argName("Set output file")
+                .desc("output file").build();
+
+        final Options options = new Options();
+        options.addOption(repoOption);
+        options.addOption(modelOption);
+        options.addOption(createAppOption);
+        options.addOption(outputOption);
+        options.addOption(includeModelOption);
+        options.addOption(propsOption);
+        options.addOption(runModeOption);
+
+        final CommandLineParser parser = new DefaultParser();
+        try {
+            final CommandLine cl = parser.parse(options, args);
+
+            if ( cl.hasOption(repoOption.getOpt()) ) {
+                repoUrls = cl.getOptionValue(repoOption.getOpt());
+            }
+            if ( cl.hasOption(modelOption.getOpt()) ) {
+                input = cl.getOptionValue(modelOption.getOpt());
+            }
+            if ( cl.hasOption(createAppOption.getOpt()) ) {
+                createApp = true;
+            }
+            if ( cl.hasOption(includeModelOption.getOpt()) ) {
+                includeModelInfo = true;
+            }
+            if ( cl.hasOption(runModeOption.getOpt()) ) {
+                runModes = cl.getOptionValue(runModeOption.getOpt());
+            }
+            if ( cl.hasOption(outputOption.getOpt()) ) {
+                output = cl.getOptionValue(outputOption.getOpt());
+            }
+            if ( cl.hasOption(propsOption.getOpt()) ) {
+                propsFile = cl.getOptionValue(propsOption.getOpt());
+            }
+        } catch ( final ParseException pe) {
+            LOGGER.error("Unable to parse command line: {}", pe.getMessage(), pe);
+            System.exit(1);
+        }
+        if ( input == null ) {
+            LOGGER.error("Required argument missing: model file or directory");
+            System.exit(1);
+        }
+    }
+
+    private static ArtifactManager getArtifactManager() {
+        final ArtifactManagerConfig amConfig = new ArtifactManagerConfig();
+        if ( repoUrls != null ) {
+            amConfig.setRepositoryUrls(repoUrls.split(","));
+        }
+        try {
+            return ArtifactManager.getArtifactManager(amConfig);
+        } catch ( IOException ioe) {
+            LOGGER.error("Unable to create artifact manager " + ioe.getMessage(), ioe);
+            System.exit(1);
+        }
+        // we never reach this, but have to keep the compiler happy
+        return null;
+    }
+
+    public static void main(final String[] args) {
+        // setup logging
+        System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
+        System.setProperty("org.slf4j.simpleLogger.showThreadName", "false");
+        System.setProperty("org.slf4j.simpleLogger.levelInBrackets", "true");
+        System.setProperty("org.slf4j.simpleLogger.showLogName", "false");
+
+        LOGGER = LoggerFactory.getLogger("modelconverter");
+
+        LOGGER.info("Apache Sling Provisiong Model to Feature Application Converter");
+        LOGGER.info("");
+
+        parseArgs(args);
+
+        final ArtifactManagerConfig amConfig = new ArtifactManagerConfig();
+        if ( repoUrls != null ) {
+            amConfig.setRepositoryUrls(repoUrls.split(","));
+        }
+        final ArtifactManager am = getArtifactManager();
+
+        final File f = new File(input);
+        final List<File> files = new ArrayList<>();
+        if ( f.isDirectory() ) {
+            for(final File file : f.listFiles()) {
+                if ( file.isFile() && !file.getName().startsWith(".") ) {
+                    files.add(file);
+                }
+            }
+            if ( files.isEmpty() ) {
+                LOGGER.error("No files found in {}", f);
+                System.exit(1);
+            }
+            Collections.sort(files);
+        } else {
+            files.add(f);
+        }
+        boolean isJson = false;
+        boolean isTxt = false;
+        for(final File t : files) {
+            if ( t.getName().endsWith(".json") ) {
+                if ( isTxt ) {
+                    LOGGER.error("Input files are a mixture of JSON and txt");
+                    System.exit(1);
+                }
+                isJson = true;
+            } else {
+                if ( isJson ) {
+                    LOGGER.error("Input files are a mixture of JSON and txt");
+                    System.exit(1);
+                }
+                isTxt = true;
+            }
+        }
+
+        if ( isTxt ) {
+            if ( output == null ) {
+                output = createApp ? "application.json" : "feature.json";
+            }
+            final Model model = createModel(files, runModes);
+
+            if ( createApp ) {
+                final Application app = buildApplication(model);
+
+                writeApplication(app, output);
+            } else {
+                final List<org.apache.sling.feature.Feature> features = buildFeatures(model);
+                int index = 1;
+                for(final org.apache.sling.feature.Feature feature : features) {
+                    writeFeature(feature, output, features.size() > 1 ? index : 0);
+                    index++;
+                }
+            }
+        } else {
+            if ( output == null ) {
+                output = createApp ? "application.txt" : "feature.txt";
+            }
+            try {
+                if ( createApp ) {
+                    // each file is an application
+                    int index = 1;
+                    for(final File appFile : files ) {
+                        try ( final FileReader r = new FileReader(appFile) ) {
+                            final Application app = ApplicationJSONReader.read(r);
+                            convert(app, files.size() > 1 ? index : 0);
+                        }
+                        index++;
+                    }
+                } else {
+                    final Application app = FeatureUtil.assembleApplication(null, am, files.stream()
+                            .map(File::getAbsolutePath)
+                            .toArray(String[]::new));
+                    convert(app, 0);
+                }
+            } catch ( final IOException ioe) {
+                LOGGER.error("Unable to read feature/application files " + ioe.getMessage(), ioe);
+                System.exit(1);
+            }
+        }
+    }
+
+    private static List<org.apache.sling.feature.Feature> buildFeatures(final Model model) {
+        final List<org.apache.sling.feature.Feature> features = new ArrayList<>();
+
+        for(final Feature feature : model.getFeatures() ) {
+            final String idString;
+            if ( feature.getName() != null ) {
+                if ( feature.getVersion() != null ) {
+                    idString = "generated/" + feature.getName() + "/" + feature.getVersion();
+                } else {
+                    idString = "generated/" + feature.getName() + "/1.0.0";
+                }
+            } else {
+                idString = "generated/feature/1.0.0";
+            }
+            final org.apache.sling.feature.Feature f = new org.apache.sling.feature.Feature(ArtifactId.fromMvnId(idString));
+            features.add(f);
+
+            buildFromFeature(feature, f.getBundles(), f.getConfigurations(), f.getExtensions(), f.getFrameworkProperties());
+        }
+
+        return features;
+    }
+
+    private static Application buildApplication(final Model model) {
+        final Application app = new Application();
+
+        for(final Feature feature : model.getFeatures() ) {
+            buildFromFeature(feature, app.getBundles(), app.getConfigurations(), app.getExtensions(), app.getFrameworkProperties());
+        }
+
+        // hard coded dependency to launchpad api
+        app.getBundles().add(1, new org.apache.sling.feature.Artifact(ArtifactId.fromMvnId("org.apache.sling/org.apache.sling.launchpad.api/1.2.0")));
+        // sling.properties (TODO)
+        if ( propsFile == null ) {
+            app.getFrameworkProperties().put("org.osgi.framework.bootdelegation", "sun.*,com.sun.*");
+        } else {
+
+        }
+        // felix framework hard coded for now
+        app.setFramework(FeatureUtil.getFelixFrameworkId(null));
+        return app;
+    }
+
+    private static void buildFromFeature(final Feature feature,
+            final Bundles bundles,
+            final Configurations configurations,
+            final Extensions extensions,
+            final KeyValueMap properties) {
+        Extension cpExtension = extensions.getByName(Extension.NAME_CONTENT_PACKAGES);
+        for(final RunMode runMode : feature.getRunModes() ) {
+            if ( !ModelConstants.FEATURE_LAUNCHPAD.equals(feature.getName()) ) {
+                for(final ArtifactGroup group : runMode.getArtifactGroups()) {
+                    for(final Artifact artifact : group) {
+                        final ArtifactId id = ArtifactId.fromMvnUrl(artifact.toMvnUrl());
+                        final org.apache.sling.feature.Artifact newArtifact = new org.apache.sling.feature.Artifact(id);
+
+                        for(final Map.Entry<String, String> entry : artifact.getMetadata().entrySet()) {
+                            newArtifact.getMetadata().put(entry.getKey(), entry.getValue());
+                        }
+
+                        if ( newArtifact.getId().getType().equals("zip") ) {
+                            if ( cpExtension == null ) {
+                                cpExtension = new Extension(ExtensionType.ARTIFACTS, Extension.NAME_CONTENT_PACKAGES, true);
+                                extensions.add(cpExtension);
+                            }
+                            cpExtension.getArtifacts().add(newArtifact);
+                        } else {
+                            int startLevel = group.getStartLevel();
+                            if ( ModelConstants.FEATURE_BOOT.equals(feature.getName()) ) {
+                                startLevel = 1;
+                            } else if ( startLevel == 0 ) {
+                                startLevel = 20;
+                            }
+                            bundles.add(startLevel, newArtifact);
+                        }
+                    }
+                }
+            }
+
+            for(final Configuration cfg : runMode.getConfigurations()) {
+                final org.apache.sling.feature.Configuration newCfg;
+                if ( cfg.getFactoryPid() != null ) {
+                    newCfg = new org.apache.sling.feature.Configuration(cfg.getFactoryPid(), cfg.getPid());
+                } else {
+                    newCfg = new org.apache.sling.feature.Configuration(cfg.getPid());
+                }
+                final Enumeration<String> keys = cfg.getProperties().keys();
+                while ( keys.hasMoreElements() ) {
+                    final String key = keys.nextElement();
+                    newCfg.getProperties().put(key, cfg.getProperties().get(key));
+                }
+                configurations.add(newCfg);
+            }
+
+            for(final Map.Entry<String, String> prop : runMode.getSettings()) {
+                properties.put(prop.getKey(), prop.getValue());
+            }
+        }
+        Extension repoExtension = extensions.getByName(Extension.NAME_REPOINIT);
+        for(final Section sect : feature.getAdditionalSections("repoinit")) {
+            final String text = sect.getContents();
+            if ( repoExtension == null ) {
+                repoExtension = new Extension(ExtensionType.TEXT, Extension.NAME_REPOINIT, true);
+                extensions.add(repoExtension);
+                repoExtension.setText(text);
+            } else {
+                repoExtension.setText(repoExtension.getText() + "\n\n" + text);
+            }
+        }
+    }
+
+    private static void writeApplication(final Application app, final String out) {
+        LOGGER.info("Writing application...");
+        final File file = new File(out);
+        try ( final FileWriter writer = new FileWriter(file)) {
+            ApplicationJSONWriter.write(writer, app);
+        } catch ( final IOException ioe) {
+            LOGGER.error("Unable to write application to {} : {}", out, ioe.getMessage(), ioe);
+            System.exit(1);
+        }
+    }
+
+    private static void writeFeature(final org.apache.sling.feature.Feature f, String out, final int index) {
+        LOGGER.info("Writing feature...");
+        if ( index > 0 ) {
+            final int lastDot = out.lastIndexOf('.');
+            if ( lastDot == -1 ) {
+                out = out + "_" + String.valueOf(index);
+            } else {
+                out = out.substring(0, lastDot) + "_" + String.valueOf(index) + out.substring(lastDot);
+            }
+        }
+        final File file = new File(out);
+        try ( final FileWriter writer = new FileWriter(file)) {
+            FeatureJSONWriter.write(writer, f);
+        } catch ( final IOException ioe) {
+            LOGGER.error("Unable to write feature to {} : {}", out, ioe.getMessage(), ioe);
+            System.exit(1);
+        }
+    }
+
+    /**
+     * Read the models and prepare the model
+     * @param files The model files
+     */
+    private static Model createModel(final List<File> files,
+            final String runModes) {
+        LOGGER.info("Assembling model...");
+        Model model = null;
+        for(final File initFile : files) {
+            try {
+                model = processModel(model, initFile);
+            } catch ( final IOException iae) {
+                LOGGER.error("Unable to read provisioning model {} : {}", initFile, iae.getMessage(), iae);
+                System.exit(1);
+            }
+        }
+
+        final Model effectiveModel = ModelUtility.getEffectiveModel(model, new ResolverOptions().variableResolver(new VariableResolver() {
+
+            @Override
+            public String resolve(Feature feature, String name) {
+                if ( "sling.home".equals(name) ) {
+                    return "${sling.home}";
+                }
+                return feature.getVariables().get(name);
+            }
+        }));
+        final Map<Traceable, String> errors = ModelUtility.validate(effectiveModel);
+        if ( errors != null ) {
+            LOGGER.error("Invalid assembled provisioning model.");
+            for(final Map.Entry<Traceable, String> entry : errors.entrySet()) {
+                LOGGER.error("- {} : {}", entry.getKey().getLocation(), entry.getValue());
+            }
+            System.exit(1);
+        }
+        final Set<String> modes = calculateRunModes(effectiveModel, runModes);
+
+        removeInactiveFeaturesAndRunModes(effectiveModel, modes);
+
+        return effectiveModel;
+    }
+
+    /**
+     * Process the given model and merge it into the provided model
+     * @param model The already read model
+     * @param modelFile The model file
+     * @return The merged model
+     * @throws IOException If reading fails
+     */
+    private static Model processModel(Model model,
+            final File modelFile) throws IOException {
+        LOGGER.info("- reading model {}", modelFile);
+
+        final Model nextModel = readProvisioningModel(modelFile);
+        // resolve references to other models
+        final ResolverOptions options = new ResolverOptions().variableResolver(new VariableResolver() {
+
+            @Override
+            public String resolve(final Feature feature, final String name) {
+                return name;
+            }
+        });
+
+
+        final Model effectiveModel = ModelUtility.getEffectiveModel(nextModel, options);
+        for(final Feature feature : effectiveModel.getFeatures()) {
+            for(final RunMode runMode : feature.getRunModes()) {
+                for(final ArtifactGroup group : runMode.getArtifactGroups()) {
+                    final List<org.apache.sling.provisioning.model.Artifact> removeList = new ArrayList<>();
+                    for(final org.apache.sling.provisioning.model.Artifact a : group) {
+                        if ( "slingstart".equals(a.getType())
+                             || "slingfeature".equals(a.getType())) {
+
+                            final ArtifactManagerConfig cfg = new ArtifactManagerConfig();
+                            final ArtifactManager mgr = ArtifactManager.getArtifactManager(cfg);
+
+                            final ArtifactId correctedId = new ArtifactId(a.getGroupId(),
+                                    a.getArtifactId(),
+                                    a.getVersion(),
+                                    "slingstart".equals(a.getType()) ? "slingfeature" : a.getClassifier(),
+                                    "txt");
+
+                            final ArtifactHandler handler = mgr.getArtifactHandler(correctedId.toMvnUrl());
+                            model = processModel(model, handler.getFile());
+
+                            removeList.add(a);
+                        } else {
+                            final org.apache.sling.provisioning.model.Artifact realArtifact = nextModel.getFeature(feature.getName()).getRunMode(runMode.getNames()).getArtifactGroup(group.getStartLevel()).search(a);
+
+                            if ( includeModelInfo ) {
+                                realArtifact.getMetadata().put("model-filename", modelFile.getName());
+                            }
+                            if ( runMode.getNames() != null ) {
+                                realArtifact.getMetadata().put("runmodes", String.join(",", runMode.getNames()));
+                            }
+                        }
+                    }
+                    for(final org.apache.sling.provisioning.model.Artifact r : removeList) {
+                        nextModel.getFeature(feature.getName()).getRunMode(runMode.getNames()).getArtifactGroup(group.getStartLevel()).remove(r);
+                    }
+                }
+            }
+        }
+
+        if ( model == null ) {
+            model = nextModel;
+        } else {
+            MergeUtility.merge(model, nextModel);
+        }
+        return model;
+    }
+
+    /**
+     * Read the provisioning model
+     */
+    private static Model readProvisioningModel(final File file)
+    throws IOException {
+        try (final FileReader is = new FileReader(file)) {
+            final Model m = ModelReader.read(is, file.getAbsolutePath());
+            return m;
+        }
+    }
+
+    private static void removeInactiveFeaturesAndRunModes(final Model m,
+            final Set<String> activeRunModes) {
+        final String[] requiredFeatures = new String[] {ModelConstants.FEATURE_LAUNCHPAD, ModelConstants.FEATURE_BOOT};
+        // first pass:
+        // - remove special features except boot required ones
+        // - remove special run modes and inactive run modes
+        // - remove special configurations (TODO)
+        final Iterator<Feature> i = m.getFeatures().iterator();
+        while ( i.hasNext() ) {
+            final Feature feature = i.next();
+            if ( feature.isSpecial() ) {
+                boolean remove = true;
+                if ( requiredFeatures != null ) {
+                    for(final String name : requiredFeatures) {
+                        if ( feature.getName().equals(name) ) {
+                            remove = false;
+                            break;
+                        }
+                    }
+                }
+                if ( remove ) {
+                    i.remove();
+                    continue;
+                }
+            }
+            feature.setComment(null);
+            final Iterator<RunMode> rmI = feature.getRunModes().iterator();
+            while ( rmI.hasNext() ) {
+                final RunMode rm = rmI.next();
+                if ( rm.isActive(activeRunModes) || rm.isRunMode(ModelConstants.RUN_MODE_STANDALONE) ) {
+                    final Iterator<Configuration> cI = rm.getConfigurations().iterator();
+                    while ( cI.hasNext() ) {
+                        final Configuration config = cI.next();
+                        if ( config.isSpecial() ) {
+                            cI.remove();
+                            continue;
+                        }
+                        config.setComment(null);
+                    }
+                } else {
+                    rmI.remove();
+                    continue;
+                }
+            }
+        }
+
+        // second pass: aggregate the settings and add them to the first required feature
+        final Feature requiredFeature = m.getFeature(requiredFeatures[0]);
+        if ( requiredFeature != null ) {
+            for(final Feature f : m.getFeatures()) {
+                if ( f.getName().equals(requiredFeature.getName()) ) {
+                    continue;
+                }
+                copyAndClearSettings(requiredFeature, f.getRunMode(new String[] {ModelConstants.RUN_MODE_STANDALONE}));
+                copyAndClearSettings(requiredFeature, f.getRunMode());
+            }
+        }
+    }
+
+    private static void copyAndClearSettings(final Feature requiredFeature, final RunMode rm) {
+        if ( rm != null && !rm.getSettings().isEmpty() ) {
+            final RunMode requiredRunMode = requiredFeature.getOrCreateRunMode(null);
+            final Set<String> keys = new HashSet<>();
+            for(final Map.Entry<String, String> entry : rm.getSettings()) {
+                requiredRunMode.getSettings().put(entry.getKey(), entry.getValue());
+                keys.add(entry.getKey());
+            }
+
+            for(final String key : keys) {
+                rm.getSettings().remove(key);
+            }
+        }
+    }
+
+    private static Set<String> calculateRunModes(final Model model, final String runModes) {
+        final Set<String> modesSet = new HashSet<>();
+
+        // check configuration property first
+        if (runModes != null && runModes.trim().length() > 0) {
+            final String[] modes = runModes.split(",");
+            for(int i=0; i < modes.length; i++) {
+                modesSet.add(modes[i].trim());
+            }
+        }
+
+        //  handle configured options
+        final Feature feature = model.getFeature(ModelConstants.FEATURE_BOOT);
+        if ( feature != null ) {
+            handleOptions(modesSet, feature.getRunMode().getSettings().get("sling.run.mode.options"));
+            handleOptions(modesSet, feature.getRunMode().getSettings().get("sling.run.mode.install.options"));
+        }
+
+        return modesSet;
+    }
+
+    private static void handleOptions(final Set<String> modesSet, final String propOptions) {
+        if ( propOptions != null && propOptions.trim().length() > 0 ) {
+
+            final String[] options = propOptions.trim().split("\\|");
+            for(final String opt : options) {
+                String selected = null;
+                final String[] modes = opt.trim().split(",");
+                for(int i=0; i<modes.length; i++) {
+                    modes[i] = modes[i].trim();
+                    if ( selected != null ) {
+                        modesSet.remove(modes[i]);
+                    } else {
+                        if ( modesSet.contains(modes[i]) ) {
+                            selected = modes[i];
+                        }
+                    }
+                }
+                if ( selected == null ) {
+                    selected = modes[0];
+                    modesSet.add(modes[0]);
+                }
+            }
+        }
+    }
+
+    private static void convert(final Application app, final int index) {
+        final Feature f = new Feature("application");
+
+        // bundles
+        for(final Map.Entry<Integer, org.apache.sling.feature.Artifact> bundle : app.getBundles()) {
+            final ArtifactId id = bundle.getValue().getId();
+            final Artifact newBundle = new Artifact(id.getGroupId(), id.getArtifactId(), id.getVersion(), id.getClassifier(), id.getType());
+            for(final Map.Entry<String, String> prop : bundle.getValue().getMetadata()) {
+                newBundle.getMetadata().put(prop.getKey(), prop.getValue());
+            }
+            f.getOrCreateRunMode(null).getOrCreateArtifactGroup(bundle.getKey()).add(newBundle);
+        }
+
+        // configurations
+        for(final org.apache.sling.feature.Configuration cfg : app.getConfigurations()) {
+            final Configuration c;
+            if ( cfg.isFactoryConfiguration() ) {
+                c = new Configuration(cfg.getName(), cfg.getFactoryPid());
+            } else {
+                c = new Configuration(cfg.getPid(), null);
+            }
+            final Enumeration<String> keys = cfg.getProperties().keys();
+            while ( keys.hasMoreElements() ) {
+                final String key = keys.nextElement();
+                c.getProperties().put(key, cfg.getProperties().get(key));
+            }
+            f.getOrCreateRunMode(null).getConfigurations().add(c);
+        }
+
+        // framework properties
+        for(final Map.Entry<String, String> prop : app.getFrameworkProperties()) {
+            f.getOrCreateRunMode(null).getSettings().put(prop.getKey(), prop.getValue());
+        }
+
+        // extensions: content packages and repoinit
+        for(final Extension ext : app.getExtensions()) {
+            if ( Extension.NAME_CONTENT_PACKAGES.equals(ext.getName()) ) {
+                for(final org.apache.sling.feature.Artifact cp : ext.getArtifacts() ) {
+                    final ArtifactId id = cp.getId();
+                    final Artifact newCP = new Artifact(id.getGroupId(), id.getArtifactId(), id.getVersion(), id.getClassifier(), id.getType());
+                    for(final Map.Entry<String, String> prop : cp.getMetadata()) {
+                        newCP.getMetadata().put(prop.getKey(), prop.getValue());
+                    }
+                    f.getOrCreateRunMode(null).getOrCreateArtifactGroup(0).add(newCP);
+                }
+
+            } else if ( Extension.NAME_REPOINIT.equals(ext.getName()) ) {
+                final Section section = new Section("repoinit");
+                section.setContents(ext.getText());
+                f.getAdditionalSections().add(section);
+            } else if ( ext.isRequired() ) {
+                LOGGER.error("Unable to convert required extension {}", ext.getName());
+                System.exit(1);
+            }
+        }
+
+        LOGGER.info("Writing feature...");
+        String out = output;
+        if ( index > 0 ) {
+            final int lastDot = out.lastIndexOf('.');
+            if ( lastDot == -1 ) {
+                out = out + "_" + String.valueOf(index);
+            } else {
+                out = out.substring(0, lastDot) + "_" + String.valueOf(index) + out.substring(lastDot);
+            }
+        }
+        final File file = new File(out);
+        final Model m = new Model();
+        m.getFeatures().add(f);
+        try ( final FileWriter writer = new FileWriter(file)) {
+            ModelWriter.write(writer, m);
+        } catch ( final IOException ioe) {
+            LOGGER.error("Unable to write feature to {} : {}", out, ioe.getMessage(), ioe);
+            System.exit(1);
+        }
+    }
+}
diff --git a/featuremodel/feature-support/pom.xml b/featuremodel/feature-support/pom.xml
new file mode 100644
index 0000000..d780884
--- /dev/null
+++ b/featuremodel/feature-support/pom.xml
@@ -0,0 +1,118 @@
+<?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.support</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+    
+    <name>Apache Sling Feature Support</name>
+    <description>
+        Support classes for the feature tools
+    </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-support</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/tooling/support/feature-support</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/tooling/support/feature-support</url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Conditional-Package>
+                            org.apache.felix.configurator.impl.json,
+                            org.apache.felix.configurator.impl.model                            
+                        </Conditional-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.osgi</artifactId>
+            <version>2.4.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.converter</artifactId>
+            <version>0.1.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.configurator</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.geronimo.specs</groupId>
+            <artifactId>geronimo-json_1.0_spec</artifactId>
+            <version>1.0-alpha-1</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Testing -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>2.8.9</version>
+            <scope>test</scope>
+        </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/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ArtifactHandler.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ArtifactHandler.java
new file mode 100644
index 0000000..93f6bc9
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ArtifactHandler.java
@@ -0,0 +1,42 @@
+/*
+ * 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.support;
+
+import java.io.File;
+
+/**
+ * A handler provides a file object for an artifact.
+ */
+public class ArtifactHandler {
+
+    private final String url;
+
+    private final File file;
+
+    public ArtifactHandler(final String url, final File file) {
+        this.url = url;
+        this.file = file;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public File getFile() {
+        return file;
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ArtifactManager.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ArtifactManager.java
new file mode 100644
index 0000000..1568703
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ArtifactManager.java
@@ -0,0 +1,366 @@
+/*
+ * 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.support;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.support.spi.ArtifactProvider;
+import org.apache.sling.feature.support.spi.ArtifactProviderContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+/**
+ * The artifact manager is the central service to get artifacts.
+ * It uses {@link ArtifactProvider}s to get artifacts. The
+ * providers are loaded using the service loader.
+ */
+public class ArtifactManager {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** The map of providers. */
+    private final Map<String, ArtifactProvider> providers;
+
+    /** The configuration */
+    private final ArtifactManagerConfig config;
+
+    /**
+     * Get an artifact manager based on the configuration
+     * @param config The configuration
+     * @return The artifact manager
+     * @throws IOException If the manager can't be initialized
+     */
+    public static ArtifactManager getArtifactManager(final ArtifactManagerConfig config) throws IOException {
+        final ServiceLoader<ArtifactProvider> loader = ServiceLoader.load(ArtifactProvider.class);
+        final Map<String, ArtifactProvider> providers = new HashMap<>();
+        for(final ArtifactProvider provider : loader) {
+            providers.put(provider.getProtocol(), provider);
+        }
+
+        final String[] repositoryURLs = new String[config.getRepositoryUrls().length];
+        int index = 0;
+        for(final String urlString : config.getRepositoryUrls()) {
+            repositoryURLs[index] = urlString;
+            index++;
+        }
+        // default
+        if ( !providers.containsKey("*") ) {
+            providers.put("*", new DefaultArtifactHandler());
+        }
+
+        return new ArtifactManager(config, providers);
+    }
+
+    ArtifactManager(final ArtifactManagerConfig config, final Map<String, ArtifactProvider> providers)
+    throws IOException {
+        this.config = config;
+        this.providers = providers;
+        try {
+            for(final ArtifactProvider provider : this.providers.values()) {
+                provider.init(config);
+            }
+        } catch ( final IOException io) {
+            shutdown();
+            throw io;
+        }
+    }
+
+    /**
+     * Shutdown the artifact manager.
+     */
+    public void shutdown() {
+        for(final ArtifactProvider provider : this.providers.values()) {
+            provider.shutdown();
+        }
+        this.providers.clear();
+    }
+
+    private final File getArtifactFromProviders(final String url, final String relativeCachePath) throws IOException {
+        final int pos = url.indexOf(":");
+        final String scheme = url.substring(0, pos);
+
+        ArtifactProvider provider = this.providers.get(scheme);
+        if ( provider == null ) {
+            provider = this.providers.get("*");
+        }
+        if ( provider == null ) {
+            throw new IOException("No URL provider found for " + url);
+        }
+        return provider.getArtifact(url, relativeCachePath);
+    }
+
+    /**
+     * Get the full artifact url and file for an artifact.
+     * @param url Artifact url or relative path.
+     * @return Absolute url and file in the form of a handler.
+     * @throws IOException If something goes wrong.
+     */
+    public ArtifactHandler getArtifactHandler(final String url) throws IOException {
+        logger.debug("Trying to get artifact for {}", url);
+
+        final String path;
+
+        if ( url.startsWith("mvn:") ) {
+            // mvn url
+            path = ArtifactId.fromMvnUrl(url).toMvnPath();
+
+        } else if ( url.startsWith(":") ) {
+            // repository path
+            path = url.substring(1);
+
+        } else if ( url.indexOf(":/") > 0 ) {
+
+            // absolute URL
+            int pos = url.indexOf(":/") + 2;
+            while ( url.charAt(pos) == '/') {
+                pos++;
+            }
+            final File file = this.getArtifactFromProviders(url, url.substring(pos));
+            if ( file == null || !file.exists()) {
+                throw new IOException("Artifact " + url + " not found.");
+            }
+            return new ArtifactHandler(url, file);
+
+        } else {
+            // file (either relative or absolute)
+            final File f = new File(url);
+            if ( !f.exists()) {
+                throw new IOException("Artifact " + url + " not found.");
+            }
+            return new ArtifactHandler(f.toURI().toString(), f);
+        }
+        logger.debug("Querying repositories for {}", path);
+
+        for(final String repoUrl : this.config.getRepositoryUrls()) {
+            final StringBuilder builder = new StringBuilder();
+            builder.append(repoUrl);
+            builder.append('/');
+            builder.append(path);
+
+            final String artifactUrl = builder.toString();
+            final int pos = artifactUrl.indexOf(":");
+            final String scheme = artifactUrl.substring(0, pos);
+
+            ArtifactProvider handler = this.providers.get(scheme);
+            if ( handler == null ) {
+                handler = this.providers.get("*");
+            }
+            if ( handler == null ) {
+                throw new IOException("No URL handler found for " + artifactUrl);
+            }
+
+            logger.debug("Checking {} to get artifact from {}", handler, artifactUrl);
+
+            final File file = handler.getArtifact(artifactUrl, path);
+            if ( file != null ) {
+                logger.debug("Found artifact {}", artifactUrl);
+                return new ArtifactHandler(artifactUrl, file);
+            }
+
+            // check for SNAPSHOT
+            final int lastSlash = artifactUrl.lastIndexOf('/');
+            final int startSnapshot = artifactUrl.indexOf("-SNAPSHOT", lastSlash + 1);
+
+            if ( startSnapshot > -1 ) {
+                // special snapshot handling
+                final String metadataUrl = artifactUrl.substring(0, lastSlash) + "/maven-metadata.xml";
+                try {
+                    final ArtifactHandler metadataHandler = this.getArtifactHandler(metadataUrl);
+
+                    final String contents = getFileContents(metadataHandler);
+
+                    final String latestVersion = getLatestSnapshot(contents);
+                    if ( latestVersion != null ) {
+                        final String name = artifactUrl.substring(lastSlash); // includes slash
+                        final String fullURL = artifactUrl.substring(0, lastSlash) + name.replace("SNAPSHOT", latestVersion);
+                        int pos2 = fullURL.indexOf(":/") + 2;
+                        while ( fullURL.charAt(pos2) == '/') {
+                            pos2++;
+                        }
+                        final File file2 = this.getArtifactFromProviders(fullURL, path);
+                        if ( file2 == null || !file2.exists()) {
+                            throw new IOException("Artifact " + fullURL + " not found.");
+                        }
+                        return new ArtifactHandler(artifactUrl, file2);
+                    }
+                } catch ( final IOException ignore ) {
+                    // we ignore this but report the original 404
+                }
+            }
+        }
+
+        throw new IOException("Artifact " + url + " not found in any repository.");
+    }
+
+    protected String getFileContents(final ArtifactHandler handler) throws IOException {
+        final StringBuilder sb = new StringBuilder();
+        for(final String line : Files.readAllLines(handler.getFile().toPath())) {
+            sb.append(line).append('\n');
+        }
+
+        return sb.toString();
+    }
+
+    public static String getValue(final String xml, final String[] xpath) {
+        String value = null;
+        int pos = 0;
+        for(final String name : xpath) {
+            final String element = '<' + name + '>';
+
+            pos = xml.indexOf(element, pos);
+            if ( pos == -1 ) {
+                final String elementWithAttributes = '<' + name + ' ';
+                pos = xml.indexOf(elementWithAttributes, pos);
+                if ( pos == -1 ) {
+                    break;
+                }
+            }
+            pos = xml.indexOf('>', pos) + 1;
+        }
+        if ( pos != -1 ) {
+            final int endPos = xml.indexOf("</", pos);
+            if ( endPos != -1 ) {
+                value = xml.substring(pos, endPos).trim();
+            }
+        }
+        return value;
+    }
+    public static String getLatestSnapshot(final String mavenMetadata) {
+        final String timestamp = getValue(mavenMetadata, new String[] {"metadata", "versioning", "snapshot", "timestamp"});
+        final String buildNumber = getValue(mavenMetadata, new String[] {"metadata", "versioning", "snapshot", "buildNumber"});
+
+        if ( timestamp != null && buildNumber != null ) {
+            return timestamp + '-' + buildNumber;
+        }
+
+        return null;
+    }
+
+    private static final class DefaultArtifactHandler implements ArtifactProvider {
+
+        private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+        private volatile File cacheDir;
+
+        private volatile ArtifactProviderContext config;
+
+        @Override
+        public String getProtocol() {
+            return "*";
+        }
+
+        @Override
+        public void init(final ArtifactProviderContext config) throws IOException {
+            this.cacheDir = config.getCacheDirectory();
+            this.config = config;
+        }
+
+        @Override
+        public void shutdown() {
+            this.config = null;
+            this.cacheDir = null;
+        }
+
+        @Override
+        public File getArtifact(final String url, final String relativeCachePath) {
+            logger.debug("Checking url to be local file {}", url);
+            // check if this is already a local file
+            try {
+                final File f = new File(new URL(url).toURI());
+                if ( f.exists() ) {
+                    this.config.incLocalArtifacts();
+                    return f;
+                }
+                return null;
+            } catch ( final URISyntaxException ise) {
+                // ignore
+            } catch ( final IllegalArgumentException iae) {
+                // ignore
+            } catch ( final MalformedURLException mue) {
+                // ignore
+            }
+            logger.debug("Checking remote url {}", url);
+            try {
+                // check for url
+                if ( url.indexOf(":") == -1 ) {
+                    return null;
+                }
+
+                final String filePath = (this.cacheDir.getAbsolutePath() + File.separatorChar + relativeCachePath).replace('/', File.separatorChar);
+                final File cacheFile = new File(filePath);
+
+                if ( !cacheFile.exists() ) {
+                    cacheFile.getParentFile().mkdirs();
+                    final URL u = new URL(url);
+                    final URLConnection con = u.openConnection();
+                    con.connect();
+
+                    final InputStream readIS = con.getInputStream();
+                    final byte[] buffer = new byte[32768];
+                    int l;
+                    OutputStream os = null;
+                    try {
+                        os = new FileOutputStream(cacheFile);
+                        while ( (l = readIS.read(buffer)) >= 0 ) {
+                            os.write(buffer, 0, l);
+                        }
+                    } finally {
+                        try {
+                            readIS.close();
+                        } catch ( final IOException ignore) {
+                            // ignore
+                        }
+                        if ( os != null ) {
+                            try {
+                                os.close();
+                            } catch ( final IOException ignore ) {
+                                // ignore
+
+                            }
+                        }
+                    }
+                    this.config.incDownloadedArtifacts();
+                } else {
+                    this.config.incCachedArtifacts();
+                }
+                return cacheFile;
+            } catch ( final Exception e) {
+                logger.info("Artifact not found in one repository", e);
+                // ignore for now
+                return null;
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "DefaultArtifactHandler";
+        }
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ArtifactManagerConfig.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ArtifactManagerConfig.java
new file mode 100644
index 0000000..7a1dfef
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ArtifactManagerConfig.java
@@ -0,0 +1,126 @@
+/*
+ * 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.support;
+
+import org.apache.sling.feature.support.spi.ArtifactProviderContext;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+/**
+ * This class holds the configuration of artifact manager.
+ */
+public class ArtifactManagerConfig implements ArtifactProviderContext {
+
+    /** The repository urls. */
+    private volatile String[] repositoryUrls;
+
+    /** The cache directory. */
+    private volatile File cacheDirectory;
+
+    private volatile long cachedArtifacts;
+
+    private volatile long downloadedArtifacts;
+
+    private volatile long localArtifacts;
+
+    /**
+     * Create a new configuration object.
+     * Set the default values
+     */
+    public ArtifactManagerConfig() {
+        // set defaults
+        this.repositoryUrls = new String[] {
+                "file://" + System.getProperty("user.home") + "/.m2/repository",
+                "https://repo.maven.apache.org/maven2",
+                "https://repository.apache.org/content/groups/snapshots"
+                };
+        try {
+            this.cacheDirectory = Files.createTempDirectory("slingfeature").toFile();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Set the repository urls
+     * @param urls The repository urls
+     */
+    public void setRepositoryUrls(final String[] urls) {
+        if ( urls == null || urls.length == 0 ) {
+            this.repositoryUrls = null;
+        } else {
+            this.repositoryUrls = new String[urls.length];
+            System.arraycopy(urls, 0, this.repositoryUrls, 0, urls.length);
+            for(int i=0; i<this.repositoryUrls.length; i++) {
+                if ( this.repositoryUrls[i].endsWith("/") ) {
+                    this.repositoryUrls[i] = this.repositoryUrls[i].substring(0, this.repositoryUrls[i].length() - 1);
+                }
+            }
+        }
+    }
+
+    /**
+     * Get the repository urls.
+     * A repository url does not end with a slash.
+     * @return The repository urls.
+     */
+    public String[] getRepositoryUrls() {
+        return repositoryUrls;
+    }
+
+    /**
+     * Get the cache directory
+     * @return The cache directory.
+     */
+    @Override
+    public File getCacheDirectory() {
+        return cacheDirectory;
+    }
+
+    public void setCacheDirectory(final File dir) {
+        this.cacheDirectory = dir;
+    }
+
+    @Override
+    public void incCachedArtifacts() {
+        this.cachedArtifacts++;
+    }
+
+    @Override
+    public void incDownloadedArtifacts() {
+        this.downloadedArtifacts++;
+    }
+
+    @Override
+    public void incLocalArtifacts() {
+        this.localArtifacts++;
+    }
+
+    public long getCachedArtifacts() {
+        return this.cachedArtifacts;
+    }
+
+    public long getDownloadedArtifacts() {
+        return this.downloadedArtifacts;
+    }
+
+    public long getLocalArtifacts() {
+        return this.localArtifacts;
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ConfigurationUtil.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ConfigurationUtil.java
new file mode 100644
index 0000000..7ba8554
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/ConfigurationUtil.java
@@ -0,0 +1,90 @@
+/*
+ * 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.support;
+
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.support.json.ConfigurationJSONWriter;
+import org.osgi.framework.Constants;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.Map;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+
+
+public class ConfigurationUtil {
+
+    public static final String REQUIRE_CONFIGURATOR_CAPABILITY =
+            "osgi.extender;filter:=\"(&(osgi.extender=osgi.configurator)(version>=1.0)(!(version>=2.0)))\"";
+
+    public static final String REQUIRE_REPOINIT_CAPABILITY =
+            "osgi.implementation;filter:=\"(&(osgi.implementation=org.apache.sling.jcr.repoinit)(version>=1.0)(!(version>=2.0)))\"";
+
+    /**
+     * Create a bundle containing the configurations to be processed the
+     * OSGi configurator
+     *
+     * @param os The output stream. The stream is not closed
+     * @param configurations The list of configurations
+     * @param symbolicName The symbolic name for the generated bundle
+     * @param version The version for the generated bundle
+     * @param additionalAttributes Optional additional attributes for the Manifest.
+     * @throws IOException If something goes wrong
+     */
+    public static void createConfiguratorBundle(final OutputStream os,
+            final Configurations configurations,
+            final String symbolicName,
+            final String version,
+            final Map<String, String> additionalAttributes)
+    throws IOException {
+
+        final Manifest mf = new Manifest();
+        mf.getMainAttributes().putValue("Manifest-Version", "1.0");
+        mf.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2");
+        mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, symbolicName);
+        mf.getMainAttributes().putValue(Constants.BUNDLE_VERSION, version);
+        mf.getMainAttributes().putValue(Constants.BUNDLE_VENDOR, "The Apache Software Foundation");
+        mf.getMainAttributes().putValue(Constants.REQUIRE_CAPABILITY, REQUIRE_CONFIGURATOR_CAPABILITY);
+
+        if ( additionalAttributes != null ) {
+            for(final Map.Entry<String, String> entry : additionalAttributes.entrySet()) {
+                if ( Constants.REQUIRE_CAPABILITY.equals(entry.getKey())
+                    && !entry.getValue().contains("osgi.extender=osgi.configurator")) {
+                    mf.getMainAttributes().putValue(entry.getKey(), entry.getValue() + "," + REQUIRE_CONFIGURATOR_CAPABILITY);
+                } else {
+                    mf.getMainAttributes().putValue(entry.getKey(), entry.getValue());
+                }
+            }
+        }
+
+        final JarOutputStream jos = new JarOutputStream(os, mf);
+
+        final ZipEntry ze = new ZipEntry("OSGI-INF/configurator/configurations.json");
+        jos.putNextEntry(ze);
+        final Writer w = new OutputStreamWriter(jos, "UTF-8");
+        ConfigurationJSONWriter.write(w, configurations);
+        w.flush();
+        jos.closeEntry();
+
+        jos.flush();
+        jos.finish();
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/FeatureUtil.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/FeatureUtil.java
new file mode 100644
index 0000000..69b245a
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/FeatureUtil.java
@@ -0,0 +1,284 @@
+/*
+ * 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.support;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.process.ApplicationBuilder;
+import org.apache.sling.feature.process.BuilderContext;
+import org.apache.sling.feature.process.FeatureProvider;
+import org.apache.sling.feature.support.json.FeatureJSONReader;
+
+public class FeatureUtil {
+
+    /**
+     * Get an artifact id for the Apache Felix framework
+     * @param version The version to use or {@code null} for the default version
+     * @return The artifact id
+     * @throws IllegalArgumentException If the provided version is invalid
+     */
+    public static ArtifactId getFelixFrameworkId(final String version) {
+        return new ArtifactId("org.apache.felix",
+                "org.apache.felix.framework",
+                version != null ? version : "5.6.8", null, null);
+    }
+
+    static final Comparator<String> FEATURE_PATH_COMP = new Comparator<String>() {
+
+        @Override
+        public int compare(final String o1, final String o2) {
+            // windows path conversion
+            final String key1 = o1.replace(File.separatorChar, '/');
+            final String key2 = o2.replace(File.separatorChar, '/');
+
+            final int lastSlash1 = key1.lastIndexOf('/');
+            final int lastSlash2 = key2.lastIndexOf('/');
+            if ( lastSlash1 == -1 || lastSlash2 == -1 ) {
+                return o1.compareTo(o2);
+            }
+            final String path1 = key1.substring(0, lastSlash1 + 1);
+            final String path2 = key2.substring(0, lastSlash2 + 1);
+            if ( path1.equals(path2) ) {
+                return o1.compareTo(o2);
+            }
+            if ( path1.startsWith(path2) ) {
+                return 1;
+            } else if ( path2.startsWith(path1) ) {
+                return -1;
+            }
+            return o1.compareTo(o2);
+        }
+    };
+
+    private static void processDir(final List<String> paths, final File dir)
+    throws IOException {
+        for(final File f : dir.listFiles()) {
+            if ( f.isFile() && !f.getName().startsWith(".")) {
+                // check if file is a reference
+                if ( f.getName().endsWith(".ref") || f.getName().endsWith(".json") ) {
+                    processFile(paths, f);
+                }
+            }
+        }
+    }
+
+    public static List<String> parseFeatureRefFile(final File file)
+    throws IOException {
+        final List<String> result = new ArrayList<>();
+        final List<String> lines = Files.readAllLines(file.toPath());
+        for(String line : lines) {
+            line = line.trim();
+            if ( !line.isEmpty() && !line.startsWith("#") ) {
+                if ( line.indexOf(':') == -1 ) {
+                    result.add(new File(line).getAbsolutePath());
+                } else {
+                    result.add(line);
+                }
+            }
+        }
+        return result;
+    }
+
+    private static void processFile(final List<String> paths, final File f)
+    throws IOException {
+        if ( f.getName().endsWith(".ref") ) {
+            paths.addAll(parseFeatureRefFile(f));
+        } else {
+            paths.add(f.getAbsolutePath());
+        }
+    }
+
+    /**
+     * Get the list of feature files.
+     * If the provided list of files is {@code null} or an empty array, the default is used.
+     * The default checks for the following places, the first one found is used. If none is
+     * found an empty list is returned.
+     * <ol>
+     *   <li>A directory named {@code feature} in the current directory
+     *   <li>A file named {@code features.json} in the current directory
+     *   <li>A directory named {@code feature} in the home directory
+     *   <li>A file named {@code features.json} in the home directory
+     * </ol>
+     *
+     * The list of files is processed one after the other. If it is relative, it is
+     * first tried to be resolved against the current directory and then against the
+     * home directory.
+     * If an entry denotes a directory, all children ending in {@code .json} or {@code .ref} of that directory are read.
+     * If a file ends in {@code .ref} the contents is read and every line not starting with the
+     * hash sign is considered a reference to a feature artifact.
+     *
+     * @param homeDirectory If relative files should be resolved, this is the directory to use
+     * @param files Optional list of files. If none is provided, a default is used.
+     * @return The list of files.
+     * @throws IOException If an error occurs.
+     */
+    public static List<String> getFeatureFiles(final File homeDirectory, final String... files) throws IOException {
+        String[] featureFiles = files;
+        if ( featureFiles == null || featureFiles.length == 0 ) {
+            // Default value - check feature directory otherwise features file
+            final File[] candidates = new File[] {
+                    new File(homeDirectory, "features"),
+                    new File(homeDirectory, "features.json"),
+                    new File("features"),
+                    new File("features.json")
+            };
+            File f = null;
+            for(final File c : candidates) {
+                if ( c.exists() ) {
+                    f = c;
+                    break;
+                }
+            }
+            // nothing found, we default to the first candidate and fail later
+            if ( f == null ) {
+                f = candidates[0];
+            }
+
+            featureFiles = new String[] {f.getAbsolutePath()};
+        }
+
+        final List<String> paths = new ArrayList<>();
+        for(final String name : featureFiles) {
+            // check for absolute
+            if ( name.indexOf(':') > 1 ) {
+                paths.add(name);
+            } else {
+                // file or relative
+                File f = null;
+                final File test = new File(name);
+                if ( test.isAbsolute() ) {
+                    f = test;
+                } else {
+                    final File[] candidates = {
+                            new File(homeDirectory, name),
+                            new File(homeDirectory, "features" + File.separatorChar + name),
+                            new File(name),
+                            new File("features" + File.separatorChar + name),
+                    };
+                    for(final File c : candidates) {
+                        if ( c.exists() && c.isFile() ) {
+                            f = c;
+                            break;
+                        }
+                    }
+                }
+
+                if ( f != null && f.exists() ) {
+                    if ( f.isFile() ) {
+                        processFile(paths, f);
+                    } else {
+                        processDir(paths, f);
+                    }
+                } else {
+                    // we simply add the path and fail later on
+                    paths.add(new File(name).getAbsolutePath());
+                }
+            }
+        }
+
+        Collections.sort(paths, FEATURE_PATH_COMP);
+        return paths;
+    }
+
+    /**
+     * Assemble an application based on the given files.
+     *
+     * Read the features and assemble the application
+     * @param app The optional application to use as a base.
+     * @param featureFiles The feature files.
+     * @param artifactManager The artifact manager
+     * @return The assembled application
+     * @throws IOException If a feature can't be read or no feature is found.
+     * @see #getFeatureFiles(File, String...)
+     */
+    public static Application assembleApplication(
+            Application app,
+            final ArtifactManager artifactManager, final String... featureFiles)
+    throws IOException {
+        final List<Feature> features = new ArrayList<>();
+        for(final String initFile : featureFiles) {
+            final Feature f = getFeature(initFile, artifactManager);
+            features.add(f);
+        }
+
+        return assembleApplication(app, artifactManager, features.toArray(new Feature[0]));
+    }
+
+    public static Application assembleApplication(
+            Application app,
+            final ArtifactManager artifactManager, final Feature... features)
+    throws IOException {
+        if ( features.length == 0 ) {
+            throw new IOException("No features found.");
+        }
+
+        app = ApplicationBuilder.assemble(app, new BuilderContext(new FeatureProvider() {
+
+            @Override
+            public Feature provide(final ArtifactId id) {
+                try {
+                    final ArtifactHandler handler = artifactManager.getArtifactHandler("mvn:" + id.toMvnPath());
+                    try (final FileReader r = new FileReader(handler.getFile())) {
+                        final Feature f = FeatureJSONReader.read(r, handler.getUrl());
+                        return f;
+                    }
+
+                } catch (final IOException e) {
+                    // ignore
+                }
+                return null;
+            }
+        }), features);
+
+        // check framework
+        if ( app.getFramework() == null ) {
+            // use hard coded Apache Felix
+            app.setFramework(getFelixFrameworkId(null));
+        }
+
+        return app;
+    }
+
+    /**
+     * Read the feature
+     *
+     * @param file The feature file
+     * @param artifactManager The artifact manager to read the feature
+     * @return The read feature
+     * @throws IOException If reading fails
+     */
+    private static Feature getFeature(final String file,
+            final ArtifactManager artifactManager)
+    throws IOException {
+        final ArtifactHandler featureArtifact = artifactManager.getArtifactHandler(file);
+
+        try (final FileReader r = new FileReader(featureArtifact.getFile())) {
+            final Feature f = FeatureJSONReader.read(r, featureArtifact.getUrl());
+            return f;
+        }
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ApplicationJSONReader.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ApplicationJSONReader.java
new file mode 100644
index 0000000..677e848
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ApplicationJSONReader.java
@@ -0,0 +1,92 @@
+/*
+ * 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.support.json;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+
+import org.apache.felix.configurator.impl.json.JSONUtil;
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.ArtifactId;
+
+/**
+ * This class offers a method to read an {@code Application} using a {@code Reader} instance.
+ */
+public class ApplicationJSONReader extends JSONReaderBase {
+
+    /**
+     * Read a new application from the reader
+     * The reader is not closed. It is up to the caller to close the reader.
+     *
+     * @param reader The reader for the feature
+     * @return The application
+     * @throws IOException If an IO errors occurs or the JSON is invalid.
+     */
+    public static Application read(final Reader reader)
+    throws IOException {
+        try {
+            final ApplicationJSONReader mr = new ApplicationJSONReader();
+            mr.readApplication(reader);
+            return mr.app;
+        } catch (final IllegalStateException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+    }
+
+    /** The read application. */
+    private final Application app;
+
+    /**
+     * Private constructor
+     */
+    private ApplicationJSONReader() {
+        super(null);
+        this.app = new Application();
+    }
+
+    /**
+     * Read a full application
+     * @param reader The reader
+     * @throws IOException If an IO error occurs or the JSON is not valid.
+     */
+    private void readApplication(final Reader reader)
+    throws IOException {
+        final JsonObject json = Json.createReader(new StringReader(minify(reader))).readObject();
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> map = (Map<String, Object>) JSONUtil.getValue(json);
+
+        final String frameworkId = this.getProperty(map, JSONConstants.APP_FRAMEWORK);
+        if ( frameworkId != null ) {
+            app.setFramework(ArtifactId.parse(frameworkId));
+        }
+        this.readBundles(map, app.getBundles(), app.getConfigurations());
+        this.readFrameworkProperties(map, app.getFrameworkProperties());
+        this.readConfigurations(map, app.getConfigurations());
+
+        this.readExtensions(map,
+                JSONConstants.APP_KNOWN_PROPERTIES,
+                this.app.getExtensions(), this.app.getConfigurations());
+    }
+}
+
+
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ApplicationJSONWriter.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ApplicationJSONWriter.java
new file mode 100644
index 0000000..9e8b43f
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ApplicationJSONWriter.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.support.json;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+
+import javax.json.Json;
+import javax.json.stream.JsonGenerator;
+import java.io.IOException;
+import java.io.Writer;
+
+
+/**
+ * Simple JSON writer for an application
+ */
+public class ApplicationJSONWriter extends JSONWriterBase {
+
+    /**
+     * Writes the application to the writer.
+     * The writer is not closed.
+     * @param writer Writer
+     * @param app The application
+     * @throws IOException If writing fails
+     */
+    public static void write(final Writer writer, final Application app)
+    throws IOException {
+        final ApplicationJSONWriter w = new ApplicationJSONWriter();
+        w.writeApp(writer, app);
+    }
+
+   private void writeApp(final Writer writer, final Application app)
+    throws IOException {
+        final JsonGenerator w = Json.createGenerator(writer);
+        w.writeStartObject();
+
+        // framework
+        if ( app.getFramework() != null ) {
+            w.write(JSONConstants.APP_FRAMEWORK, app.getFramework().toMvnId());
+        }
+
+        // features
+        if ( !app.getFeatureIds().isEmpty() ) {
+            w.writeStartArray(JSONConstants.APP_FEATURES);
+            for(final ArtifactId id : app.getFeatureIds()) {
+                w.write(id.toMvnId());
+            }
+            w.writeEnd();
+        }
+
+        // bundles
+        writeBundles(w, app.getBundles(), app.getConfigurations());
+
+        // configurations
+        final Configurations cfgs = new Configurations();
+        for(final Configuration cfg : app.getConfigurations()) {
+            final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT);
+            if (  artifactProp == null ) {
+                cfgs.add(cfg);
+            }
+        }
+        writeConfigurations(w, cfgs);
+
+        // framework properties
+        writeFrameworkProperties(w, app.getFrameworkProperties());
+
+        // extensions
+        writeExtensions(w, app.getExtensions(), app.getConfigurations());
+
+        w.writeEnd();
+        w.flush();
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ConfigurationJSONReader.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ConfigurationJSONReader.java
new file mode 100644
index 0000000..d10bafe
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ConfigurationJSONReader.java
@@ -0,0 +1,78 @@
+/*
+ * 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.support.json;
+
+import org.apache.felix.configurator.impl.json.JSONUtil;
+import org.apache.sling.feature.Configurations;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * JSON Reader for configurations.
+ */
+public class ConfigurationJSONReader extends JSONReaderBase {
+
+    /**
+     * Read a map of configurations from the reader
+     * The reader is not closed. It is up to the caller to close the reader.
+     *
+     * @param reader The reader for the configuration
+     * @param location Optional location
+     * @return The read configurations
+     * @throws IOException If an IO errors occurs or the JSON is invalid.
+     */
+    public static Configurations read(final Reader reader, final String location)
+    throws IOException {
+        try {
+            final ConfigurationJSONReader mr = new ConfigurationJSONReader(location);
+            return mr.readConfigurations(reader);
+        } catch (final IllegalStateException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Private constructor
+     * @param location Optional location
+     */
+    ConfigurationJSONReader(final String location) {
+        super(location);
+    }
+
+    Configurations readConfigurations(final Reader reader) throws IOException {
+        final Configurations result = new Configurations();
+
+        final JsonObject json = Json.createReader(new StringReader(minify(reader))).readObject();
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> map = (Map<String, Object>) JSONUtil.getValue(json);
+
+        final Map<String, Object> objMap = Collections.singletonMap(JSONConstants.FEATURE_CONFIGURATIONS, (Object)map);
+
+        readConfigurations(objMap, result);
+
+        return result;
+    }
+}
+
+
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ConfigurationJSONWriter.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ConfigurationJSONWriter.java
new file mode 100644
index 0000000..d72bad0
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/ConfigurationJSONWriter.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.support.json;
+
+import org.apache.sling.feature.Configurations;
+
+import javax.json.Json;
+import javax.json.stream.JsonGenerator;
+import java.io.IOException;
+import java.io.Writer;
+
+
+/**
+ * JSON writer for configurations
+ */
+public class ConfigurationJSONWriter extends JSONWriterBase {
+
+    /**
+     * Writes the configurations to the writer.
+     * The writer is not closed.
+     * @param writer Writer
+     * @param configs List of configurations
+     * @throws IOException If writing fails
+     */
+    public static void write(final Writer writer, final Configurations configs)
+    throws IOException {
+        final ConfigurationJSONWriter w = new ConfigurationJSONWriter();
+        w.writeConfigurations(writer, configs);
+    }
+
+    private void writeConfigurations(final Writer writer, final Configurations configs)
+    throws IOException {
+        final JsonGenerator w = Json.createGenerator(writer);
+        w.writeStartObject();
+
+        writeConfigurationsMap(w, configs);
+
+        w.writeEnd();
+        w.flush();
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/FeatureJSONReader.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/FeatureJSONReader.java
new file mode 100644
index 0000000..cffd875
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/FeatureJSONReader.java
@@ -0,0 +1,332 @@
+/*
+ * 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.support.json;
+
+import static org.apache.sling.feature.support.util.LambdaUtil.rethrowBiConsumer;
+import static org.apache.sling.feature.support.util.ManifestUtil.unmarshalAttribute;
+import static org.apache.sling.feature.support.util.ManifestUtil.unmarshalDirective;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+
+import org.apache.felix.configurator.impl.json.JSONUtil;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Capability;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Include;
+import org.apache.sling.feature.Requirement;
+
+/**
+ * This class offers a method to read a {@code Feature} using a {@code Reader} instance.
+ */
+public class FeatureJSONReader extends JSONReaderBase {
+
+    /**
+     * Read a new feature from the reader
+     * The reader is not closed. It is up to the caller to close the reader.
+     *
+     * @param reader The reader for the feature
+     * @param location Optional location
+     * @return The read feature
+     * @throws IOException If an IO errors occurs or the JSON is invalid.
+     */
+    public static Feature read(final Reader reader, final String location)
+    throws IOException {
+        return read(reader, null, location);
+    }
+
+    /**
+     * Read a new feature from the reader
+     * The reader is not closed. It is up to the caller to close the reader.
+     *
+     * @param reader The reader for the feature
+     * @param providedId Optional artifact id
+     * @param location Optional location
+     * @return The read feature
+     * @throws IOException If an IO errors occurs or the JSON is invalid.
+     */
+    public static Feature read(final Reader reader,
+            final ArtifactId providedId,
+            final String location)
+    throws IOException {
+        try {
+            final FeatureJSONReader mr = new FeatureJSONReader(providedId, location);
+            return mr.readFeature(reader);
+        } catch (final IllegalStateException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+    }
+
+    /** The read feature. */
+    private Feature feature;
+
+    /** The provided id. */
+    private final ArtifactId providedId;
+
+    /**
+     * Private constructor
+     * @param pId Optional id
+     * @param location Optional location
+     */
+    private FeatureJSONReader(final ArtifactId pId, final String location) {
+        super(location);
+        this.providedId = pId;
+    }
+
+    /**
+     * Read a full feature
+     * @param reader The reader
+     * @return The feature object
+     * @throws IOException If an IO error occurs or the JSON is not valid.
+     */
+    private Feature readFeature(final Reader reader)
+    throws IOException {
+        final JsonObject json = Json.createReader(new StringReader(minify(reader))).readObject();
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> map = (Map<String, Object>) JSONUtil.getValue(json);
+
+        final ArtifactId fId;
+        if ( !map.containsKey(JSONConstants.FEATURE_ID) ) {
+            if ( this.providedId == null ) {
+                throw new IOException(this.exceptionPrefix + "Feature id is missing");
+            }
+            fId = this.providedId;
+        } else {
+            final Object idObj = map.get(JSONConstants.FEATURE_ID);
+            checkType(JSONConstants.FEATURE_ID, idObj, String.class);
+            fId = ArtifactId.parse(idObj.toString());
+        }
+        this.feature = new Feature(fId);
+        this.feature.setLocation(this.location);
+
+        // title, description, vendor and license
+        this.feature.setTitle(getProperty(map, JSONConstants.FEATURE_TITLE));
+        this.feature.setDescription(getProperty(map, JSONConstants.FEATURE_DESCRIPTION));
+        this.feature.setVendor(getProperty(map, JSONConstants.FEATURE_VENDOR));
+        this.feature.setLicense(getProperty(map, JSONConstants.FEATURE_LICENSE));
+
+        this.readBundles(map, feature.getBundles(), feature.getConfigurations());
+        this.readFrameworkProperties(map, feature.getFrameworkProperties());
+        this.readConfigurations(map, feature.getConfigurations());
+
+        this.readCapabilities(map);
+        this.readRequirements(map);
+        this.readIncludes(map);
+
+        if ( map.containsKey(JSONConstants.FEATURE_UPGRADEOF) ) {
+            final Object idObj = map.get(JSONConstants.FEATURE_UPGRADEOF);
+            checkType(JSONConstants.FEATURE_UPGRADEOF, idObj, String.class);
+            this.feature.setUpgradeOf(ArtifactId.parse(idObj.toString()));
+        }
+        if ( map.containsKey(JSONConstants.FEATURE_UPGRADES) ) {
+            final Object listObj = map.get(JSONConstants.FEATURE_UPGRADES);
+            checkType(JSONConstants.FEATURE_UPGRADES, listObj, List.class);
+            @SuppressWarnings("unchecked")
+            final List<Object> list = (List<Object>) listObj;
+            for(final Object element : list) {
+                checkType(JSONConstants.FEATURE_UPGRADES, element, String.class);
+                feature.getUpgrades().add(ArtifactId.parse(element.toString()));
+            }
+
+        }
+        this.readExtensions(map,
+                JSONConstants.FEATURE_KNOWN_PROPERTIES,
+                this.feature.getExtensions(), this.feature.getConfigurations());
+
+        return feature;
+    }
+
+    private void readIncludes(final Map<String, Object> map) throws IOException {
+        if ( map.containsKey(JSONConstants.FEATURE_INCLUDES)) {
+            final Object includesObj = map.get(JSONConstants.FEATURE_INCLUDES);
+            checkType(JSONConstants.FEATURE_INCLUDES, includesObj, List.class);
+
+            @SuppressWarnings("unchecked")
+            final List<Object> includes = (List<Object>)includesObj;
+            for(final Object inc : includes) {
+                checkType("Include", inc, Map.class, String.class);
+                final Include include;
+                if ( inc instanceof String ) {
+                    final ArtifactId id = ArtifactId.parse(inc.toString());
+                    include = new Include(id);
+                } else {
+                    @SuppressWarnings("unchecked")
+                    final Map<String, Object> obj = (Map<String, Object>) inc;
+                    if ( !obj.containsKey(JSONConstants.ARTIFACT_ID) ) {
+                        throw new IOException(exceptionPrefix + " include is missing required artifact id");
+                    }
+                    checkType("Include " + JSONConstants.ARTIFACT_ID, obj.get(JSONConstants.ARTIFACT_ID), String.class);
+                    final ArtifactId id = ArtifactId.parse(obj.get(JSONConstants.ARTIFACT_ID).toString());
+                    include = new Include(id);
+
+                    if ( obj.containsKey(JSONConstants.INCLUDE_REMOVALS) ) {
+                        checkType("Include removals", obj.get(JSONConstants.INCLUDE_REMOVALS), Map.class);
+                        @SuppressWarnings("unchecked")
+                        final Map<String, Object> removalObj = (Map<String, Object>) obj.get(JSONConstants.INCLUDE_REMOVALS);
+                        if ( removalObj.containsKey(JSONConstants.FEATURE_BUNDLES) ) {
+                            checkType("Include removal bundles", removalObj.get(JSONConstants.FEATURE_BUNDLES), List.class);
+                            @SuppressWarnings("unchecked")
+                            final List<Object> list = (List<Object>)removalObj.get(JSONConstants.FEATURE_BUNDLES);
+                            for(final Object val : list) {
+                                checkType("Include removal bundles", val, String.class);
+                                include.getBundleRemovals().add(ArtifactId.parse(val.toString()));
+                            }
+                        }
+                        if ( removalObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
+                            checkType("Include removal configuration", removalObj.get(JSONConstants.FEATURE_CONFIGURATIONS), List.class);
+                            @SuppressWarnings("unchecked")
+                            final List<Object> list = (List<Object>)removalObj.get(JSONConstants.FEATURE_CONFIGURATIONS);
+                            for(final Object val : list) {
+                                checkType("Include removal bundles", val, String.class);
+                                include.getConfigurationRemovals().add(val.toString());
+                            }
+                        }
+                        if ( removalObj.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) {
+                            checkType("Include removal framework properties", removalObj.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES), List.class);
+                            @SuppressWarnings("unchecked")
+                            final List<Object> list = (List<Object>)removalObj.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
+                            for(final Object val : list) {
+                                checkType("Include removal bundles", val, String.class);
+                                include.getFrameworkPropertiesRemovals().add(val.toString());
+                            }
+                        }
+                        if ( removalObj.containsKey(JSONConstants.INCLUDE_EXTENSION_REMOVALS) ) {
+                            checkType("Include removal extensions", removalObj.get(JSONConstants.INCLUDE_EXTENSION_REMOVALS), List.class);
+                            @SuppressWarnings("unchecked")
+                            final List<Object> list = (List<Object>)removalObj.get(JSONConstants.INCLUDE_EXTENSION_REMOVALS);
+                            for(final Object val : list) {
+                                checkType("Include removal extension", val, String.class, Map.class);
+                                if ( val instanceof String ) {
+                                    include.getExtensionRemovals().add(val.toString());
+                                } else {
+                                    @SuppressWarnings("unchecked")
+                                    final Map<String, Object> removalMap = (Map<String, Object>)val;
+                                    final Object nameObj = removalMap.get("name");
+                                    checkType("Include removal extension", nameObj, String.class);
+                                    if ( removalMap.containsKey("artifacts") ) {
+                                        checkType("Include removal extension artifacts", removalMap.get("artifacts"), List.class);
+                                        @SuppressWarnings("unchecked")
+                                        final List<Object> artifactList = (List<Object>)removalMap.get("artifacts");
+                                        final List<ArtifactId> ids = new ArrayList<>();
+                                        for(final Object aid : artifactList) {
+                                            checkType("Include removal extension artifact", aid, String.class);
+                                            ids.add(ArtifactId.parse(aid.toString()));
+                                        }
+                                        include.getArtifactExtensionRemovals().put(nameObj.toString(), ids);
+                                    } else {
+                                        include.getExtensionRemovals().add(nameObj.toString());
+                                    }
+                                }
+                            }
+                        }
+
+                    }
+                }
+                for(final Include i : feature.getIncludes()) {
+                    if ( i.getId().equals(include.getId()) ) {
+                        throw new IOException(exceptionPrefix + "Duplicate include of " + include.getId());
+                    }
+                }
+                feature.getIncludes().add(include);
+            }
+        }
+    }
+
+    private void readRequirements(Map<String, Object> map) throws IOException {
+        if ( map.containsKey(JSONConstants.FEATURE_REQUIREMENTS)) {
+            final Object reqObj = map.get(JSONConstants.FEATURE_REQUIREMENTS);
+            checkType(JSONConstants.FEATURE_REQUIREMENTS, reqObj, List.class);
+
+            @SuppressWarnings("unchecked")
+            final List<Object> requirements = (List<Object>)reqObj;
+            for(final Object req : requirements) {
+                checkType("Requirement", req, Map.class);
+                @SuppressWarnings("unchecked")
+                final Map<String, Object> obj = (Map<String, Object>) req;
+
+                if ( !obj.containsKey(JSONConstants.REQCAP_NAMESPACE) ) {
+                    throw new IOException(this.exceptionPrefix + "Namespace is missing for requirement");
+                }
+                checkType("Requirement namespace", obj.get(JSONConstants.REQCAP_NAMESPACE), String.class);
+
+                final Requirement r = new Requirement(obj.get(JSONConstants.REQCAP_NAMESPACE).toString());
+                feature.getRequirements().add(r);
+
+                if ( obj.containsKey(JSONConstants.REQCAP_ATTRIBUTES) ) {
+                    checkType("Requirement attributes", obj.get(JSONConstants.REQCAP_ATTRIBUTES), Map.class);
+                    @SuppressWarnings("unchecked")
+                    final Map<String, Object> attrs = (Map<String, Object>)obj.get(JSONConstants.REQCAP_ATTRIBUTES);
+                    attrs.forEach(rethrowBiConsumer((key, value) -> unmarshalAttribute(key, value, r.getAttributes()::put)));
+                }
+
+                if ( obj.containsKey(JSONConstants.REQCAP_DIRECTIVES) ) {
+                    checkType("Requirement directives", obj.get(JSONConstants.REQCAP_DIRECTIVES), Map.class);
+                    @SuppressWarnings("unchecked")
+                    final Map<String, Object> dirs = (Map<String, Object>)obj.get(JSONConstants.REQCAP_DIRECTIVES);
+                    dirs.forEach(rethrowBiConsumer((key, value) -> unmarshalDirective(key, value, r.getDirectives()::put)));
+                }
+            }
+        }
+    }
+
+    private void readCapabilities(Map<String, Object> map) throws IOException {
+        if ( map.containsKey(JSONConstants.FEATURE_CAPABILITIES)) {
+            final Object capObj = map.get(JSONConstants.FEATURE_CAPABILITIES);
+            checkType(JSONConstants.FEATURE_CAPABILITIES, capObj, List.class);
+
+            @SuppressWarnings("unchecked")
+            final List<Object> capabilities = (List<Object>)capObj;
+            for(final Object cap : capabilities) {
+                checkType("Capability", cap, Map.class);
+                @SuppressWarnings("unchecked")
+                final Map<String, Object> obj = (Map<String, Object>) cap;
+
+                if ( !obj.containsKey(JSONConstants.REQCAP_NAMESPACE) ) {
+                    throw new IOException(this.exceptionPrefix + "Namespace is missing for capability");
+                }
+                checkType("Capability namespace", obj.get(JSONConstants.REQCAP_NAMESPACE), String.class);
+
+                final Capability c = new Capability(obj.get(JSONConstants.REQCAP_NAMESPACE).toString());
+                feature.getCapabilities().add(c);
+
+                if ( obj.containsKey(JSONConstants.REQCAP_ATTRIBUTES) ) {
+                    checkType("Capability attributes", obj.get(JSONConstants.REQCAP_ATTRIBUTES), Map.class);
+                    @SuppressWarnings("unchecked")
+                    final Map<String, Object> attrs = (Map<String, Object>)obj.get(JSONConstants.REQCAP_ATTRIBUTES);
+                    attrs.forEach(rethrowBiConsumer((key, value) -> unmarshalAttribute(key, value, c.getAttributes()::put)));
+                }
+
+                if ( obj.containsKey(JSONConstants.REQCAP_DIRECTIVES) ) {
+                    checkType("Capability directives", obj.get(JSONConstants.REQCAP_DIRECTIVES), Map.class);
+                    @SuppressWarnings("unchecked")
+                    final Map<String, Object> dirs = (Map<String, Object>) obj.get(JSONConstants.REQCAP_DIRECTIVES);
+                    dirs.forEach(rethrowBiConsumer((key, value) -> unmarshalDirective(key, value, c.getDirectives()::put)));
+                }
+            }
+        }
+    }
+}
+
+
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/FeatureJSONWriter.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/FeatureJSONWriter.java
new file mode 100644
index 0000000..1c299c9
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/FeatureJSONWriter.java
@@ -0,0 +1,214 @@
+/*
+ * 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.support.json;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Capability;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Include;
+import org.apache.sling.feature.Requirement;
+
+import javax.json.Json;
+import javax.json.stream.JsonGenerator;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.sling.feature.support.util.ManifestUtil.marshalAttribute;
+import static org.apache.sling.feature.support.util.ManifestUtil.marshalDirective;
+
+
+/**
+ * Simple JSON writer for a feature
+ */
+public class FeatureJSONWriter extends JSONWriterBase {
+
+    /**
+     * Writes the feature to the writer.
+     * The writer is not closed.
+     * @param writer Writer
+     * @param feature Feature
+     * @throws IOException If writing fails
+     */
+    public static void write(final Writer writer, final Feature feature)
+    throws IOException {
+        final FeatureJSONWriter w = new FeatureJSONWriter();
+        w.writeFeature(writer, feature);
+    }
+
+    private void writeProperty(final JsonGenerator w, final String key, final String value) {
+        if ( value != null ) {
+            w.write(key, value);
+        }
+    }
+
+    private void writeFeature(final Writer writer, final Feature feature)
+    throws IOException {
+        final JsonGenerator w = Json.createGenerator(writer);
+        w.writeStartObject();
+
+        w.write(JSONConstants.FEATURE_ID, feature.getId().toMvnId());
+
+        // title, description, vendor, license
+        writeProperty(w, JSONConstants.FEATURE_TITLE, feature.getTitle());
+        writeProperty(w, JSONConstants.FEATURE_DESCRIPTION, feature.getDescription());
+        writeProperty(w, JSONConstants.FEATURE_VENDOR, feature.getVendor());
+        writeProperty(w, JSONConstants.FEATURE_LICENSE, feature.getLicense());
+
+        // upgradeOf
+        if ( feature.getUpgradeOf() != null ) {
+            writeProperty(w, JSONConstants.FEATURE_UPGRADEOF, feature.getUpgradeOf().toMvnId());
+        }
+
+        // upgrades
+        if ( !feature.getUpgrades().isEmpty() ) {
+            w.writeStartArray(JSONConstants.FEATURE_UPGRADES);
+            for(final ArtifactId id : feature.getUpgrades()) {
+                w.write(id.toMvnId());
+            }
+            w.writeEnd();
+        }
+
+        // includes
+        if ( !feature.getIncludes().isEmpty() ) {
+            w.writeStartArray(JSONConstants.FEATURE_INCLUDES);
+
+            for(final Include inc : feature.getIncludes()) {
+                if ( inc.getArtifactExtensionRemovals().isEmpty()
+                     && inc.getBundleRemovals().isEmpty()
+                     && inc.getConfigurationRemovals().isEmpty()
+                     && inc.getFrameworkPropertiesRemovals().isEmpty() ) {
+                    w.write(inc.getId().toMvnId());
+                } else {
+                    w.writeStartObject();
+                    w.write(JSONConstants.ARTIFACT_ID, inc.getId().toMvnId());
+                    w.writeStartObject(JSONConstants.INCLUDE_REMOVALS);
+                    if ( !inc.getArtifactExtensionRemovals().isEmpty()
+                         || inc.getExtensionRemovals().isEmpty() ) {
+                        w.writeStartArray(JSONConstants.INCLUDE_EXTENSION_REMOVALS);
+                        for(final String id : inc.getExtensionRemovals()) {
+                            w.write(id);
+                        }
+                        for(final Map.Entry<String, List<ArtifactId>> entry : inc.getArtifactExtensionRemovals().entrySet()) {
+                            w.writeStartObject(entry.getKey());
+                            w.writeStartArray();
+                            for(final ArtifactId id : entry.getValue()) {
+                                w.write(id.toMvnId());
+                            }
+                            w.writeEnd();
+                            w.writeEnd();
+                        }
+                        w.writeEnd();
+                    }
+                    if ( !inc.getConfigurationRemovals().isEmpty() ) {
+                        w.writeStartArray(JSONConstants.FEATURE_CONFIGURATIONS);
+                        for(final String val : inc.getConfigurationRemovals()) {
+                            w.write(val);
+                        }
+                        w.writeEnd();
+                    }
+                    if ( !inc.getBundleRemovals().isEmpty() ) {
+                        w.writeStartArray(JSONConstants.FEATURE_BUNDLES);
+                        for(final ArtifactId val : inc.getBundleRemovals()) {
+                            w.write(val.toMvnId());
+                        }
+                        w.writeEnd();
+                    }
+                    if ( !inc.getFrameworkPropertiesRemovals().isEmpty() ) {
+                        w.writeStartArray(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
+                        for(final String val : inc.getFrameworkPropertiesRemovals()) {
+                            w.write(val);
+                        }
+                        w.writeEnd();
+                    }
+                    w.writeEnd();
+                    w.writeEnd();
+                }
+            }
+            w.writeEnd();
+        }
+
+        // requirements
+        if ( !feature.getRequirements().isEmpty() ) {
+            w.writeStartArray(JSONConstants.FEATURE_REQUIREMENTS);
+
+            for(final Requirement req : feature.getRequirements()) {
+                w.writeStartObject();
+                w.write(JSONConstants.REQCAP_NAMESPACE, req.getNamespace());
+                if ( !req.getAttributes().isEmpty() ) {
+                    w.writeStartObject(JSONConstants.REQCAP_ATTRIBUTES);
+                    req.getAttributes().forEach((key, value) -> marshalAttribute(key, value, w::write));
+                    w.writeEnd();
+                }
+                if ( !req.getDirectives().isEmpty() ) {
+                    w.writeStartObject(JSONConstants.REQCAP_DIRECTIVES);
+                    req.getDirectives().forEach((key, value) -> marshalDirective(key, value, w::write));
+                    w.writeEnd();
+                }
+                w.writeEnd();
+            }
+            w.writeEnd();
+        }
+
+        // capabilities
+        if ( !feature.getCapabilities().isEmpty() ) {
+            w.writeStartArray(JSONConstants.FEATURE_CAPABILITIES);
+
+            for(final Capability cap : feature.getCapabilities()) {
+                w.writeStartObject();
+                w.write(JSONConstants.REQCAP_NAMESPACE, cap.getNamespace());
+                if ( !cap.getAttributes().isEmpty() ) {
+                    w.writeStartObject(JSONConstants.REQCAP_ATTRIBUTES);
+                    cap.getAttributes().forEach((key, value) -> marshalAttribute(key, value, w::write));
+                    w.writeEnd();
+                }
+                if ( !cap.getDirectives().isEmpty() ) {
+                    w.writeStartObject(JSONConstants.REQCAP_DIRECTIVES);
+                    cap.getDirectives().forEach((key, value) -> marshalDirective(key, value, w::write));
+                    w.writeEnd();
+                }
+                w.writeEnd();
+            }
+            w.writeEnd();
+        }
+
+        // bundles
+        writeBundles(w, feature.getBundles(), feature.getConfigurations());
+
+        // configurations
+        final Configurations cfgs = new Configurations();
+        for(final Configuration cfg : feature.getConfigurations()) {
+            final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT);
+            if (  artifactProp == null ) {
+                cfgs.add(cfg);
+            }
+        }
+        writeConfigurations(w, cfgs);
+
+        // framework properties
+        writeFrameworkProperties(w, feature.getFrameworkProperties());
+
+        // extensions
+        writeExtensions(w, feature.getExtensions(), feature.getConfigurations());
+
+        w.writeEnd();
+        w.flush();
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/JSONConstants.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/JSONConstants.java
new file mode 100644
index 0000000..d34bd80
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/JSONConstants.java
@@ -0,0 +1,88 @@
+/*
+ * 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.support.json;
+
+import org.apache.sling.feature.Configuration;
+
+import java.util.Arrays;
+import java.util.List;
+
+public abstract class JSONConstants {
+
+    public static final String FEATURE_ID = "id";
+
+    public static final String FEATURE_BUNDLES = "bundles";
+
+    public static final String FEATURE_FRAMEWORK_PROPERTIES = "framework-properties";
+
+    public static final String FEATURE_CONFIGURATIONS = "configurations";
+
+    public static final String FEATURE_INCLUDES = "includes";
+
+    public static final String FEATURE_REQUIREMENTS = "requirements";
+
+    public static final String FEATURE_CAPABILITIES = "capabilities";
+
+    public static final String FEATURE_TITLE = "title";
+
+    public static final String FEATURE_DESCRIPTION = "description";
+
+    public static final String FEATURE_VENDOR = "vendor";
+
+    public static final String FEATURE_LICENSE = "license";
+
+    public static final String FEATURE_UPGRADES = "upgrades";
+
+    public static final String FEATURE_UPGRADEOF = "upgradeOf";
+
+    public static final List<String> FEATURE_KNOWN_PROPERTIES = Arrays.asList(FEATURE_ID,
+            FEATURE_BUNDLES,
+            FEATURE_FRAMEWORK_PROPERTIES,
+            FEATURE_CONFIGURATIONS,
+            FEATURE_INCLUDES,
+            FEATURE_REQUIREMENTS,
+            FEATURE_CAPABILITIES,
+            FEATURE_TITLE,
+            FEATURE_DESCRIPTION,
+            FEATURE_VENDOR,
+            FEATURE_LICENSE,
+            FEATURE_UPGRADES,
+            FEATURE_UPGRADEOF);
+
+    public static final String ARTIFACT_ID = "id";
+
+    public static final List<String> ARTIFACT_KNOWN_PROPERTIES = Arrays.asList(ARTIFACT_ID,
+            Configuration.PROP_ARTIFACT,
+            FEATURE_CONFIGURATIONS);
+
+    public static final String INCLUDE_REMOVALS = "removals";
+
+    public static final String INCLUDE_EXTENSION_REMOVALS = "extensions";
+
+    public static final String REQCAP_NAMESPACE = "namespace";
+    public static final String REQCAP_ATTRIBUTES = "attributes";
+    public static final String REQCAP_DIRECTIVES = "directives";
+
+    public static final String APP_FRAMEWORK = "frameworkId";
+    public static final String APP_FEATURES = "features";
+
+    public static final List<String> APP_KNOWN_PROPERTIES = Arrays.asList(APP_FRAMEWORK,
+            FEATURE_BUNDLES,
+            FEATURE_FRAMEWORK_PROPERTIES,
+            FEATURE_CONFIGURATIONS,
+            APP_FEATURES);
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/JSONReaderBase.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/JSONReaderBase.java
new file mode 100644
index 0000000..411b734
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/JSONReaderBase.java
@@ -0,0 +1,396 @@
+/*
+ * 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.support.json;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonStructure;
+import javax.json.JsonWriter;
+
+import org.apache.felix.configurator.impl.json.JSMin;
+import org.apache.felix.configurator.impl.json.JSONUtil;
+import org.apache.felix.configurator.impl.json.TypeConverter;
+import org.apache.felix.configurator.impl.model.Config;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Extensions;
+import org.apache.sling.feature.KeyValueMap;
+
+/**
+ * Common methods for JSON reading.
+ */
+abstract class JSONReaderBase {
+
+    /** The optional location. */
+    protected final String location;
+
+    /** Exception prefix containing the location (if set) */
+    protected final String exceptionPrefix;
+
+    /**
+     * Private constructor
+     * @param location Optional location
+     */
+    JSONReaderBase(final String location) {
+        this.location = location;
+        if ( location == null ) {
+            exceptionPrefix = "";
+        } else {
+            exceptionPrefix = location + " : ";
+        }
+    }
+
+    protected String minify(final Reader reader) throws IOException {
+       // minify JSON (remove comments)
+        final String contents;
+        try ( final Writer out = new StringWriter()) {
+            final JSMin min = new JSMin(reader, out);
+            min.jsmin();
+            contents = out.toString();
+        }
+        return contents;
+    }
+
+    protected String getProperty(final Map<String, Object> map, final String key) throws IOException {
+        final Object val = map.get(key);
+        if ( val != null ) {
+            checkType(key, val, String.class);
+            return val.toString();
+        }
+        return null;
+    }
+
+    /**
+     * Read the bundles / start levels section
+     * @param map The map describing the feature
+     * @param container The bundles container
+     * @param configContainer The configurations container
+     * @throws IOException If the json is invalid.
+     */
+    protected void readBundles(
+            final Map<String, Object> map,
+            final Bundles container,
+            final Configurations configContainer) throws IOException {
+        if ( map.containsKey(JSONConstants.FEATURE_BUNDLES)) {
+            final Object bundlesObj = map.get(JSONConstants.FEATURE_BUNDLES);
+            checkType(JSONConstants.FEATURE_BUNDLES, bundlesObj, Map.class);
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> bundles = (Map<String, Object>) bundlesObj;
+            for(final String startLevelVal : bundles.keySet()) {
+                int startLevel = -1;
+                try {
+                    startLevel = Integer.valueOf(startLevelVal);
+                } catch ( final NumberFormatException nfe) {
+                    throw new IOException(exceptionPrefix + "Illegal start level '" + startLevelVal + "'");
+                }
+                final Object val = bundles.get(startLevelVal);
+                final List<Artifact> list = new ArrayList<>();
+                readArtifacts("startLevel", "bundle", list, val, configContainer);
+                for(final Artifact a : list) {
+                    if ( container.containsSame(a.getId()) ) {
+                        throw new IOException(exceptionPrefix + "Duplicate bundle " + a.getId().toMvnId());
+                    }
+                    container.add(startLevel, a);
+                }
+            }
+        }
+    }
+
+    protected void readArtifacts(final String section,
+            final String artifactType,
+            final List<Artifact> artifacts,
+            final Object listObj,
+            final Configurations container)
+    throws IOException {
+        checkType(section, listObj, List.class);
+        @SuppressWarnings("unchecked")
+        final List<Object> list = (List<Object>) listObj;
+        for(final Object entry : list) {
+            final Artifact artifact;
+            checkType(artifactType, entry, Map.class, String.class);
+            if ( entry instanceof String ) {
+                artifact = new Artifact(ArtifactId.parse(entry.toString()));
+            } else {
+                @SuppressWarnings("unchecked")
+                final Map<String, Object> bundleObj = (Map<String, Object>) entry;
+                if ( !bundleObj.containsKey(JSONConstants.ARTIFACT_ID) ) {
+                    throw new IOException(exceptionPrefix + " " + artifactType + " is missing required artifact id");
+                }
+                checkType(artifactType + " " + JSONConstants.ARTIFACT_ID, bundleObj.get(JSONConstants.ARTIFACT_ID), String.class);
+                final ArtifactId id = ArtifactId.parse(bundleObj.get(JSONConstants.ARTIFACT_ID).toString());
+
+                artifact = new Artifact(id);
+                for(final Map.Entry<String, Object> metadataEntry : bundleObj.entrySet()) {
+                    final String key = metadataEntry.getKey();
+                    if ( JSONConstants.ARTIFACT_KNOWN_PROPERTIES.contains(key) ) {
+                        continue;
+                    }
+                    checkType(artifactType + " metadata " + key, metadataEntry.getValue(), String.class, Number.class, Boolean.class);
+                    artifact.getMetadata().put(key, metadataEntry.getValue().toString());
+                }
+                if ( bundleObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
+                    checkType(artifactType + " configurations", bundleObj.get(JSONConstants.FEATURE_CONFIGURATIONS), Map.class);
+                    addConfigurations(bundleObj, artifact, container);
+                }
+            }
+            artifacts.add(artifact);
+        }
+    }
+
+    protected void addConfigurations(final Map<String, Object> map,
+            final Artifact artifact,
+            final Configurations container) throws IOException {
+        final JSONUtil.Report report = new JSONUtil.Report();
+        @SuppressWarnings("unchecked")
+        final List<Config> configs = JSONUtil.readConfigurationsJSON(new TypeConverter(null),
+                0, "", (Map<String, ?>)map.get(JSONConstants.FEATURE_CONFIGURATIONS), report);
+        if ( !report.errors.isEmpty() || !report.warnings.isEmpty() ) {
+            final StringBuilder builder = new StringBuilder(exceptionPrefix);
+            builder.append("Errors in configurations:");
+            for(final String w : report.warnings) {
+                builder.append("\n");
+                builder.append(w);
+            }
+            for(final String e : report.errors) {
+                builder.append("\n");
+                builder.append(e);
+            }
+            throw new IOException(builder.toString());
+        }
+        for(final Config c : configs) {
+            final int pos = c.getPid().indexOf('~');
+            final Configuration config;
+            if ( pos != -1 ) {
+                config = new Configuration(c.getPid().substring(0, pos), c.getPid().substring(pos + 1));
+            } else {
+                config = new Configuration(c.getPid());
+            }
+            final Enumeration<String> keyEnum = c.getProperties().keys();
+            while ( keyEnum.hasMoreElements() ) {
+                final String key = keyEnum.nextElement();
+                if ( key.startsWith(":configurator:") ) {
+                    throw new IOException(exceptionPrefix + "Configuration must not define configurator property " + key);
+                }
+                final Object val = c.getProperties().get(key);
+                config.getProperties().put(key, val);
+            }
+            if ( config.getProperties().get(Configuration.PROP_ARTIFACT) != null ) {
+                throw new IOException(exceptionPrefix + "Configuration must not define property " + Configuration.PROP_ARTIFACT);
+            }
+            if ( artifact != null ) {
+                config.getProperties().put(Configuration.PROP_ARTIFACT, artifact.getId().toMvnId());
+            }
+            for(final Configuration current : container) {
+                if ( current.equals(config) ) {
+                    throw new IOException(exceptionPrefix + "Duplicate configuration " + config);
+                }
+            }
+            container.add(config);
+        }
+
+    }
+
+
+    protected void readConfigurations(final Map<String, Object> map,
+            final Configurations container) throws IOException {
+        if ( map.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
+            checkType(JSONConstants.FEATURE_CONFIGURATIONS, map.get(JSONConstants.FEATURE_CONFIGURATIONS), Map.class);
+            addConfigurations(map, null, container);
+        }
+    }
+
+    protected void readFrameworkProperties(final Map<String, Object> map,
+            final KeyValueMap container) throws IOException {
+        if ( map.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) {
+            final Object propsObj= map.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
+            checkType(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES, propsObj, Map.class);
+
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> props = (Map<String, Object>) propsObj;
+            for(final Map.Entry<String, Object> entry : props.entrySet()) {
+                checkType("framework property value", entry.getValue(), String.class, Boolean.class, Number.class);
+                if ( container.get(entry.getKey()) != null ) {
+                    throw new IOException(this.exceptionPrefix + "Duplicate framework property " + entry.getKey());
+                }
+                container.put(entry.getKey(), entry.getValue().toString());
+            }
+
+        }
+    }
+
+    protected void readExtensions(final Map<String, Object> map,
+            final List<String> keywords,
+            final Extensions container,
+            final Configurations configContainer) throws IOException {
+        final Set<String> keySet = new HashSet<>(map.keySet());
+        keySet.removeAll(keywords);
+        // the remaining keys are considered extensions!
+        for(final String key : keySet) {
+            final int pos = key.indexOf(':');
+            final String postfix = pos == -1 ? null : key.substring(pos + 1);
+            final int sep = (postfix == null ? key.indexOf('|') : postfix.indexOf('|'));
+            final String name;
+            final String type;
+            final String optional;
+            if ( pos == -1 ) {
+                type = ExtensionType.ARTIFACTS.name();
+                if ( sep == -1 ) {
+                    name = key;
+                    optional = Boolean.FALSE.toString();
+                } else {
+                    name = key.substring(0, sep);
+                    optional = key.substring(sep + 1);
+                }
+            } else {
+                name = key.substring(0, pos);
+                if ( sep == -1 ) {
+                    type = postfix;
+                    optional = Boolean.FALSE.toString();
+                } else {
+                    type = postfix.substring(0, sep);
+                    optional = postfix.substring(sep + 1);
+                }
+            }
+            if ( JSONConstants.APP_KNOWN_PROPERTIES.contains(name) ) {
+                throw new IOException(this.exceptionPrefix + "Extension is using reserved name : " + name);
+            }
+            if ( JSONConstants.FEATURE_KNOWN_PROPERTIES.contains(name) ) {
+                throw new IOException(this.exceptionPrefix + "Extension is using reserved name : " + name);
+            }
+            if ( container.getByName(name) != null ) {
+                throw new IOException(exceptionPrefix + "Duplicate extension with name " + name);
+            }
+
+            final ExtensionType extType = ExtensionType.valueOf(type);
+            final boolean opt = Boolean.valueOf(optional).booleanValue();
+
+            final Extension ext = new Extension(extType, name, opt);
+            final Object value = map.get(key);
+            switch ( extType ) {
+                case ARTIFACTS : final List<Artifact> list = new ArrayList<>();
+                                 readArtifacts("Extension " + name, "artifact", list, value, configContainer);
+                                 for(final Artifact a : list) {
+                                     if ( ext.getArtifacts().contains(a) ) {
+                                         throw new IOException(exceptionPrefix + "Duplicate artifact in extension " + name + " : " + a.getId().toMvnId());
+                                     }
+                                     ext.getArtifacts().add(a);
+                                 }
+                                 break;
+                case JSON : checkType("JSON Extension " + name, value, Map.class, List.class);
+                            final JsonStructure struct = build(value);
+                            try ( final StringWriter w = new StringWriter()) {
+                                final JsonWriter jw = Json.createWriter(w);
+                                jw.write(struct);
+                                w.flush();
+                                ext.setJSON(w.toString());
+                            }
+                            break;
+                case TEXT : checkType("Text Extension " + name, value, String.class);
+                            ext.setText(value.toString());
+                            break;
+            }
+
+            container.add(ext);
+        }
+    }
+
+    private JsonStructure build(final Object value) {
+        if ( value instanceof List ) {
+            @SuppressWarnings("unchecked")
+            final List<Object> list = (List<Object>)value;
+            final JsonArrayBuilder builder = Json.createArrayBuilder();
+            for(final Object obj : list) {
+                if ( obj instanceof String ) {
+                    builder.add(obj.toString());
+                } else if ( obj instanceof Long ) {
+                    builder.add((Long)obj);
+                } else if ( obj instanceof Double ) {
+                    builder.add((Double)obj);
+                } else if (obj instanceof Boolean ) {
+                    builder.add((Boolean)obj);
+                } else if ( obj instanceof Map ) {
+                    builder.add(build(obj));
+                } else if ( obj instanceof List ) {
+                    builder.add(build(obj));
+                }
+
+            }
+            return builder.build();
+        } else if ( value instanceof Map ) {
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> map = (Map<String, Object>)value;
+            final JsonObjectBuilder builder = Json.createObjectBuilder();
+            for(final Map.Entry<String, Object> entry : map.entrySet()) {
+                if ( entry.getValue() instanceof String ) {
+                    builder.add(entry.getKey(), entry.getValue().toString());
+                } else if ( entry.getValue() instanceof Long ) {
+                    builder.add(entry.getKey(), (Long)entry.getValue());
+                } else if ( entry.getValue() instanceof Double ) {
+                    builder.add(entry.getKey(), (Double)entry.getValue());
+                } else if ( entry.getValue() instanceof Boolean ) {
+                    builder.add(entry.getKey(), (Boolean)entry.getValue());
+                } else if ( entry.getValue() instanceof Map ) {
+                    builder.add(entry.getKey(), build(entry.getValue()));
+                } else if ( entry.getValue() instanceof List ) {
+                    builder.add(entry.getKey(), build(entry.getValue()));
+                }
+            }
+            return builder.build();
+        }
+        return null;
+    }
+
+    /**
+     * Check if the value is one of the provided types
+     * @param key A key for the error message
+     * @param val The value to check
+     * @param types The allowed types
+     * @throws IOException If the val is not of the specified types
+     */
+    protected void checkType(final String key, final Object val, Class<?>...types) throws IOException {
+        boolean valid = false;
+        for(final Class<?> c : types) {
+            if ( c.isInstance(val) ) {
+                valid = true;
+                break;
+            }
+        }
+        if ( !valid ) {
+            throw new IOException(this.exceptionPrefix + "Key " + key + " is not one of the allowed types " + Arrays.toString(types) + " : " + val.getClass());
+        }
+    }
+}
+
+
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/JSONWriterBase.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/JSONWriterBase.java
new file mode 100644
index 0000000..40c8c7c
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/JSONWriterBase.java
@@ -0,0 +1,235 @@
+/*
+ * 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.support.json;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.KeyValueMap;
+
+import javax.json.Json;
+import javax.json.JsonStructure;
+import javax.json.stream.JsonGenerator;
+import java.io.StringReader;
+import java.lang.reflect.Array;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Common functionality for writing JSON
+ */
+abstract class JSONWriterBase {
+
+
+    protected void writeBundles(final JsonGenerator w,
+            final Bundles bundles,
+            final Configurations allConfigs) {
+        // bundles
+        if ( !bundles.getBundlesByStartLevel().isEmpty() ) {
+            w.writeStartObject(JSONConstants.FEATURE_BUNDLES);
+            for(final Map.Entry<Integer, List<Artifact>> entry : bundles.getBundlesByStartLevel().entrySet()) {
+                w.writeStartArray(String.valueOf(entry.getKey()));
+
+                for(final Artifact artifact : entry.getValue()) {
+                    final Configurations cfgs = new Configurations();
+                    for(final Configuration cfg : allConfigs) {
+                        final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT);
+                        if (  artifact.getId().toMvnId().equals(artifactProp) ) {
+                            cfgs.add(cfg);
+                        }
+                    }
+                    if ( artifact.getMetadata().isEmpty() && cfgs.isEmpty() ) {
+                        w.write(artifact.getId().toMvnId());
+                    } else {
+                        w.writeStartObject();
+                        w.write(JSONConstants.ARTIFACT_ID, artifact.getId().toMvnId());
+
+                        for(final Map.Entry<String, String> me : artifact.getMetadata()) {
+                            w.write(me.getKey(), me.getValue());
+                        }
+
+                        writeConfigurations(w, cfgs);
+                        w.writeEnd();
+                    }
+                }
+                w.writeEnd();
+            }
+
+            w.writeEnd();
+        }
+    }
+
+    /**
+     * Write the list of configurations into a "configurations" element
+     * @param w The json generator
+     * @param cfgs The list of configurations
+     */
+    protected void writeConfigurations(final JsonGenerator w, final Configurations cfgs) {
+        if ( !cfgs.isEmpty() ) {
+            w.writeStartObject(JSONConstants.FEATURE_CONFIGURATIONS);
+
+            writeConfigurationsMap(w, cfgs);
+
+            w.writeEnd();
+        }
+    }
+
+    /**
+     * Write the list of configurations into a "configurations" element
+     * @param w The json generator
+     * @param cfgs The list of configurations
+     */
+    protected void writeConfigurationsMap(final JsonGenerator w, final Configurations cfgs) {
+        for(final Configuration cfg : cfgs) {
+            final String key;
+            if ( cfg.isFactoryConfiguration() ) {
+                key = cfg.getFactoryPid() + "~" + cfg.getName();
+            } else {
+                key = cfg.getPid();
+            }
+            w.writeStartObject(key);
+
+            final Enumeration<String> e = cfg.getProperties().keys();
+            while ( e.hasMoreElements() ) {
+                final String name = e.nextElement();
+                if ( Configuration.PROP_ARTIFACT.equals(name) ) {
+                    continue;
+                }
+
+                final Object val = cfg.getProperties().get(name);
+
+                String typePostFix = null;
+                final Object typeCheck;
+                if ( val.getClass().isArray() ) {
+                    if ( Array.getLength(val) > 0 ) {
+                        typeCheck = Array.get(val, 0);
+                    } else {
+                        typeCheck = null;
+                    }
+                } else {
+                    typeCheck = val;
+                }
+
+                if ( typeCheck instanceof Integer ) {
+                    typePostFix = ":Integer";
+                } else if ( typeCheck instanceof Byte ) {
+                    typePostFix = ":Byte";
+                } else if ( typeCheck instanceof Character ) {
+                    typePostFix = ":Character";
+                } else if ( typeCheck instanceof Float ) {
+                    typePostFix = ":Float";
+                }
+
+                if ( val.getClass().isArray() ) {
+                    w.writeStartArray(name);
+                    for(int i=0; i<Array.getLength(val);i++ ) {
+                        final Object obj = Array.get(val, i);
+                        if ( typePostFix == null ) {
+                            if ( obj instanceof String ) {
+                                w.write((String)obj);
+                            } else if ( obj instanceof Boolean ) {
+                                w.write((Boolean)obj);
+                            } else if ( obj instanceof Long ) {
+                                w.write((Long)obj);
+                            } else if ( obj instanceof Double ) {
+                                w.write((Double)obj);
+                            }
+                        } else {
+                            w.write(obj.toString());
+                        }
+                    }
+                    w.writeEnd();
+                } else {
+                    if ( typePostFix == null ) {
+                        if ( val instanceof String ) {
+                            w.write(name, (String)val);
+                        } else if ( val instanceof Boolean ) {
+                            w.write(name, (Boolean)val);
+                        } else if ( val instanceof Long ) {
+                            w.write(name, (Long)val);
+                        } else if ( val instanceof Double ) {
+                            w.write(name, (Double)val);
+                        }
+                    } else {
+                        w.write(name + typePostFix, val.toString());
+                    }
+                }
+            }
+
+            w.writeEnd();
+        }
+    }
+
+    protected void writeFrameworkProperties(final JsonGenerator w, final KeyValueMap props) {
+        // framework properties
+        if ( !props.isEmpty() ) {
+            w.writeStartObject(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
+
+            for(final Map.Entry<String, String> entry : props) {
+                w.write(entry.getKey(), entry.getValue());
+            }
+            w.writeEnd();
+        }
+    }
+
+    protected void writeExtensions(final JsonGenerator w,
+            final List<Extension> extensions,
+            final Configurations allConfigs) {
+        for(final Extension ext : extensions) {
+            final String key = ext.getName() + ":" + ext.getType().name() + "|" + ext.isOptional();
+            if ( ext.getType() == ExtensionType.JSON ) {
+                final JsonStructure struct;
+                try ( final StringReader reader = new StringReader(ext.getJSON()) ) {
+                    struct = Json.createReader(reader).read();
+                }
+                w.write(key, struct);
+            } else if ( ext.getType() == ExtensionType.TEXT ) {
+                w.write(key, ext.getText());
+            } else {
+                w.writeStartArray(key);
+                for(final Artifact artifact : ext.getArtifacts()) {
+                    final Configurations artifactCfgs = new Configurations();
+                    for(final Configuration cfg : allConfigs) {
+                        final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT);
+                        if (  artifact.getId().toMvnId().equals(artifactProp) ) {
+                            artifactCfgs.add(cfg);
+                        }
+                    }
+                    if ( artifact.getMetadata().isEmpty() && artifactCfgs.isEmpty() ) {
+                        w.write(artifact.getId().toMvnId());
+                    } else {
+                        w.writeStartObject();
+                        w.write(JSONConstants.ARTIFACT_ID, artifact.getId().toMvnId());
+
+                        for(final Map.Entry<String, String> me : artifact.getMetadata()) {
+                            w.write(me.getKey(), me.getValue());
+                        }
+
+                        writeConfigurations(w, artifactCfgs);
+                        w.writeEnd();
+                    }
+                }
+                w.writeEnd();
+            }
+        }
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/package-info.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/package-info.java
new file mode 100644
index 0000000..968df84
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/json/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.support.json;
+
+
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/package-info.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/package-info.java
new file mode 100644
index 0000000..8eae6a4
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/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.support;
+
+
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/spi/ArtifactProvider.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/spi/ArtifactProvider.java
new file mode 100644
index 0000000..0534182
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/spi/ArtifactProvider.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.support.spi;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * The artifact provider is an extension point for providing artifacts
+ * from different sources, like for example s3.
+ */
+public interface ArtifactProvider {
+
+    /**
+     * The protocol name of the provider, e.g. "s3"
+     * @return The protocol name.
+     */
+    String getProtocol();
+
+    /**
+     * Initialize the provider.
+     * @param context The context
+     * @throws IOException If the provider can't be initialized.
+     */
+    void init(ArtifactProviderContext context) throws IOException;
+
+    /**
+     * Shutdown the provider.
+     */
+    void shutdown();
+
+    /**
+     * Get a local file for the artifact URL.
+     *
+     * @param url Artifact url
+     * @param relativeCachePath A relative path that can be used as a cache path
+     *                          by the provider. The path does not start with a slash.
+     * @return A file if the artifact exists or {@code null}
+     */
+    File getArtifact(String url, String relativeCachePath);
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/spi/ArtifactProviderContext.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/spi/ArtifactProviderContext.java
new file mode 100644
index 0000000..b6e11bc
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/spi/ArtifactProviderContext.java
@@ -0,0 +1,46 @@
+/*
+ * 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.support.spi;
+
+import java.io.File;
+
+/**
+ * This is the context for the artifact providers
+ */
+public interface ArtifactProviderContext {
+
+    /**
+     * Get the cache directory
+     * @return The cache directory.
+     */
+    File getCacheDirectory();
+
+    /**
+     * Inform about an artifact found in the cache.
+     */
+    void incCachedArtifacts();
+
+    /**
+     * Inform about an artifact being downloaded
+     */
+    void incDownloadedArtifacts();
+
+    /**
+     * Inform about an artifact found locally.
+     */
+    void incLocalArtifacts();
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/spi/package-info.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/spi/package-info.java
new file mode 100644
index 0000000..da39f4a
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/spi/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.support.spi;
+
+
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/CapabilityMatcher.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/CapabilityMatcher.java
new file mode 100644
index 0000000..74b1466
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/CapabilityMatcher.java
@@ -0,0 +1,621 @@
+package org.apache.sling.feature.support.util;
+
+import org.apache.sling.feature.Capability;
+import org.apache.sling.feature.Requirement;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static org.osgi.framework.Constants.RESOLUTION_DIRECTIVE;
+import static org.osgi.framework.Constants.RESOLUTION_OPTIONAL;
+
+/*
+ * 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.
+ */
+public class CapabilityMatcher
+{
+    public static Set<Capability> match(Set<Capability> caps, final SimpleFilter sf)
+    {
+        Set<Capability> matches = Collections.newSetFromMap(new ConcurrentHashMap<Capability, Boolean>());
+
+        if (sf.getOperation() == SimpleFilter.MATCH_ALL)
+        {
+            matches.addAll(caps);
+        }
+        else if (sf.getOperation() == SimpleFilter.AND)
+        {
+            // Evaluate each subfilter against the remaining capabilities.
+            // For AND we calculate the intersection of each subfilter.
+            // We can short-circuit the AND operation if there are no
+            // remaining capabilities.
+            final List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
+            for (int i = 0; (caps.size() > 0) && (i < sfs.size()); i++)
+            {
+                matches = match(caps, sfs.get(i));
+                caps = matches;
+            }
+        }
+        else if (sf.getOperation() == SimpleFilter.OR)
+        {
+            // Evaluate each subfilter against the remaining capabilities.
+            // For OR we calculate the union of each subfilter.
+            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
+            for (int i = 0; i < sfs.size(); i++)
+            {
+                matches.addAll(match(caps, sfs.get(i)));
+            }
+        }
+        else if (sf.getOperation() == SimpleFilter.NOT)
+        {
+            // Evaluate each subfilter against the remaining capabilities.
+            // For OR we calculate the union of each subfilter.
+            matches.addAll(caps);
+            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
+            for (int i = 0; i < sfs.size(); i++)
+            {
+                matches.removeAll(match(caps, sfs.get(i)));
+            }
+        }
+        else
+        {
+            for (Iterator<Capability> it = caps.iterator(); it.hasNext(); )
+            {
+                Capability cap = it.next();
+                Object lhs = cap.getAttributes().get(sf.getName());
+                if (lhs != null)
+                {
+                    if (compare(lhs, sf.getValue(), sf.getOperation()))
+                    {
+                        matches.add(cap);
+                    }
+                }
+            }
+        }
+
+        return matches;
+    }
+
+    public static boolean matches(Capability cap, SimpleFilter sf)
+    {
+        return matchesInternal(cap, sf) && matchMandatory(cap, sf);
+    }
+
+    private static boolean matchesInternal(Capability cap, SimpleFilter sf)
+    {
+        boolean matched = true;
+
+        if (sf.getOperation() == SimpleFilter.MATCH_ALL)
+        {
+            matched = true;
+        }
+        else if (sf.getOperation() == SimpleFilter.AND)
+        {
+            // Evaluate each subfilter against the remaining capabilities.
+            // For AND we calculate the intersection of each subfilter.
+            // We can short-circuit the AND operation if there are no
+            // remaining capabilities.
+            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
+            for (int i = 0; matched && (i < sfs.size()); i++)
+            {
+                matched = matchesInternal(cap, sfs.get(i));
+            }
+        }
+        else if (sf.getOperation() == SimpleFilter.OR)
+        {
+            // Evaluate each subfilter against the remaining capabilities.
+            // For OR we calculate the union of each subfilter.
+            matched = false;
+            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
+            for (int i = 0; !matched && (i < sfs.size()); i++)
+            {
+                matched = matchesInternal(cap, sfs.get(i));
+            }
+        }
+        else if (sf.getOperation() == SimpleFilter.NOT)
+        {
+            // Evaluate each subfilter against the remaining capabilities.
+            // For OR we calculate the union of each subfilter.
+            List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue();
+            for (int i = 0; i < sfs.size(); i++)
+            {
+                matched = !(matchesInternal(cap, sfs.get(i)));
+            }
+        }
+        else
+        {
+            matched = false;
+            Object lhs = cap.getAttributes().get(sf.getName());
+            if (lhs != null)
+            {
+                matched = compare(lhs, sf.getValue(), sf.getOperation());
+            }
+        }
+
+        return matched;
+    }
+
+    private static Set<Capability> matchMandatory(
+            Set<Capability> caps, SimpleFilter sf)
+    {
+        for (Iterator<Capability> it = caps.iterator(); it.hasNext(); )
+        {
+            Capability cap = it.next();
+            if (!matchMandatory(cap, sf))
+            {
+                it.remove();
+            }
+        }
+        return caps;
+    }
+
+    private static boolean matchMandatory(Capability cap, SimpleFilter sf)
+    {
+        Map<String, Object> attrs = cap.getAttributes();
+        for (Entry<String, Object> entry : attrs.entrySet())
+        {
+            if (isAttributeMandatory(cap, entry.getKey())
+                    && !matchMandatoryAttribute(entry.getKey(), sf))
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static boolean matchMandatoryAttribute(String attrName, SimpleFilter sf)
+    {
+        if ((sf.getName() != null) && sf.getName().equals(attrName))
+        {
+            return true;
+        }
+        else if (sf.getOperation() == SimpleFilter.AND)
+        {
+            List list = (List) sf.getValue();
+            for (int i = 0; i < list.size(); i++)
+            {
+                SimpleFilter sf2 = (SimpleFilter) list.get(i);
+                if ((sf2.getName() != null)
+                        && sf2.getName().equals(attrName))
+                {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private static final Class<?>[] STRING_CLASS = new Class[] { String.class };
+    private static final String VALUE_OF_METHOD_NAME = "valueOf";
+
+    private static boolean compare(Object lhs, Object rhsUnknown, int op)
+    {
+        if (lhs == null)
+        {
+            return false;
+        }
+
+        // If this is a PRESENT operation, then just return true immediately
+        // since we wouldn't be here if the attribute wasn't present.
+        if (op == SimpleFilter.PRESENT)
+        {
+            return true;
+        }
+
+        //Need a special case here when lhs is a Version and rhs is a VersionRange
+        //Version is comparable so we need to check this first
+        if(lhs instanceof Version && op == SimpleFilter.EQ)
+        {
+            Object rhs = null;
+            try
+            {
+                rhs = coerceType(lhs, (String) rhsUnknown);
+            }
+            catch (Exception ex)
+            {
+                //Do nothing will check later if rhs is null
+            }
+
+            if(rhs != null && rhs instanceof VersionRange)
+            {
+                return ((VersionRange)rhs).isInRange((Version)lhs);
+            }
+        }
+
+        // If the type is comparable, then we can just return the
+        // result immediately.
+        if (lhs instanceof Comparable)
+        {
+            // Spec says SUBSTRING is false for all types other than string.
+            if ((op == SimpleFilter.SUBSTRING) && !(lhs instanceof String))
+            {
+                return false;
+            }
+
+            Object rhs;
+            if (op == SimpleFilter.SUBSTRING)
+            {
+                rhs = rhsUnknown;
+            }
+            else
+            {
+                try
+                {
+                    rhs = coerceType(lhs, (String) rhsUnknown);
+                }
+                catch (Exception ex)
+                {
+                    return false;
+                }
+            }
+
+            switch (op)
+            {
+                case SimpleFilter.EQ :
+                    try
+                    {
+                        return (((Comparable) lhs).compareTo(rhs) == 0);
+                    }
+                    catch (Exception ex)
+                    {
+                        return false;
+                    }
+                case SimpleFilter.GTE :
+                    try
+                    {
+                        return (((Comparable) lhs).compareTo(rhs) >= 0);
+                    }
+                    catch (Exception ex)
+                    {
+                        return false;
+                    }
+                case SimpleFilter.LTE :
+                    try
+                    {
+                        return (((Comparable) lhs).compareTo(rhs) <= 0);
+                    }
+                    catch (Exception ex)
+                    {
+                        return false;
+                    }
+                case SimpleFilter.APPROX :
+                    return compareApproximate(lhs, rhs);
+                case SimpleFilter.SUBSTRING :
+                    return SimpleFilter.compareSubstring((List<String>) rhs, (String) lhs);
+                default:
+                    throw new RuntimeException(
+                            "Unknown comparison operator: " + op);
+            }
+        }
+        // Booleans do not implement comparable, so special case them.
+        else if (lhs instanceof Boolean)
+        {
+            Object rhs;
+            try
+            {
+                rhs = coerceType(lhs, (String) rhsUnknown);
+            }
+            catch (Exception ex)
+            {
+                return false;
+            }
+
+            switch (op)
+            {
+                case SimpleFilter.EQ :
+                case SimpleFilter.GTE :
+                case SimpleFilter.LTE :
+                case SimpleFilter.APPROX :
+                    return (lhs.equals(rhs));
+                default:
+                    throw new RuntimeException(
+                            "Unknown comparison operator: " + op);
+            }
+        }
+
+        // If the LHS is not a comparable or boolean, check if it is an
+        // array. If so, convert it to a list so we can treat it as a
+        // collection.
+        if (lhs.getClass().isArray())
+        {
+            lhs = convertArrayToList(lhs);
+        }
+
+        // If LHS is a collection, then call compare() on each element
+        // of the collection until a match is found.
+        if (lhs instanceof Collection)
+        {
+            for (Iterator iter = ((Collection) lhs).iterator(); iter.hasNext(); )
+            {
+                if (compare(iter.next(), rhsUnknown, op))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        // Spec says SUBSTRING is false for all types other than string.
+        if ((op == SimpleFilter.SUBSTRING) && !(lhs instanceof String))
+        {
+            return false;
+        }
+
+        // Since we cannot identify the LHS type, then we can only perform
+        // equality comparison.
+        try
+        {
+            return lhs.equals(coerceType(lhs, (String) rhsUnknown));
+        }
+        catch (Exception ex)
+        {
+            return false;
+        }
+    }
+
+    private static boolean compareApproximate(Object lhs, Object rhs)
+    {
+        if (rhs instanceof String)
+        {
+            return removeWhitespace((String) lhs)
+                    .equalsIgnoreCase(removeWhitespace((String) rhs));
+        }
+        else if (rhs instanceof Character)
+        {
+            return Character.toLowerCase(((Character) lhs))
+                    == Character.toLowerCase(((Character) rhs));
+        }
+        return lhs.equals(rhs);
+    }
+
+    private static String removeWhitespace(String s)
+    {
+        StringBuffer sb = new StringBuffer(s.length());
+        for (int i = 0; i < s.length(); i++)
+        {
+            if (!Character.isWhitespace(s.charAt(i)))
+            {
+                sb.append(s.charAt(i));
+            }
+        }
+        return sb.toString();
+    }
+
+    private static Object coerceType(Object lhs, String rhsString) throws Exception
+    {
+        // If the LHS expects a string, then we can just return
+        // the RHS since it is a string.
+        if (lhs.getClass() == rhsString.getClass())
+        {
+            return rhsString;
+        }
+
+        // Try to convert the RHS type to the LHS type by using
+        // the string constructor of the LHS class, if it has one.
+        Object rhs = null;
+        try
+        {
+            // The Character class is a special case, since its constructor
+            // does not take a string, so handle it separately.
+            if (lhs instanceof Character)
+            {
+                rhs = new Character(rhsString.charAt(0));
+            }
+            else if(lhs instanceof Version && rhsString.indexOf(',') >= 0)
+            {
+                rhs = VersionRange.parse(rhsString);
+            }
+            else
+            {
+                // Spec says we should trim number types.
+                if ((lhs instanceof Number) || (lhs instanceof Boolean))
+                {
+                    rhsString = rhsString.trim();
+                }
+
+                try
+                {
+                    // Try to find a suitable static valueOf method
+                    Method valueOfMethod = lhs.getClass().getDeclaredMethod(VALUE_OF_METHOD_NAME, STRING_CLASS);
+                    if (valueOfMethod.getReturnType().isAssignableFrom(lhs.getClass())
+                            && ((valueOfMethod.getModifiers() & Modifier.STATIC) > 0))
+                    {
+                        valueOfMethod.setAccessible(true);
+                        rhs = valueOfMethod.invoke(null, new Object[] { rhsString });
+                    }
+                }
+                catch (Exception ex)
+                {
+                    // Static valueOf fails, try the next conversion mechanism
+                }
+
+                if (rhs == null)
+                {
+                    Constructor ctor = lhs.getClass().getConstructor(STRING_CLASS);
+                    ctor.setAccessible(true);
+                    rhs = ctor.newInstance(new Object[] { rhsString });
+                }
+            }
+        }
+        catch (Exception ex)
+        {
+            throw new Exception(
+                    "Could not instantiate class "
+                            + lhs.getClass().getName()
+                            + " from string constructor with argument '"
+                            + rhsString + "' because " + ex);
+        }
+
+        return rhs;
+    }
+
+    /**
+     * This is an ugly utility method to convert an array of primitives
+     * to an array of primitive wrapper objects. This method simplifies
+     * processing LDAP filters since the special case of primitive arrays
+     * can be ignored.
+     * @param array An array of primitive types.
+     * @return An corresponding array using pritive wrapper objects.
+     **/
+    private static List convertArrayToList(Object array)
+    {
+        int len = Array.getLength(array);
+        List list = new ArrayList(len);
+        for (int i = 0; i < len; i++)
+        {
+            list.add(Array.get(array, i));
+        }
+        return list;
+    }
+
+
+    public static  boolean matches(Capability capability, Requirement requirement) {
+        if (requirement.getNamespace().equals(capability.getNamespace())) {
+            String filter = (String) requirement.getDirectives().get(Constants.FILTER_DIRECTIVE);
+            if (filter != null) {
+                return matches(capability, SimpleFilter.parse(filter));
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public static boolean isOptional(Requirement requirement) {
+        return RESOLUTION_OPTIONAL.equals(requirement. getDirectives().get(RESOLUTION_DIRECTIVE));
+    }
+
+    public static boolean isAttributeMandatory(Capability capability, String name)
+    {
+        String value = (String) capability.getDirectives().get(Constants.MANDATORY_DIRECTIVE);
+        if (value != null)
+        {
+            return parseDelimitedString(value, ",").contains(name);
+        }
+        return false;
+    }
+
+    public static List<String> parseDelimitedString(String value, String delim)
+    {
+        return CapabilityMatcher.parseDelimitedString(value, delim, true);
+    }
+
+    /**
+     * Parses delimited string and returns an array containing the tokens. This
+     * parser obeys quotes, so the delimiter character will be ignored if it is
+     * inside of a quote. This method assumes that the quote character is not
+     * included in the set of delimiter characters.
+     * @param value the delimited string to parse.
+     * @param delim the characters delimiting the tokens.
+     * @return a list of string or an empty list if there are none.
+     **/
+    public static List<String> parseDelimitedString(String value, String delim, boolean trim)
+    {
+        if (value == null)
+        {
+            value = "";
+        }
+
+        List<String> list = new ArrayList<String>();
+
+        int CHAR = 1;
+        int DELIMITER = 2;
+        int STARTQUOTE = 4;
+        int ENDQUOTE = 8;
+
+        StringBuffer sb = new StringBuffer();
+
+        int expecting = (CHAR | DELIMITER | STARTQUOTE);
+
+        boolean isEscaped = false;
+        for (int i = 0; i < value.length(); i++)
+        {
+            char c = value.charAt(i);
+
+            boolean isDelimiter = (delim.indexOf(c) >= 0);
+
+            if (!isEscaped && (c == '\\'))
+            {
+                isEscaped = true;
+                continue;
+            }
+
+            if (isEscaped)
+            {
+                sb.append(c);
+            }
+            else if (isDelimiter && ((expecting & DELIMITER) > 0))
+            {
+                if (trim)
+                {
+                    list.add(sb.toString().trim());
+                }
+                else
+                {
+                    list.add(sb.toString());
+                }
+                sb.delete(0, sb.length());
+                expecting = (CHAR | DELIMITER | STARTQUOTE);
+            }
+            else if ((c == '"') && ((expecting & STARTQUOTE) > 0))
+            {
+                sb.append(c);
+                expecting = CHAR | ENDQUOTE;
+            }
+            else if ((c == '"') && ((expecting & ENDQUOTE) > 0))
+            {
+                sb.append(c);
+                expecting = (CHAR | STARTQUOTE | DELIMITER);
+            }
+            else if ((expecting & CHAR) > 0)
+            {
+                sb.append(c);
+            }
+            else
+            {
+                throw new IllegalArgumentException("Invalid delimited string: " + value);
+            }
+
+            isEscaped = false;
+        }
+
+        if (sb.length() > 0)
+        {
+            if (trim)
+            {
+                list.add(sb.toString().trim());
+            }
+            else
+            {
+                list.add(sb.toString());
+            }
+        }
+
+        return list;
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/LambdaUtil.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/LambdaUtil.java
new file mode 100644
index 0000000..3f55ec1
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/LambdaUtil.java
@@ -0,0 +1,99 @@
+/*
+ * 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.support.util;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+
+public final class LambdaUtil {
+
+    @FunctionalInterface
+    public interface Consumer_WithExceptions<T, E extends Exception> {
+        void accept(T t) throws E;
+    }
+
+    @FunctionalInterface
+    public interface BiConsumer_WithExceptions<T, U, E extends Exception> {
+        void accept(T t, U u) throws E;
+    }
+
+    @FunctionalInterface
+    public interface Function_WithExceptions<T, R, E extends Exception> {
+        R apply(T t) throws E;
+    }
+
+    @FunctionalInterface
+    public interface Supplier_WithExceptions<T, E extends Exception> {
+        T get() throws E;
+    }
+
+    @FunctionalInterface
+    public interface Runnable_WithExceptions<E extends Exception> {
+        void run() throws E;
+    }
+
+    public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) {
+        return t -> {
+            try { consumer.accept(t); }
+            catch (Exception exception) { throwAsUnchecked(exception); }
+        };
+    }
+
+    public static <T, U, E extends Exception> BiConsumer<T, U> rethrowBiConsumer(BiConsumer_WithExceptions<T, U, E> biConsumer) {
+        return (t, u) -> {
+            try { biConsumer.accept(t, u); }
+            catch (Exception exception) { throwAsUnchecked(exception); }
+        };
+    }
+
+    public static <T, R, E extends Exception> Function<T, R> rethrowFunction(Function_WithExceptions<T, R, E> function) {
+        return t -> {
+            try { return function.apply(t); }
+            catch (Exception exception) { throwAsUnchecked(exception); return null; }
+        };
+    }
+
+    public static <T, E extends Exception> Supplier<T> rethrowSupplier(Supplier_WithExceptions<T, E> function) {
+        return () -> {
+            try { return function.get(); }
+            catch (Exception exception) { throwAsUnchecked(exception); return null; }
+        };
+    }
+
+    public static void uncheck(Runnable_WithExceptions t)
+    {
+        try { t.run(); }
+        catch (Exception exception) { throwAsUnchecked(exception); }
+    }
+
+    public static <R, E extends Exception> R uncheck(Supplier_WithExceptions<R, E> supplier)
+    {
+        try { return supplier.get(); }
+        catch (Exception exception) { throwAsUnchecked(exception); return null; }
+    }
+
+    public static <T, R, E extends Exception> R uncheck(Function_WithExceptions<T, R, E> function, T t) {
+        try { return function.apply(t); }
+        catch (Exception exception) { throwAsUnchecked(exception); return null; }
+    }
+
+    @SuppressWarnings ("unchecked")
+    private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E { throw (E)exception; }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/ManifestParser.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/ManifestParser.java
new file mode 100644
index 0000000..d4b0030
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/ManifestParser.java
@@ -0,0 +1,1038 @@
+/*
+ * 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.support.util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.jar.Manifest;
+
+import org.apache.sling.feature.Capability;
+import org.apache.sling.feature.Requirement;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+import org.osgi.framework.namespace.BundleNamespace;
+import org.osgi.framework.namespace.ExecutionEnvironmentNamespace;
+import org.osgi.framework.namespace.IdentityNamespace;
+import org.osgi.framework.wiring.BundleRevision;
+
+public class ManifestParser
+{
+    private static final String BUNDLE_LICENSE_HEADER = "Bundle-License"; // No constant defined by OSGi...
+
+    private final Manifest m_headerMap;
+    private volatile String m_bundleSymbolicName;
+    private volatile Version m_bundleVersion;
+    private volatile List<Capability> m_capabilities;
+    private volatile List<Requirement> m_requirements;
+
+    public ManifestParser(Manifest m)
+            throws BundleException
+    {
+        m_headerMap = m;
+
+        // Verify that only manifest version 2 is specified.
+        String manifestVersion = getManifestVersion(m_headerMap);
+        if ((manifestVersion != null) && !manifestVersion.equals("2"))
+        {
+            throw new BundleException(
+                    "Unknown 'Bundle-ManifestVersion' value: " + manifestVersion);
+        }
+
+        List<Capability> capList = new ArrayList<>();
+
+        //
+        // Parse bundle version.
+        //
+
+        m_bundleVersion = Version.emptyVersion;
+        if (m_headerMap.getMainAttributes().getValue(Constants.BUNDLE_VERSION) != null)
+        {
+            try
+            {
+                m_bundleVersion = Version.parseVersion(m_headerMap.getMainAttributes().getValue(Constants.BUNDLE_VERSION));
+            }
+            catch (RuntimeException ex)
+            {
+                // R4 bundle versions must parse, R3 bundle version may not.
+                if (getManifestVersion().equals("2"))
+                {
+                    throw ex;
+                }
+                m_bundleVersion = Version.emptyVersion;
+            }
+        }
+
+        //
+        // Parse bundle symbolic name.
+        //
+
+        Capability bundleCap = parseBundleSymbolicName(m_headerMap);
+        if (bundleCap != null)
+        {
+            m_bundleSymbolicName = (String)
+                    bundleCap.getAttributes().get(BundleRevision.BUNDLE_NAMESPACE);
+
+            // Add a bundle capability and a host capability to all
+            // non-fragment bundles. A host capability is the same
+            // as a require capability, but with a different capability
+            // namespace. Bundle capabilities resolve required-bundle
+            // dependencies, while host capabilities resolve fragment-host
+            // dependencies.
+            if (m_headerMap.getMainAttributes().getValue(Constants.FRAGMENT_HOST) == null)
+            {
+                // All non-fragment bundles have host capabilities.
+                capList.add(bundleCap);
+                // A non-fragment bundle can choose to not have a host capability.
+                String attachment =
+                        (String) bundleCap.getDirectives().get(Constants.FRAGMENT_ATTACHMENT_DIRECTIVE);
+                attachment = (attachment == null)
+                        ? Constants.FRAGMENT_ATTACHMENT_RESOLVETIME
+                        : attachment;
+                if (!attachment.equalsIgnoreCase(Constants.FRAGMENT_ATTACHMENT_NEVER))
+                {
+                    Map<String, Object> hostAttrs =
+                            new HashMap<>(bundleCap.getAttributes());
+                    Object value = hostAttrs.remove(BundleRevision.BUNDLE_NAMESPACE);
+                    hostAttrs.put(BundleRevision.HOST_NAMESPACE, value);
+                    Capability cap = new Capability(BundleRevision.HOST_NAMESPACE);
+                    cap.getAttributes().putAll(hostAttrs);
+                    cap.getDirectives().putAll(bundleCap.getDirectives());
+                    capList.add(cap);
+                }
+            }
+
+            //
+            // Add the osgi.identity capability.
+            //
+            capList.add(addIdentityCapability(m_headerMap, bundleCap));
+        }
+
+        // Verify that bundle symbolic name is specified.
+        if (getManifestVersion().equals("2") && (m_bundleSymbolicName == null))
+        {
+            throw new BundleException(
+                    "R4 bundle manifests must include bundle symbolic name.");
+        }
+
+        //
+        // Parse Fragment-Host.
+        //
+
+        List<Requirement> hostReqs = parseFragmentHost(m_headerMap);
+
+        //
+        // Parse Require-Bundle
+        //
+
+        List<ParsedHeaderClause> rbClauses =
+                parseStandardHeader(m_headerMap.getMainAttributes().getValue(Constants.REQUIRE_BUNDLE));
+        rbClauses = normalizeRequireClauses(rbClauses, getManifestVersion());
+        List<Requirement> rbReqs = convertRequires(rbClauses);
+
+
+        //
+        // Parse Require-Capability.
+        //
+
+        List<ParsedHeaderClause> requireClauses =
+                parseStandardHeader(m_headerMap.getMainAttributes().getValue(Constants.REQUIRE_CAPABILITY));
+        List<Requirement> requireReqs = convertRequireCapabilities(normalizeCapabilityClauses( requireClauses, getManifestVersion()));
+
+        //
+        // Parse Provide-Capability.
+        //
+
+        List<ParsedHeaderClause> provideClauses =
+                parseStandardHeader(m_headerMap.getMainAttributes().getValue(Constants.PROVIDE_CAPABILITY));
+
+        List<Capability> provideCaps = convertProvideCapabilities(normalizeCapabilityClauses(provideClauses, getManifestVersion()));
+
+        // Combine all requirements.
+        m_requirements = new ArrayList<>();
+        m_requirements.addAll(hostReqs);
+        m_requirements.addAll(rbReqs);
+        m_requirements.addAll(requireReqs);
+
+        // Combine all capabilities.
+        m_capabilities = new ArrayList<>();
+        m_capabilities.addAll(capList);
+        m_capabilities.addAll(provideCaps);
+    }
+
+
+    public static List<Requirement> convertRequireCapabilities(
+            List<ParsedHeaderClause> clauses)
+            throws BundleException
+    {
+        // Now convert generic header clauses into requirements.
+        List<Requirement> reqList = new ArrayList<>();
+        for (ParsedHeaderClause clause : clauses)
+        {
+            for (String path : clause.m_paths)
+            {
+                if (path.startsWith("osgi.wiring."))
+                {
+                    throw new BundleException("Manifest cannot use Require-Capability for '"
+                            + path
+                            + "' namespace.");
+                }
+
+                Requirement req = new Requirement(path);
+                req.getAttributes().putAll(clause.m_attrs);
+                req.getDirectives().putAll(clause.m_dirs);
+                // Create requirement and add to requirement list.
+                reqList.add(req);
+            }
+        }
+
+        return reqList;
+    }
+
+    public static List<ParsedHeaderClause> normalizeCapabilityClauses(
+            List<ParsedHeaderClause> clauses, String mv)
+            throws BundleException
+    {
+
+        if (!mv.equals("2") && !clauses.isEmpty())
+        {
+            // Should we error here if we are not an R4 bundle?
+        }
+
+        // Convert attributes into specified types.
+        for (ParsedHeaderClause clause : clauses)
+        {
+            for (Entry<String, String> entry : clause.m_types.entrySet())
+            {
+                String type = entry.getValue();
+                if (!type.equals("String"))
+                {
+                    if (type.equals("Double"))
+                    {
+                        clause.m_attrs.put(
+                                entry.getKey(),
+                                new Double(clause.m_attrs.get(entry.getKey()).toString().trim()));
+                    }
+                    else if (type.equals("Version"))
+                    {
+                        clause.m_attrs.put(
+                                entry.getKey(),
+                                new Version(clause.m_attrs.get(entry.getKey()).toString().trim()));
+                    }
+                    else if (type.equals("Long"))
+                    {
+                        clause.m_attrs.put(
+                                entry.getKey(),
+                                new Long(clause.m_attrs.get(entry.getKey()).toString().trim()));
+                    }
+                    else if (type.startsWith("List"))
+                    {
+                        int startIdx = type.indexOf('<');
+                        int endIdx = type.indexOf('>');
+                        if (((startIdx > 0) && (endIdx <= startIdx))
+                                || ((startIdx < 0) && (endIdx > 0)))
+                        {
+                            throw new BundleException(
+                                    "Invalid Provide-Capability attribute list type for '"
+                                            + entry.getKey()
+                                            + "' : "
+                                            + type);
+                        }
+
+                        String listType = "String";
+                        if (endIdx > startIdx)
+                        {
+                            listType = type.substring(startIdx + 1, endIdx).trim();
+                        }
+
+                        List<String> tokens = parseDelimitedString(
+                                clause.m_attrs.get(entry.getKey()).toString(), ",", false);
+                        List<Object> values = new ArrayList<>(tokens.size());
+                        for (String token : tokens)
+                        {
+                            if (listType.equals("String"))
+                            {
+                                values.add(token);
+                            }
+                            else if (listType.equals("Double"))
+                            {
+                                values.add(new Double(token.trim()));
+                            }
+                            else if (listType.equals("Version"))
+                            {
+                                values.add(new Version(token.trim()));
+                            }
+                            else if (listType.equals("Long"))
+                            {
+                                values.add(new Long(token.trim()));
+                            }
+                            else
+                            {
+                                throw new BundleException(
+                                        "Unknown Provide-Capability attribute list type for '"
+                                                + entry.getKey()
+                                                + "' : "
+                                                + type);
+                            }
+                        }
+                        clause.m_attrs.put(
+                                entry.getKey(),
+                                values);
+                    }
+                    else
+                    {
+                        throw new BundleException(
+                                "Unknown Provide-Capability attribute type for '"
+                                        + entry.getKey()
+                                        + "' : "
+                                        + type);
+                    }
+                }
+            }
+        }
+
+        return clauses;
+    }
+
+    public static List<Capability> convertProvideCapabilities(
+            List<ParsedHeaderClause> clauses)
+            throws BundleException
+    {
+        List<Capability> capList = new ArrayList<>();
+        for (ParsedHeaderClause clause : clauses)
+        {
+            for (String path : clause.m_paths)
+            {
+                if (path.startsWith("osgi.wiring."))
+                {
+                    throw new BundleException("Manifest cannot use Provide-Capability for '"
+                            + path
+                            + "' namespace.");
+                }
+
+                Capability capability = new Capability(path);
+                capability.getAttributes().putAll(clause.m_attrs);
+                capability.getDirectives().putAll(clause.m_dirs);
+                // Create package capability and add to capability list.
+                capList.add(capability);
+            }
+        }
+
+        return capList;
+    }
+
+
+    public String getManifestVersion()
+    {
+        String manifestVersion = getManifestVersion(m_headerMap);
+        return (manifestVersion == null) ? "1" : manifestVersion;
+    }
+
+    private static String getManifestVersion(Manifest headerMap)
+    {
+        String manifestVersion = headerMap.getMainAttributes().getValue(Constants.BUNDLE_MANIFESTVERSION);
+        return (manifestVersion == null) ? null : manifestVersion.trim();
+    }
+
+    public String getSymbolicName()
+    {
+        return m_bundleSymbolicName;
+    }
+
+    public Version getBundleVersion()
+    {
+        return m_bundleVersion;
+    }
+
+    public List<Capability> getCapabilities()
+    {
+        return m_capabilities;
+    }
+
+    public List<Requirement> getRequirements()
+    {
+        return m_requirements;
+    }
+
+
+    static class ParsedHeaderClause
+    {
+        public final List<String> m_paths;
+        public final Map<String, String> m_dirs;
+        public final Map<String, Object> m_attrs;
+        public final Map<String, String> m_types;
+
+        public ParsedHeaderClause(
+                List<String> paths, Map<String, String> dirs, Map<String, Object> attrs,
+                Map<String, String> types)
+        {
+            m_paths = paths;
+            m_dirs = dirs;
+            m_attrs = attrs;
+            m_types = types;
+        }
+    }
+
+    private static Capability parseBundleSymbolicName(
+            Manifest headerMap)
+            throws BundleException
+    {
+        List<ParsedHeaderClause> clauses = parseStandardHeader(headerMap.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME));
+        if (clauses.size() > 0)
+        {
+            if (clauses.size() > 1)
+            {
+                throw new BundleException(
+                        "Cannot have multiple symbolic names: "
+                                + headerMap.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME));
+            }
+            else if (clauses.get(0).m_paths.size() > 1)
+            {
+                throw new BundleException(
+                        "Cannot have multiple symbolic names: "
+                                + headerMap.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME));
+            }
+
+            // Get bundle version.
+            Version bundleVersion = Version.emptyVersion;
+            if (headerMap.getMainAttributes().getValue(Constants.BUNDLE_VERSION) != null)
+            {
+                try
+                {
+                    bundleVersion = Version.parseVersion(
+                            headerMap.getMainAttributes().getValue(Constants.BUNDLE_VERSION));
+                }
+                catch (RuntimeException ex)
+                {
+                    // R4 bundle versions must parse, R3 bundle version may not.
+                    String mv = getManifestVersion(headerMap);
+                    if (mv != null)
+                    {
+                        throw ex;
+                    }
+                    bundleVersion = Version.emptyVersion;
+                }
+            }
+
+            // Create a require capability and return it.
+            String symName = clauses.get(0).m_paths.get(0);
+            clauses.get(0).m_attrs.put(BundleRevision.BUNDLE_NAMESPACE, symName);
+            clauses.get(0).m_attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, bundleVersion);
+            Capability cap = new Capability(BundleRevision.BUNDLE_NAMESPACE);
+            cap.getAttributes().putAll(clauses.get(0).m_attrs);
+            cap.getAttributes().putAll(clauses.get(0).m_dirs);
+
+            return cap;
+        }
+
+        return null;
+    }
+
+    private static Capability addIdentityCapability(Manifest headerMap, Capability bundleCap)
+    {
+        Map<String, Object> attrs = new HashMap<>();
+
+        attrs.put(IdentityNamespace.IDENTITY_NAMESPACE,
+                bundleCap.getAttributes().get(BundleNamespace.BUNDLE_NAMESPACE));
+        attrs.put(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE,
+                headerMap.getMainAttributes().getValue(Constants.FRAGMENT_HOST) == null
+                        ? IdentityNamespace.TYPE_BUNDLE
+                        : IdentityNamespace.TYPE_FRAGMENT);
+        attrs.put(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE,
+                bundleCap.getAttributes().get(Constants.BUNDLE_VERSION_ATTRIBUTE));
+
+        if (headerMap.getMainAttributes().getValue(Constants.BUNDLE_COPYRIGHT) != null)
+        {
+            attrs.put(IdentityNamespace.CAPABILITY_COPYRIGHT_ATTRIBUTE,
+                    headerMap.getMainAttributes().getValue(Constants.BUNDLE_COPYRIGHT));
+        }
+
+        if (headerMap.getMainAttributes().getValue(Constants.BUNDLE_DESCRIPTION) != null)
+        {
+            attrs.put(IdentityNamespace.CAPABILITY_DESCRIPTION_ATTRIBUTE,
+                    headerMap.getMainAttributes().getValue(Constants.BUNDLE_DESCRIPTION));
+        }
+        if (headerMap.getMainAttributes().getValue(Constants.BUNDLE_DOCURL) != null)
+        {
+            attrs.put(IdentityNamespace.CAPABILITY_DOCUMENTATION_ATTRIBUTE,
+                    headerMap.getMainAttributes().getValue(Constants.BUNDLE_DOCURL));
+        }
+        if (headerMap.getMainAttributes().getValue(BUNDLE_LICENSE_HEADER) != null)
+        {
+            attrs.put(IdentityNamespace.CAPABILITY_LICENSE_ATTRIBUTE,
+                    headerMap.getMainAttributes().getValue(BUNDLE_LICENSE_HEADER));
+        }
+
+        Map<String, String> dirs;
+        if (bundleCap.getDirectives().get(Constants.SINGLETON_DIRECTIVE) != null)
+        {
+            dirs = Collections.singletonMap(IdentityNamespace.CAPABILITY_SINGLETON_DIRECTIVE,
+                    (String) bundleCap.getDirectives().get(Constants.SINGLETON_DIRECTIVE));
+        }
+        else
+        {
+            dirs = Collections.emptyMap();
+        }
+        Capability cap = new Capability(IdentityNamespace.IDENTITY_NAMESPACE);
+        cap.getAttributes().putAll(attrs);
+        cap.getDirectives().putAll(dirs);
+        return cap;
+    }
+
+    private static List<Requirement> parseFragmentHost(
+            Manifest headerMap)
+            throws BundleException
+    {
+        List<Requirement> reqs = new ArrayList<>();
+
+        String mv = getManifestVersion(headerMap);
+        if ((mv != null) && mv.equals("2"))
+        {
+            List<ParsedHeaderClause> clauses = parseStandardHeader(
+                    headerMap.getMainAttributes().getValue(Constants.FRAGMENT_HOST));
+            if (clauses.size() > 0)
+            {
+                // Make sure that only one fragment host symbolic name is specified.
+                if (clauses.size() > 1)
+                {
+                    throw new BundleException(
+                            "Fragments cannot have multiple hosts: "
+                                    + headerMap.getMainAttributes().getValue(Constants.FRAGMENT_HOST));
+                }
+                else if (clauses.get(0).m_paths.size() > 1)
+                {
+                    throw new BundleException(
+                            "Fragments cannot have multiple hosts: "
+                                    + headerMap.getMainAttributes().getValue(Constants.FRAGMENT_HOST));
+                }
+
+                // If the bundle-version attribute is specified, then convert
+                // it to the proper type.
+                Object value = clauses.get(0).m_attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE);
+                value = (value == null) ? "0.0.0" : value;
+                if (value != null)
+                {
+                    clauses.get(0).m_attrs.put(
+                            Constants.BUNDLE_VERSION_ATTRIBUTE,
+                            VersionRange.parse(value.toString()));
+                }
+
+                // Note that we use a linked hash map here to ensure the
+                // host symbolic name is first, which will make indexing
+                // more efficient.
+// TODO: OSGi R4.3 - This is ordering is kind of hacky.
+                // Prepend the host symbolic name to the map of attributes.
+                Map<String, Object> attrs = clauses.get(0).m_attrs;
+                Map<String, Object> newAttrs = new LinkedHashMap<>(attrs.size() + 1);
+                // We want this first from an indexing perspective.
+                newAttrs.put(
+                        BundleRevision.HOST_NAMESPACE,
+                        clauses.get(0).m_paths.get(0));
+                newAttrs.putAll(attrs);
+                // But we need to put it again to make sure it wasn't overwritten.
+                newAttrs.put(
+                        BundleRevision.HOST_NAMESPACE,
+                        clauses.get(0).m_paths.get(0));
+
+                // Create filter now so we can inject filter directive.
+                SimpleFilter sf = SimpleFilter.convert(newAttrs);
+
+                // Inject filter directive.
+// TODO: OSGi R4.3 - Can we insert this on demand somehow?
+                Map<String, String> dirs = clauses.get(0).m_dirs;
+                Map<String, String> newDirs = new HashMap<>(dirs.size() + 1);
+                newDirs.putAll(dirs);
+                newDirs.put(
+                        Constants.FILTER_DIRECTIVE,
+                        sf.toString());
+
+                Requirement req = new Requirement(BundleRevision.HOST_NAMESPACE);
+                req.getAttributes().putAll(newAttrs);
+                req.getDirectives().putAll(newDirs);
+                reqs.add(req);
+            }
+        }
+        else if (headerMap.getMainAttributes().getValue(Constants.FRAGMENT_HOST) != null)
+        {
+            String s = headerMap.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+            s = (s == null) ? headerMap.getMainAttributes().getValue(Constants.BUNDLE_NAME) : s;
+            s = (s == null) ? headerMap.toString() : s;
+        }
+
+        return reqs;
+    }
+
+
+    private static List<Requirement> parseBreeHeader(String header)
+    {
+        List<String> filters = new ArrayList<>();
+        for (String entry : parseDelimitedString(header, ","))
+        {
+            List<String> names = parseDelimitedString(entry, "/");
+            List<String> left = parseDelimitedString(names.get(0), "-");
+
+            String lName = left.get(0);
+            Version lVer;
+            try
+            {
+                lVer = Version.parseVersion(left.get(1));
+            }
+            catch (Exception ex)
+            {
+                // Version doesn't parse. Make it part of the name.
+                lName = names.get(0);
+                lVer = null;
+            }
+
+            String rName = null;
+            Version rVer = null;
+            if (names.size() > 1)
+            {
+                List<String> right = parseDelimitedString(names.get(1), "-");
+                rName = right.get(0);
+                try
+                {
+                    rVer = Version.parseVersion(right.get(1));
+                }
+                catch (Exception ex)
+                {
+                    rName = names.get(1);
+                    rVer = null;
+                }
+            }
+
+            String versionClause;
+            if (lVer != null)
+            {
+                if ((rVer != null) && (!rVer.equals(lVer)))
+                {
+                    // Both versions are defined, but different. Make each of them part of the name
+                    lName = names.get(0);
+                    rName = names.get(1);
+                    versionClause = null;
+                }
+                else
+                {
+                    versionClause = getBreeVersionClause(lVer);
+                }
+            }
+            else
+            {
+                versionClause = getBreeVersionClause(rVer);
+            }
+
+            if ("J2SE".equals(lName))
+            {
+                // J2SE is not used in the Capability variant of BREE, use JavaSE here
+                // This can only happen with the lName part...
+                lName = "JavaSE";
+            }
+
+            String nameClause;
+            if (rName != null)
+                nameClause = "(" + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE + "=" + lName + "/" + rName + ")";
+            else
+                nameClause = "(" + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE + "=" + lName + ")";
+
+            String filter;
+            if (versionClause != null)
+                filter = "(&" + nameClause + versionClause + ")";
+            else
+                filter = nameClause;
+
+            filters.add(filter);
+        }
+
+        if (filters.size() == 0)
+        {
+            return Collections.emptyList();
+        }
+        else
+        {
+            String reqFilter;
+            if (filters.size() == 1)
+            {
+                reqFilter = filters.get(0);
+            }
+            else
+            {
+                // If there are more BREE filters, we need to or them together
+                StringBuilder sb = new StringBuilder("(|");
+                for (String f : filters)
+                {
+                    sb.append(f);
+                }
+                sb.append(")");
+                reqFilter = sb.toString();
+            }
+
+            SimpleFilter sf = SimpleFilter.parse(reqFilter);
+            Requirement req = new Requirement(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE);
+            req.getDirectives().put(ExecutionEnvironmentNamespace.REQUIREMENT_FILTER_DIRECTIVE, reqFilter);
+            return Collections.<Requirement>singletonList(req);
+        }
+    }
+
+    private static String getBreeVersionClause(Version ver) {
+        if (ver == null)
+            return null;
+
+        return "(" + ExecutionEnvironmentNamespace.CAPABILITY_VERSION_ATTRIBUTE + "=" + ver + ")";
+    }
+
+    static List<ParsedHeaderClause> normalizeRequireClauses(List<ParsedHeaderClause> clauses, String mv)
+    {
+        // R3 bundles cannot require other bundles.
+        if (!mv.equals("2"))
+        {
+            clauses.clear();
+        }
+        else
+        {
+            // Convert bundle version attribute to VersionRange type.
+            for (ParsedHeaderClause clause : clauses)
+            {
+                Object value = clause.m_attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE);
+                if (value != null)
+                {
+                    clause.m_attrs.put(
+                            Constants.BUNDLE_VERSION_ATTRIBUTE,
+                            VersionRange.parse(value.toString()));
+                }
+            }
+        }
+
+        return clauses;
+    }
+
+    private static List<Requirement> convertRequires(
+            List<ParsedHeaderClause> clauses)
+    {
+        List<Requirement> reqList = new ArrayList<>();
+        for (ParsedHeaderClause clause : clauses)
+        {
+            for (String path : clause.m_paths)
+            {
+                // Prepend the bundle symbolic name to the array of attributes.
+                Map<String, Object> attrs = clause.m_attrs;
+                // Note that we use a linked hash map here to ensure the
+                // symbolic name attribute is first, which will make indexing
+                // more efficient.
+// TODO: OSGi R4.3 - This is ordering is kind of hacky.
+                // Prepend the symbolic name to the array of attributes.
+                Map<String, Object> newAttrs = new LinkedHashMap<>(attrs.size() + 1);
+                // We want this first from an indexing perspective.
+                newAttrs.put(
+                        BundleRevision.BUNDLE_NAMESPACE,
+                        path);
+                newAttrs.putAll(attrs);
+                // But we need to put it again to make sure it wasn't overwritten.
+                newAttrs.put(
+                        BundleRevision.BUNDLE_NAMESPACE,
+                        path);
+
+                // Create filter now so we can inject filter directive.
+                SimpleFilter sf = SimpleFilter.convert(newAttrs);
+
+                // Inject filter directive.
+// TODO: OSGi R4.3 - Can we insert this on demand somehow?
+                Map<String, String> dirs = clause.m_dirs;
+                Map<String, String> newDirs = new HashMap<>(dirs.size() + 1);
+                newDirs.putAll(dirs);
+                newDirs.put(
+                        Constants.FILTER_DIRECTIVE,
+                        sf.toString());
+
+                Requirement req = new Requirement(BundleRevision.BUNDLE_NAMESPACE);
+                req.getAttributes().putAll(newAttrs);
+                req.getDirectives().putAll(newDirs);
+                reqList.add(req);
+            }
+        }
+
+        return reqList;
+    }
+
+    private static final char EOF = (char) -1;
+
+    private static char charAt(int pos, String headers, int length)
+    {
+        if (pos >= length)
+        {
+            return EOF;
+        }
+        return headers.charAt(pos);
+    }
+
+    private static final int CLAUSE_START = 0;
+    private static final int PARAMETER_START = 1;
+    private static final int KEY = 2;
+    private static final int DIRECTIVE_OR_TYPEDATTRIBUTE = 4;
+    private static final int ARGUMENT = 8;
+    private static final int VALUE = 16;
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static List<ParsedHeaderClause> parseStandardHeader(String header)
+    {
+        List<ParsedHeaderClause> clauses = new ArrayList<>();
+        if (header == null)
+        {
+            return clauses;
+        }
+        ParsedHeaderClause clause = null;
+        String key = null;
+        Map targetMap = null;
+        int state = CLAUSE_START;
+        int currentPosition = 0;
+        int startPosition = 0;
+        int length = header.length();
+        boolean quoted = false;
+        boolean escaped = false;
+
+        char currentChar = EOF;
+        do
+        {
+            currentChar = charAt(currentPosition, header, length);
+            switch (state)
+            {
+                case CLAUSE_START:
+                    clause = new ParsedHeaderClause(
+                            new ArrayList<>(),
+                            new HashMap<>(),
+                            new HashMap<>(),
+                            new HashMap<>());
+                    clauses.add(clause);
+                    state = PARAMETER_START;
+                case PARAMETER_START:
+                    startPosition = currentPosition;
+                    state = KEY;
+                case KEY:
+                    switch (currentChar)
+                    {
+                        case ':':
+                        case '=':
+                            key = header.substring(startPosition, currentPosition).trim();
+                            startPosition = currentPosition + 1;
+                            targetMap = clause.m_attrs;
+                            state = currentChar == ':' ? DIRECTIVE_OR_TYPEDATTRIBUTE : ARGUMENT;
+                            break;
+                        case EOF:
+                        case ',':
+                        case ';':
+                            clause.m_paths.add(header.substring(startPosition, currentPosition).trim());
+                            state = currentChar == ',' ? CLAUSE_START : PARAMETER_START;
+                            break;
+                        default:
+                            break;
+                    }
+                    currentPosition++;
+                    break;
+                case DIRECTIVE_OR_TYPEDATTRIBUTE:
+                    switch(currentChar)
+                    {
+                        case '=':
+                            if (startPosition != currentPosition)
+                            {
+                                clause.m_types.put(key, header.substring(startPosition, currentPosition).trim());
+                            }
+                            else
+                            {
+                                targetMap = clause.m_dirs;
+                            }
+                            state = ARGUMENT;
+                            startPosition = currentPosition + 1;
+                            break;
+                        default:
+                            break;
+                    }
+                    currentPosition++;
+                    break;
+                case ARGUMENT:
+                    if (currentChar == '\"')
+                    {
+                        quoted = true;
+                        currentPosition++;
+                    }
+                    else
+                    {
+                        quoted = false;
+                    }
+                    if (!Character.isWhitespace(currentChar)) {
+                        state = VALUE;
+                    }
+                    else {
+                        currentPosition++;
+                    }
+                    break;
+                case VALUE:
+                    if (escaped)
+                    {
+                        escaped = false;
+                    }
+                    else
+                    {
+                        if (currentChar == '\\' )
+                        {
+                            escaped = true;
+                        }
+                        else if (quoted && currentChar == '\"')
+                        {
+                            quoted = false;
+                        }
+                        else if (!quoted)
+                        {
+                            String value = null;
+                            switch(currentChar)
+                            {
+                                case EOF:
+                                case ';':
+                                case ',':
+                                    value = header.substring(startPosition, currentPosition).trim();
+                                    if (value.startsWith("\"") && value.endsWith("\""))
+                                    {
+                                        value = value.substring(1, value.length() - 1);
+                                    }
+                                    if (targetMap.put(key, value) != null)
+                                    {
+                                        throw new IllegalArgumentException(
+                                                "Duplicate '" + key + "' in: " + header);
+                                    }
+                                    state = currentChar == ';' ? PARAMETER_START : CLAUSE_START;
+                                    break;
+                                default:
+                                    break;
+                            }
+                        }
+                    }
+                    currentPosition++;
+                    break;
+                default:
+                    break;
+            }
+        } while ( currentChar != EOF);
+
+        if (state > PARAMETER_START)
+        {
+            throw new IllegalArgumentException("Unable to parse header: " + header);
+        }
+        return clauses;
+    }
+
+    public static List<String> parseDelimitedString(String value, String delim)
+    {
+        return parseDelimitedString(value, delim, true);
+    }
+
+    /**
+     * Parses delimited string and returns an array containing the tokens. This
+     * parser obeys quotes, so the delimiter character will be ignored if it is
+     * inside of a quote. This method assumes that the quote character is not
+     * included in the set of delimiter characters.
+     * @param value the delimited string to parse.
+     * @param delim the characters delimiting the tokens.
+     * @return a list of string or an empty list if there are none.
+     **/
+    public static List<String> parseDelimitedString(String value, String delim, boolean trim)
+    {
+        if (value == null)
+        {
+            value = "";
+        }
+
+        List<String> list = new ArrayList<>();
+
+        int CHAR = 1;
+        int DELIMITER = 2;
+        int STARTQUOTE = 4;
+        int ENDQUOTE = 8;
+
+        StringBuffer sb = new StringBuffer();
+
+        int expecting = (CHAR | DELIMITER | STARTQUOTE);
+
+        boolean isEscaped = false;
+        for (int i = 0; i < value.length(); i++)
+        {
+            char c = value.charAt(i);
+
+            boolean isDelimiter = (delim.indexOf(c) >= 0);
+
+            if (!isEscaped && (c == '\\'))
+            {
+                isEscaped = true;
+                continue;
+            }
+
+            if (isEscaped)
+            {
+                sb.append(c);
+            }
+            else if (isDelimiter && ((expecting & DELIMITER) > 0))
+            {
+                if (trim)
+                {
+                    list.add(sb.toString().trim());
+                }
+                else
+                {
+                    list.add(sb.toString());
+                }
+                sb.delete(0, sb.length());
+                expecting = (CHAR | DELIMITER | STARTQUOTE);
+            }
+            else if ((c == '"') && ((expecting & STARTQUOTE) > 0))
+            {
+                sb.append(c);
+                expecting = CHAR | ENDQUOTE;
+            }
+            else if ((c == '"') && ((expecting & ENDQUOTE) > 0))
+            {
+                sb.append(c);
+                expecting = (CHAR | STARTQUOTE | DELIMITER);
+            }
+            else if ((expecting & CHAR) > 0)
+            {
+                sb.append(c);
+            }
+            else
+            {
+                throw new IllegalArgumentException("Invalid delimited string: " + value);
+            }
+
+            isEscaped = false;
+        }
+
+        if (sb.length() > 0)
+        {
+            if (trim)
+            {
+                list.add(sb.toString().trim());
+            }
+            else
+            {
+                list.add(sb.toString());
+            }
+        }
+
+        return list;
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/ManifestUtil.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/ManifestUtil.java
new file mode 100644
index 0000000..4158ff3
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/ManifestUtil.java
@@ -0,0 +1,174 @@
+/*
+ * 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.support.util;
+
+import org.apache.sling.commons.osgi.ManifestHeader;
+import org.apache.sling.feature.Capability;
+import org.apache.sling.feature.Requirement;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.stream.Collectors;
+
+import static org.apache.sling.feature.support.util.ManifestParser.convertProvideCapabilities;
+import static org.apache.sling.feature.support.util.ManifestParser.normalizeCapabilityClauses;
+import static org.apache.sling.feature.support.util.ManifestParser.parseStandardHeader;
+
+public class ManifestUtil {
+
+    /**
+     * Get the manifest from the artifact.
+     * @param artifact The file
+     * @throws IOException If the manifest can't be read
+     */
+    public static Manifest getManifest(final File artifact) throws IOException {
+        try (final JarFile file = new JarFile(artifact) ) {
+            return file.getManifest();
+        }
+    }
+
+    public static List<PackageInfo> extractPackages(final Manifest m,
+            final String headerName,
+            final String defaultVersion,
+            final boolean checkOptional) {
+        final String pckInfo = m.getMainAttributes().getValue(headerName);
+        if (pckInfo != null) {
+            final ManifestHeader header = ManifestHeader.parse(pckInfo);
+
+            final List<PackageInfo> pcks = new ArrayList<>();
+            for(final ManifestHeader.Entry entry : header.getEntries()) {
+                String version = entry.getAttributeValue("version");
+                if ( version == null ) {
+                    version = defaultVersion;
+                }
+                boolean optional = false;
+                if ( checkOptional ) {
+                    final String resolution = entry.getDirectiveValue("resolution");
+                    optional = "optional".equalsIgnoreCase(resolution);
+                }
+                final PackageInfo pck = new PackageInfo(entry.getValue(),
+                        version,
+                        optional);
+                pcks.add(pck);
+            }
+
+            return pcks;
+        }
+        return Collections.emptyList();
+    }
+
+    public static List<PackageInfo> extractExportedPackages(final Manifest m) {
+        return extractPackages(m, Constants.EXPORT_PACKAGE, "0.0.0", false);
+    }
+
+    public static List<PackageInfo> extractImportedPackages(final Manifest m) {
+        return extractPackages(m, Constants.IMPORT_PACKAGE, null, true);
+    }
+
+    public static List<PackageInfo> extractDynamicImportedPackages(final Manifest m) {
+        return extractPackages(m, Constants.DYNAMICIMPORT_PACKAGE, null, false);
+    }
+
+    public static List<Capability> extractCapabilities(ManifestParser parser) {
+        return parser.getCapabilities();
+    }
+
+    public static List<Requirement> extractRequirements(ManifestParser parser)  {
+        return parser.getRequirements();
+    }
+
+    public static void unmarshalAttribute(String key, Object value, BiConsumer<String, Object> sink) throws IOException {
+        unmarshal(key + "=" + value, Capability::getAttributes, sink);
+    }
+
+    public static void unmarshalDirective(String key, Object value, BiConsumer<String, Object> sink) throws IOException {
+        unmarshal(key + ":=" + value, Capability::getDirectives, sink);
+    }
+
+    private static void unmarshal(String header, Function<Capability, Map<String, Object>> lookup, BiConsumer<String, Object> sink) throws IOException {
+        try {
+            convertProvideCapabilities(
+                    normalizeCapabilityClauses(parseStandardHeader("foo;" + header), "2"))
+                    .forEach(capability -> lookup.apply(capability).forEach(sink));
+        } catch (Exception e) {
+            throw new IOException(e);
+        }
+    }
+
+    public static void marshalAttribute(String key, Object value, BiConsumer<String, String> sink) {
+        marshal(key, value, sink);
+    }
+
+    public static void marshalDirective(String key, Object value, BiConsumer<String, String> sink) {
+        marshal(key, value, sink);
+    }
+
+    private static void marshal(String key, Object value, BiConsumer<String, String> sink) {
+        StringBuilder keyBuilder = new StringBuilder(key);
+        if (value instanceof  List) {
+            List list = (List) value;
+            keyBuilder.append(":List");
+            if (!list.isEmpty()) {
+                String type = type(list.get(0));
+                if (!type.equals("String")) {
+                    keyBuilder.append('<').append(type).append('>');
+                }
+                value = list.stream().map(
+                        v -> v.toString().replace(",", "\\,")
+                ).collect(Collectors.joining(","));
+            }
+            else {
+                value = "";
+            }
+        }
+        else {
+            String type = type(value);
+            if (!type.equals("String")) {
+                keyBuilder.append(':').append(type);
+            }
+        }
+        sink.accept(keyBuilder.toString(), value.toString());
+    }
+
+    private static String type(Object value) {
+        if (value instanceof Long) {
+            return "Long";
+        }
+        else if (value instanceof Double)
+        {
+            return "Double";
+        }
+        else if (value instanceof Version)
+        {
+            return "Version";
+        }
+        else
+        {
+            return "String";
+        }
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/PackageInfo.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/PackageInfo.java
new file mode 100644
index 0000000..83d4e79
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/PackageInfo.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.support.util;
+
+import org.osgi.framework.Version;
+import org.osgi.framework.VersionRange;
+
+public class PackageInfo implements Comparable<PackageInfo> {
+
+    private final boolean optional;
+    private final String name;
+    private final String version;
+
+    public PackageInfo(final String name, final String version, final boolean optional) {
+        this.name = name;
+        this.version = version;
+        this.optional = optional;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public boolean isOptional() {
+        return optional;
+    }
+
+    public Version getPackageVersion() {
+        return new Version(this.version);
+    }
+
+    public VersionRange getPackageVersionRange() {
+        return new VersionRange(this.version);
+    }
+
+    @Override
+    public String toString() {
+        return "Package " + name
+                + ";version=" + version
+                + (this.optional ? " (optional)" : "");
+    }
+
+    @Override
+    public int compareTo(final PackageInfo o) {
+        int result = this.name.compareTo(o.name);
+        if ( result == 0 ) {
+            result = this.version.compareTo(o.version);
+        }
+        return result;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((name == null) ? 0 : name.hashCode());
+        result = prime * result + (optional ? 1231 : 1237);
+        result = prime * result + ((version == null) ? 0 : version.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;
+        PackageInfo other = (PackageInfo) obj;
+        if (name == null) {
+            if (other.name != null)
+                return false;
+        } else if (!name.equals(other.name))
+            return false;
+        if (optional != other.optional)
+            return false;
+        if (version == null) {
+            if (other.version != null)
+                return false;
+        } else if (!version.equals(other.version))
+            return false;
+        return true;
+    }
+
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/SimpleFilter.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/SimpleFilter.java
new file mode 100644
index 0000000..a56b57c
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/SimpleFilter.java
@@ -0,0 +1,647 @@
+/*
+ * 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.support.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+public class SimpleFilter
+{
+    public static final int MATCH_ALL = 0;
+    public static final int AND = 1;
+    public static final int OR = 2;
+    public static final int NOT = 3;
+    public static final int EQ = 4;
+    public static final int LTE = 5;
+    public static final int GTE = 6;
+    public static final int SUBSTRING = 7;
+    public static final int PRESENT = 8;
+    public static final int APPROX = 9;
+
+    private final String m_name;
+    private final Object m_value;
+    private final int m_op;
+
+    public SimpleFilter(String attr, Object value, int op)
+    {
+        m_name = attr;
+        m_value = value;
+        m_op = op;
+    }
+
+    public String getName()
+    {
+        return m_name;
+    }
+
+    public Object getValue()
+    {
+        return m_value;
+    }
+
+    public int getOperation()
+    {
+        return m_op;
+    }
+
+    public String toString()
+    {
+        String s = null;
+        switch (m_op)
+        {
+            case AND:
+                s = "(&" + toString((List) m_value) + ")";
+                break;
+            case OR:
+                s = "(|" + toString((List) m_value) + ")";
+                break;
+            case NOT:
+                s = "(!" + toString((List) m_value) + ")";
+                break;
+            case EQ:
+                s = "(" + m_name + "=" + toEncodedString(m_value) + ")";
+                break;
+            case LTE:
+                s = "(" + m_name + "<=" + toEncodedString(m_value) + ")";
+                break;
+            case GTE:
+                s = "(" + m_name + ">=" + toEncodedString(m_value) + ")";
+                break;
+            case SUBSTRING:
+                s = "(" + m_name + "=" + unparseSubstring((List<String>) m_value) + ")";
+                break;
+            case PRESENT:
+                s = "(" + m_name + "=*)";
+                break;
+            case APPROX:
+                s = "(" + m_name + "~=" + toEncodedString(m_value) + ")";
+                break;
+            case MATCH_ALL:
+                s = "(*)";
+                break;
+        }
+        return s;
+    }
+
+    private static String toString(List list)
+    {
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < list.size(); i++)
+        {
+            sb.append(list.get(i).toString());
+        }
+        return sb.toString();
+    }
+
+    private static String toDecodedString(String s, int startIdx, int endIdx)
+    {
+        StringBuffer sb = new StringBuffer(endIdx - startIdx);
+        boolean escaped = false;
+        for (int i = 0; i < (endIdx - startIdx); i++)
+        {
+            char c = s.charAt(startIdx + i);
+            if (!escaped && (c == '\\'))
+            {
+                escaped = true;
+            }
+            else
+            {
+                escaped = false;
+                sb.append(c);
+            }
+        }
+
+        return sb.toString();
+    }
+
+    private static String toEncodedString(Object o)
+    {
+        if (o instanceof String)
+        {
+            String s = (String) o;
+            StringBuffer sb = new StringBuffer();
+            for (int i = 0; i < s.length(); i++)
+            {
+                char c = s.charAt(i);
+                if ((c == '\\') || (c == '(') || (c == ')') || (c == '*'))
+                {
+                    sb.append('\\');
+                }
+                sb.append(c);
+            }
+
+            o = sb.toString();
+        }
+
+        return o.toString();
+    }
+
+    public static SimpleFilter parse(String filter)
+    {
+        int idx = skipWhitespace(filter, 0);
+
+        if ((filter == null) || (filter.length() == 0) || (idx >= filter.length()))
+        {
+            throw new IllegalArgumentException("Null or empty filter.");
+        }
+        else if (filter.charAt(idx) != '(')
+        {
+            throw new IllegalArgumentException("Missing opening parenthesis: " + filter);
+        }
+
+        SimpleFilter sf = null;
+        List stack = new ArrayList();
+        boolean isEscaped = false;
+        while (idx < filter.length())
+        {
+            if (sf != null)
+            {
+                throw new IllegalArgumentException(
+                        "Only one top-level operation allowed: " + filter);
+            }
+
+            if (!isEscaped && (filter.charAt(idx) == '('))
+            {
+                // Skip paren and following whitespace.
+                idx = skipWhitespace(filter, idx + 1);
+
+                if (filter.charAt(idx) == '&')
+                {
+                    int peek = skipWhitespace(filter, idx + 1);
+                    if (filter.charAt(peek) == '(')
+                    {
+                        idx = peek - 1;
+                        stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.AND));
+                    }
+                    else
+                    {
+                        stack.add(0, new Integer(idx));
+                    }
+                }
+                else if (filter.charAt(idx) == '|')
+                {
+                    int peek = skipWhitespace(filter, idx + 1);
+                    if (filter.charAt(peek) == '(')
+                    {
+                        idx = peek - 1;
+                        stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.OR));
+                    }
+                    else
+                    {
+                        stack.add(0, new Integer(idx));
+                    }
+                }
+                else if (filter.charAt(idx) == '!')
+                {
+                    int peek = skipWhitespace(filter, idx + 1);
+                    if (filter.charAt(peek) == '(')
+                    {
+                        idx = peek - 1;
+                        stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT));
+                    }
+                    else
+                    {
+                        stack.add(0, new Integer(idx));
+                    }
+                }
+                else
+                {
+                    stack.add(0, new Integer(idx));
+                }
+            }
+            else if (!isEscaped && (filter.charAt(idx) == ')'))
+            {
+                Object top = stack.remove(0);
+                if (top instanceof SimpleFilter)
+                {
+                    if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter))
+                    {
+                        ((List) ((SimpleFilter) stack.get(0)).m_value).add(top);
+                    }
+                    else
+                    {
+                        sf = (SimpleFilter) top;
+                    }
+                }
+                else if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter))
+                {
+                    ((List) ((SimpleFilter) stack.get(0)).m_value).add(
+                            SimpleFilter.subfilter(filter, ((Integer) top).intValue(), idx));
+                }
+                else
+                {
+                    sf = SimpleFilter.subfilter(filter, ((Integer) top).intValue(), idx);
+                }
+            }
+            else if (!isEscaped && (filter.charAt(idx) == '\\'))
+            {
+                isEscaped = true;
+            }
+            else
+            {
+                isEscaped = false;
+            }
+
+            idx = skipWhitespace(filter, idx + 1);
+        }
+
+        if (sf == null)
+        {
+            throw new IllegalArgumentException("Missing closing parenthesis: " + filter);
+        }
+
+        return sf;
+    }
+
+    private static SimpleFilter subfilter(String filter, int startIdx, int endIdx)
+    {
+        final String opChars = "=<>~";
+
+        // Determine the ending index of the attribute name.
+        int attrEndIdx = startIdx;
+        for (int i = 0; i < (endIdx - startIdx); i++)
+        {
+            char c = filter.charAt(startIdx + i);
+            if (opChars.indexOf(c) >= 0)
+            {
+                break;
+            }
+            else if (!Character.isWhitespace(c))
+            {
+                attrEndIdx = startIdx + i + 1;
+            }
+        }
+        if (attrEndIdx == startIdx)
+        {
+            throw new IllegalArgumentException(
+                    "Missing attribute name: " + filter.substring(startIdx, endIdx));
+        }
+        String attr = filter.substring(startIdx, attrEndIdx);
+
+        // Skip the attribute name and any following whitespace.
+        startIdx = skipWhitespace(filter, attrEndIdx);
+
+        // Determine the operator type.
+        int op = -1;
+        switch (filter.charAt(startIdx))
+        {
+            case '=':
+                op = EQ;
+                startIdx++;
+                break;
+            case '<':
+                if (filter.charAt(startIdx + 1) != '=')
+                {
+                    throw new IllegalArgumentException(
+                            "Unknown operator: " + filter.substring(startIdx, endIdx));
+                }
+                op = LTE;
+                startIdx += 2;
+                break;
+            case '>':
+                if (filter.charAt(startIdx + 1) != '=')
+                {
+                    throw new IllegalArgumentException(
+                            "Unknown operator: " + filter.substring(startIdx, endIdx));
+                }
+                op = GTE;
+                startIdx += 2;
+                break;
+            case '~':
+                if (filter.charAt(startIdx + 1) != '=')
+                {
+                    throw new IllegalArgumentException(
+                            "Unknown operator: " + filter.substring(startIdx, endIdx));
+                }
+                op = APPROX;
+                startIdx += 2;
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Unknown operator: " + filter.substring(startIdx, endIdx));
+        }
+
+        // Parse value.
+        Object value = toDecodedString(filter, startIdx, endIdx);
+
+        // Check if the equality comparison is actually a substring
+        // or present operation.
+        if (op == EQ)
+        {
+            String valueStr = filter.substring(startIdx, endIdx);
+            List<String> values = parseSubstring(valueStr);
+            if ((values.size() == 2)
+                    && (values.get(0).length() == 0)
+                    && (values.get(1).length() == 0))
+            {
+                op = PRESENT;
+            }
+            else if (values.size() > 1)
+            {
+                op = SUBSTRING;
+                value = values;
+            }
+        }
+
+        return new SimpleFilter(attr, value, op);
+    }
+
+    public static List<String> parseSubstring(String value)
+    {
+        List<String> pieces = new ArrayList();
+        StringBuffer ss = new StringBuffer();
+        // int kind = SIMPLE; // assume until proven otherwise
+        boolean wasStar = false; // indicates last piece was a star
+        boolean leftstar = false; // track if the initial piece is a star
+        boolean rightstar = false; // track if the final piece is a star
+
+        int idx = 0;
+
+        // We assume (sub)strings can contain leading and trailing blanks
+        boolean escaped = false;
+        loop:   for (;;)
+        {
+            if (idx >= value.length())
+            {
+                if (wasStar)
+                {
+                    // insert last piece as "" to handle trailing star
+                    rightstar = true;
+                }
+                else
+                {
+                    pieces.add(ss.toString());
+                    // accumulate the last piece
+                    // note that in the case of
+                    // (cn=); this might be
+                    // the string "" (!=null)
+                }
+                ss.setLength(0);
+                break loop;
+            }
+
+            // Read the next character and account for escapes.
+            char c = value.charAt(idx++);
+            if (!escaped && (c == '*'))
+            {
+                // If we have successive '*' characters, then we can
+                // effectively collapse them by ignoring succeeding ones.
+                if (!wasStar)
+                {
+                    if (ss.length() > 0)
+                    {
+                        pieces.add(ss.toString()); // accumulate the pieces
+                        // between '*' occurrences
+                    }
+                    ss.setLength(0);
+                    // if this is a leading star, then track it
+                    if (pieces.isEmpty())
+                    {
+                        leftstar = true;
+                    }
+                    wasStar = true;
+                }
+            }
+            else if (!escaped && (c == '\\'))
+            {
+                escaped = true;
+            }
+            else
+            {
+                escaped = false;
+                wasStar = false;
+                ss.append(c);
+            }
+        }
+        if (leftstar || rightstar || pieces.size() > 1)
+        {
+            // insert leading and/or trailing "" to anchor ends
+            if (rightstar)
+            {
+                pieces.add("");
+            }
+            if (leftstar)
+            {
+                pieces.add(0, "");
+            }
+        }
+        return pieces;
+    }
+
+    public static String unparseSubstring(List<String> pieces)
+    {
+        StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < pieces.size(); i++)
+        {
+            if (i > 0)
+            {
+                sb.append("*");
+            }
+            sb.append(toEncodedString(pieces.get(i)));
+        }
+        return sb.toString();
+    }
+
+    public static boolean compareSubstring(List<String> pieces, String s)
+    {
+        // Walk the pieces to match the string
+        // There are implicit stars between each piece,
+        // and the first and last pieces might be "" to anchor the match.
+        // assert (pieces.length > 1)
+        // minimal case is <string>*<string>
+
+        boolean result = true;
+        int len = pieces.size();
+
+        // Special case, if there is only one piece, then
+        // we must perform an equality test.
+        if (len == 1)
+        {
+            return s.equals(pieces.get(0));
+        }
+
+        // Otherwise, check whether the pieces match
+        // the specified string.
+
+        int index = 0;
+
+        loop:   for (int i = 0; i < len; i++)
+        {
+            String piece = pieces.get(i);
+
+            // If this is the first piece, then make sure the
+            // string starts with it.
+            if (i == 0)
+            {
+                if (!s.startsWith(piece))
+                {
+                    result = false;
+                    break loop;
+                }
+            }
+
+            // If this is the last piece, then make sure the
+            // string ends with it.
+            if (i == (len - 1))
+            {
+                if (s.endsWith(piece) && (s.length() >= (index + piece.length())))
+                {
+                    result = true;
+                }
+                else
+                {
+                    result = false;
+                }
+                break loop;
+            }
+
+            // If this is neither the first or last piece, then
+            // make sure the string contains it.
+            if ((i > 0) && (i < (len - 1)))
+            {
+                index = s.indexOf(piece, index);
+                if (index < 0)
+                {
+                    result = false;
+                    break loop;
+                }
+            }
+
+            // Move string index beyond the matching piece.
+            index += piece.length();
+        }
+
+        return result;
+    }
+
+    private static int skipWhitespace(String s, int startIdx)
+    {
+        int len = s.length();
+        while ((startIdx < len) && Character.isWhitespace(s.charAt(startIdx)))
+        {
+            startIdx++;
+        }
+        return startIdx;
+    }
+
+    /**
+     * Converts a attribute map to a filter. The filter is created by iterating
+     * over the map's entry set. If ordering of attributes is important (e.g.,
+     * for hitting attribute indices), then the map's entry set should iterate
+     * in the desired order. Equality testing is assumed for all attribute types
+     * other than version ranges, which are handled appropriated. If the attribute
+     * map is empty, then a filter that matches anything is returned.
+     * @param attrs Map of attributes to convert to a filter.
+     * @return A filter corresponding to the attributes.
+     */
+    public static SimpleFilter convert(Map<String, Object> attrs)
+    {
+        // Rather than building a filter string to be parsed into a SimpleFilter,
+        // we will just create the parsed SimpleFilter directly.
+
+        List<SimpleFilter> filters = new ArrayList<SimpleFilter>();
+
+        for (Entry<String, Object> entry : attrs.entrySet())
+        {
+            if (entry.getValue() instanceof VersionRange)
+            {
+                VersionRange vr = (VersionRange) entry.getValue();
+                if (vr.isFloorInclusive())
+                {
+                    filters.add(
+                            new SimpleFilter(
+                                    entry.getKey(),
+                                    vr.getFloor().toString(),
+                                    SimpleFilter.GTE));
+                }
+                else
+                {
+                    SimpleFilter not =
+                            new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT);
+                    ((List) not.getValue()).add(
+                            new SimpleFilter(
+                                    entry.getKey(),
+                                    vr.getFloor().toString(),
+                                    SimpleFilter.LTE));
+                    filters.add(not);
+                }
+
+                if (vr.getCeiling() != null)
+                {
+                    if (vr.isCeilingInclusive())
+                    {
+                        filters.add(
+                                new SimpleFilter(
+                                        entry.getKey(),
+                                        vr.getCeiling().toString(),
+                                        SimpleFilter.LTE));
+                    }
+                    else
+                    {
+                        SimpleFilter not =
+                                new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT);
+                        ((List) not.getValue()).add(
+                                new SimpleFilter(
+                                        entry.getKey(),
+                                        vr.getCeiling().toString(),
+                                        SimpleFilter.GTE));
+                        filters.add(not);
+                    }
+                }
+            }
+            else
+            {
+                List<String> values = SimpleFilter.parseSubstring(entry.getValue().toString());
+                if (values.size() > 1)
+                {
+                    filters.add(
+                            new SimpleFilter(
+                                    entry.getKey(),
+                                    values,
+                                    SimpleFilter.SUBSTRING));
+                }
+                else
+                {
+                    filters.add(
+                            new SimpleFilter(
+                                    entry.getKey(),
+                                    values.get(0),
+                                    SimpleFilter.EQ));
+                }
+            }
+        }
+
+        SimpleFilter sf = null;
+
+        if (filters.size() == 1)
+        {
+            sf = filters.get(0);
+        }
+        else if (attrs.size() > 1)
+        {
+            sf = new SimpleFilter(null, filters, SimpleFilter.AND);
+        }
+        else if (filters.isEmpty())
+        {
+            sf = new SimpleFilter(null, null, SimpleFilter.MATCH_ALL);
+        }
+
+        return sf;
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/StringComparator.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/StringComparator.java
new file mode 100644
index 0000000..866c14c
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/StringComparator.java
@@ -0,0 +1,71 @@
+/*
+ * 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.support.util;
+
+import java.util.Comparator;
+
+public class StringComparator implements Comparator<String>
+{
+
+    public static final StringComparator COMPARATOR = new StringComparator();
+
+    public int compare(String s1, String s2)
+    {
+        int n1 = s1.length();
+        int n2 = s2.length();
+        int min = n1 < n2 ? n1 : n2;
+        for ( int i = 0; i < min; i++ )
+        {
+            char c1 = s1.charAt( i );
+            char c2 = s2.charAt( i );
+            if ( c1 != c2 )
+            {
+                // Fast check for simple ascii codes
+                if ( c1 <= 128 && c2 <= 128 )
+                {
+                    c1 = toLowerCaseFast(c1);
+                    c2 = toLowerCaseFast(c2);
+                    if ( c1 != c2 )
+                    {
+                        return c1 - c2;
+                    }
+                }
+                else
+                {
+                    c1 = Character.toUpperCase( c1 );
+                    c2 = Character.toUpperCase( c2 );
+                    if ( c1 != c2 )
+                    {
+                        c1 = Character.toLowerCase( c1 );
+                        c2 = Character.toLowerCase( c2 );
+                        if ( c1 != c2 )
+                        {
+                            // No overflow because of numeric promotion
+                            return c1 - c2;
+                        }
+                    }
+                }
+            }
+        }
+        return n1 - n2;
+    }
+
+    private static char toLowerCaseFast( char ch )
+    {
+        return ( ch >= 'A' && ch <= 'Z' ) ? ( char ) ( ch + 'a' - 'A' ) : ch;
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/SubstVarUtil.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/SubstVarUtil.java
new file mode 100644
index 0000000..d601624
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/SubstVarUtil.java
@@ -0,0 +1,149 @@
+/*
+ * 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.support.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+public class SubstVarUtil {
+    private static final String DELIM_START = "${";
+    private static final String DELIM_STOP  = "}";
+
+    /**
+     * <p>
+     * This method performs property variable substitution on the
+     * specified value. If the specified value contains the syntax
+     * <tt>${&lt;prop-name&gt;}</tt>, where <tt>&lt;prop-name&gt;</tt>
+     * refers to either a configuration property or a system property,
+     * then the corresponding property value is substituted for the variable
+     * placeholder. Multiple variable place holders may exist in the
+     * specified value as well as nested variable place holders, which
+     * are substituted from inner most to outer most. Configuration
+     * properties override system properties.
+     * </p>
+     * @param val The string on which to perform property substitution.
+     * @param currentKey The key of the property being evaluated used to
+     *        detect cycles.
+     * @param cycleMap Map of variable references used to detect nested cycles - may be null.
+     * @param configProps Set of configuration properties.
+     * @return The value of the specified string after system property substitution.
+     * @throws IllegalArgumentException If there was a syntax error in the
+     *         property placeholder syntax or a recursive variable reference.
+     **/
+    public static String substVars(String val, String currentKey,
+                                    Map<String, String> cycleMap, Map<String, String> configProps)
+            throws IllegalArgumentException
+    {
+        // If there is currently no cycle map, then create
+        // one for detecting cycles for this invocation.
+        if (cycleMap == null)
+        {
+            cycleMap = new HashMap<>();
+        }
+
+        // Put the current key in the cycle map.
+        cycleMap.put(currentKey, currentKey);
+
+        // Assume we have a value that is something like:
+        // "leading ${foo.${bar}} middle ${baz} trailing"
+
+        // Find the first ending '}' variable delimiter, which
+        // will correspond to the first deepest nested variable
+        // placeholder.
+        int stopDelim = -1;
+        int startDelim = -1;
+
+        do
+        {
+            stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
+            // If there is no stopping delimiter, then just return
+            // the value since there is no variable declared.
+            if (stopDelim < 0)
+            {
+                return val;
+            }
+            // Try to find the matching start delimiter by
+            // looping until we find a start delimiter that is
+            // greater than the stop delimiter we have found.
+            startDelim = val.indexOf(DELIM_START);
+            // If there is no starting delimiter, then just return
+            // the value since there is no variable declared.
+            if (startDelim < 0)
+            {
+                return val;
+            }
+            while (stopDelim >= 0)
+            {
+                int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
+                if ((idx < 0) || (idx > stopDelim))
+                {
+                    break;
+                }
+                else if (idx < stopDelim)
+                {
+                    startDelim = idx;
+                }
+            }
+        }
+        while ((startDelim > stopDelim) && (stopDelim >= 0));
+
+        // At this point, we have found a variable placeholder so
+        // we must perform a variable substitution on it.
+        // Using the start and stop delimiter indices, extract
+        // the first, deepest nested variable placeholder.
+        String variable =
+                val.substring(startDelim + DELIM_START.length(), stopDelim);
+
+        // Verify that this is not a recursive variable reference.
+        if (cycleMap.get(variable) != null)
+        {
+            throw new IllegalArgumentException(
+                    "recursive variable reference: " + variable);
+        }
+
+        // Get the value of the deepest nested variable placeholder.
+        // Try to configuration properties first.
+        String substValue = (configProps != null)
+                ? configProps.get(variable)
+                : null;
+        if (substValue == null)
+        {
+            // Ignore unknown property values.
+            substValue = System.getProperty(variable, "");
+        }
+
+        // Remove the found variable from the cycle map, since
+        // it may appear more than once in the value and we don't
+        // want such situations to appear as a recursive reference.
+        cycleMap.remove(variable);
+
+        // Append the leading characters, the substituted value of
+        // the variable, and the trailing characters to get the new
+        // value.
+        val = val.substring(0, startDelim)
+                + substValue
+                + val.substring(stopDelim + DELIM_STOP.length(), val.length());
+
+        // Now perform substitution again, since there could still
+        // be substitutions to make.
+        val = substVars(val, currentKey, cycleMap, configProps);
+
+        // Return the value.
+        return val;
+    }
+}
diff --git a/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/VersionRange.java b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/VersionRange.java
new file mode 100644
index 0000000..3d4ee86
--- /dev/null
+++ b/featuremodel/feature-support/src/main/java/org/apache/sling/feature/support/util/VersionRange.java
@@ -0,0 +1,157 @@
+/*
+ * 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.support.util;
+
+import org.osgi.framework.Version;
+
+public class VersionRange
+{
+    private final Version m_floor;
+    private final boolean m_isFloorInclusive;
+    private final Version m_ceiling;
+    private final boolean m_isCeilingInclusive;
+    public static final VersionRange infiniteRange
+            = new VersionRange(Version.emptyVersion, true, null, true);
+
+    public VersionRange(
+            Version low, boolean isLowInclusive,
+            Version high, boolean isHighInclusive)
+    {
+        m_floor = low;
+        m_isFloorInclusive = isLowInclusive;
+        m_ceiling = high;
+        m_isCeilingInclusive = isHighInclusive;
+    }
+
+    public Version getFloor()
+    {
+        return m_floor;
+    }
+
+    public boolean isFloorInclusive()
+    {
+        return m_isFloorInclusive;
+    }
+
+    public Version getCeiling()
+    {
+        return m_ceiling;
+    }
+
+    public boolean isCeilingInclusive()
+    {
+        return m_isCeilingInclusive;
+    }
+
+    public boolean isInRange(Version version)
+    {
+        // We might not have an upper end to the range.
+        if (m_ceiling == null)
+        {
+            return (version.compareTo(m_floor) >= 0);
+        }
+        else if (isFloorInclusive() && isCeilingInclusive())
+        {
+            return (version.compareTo(m_floor) >= 0) && (version.compareTo(m_ceiling) <= 0);
+        }
+        else if (isCeilingInclusive())
+        {
+            return (version.compareTo(m_floor) > 0) && (version.compareTo(m_ceiling) <= 0);
+        }
+        else if (isFloorInclusive())
+        {
+            return (version.compareTo(m_floor) >= 0) && (version.compareTo(m_ceiling) < 0);
+        }
+        return (version.compareTo(m_floor) > 0) && (version.compareTo(m_ceiling) < 0);
+    }
+
+    public static VersionRange parse(String range)
+    {
+        // Check if the version is an interval.
+        if (range.indexOf(',') >= 0)
+        {
+            String s = range.substring(1, range.length() - 1);
+            String vlo = s.substring(0, s.indexOf(',')).trim();
+            String vhi = s.substring(s.indexOf(',') + 1, s.length()).trim();
+            return new VersionRange (
+                    new Version(vlo), (range.charAt(0) == '['),
+                    new Version(vhi), (range.charAt(range.length() - 1) == ']'));
+        }
+        else
+        {
+            return new VersionRange(new Version(range), true, null, false);
+        }
+    }
+
+    public boolean equals(Object obj)
+    {
+        if (obj == null)
+        {
+            return false;
+        }
+        if (getClass() != obj.getClass())
+        {
+            return false;
+        }
+        final VersionRange other = (VersionRange) obj;
+        if (m_floor != other.m_floor && (m_floor == null || !m_floor.equals(other.m_floor)))
+        {
+            return false;
+        }
+        if (m_isFloorInclusive != other.m_isFloorInclusive)
+        {
+            return false;
+        }
+        if (m_ceiling != other.m_ceiling && (m_ceiling == null || !m_ceiling.equals(other.m_ceiling)))
+        {
+            return false;
+        }
+        if (m_isCeilingInclusive != other.m_isCeilingInclusive)
+        {
+            return false;
+        }
+        return true;
+    }
+
+    public int hashCode()
+    {
+        int hash = 5;
+        hash = 97 * hash + (m_floor != null ? m_floor.hashCode() : 0);
+        hash = 97 * hash + (m_isFloorInclusive ? 1 : 0);
+        hash = 97 * hash + (m_ceiling != null ? m_ceiling.hashCode() : 0);
+        hash = 97 * hash + (m_isCeilingInclusive ? 1 : 0);
+        return hash;
+    }
+
+    public String toString()
+    {
+        if (m_ceiling != null)
+        {
+            StringBuffer sb = new StringBuffer();
+            sb.append(m_isFloorInclusive ? '[' : '(');
+            sb.append(m_floor.toString());
+            sb.append(',');
+            sb.append(m_ceiling.toString());
+            sb.append(m_isCeilingInclusive ? ']' : ')');
+            return sb.toString();
+        }
+        else
+        {
+            return m_floor.toString();
+        }
+    }
+}
diff --git a/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/ArtifactManagerTest.java b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/ArtifactManagerTest.java
new file mode 100644
index 0000000..a6daf59
--- /dev/null
+++ b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/ArtifactManagerTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.support;
+
+import org.apache.sling.feature.support.spi.ArtifactProvider;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ArtifactManagerTest {
+
+    private static final String METADATA = "<metadata modelVersion=\"1.1.0\">\n" +
+            "<groupId>org.apache.sling.samples</groupId>\n" +
+            "<artifactId>slingshot</artifactId>\n" +
+            "<version>0-DEFAULT-SNAPSHOT</version>\n" +
+            "<versioning>\n" +
+                "<snapshot>\n" +
+                    "<timestamp>20160321.103951</timestamp>\n" +
+                    "<buildNumber>1</buildNumber>\n" +
+                "</snapshot>\n" +
+                "<lastUpdated>20160321103951</lastUpdated>\n" +
+                "<snapshotVersions>\n" +
+                    "<snapshotVersion>\n" +
+                        "<extension>txt</extension>\n" +
+                        "<value>0-DEFAULT-20160321.103951-1</value>\n" +
+                        "<updated>20160321103951</updated>\n" +
+                    "</snapshotVersion>\n" +
+                    "<snapshotVersion>\n" +
+                        "<extension>pom</extension>\n" +
+                        "<value>0-DEFAULT-20160321.103951-1</value>\n" +
+                        "<updated>20160321103951</updated>\n" +
+                    "</snapshotVersion>\n" +
+                "</snapshotVersions>\n" +
+            "</versioning></metadata>";
+
+    @Test public void testMetadataParsing() {
+        final String version = ArtifactManager.getLatestSnapshot(METADATA);
+        assertEquals("20160321.103951-1", version);
+    }
+
+    @Test public void testSnapshotHandling() throws IOException {
+        final String REPO = "http://org.apache.sling";
+        final ArtifactManagerConfig config = mock(ArtifactManagerConfig.class);
+        when(config.getRepositoryUrls()).thenReturn(new String[] {REPO});
+
+        final File metadataFile = mock(File.class);
+        when(metadataFile.exists()).thenReturn(true);
+        when(metadataFile.getPath()).thenReturn("/maven-metadata.xml");
+
+        final File artifactFile = mock(File.class);
+        when(artifactFile.exists()).thenReturn(true);
+
+        final ArtifactProvider provider = mock(ArtifactProvider.class);
+        when(provider.getArtifact(REPO + "/group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-SNAPSHOT.txt", "group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-SNAPSHOT.txt")).thenReturn(null);
+        when(provider.getArtifact(REPO + "/group/artifact/1.0.0-SNAPSHOT/maven-metadata.xml", "org.apache.sling/group/artifact/1.0.0-SNAPSHOT/maven-metadata.xml")).thenReturn(metadataFile);
+        when(provider.getArtifact(REPO + "/group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-20160321.103951-1.txt", "group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-SNAPSHOT.txt")).thenReturn(artifactFile);
+
+        final Map<String, ArtifactProvider> providers = new HashMap<>();
+        providers.put("*", provider);
+
+        final ArtifactManager mgr = new ArtifactManager(config, providers) {
+
+            @Override
+            protected String getFileContents(final ArtifactHandler handler) throws IOException {
+                final String path = handler.getFile().getPath();
+                if ( "/maven-metadata.xml".equals(path) ) {
+                    return METADATA;
+                }
+                return super.getFileContents(handler);
+            }
+        };
+
+        final ArtifactHandler handler = mgr.getArtifactHandler("mvn:group/artifact/1.0.0-SNAPSHOT/txt");
+        assertNotNull(handler);
+        assertEquals(artifactFile, handler.getFile());
+    }
+}
diff --git a/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/FeatureUtilTest.java b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/FeatureUtilTest.java
new file mode 100644
index 0000000..1f3ff48
--- /dev/null
+++ b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/FeatureUtilTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.support;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class FeatureUtilTest {
+
+    @Test public void testFileSort() {
+        final String[] files = new String[] {
+            "/different/path/app.json",
+            "/path/to/base.json",
+            "/path/to/feature.json",
+            "/path/to/amode/feature.json",
+            "/path/to/later/feature.json",
+            "http://sling.apache.org/features/one.json",
+            "http://sling.apache.org/features/two.json",
+            "http://sling.apache.org/features/amode/feature.json"
+        };
+
+        final List<String> l = new ArrayList<>(Arrays.asList(files));
+        Collections.sort(l, FeatureUtil.FEATURE_PATH_COMP);
+        for(int i=0; i<files.length; i++) {
+            assertEquals(files[i], l.get(i));
+        }
+    }
+
+}
diff --git a/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/json/FeatureJSONReaderTest.java b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/json/FeatureJSONReaderTest.java
new file mode 100644
index 0000000..f84838b
--- /dev/null
+++ b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/json/FeatureJSONReaderTest.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.support.json;
+
+import org.apache.sling.feature.Capability;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Feature;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+public class FeatureJSONReaderTest {
+
+    @Test public void testRead() throws Exception {
+        final Feature feature = U.readFeature("test");
+        assertNotNull(feature);
+        assertNotNull(feature.getId());
+        assertEquals("org.apache.sling", feature.getId().getGroupId());
+        assertEquals("test-feature", feature.getId().getArtifactId());
+        assertEquals("1.1", feature.getId().getVersion());
+        assertEquals("jar", feature.getId().getType());
+        assertNull(feature.getId().getClassifier());
+
+        assertEquals(2, feature.getConfigurations().size());
+        final Configuration cfg1 = U.findConfiguration(feature.getConfigurations(), "my.pid");
+        assertEquals(7, cfg1.getProperties().get("number"));
+        final Configuration cfg2 = U.findFactoryConfiguration(feature.getConfigurations(), "my.factory.pid", "name");
+        assertEquals("yeah", cfg2.getProperties().get("a.value"));
+
+        assertEquals(3, feature.getCapabilities().size());
+        Capability capability = U.findCapability(feature.getCapabilities(),"osgi.service");
+        assertNotNull(capability.getAttributes().get("objectClass"));
+
+        assertEquals(Arrays.asList("org.osgi.service.http.runtime.HttpServiceRuntime"), capability.getAttributes().get("objectClass"));
+
+    }
+
+}
diff --git a/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/json/FeatureJSONWriterTest.java b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/json/FeatureJSONWriterTest.java
new file mode 100644
index 0000000..6ba5bd4
--- /dev/null
+++ b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/json/FeatureJSONWriterTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.support.json;
+
+import org.apache.sling.feature.Feature;
+import org.junit.Test;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Arrays;
+
+import static org.junit.Assert.assertEquals;
+
+public class FeatureJSONWriterTest {
+
+    @Test public void testRead() throws Exception {
+        final Feature feature = U.readFeature("test");
+        final Feature readFeature;
+        try ( final StringWriter writer = new StringWriter() ) {
+            FeatureJSONWriter.write(writer, feature);
+            try ( final StringReader reader = new StringReader(writer.toString()) ) {
+                readFeature = FeatureJSONReader.read(reader, null);
+            }
+        }
+        assertEquals(feature.getId(), readFeature.getId());
+        assertEquals(Arrays.asList("org.osgi.service.http.runtime.HttpServiceRuntime"),
+                U.findCapability(readFeature.getCapabilities(), "osgi.service").getAttributes().get("objectClass"));
+    }
+
+}
diff --git a/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/json/U.java b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/json/U.java
new file mode 100644
index 0000000..fd3007f
--- /dev/null
+++ b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/json/U.java
@@ -0,0 +1,82 @@
+/*
+ * 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.support.json;
+
+import org.apache.sling.feature.Capability;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Requirement;
+
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.List;
+
+import static org.junit.Assert.fail;
+
+/** Test utilities */
+public class U {
+
+    /** Read the feature from the provided resource */
+    public static Feature readFeature(final String name) throws Exception {
+        try ( final Reader reader = new InputStreamReader(U.class.getResourceAsStream("/features/" + name + ".json"),
+                "UTF-8") ) {
+            return FeatureJSONReader.read(reader, name);
+        }
+    }
+
+    public static Configuration findConfiguration(final List<Configuration> cfgs, final String pid) {
+        for(final Configuration c : cfgs) {
+            if ( !c.isFactoryConfiguration() && pid.equals(c.getPid()) ) {
+                return c;
+            }
+        }
+        fail("Configuration not found " + pid);
+        return null;
+    }
+
+    public static Configuration findFactoryConfiguration(final List<Configuration> cfgs, final String factoryid, final String name) {
+        for(final Configuration c : cfgs) {
+            if ( c.isFactoryConfiguration() && factoryid.equals(c.getFactoryPid()) && name.equals(c.getName())) {
+                return c;
+            }
+        }
+        fail("Factory Configuration not found " + factoryid + "~" + name);
+        return null;
+    }
+
+    public static Capability findCapability(List<Capability> capabilities, final String namespace) {
+        for (Capability capability : capabilities) {
+            if (capability.getNamespace().equals(namespace)) {
+                return capability;
+            }
+        }
+
+        fail(String.format("No Capability with namespace '%s' found", namespace));
+        return null;
+    }
+
+    public static Requirement findRequirement(List<Requirement> requirements, final String namespace) {
+        for (Requirement requirement : requirements) {
+            if (requirement.getNamespace().equals(namespace)) {
+                return requirement;
+            }
+        }
+
+        fail(String.format("No Requirement with namespace '%s' found", namespace));
+        return null;
+    }
+}
diff --git a/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/util/CapabilityMatcherTest.java b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/util/CapabilityMatcherTest.java
new file mode 100644
index 0000000..1b88562
--- /dev/null
+++ b/featuremodel/feature-support/src/test/java/org/apache/sling/feature/support/util/CapabilityMatcherTest.java
@@ -0,0 +1,33 @@
+/*
+ * 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.support.util;
+
+import static junit.framework.TestCase.assertTrue;
+
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Requirement;
+import org.apache.sling.feature.support.json.U;
+import org.junit.Test;
+
+public class CapabilityMatcherTest {
+    @Test public void testCapabilityMatching() throws Exception {
+        Feature feature = U.readFeature("test");
+        Requirement requirement = U.findRequirement(feature.getRequirements(), "osgi.contract");
+        assertTrue(CapabilityMatcher.matches(U.findCapability(feature.getCapabilities(), "osgi.contract"),
+                SimpleFilter.parse((String) requirement.getDirectives().get("filter"))));
+    }
+}
diff --git a/featuremodel/feature-support/src/test/resources/features/test.json b/featuremodel/feature-support/src/test/resources/features/test.json
new file mode 100644
index 0000000..5c0348a
--- /dev/null
+++ b/featuremodel/feature-support/src/test/resources/features/test.json
@@ -0,0 +1,85 @@
+{
+    "id" : "org.apache.sling/test-feature/1.1",
+
+    "includes" : [
+         {
+             "id" : "org.apache.sling/sling/9",
+             "removals" : {
+                 "configurations" : [
+                 ],
+                 "bundles" : [
+                 ],
+                 "framework-properties" : [
+                 ]
+             }
+         }
+    ],
+    "requirements" : [
+          {
+              "namespace" : "osgi.contract",
+              "directives" : {
+                  "filter" : "(&(osgi.contract=JavaServlet)(&(version>=3.0)(!(version>=4.0))))"
+              }
+          }
+    ],
+    "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"
+             }
+        },
+        {
+          "namespace" : "osgi.contract",
+          "attributes" : {
+            "osgi.contract" : "JavaServlet",
+            "osgi.implementation" : "osgi.http",
+            "version:Version" : "3.1"
+          },
+          "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/oak-server/1.0.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"
+        }
+    }
+}
\ No newline at end of file
diff --git a/featuremodel/feature/pom.xml b/featuremodel/feature/pom.xml
new file mode 100644
index 0000000..5cb1614
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/readme.md b/featuremodel/feature/readme.md
new file mode 100644
index 0000000..2fbf1f4
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Application.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Application.java
new file mode 100644
index 0000000..7ca58e4
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Artifact.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Artifact.java
new file mode 100644
index 0000000..4fada27
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/ArtifactId.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/ArtifactId.java
new file mode 100644
index 0000000..a7231c8
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Bundles.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Bundles.java
new file mode 100644
index 0000000..a359575
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Capability.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Capability.java
new file mode 100644
index 0000000..53c9c3b
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Configuration.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Configuration.java
new file mode 100644
index 0000000..721badf
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Configurations.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Configurations.java
new file mode 100644
index 0000000..0a78964
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Extension.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Extension.java
new file mode 100644
index 0000000..a3e8082
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/ExtensionType.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/ExtensionType.java
new file mode 100644
index 0000000..2b8b221
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Extensions.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Extensions.java
new file mode 100644
index 0000000..703eac2
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Feature.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Feature.java
new file mode 100644
index 0000000..3665689
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Include.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Include.java
new file mode 100644
index 0000000..eb28293
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/KeyValueMap.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/KeyValueMap.java
new file mode 100644
index 0000000..ce8c1c8
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/Requirement.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/Requirement.java
new file mode 100644
index 0000000..3409e51
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/package-info.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/package-info.java
new file mode 100644
index 0000000..0ec26d7
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/process/ApplicationBuilder.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/process/ApplicationBuilder.java
new file mode 100644
index 0000000..cc0f235
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/process/BuilderContext.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/process/BuilderContext.java
new file mode 100644
index 0000000..024076d
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/process/BuilderUtil.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/process/BuilderUtil.java
new file mode 100644
index 0000000..19c606c
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/process/FeatureBuilder.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/process/FeatureBuilder.java
new file mode 100644
index 0000000..7208f00
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/process/FeatureExtensionHandler.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/process/FeatureExtensionHandler.java
new file mode 100644
index 0000000..c2a9460
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/process/FeatureProvider.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/process/FeatureProvider.java
new file mode 100644
index 0000000..8d3c35a
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/main/java/org/apache/sling/feature/process/package-info.java b/featuremodel/feature/src/main/java/org/apache/sling/feature/process/package-info.java
new file mode 100644
index 0000000..f6d563a
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/test/java/org/apache/sling/feature/ArtifactIdTest.java b/featuremodel/feature/src/test/java/org/apache/sling/feature/ArtifactIdTest.java
new file mode 100644
index 0000000..958e117
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/test/java/org/apache/sling/feature/BundlesTest.java b/featuremodel/feature/src/test/java/org/apache/sling/feature/BundlesTest.java
new file mode 100644
index 0000000..3346f19
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/test/java/org/apache/sling/feature/process/BuilderUtilTest.java b/featuremodel/feature/src/test/java/org/apache/sling/feature/process/BuilderUtilTest.java
new file mode 100644
index 0000000..a33ef95
--- /dev/null
+++ b/featuremodel/feature/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/featuremodel/feature/src/test/java/org/apache/sling/feature/process/FeatureBuilderTest.java b/featuremodel/feature/src/test/java/org/apache/sling/feature/process/FeatureBuilderTest.java
new file mode 100644
index 0000000..5ef1a62
--- /dev/null
+++ b/featuremodel/feature/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);
+    }
+}
diff --git a/featuremodel/osgifeature-maven-plugin/pom.xml b/featuremodel/osgifeature-maven-plugin/pom.xml
new file mode 100644
index 0000000..0d887cf
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/pom.xml
@@ -0,0 +1,192 @@
+<?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>osgifeature-maven-plugin</artifactId>
+    <version>0.01.7-SNAPSHOT</version>
+    <packaging>maven-plugin</packaging>
+
+    <name>Apache Sling OSGi Feature Maven Plugin</name>
+    <description>
+        Maven Plugin for OSGi Applicatgions
+    </description>
+
+    <properties>
+        <sling.java.version>8</sling.java.version>
+        <maven.version>3.5.0</maven.version>
+        <maven.site.path>${project.artifactId}-archives/${project.artifactId}-LATEST</maven.site.path>
+    </properties>
+
+    <scm>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/tooling/maven/osgifeature-maven-plugin</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/tooling/maven/osgifeature-maven-plugin</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/tooling/maven/osgifeature-maven-plugin</url>
+    </scm>
+
+    <!-- Support for publishing the mvn site. -->
+    <distributionManagement>
+        <site>
+            <id>apache.website</id>
+            <url>scm:svn:https://svn.apache.org/repos/infra/websites/production/sling/content/components/${maven.site.path}</url>
+        </site>
+    </distributionManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.plexus</groupId>
+                <artifactId>plexus-component-metadata</artifactId>
+                <version>1.7.1</version>
+                <executions>
+                    <execution>
+                        <id>generate-metadata</id>
+                        <goals>
+                            <goal>generate-metadata</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-plugin-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>mojo-descriptor</id>
+                        <phase>process-classes</phase>
+                        <goals>
+                            <goal>descriptor</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>generated-helpmojo</id>
+                        <goals>
+                            <goal>helpmojo</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <skipErrorNoDescriptorsFound>true</skipErrorNoDescriptorsFound>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-scm-publish-plugin</artifactId>
+                <configuration>
+                    <checkoutDirectory>${user.home}/maven-sites/${maven.site.path}</checkoutDirectory>
+                    <tryUpdate>true</tryUpdate>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>src/site/markdown/**</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.support</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-core</artifactId>
+            <version>${maven.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-plugin-api</artifactId>
+            <version>${maven.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-artifact</artifactId>
+            <version>${maven.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-compat</artifactId>
+            <version>${maven.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven.plugin-tools</groupId>
+            <artifactId>maven-plugin-annotations</artifactId>
+            <version>3.5</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.plexus</groupId>
+            <artifactId>plexus-interactivity-api</artifactId>
+            <version>1.0-alpha-6</version>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.plexus</groupId>
+            <artifactId>plexus-archiver</artifactId>
+            <version>3.5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.plexus</groupId>
+            <artifactId>plexus-utils</artifactId>
+            <version>3.0.24</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.maven.shared</groupId>
+            <artifactId>maven-filtering</artifactId>
+            <version>3.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.10.19</version>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+    </dependencies>
+
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-plugin-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </reporting>
+
+</project>
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ApplicationProjectConfig.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ApplicationProjectConfig.java
new file mode 100644
index 0000000..78cd363
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ApplicationProjectConfig.java
@@ -0,0 +1,116 @@
+/*
+ * 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.maven;
+
+import org.apache.maven.artifact.Artifact;
+
+public class ApplicationProjectConfig {
+
+    public static final String CFG_FEATURES = "features";
+
+    public static final String CFG_TEST_FEATURES = "testFeatures";
+
+    public static final String CFG_FEATURE_REFS = "featureRefs";
+
+    public static final String CFG_TEST_FEATURE_REFS = "testFeatureRefs";
+
+    public static final String DEFAULT_FEATURE_DIR = "src/main/osgi/features";
+
+    public static final String DEFAULT_TEST_FEATURE_DIR = "src/test/osgi/features";
+
+    public static final String DEFAULT_REF_DIR = "src/main/osgi/feature-refs";
+
+    public static final String DEFAULT_TEST_REF_DIR = "src/test/osgi/feature-refs";
+
+    private final String featuresDirName;
+
+    private final String featureRefsDirName;
+
+    private final boolean skipAddDep;
+
+    private final String name;
+
+    private final String scope;
+
+    private final boolean isTest;
+
+    public static ApplicationProjectConfig getMainConfig(final ApplicationProjectInfo info) {
+        return new ApplicationProjectConfig(info, false);
+    }
+
+    public static ApplicationProjectConfig getTestConfig(final ApplicationProjectInfo info) {
+        return new ApplicationProjectConfig(info, true);
+    }
+
+    private ApplicationProjectConfig(final ApplicationProjectInfo info, final boolean test) {
+        this.isTest = test;
+        final String featuresDirCfgName;
+        final String featureRefsDirCfgName;
+        final String defaultDir;
+        final String defaultRefDir;
+        final String skipAddDepCfgName;
+        final String defaultSkipValue;
+        if ( test ) {
+            featuresDirCfgName = CFG_TEST_FEATURES;
+            featureRefsDirCfgName = CFG_TEST_FEATURE_REFS;
+            defaultDir = DEFAULT_TEST_FEATURE_DIR;
+            defaultRefDir = DEFAULT_TEST_REF_DIR;
+            skipAddDepCfgName = FeatureProjectConfig.CFG_SKIP_ADD_TEST_FEATURE_DEPENDENCIES;
+            defaultSkipValue = "true";
+            this.scope = Artifact.SCOPE_TEST;
+            this.name = "test features";
+        } else {
+            featuresDirCfgName = CFG_FEATURES;
+            featureRefsDirCfgName = CFG_FEATURE_REFS;
+            defaultDir = DEFAULT_FEATURE_DIR;
+            defaultRefDir = DEFAULT_REF_DIR;
+            skipAddDepCfgName = FeatureProjectConfig.CFG_SKIP_ADD_FEATURE_DEPENDENCIES;
+            defaultSkipValue = "false";
+            this.scope = Artifact.SCOPE_PROVIDED;
+            this.name = "features";
+        }
+        this.featuresDirName = ProjectHelper.getConfigValue(info.plugin, featuresDirCfgName, defaultRefDir);
+        this.featureRefsDirName = ProjectHelper.getConfigValue(info.plugin, featureRefsDirCfgName, defaultDir);
+        final String skipCfg = ProjectHelper.getConfigValue(info.plugin, skipAddDepCfgName, defaultSkipValue);
+        this.skipAddDep = "true".equals(skipCfg.toLowerCase());
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public String getFeatureDir() {
+        return this.featuresDirName;
+    }
+
+    public String getFeatureRefDir() {
+        return this.featuresDirName;
+    }
+
+    public boolean isSkipAddDependencies() {
+        return this.skipAddDep;
+    }
+
+    public String getScope() {
+        return this.scope;
+    }
+
+    public boolean isTestConfig() {
+        return this.isTest;
+    }
+}
+
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ApplicationProjectInfo.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ApplicationProjectInfo.java
new file mode 100644
index 0000000..dc0e393
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ApplicationProjectInfo.java
@@ -0,0 +1,31 @@
+/*
+ * 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.maven;
+
+import org.apache.sling.feature.Feature;
+
+import java.util.List;
+
+public class ApplicationProjectInfo extends ProjectInfo {
+
+    public List<Feature> features;
+    public List<Feature> testFeatures;
+
+    public List<Feature> assembledFeatures;
+    public List<Feature> assembledTestFeatures;
+}
+
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/Environment.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/Environment.java
new file mode 100644
index 0000000..ca5592d
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/Environment.java
@@ -0,0 +1,35 @@
+/*
+ * 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.maven;
+
+import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
+import org.apache.maven.artifact.resolver.ArtifactResolver;
+import org.apache.maven.execution.MavenSession;
+import org.codehaus.plexus.logging.Logger;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Environment {
+
+    public ArtifactHandlerManager artifactHandlerManager;
+    public ArtifactResolver resolver;
+    public MavenSession session;
+    public Logger logger;
+    public final Map<String, ProjectInfo> modelProjects = new HashMap<>();
+}
+
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/FeatureConstants.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/FeatureConstants.java
new file mode 100644
index 0000000..96a3b2c
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/FeatureConstants.java
@@ -0,0 +1,32 @@
+/*
+ * 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.maven;
+
+public abstract class FeatureConstants {
+
+    public static final String PACKAGING_FEATURE = "osgifeature";
+
+    public static final String PACKAGING_APPLICATION = "osgiapp";
+
+    public static final String CLASSIFIER_FEATURE = "feature";
+
+    public static final String CLASSIFIER_TEST_FEATURE = "testfeature";
+
+    public static final String FEATURE_ARTIFACT_NAME = "feature.json";
+
+    public static final String TEST_FEATURE_ARTIFACT_NAME = "testfeature.json";
+}
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/FeatureProjectConfig.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/FeatureProjectConfig.java
new file mode 100644
index 0000000..a9ba4f5
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/FeatureProjectConfig.java
@@ -0,0 +1,138 @@
+/*
+ * 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.maven;
+
+import org.apache.maven.artifact.Artifact;
+
+public class FeatureProjectConfig {
+
+    public static final String CFG_SKIP_ADD_FEATURE_DEPENDENCIES = "skipAddFeatureDependencies";
+
+    public static final String CFG_SKIP_ADD_TEST_FEATURE_DEPENDENCIES = "skipAddTestFeatureDependencies";
+
+    public static final String CFG_FEATURE_FILE = "featureFile";
+
+    public static final String CFG_TEST_FEATURE_FILE = "testFeatureFile";
+
+    public static final String CFG_FEATURE_INLINED = "feature";
+
+    public static final String CFG_TEST_FEATURE_INLINED = "testFeature";
+
+    public static final String CFG_SKIP_ADD_JAR_TO_FEATURE = "skipAddJarToFeature";
+
+    public static final String CFG_SKIP_ADD_JAR_TO_TEST_FEATURE = "skipAddJarToTestFeature";
+
+    public static final String CFG_JAR_START_LEVEL = "jarStartLevel";
+
+    public static final String DEFAULT_FEATURE_FILE = "src/main/osgi/feature.json";
+
+    public static final String DEFAULT_TEST_FEATURE_FILE = "src/test/osgi/feature.json";
+
+    public static final String DEFAULT_START_LEVEL = "20";
+
+    private final String inlinedFeature;
+
+    private final String featureFileName;
+
+    private final boolean skipAddDep;
+
+    private final String name;
+
+    private final String scope;
+
+    private final boolean isTest;
+
+    private final String jarStartLevel;
+
+    private final boolean skipAddJar;
+
+    public static FeatureProjectConfig getMainConfig(final FeatureProjectInfo info) {
+        return new FeatureProjectConfig(info, false);
+    }
+
+    public static FeatureProjectConfig getTestConfig(final FeatureProjectInfo info) {
+        return new FeatureProjectConfig(info, true);
+    }
+
+    private FeatureProjectConfig(final FeatureProjectInfo info, final boolean test) {
+        this.isTest = test;
+        final String inlineCfgName;
+        final String fileCfgName;
+        final String defaultFile;
+        final String skipAddDepCfgName;
+        final String defaultSkipValue;
+        if ( test ) {
+            inlineCfgName = CFG_TEST_FEATURE_INLINED;
+            fileCfgName = CFG_TEST_FEATURE_FILE;
+            defaultFile = DEFAULT_TEST_FEATURE_FILE;
+            this.scope = Artifact.SCOPE_TEST;
+            skipAddDepCfgName = CFG_SKIP_ADD_TEST_FEATURE_DEPENDENCIES;
+            defaultSkipValue = "true";
+            this.name = "test feature";
+            this.skipAddJar = "true".equals(ProjectHelper.getConfigValue(info.plugin, CFG_SKIP_ADD_JAR_TO_TEST_FEATURE, "true"));
+        } else {
+            inlineCfgName = CFG_FEATURE_INLINED;
+            fileCfgName = CFG_TEST_FEATURE_FILE;
+            defaultFile = DEFAULT_FEATURE_FILE;
+            this.scope = Artifact.SCOPE_PROVIDED;
+            skipAddDepCfgName = CFG_SKIP_ADD_FEATURE_DEPENDENCIES;
+            defaultSkipValue = "false";
+            this.name = "feature";
+            this.skipAddJar = "true".equals(ProjectHelper.getConfigValue(info.plugin, CFG_SKIP_ADD_JAR_TO_FEATURE, "true"));
+        }
+        this.inlinedFeature = ProjectHelper.getConfigValue(info.plugin, inlineCfgName, null);
+        this.featureFileName = ProjectHelper.getConfigValue(info.plugin, fileCfgName, defaultFile);
+        final String skipCfg = ProjectHelper.getConfigValue(info.plugin, skipAddDepCfgName, defaultSkipValue);
+        this.skipAddDep = "true".equals(skipCfg.toLowerCase());
+        this.jarStartLevel = ProjectHelper.getConfigValue(info.plugin, CFG_JAR_START_LEVEL, DEFAULT_START_LEVEL);
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public String getInlinedFeature() {
+        return this.inlinedFeature;
+    }
+
+    public String getFeatureFileName() {
+        return this.featureFileName;
+    }
+
+    public boolean isSkipAddDependencies() {
+        return this.skipAddDep;
+    }
+
+    public String getScope() {
+        return this.scope;
+    }
+
+    public boolean isTestConfig() {
+        return this.isTest;
+    }
+
+    public String getJarStartLevel() {
+        return this.jarStartLevel;
+    }
+
+    public boolean isSkipAddJarToFeature() {
+        return this.skipAddJar;
+    }
+
+
+}
+
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/FeatureProjectInfo.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/FeatureProjectInfo.java
new file mode 100644
index 0000000..b0f379b
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/FeatureProjectInfo.java
@@ -0,0 +1,32 @@
+/*
+ * 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.maven;
+
+import org.apache.sling.feature.Feature;
+
+public class FeatureProjectInfo extends ProjectInfo {
+
+    public boolean featureDone = false;
+    public boolean testFeatureDone = false;
+
+    public Feature feature;
+    public Feature assembledFeature;
+
+    public Feature testFeature;
+    public Feature assembledTestFeature;
+}
+
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/Preprocessor.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/Preprocessor.java
new file mode 100644
index 0000000..47a09c5
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/Preprocessor.java
@@ -0,0 +1,515 @@
+/*
+ * 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.maven;
+
+import org.apache.maven.model.Dependency;
+import org.apache.maven.project.MavenProject;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.process.BuilderContext;
+import org.apache.sling.feature.process.FeatureBuilder;
+import org.apache.sling.feature.process.FeatureProvider;
+import org.apache.sling.feature.support.FeatureUtil;
+import org.apache.sling.feature.support.json.FeatureJSONReader;
+import org.codehaus.plexus.logging.Logger;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * The processor processes all feature projects.
+ */
+public class Preprocessor {
+
+    /**
+     * Process the provided projects.
+     * @param env The environment with all maven settings and projects
+     */
+    public void process(final Environment env) {
+        for(final ProjectInfo info : env.modelProjects.values()) {
+            if ( info instanceof FeatureProjectInfo ) {
+                final FeatureProjectInfo finfo = (FeatureProjectInfo)info;
+                process(env, finfo, FeatureProjectConfig.getMainConfig(finfo));
+                process(env, finfo, FeatureProjectConfig.getTestConfig(finfo));
+                if ( FeatureConstants.PACKAGING_FEATURE.equals(info.project.getPackaging()) && finfo.feature == null ) {
+                    throw new RuntimeException("Feature project has no feature defined: " + info.project.getId());
+                }
+
+                ProjectHelper.storeProjectInfo(finfo);
+            } else {
+                final ApplicationProjectInfo ainfo = (ApplicationProjectInfo)info;
+                process(env, ainfo, ApplicationProjectConfig.getMainConfig(ainfo));
+                process(env, ainfo, ApplicationProjectConfig.getTestConfig(ainfo));
+
+                ProjectHelper.storeProjectInfo(ainfo);
+            }
+        }
+    }
+
+    /**
+     * Process a single feature project.
+     *
+     * @param env The environment with all maven settings and projects
+     * @param info The project to process.
+     * @param config The configuration for the project.
+     */
+    private void process(final Environment env,
+            final FeatureProjectInfo info,
+            final FeatureProjectConfig config) {
+        if ( (config.isTestConfig() && info.testFeatureDone == true )
+             || (!config.isTestConfig() && info.featureDone == true) ) {
+            env.logger.debug("Return assembled " + config.getName() + " for " + info.project.getId());
+            return;
+        }
+        // prevent recursion and multiple processing
+        if ( config.isTestConfig() ) {
+            info.testFeatureDone = true;
+        } else {
+            info.featureDone = true;
+        }
+        env.logger.debug("Processing " + config.getName() + " in project " + info.project.getId());
+
+        // read project feature, either inlined or from file
+        final Feature feature = readProjectFeature(env.logger, info.project, config);
+        if ( feature == null ) {
+            env.logger.debug("No " + config.getName() + " found in project " + info.project.getId());
+            return;
+        }
+        if ( config.isTestConfig() ) {
+            info.testFeature = feature;
+        } else {
+            info.feature = feature;
+        }
+
+        // process attachments (only for jar or bundle)
+        if ( "jar".equals(info.project.getPackaging())
+             || "bundle".equals(info.project.getPackaging())) {
+            if ( config.isSkipAddJarToFeature() ) {
+                env.logger.debug("Skip adding jar to " + config.getName());
+            } else {
+                final Artifact jar = new Artifact(new ArtifactId(info.project.getGroupId(),
+                        info.project.getArtifactId(),
+                        info.project.getVersion(),
+                        null,
+                        "jar"));
+                feature.getBundles().add(Integer.valueOf(config.getJarStartLevel()), jar);
+            }
+        }
+
+        final Feature assembledFeature = FeatureBuilder.assemble(feature, new BuilderContext(this.createFeatureProvider(env,
+                info,
+                config.isTestConfig(),
+                config.isSkipAddDependencies(),
+                config.getScope(), null)));
+        if ( config.isTestConfig() ) {
+            info.assembledTestFeature = assembledFeature;
+        } else {
+            info.assembledFeature = assembledFeature;
+        }
+
+        if ( config.isSkipAddDependencies() ) {
+            env.logger.debug("Not adding artifacts from feature as dependencies");
+        } else {
+            addDependenciesFromFeature(env, info, assembledFeature, config.getScope());
+        }
+    }
+
+    private void scan(final List<File> files, final File dir, final String ext) {
+        for(final File f : dir.listFiles()) {
+            if ( !f.getName().startsWith(".") ) {
+                if ( f.isDirectory() ) {
+                    scan(files, f, ext);
+                } else if ( f.getName().endsWith("." + ext) ) {
+                    files.add(f);
+                }
+            }
+        }
+    }
+
+    /**
+     * Process a single application project.
+     *
+     * @param env The environment with all maven settings and projects
+     * @param info The project to process.
+     * @param config The configuration for the project.
+     */
+    private void process(final Environment env,
+            final ApplicationProjectInfo info,
+            final ApplicationProjectConfig config) {
+        final List<Feature> featureList = new ArrayList<>();
+        env.logger.debug("Processing " + config.getName() + " in project " + info.project.getId());
+
+        // an application supports two sets of files:
+        // features and references to features
+
+        // feature files first:
+        final File dir = new File(info.project.getBasedir(), config.getFeatureDir());
+        if ( dir.exists() ) {
+            final List<File> files = new ArrayList<>();
+            scan(files, dir, "json");
+
+            for(final File file : files) {
+                // create id in case the file does not contain one
+                // classifier is the hard part, we use the file path/name
+                String fileName = file.getAbsolutePath().substring(dir.getAbsolutePath().length() + 1);
+                fileName = fileName.substring(0, fileName.length() - 5); // remove .json
+                fileName = fileName.replace(File.separatorChar, '_');
+                fileName = fileName.replace('-', '_');
+                final String classifier;
+                if ( config.isTestConfig() ) {
+                    classifier = "test_" + fileName;
+                } else {
+                    classifier = fileName;
+                }
+                final ArtifactId id = new ArtifactId(info.project.getGroupId(),
+                        info.project.getArtifactId(),
+                        info.project.getVersion(),
+                        classifier,
+                        FeatureConstants.PACKAGING_FEATURE);
+
+                // We should pass in an "id" to FeatureJSONReader.read and later on check the id (again, need to handle ref files)
+                try (final FileReader reader = new FileReader(file)) {
+                    final Feature feature = FeatureJSONReader.read(reader, id, file.getAbsolutePath());
+
+                    this.checkFeatureId(id, feature);
+
+                    this.setProjectInfo(info.project, feature);
+                    this.postProcessReadFeature(feature);
+                    featureList.add(feature);
+
+                } catch ( final IOException io) {
+                    throw new RuntimeException("Unable to read feature " + file.getAbsolutePath(), io);
+                }
+            }
+        } else {
+            env.logger.debug("Feature directory " + config.getFeatureDir() + " does not exist in project " + info.project.getId());
+        }
+        final List<Feature> assembledFeatureList = new ArrayList<>();
+        for(final Feature feature : featureList) {
+            final Feature assembledFeature = FeatureBuilder.assemble(feature, new BuilderContext(this.createFeatureProvider(env,
+                    info,
+                    config.isTestConfig(),
+                    config.isSkipAddDependencies(),
+                    config.getScope(),
+                    featureList)));
+            assembledFeatureList.add(assembledFeature);
+        }
+        if ( config.isTestConfig() ) {
+            info.testFeatures = featureList;
+            info.assembledTestFeatures = assembledFeatureList;
+        } else {
+            info.features = featureList;
+            info.assembledFeatures = assembledFeatureList;
+        }
+
+        // and now references
+        final List<Feature> featureRefList = new ArrayList<>();
+        final File refDir = new File(info.project.getBasedir(), config.getFeatureRefDir());
+        if ( refDir.exists() ) {
+            final List<File> files = new ArrayList<>();
+            scan(files, refDir, "ref");
+
+            for(final File file : files) {
+                try {
+                    final List<String> features = FeatureUtil.parseFeatureRefFile(file);
+                    if ( features.isEmpty() ) {
+                        env.logger.debug("Empty feature ref file at " + file);
+                    } else {
+                        for(final String ref : features) {
+                            if ( !ref.startsWith("mvn:") ) {
+                                throw new RuntimeException("Unsupported feature ref in feature ref file at " + file + " : " + ref);
+                            }
+                            final ArtifactId id = ArtifactId.fromMvnUrl(ref);
+                            final Feature feature = this.createFeatureProvider(env, info, config.isTestConfig(), config.isSkipAddDependencies(), config.getScope(), null).provide(id);
+                            if ( feature == null ) {
+                                throw new RuntimeException("Unable to resolve feature " + id);
+                            }
+                            featureRefList.add(feature);
+                        }
+                    }
+                } catch ( final IOException io) {
+                    throw new RuntimeException("Unable to read feature " + file.getAbsolutePath(), io);
+                }
+            }
+        }
+        final List<Feature> assembledFeatureRefList = new ArrayList<>();
+        for(final Feature feature : featureRefList) {
+            final Feature assembledFeature = FeatureBuilder.assemble(feature, new BuilderContext(this.createFeatureProvider(env,
+                    info,
+                    config.isTestConfig(),
+                    config.isSkipAddDependencies(),
+                    config.getScope(),
+                    featureList)));
+            assembledFeatureRefList.add(assembledFeature);
+        }
+        if ( config.isTestConfig() ) {
+            info.testFeatures.addAll(featureRefList);
+            info.assembledTestFeatures.addAll(assembledFeatureRefList);
+        } else {
+            info.features.addAll(featureRefList);
+            info.assembledFeatures.addAll(assembledFeatureRefList);
+        }
+
+        if ( config.isSkipAddDependencies() ) {
+            env.logger.debug("Not adding artifacts from features as dependencies");
+        } else {
+            for(final Feature feature : assembledFeatureList) {
+                addDependenciesFromFeature(env, info, feature, config.getScope());
+            }
+        }
+    }
+
+    /**
+     * Add all dependencies from the feature
+     * @param env The environment
+     * @param info The project info
+     * @param assembledFeature The assembled feature for finding the artifacts.
+     * @param scope The scope which the new dependencies should have
+     */
+    private void addDependenciesFromFeature(
+            final Environment env,
+            final ProjectInfo info,
+            final Feature assembledFeature,
+            final String scope) {
+        for(final Map.Entry<Integer, org.apache.sling.feature.Artifact> entry : assembledFeature.getBundles()) {
+            final ArtifactId a = entry.getValue().getId();
+            if ( a.getGroupId().equals(info.project.getGroupId())
+                 && a.getArtifactId().equals(info.project.getArtifactId())
+                 && a.getVersion().equals(info.project.getVersion()) ) {
+                // skip artifact from the same project
+                env.logger.debug("- skipping dependency " + a.toMvnId());
+                continue;
+            }
+
+            env.logger.debug("- adding dependency " + a.toMvnId());
+            final Dependency dep = ProjectHelper.toDependency(a, scope);
+            info.project.getDependencies().add(dep);
+        }
+        for(final Extension ext : assembledFeature.getExtensions()) {
+            if ( ext.getType() != ExtensionType.ARTIFACTS ) {
+                continue;
+            }
+            for(final org.apache.sling.feature.Artifact art : ext.getArtifacts()) {
+                final ArtifactId a = art.getId();
+                if ( a.getGroupId().equals(info.project.getGroupId())
+                     && a.getArtifactId().equals(info.project.getArtifactId())
+                     && a.getVersion().equals(info.project.getVersion()) ) {
+                    // skip artifact from the same project
+                    env.logger.debug("- skipping dependency " + a.toMvnId());
+                    continue;
+                }
+                env.logger.debug("- adding dependency " + a.toMvnId());
+                final Dependency dep = ProjectHelper.toDependency(a, scope);
+                info.project.getDependencies().add(dep);
+            }
+        }
+    }
+
+    /**
+     * Read the feature for a feature project.
+     * The feature is either inlined in the pom or stored in a file in the project.
+     *
+     * @param logger The logger
+     * @param project The current maven project
+     * @param config The configuration
+     * @return The feature or {@code null}
+     */
+    protected Feature readProjectFeature(
+            final Logger logger,
+            final MavenProject project,
+            final FeatureProjectConfig config) {
+        final File featureFile = new File(project.getBasedir(), config.getFeatureFileName());
+        logger.debug("Checking feature file " + config.getFeatureFileName() + " : " + featureFile.exists());
+        logger.debug("Inlined feature : " + (config.getInlinedFeature() != null));
+
+        if ( config.getInlinedFeature() != null && featureFile.exists() ) {
+            throw new RuntimeException("Only one (feature file or inlined feature) can be specified - but not both");
+        }
+
+        final String classifier;
+        if ( config.isTestConfig() ) {
+            classifier = FeatureConstants.CLASSIFIER_TEST_FEATURE;
+        } else if ( FeatureConstants.PACKAGING_FEATURE.equals(project.getPackaging()) ) {
+            classifier = null;
+        } else {
+            classifier = FeatureConstants.CLASSIFIER_FEATURE;
+        }
+        final ArtifactId id = new ArtifactId(project.getGroupId(),
+                project.getArtifactId(),
+                project.getVersion(),
+                classifier,
+                FeatureConstants.PACKAGING_FEATURE);
+
+        final Feature feature;
+        if ( config.getInlinedFeature() != null ) {
+            logger.debug("Reading inlined model from project " + project.getId());
+            try (final Reader reader = new StringReader(config.getInlinedFeature())) {
+                feature = FeatureJSONReader.read(reader, id, null);
+            } catch ( final IOException io) {
+                throw new RuntimeException("Unable to read inlined feature", io);
+            }
+        } else {
+            if ( !featureFile.exists() ) {
+                logger.debug("Feature file " + featureFile + " in project " + project.getId() + " does not exist.");
+                return null;
+            }
+            logger.debug("Reading feature " + featureFile + " in project " + project.getId());
+            try (final FileReader reader = new FileReader(featureFile)) {
+                feature = FeatureJSONReader.read(reader, id, featureFile.getAbsolutePath());
+            } catch ( final IOException io) {
+                throw new RuntimeException("Unable to read feature " + featureFile, io);
+            }
+        }
+        this.checkFeatureId(id, feature);
+
+        this.setProjectInfo(project, feature);
+
+        // post process and return
+        return postProcessReadFeature(feature);
+    }
+
+    private void checkFeatureId(final ArtifactId id, final Feature feature) {
+        // check feature id
+        if ( !id.getGroupId().equals(feature.getId().getGroupId()) ) {
+            throw new RuntimeException("Wrong group id for feature. It should be " + id.getGroupId() + " but is " + feature.getId().getGroupId());
+        }
+        if ( !id.getArtifactId().equals(feature.getId().getArtifactId()) ) {
+            throw new RuntimeException("Wrong artifact id for feature. It should be " + id.getArtifactId() + " but is " + feature.getId().getArtifactId());
+        }
+        if ( !id.getVersion().equals(feature.getId().getVersion()) ) {
+            throw new RuntimeException("Wrong version for feature. It should be " + id.getVersion() + " but is " + feature.getId().getVersion());
+        }
+    }
+
+    /**
+     * Hook to post process the local feature
+     * @param result The read feature
+     * @return The post processed feature
+     */
+    protected Feature postProcessReadFeature(final Feature result)  {
+        return result;
+    }
+
+    protected void setProjectInfo(final MavenProject project, final Feature feature) {
+        // set title, description, vendor, license
+        if ( feature.getTitle() == null ) {
+            feature.setTitle(project.getName());
+        }
+        if ( feature.getDescription() == null ) {
+            feature.setDescription(project.getDescription());
+        }
+        if ( feature.getVendor() == null && project.getOrganization() != null ) {
+            feature.setVendor(project.getOrganization().getName());
+        }
+        if ( feature.getLicense() == null
+             && project.getLicenses() != null
+             && !project.getLicenses().isEmpty()) {
+            final String license = project.getLicenses().stream()
+                    .filter(l -> l.getName() != null )
+                    .map(l -> l.getName())
+                    .collect(Collectors.joining(", "));
+
+            feature.setLicense(license);
+        }
+    }
+
+    protected FeatureProvider createFeatureProvider(final Environment env,
+            final ProjectInfo info,
+            final boolean isTest,
+            final boolean skipAddDependencies,
+            final String dependencyScope,
+            final List<Feature> projectFeatures) {
+        return new FeatureProvider() {
+
+            @Override
+            public Feature provide(final ArtifactId id) {
+
+                final Dependency dep = ProjectHelper.toDependency(id, dependencyScope);
+                if ( !skipAddDependencies ) {
+
+                    env.logger.debug("- adding feature dependency " + id.toMvnId());
+                    info.project.getDependencies().add(dep);
+                }
+
+                // if it's a project from the current reactor build, we can't resolve it right now
+                final String key = id.getGroupId() + ":" + id.getArtifactId();
+                final ProjectInfo depProjectInfo = env.modelProjects.get(key);
+                if ( depProjectInfo != null ) {
+                    env.logger.debug("Found reactor " + id.getType() + " dependency to project: " + id);
+                    // check if it is a feature project
+                    if ( depProjectInfo instanceof FeatureProjectInfo ) {
+                        final FeatureProjectInfo depInfo = (FeatureProjectInfo)depProjectInfo;
+                        if ( isTest ) {
+                            process(env, depInfo, FeatureProjectConfig.getTestConfig(depInfo));
+                        } else {
+                            process(env, depInfo, FeatureProjectConfig.getMainConfig(depInfo));
+                        }
+                        if ( isTest && depInfo.assembledTestFeature == null ) {
+                            env.logger.error("Unable to get feature " + id.toMvnId() + " : Recursive test feature dependency list including project " + info.project);
+                        } else if ( !isTest && depInfo.assembledFeature == null ) {
+                            env.logger.error("Unable to get feature " + id.toMvnId() + " : Recursive feature dependency list including project " + info.project);
+                        } else {
+
+                            if ( isTest ) {
+                                return depInfo.testFeature;
+                            } else {
+                                return depInfo.feature;
+                            }
+                        }
+                    } else {
+                        // we only support a dependency to *this* application project
+                        final ApplicationProjectInfo depInfo = (ApplicationProjectInfo)depProjectInfo;
+                        if ( depInfo != info) {
+                            env.logger.error("Unable to get feature " + id.toMvnId() + " : Feature dependency is to a different application project from " + info.project);
+                            return null;
+                        }
+                        if ( projectFeatures != null ) {
+                            for(final Feature f : projectFeatures) {
+                                if ( f.getId().equals(id)) {
+                                    return f;
+                                }
+                            }
+                        }
+                        return null;
+                    }
+                } else {
+                    env.logger.debug("Found external " + id.getType() + " dependency: " + id);
+
+                    // "external" dependency, we can already resolve it
+                    final File featureFile = ProjectHelper.getOrResolveArtifact(info.project, env.session, env.artifactHandlerManager, env.resolver, id).getFile();
+                    try (final FileReader r = new FileReader(featureFile)) {
+                        return FeatureJSONReader.read(r, featureFile.getAbsolutePath());
+                    } catch ( final IOException ioe) {
+                        env.logger.error("Unable to read feature file from " + featureFile, ioe);
+                    }
+                }
+
+                return null;
+            }
+        };
+    }
+}
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ProjectHelper.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ProjectHelper.java
new file mode 100644
index 0000000..a314113
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ProjectHelper.java
@@ -0,0 +1,319 @@
+/*
+ * 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.maven;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.DefaultArtifact;
+import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
+import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
+import org.apache.maven.artifact.resolver.ArtifactResolutionException;
+import org.apache.maven.artifact.resolver.ArtifactResolver;
+import org.apache.maven.artifact.versioning.VersionRange;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.model.Plugin;
+import org.apache.maven.model.PluginExecution;
+import org.apache.maven.project.MavenProject;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.support.json.FeatureJSONReader;
+import org.apache.sling.feature.support.json.FeatureJSONWriter;
+import org.codehaus.plexus.util.xml.Xpp3Dom;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public abstract class ProjectHelper {
+
+    /** Read feature. */
+    private static final String RAW_FEATURE_JSON = Feature.class.getName() + "/rawmain.json";
+    private static final String RAW_TEST_FEATURE_JSON = Feature.class.getName() + "/rawtest.json";
+
+    /** Assembled feature. */
+    private static final String ASSEMBLED_FEATURE_JSON = Feature.class.getName() + "/assembledmain.json";
+    private static final String ASSEMBLED_TEST_FEATURE_JSON = Feature.class.getName() + "/assembledtest.json";
+
+    private static void store(final MavenProject project, final String key, final Feature feature) {
+        if ( feature != null ) {
+            // we have to serialize as the dependency lifecycle participant uses a different class loader (!)
+            try ( final StringWriter w1 = new StringWriter() ) {
+                FeatureJSONWriter.write(w1, feature);
+                project.setContextValue(key, w1.toString());
+            } catch ( final IOException ioe) {
+                throw new RuntimeException(ioe.getMessage(), ioe);
+            }
+        }
+    }
+
+    private static void store(final MavenProject project, final String key, final List<Feature> features) {
+        if ( features != null && !features.isEmpty()) {
+            project.setContextValue(key, features.size());
+            // we have to serialize as the dependency lifecycle participant uses a different class loader (!)
+            int index = 0;
+            for(final Feature f : features) {
+                try ( final StringWriter w1 = new StringWriter() ) {
+                    FeatureJSONWriter.write(w1, f);
+                    project.setContextValue(key + "_" + String.valueOf(index), w1.toString());
+                    index++;
+                } catch ( final IOException ioe) {
+                    throw new RuntimeException(ioe.getMessage(), ioe);
+                }
+            }
+        }
+    }
+
+    private static Feature getFeature(final MavenProject project, final String key) {
+        final String cacheKey = key + "-cache";
+        Feature result = null;
+        try {
+            result = (Feature) project.getContextValue(cacheKey);
+        } catch ( final Exception e) {
+            // if we get a class cast exception, we read again
+        }
+        if ( result == null ) {
+            final String text = (String)project.getContextValue(key);
+            if ( text == null ) {
+                result = null;
+            } else {
+                try ( final StringReader r = new StringReader(text) ) {
+                    result = FeatureJSONReader.read(r, project.getId());
+                    project.setContextValue(cacheKey, result);
+                } catch ( final IOException ioe) {
+                    throw new RuntimeException(ioe.getMessage(), ioe);
+                }
+            }
+        }
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<Feature> getFeatures(final MavenProject project, final String key) {
+        final String cacheKey = key + "-cache";
+        List<Feature> result = null;
+        try {
+            result = (List<Feature>) project.getContextValue(cacheKey);
+        } catch ( final Exception e) {
+            // if we get a class cast exception, we read again
+        }
+        if ( result == null ) {
+            final Integer size = (Integer)project.getContextValue(key);
+            if ( size != null ) {
+                result = new ArrayList<>();
+                for(int i=0; i<size;i++) {
+                    final String text = (String)project.getContextValue(key + "_" + String.valueOf(i));
+                    if ( text == null ) {
+                        throw new RuntimeException("Unable to get feature from internal store.");
+                    }
+                    try ( final StringReader r = new StringReader(text) ) {
+                        final Feature feature = FeatureJSONReader.read(r, project.getId());
+                        result.add(feature);
+                    } catch ( final IOException ioe) {
+                        throw new RuntimeException(ioe.getMessage(), ioe);
+                    }
+                }
+                project.setContextValue(cacheKey, result);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Store all relevant information about the project for plugins to be
+     * retrieved
+     * @param info The project info
+     */
+    public static void storeProjectInfo(final FeatureProjectInfo info) {
+        store(info.project, RAW_FEATURE_JSON, info.feature);
+        store(info.project, RAW_TEST_FEATURE_JSON, info.testFeature);
+        store(info.project, ASSEMBLED_FEATURE_JSON, info.assembledFeature);
+        store(info.project, ASSEMBLED_TEST_FEATURE_JSON, info.assembledTestFeature);
+    }
+
+    /**
+     * Get the assembled feature from the project
+     * @param project The maven projet
+     * @return The assembled feature or {@code null}
+     */
+    public static Feature getAssembledFeature(final MavenProject project) {
+        return getFeature(project, ASSEMBLED_FEATURE_JSON);
+    }
+
+    /**
+     * Get the raw feature from the project
+     * @param project The maven projet
+     * @return The raw feature or {@code null}
+     */
+    public static Feature getFeature(final MavenProject project) {
+        return getFeature(project, RAW_FEATURE_JSON);
+    }
+
+    /**
+     * Get the assembled test feature from the project
+     * @param project The maven projet
+     * @return The assembled feature or {@code null}
+     */
+    public static Feature getAssembledTestFeature(final MavenProject project) {
+        return getFeature(project, ASSEMBLED_TEST_FEATURE_JSON);
+    }
+
+    /**
+     * Get the raw test feature from the project
+     * @param project The maven projet
+     * @return The raw feature or {@code null}
+     */
+    public static Feature getTestFeature(final MavenProject project) {
+        return getFeature(project, RAW_TEST_FEATURE_JSON);
+    }
+
+    /**
+     * Store all relevant information about the project for plugins to be
+     * retrieved
+     * @param info The project info
+     */
+    public static void storeProjectInfo(final ApplicationProjectInfo info) {
+        store(info.project, RAW_FEATURE_JSON, info.features);
+        store(info.project, RAW_TEST_FEATURE_JSON, info.testFeatures);
+        store(info.project, ASSEMBLED_FEATURE_JSON, info.assembledFeatures);
+        store(info.project, ASSEMBLED_TEST_FEATURE_JSON, info.assembledTestFeatures);
+    }
+
+    /**
+     * Get the assembled features from the project
+     * @param project The maven projet
+     * @return The assembled features or {@code null}
+     */
+    public static List<Feature> getAssembledFeatures(final MavenProject project) {
+        return getFeatures(project, ASSEMBLED_FEATURE_JSON);
+    }
+
+    /**
+     * Get the raw feature from the project
+     * @param project The maven projet
+     * @return The raw features or {@code null}
+     */
+    public static List<Feature> getFeatures(final MavenProject project) {
+        return getFeatures(project, RAW_FEATURE_JSON);
+    }
+
+    /**
+     * Get the assembled test feature from the project
+     * @param project The maven projet
+     * @return The assembled features or {@code null}
+     */
+    public static List<Feature> getAssembledTestFeatures(final MavenProject project) {
+        return getFeatures(project, ASSEMBLED_TEST_FEATURE_JSON);
+    }
+
+    /**
+     * Get the raw test feature from the project
+     * @param project The maven projet
+     * @return The raw features or {@code null}
+     */
+    public static List<Feature> getTestFeatures(final MavenProject project) {
+        return getFeatures(project, RAW_TEST_FEATURE_JSON);
+    }
+
+    /**
+     * Gets a configuration value for a plugin if it is set in the configuration for
+     * the plugin or any configuration for an execution of the plugin.
+     * @param plugin Plugin
+     * @param name Configuration parameter.
+     * @param defaultValue The default value if no configuration is found.
+     * @return The default value if nothing is configured, the value otherwise.
+     * @throws RuntimeException If more than one value is configured
+     */
+    public static String getConfigValue(final Plugin plugin,
+            final String name,
+            final String defaultValue) {
+        final Set<String> values = new HashSet<>();
+        final Xpp3Dom config = plugin == null ? null : (Xpp3Dom)plugin.getConfiguration();
+        final Xpp3Dom globalNode = (config == null ? null : config.getChild(name));
+        if ( globalNode != null ) {
+            values.add(globalNode.getValue());
+        }
+        for(final PluginExecution exec : plugin.getExecutions()) {
+            final Xpp3Dom cfg = (Xpp3Dom)exec.getConfiguration();
+            final Xpp3Dom pluginNode = (cfg == null ? null : cfg.getChild(name));
+            if ( pluginNode != null ) {
+                values.add(pluginNode.getValue());
+            }
+        }
+        if ( values.size() > 1 ) {
+            throw new RuntimeException("More than one value configured in plugin (executions) of "
+                    + plugin.getKey() + " for " + name + " : " + values);
+        }
+        return values.isEmpty() ? defaultValue : values.iterator().next();
+    }
+
+    /**
+     * Get a resolved Artifact from the coordinates provided
+     *
+     * @return the artifact, which has been resolved.
+     */
+    public static Artifact getOrResolveArtifact(final MavenProject project,
+            final MavenSession session,
+            final ArtifactHandlerManager artifactHandlerManager,
+            final ArtifactResolver resolver,
+            final ArtifactId id) {
+        final Set<Artifact> artifacts = project.getDependencyArtifacts();
+        for(final Artifact artifact : artifacts) {
+            if ( artifact.getGroupId().equals(id.getGroupId())
+               && artifact.getArtifactId().equals(id.getArtifactId())
+               && artifact.getVersion().equals(id.getVersion())
+               && artifact.getType().equals(id.getVersion())
+               && ((id.getClassifier() == null && artifact.getClassifier() == null) || (id.getClassifier() != null && id.getClassifier().equals(artifact.getClassifier()))) ) {
+                return artifact;
+            }
+        }
+        final Artifact prjArtifact = new DefaultArtifact(id.getGroupId(),
+                id.getArtifactId(),
+                VersionRange.createFromVersion(id.getVersion()),
+                Artifact.SCOPE_PROVIDED,
+                id.getType(),
+                id.getClassifier(),
+                artifactHandlerManager.getArtifactHandler(id.getType()));
+        try {
+            resolver.resolve(prjArtifact, project.getRemoteArtifactRepositories(), session.getLocalRepository());
+        } catch (final ArtifactResolutionException | ArtifactNotFoundException e) {
+            throw new RuntimeException("Unable to get artifact for " + id.toMvnId(), e);
+        }
+        return prjArtifact;
+    }
+
+    public static String toString(final Dependency d) {
+        return "Dependency {groupId=" + d.getGroupId() + ", artifactId=" + d.getArtifactId() + ", version=" + d.getVersion() +
+                (d.getClassifier() != null ? ", classifier=" + d.getClassifier() : "") +
+                ", type=" + d.getType() + "}";
+    }
+
+    public static Dependency toDependency(final ArtifactId id, final String scope) {
+        final Dependency dep = new Dependency();
+        dep.setGroupId(id.getGroupId());
+        dep.setArtifactId(id.getArtifactId());
+        dep.setVersion(id.getVersion());
+        dep.setType(id.getType());
+        dep.setClassifier(id.getClassifier());
+        dep.setScope(scope);
+
+        return dep;
+    }
+}
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ProjectInfo.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ProjectInfo.java
new file mode 100644
index 0000000..6e3fa0d
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/ProjectInfo.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.maven;
+
+import org.apache.maven.model.Plugin;
+import org.apache.maven.project.MavenProject;
+
+public class ProjectInfo {
+
+    public MavenProject project;
+    public Plugin       plugin;
+}
+
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/mojos/AbstractFeatureMojo.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/mojos/AbstractFeatureMojo.java
new file mode 100644
index 0000000..7c7e2ea
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/mojos/AbstractFeatureMojo.java
@@ -0,0 +1,116 @@
+/*
+ * 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.maven.mojos;
+
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.MavenProjectHelper;
+import org.apache.sling.feature.maven.FeatureProjectConfig;
+
+import java.io.File;
+
+/**
+ * Base class for all mojos.
+ */
+public abstract class AbstractFeatureMojo extends AbstractMojo {
+
+    /**
+     * The feature file..
+     * This parameter is evaluated in the {@link DependencyLifecycleParticipant}.
+     */
+    @Parameter(name = FeatureProjectConfig.CFG_FEATURE_FILE,
+            defaultValue="${basedir}/" + FeatureProjectConfig.DEFAULT_FEATURE_FILE)
+    private File featureFile;
+
+    /**
+     * The test feature file..
+     * This parameter is evaluated in the {@link DependencyLifecycleParticipant}.
+     */
+    @Parameter(name = FeatureProjectConfig.CFG_TEST_FEATURE_FILE,
+            defaultValue="${basedir}/" + FeatureProjectConfig.DEFAULT_TEST_FEATURE_FILE)
+    private File testFeatureFile;
+
+    /**
+     * Inlined model.
+     * This parameter is evaluated in the {@link DependencyLifecycleParticipant}.
+     */
+    @Parameter(name = FeatureProjectConfig.CFG_FEATURE_INLINED)
+    private String feature;
+
+    /**
+     * Inlined test model.
+     * This parameter is evaluated in the {@link DependencyLifecycleParticipant}.
+     */
+    @Parameter(name = FeatureProjectConfig.CFG_TEST_FEATURE_INLINED)
+    private String testFeature;
+
+    /**
+     * If set to {@code true} the artifacts from the feature are not as dependencies to the project.
+     * This parameter is evaluated in the {@link DependencyLifecycleParticipant}.
+     */
+    @Parameter(name=FeatureProjectConfig.CFG_SKIP_ADD_FEATURE_DEPENDENCIES,
+            defaultValue="false")
+    private boolean skipAddFeatureDependencies;
+
+    /**
+     * If set to {@code true} the artifacts from the test feature are not as dependencies to the project.
+     * This parameter is evaluated in the {@link DependencyLifecycleParticipant}.
+     */
+    @Parameter(name=FeatureProjectConfig.CFG_SKIP_ADD_TEST_FEATURE_DEPENDENCIES,
+            defaultValue="true")
+    private boolean skipAddTestFeatureDependencies;
+
+    /**
+     * If set to {@code true} the main jar artifact is not added to the feature.
+     * This parameter is evaluated in the {@link DependencyLifecycleParticipant}.
+     */
+    @Parameter(name=FeatureProjectConfig.CFG_SKIP_ADD_JAR_TO_FEATURE,
+            defaultValue="false")
+    private boolean skipAddJarToFeature;
+
+    /**
+     * If set to {@code true} the main jar artifact is not added to the test feature.
+     * This parameter is evaluated in the {@link DependencyLifecycleParticipant}.
+     */
+    @Parameter(name=FeatureProjectConfig.CFG_SKIP_ADD_JAR_TO_TEST_FEATURE,
+            defaultValue="false")
+    private boolean skipAddJarToTestFeature;
+
+    /**
+     * The start level for the attached jar/bundle.
+     * This parameter is evaluated in the {@link DependencyLifecycleParticipant}.
+     */
+    @Parameter(name=FeatureProjectConfig.CFG_JAR_START_LEVEL,
+            defaultValue=FeatureProjectConfig.DEFAULT_START_LEVEL)
+    private int jarStartLevel;
+
+    @Parameter(property = "project", readonly = true, required = true)
+    protected MavenProject project;
+
+    @Parameter(property = "session", readonly = true, required = true)
+    protected MavenSession mavenSession;
+
+    @Component
+    protected MavenProjectHelper projectHelper;
+
+    protected File getTmpDir() {
+        return new File(this.project.getBuild().getDirectory(), "osgifeature-tmp");
+    }
+}
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/mojos/AttachFeature.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/mojos/AttachFeature.java
new file mode 100644
index 0000000..79c5c23
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/mojos/AttachFeature.java
@@ -0,0 +1,77 @@
+/*
+ * 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.maven.mojos;
+
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.maven.FeatureConstants;
+import org.apache.sling.feature.maven.ProjectHelper;
+import org.apache.sling.feature.support.json.FeatureJSONWriter;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Attach the feature as a project artifact.
+ */
+@Mojo(name = "attach-feature",
+      defaultPhase = LifecyclePhase.PACKAGE,
+      requiresDependencyResolution = ResolutionScope.TEST,
+      threadSafe = true
+    )
+public class AttachFeature extends AbstractFeatureMojo {
+
+    private void attach(final Feature feature,
+            final String artifactName,
+            final String classifier)
+    throws MojoExecutionException {
+        if ( feature != null ) {
+
+            // write the feature
+            final File outputFile = new File(this.project.getBuild().getDirectory() + File.separatorChar + artifactName);
+            outputFile.getParentFile().mkdirs();
+
+            try ( final Writer writer = new FileWriter(outputFile)) {
+                FeatureJSONWriter.write(writer, feature);
+            } catch (final IOException e) {
+                throw new MojoExecutionException("Unable to write feature to " + outputFile, e);
+            }
+
+            // if this project is a feature, it's the main artifact
+            if ( project.getPackaging().equals(FeatureConstants.PACKAGING_FEATURE)
+                 && (FeatureConstants.CLASSIFIER_FEATURE.equals(classifier))) {
+                project.getArtifact().setFile(outputFile);
+            } else {
+                // otherwise attach it as an additional artifact
+                projectHelper.attachArtifact(project, FeatureConstants.PACKAGING_FEATURE,
+                    classifier, outputFile);
+            }
+        }
+    }
+
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        attach(ProjectHelper.getFeature(this.project), FeatureConstants.FEATURE_ARTIFACT_NAME, FeatureConstants.CLASSIFIER_FEATURE);
+        attach(ProjectHelper.getTestFeature(this.project), FeatureConstants.TEST_FEATURE_ARTIFACT_NAME, FeatureConstants.CLASSIFIER_TEST_FEATURE);
+    }
+}
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/mojos/DependencyLifecycleParticipant.java b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/mojos/DependencyLifecycleParticipant.java
new file mode 100644
index 0000000..ba53623
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/java/org/apache/sling/feature/maven/mojos/DependencyLifecycleParticipant.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.maven.mojos;
+
+import org.apache.maven.AbstractMavenLifecycleParticipant;
+import org.apache.maven.MavenExecutionException;
+import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
+import org.apache.maven.artifact.resolver.ArtifactResolver;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.model.Plugin;
+import org.apache.maven.project.MavenProject;
+import org.apache.sling.feature.maven.ApplicationProjectInfo;
+import org.apache.sling.feature.maven.Environment;
+import org.apache.sling.feature.maven.FeatureConstants;
+import org.apache.sling.feature.maven.FeatureProjectInfo;
+import org.apache.sling.feature.maven.Preprocessor;
+import org.apache.sling.feature.maven.ProjectInfo;
+import org.codehaus.plexus.component.annotations.Component;
+import org.codehaus.plexus.component.annotations.Requirement;
+import org.codehaus.plexus.logging.Logger;
+
+/**
+ * Maven lifecycle participant which adds the artifacts of the model to the dependencies.
+ */
+@Component(role = AbstractMavenLifecycleParticipant.class)
+public class DependencyLifecycleParticipant extends AbstractMavenLifecycleParticipant {
+
+    /**
+     * The plugin ID consists of <code>groupId:artifactId</code>, see {@link Plugin#constructKey(String, String)}
+     */
+    private static final String PLUGIN_ID = "org.apache.sling:osgifeature-maven-plugin";
+
+    @Requirement
+    private Logger logger;
+
+    @Requirement
+    private ArtifactHandlerManager artifactHandlerManager;
+
+    /**
+     * Used to look up Artifacts in the remote repository.
+     *
+     */
+    @Requirement
+    private ArtifactResolver resolver;
+
+    @Override
+    public void afterProjectsRead(final MavenSession session) throws MavenExecutionException {
+        final Environment env = new Environment();
+        env.artifactHandlerManager = artifactHandlerManager;
+        env.resolver = resolver;
+        env.logger = logger;
+        env.session = session;
+
+        logger.debug("Searching for project using plugin '" + PLUGIN_ID + "'...");
+
+        for (final MavenProject project : session.getProjects()) {
+            // consider all projects where this plugin is configured
+            Plugin plugin = project.getPlugin(PLUGIN_ID);
+            if (plugin != null) {
+                logger.debug("Found project " + project.getId() + " using " + PLUGIN_ID);
+                final ProjectInfo info;
+                if ( FeatureConstants.PACKAGING_APPLICATION.equals(project.getPackaging()) ) {
+                    info = new ApplicationProjectInfo();
+                } else {
+                    info = new FeatureProjectInfo();
+                }
+                info.plugin = plugin;
+                info.project = project;
+                env.modelProjects.put(project.getGroupId() + ":" + project.getArtifactId(), info);
+            }
+        }
+
+        new Preprocessor().process(env);
+    }
+}
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml b/featuremodel/osgifeature-maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml
new file mode 100644
index 0000000..0f2b325
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml
@@ -0,0 +1,32 @@
+<!--
+ 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.
+-->
+<lifecycleMappingMetadata>
+    <pluginExecutions>
+        <pluginExecution>
+            <pluginExecutionFilter>
+                <goals>
+                    <goal>attach-feature</goal>
+                </goals>
+            </pluginExecutionFilter>
+            <action>
+                <ignore/>
+            </action>
+        </pluginExecution>
+    </pluginExecutions>
+</lifecycleMappingMetadata>
diff --git a/featuremodel/osgifeature-maven-plugin/src/main/resources/META-INF/plexus/components.xml b/featuremodel/osgifeature-maven-plugin/src/main/resources/META-INF/plexus/components.xml
new file mode 100644
index 0000000..6933d3b
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/main/resources/META-INF/plexus/components.xml
@@ -0,0 +1,103 @@
+<!--
+ 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.
+-->
+<component-set>
+  <components>
+    <component>
+      <role>org.apache.maven.lifecycle.mapping.LifecycleMapping</role>
+      <role-hint>osgifeature</role-hint>
+      <implementation>org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping</implementation>
+      <configuration>
+        <lifecycles>
+          <lifecycle>
+            <id>default</id>
+            <phases>
+              <package>org.apache.sling:slingstart-maven-plugin:attach-slingfeature</package>
+              <install>org.apache.maven.plugins:maven-install-plugin:install</install>
+              <deploy>org.apache.maven.plugins:maven-deploy-plugin:deploy</deploy>
+            </phases>
+          </lifecycle>
+        </lifecycles>
+      </configuration>
+    </component>
+    <component>
+      <role>org.apache.maven.lifecycle.mapping.LifecycleMapping</role>
+      <role-hint>osgiapp</role-hint>
+      <implementation>org.apache.maven.lifecycle.mapping.DefaultLifecycleMapping</implementation>
+      <configuration>
+        <lifecycles>
+          <lifecycle>
+            <id>default</id>
+            <phases>
+              <process-resources>org.apache.maven.plugins:maven-resources-plugin:resources</process-resources>
+              <compile>org.apache.maven.plugins:maven-compiler-plugin:compile</compile>
+              <process-test-resources>
+                  org.apache.maven.plugins:maven-resources-plugin:testResources,
+              </process-test-resources>
+              <test-compile>org.apache.maven.plugins:maven-compiler-plugin:testCompile</test-compile>
+              <test>org.apache.maven.plugins:maven-surefire-plugin:test</test>
+              <prepare-package>org.apache.sling:slingstart-maven-plugin:prepare-package</prepare-package>              
+              <package>
+                  org.apache.sling:slingstart-maven-plugin:attach-slingfeature,
+                  org.apache.sling:slingstart-maven-plugin:package
+              </package>
+              <install>org.apache.maven.plugins:maven-install-plugin:install</install>
+              <deploy>org.apache.maven.plugins:maven-deploy-plugin:deploy</deploy>
+            </phases>
+          </lifecycle>
+        </lifecycles>
+      </configuration>
+    </component>
+    <component>
+      <role>org.apache.maven.artifact.handler.ArtifactHandler</role>
+      <role-hint>osgifeature</role-hint>
+      <implementation>org.apache.maven.artifact.handler.DefaultArtifactHandler</implementation>
+      <configuration>
+        <type>osgifeature</type>
+        <includesDependencies>false</includesDependencies>
+        <language>json</language>
+        <extension>json</extension>
+        <addedToClasspath>false</addedToClasspath>
+      </configuration>
+    </component>
+    <component>
+      <role>org.apache.maven.artifact.handler.ArtifactHandler</role>
+      <role-hint>osgiapp</role-hint>
+      <implementation>org.apache.maven.artifact.handler.DefaultArtifactHandler</implementation>
+      <configuration>
+        <type>osgiapp</type>
+        <includesDependencies>false</includesDependencies>
+        <language>java</language>
+        <extension>jar</extension>
+        <addedToClasspath>false</addedToClasspath>
+      </configuration>
+    </component>
+    <component>
+      <role>org.apache.maven.artifact.handler.ArtifactHandler</role>
+      <role-hint>osgijar</role-hint>
+      <implementation>org.apache.maven.artifact.handler.DefaultArtifactHandler</implementation>
+      <configuration>
+        <type>osgijar</type>
+        <includesDependencies>false</includesDependencies>
+        <language>java</language>
+        <extension>zip</extension>
+        <addedToClasspath>false</addedToClasspath>
+      </configuration>
+    </component>
+  </components>
+</component-set>
diff --git a/featuremodel/osgifeature-maven-plugin/src/site/markdown/index.md b/featuremodel/osgifeature-maven-plugin/src/site/markdown/index.md
new file mode 100644
index 0000000..bd08e61
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/site/markdown/index.md
@@ -0,0 +1,6 @@
+OSGiMaven Plugin
+================
+
+Maven Plugin for OSGi Applications
+
+See [Goals](plugin-info.html) for a list of supported goals.
diff --git a/featuremodel/osgifeature-maven-plugin/src/site/site.xml b/featuremodel/osgifeature-maven-plugin/src/site/site.xml
new file mode 100644
index 0000000..9abab11
--- /dev/null
+++ b/featuremodel/osgifeature-maven-plugin/src/site/site.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+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>
+  <skin>
+    <groupId>org.apache.maven.skins</groupId>
+    <artifactId>maven-fluido-skin</artifactId>
+    <version>1.6</version>
+  </skin>
+  <body>
+    <menu name="Overview">
+      <item name="Introduction" href="index.html"/>
+      <item name="Goals" href="plugin-info.html"/>
+    </menu>
+    <menu ref="reports"/>
+  </body>
+</project>
diff --git a/featuremodel/pom.xml b/featuremodel/pom.xml
new file mode 100644
index 0000000..110bd53
--- /dev/null
+++ b/featuremodel/pom.xml
@@ -0,0 +1,69 @@
+<?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</groupId>
+        <artifactId>apache</artifactId>
+        <version>18</version>
+    </parent>
+
+    <groupId>org.apache.sling</groupId>
+    <artifactId>sling-feature-builder</artifactId>
+    <packaging>pom</packaging>
+    <version>1-SNAPSHOT</version>
+
+    <name>Apache Sling Feature (Builder)</name>
+    <description>
+        Pseudo project to build the complete Apache Sling Feature project.
+    </description>
+
+    <inceptionYear>2007</inceptionYear>
+
+    <issueManagement>
+        <system>Jira</system>
+        <url>http://issues.apache.org/jira/browse/SLING</url>
+    </issueManagement>
+
+
+    <scm>
+        <connection>
+            scm:svn:http://svn.apache.org/repos/asf/sling/whiteboard/cziegeler
+        </connection>
+        <developerConnection>
+            scm:svn:https://svn.apache.org/repos/asf/sling/whiteboard/cziegeler
+        </developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/whiteboard/cziegeler</url>
+    </scm>
+
+    <modules>
+        <module>feature</module> 
+        <module>feature-karaf</module>
+        <module>feature-modelconverter</module>
+        <module>osgifeature-maven-plugin</module>
+        <module>feature-analyser</module>
+        <module>feature-launcher</module>
+        <module>feature-support</module>	
+        <module>feature-applicationbuilder</module>
+    </modules>
+
+</project>

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.