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:06 UTC

[sling-org-apache-sling-provisioning-model] 10/34: Implement txt format for reading and writing

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 1a36bd0e2702a0cc2ec385039b46d94bb5481028
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Fri Sep 26 15:25:23 2014 +0000

    Implement txt format for reading and writing
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/tooling/support/slingstart-model@1627807 13f79535-47bb-0310-9956-ffa450edef68
---
 .../apache/sling/slingstart/model/SSMArtifact.java |  61 +++--
 .../sling/slingstart/model/SSMConfiguration.java   |  28 +--
 .../sling/slingstart/model/SSMConstants.java       |  13 +
 .../sling/slingstart/model/SSMDeliverable.java     |  19 +-
 .../apache/sling/slingstart/model/SSMFeature.java  |  44 +---
 .../sling/slingstart/model/SSMStartLevel.java      |  21 +-
 .../model/{SSMConstants.java => SSMTraceable.java} |  30 ++-
 .../sling/slingstart/model/SSMValidator.java       |  94 ++++++++
 .../slingstart/model/txt/TXTSSMModelReader.java    | 265 +++++++++++++--------
 .../slingstart/model/txt/TXTSSMModelWriter.java    | 191 +++++++++++++++
 .../slingstart/model/xml/XMLSSMModelReader.java    |  71 ++++--
 11 files changed, 590 insertions(+), 247 deletions(-)

diff --git a/src/main/java/org/apache/sling/slingstart/model/SSMArtifact.java b/src/main/java/org/apache/sling/slingstart/model/SSMArtifact.java
index d05e8ff..7cc9b3b 100644
--- a/src/main/java/org/apache/sling/slingstart/model/SSMArtifact.java
+++ b/src/main/java/org/apache/sling/slingstart/model/SSMArtifact.java
@@ -25,7 +25,7 @@ import java.util.Map;
  * In addition, the classifier and type can be specified as well.
  * An artifact can have any metadata.
  */
