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

[sling-org-apache-sling-provisioning-model] 16/34: Implement provisioning model

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

rombert pushed a commit to annotated tag org.apache.sling.provisioning.model-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-provisioning-model.git

commit 456ebb16d5e6812308b3a0926e18d44707ce9cb8
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Tue Sep 30 13:27:21 2014 +0000

    Implement provisioning model
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/tooling/support/slingstart-model@1628438 13f79535-47bb-0310-9956-ffa450edef68
---
 .../apache/sling/provisioning/model/Artifact.java  | 232 ++++++++++++
 .../sling/provisioning/model/ArtifactGroup.java    |  80 +++++
 .../sling/provisioning/model/Configuration.java    |  83 +++++
 .../apache/sling/provisioning/model/Feature.java   | 118 ++++++
 .../org/apache/sling/provisioning/model/Model.java |  76 ++++
 .../sling/provisioning/model/ModelConstants.java   |  50 +++
 .../sling/provisioning/model/ModelUtility.java     | 329 +++++++++++++++++
 .../apache/sling/provisioning/model/RunMode.java   | 216 +++++++++++
 .../apache/sling/provisioning/model/Traceable.java |  71 ++++
 .../sling/provisioning/model/io/ModelReader.java   | 397 +++++++++++++++++++++
 .../sling/provisioning/model/io/ModelWriter.java   | 233 ++++++++++++
 .../sling/provisioning/model/io/package-info.java  |  24 ++
 .../sling/provisioning/model/package-info.java     |  24 ++
 13 files changed, 1933 insertions(+)

