SLING-9187: Allow to provide provisioning model name and runmodes via feature model folders
diff --git a/src/main/java/org/apache/sling/maven/slingstart/FeatureModelConverter.java b/src/main/java/org/apache/sling/maven/slingstart/FeatureModelConverter.java
index 0825f5f..bb0c564 100644
--- a/src/main/java/org/apache/sling/maven/slingstart/FeatureModelConverter.java
+++ b/src/main/java/org/apache/sling/maven/slingstart/FeatureModelConverter.java
@@ -27,7 +27,7 @@
 import java.io.StringWriter;
 import java.io.Writer;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -81,6 +81,7 @@
     public static class FeatureFileEntry {
         public File file;
         public String runModes;
+        public String model;
     }
 
     public static void convertDirectories(String featuresDirectory, MavenProject project, String defaultProvName,
@@ -92,16 +93,14 @@
         }
     }
 
-    private static List<FeatureFileEntry> getFeatureFiles(final File baseDir, final String config) {
+    static List<FeatureFileEntry> getFeatureFiles(final File baseDir, final String config) {
         final List<FeatureFileEntry> files = new ArrayList<>();
-        for (final String cfg : config.split(",")) {
-            final String[] directoryCfg = cfg.split("\\|");
-            final String directory = directoryCfg[0].trim().replace('/', File.separatorChar);
+        for (final ParsedHeaderClause cfg : parseStandardHeader(config)) {
+            final String directory = cfg.m_paths.get(0).trim().replace('/', File.separatorChar);
 
-            String runmodes = null;
-            if (directoryCfg.length > 1) {
-                runmodes = String.join(",", Arrays.copyOfRange(directoryCfg, 1, directoryCfg.length));
-            }
+            String runmodes = (String) cfg.m_attrs.get(PROVISIONING_RUNMODES);
+            String model = (String) cfg.m_attrs.get(PROVISIONING_MODEL_NAME_VARIABLE);
+
             final File featuresDir = new File(baseDir, directory);
             final File[] children = featuresDir.listFiles();
             if (children != null) {
@@ -110,6 +109,7 @@
                         final FeatureFileEntry ff = new FeatureFileEntry();
                         ff.file = f;
                         ff.runModes = runmodes;
+                        ff.model = model;
                         files.add(ff);
                     }
                 }
@@ -154,7 +154,7 @@
                     String json = readFeatureFile(project, f, suggestedClassifier);
 
                     // check for prov model name
-                    if (defaultProvName != null || featureFile.runModes != null) {
+                    if (defaultProvName != null || featureFile.runModes != null || featureFile.model != null) {
                         try (final Reader reader = new StringReader(json)) {
                             final Feature feature = FeatureJSONReader.read(reader, f.getAbsolutePath());
                             boolean update = false;
@@ -170,11 +170,20 @@
                                 update = true;
                             }
 
-                            if (defaultProvName != null
-                                    && feature.getVariables().get(PROVISIONING_MODEL_NAME_VARIABLE) == null) {
-                                feature.getVariables().put(PROVISIONING_MODEL_NAME_VARIABLE, defaultProvName);
-                                update = true;
+                            if (feature.getVariables().get(PROVISIONING_MODEL_NAME_VARIABLE) == null) {
+                                boolean updateInner = true;
+                                if (featureFile.model != null) {
+                                    feature.getVariables().put(PROVISIONING_MODEL_NAME_VARIABLE, featureFile.model);
+                                }
+                                else if (defaultProvName != null) {
+                                    feature.getVariables().put(PROVISIONING_MODEL_NAME_VARIABLE, defaultProvName);
+                                }
+                                else {
+                                    updateInner = update;
+                                }
+                                update = updateInner;
                             }
+
                             if (update) {
                                 try (final Writer writer = new StringWriter()) {
                                     FeatureJSONWriter.write(writer, feature);
@@ -320,4 +329,187 @@
         final MavenVersion mavenVersion = new MavenVersion(sb.toString());
         return mavenVersion.getOSGiVersion().toString();
     }
+
+    private static class ParsedHeaderClause
+    {
+        public final List<String> m_paths;
+        public final Map<String, String> m_dirs;
+        public final Map<String, Object> m_attrs;
+        public final Map<String, String> m_types;
+
+        public ParsedHeaderClause(
+            List<String> paths, Map<String, String> dirs, Map<String, Object> attrs,
+            Map<String, String> types)
+        {
+            m_paths = paths;
+            m_dirs = dirs;
+            m_attrs = attrs;
+            m_types = types;
+        }
+    }
+
+    private static final char EOF = (char) -1;
+
+    private static char charAt(int pos, String headers, int length)
+    {
+        if (pos >= length)
+        {
+            return EOF;
+        }
+        return headers.charAt(pos);
+    }
+
+    private static final int CLAUSE_START = 0;
+    private static final int PARAMETER_START = 1;
+    private static final int KEY = 2;
+    private static final int DIRECTIVE_OR_TYPEDATTRIBUTE = 4;
+    private static final int ARGUMENT = 8;
+    private static final int VALUE = 16;
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private static List<ParsedHeaderClause> parseStandardHeader(String header)
+    {
+        List<ParsedHeaderClause> clauses = new ArrayList<ParsedHeaderClause>();
+        if (header == null)
+        {
+            return clauses;
+        }
+        ParsedHeaderClause clause = null;
+        String key = null;
+        Map targetMap = null;
+        int state = CLAUSE_START;
+        int currentPosition = 0;
+        int startPosition = 0;
+        int length = header.length();
+        boolean quoted = false;
+        boolean escaped = false;
+
+        char currentChar = EOF;
+        do
+        {
+            currentChar = charAt(currentPosition, header, length);
+            switch (state)
+            {
+                case CLAUSE_START:
+                    clause = new ParsedHeaderClause(
+                        new ArrayList<String>(),
+                        new HashMap<String, String>(),
+                        new HashMap<String, Object>(),
+                        new HashMap<String, String>());
+                    clauses.add(clause);
+                    state = PARAMETER_START;
+                case PARAMETER_START:
+                    startPosition = currentPosition;
+                    state = KEY;
+                case KEY:
+                    switch (currentChar)
+                    {
+                        case ':':
+                        case '=':
+                            key = header.substring(startPosition, currentPosition).trim();
+                            startPosition = currentPosition + 1;
+                            targetMap = clause.m_attrs;
+                            state = currentChar == ':' ? DIRECTIVE_OR_TYPEDATTRIBUTE : ARGUMENT;
+                            break;
+                        case EOF:
+                        case ',':
+                        case ';':
+                            clause.m_paths.add(header.substring(startPosition, currentPosition).trim());
+                            state = currentChar == ',' ? CLAUSE_START : PARAMETER_START;
+                            break;
+                        default:
+                            break;
+                    }
+                    currentPosition++;
+                    break;
+                case DIRECTIVE_OR_TYPEDATTRIBUTE:
+                    switch(currentChar)
+                    {
+                        case '=':
+                            if (startPosition != currentPosition)
+                            {
+                                clause.m_types.put(key, header.substring(startPosition, currentPosition).trim());
+                            }
+                            else
+                            {
+                                targetMap = clause.m_dirs;
+                            }
+                            state = ARGUMENT;
+                            startPosition = currentPosition + 1;
+                            break;
+                        default:
+                            break;
+                    }
+                    currentPosition++;
+                    break;
+                case ARGUMENT:
+                    if (currentChar == '\"')
+                    {
+                        quoted = true;
+                        currentPosition++;
+                    }
+                    else
+                    {
+                        quoted = false;
+                    }
+                    if (!Character.isWhitespace(currentChar)) {
+                        state = VALUE;
+                    }
+                    else {
+                        currentPosition++;
+                    }
+                    break;
+                case VALUE:
+                    if (escaped)
+                    {
+                        escaped = false;
+                    }
+                    else
+                    {
+                        if (currentChar == '\\' )
+                        {
+                            escaped = true;
+                        }
+                        else if (quoted && currentChar == '\"')
+                        {
+                            quoted = false;
+                        }
+                        else if (!quoted)
+                        {
+                            String value = null;
+                            switch(currentChar)
+                            {
+                                case EOF:
+                                case ';':
+                                case ',':
+                                    value = header.substring(startPosition, currentPosition).trim();
+                                    if (value.startsWith("\"") && value.endsWith("\""))
+                                    {
+                                        value = value.substring(1, value.length() - 1);
+                                    }
+                                    if (targetMap.put(key, value) != null)
+                                    {
+                                        throw new IllegalArgumentException(
+                                            "Duplicate '" + key + "' in: " + header);
+                                    }
+                                    state = currentChar == ';' ? PARAMETER_START : CLAUSE_START;
+                                    break;
+                                default:
+                                    break;
+                            }
+                        }
+                    }
+                    currentPosition++;
+                    break;
+                default:
+                    break;
+            }
+        } while ( currentChar != EOF);
+
+        if (state > PARAMETER_START)
+        {
+            throw new IllegalArgumentException("Unable to parse header: " + header);
+        }
+        return clauses;
+    }
 }
diff --git a/src/test/java/org/apache/sling/maven/slingstart/FeatureModelConverterTest.java b/src/test/java/org/apache/sling/maven/slingstart/FeatureModelConverterTest.java
index 563ca62..ce78f1e 100644
--- a/src/test/java/org/apache/sling/maven/slingstart/FeatureModelConverterTest.java
+++ b/src/test/java/org/apache/sling/maven/slingstart/FeatureModelConverterTest.java
@@ -16,7 +16,9 @@
  */
 package org.apache.sling.maven.slingstart;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
@@ -24,6 +26,7 @@
 import java.io.IOException;
 import java.net.URL;
 import java.nio.file.Files;
+import java.util.List;
 import java.util.Properties;
 
 import org.apache.maven.artifact.repository.ArtifactRepository;
@@ -144,4 +147,37 @@
         assertTrue(inheritsProv.contains("org.apache.aries/org.apache.aries.util/1.1.3"));
         assertTrue(inheritsProv.contains("org.apache.sling/org.apache.sling.commons.log/5.1.0"));
     }
+
+    @Test
+    public void testFeatureDirectoryDirectives() throws Exception {
+        List<FeatureModelConverter.FeatureFileEntry> featureFiles =
+            FeatureModelConverter.getFeatureFiles(new File(getClass().getResource("/features1").toURI()).getParentFile(),
+            "features1"
+                + "," + "features1"
+                + ";" + FeatureModelConverter.PROVISIONING_MODEL_NAME_VARIABLE + "=" + "quickstart"
+                + "," + "features1"
+                + ";" + FeatureModelConverter.PROVISIONING_MODEL_NAME_VARIABLE + "=" + "quickstart"
+                + ";" + FeatureModelConverter.PROVISIONING_RUNMODES + "=" + "author"
+                + "," + "features1"
+                + ";" + FeatureModelConverter.PROVISIONING_MODEL_NAME_VARIABLE + "=" + "samplecontent"
+                + "," + "features1"
+                + ";" + FeatureModelConverter.PROVISIONING_MODEL_NAME_VARIABLE + "=" + ":boot"
+                + ";" + FeatureModelConverter.PROVISIONING_RUNMODES + ":List<Integer>=" + "\"1,2,3\""
+        );
+
+        assertNull(featureFiles.get(0).model);
+        assertNull(featureFiles.get(0).runModes);
+
+        assertEquals("quickstart", featureFiles.get(1).model, "quickstart");
+        assertNull(featureFiles.get(1).runModes);
+
+        assertEquals("quickstart", featureFiles.get(2).model);
+        assertEquals("author", featureFiles.get(2).runModes);
+
+        assertEquals("samplecontent", featureFiles.get(3).model);
+        assertNull(featureFiles.get(3).runModes);
+
+        assertEquals(":boot", featureFiles.get(4).model);
+        assertEquals("1,2,3", featureFiles.get(4).runModes);
+    }
 }