-public class SSMArtifact {
+public class SSMArtifact extends SSMTraceable {
 
     private final String groupId;
     private final String artifactId;
@@ -126,6 +126,27 @@ public class SSMArtifact {
     }
 
     /**
+     * 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);
+        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.
      */
@@ -196,38 +217,14 @@ public class SSMArtifact {
         return sb.toString();
     }
 
-    /**
-     * Validates the object and throws an IllegalStateException
-     * This object needs:
-     * - groupId
-     * - artifactId
-     * - version
-     * If type is null, it's set to "jar"
-     * If type is "bundle", it's set to "jar"
-     * - classifier is optional
-     *
-     * @throws IllegalStateException
-     */
-    public void validate() {
-        // check/correct values
-        if ( groupId == null || groupId.isEmpty() ) {
-            throw new IllegalStateException(this + " : groupId");
-        }
-        if ( artifactId == null || artifactId.isEmpty() ) {
-            throw new IllegalStateException(this + " : artifactId");
-        }
-        if ( version == null || version.isEmpty() ) {
-            throw new IllegalStateException(this + " : version");
-        }
-        if ( type == null || type.isEmpty() ) {
-            throw new IllegalStateException(this + " : type");
-        }
-    }
-
     @Override
     public String toString() {
-        return "SSMArtifact [groupId=" + groupId + ", artifactId=" + artifactId
-                + ", version=" + version + ", classifier=" + classifier
-                + ", type=" + type + "]";
+        return "SSMArtifact [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/slingstart/model/SSMConfiguration.java b/src/main/java/org/apache/sling/slingstart/model/SSMConfiguration.java
index 3a3f0a0..f2342e2 100644
--- a/src/main/java/org/apache/sling/slingstart/model/SSMConfiguration.java
+++ b/src/main/java/org/apache/sling/slingstart/model/SSMConfiguration.java
@@ -23,7 +23,7 @@ import java.util.Hashtable;
 /**
  * Configuration
  */
-public class SSMConfiguration {
+public class SSMConfiguration extends SSMTraceable {
 
     private final String pid;
 
@@ -54,25 +54,6 @@ public class SSMConfiguration {
     }
 
     /**
-     * validates the object and throws an IllegalStateException
-     * This object needs:
-     * - pid
-     * - properties
-     * - factoryPid is optional
-     *
-     * @throws IllegalStateException
-     */
-    public void validate() {
-        // check/correct values
-        if ( pid == null || pid.isEmpty() ) {
-            throw new IllegalStateException("pid");
-        }
-        if ( properties == null || properties.isEmpty() ) {
-            throw new IllegalStateException("properties");
-        }
-    }
-
-    /**
      * Is this a special configuration?
      * @return Special config
      */
@@ -93,7 +74,10 @@ public class SSMConfiguration {
 
     @Override
     public String toString() {
-        return "SSMConfiguration [pid=" + pid + ", factoryPid=" + factoryPid
-                + ", properties=" + properties + "]";
+        return "SSMConfiguration [pid=" + pid
+                + ", factoryPid=" + factoryPid
+                + ", properties=" + properties
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+                + "]";
     }
 }
diff --git a/src/main/java/org/apache/sling/slingstart/model/SSMConstants.java b/src/main/java/org/apache/sling/slingstart/model/SSMConstants.java
index ca91467..3b0fa18 100644
--- a/src/main/java/org/apache/sling/slingstart/model/SSMConstants.java
+++ b/src/main/java/org/apache/sling/slingstart/model/SSMConstants.java
@@ -24,4 +24,17 @@ public abstract class SSMConstants {
 
     /** Name of the configuration for the bootstrap contents. */
     public static final String CFG_BOOTSTRAP = ":bootstrap";
+
+    /** 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/slingstart/model/SSMDeliverable.java b/src/main/java/org/apache/sling/slingstart/model/SSMDeliverable.java
index 1960d24..a931981 100644
--- a/src/main/java/org/apache/sling/slingstart/model/SSMDeliverable.java
+++ b/src/main/java/org/apache/sling/slingstart/model/SSMDeliverable.java
@@ -31,7 +31,7 @@ import java.util.Map;
  *
  * At least it has a "global" feature which contains artifacts that are always installed..
  */
-public class SSMDeliverable {
+public class SSMDeliverable extends SSMTraceable {
 
     private final List<SSMFeature> features = new ArrayList<SSMFeature>();
 
@@ -84,17 +84,6 @@ public class SSMDeliverable {
     }
 
     /**
-     * validates the object and throws an IllegalStateException
-     *
-     * @throws IllegalStateException
-     */
-    public void validate() {
-        for(final SSMFeature f : this.features) {
-            f.validate();
-        }
-    }
-
-    /**
      * Replace properties in the string.
      *
      * @throws IllegalArgumentException
@@ -145,7 +134,9 @@ public class SSMDeliverable {
 
     @Override
     public String toString() {
-        return "SSMDeliverable [features=" + features + ", variables="
-                + variables + "]";
+        return "SSMDeliverable [features=" + features
+                + ", variables=" + variables
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+                + "]";
     }
 }
diff --git a/src/main/java/org/apache/sling/slingstart/model/SSMFeature.java b/src/main/java/org/apache/sling/slingstart/model/SSMFeature.java
index 86f7a81..0a8e175 100644
--- a/src/main/java/org/apache/sling/slingstart/model/SSMFeature.java
+++ b/src/main/java/org/apache/sling/slingstart/model/SSMFeature.java
@@ -36,15 +36,9 @@ import java.util.Set;
  * In addition to custom, user defined run modes, special run modes exists.
  * A special run mode name starts with a colon.
  */
-public class SSMFeature implements Comparable<SSMFeature> {
-
-    public static final String RUN_MODE_BASE = ":base";
-
-    public static final String RUN_MODE_BOOT = ":boot";
-
-    public static final String RUN_MODE_WEBAPP = ":webapp";
-
-    public static final String RUN_MODE_STANDALONE = ":standalone";
+public class SSMFeature
+    extends SSMTraceable
+    implements Comparable<SSMFeature> {
 
     private final String[] runModes;
 
@@ -82,31 +76,6 @@ public class SSMFeature implements Comparable<SSMFeature> {
     }
 
     /**
-     * validates the object and throws an IllegalStateException
-     *
-     * @throws IllegalStateException
-     */
-    public void validate() {
-        if ( this.runModes != null ) {
-            boolean hasSpecial = false;
-            for(String m : this.runModes) {
-                if ( m.startsWith(":") ) {
-                    if ( hasSpecial ) {
-                        throw new IllegalStateException("Invalid modes " + Arrays.toString(this.runModes));
-                    }
-                    hasSpecial = true;
-                }
-            }
-        }
-        for(final SSMStartLevel sl : this.startLevels) {
-            sl.validate();
-        }
-        for(final SSMConfiguration c : this.configurations) {
-            c.validate();
-        }
-    }
-
-    /**
      * Check if this feature is active wrt the given set of active run modes.
      */
     public boolean isActive(final Set<String> activeRunModes) {
@@ -261,7 +230,10 @@ public class SSMFeature implements Comparable<SSMFeature> {
     @Override
     public String toString() {
         return "SSMFeature [runModes=" + Arrays.toString(runModes)
-                + ", startLevels=" + startLevels + ", configurations="
-                + configurations + ", settings=" + settings + "]";
+                + ", startLevels=" + startLevels
+                + ", configurations=" + configurations
+                + ", settings=" + settings
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+                + "]";
     }
 }
diff --git a/src/main/java/org/apache/sling/slingstart/model/SSMStartLevel.java b/src/main/java/org/apache/sling/slingstart/model/SSMStartLevel.java
index ce2e08b..572bab0 100644
--- a/src/main/java/org/apache/sling/slingstart/model/SSMStartLevel.java
+++ b/src/main/java/org/apache/sling/slingstart/model/SSMStartLevel.java
@@ -23,7 +23,8 @@ import java.util.List;
  * A start level holds a set of artifacts.
  * A valid start level is positive, start level 0 means the default OSGi start level.
  */
-public class SSMStartLevel implements Comparable<SSMStartLevel> {
+public class SSMStartLevel extends SSMTraceable
+    implements Comparable<SSMStartLevel> {
 
     private final int level;
 
@@ -42,20 +43,6 @@ public class SSMStartLevel implements Comparable<SSMStartLevel> {
     }
 
     /**
-     * validates the object and throws an IllegalStateException
-     *
-     * @throws IllegalStateException
-     */
-    public void validate() {
-        for(final SSMArtifact sl : this.artifacts) {
-            sl.validate();
-        }
-        if ( level < 0 ) {
-            throw new IllegalStateException("level");
-        }
-    }
-
-    /**
      * Search an artifact with the same groupId, artifactId, version, type and classifier.
      * Version is not considered.
      */
@@ -95,7 +82,9 @@ public class SSMStartLevel implements Comparable<SSMStartLevel> {
 
     @Override
     public String toString() {
-        return "SSMStartLevel [level=" + level + ", artifacts=" + artifacts
+        return "SSMStartLevel [level=" + level
+                + ", artifacts=" + artifacts
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
                 + "]";
     }
 }
diff --git a/src/main/java/org/apache/sling/slingstart/model/SSMConstants.java b/src/main/java/org/apache/sling/slingstart/model/SSMTraceable.java
similarity index 60%
copy from src/main/java/org/apache/sling/slingstart/model/SSMConstants.java
copy to src/main/java/org/apache/sling/slingstart/model/SSMTraceable.java
index ca91467..78be78c 100644
--- a/src/main/java/org/apache/sling/slingstart/model/SSMConstants.java
+++ b/src/main/java/org/apache/sling/slingstart/model/SSMTraceable.java
@@ -16,12 +16,32 @@
  */
 package org.apache.sling.slingstart.model;
 
+public abstract class SSMTraceable {
 
-public abstract class SSMConstants {
+    private String location;
 
-    /** Name of the configuration containing the web.xml. */
-    public static final String CFG_WEB_XML = ":web.xml";
+    private String comment;
 
-    /** Name of the configuration for the bootstrap contents. */
-    public static final String CFG_BOOTSTRAP = ":bootstrap";
+    public String getLocation() {
+        return this.location;
+    }
+
+    public void setLocation(final String value) {
+        this.location = value;
+    }
+
+    public String getComment() {
+        return this.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/slingstart/model/SSMValidator.java b/src/main/java/org/apache/sling/slingstart/model/SSMValidator.java
new file mode 100644
index 0000000..1ccb94f
--- /dev/null
+++ b/src/main/java/org/apache/sling/slingstart/model/SSMValidator.java
@@ -0,0 +1,94 @@
+/*
+ * 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.slingstart.model;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Validate a complete model.
+ */
+public class SSMValidator {
+
+    /**
+     * Validates the model.
+     * @param model
+     * @return
+     */
+    public Map<SSMTraceable, String> validate(final SSMDeliverable model) {
+        final Map<SSMTraceable, String> errors = new HashMap<SSMTraceable, String>();
+
+        for(final SSMFeature feature : model.getFeatures() ) {
+            final String[] rm = feature.getRunModes();
+            if ( rm != null ) {
+                boolean hasSpecial = false;
+                for(final String m : rm) {
+                    if ( m.startsWith(":") ) {
+                        if ( hasSpecial ) {
+                            errors.put(feature, "Invalid modes " + Arrays.toString(rm));
+                            break;
+                        }
+                        hasSpecial = true;
+                    }
+                }
+            }
+            for(final SSMStartLevel sl : feature.getStartLevels()) {
+                if ( sl.getLevel() < 0 ) {
+                    errors.put(sl, "Invalid start level " + sl.getLevel());
+                }
+                for(final SSMArtifact 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 SSMConfiguration c : feature.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/slingstart/model/txt/TXTSSMModelReader.java b/src/main/java/org/apache/sling/slingstart/model/txt/TXTSSMModelReader.java
index 9c2f9c0..c41262d 100644
--- a/src/main/java/org/apache/sling/slingstart/model/txt/TXTSSMModelReader.java
+++ b/src/main/java/org/apache/sling/slingstart/model/txt/TXTSSMModelReader.java
@@ -20,118 +20,211 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.LineNumberReader;
 import java.io.Reader;
-import java.io.StringReader;
 import java.util.Dictionary;
 import java.util.Enumeration;
-import java.util.UUID;
 
 import org.apache.felix.cm.file.ConfigurationHandler;
 import org.apache.sling.slingstart.model.SSMArtifact;
 import org.apache.sling.slingstart.model.SSMConfiguration;
 import org.apache.sling.slingstart.model.SSMDeliverable;
 import org.apache.sling.slingstart.model.SSMFeature;
+import org.apache.sling.slingstart.model.SSMStartLevel;
+import org.apache.sling.slingstart.model.SSMTraceable;
 
 
 public class TXTSSMModelReader {
 
     public static final String FELIX_FORMAT_SUFFIX = "FORMAT:felix.config";
 
+    private enum MODE {
+        NONE,
+        VARS,
+        FEATURE,
+        START_LEVEL,
+        CONFIGURATION,
+        SETTINGS,
+        ARTIFACT
+    }
+
     /**
      * Reads the deliverable file
      * The reader is not closed.
      * @throws IOException
      */
-    public static SSMDeliverable read(final Reader reader)
+    public static SSMDeliverable read(final Reader reader, final String location)
+    throws IOException {
+        final TXTSSMModelReader mr = new TXTSSMModelReader(location);
+        return mr.readModel(reader);
+    }
+
+    private MODE mode = MODE.NONE;
+
+    private final SSMDeliverable model = new SSMDeliverable();
+
+    private SSMFeature feature = null;
+    private SSMStartLevel startLevel = null;
+    private SSMConfiguration config = null;
+    private SSMArtifact artifact = null;
+
+    private String comment = null;
+
+    private StringBuilder configBuilder = null;
+    private boolean configFelixFormat = false;
+
+    private LineNumberReader lineNumberReader;
+
+    private TXTSSMModelReader(final String location) {
+        this.model.setLocation(location);
+    }
+
+    private SSMDeliverable readModel(final Reader reader)
     throws IOException {
-        final SSMDeliverable model = new SSMDeliverable();
-        final LineNumberReader lnr = new LineNumberReader(reader);
+
+        boolean global = true;
+
+        lineNumberReader = new LineNumberReader(reader);
         String line;
-        while ( (line = lnr.readLine()) != null ) {
-            if ( ignore(line) ) {
+        while ( (line = lineNumberReader.readLine()) != null ) {
+            // ignore empty line
+            if ( line.trim().isEmpty() ) {
+                continue;
+            }
+            // comment?
+            if ( line.startsWith("#") ) {
+                checkConfig();
+                mode = MODE.NONE;
+                final String c = line.substring(1).trim();
+                if ( comment == null ) {
+                    comment = c;
+                } else {
+                    comment = comment + "\n" + c;
+                }
                 continue;
             }
 
-            // Command must start with a verb, optionally followed
-            // by properties
-            if (!isVerb(line)) {
-                throw new IOException("Expecting verb, current line is " + line);
+            if ( global ) {
+                global = false;
+                model.setComment(comment);
+                comment = null;
             }
 
-            // Parse verb and qualifier from first line
-            final String [] firstLine= line.split(" ");
-            final String verb = firstLine[0];
-            final StringBuilder builder = new StringBuilder();
-            for(int i=1; i < firstLine.length; i++) {
-                if (builder.length() > 0) {
-                    builder.append(' ');
+            final String trimmedLine = line.trim();
+            final int pos = line.indexOf(':');
+            final String params = (pos != -1 ? line.substring(pos + 1).trim() : null);
+
+            if ( trimmedLine.startsWith("feature:") ) {
+                checkConfig();
+
+                mode = MODE.FEATURE;
+                feature = model.getOrCreateFeature(params.split(","));
+                this.init(feature);
+                startLevel = feature.getOrCreateStartLevel(0);
+
+            } else if ( trimmedLine.startsWith("variables:") ) {
+                checkConfig();
+
+                if ( comment != null ) {
+                    throw new IOException("comment not allowed for variables in line " + this.lineNumberReader.getLineNumber());
                 }
-                builder.append(firstLine[i]);
-            }
-            final String qualifier = builder.toString();
-
-            // Parse properties from optional indented lines
-            // that follow verb line
-            StringBuilder props = null;
-            do {
-                line = lnr.readLine();
-                if (line != null && !isVerb(line)) {
-                    if (props == null) {
-                        props = new StringBuilder();
-                    }
-                    addProperty(props, line);
+                mode = MODE.VARS;
+
+            } else if ( trimmedLine.startsWith("startLevel:") ) {
+                checkConfig();
+
+                if ( feature == null ) {
+                    throw new IOException("startlevel outside of feature in line " + this.lineNumberReader.getLineNumber());
                 }
-            } while ( line != null && !isVerb(line));
-
-            if ( "classpath".equals("verb") ) {
-                final SSMFeature boot = model.getOrCreateFeature(new String[] {SSMFeature.RUN_MODE_BOOT});
-                final SSMArtifact artifact = SSMArtifact.fromMvnUrl(qualifier);
-                boot.getOrCreateStartLevel(0).getArtifacts().add(artifact);
-            } else if ( "bundle".equals(verb) ) {
-                final SSMFeature feature = model.getOrCreateFeature(null);
-                final SSMArtifact artifact = SSMArtifact.fromMvnUrl(qualifier);
-                feature.getOrCreateStartLevel(0).getArtifacts().add(artifact);
-            } else if ( "config".equals(verb) ) {
-                final SSMFeature feature = model.getOrCreateFeature(null);
-                boolean felixFormat = false;
-                final String pid;
-                if (qualifier.endsWith(FELIX_FORMAT_SUFFIX)) {
-                    felixFormat = true;
-                    pid = qualifier.split(" ")[0].trim();
+                int level = (params.length() == 0 ? level = 0 : Integer.valueOf(params));
+                startLevel = feature.getOrCreateStartLevel(level);
+                this.init(startLevel);
+                mode = MODE.START_LEVEL;
+
+            } else if ( trimmedLine.startsWith("config:FELIX ") || trimmedLine.startsWith("config: ") ) {
+                checkConfig();
+
+                mode = MODE.CONFIGURATION;
+                final int factoryPos = params.indexOf('-');
+                if ( factoryPos == -1 ) {
+                    config = new SSMConfiguration(params, null);
                 } else {
-                    pid = qualifier;
+                    config = new SSMConfiguration(params.substring(pos + 1), params.substring(0, pos));
                 }
-                final SSMConfiguration config = feature.getOrCreateConfiguration(pid, null);
-                if ( props != null ) {
-                    processConfigurationProperties(config, props.toString(), felixFormat);
+                this.init(config);
+                configBuilder = new StringBuilder();
+                configFelixFormat = trimmedLine.startsWith("config:FELIX ");
+
+            } else if ( trimmedLine.startsWith("settings:") ) {
+                checkConfig();
+
+                if ( comment != null ) {
+                    throw new IOException("comment not allowed for settings in line " + this.lineNumberReader.getLineNumber());
                 }
-            } else if ( "config.factory".equals(verb) ) {
-                final SSMFeature feature = model.getOrCreateFeature(null);
-                boolean felixFormat = false;
-                final String factoryPid;
-                if (qualifier.endsWith(FELIX_FORMAT_SUFFIX)) {
-                    felixFormat = true;
-                    factoryPid = qualifier.split(" ")[0].trim();
-                } else {
-                    factoryPid = qualifier;
+                if ( startLevel == null ) {
+                    throw new IOException("settings outside of feature/startlevel in line " + this.lineNumberReader.getLineNumber());
                 }
-                // create unique alias
-                final SSMConfiguration config = feature.getOrCreateConfiguration(UUID.randomUUID().toString(), factoryPid);
-                if ( props != null ) {
-                    processConfigurationProperties(config, props.toString(), felixFormat);
+                mode = MODE.SETTINGS;
+
+            } else if ( trimmedLine.startsWith("artifact:") ) {
+                checkConfig();
+
+                if ( startLevel == null ) {
+                    throw new IOException("artifact outside of feature/startlevel in line " + this.lineNumberReader.getLineNumber());
+                }
+
+                mode = MODE.ARTIFACT;
+                try {
+                    artifact = SSMArtifact.fromMvnUrl("mvn:" + params);
+                } catch ( final IllegalArgumentException iae) {
+                    throw new IOException(iae.getMessage() + " in line " + this.lineNumberReader.getLineNumber(), iae);
+                }
+                this.init(artifact);
+                startLevel.getArtifacts().add(artifact);
+
+            } else {
+                switch ( mode ) {
+                    case NONE:  throw new IOException("No global contents allowed in line " + this.lineNumberReader.getLineNumber());
+                    case ARTIFACT : final String[] metadata = parseProperty(trimmedLine);
+                                    artifact.getMetadata().put(metadata[0], metadata[1]);
+                                    break;
+                    case VARS : final String[] vars = parseProperty(trimmedLine);
+                                model.getVariables().put(vars[0], vars[1]);
+                                break;
+                    case SETTINGS : final String[] settings = parseProperty(trimmedLine);
+                                    feature.getSettings().put(settings[0], settings[1]);
+                                    break;
+                    case FEATURE: throw new IOException("No contents allowed for feature in line " + this.lineNumberReader.getLineNumber());
+                    case START_LEVEL: throw new IOException("No contents allowed for feature in line " + this.lineNumberReader.getLineNumber());
+                    case CONFIGURATION: configBuilder.append(trimmedLine);
+                                        configBuilder.append('\n');
+                                        break;
                 }
             }
         }
+        checkConfig();
+        if ( comment != null ) {
+            throw new IOException("Comment not allowed at the end of file");
+        }
 
         return model;
     }
 
-    private static void processConfigurationProperties(final SSMConfiguration config, final String textValue,
-            final boolean felixFormat)
+    private void init(final SSMTraceable 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()
     throws IOException {
-        if ( felixFormat ) {
+        if ( config != null ) {
             ByteArrayInputStream bais = null;
             try {
-                bais = new ByteArrayInputStream(textValue.getBytes("UTF-8"));
+                bais = new ByteArrayInputStream(configBuilder.toString().getBytes("UTF-8"));
                 @SuppressWarnings("unchecked")
                 final Dictionary<String, Object> props = ConfigurationHandler.read(bais);
                 final Enumeration<String> e = props.keys();
@@ -148,39 +241,19 @@ public class TXTSSMModelReader {
                     }
                 }
             }
-        } else if ( config.isSpecial() ) {
-            config.getProperties().put(config.getPid(), textValue);
-        } else {
-            final LineNumberReader lnr = new LineNumberReader(new StringReader(textValue));
-            String line;
-            while ( (line = lnr.readLine()) != null ) {
-                final int pos = line.indexOf('=');
-                config.getProperties().put(line.substring(0, pos), line.substring(pos + 1));
-            }
         }
+        config = null;
+        configBuilder = null;
     }
 
-    private static boolean ignore(final String line) {
-        return line.trim().length() == 0 || line.startsWith("#");
-    }
-
-    private static boolean isVerb(final String line) {
-        return line.length() > 0 && !Character.isWhitespace(line.charAt(0));
-    }
-
-   private static void addProperty(final StringBuilder builder, final String line)
-   throws IOException {
+    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.trim().isEmpty() || value.trim().isEmpty() ) {
-            throw new IOException("Invalid property line [" + line + "]");
+        if (key.isEmpty() || value.isEmpty() ) {
+            throw new IOException("Invalid property; " + line + " in line " + this.lineNumberReader.getLineNumber());
         }
-
-        builder.append(key);
-        builder.append('=');
-        builder.append(value);
-        builder.append('\n');
+        return new String[] {key, value};
     }
 }
 
diff --git a/src/main/java/org/apache/sling/slingstart/model/txt/TXTSSMModelWriter.java b/src/main/java/org/apache/sling/slingstart/model/txt/TXTSSMModelWriter.java
new file mode 100644
index 0000000..0df8848
--- /dev/null
+++ b/src/main/java/org/apache/sling/slingstart/model/txt/TXTSSMModelWriter.java
@@ -0,0 +1,191 @@
+/*
+ * 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.slingstart.model.txt;
+
+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.slingstart.model.SSMArtifact;
+import org.apache.sling.slingstart.model.SSMConfiguration;
+import org.apache.sling.slingstart.model.SSMDeliverable;
+import org.apache.sling.slingstart.model.SSMFeature;
+import org.apache.sling.slingstart.model.SSMStartLevel;
+import org.apache.sling.slingstart.model.SSMTraceable;
+
+/**
+ * Simple writer for the a model
+ */
+public class TXTSSMModelWriter {
+
+    private static void writeComment(final PrintWriter pw, final SSMTraceable 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 SSMDeliverable model)
+    throws IOException {
+        final PrintWriter pw = new PrintWriter(writer);
+
+        writeComment(pw, model);
+        // variables
+        if ( model.getVariables().size() > 0 ) {
+            pw.println("variables:");
+            for(final Map.Entry<String, String> entry : model.getVariables().entrySet()) {
+                pw.print("  ");
+                pw.print(entry.getKey());
+                pw.print("=");
+                pw.println(entry.getValue());
+            }
+            pw.println();
+        }
+
+        // features
+        for(final SSMFeature feature : model.getFeatures()) {
+            // skip empty feature
+            if ( feature.getConfigurations().isEmpty() && feature.getSettings().isEmpty() ) {
+                boolean hasArtifacts = false;
+                for(final SSMStartLevel sl : feature.getStartLevels()) {
+                    if ( !sl.getArtifacts().isEmpty() ) {
+                        hasArtifacts = true;
+                        break;
+                    }
+                }
+                if ( !hasArtifacts ) {
+                    continue;
+                }
+            }
+            writeComment(pw, feature);
+            pw.print("feature:");
+            final String[] runModes = feature.getRunModes();
+            if ( runModes != null && runModes.length > 0 ) {
+                pw.print(" ");
+                boolean first = true;
+                for(final String mode : runModes) {
+                    if ( first ) {
+                        first = false;
+                    } else {
+                        pw.print(",");
+                    }
+                    pw.print(mode);
+                }
+            }
+            pw.println();
+
+            // settings
+            if ( feature.getSettings().size() > 0 ) {
+                pw.println("  settings:");
+
+                for(final Map.Entry<String, String> entry :feature.getSettings().entrySet()) {
+                    pw.print("    ");
+                    pw.print(entry.getKey());
+                    pw.print("=");
+                    pw.println(entry.getValue());
+                }
+                pw.println();
+            }
+
+            // start level
+            for(final SSMStartLevel startLevel : feature.getStartLevels()) {
+                // skip empty levels
+                if ( startLevel.getArtifacts().size() == 0 ) {
+                    continue;
+                }
+                writeComment(pw, startLevel);
+                pw.print("  startLevel: ");
+                pw.print(String.valueOf(startLevel.getLevel()));
+                pw.println();
+                pw.println();
+
+                // artifacts
+                for(final SSMArtifact ad : startLevel.getArtifacts()) {
+                    writeComment(pw, ad);
+                    pw.print("    ");
+                    pw.print("artifact: ");
+                    pw.print(ad.toMvnUrl().substring(4));
+                    pw.println();
+                    if ( ad.getMetadata().size() > 0 ) {
+                        for(final Map.Entry<String, String> entry : ad.getMetadata().entrySet()) {
+                            pw.print("      ");
+                            pw.print(entry.getKey());
+                            pw.print("=");
+                            pw.println(entry.getValue());
+                        }
+                    }
+                }
+                if ( startLevel.getArtifacts().size() > 0 ) {
+                    pw.println();
+                }
+            }
+
+            // configurations
+            for(final SSMConfiguration config : feature.getConfigurations()) {
+                writeComment(pw, config);
+                pw.print("  config: ");
+                if ( config.getFactoryPid() != null ) {
+                    pw.print(config.getFactoryPid());
+                    pw.print("-");
+                }
+                pw.print(config.getPid());
+                pw.println();
+                final String configString;
+                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 ) {
+                    pw.print("    ");
+                    pw.println(line);
+                }
+                pw.println();
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/slingstart/model/xml/XMLSSMModelReader.java b/src/main/java/org/apache/sling/slingstart/model/xml/XMLSSMModelReader.java
index 08b975a..bec89cc 100644
--- a/src/main/java/org/apache/sling/slingstart/model/xml/XMLSSMModelReader.java
+++ b/src/main/java/org/apache/sling/slingstart/model/xml/XMLSSMModelReader.java
@@ -23,6 +23,8 @@ import java.io.Reader;
 import java.io.StringReader;
 import java.util.Dictionary;
 import java.util.Enumeration;
+import java.util.Map;
+import java.util.Properties;
 import java.util.Stack;
 
 import javax.xml.parsers.ParserConfigurationException;
@@ -34,6 +36,8 @@ import org.apache.sling.slingstart.model.SSMArtifact;
 import org.apache.sling.slingstart.model.SSMConfiguration;
 import org.apache.sling.slingstart.model.SSMDeliverable;
 import org.apache.sling.slingstart.model.SSMFeature;
+import org.apache.sling.slingstart.model.SSMTraceable;
+import org.apache.sling.slingstart.model.SSMValidator;
 import org.xml.sax.Attributes;
 import org.xml.sax.ContentHandler;
 import org.xml.sax.InputSource;
@@ -114,6 +118,9 @@ public class XMLSSMModelReader {
                 /** Current configuration. */
                 private SSMConfiguration configuration;
 
+                /** Felix config format (default) */
+                private boolean isFelixConfigurationFormat = true;
+
                 @Override
                 public void startElement(final String uri,
                         final String localName,
@@ -159,15 +166,13 @@ public class XMLSSMModelReader {
                                 this.feature.getOrCreateStartLevel(this.startLevel).getArtifacts().add(artifact);
                             } else if ( this.mode == MODE.CONFIGURATION || this.mode == MODE.FEATURE_CONFIGURATION) {
                                 this.configuration = this.feature.getOrCreateConfiguration(atts.getValue("pid"), atts.getValue("factory"));
+                                this.isFelixConfigurationFormat = !"true".equals(atts.getValue("props"));
                                 this.text = new StringBuilder();
                             } else if ( this.mode == MODE.SETTINGS || this.mode == MODE.FEATURE_SETTINGS) {
-                                if ( this.feature.getSettings() != null ) {
-                                    throw new SAXException("Duplicate settings section");
-                                }
                                 this.text = new StringBuilder();
 
                             } else if ( this.mode == MODE.FEATURE ) {
-                                final String runMode = atts.getValue("modes");
+                                final String runMode = atts.getValue("runModes");
                                 if ( runMode == null || runMode.trim().length() == 0 ) {
                                     throw new SAXException("Required attribute runModes missing for runMode element");
                                 }
@@ -216,26 +221,38 @@ public class XMLSSMModelReader {
                             if ( this.configuration.isSpecial() ) {
                                 this.configuration.getProperties().put(this.configuration.getPid(), textValue);
                             } else {
-                                ByteArrayInputStream bais = null;
-                                try {
-                                    bais = new ByteArrayInputStream(textValue.getBytes("UTF-8"));
-                                    @SuppressWarnings("unchecked")
-                                    final Dictionary<String, Object> props = ConfigurationHandler.read(bais);
-                                    final Enumeration<String> e = props.keys();
-                                    while ( e.hasMoreElements() ) {
-                                        final String key = e.nextElement();
-                                        this.configuration.getProperties().put(key, props.get(key));
-                                    }
-                                } catch ( final IOException ioe ) {
-                                    throw new SAXException(ioe);
-                                } finally {
-                                    if ( bais != null ) {
-                                        try {
-                                            bais.close();
-                                        } catch ( final IOException ignore ) {
-                                            // ignore
+                                if ( this.isFelixConfigurationFormat ) {
+                                    ByteArrayInputStream bais = null;
+                                    try {
+                                        bais = new ByteArrayInputStream(textValue.getBytes("UTF-8"));
+                                        @SuppressWarnings("unchecked")
+                                        final Dictionary<String, Object> props = ConfigurationHandler.read(bais);
+                                        final Enumeration<String> e = props.keys();
+                                        while ( e.hasMoreElements() ) {
+                                            final String key = e.nextElement();
+                                            this.configuration.getProperties().put(key, props.get(key));
+                                        }
+                                    } catch ( final IOException ioe ) {
+                                        throw new SAXException(ioe);
+                                    } finally {
+                                        if ( bais != null ) {
+                                            try {
+                                                bais.close();
+                                            } catch ( final IOException ignore ) {
+                                                // ignore
+                                            }
                                         }
                                     }
+                                } else {
+                                    final Properties props = new Properties();
+                                    try {
+                                        props.load(new StringReader(textValue));
+                                    } catch ( final IOException ioe ) {
+                                        throw new SAXException(ioe);
+                                    }
+                                    for(final Map.Entry<Object, Object> entry : props.entrySet()) {
+                                        this.configuration.getProperties().put((String)entry.getKey(), (String)entry.getValue());
+                                    }
                                 }
                             }
                             this.configuration = null;
@@ -244,6 +261,9 @@ public class XMLSSMModelReader {
                             String line = null;
                             try {
                                 while ( (line = reader.readLine()) != null ) {
+                                    if ( line.startsWith("#") || line.trim().isEmpty()) {
+                                        continue;
+                                    }
                                     final int pos = line.indexOf("=");
                                     if ( pos == -1 || line.indexOf("=", pos + 1 ) != -1 ) {
                                         throw new SAXException("Invalid property definition: " + line);
@@ -328,10 +348,9 @@ public class XMLSSMModelReader {
             });
             xmlReader.parse(new InputSource(reader));
 
-            try {
-                result.validate();
-            } catch ( final IllegalStateException ise) {
-                throw (IOException)new IOException("Invalid subsystem definition: " + ise.getMessage()).initCause(ise);
+            final Map<SSMTraceable, String> errors = new SSMValidator().validate(result);
+            if ( errors != null ) {
+                throw new IOException("Invalid model definition: " + errors);
             }
             return result;
         } catch ( final SAXException se) {

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