diff --git a/src/main/java/org/apache/sling/provisioning/model/Artifact.java b/src/main/java/org/apache/sling/provisioning/model/Artifact.java
new file mode 100644
index 0000000..bb5d844
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Artifact.java
@@ -0,0 +1,232 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Description of an artifact.
+ * 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 as well.
+ * An artifact can have any metadata.
+ */
+public class Artifact extends Traceable {
+
+    private final String groupId;
+    private final String artifactId;
+    private final String version;
+    private final String classifier;
+    private final String type;
+
+    private final Map<String, String> metadata = new HashMap<String, String>();
+
+    /**
+     * Create a new artifact object
+     * @param gId   The group id (required)
+     * @param aId   The artifact id (required)
+     * @param version The version (required)
+     * @param classifier The classifier (optional)
+     * @param type The type/extension (optional, defaults to jar)
+     */
+    public Artifact(final String gId,
+            final String aId,
+            final String version,
+            final String classifier,
+            final String type) {
+        this.groupId = (gId != null ? gId.trim() : null);
+        this.artifactId = (aId != null ? aId.trim() : null);
+        this.version = (version != null ? version.trim() : null);
+        final String trimmedType = (type != null ? type.trim() : null);
+        if ( "bundle".equals(trimmedType) || trimmedType == null || trimmedType.isEmpty() ) {
+            this.type = "jar";
+        } else {
+            this.type = trimmedType;
+        }
+        final String trimmedClassifier = (classifier != null ? classifier.trim() : null);
+        if ( trimmedClassifier != null && trimmedClassifier.isEmpty() ) {
+            this.classifier = null;
+        } else {
+            this.classifier = trimmedClassifier;
+        }
+    }
+
+    /**
+     * Create a new artifact from a maven url,
+     * 'mvn:' [ repository-url '!' ] group-id '/' artifact-id [ '/' [version] [ '/' [type] [ '/' classifier ] ] ] ]
+     * @param url The url
+     * @return A new artifact
+     * @throws IllegalArgumentException If the url is not valid
+     */
+    public static Artifact fromMvnUrl(final String url) {
+        if ( url == null || !url.startsWith("mvn:") ) {
+            throw new IllegalArgumentException("Invalid mvn url: " + url);
+        }
+        final String content = url.substring(4);
+        // ignore repository url
+        int pos = content.indexOf('!');
+        if ( pos != -1 ) {
+            throw new IllegalArgumentException("Repository url is not supported for Maven artifacts at the moment.");
+        }
+        final String coordinates = (pos == -1 ? content : content.substring(pos + 1));
+        String gId = null;
+        String aId = null;
+        String version = null;
+        String type = null;
+        String classifier = null;
+        int part = 0;
+        String value = coordinates;
+        while ( value != null ) {
+            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++;
+        }
+        if ( version == null ) {
+            version = "LATEST";
+        }
+        return new Artifact(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(this.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 version.
+     * @return The version.
+     */
+    public String getVersion() {
+        return version;
+    }
+
+    /**
+     * Return the optional classifier.
+     * @return The classifier or null.
+     */
+    public String getClassifier() {
+        return classifier;
+    }
+
+    /**
+     * Return the type.
+     * @return The type.
+     */
+    public String getType() {
+        return type;
+    }
+
+    /**
+     * Get the metadata of the artifact.
+     * @return The metadata.
+     */
+    public Map<String, String> getMetadata() {
+        return this.metadata;
+    }
+
+    /**
+     * Create a Maven like relative repository path.
+     */
+    public String getRepositoryPath() {
+        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 String toString() {
+        return "Artifact [groupId=" + groupId
+                + ", artifactId=" + artifactId
+                + ", version=" + version
+                + ", classifier=" + classifier
+                + ", type=" + type
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+                + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/ArtifactGroup.java b/src/main/java/org/apache/sling/provisioning/model/ArtifactGroup.java
new file mode 100644
index 0000000..0d0ccf9
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/ArtifactGroup.java
@@ -0,0 +1,80 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A artifact group holds a set of artifacts.
+ * A valid start level is positive, start level 0 means the default OSGi start level.
+ */
+public class ArtifactGroup extends Traceable
+    implements Comparable<ArtifactGroup> {
+
+    private final int level;
+
+    private final List<Artifact> artifacts = new ArrayList<Artifact>();
+
+    public ArtifactGroup(final int level) {
+        this.level = level;
+    }
+
+    public int getLevel() {
+        return this.level;
+    }
+
+    public List<Artifact> getArtifacts() {
+        return this.artifacts;
+    }
+
+    /**
+     * Search an artifact with the same groupId, artifactId, version, type and classifier.
+     * Version is not considered.
+     */
+    public Artifact search(final Artifact template) {
+        Artifact found = null;
+        for(final Artifact current : this.artifacts) {
+            if ( current.getGroupId().equals(template.getGroupId())
+              && current.getArtifactId().equals(template.getArtifactId())
+              && current.getClassifier().equals(template.getClassifier())
+              && current.getType().equals(template.getType()) ) {
+                found = current;
+                break;
+            }
+        }
+        return found;
+    }
+
+    @Override
+    public int compareTo(final ArtifactGroup o) {
+        if ( this.level < o.level ) {
+            return -1;
+        } else if ( this.level > o.level ) {
+            return 1;
+        }
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        return "ArtifactGroup [level=" + level
+                + ", artifacts=" + artifacts
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+                + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/Configuration.java b/src/main/java/org/apache/sling/provisioning/model/Configuration.java
new file mode 100644
index 0000000..67535ce
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Configuration.java
@@ -0,0 +1,83 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+
+/**
+ * Configuration
+ */
+public class Configuration extends Traceable {
+
+    private final String pid;
+
+    private final String factoryPid;
+
+    private final Dictionary<String, Object> properties = new Hashtable<String, Object>();
+
+    public Configuration(final String pid, final String factoryPid) {
+        this.pid = (pid != null ? pid.trim() : null);
+        this.factoryPid = (factoryPid != null ? factoryPid.trim() : null);
+    }
+
+    /**
+     * Get the pid.
+     * If this is a factory configuration, it returns the alias for the configuration
+     * @return The pid.
+     */
+    public String getPid() {
+        return this.pid;
+    }
+
+    /**
+     * Return the factory pid
+     * @return The factory pid or null.
+     */
+    public String getFactoryPid() {
+        return this.factoryPid;
+    }
+
+    /**
+     * Is this a special configuration?
+     * @return Special config
+     */
+    public boolean isSpecial() {
+        if ( pid != null && pid.startsWith(":") ) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Get all properties of the configuration.
+     * @return The properties
+     */
+    public Dictionary<String, Object> getProperties() {
+        return this.properties;
+    }
+
+    @Override
+    public String toString() {
+        return "Configuration [pid=" + pid
+                + ", factoryPid=" + factoryPid
+                + ", properties=" + properties
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+                + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/Feature.java b/src/main/java/org/apache/sling/provisioning/model/Feature.java
new file mode 100644
index 0000000..7ca9da6
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Feature.java
@@ -0,0 +1,118 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * A feature is a collection of
+ * - variables
+ * - run modes
+ */
+public class Feature
+    extends Traceable
+    implements Comparable<Feature> {
+
+    /** All run modes. */
+    private final List<RunMode> runModes = new ArrayList<RunMode>();
+
+    /** Variables. */
+    private final Map<String, String> variables = new HashMap<String, String>();
+
+    private final String name;
+
+    /**
+     * Construct a new feature.
+     * @param name The feature name
+     */
+    public Feature(final String name) {
+        this.name = name;
+    }
+
+    /**
+     * Get the name of the feature.
+     * @return The name or {@code null} for an anonymous feature.
+     */
+    public String getName() {
+        return this.name;
+    }
+
+    /**
+     * Get all variables
+     * @return The set of variables
+     */
+    public Map<String, String> getVariables() {
+        return this.variables;
+    }
+
+    public List<RunMode> getRunModes() {
+        return this.runModes;
+    }
+
+    /**
+     * Find the run mode if available
+     * @param runModes
+     * @return The feature or null.
+     */
+    public RunMode findRunMode(final String[] runModes) {
+        final String[] sortedRunModes = RunMode.getSortedRunModesArray(runModes);
+        RunMode result = null;
+        for(final RunMode current : this.runModes) {
+            if ( Arrays.equals(sortedRunModes, current.getRunModes()) ) {
+                result = current;
+                break;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Get or create the run mode.
+     * @param runModes The run modes.
+     * @return The feature for the given run modes.
+     */
+    public RunMode getOrCreateFeature(final String[] runModes) {
+        RunMode result = findRunMode(runModes);
+        if ( result == null ) {
+            result = new RunMode(runModes);
+            this.runModes.add(result);
+            Collections.sort(this.runModes);
+        }
+        return result;
+    }
+
+    @Override
+    public int compareTo(final Feature o) {
+        if ( this.name == null ) {
+            if ( o.name == null ) {
+                return 0;
+            }
+            return -1;
+        }
+        if ( o.name == null ) {
+            return 1;
+        }
+        return this.name.compareTo(o.name);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/Model.java b/src/main/java/org/apache/sling/provisioning/model/Model.java
new file mode 100644
index 0000000..5ce2a55
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Model.java
@@ -0,0 +1,76 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A model is the central object.
+ * It consists of features.
+ */
+public class Model extends Traceable {
+
+    /** All features. */
+    private final List<Feature> features = new ArrayList<Feature>();
+
+    /**
+     * Find the feature if available
+     * @param name The feature name
+     * @return The feature or {@code null}.
+     */
+    public Feature findFeature(final String name) {
+        for(final Feature f : this.features) {
+            if ( name.equals(f.getName()) ) {
+                return f;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get or create the feature.
+     * @param runModes The run modes.
+     * @return The feature for the given run modes.
+     */
+    public Feature getOrCreateFeature(final String name) {
+        Feature result = findFeature(name);
+        if ( result == null ) {
+            result = new Feature(name);
+            this.features.add(result);
+            Collections.sort(this.features);
+        }
+        return result;
+    }
+
+    /**
+     * Return all features.
+     * The returned list is modifiable and directly modifies the model.
+     * @return The list of features.
+     */
+    public List<Feature> getFeatures() {
+        return this.features;
+    }
+
+    @Override
+    public String toString() {
+        return "Model [features=" + features
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+                + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/ModelConstants.java b/src/main/java/org/apache/sling/provisioning/model/ModelConstants.java
new file mode 100644
index 0000000..14bb646
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/ModelConstants.java
@@ -0,0 +1,50 @@
+/*
+ * 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.provisioning.model;
+
+
+public abstract class ModelConstants {
+
+    /** Name of the configuration containing the web.xml. */
+    public static final String CFG_WEB_XML = ":web.xml";
+
+    /** Name of the configuration for the bootstrap contents. */
+    public static final String CFG_BOOTSTRAP = ":bootstrap";
+
+    /** Unprocessed configuration values. */
+    public static final String CFG_UNPROCESSED = ":rawconfig";
+
+    /** Format of the unprocessed configuration values. */
+    public static final String CFG_UNPROCESSED_FORMAT = ":rawconfig.format";
+
+    public static final String CFG_FORMAT_FELIX_CA = "felixca";
+
+    public static final String CFG_FORMAT_PROPERTIES = "properties";
+
+    /** Name of the base run mode for the Sling launchpad. */
+    public static final String RUN_MODE_BASE = ":base";
+
+    /** Name of the boot run mode. */
+    public static final String RUN_MODE_BOOT = ":boot";
+
+    /** Name of the webapp run mode. */
+    public static final String RUN_MODE_WEBAPP = ":webapp";
+
+    /** Name of the standalone run mode. */
+    public static final String RUN_MODE_STANDALONE = ":standalone";
+
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/ModelUtility.java b/src/main/java/org/apache/sling/provisioning/model/ModelUtility.java
new file mode 100644
index 0000000..d479810
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/ModelUtility.java
@@ -0,0 +1,329 @@
+/*
+ * 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.provisioning.model;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.felix.cm.file.ConfigurationHandler;
+
+
+/**
+ * Merge two models
+ */
+public abstract class ModelUtility {
+
+    /**
+     * Merge the additional model into the base model.
+     * @param base The base model.
+     * @param additional The additional model.
+     */
+    public static void merge(final Model base, final Model additional) {
+        // features
+        for(final Feature feature : additional.getFeatures()) {
+            final Feature baseFeature = base.getOrCreateFeature(feature.getName());
+
+            // variables
+            baseFeature.getVariables().putAll(feature.getVariables());
+
+            // run modes
+            for(final RunMode runMode : feature.getRunModes()) {
+                final RunMode baseRunMode = baseFeature.getOrCreateFeature(runMode.getRunModes());
+
+                // artifact groups
+                for(final ArtifactGroup group : runMode.getArtifactGroups()) {
+                    final ArtifactGroup baseGroup = baseRunMode.getOrCreateArtifactGroup(group.getLevel());
+
+                    for(final Artifact artifact : group.getArtifacts()) {
+                        final Artifact found = baseGroup.search(artifact);
+                        if ( found != null ) {
+                            baseGroup.getArtifacts().remove(found);
+                        }
+                        baseGroup.getArtifacts().add(artifact);
+                    }
+                }
+
+                // configurations
+                for(final Configuration config : runMode.getConfigurations()) {
+                    final Configuration found = baseRunMode.getOrCreateConfiguration(config.getPid(), config.getFactoryPid());
+                    final Enumeration<String> e = config.getProperties().keys();
+                    while ( e.hasMoreElements() ) {
+                        final String key = e.nextElement();
+                        found.getProperties().put(key, config.getProperties().get(key));
+                    }
+                }
+
+                // settings
+                for(final Map.Entry<String, String> entry : runMode.getSettings().entrySet() ) {
+                    baseRunMode.getSettings().put(entry.getKey(), entry.getValue());
+                }
+            }
+
+        }
+    }
+
+    /**
+     * Optional variable resolver
+     */
+    public interface VariableResolver {
+
+        /**
+         * Resolve the variable.
+         * An implementation might get the value of a variable from the system properties,
+         * or the environment etc.
+         * As a fallback, the resolver should check the variables of the model.
+         * @param model The model
+         * @param name The variable name
+         * @return The variable value or null.
+         */
+        String resolve(final Feature model, final String name);
+    }
+
+    /**
+     * Replace all variables in the model and return a new model with the replaced values.
+     * @param model The base model.
+     * @param resolver Optional variable resolver.
+     * @return The model with replaced variables.
+     * @throws IllegalArgumentException If a variable can't be replaced or configuration properties can't be parsed
+     */
+    public static Model getEffectiveModel(final Model model, final VariableResolver resolver) {
+        final Model result = new Model();
+        result.setComment(model.getComment());
+        result.setLocation(model.getLocation());
+
+        for(final Feature feature : model.getFeatures()) {
+            final Feature newFeature = result.getOrCreateFeature(feature.getName());
+            newFeature.setComment(feature.getComment());
+            newFeature.setLocation(feature.getLocation());
+
+            newFeature.getVariables().putAll(feature.getVariables());
+
+            for(final RunMode runMode : feature.getRunModes()) {
+                final RunMode newRunMode = newFeature.getOrCreateFeature(runMode.getRunModes());
+                newRunMode.setComment(runMode.getComment());
+                newRunMode.setLocation(runMode.getLocation());
+
+                for(final ArtifactGroup group : runMode.getArtifactGroups()) {
+                    final ArtifactGroup newGroup = newRunMode.getOrCreateArtifactGroup(group.getLevel());
+                    newGroup.setComment(group.getComment());
+                    newGroup.setLocation(group.getLocation());
+
+                    for(final Artifact artifact : group.getArtifacts()) {
+                        final Artifact newArtifact = new Artifact(replace(feature, artifact.getGroupId(), resolver),
+                                replace(feature, artifact.getArtifactId(), resolver),
+                                replace(feature, artifact.getVersion(), resolver),
+                                replace(feature, artifact.getClassifier(), resolver),
+                                replace(feature, artifact.getType(), resolver));
+                        newArtifact.setComment(artifact.getComment());
+                        newArtifact.setLocation(artifact.getLocation());
+
+                        newGroup.getArtifacts().add(newArtifact);
+                    }
+                }
+
+                for(final Configuration config : runMode.getConfigurations()) {
+                    final Configuration newConfig = new Configuration(config.getPid(), config.getFactoryPid());
+                    newConfig.setComment(config.getComment());
+                    newConfig.setLocation(config.getLocation());
+
+                    // check for raw configuration
+                    final String rawConfig = (String)config.getProperties().get(ModelConstants.CFG_UNPROCESSED);
+                    if ( rawConfig != null ) {
+                        final String format = (String)config.getProperties().get(ModelConstants.CFG_UNPROCESSED_FORMAT);
+
+                        if ( ModelConstants.CFG_FORMAT_PROPERTIES.equals(format) ) {
+                            // properties
+                            final Properties props = new Properties();
+                            try {
+                                props.load(new StringReader(rawConfig));
+                            } catch ( final IOException ioe) {
+                                throw new IllegalArgumentException("Unable to read configuration properties.", ioe);
+                            }
+                            final Enumeration<Object> i = props.keys();
+                            while ( i.hasMoreElements() ) {
+                                final String key = (String)i.nextElement();
+                                newConfig.getProperties().put(key, props.get(key));
+                            }
+                        } else {
+                            // Apache Felix CA format
+                            ByteArrayInputStream bais = null;
+                            try {
+                                bais = new ByteArrayInputStream(rawConfig.getBytes("UTF-8"));
+                                @SuppressWarnings("unchecked")
+                                final Dictionary<String, Object> props = ConfigurationHandler.read(bais);
+                                final Enumeration<String> i = props.keys();
+                                while ( i.hasMoreElements() ) {
+                                    final String key = i.nextElement();
+                                    newConfig.getProperties().put(key, props.get(key));
+                                }
+                            } catch ( final IOException ioe) {
+                                throw new IllegalArgumentException("Unable to read configuration properties.", ioe);
+                            } finally {
+                                if ( bais != null ) {
+                                    try {
+                                        bais.close();
+                                    } catch ( final IOException ignore ) {
+                                        // ignore
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        // simply copy
+                        final Enumeration<String> i = config.getProperties().keys();
+                        while ( i.hasMoreElements() ) {
+                            final String key = i.nextElement();
+                            newConfig.getProperties().put(key, config.getProperties().get(key));
+                        }
+                    }
+
+                    newRunMode.getConfigurations().add(newConfig);
+                }
+
+                for(final Map.Entry<String, String> entry : runMode.getSettings().entrySet() ) {
+                    newRunMode.getSettings().put(entry.getKey(), replace(feature, entry.getValue(), resolver));
+                }
+            }
+
+        }
+        return result;
+    }
+
+    /**
+     * Replace properties in the string.
+     *
+     * @param model The model
+     * @param v The variable name
+     * @param resolver Optional resolver
+     * @result The value of the variable
+     * @throws IllegalArgumentException
+     */
+    private static String replace(final Feature model, final String v, final VariableResolver resolver) {
+        if ( v == null ) {
+            return null;
+        }
+        String msg = v;
+        // check for variables
+        int pos = -1;
+        int start = 0;
+        while ( ( pos = msg.indexOf('$', start) ) != -1 ) {
+            if ( msg.length() > pos && msg.charAt(pos + 1) == '{' && (pos == 0 || msg.charAt(pos - 1) != '$') ) {
+                final int endPos = msg.indexOf('}', pos);
+                if ( endPos == -1 ) {
+                    start = pos + 1;
+                } else {
+                    final String name = msg.substring(pos + 2, endPos);
+                    final String value;
+                    if ( resolver != null ) {
+                        value = resolver.resolve(model, name);
+                    } else {
+                        value = model.getVariables().get(name);
+                    }
+                    if ( value == null ) {
+                        throw new IllegalArgumentException("Unknown variable: " + name);
+                    }
+                    msg = msg.substring(0, pos) + value + msg.substring(endPos + 1);
+                }
+            } else {
+                start = pos + 1;
+            }
+        }
+        return msg;
+    }
+
+    /**
+     * Validates the model.
+     * @param model
+     * @return A map with errors or {@code null}.
+     */
+    public static Map<Traceable, String> validate(final Model model) {
+        final Map<Traceable, String> errors = new HashMap<Traceable, String>();
+
+        for(final Feature feature : model.getFeatures() ) {
+            // validate feature
+            if ( feature.getName() == null || feature.getName().isEmpty() ) {
+                errors.put(feature, "Name is required for a feature.");
+            }
+            for(final RunMode runMode : feature.getRunModes()) {
+                final String[] rm = runMode.getRunModes();
+                if ( rm != null ) {
+                    boolean hasSpecial = false;
+                    for(final String m : rm) {
+                        if ( m.startsWith(":") ) {
+                            if ( hasSpecial ) {
+                                errors.put(runMode, "Invalid modes " + Arrays.toString(rm));
+                                break;
+                            }
+                            hasSpecial = true;
+                        }
+                    }
+                }
+
+                for(final ArtifactGroup sl : runMode.getArtifactGroups()) {
+                    if ( sl.getLevel() < 0 ) {
+                        errors.put(sl, "Invalid start level " + sl.getLevel());
+                    }
+                    for(final Artifact a : sl.getArtifacts()) {
+                        String error = null;
+                        if ( a.getGroupId() == null || a.getGroupId().isEmpty() ) {
+                            error = "groupId missing";
+                        }
+                        if ( a.getArtifactId() == null || a.getArtifactId().isEmpty() ) {
+                            error = (error != null ? error + ", " : "") + "artifactId missing";
+                        }
+                        if ( a.getVersion() == null || a.getVersion().isEmpty() ) {
+                            error = (error != null ? error + ", " : "") + "version missing";
+                        }
+                        if ( a.getType() == null || a.getType().isEmpty() ) {
+                            error = (error != null ? error + ", " : "") + "type missing";
+                        }
+                        if (error != null) {
+                            errors.put(a, error);
+                        }
+                    }
+                }
+
+                for(final Configuration c : runMode.getConfigurations()) {
+                    String error = null;
+                    if ( c.getPid() == null || c.getPid().isEmpty() ) {
+                        error = "pid missing";
+                    }
+                    if ( c.isSpecial() && c.getFactoryPid() != null ) {
+                        error = (error != null ? error + ", " : "") + "factory pid not allowed for special configuration";
+                    }
+                    if ( c.getProperties().isEmpty() ) {
+                        error = (error != null ? error + ", " : "") + "configuration properties missing";
+                    }
+                    if (error != null) {
+                        errors.put(c, error);
+                    }
+                }
+            }
+        }
+        if ( errors.size() == 0 ) {
+            return null;
+        }
+        return errors;
+    }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/RunMode.java b/src/main/java/org/apache/sling/provisioning/model/RunMode.java
new file mode 100644
index 0000000..677abaf
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/RunMode.java
@@ -0,0 +1,216 @@
+/*
+ * 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.provisioning.model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A feature is a collection of
+ * - artifacts (through start levels)
+ * - configurations
+ * - settings
+ *
+ * A feature might be tied to run modes. Only if all run modes are active,
+ * this feature is active.
+ * In addition to custom, user defined run modes, special run modes exists.
+ * A special run mode name starts with a colon.
+ */
+public class RunMode
+    extends Traceable
+    implements Comparable<RunMode> {
+
+    private final String[] runModes;
+
+    private final List<ArtifactGroup> groups = new ArrayList<ArtifactGroup>();
+
+    private final List<Configuration> configurations = new ArrayList<Configuration>();
+
+    private final Map<String, String> settings = new HashMap<String, String>();
+
+    public RunMode(final String[] runModes) {
+        this.runModes = getSortedRunModesArray(runModes);
+    }
+
+    public static String[] getSortedRunModesArray(final String[] runModes) {
+        // sort run modes
+        if ( runModes != null ) {
+            final List<String> list = new ArrayList<String>();
+            for(final String m : runModes) {
+                if ( m != null ) {
+                    if ( !m.trim().isEmpty() ) {
+                        list.add(m.trim());
+                    }
+                }
+            }
+            if ( list.size() > 0 ) {
+                Collections.sort(list);
+                return list.toArray(new String[list.size()]);
+            }
+        }
+        return null;
+    }
+
+    public String[] getRunModes() {
+        return this.runModes;
+    }
+
+    /**
+     * Check if this feature is active wrt the given set of active run modes.
+     */
+    public boolean isActive(final Set<String> activeRunModes) {
+        boolean active = true;
+        if ( runModes != null ) {
+            for(final String mode : runModes) {
+                if ( !activeRunModes.contains(mode) ) {
+                    active = false;
+                    break;
+                }
+            }
+        }
+        return active;
+    }
+
+    /**
+     * Check whether this feature is a special one
+     */
+    public boolean isSpecial() {
+        if ( runModes != null && runModes.length == 1 && runModes[0].startsWith(":") ) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Check if this feature is tied to a single specific run mode.
+     */
+    public boolean isRunMode(final String mode) {
+        if ( mode == null && this.runModes == null ) {
+            return true;
+        }
+        if ( mode != null
+             && this.runModes != null
+             && this.runModes.length == 1
+             && this.runModes[0].equals(mode) ) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Find the artifact group.
+     */
+    public ArtifactGroup findArtifactGroup(final int startLevel) {
+        for(final ArtifactGroup g : this.groups) {
+            if ( g.getLevel() == startLevel ) {
+                return g;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get or create an artifact group
+     */
+    public ArtifactGroup getOrCreateArtifactGroup(final int startLevel) {
+        ArtifactGroup result = this.findArtifactGroup(startLevel);
+        if ( result == null ) {
+            result = new ArtifactGroup(startLevel);
+            this.groups.add(result);
+            Collections.sort(this.groups);
+        }
+        return result;
+    }
+
+    /**
+     * Search a configuration with a pid
+     */
+    public Configuration getConfiguration(final String pid) {
+        for(final Configuration c : this.configurations) {
+            if ( pid.equals(c.getPid()) ) {
+                return c;
+            }
+        }
+        return null;
+    }
+
+    public Configuration getOrCreateConfiguration(final String pid, final String factoryPid) {
+        Configuration found = null;
+        for(final Configuration current : this.configurations) {
+            if ( factoryPid == null ) {
+                if ( current.getFactoryPid() == null && current.getPid().equals(pid) ) {
+                    found = current;
+                    break;
+                }
+            } else {
+                if ( factoryPid.equals(current.getFactoryPid()) && current.getPid().equals(pid) ) {
+                    found = current;
+                    break;
+                }
+            }
+        }
+        if ( found == null ) {
+            found = new Configuration(pid, factoryPid);
+            this.configurations.add(found);
+        }
+        return found;
+    }
+
+    public List<ArtifactGroup> getArtifactGroups() {
+        return this.groups;
+    }
+
+    public List<Configuration> getConfigurations() {
+        return this.configurations;
+    }
+
+    public Map<String, String> getSettings() {
+        return this.settings;
+    }
+
+    /**
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    @Override
+    public int compareTo( RunMode o2) {
+        if ( this.runModes == null ) {
+            if ( o2.runModes == null ) {
+                return 0;
+            }
+            return -1;
+        }
+        if ( o2.runModes == null ) {
+            return 1;
+        }
+        return Arrays.toString(this.runModes).compareTo(Arrays.toString(o2.runModes));
+    }
+
+    @Override
+    public String toString() {
+        return "RunMode [runModes=" + Arrays.toString(runModes) + ", groups="
+                + groups + ", configurations=" + configurations + ", settings="
+                + settings
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+                + "]";
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/Traceable.java b/src/main/java/org/apache/sling/provisioning/model/Traceable.java
new file mode 100644
index 0000000..4345ab5
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Traceable.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.provisioning.model;
+
+/**
+ * A traceable has a comment and a location.
+ * Both are optional.
+ */
+public abstract class Traceable {
+
+    /** The location. */
+    private String location;
+
+    /** The comment. */
+    private String comment;
+
+    /**
+     * Get the location.
+     * The location might be the location of the model 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 comment.
+     * @return The comment or {@code null}.
+     */
+    public String getComment() {
+        return this.comment;
+    }
+
+    /**
+     * Set the comment.
+     * @param value The new comment.
+     */
+    public void setComment(final String value) {
+        this.comment = value;
+    }
+
+    @Override
+    public String toString() {
+        return "SSMTraceable [location=" + location + ", comment=" + comment
+                + "]";
+    }
+}
+
diff --git a/src/main/java/org/apache/sling/provisioning/model/io/ModelReader.java b/src/main/java/org/apache/sling/provisioning/model/io/ModelReader.java
new file mode 100644
index 0000000..17a5b34
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/io/ModelReader.java
@@ -0,0 +1,397 @@
+/*
+ * 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.provisioning.model.io;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+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.Model;
+import org.apache.sling.provisioning.model.ModelConstants;
+import org.apache.sling.provisioning.model.RunMode;
+import org.apache.sling.provisioning.model.Traceable;
+
+
+public class ModelReader {
+
+    private enum CATEGORY {
+        NONE(null),
+        FEATURE("feature"),
+        VARIABLES("variables"),
+        GLOBAL("global"),
+        RUN_MODE("runMode"),
+        ARTIFACTS("artifacts"),
+        SETTINGS("settings"),
+        CONFIGURATIONS("configurations"),
+        CONFIG(null);
+
+        public final String name;
+
+        private CATEGORY(final String n) {
+            this.name = n;
+        }
+    }
+
+    /**
+     * Reads the model file
+     * The reader is not closed.
+     * @throws IOException
+     */
+    public static Model read(final Reader reader, final String location)
+    throws IOException {
+        final ModelReader mr = new ModelReader(location);
+        return mr.readModel(reader);
+    }
+
+    /** Is this a single feature model? */
+    private boolean isSingleFeature = false;
+
+    private CATEGORY mode = CATEGORY.NONE;
+
+    private final Model model = new Model();
+
+    private Feature feature = null;
+    private RunMode runMode = null;
+    private ArtifactGroup artifactGroup = null;
+    private Configuration config = null;
+
+    private String comment = null;
+
+    private StringBuilder configBuilder = null;
+
+    private LineNumberReader lineNumberReader;
+
+    private final String exceptionPrefix;
+
+    private ModelReader(final String location) {
+        this.model.setLocation(location);
+        if ( location == null ) {
+            exceptionPrefix = "";
+        } else {
+            exceptionPrefix = location + " : ";
+        }
+    }
+
+    private Model readModel(final Reader reader)
+    throws IOException {
+
+        boolean global = true;
+
+        lineNumberReader = new LineNumberReader(reader);
+        String line;
+        while ( (line = lineNumberReader.readLine()) != null ) {
+            line = line.trim();
+            // ignore empty line
+            if ( line.isEmpty() ) {
+                checkConfig();
+                continue;
+            }
+            // comment?
+            if ( line.startsWith("#") ) {
+                checkConfig();
+                mode = CATEGORY.NONE;
+                final String c = line.substring(1).trim();
+                if ( comment == null ) {
+                    comment = c;
+                } else {
+                    comment = comment + "\n" + c;
+                }
+                continue;
+            }
+
+            if ( global ) {
+                global = false;
+                model.setComment(comment);
+                comment = null;
+            }
+
+            if ( line.startsWith("[") ) {
+                if ( !line.endsWith("]") ) {
+                    throw new IOException(exceptionPrefix + "Illegal category definition in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                }
+                int pos = 1;
+                while ( line.charAt(pos) != ']' && !Character.isWhitespace(line.charAt(pos))) {
+                    pos++;
+                }
+                final String category = line.substring(1, pos);
+                CATEGORY found = null;
+                for (CATEGORY c : CATEGORY.values()) {
+                    if ( category.equals(c.name)) {
+                        found = c;
+                        break;
+                    }
+                }
+                if ( found == null ) {
+                    throw new IOException(exceptionPrefix + "Unknown category in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                }
+                this.mode = found;
+                Map<String, String> parameters = Collections.emptyMap();
+                if (line.charAt(pos) != ']') {
+                    final String parameterLine = line.substring(pos + 1, line.length() - 1).trim();
+                    parameters = parseParameters(parameterLine);
+                }
+
+                switch ( this.mode ) {
+                    case NONE : break; // this can never happen
+                    case CONFIG : break; // this can never happen
+                    case FEATURE : if ( this.isSingleFeature ) {
+                                       throw new IOException(exceptionPrefix + "Single feature model allows only one feature.");
+                                   }
+                                   final String name = parameters.get("name");
+                                   if ( name == null ) {
+                                       throw new IOException(exceptionPrefix + "Feature name missing in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                   }
+                                   if ( parameters.size() > 1 ) {
+                                       throw new IOException(exceptionPrefix + "Unknown feature parameters in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                   }
+                                   if ( model.findFeature(name) != null ) {
+                                       throw new IOException(exceptionPrefix + "Duplicate feature in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                   }
+                                   this.feature = model.getOrCreateFeature(name);
+                                   this.init(this.feature);
+                                   this.runMode = null;
+                                   this.artifactGroup = null;
+                                   break;
+                    case VARIABLES : checkFeature();
+                                     break;
+                    case RUN_MODE : checkFeature();
+                                    final String names = parameters.get("names");
+                                    if ( names == null ) {
+                                        throw new IOException(exceptionPrefix + "Run mode names missing in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                    }
+                                    if ( parameters.size() > 1 ) {
+                                        throw new IOException(exceptionPrefix + "Unknown run mode parameters in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                    }
+                                    final String[] rm = names.split(",");
+                                    if ( this.feature.findRunMode(rm) != null ) {
+                                        throw new IOException(exceptionPrefix + "Duplicate run mode in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                    }
+                                    this.runMode = this.feature.getOrCreateFeature(rm);
+                                    this.init(this.runMode);
+                                    this.artifactGroup = null;
+                                    break;
+                    case GLOBAL : checkFeature();
+                                  if ( !parameters.isEmpty() ) {
+                                      throw new IOException(exceptionPrefix + "Unknown global parameters in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                  }
+                                  if ( this.feature.findRunMode(null) != null ) {
+                                      throw new IOException(exceptionPrefix + "Duplicate global run mode in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                  }
+                                  this.runMode = this.feature.getOrCreateFeature(null);
+                                  this.init(this.runMode);
+                                  this.artifactGroup = null;
+                                  break;
+                    case SETTINGS: checkFeature();
+                                   checkRunMode();
+                                   break;
+                    case ARTIFACTS: checkFeature();
+                                    checkRunMode();
+                                    String level = parameters.get("startLevel");
+                                    if ( (level == null && !parameters.isEmpty())
+                                        || (level != null && parameters.size() > 1 ) ) {
+                                        throw new IOException(exceptionPrefix + "Unknown artifacts parameters in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                    }
+                                    int startLevel = 0;
+                                    if ( level != null ) {
+                                        try {
+                                            startLevel = Integer.valueOf(level);
+                                        } catch ( final NumberFormatException nfe) {
+                                            throw new IOException(exceptionPrefix + "Invalid start level in line " + this.lineNumberReader.getLineNumber() + ": " + line + ":" + level);
+                                        }
+                                    }
+                                    if ( this.runMode.findArtifactGroup(startLevel) != null ) {
+                                        throw new IOException(exceptionPrefix + "Duplicate artifact group in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                    }
+                                    this.artifactGroup = this.runMode.getOrCreateArtifactGroup(startLevel);
+                                    this.init(this.artifactGroup);
+                                    break;
+                    case CONFIGURATIONS: checkFeature();
+                                         checkRunMode();
+                                         break;
+                }
+            } else {
+                switch ( this.mode ) {
+                    case NONE : break;
+                    case VARIABLES : final String[] vars = parseProperty(line);
+                                     feature.getVariables().put(vars[0], vars[1]);
+                                     break;
+                    case SETTINGS : final String[] settings = parseProperty(line);
+                                    runMode.getSettings().put(settings[0], settings[1]);
+                                    break;
+                    case FEATURE:
+                    case RUN_MODE:
+                    case GLOBAL:
+                    case ARTIFACTS : this.checkFeature();
+                                     this.checkRunMode();
+                                     if ( this.artifactGroup == null ) {
+                                         this.artifactGroup = this.runMode.getOrCreateArtifactGroup(0);
+                                     }
+                                     String artifactUrl = line;
+                                     Map<String, String> parameters = Collections.emptyMap();
+                                     if ( line.endsWith("]") ) {
+                                         final int startPos = line.indexOf("[");
+                                         if ( startPos != -1 ) {
+                                             artifactUrl = line.substring(0, startPos).trim();
+                                             parameters = parseParameters(line.substring(startPos + 1, line.length() - 1).trim());
+                                         }
+                                     }
+                                     try {
+                                         final Artifact artifact = Artifact.fromMvnUrl("mvn:" + artifactUrl);
+                                         this.init(artifact);
+                                         this.artifactGroup.getArtifacts().add(artifact);
+                                         artifact.getMetadata().putAll(parameters);
+                                     } catch ( final IllegalArgumentException iae) {
+                                         throw new IOException(exceptionPrefix + iae.getMessage() + " in line " + this.lineNumberReader.getLineNumber(), iae);
+                                     }
+                                     break;
+                    case CONFIGURATIONS : String configId = line;
+                                          Map<String, String> cfgPars = Collections.emptyMap();
+                                          if ( line.endsWith("]") ) {
+                                              final int startPos = line.indexOf("[");
+                                              if ( startPos != -1 ) {
+                                                  configId = line.substring(0, startPos).trim();
+                                                  cfgPars = parseParameters(line.substring(startPos + 1, line.length() - 1).trim());
+                                              }
+                                          }
+                                          String format = cfgPars.get("format");
+                                          if ( (format == null && !cfgPars.isEmpty())
+                                               || (format != null && cfgPars.size() > 1 ) ) {
+                                              throw new IOException(exceptionPrefix + "Unknown configuration parameters in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                          }
+                                          if ( format != null ) {
+                                              if ( !ModelConstants.CFG_FORMAT_FELIX_CA.equals(format)
+                                                  && !ModelConstants.CFG_FORMAT_PROPERTIES.equals(format) ) {
+                                                  throw new IOException(exceptionPrefix + "Unknown format configuration parameter in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                                              }
+                                          } else {
+                                              format = ModelConstants.CFG_FORMAT_FELIX_CA;
+                                          }
+                                          final int factoryPos = configId.indexOf('-');
+                                          if ( factoryPos == -1 ) {
+                                              config = new Configuration(configId, null);
+                                          } else {
+                                              config = new Configuration(configId.substring(factoryPos + 1), configId.substring(0, factoryPos));
+                                          }
+                                          this.init(config);
+                                          config.getProperties().put(ModelConstants.CFG_UNPROCESSED_FORMAT, format);
+                                          runMode.getConfigurations().add(config);
+                                          configBuilder = new StringBuilder();
+                                          mode = CATEGORY.CONFIG;
+                                          break;
+                    case CONFIG : configBuilder.append(line);
+                                  break;
+                }
+            }
+
+        }
+        checkConfig();
+        if ( comment != null ) {
+            throw new IOException(exceptionPrefix + "Comment not allowed at the end of file");
+        }
+
+        return model;
+    }
+
+    /**
+     * Check for a feature object
+     */
+    private void checkFeature() throws IOException {
+        if ( feature == null ) {
+            if ( model.getLocation() == null ) {
+                throw new IOException(exceptionPrefix + "No preceding feature definition in line " + this.lineNumberReader.getLineNumber());
+            }
+            final int beginPos = model.getLocation().replace('\\', '/').lastIndexOf("/");
+            String newName = model.getLocation().substring(beginPos + 1);
+            final int endPos = newName.lastIndexOf('.');
+            if ( endPos != -1 ) {
+                newName = newName.substring(0, endPos);
+            }
+            this.isSingleFeature = true;
+            feature = model.getOrCreateFeature(newName);
+        }
+    }
+
+    /**
+     * Check for a run mode object
+     */
+    private void checkRunMode() throws IOException {
+        if ( runMode == null ) {
+            runMode = this.feature.getOrCreateFeature(null);
+        }
+    }
+
+    private void init(final Traceable traceable) {
+        traceable.setComment(this.comment);
+        this.comment = null;
+        final String number = String.valueOf(this.lineNumberReader.getLineNumber());
+        if ( model.getLocation() != null ) {
+            traceable.setLocation(model.getLocation() + ":" + number);
+        } else {
+            traceable.setLocation(number);
+        }
+    }
+
+    private void checkConfig() {
+        if ( config != null ) {
+            config.getProperties().put(ModelConstants.CFG_UNPROCESSED, configBuilder.toString());
+            this.mode = CATEGORY.CONFIGURATIONS;
+        }
+        config = null;
+        configBuilder = null;
+    }
+
+    /**
+     * Parse a single property line
+     * @param line The line
+     * @return The key and the value
+     * @throws IOException If something goes wrong
+     */
+    private String[] parseProperty(final String line) throws IOException {
+        final int equalsPos = line.indexOf('=');
+        final String key = line.substring(0, equalsPos).trim();
+        final String value = line.substring(equalsPos + 1).trim();
+        if (key.isEmpty() || value.isEmpty() ) {
+            throw new IOException(exceptionPrefix + "Invalid property; " + line + " in line " + this.lineNumberReader.getLineNumber());
+        }
+        return new String[] {key, value};
+    }
+
+    private Map<String, String> parseParameters(final String line) throws IOException {
+        final Map<String, String>parameters = new HashMap<String, String>();
+        final String[] keyValuePairs = line.split(" ");
+        for(String kv : keyValuePairs) {
+            kv = kv.trim();
+            if ( !kv.isEmpty() ) {
+                final int sep = kv.indexOf('=');
+                if ( sep == -1 ) {
+                    throw new IOException(exceptionPrefix + "Invalid parameter definition in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                }
+                parameters.put(kv.substring(0, sep).trim(), kv.substring(sep + 1).trim());
+            }
+        }
+        return parameters;
+    }
+}
+
+
diff --git a/src/main/java/org/apache/sling/provisioning/model/io/ModelWriter.java b/src/main/java/org/apache/sling/provisioning/model/io/ModelWriter.java
new file mode 100644
index 0000000..4b023f0
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/io/ModelWriter.java
@@ -0,0 +1,233 @@
+/*
+ * 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.provisioning.model.io;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.Writer;
+import java.util.Map;
+
+import org.apache.felix.cm.file.ConfigurationHandler;
+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.Model;
+import org.apache.sling.provisioning.model.ModelConstants;
+import org.apache.sling.provisioning.model.RunMode;
+import org.apache.sling.provisioning.model.Traceable;
+
+/**
+ * Simple writer for the a model
+ */
+public class ModelWriter {
+
+    private static void writeComment(final PrintWriter pw, final Traceable traceable)
+    throws IOException {
+        if ( traceable.getComment() != null ) {
+            final LineNumberReader lnr = new LineNumberReader(new StringReader(traceable.getComment()));
+            try {
+                String line = null;
+                while ( (line = lnr.readLine()) != null ) {
+                    pw.print("# ");
+                    pw.println(line);
+                }
+            } finally {
+                lnr.close();
+            }
+        }
+    }
+
+    /**
+     * Writes the model to the writer.
+     * The writer is not closed.
+     * @param writer
+     * @param subystem
+     * @throws IOException
+     */
+    public static void write(final Writer writer, final Model model)
+    throws IOException {
+        final PrintWriter pw = new PrintWriter(writer);
+
+        writeComment(pw, model);
+
+        // features
+        for(final Feature feature : model.getFeatures()) {
+            writeComment(pw, feature);
+            pw.print("[feature name=");
+            pw.print(feature.getName());
+            pw.println("]");
+            pw.println();
+
+            // variables
+            if ( !feature.getVariables().isEmpty() ) {
+                pw.println("[variables]");
+                for(final Map.Entry<String, String> entry : feature.getVariables().entrySet()) {
+                    pw.print("  ");
+                    pw.print(entry.getKey());
+                    pw.print("=");
+                    pw.println(entry.getValue());
+                }
+                pw.println();
+            }
+
+            // run modes
+            for(final RunMode runMode : feature.getRunModes()) {
+                // skip empty run mode
+                if ( runMode.getConfigurations().isEmpty() && runMode.getSettings().isEmpty() ) {
+                    boolean hasArtifacts = false;
+                    for(final ArtifactGroup sl : runMode.getArtifactGroups()) {
+                        if ( !sl.getArtifacts().isEmpty() ) {
+                            hasArtifacts = true;
+                            break;
+                        }
+                    }
+                    if ( !hasArtifacts ) {
+                        continue;
+                    }
+                }
+                writeComment(pw, runMode);
+                final String[] runModes = runMode.getRunModes();
+                if ( runModes == null || runModes.length == 0 ) {
+                    pw.println("[global]");
+                } else {
+                    pw.print("[runMode names=");
+                    boolean first = true;
+                    for(final String mode : runModes) {
+                        if ( first ) {
+                            first = false;
+                        } else {
+                            pw.print(",");
+                        }
+                        pw.print(mode);
+                    }
+                    pw.println("]");
+                }
+                pw.println();
+
+                // settings
+                if ( !runMode.getSettings().isEmpty() ) {
+                    pw.println("[settings]");
+
+                    for(final Map.Entry<String, String> entry : runMode.getSettings().entrySet()) {
+                        pw.print("  ");
+                        pw.print(entry.getKey());
+                        pw.print("=");
+                        pw.println(entry.getValue());
+                    }
+                    pw.println();
+                }
+
+                // artifact groups
+                for(final ArtifactGroup group : runMode.getArtifactGroups()) {
+                    // skip empty groups
+                    if ( group.getArtifacts().isEmpty() ) {
+                        continue;
+                    }
+                    writeComment(pw, group);
+                    pw.print("[artifacts");
+                    if ( group.getLevel() > 0 ) {
+                        pw.print(" startLevel=");
+                        pw.print(String.valueOf(group.getLevel()));
+                    }
+                    pw.println("]");
+                    pw.println();
+
+                    // artifacts
+                    for(final Artifact ad : group.getArtifacts()) {
+                        writeComment(pw, ad);
+                        pw.print("  ");
+                        pw.print(ad.toMvnUrl().substring(4));
+                        if ( !ad.getMetadata().isEmpty() ) {
+                            boolean first = true;
+                            for(final Map.Entry<String, String> entry : ad.getMetadata().entrySet()) {
+                                if ( first ) {
+                                    first = false;
+                                    pw.print("{ ");
+                                } else {
+                                    pw.print(", ");
+                                }
+                                pw.print(entry.getKey());
+                                pw.print("=");
+                                pw.println(entry.getValue());
+                            }
+                            pw.print("}");
+                        }
+                        pw.println();
+                    }
+                    if ( !group.getArtifacts().isEmpty() ) {
+                        pw.println();
+                    }
+                }
+
+                // configurations
+                if ( !runMode.getConfigurations().isEmpty() ) {
+                    pw.println("[configurations]");
+                    for(final Configuration config : runMode.getConfigurations()) {
+                        writeComment(pw, config);
+                        final String raw = (String)config.getProperties().get(ModelConstants.CFG_UNPROCESSED);
+                        String format = (String)config.getProperties().get(ModelConstants.CFG_UNPROCESSED_FORMAT);
+                        if ( format == null ) {
+                            format = ModelConstants.CFG_FORMAT_FELIX_CA;
+                        }
+                        pw.print("  ");
+                        if ( config.getFactoryPid() != null ) {
+                            pw.print(config.getFactoryPid());
+                            pw.print("-");
+                        }
+                        pw.print(config.getPid());
+                        if ( !ModelConstants.CFG_FORMAT_FELIX_CA.equals(format) ) {
+                            pw.print(" { format=}");
+                            pw.print(format);
+                            pw.print(" }");
+                        }
+                        pw.println();
+
+                        final String configString;
+                        if ( raw != null ) {
+                            configString = raw;
+                        } else if ( config.isSpecial() ) {
+                            configString = config.getProperties().get(config.getPid()).toString();
+                        } else {
+                            final ByteArrayOutputStream os = new ByteArrayOutputStream();
+                            try {
+                                ConfigurationHandler.write(os , config.getProperties());
+                            } finally {
+                                os.close();
+                            }
+                            configString = new String(os.toByteArray(), "UTF-8");
+                        }
+                        // we have to read the configuration line by line to properly indent
+                        final LineNumberReader lnr = new LineNumberReader(new StringReader(configString));
+                        String line = null;
+                        while ((line = lnr.readLine()) != null ) {
+                            if ( line.trim().isEmpty() ) {
+                                continue;
+                            }
+                            pw.print("    ");
+                            pw.println(line.trim());
+                        }
+                        pw.println();
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/provisioning/model/io/package-info.java b/src/main/java/org/apache/sling/provisioning/model/io/package-info.java
new file mode 100644
index 0000000..31f116d
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/io/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+@Version("1.0")
+package org.apache.sling.provisioning.model.io;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/provisioning/model/package-info.java b/src/main/java/org/apache/sling/provisioning/model/package-info.java
new file mode 100644
index 0000000..5e50b7f
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+@Version("1.0")
+package org.apache.sling.provisioning.model;
+
+import aQute.bnd.annotation.Version;
+

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