SLING-5172 : Provide support for custom sections in the provisioning model

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1709592 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/sling/provisioning/model/Feature.java b/src/main/java/org/apache/sling/provisioning/model/Feature.java
index 1d892ec..c78cb3e 100644
--- a/src/main/java/org/apache/sling/provisioning/model/Feature.java
+++ b/src/main/java/org/apache/sling/provisioning/model/Feature.java
@@ -32,6 +32,10 @@
     extends Commentable
     implements Comparable<Feature> {
 
+    /**
+     * The feature type
+     * @since 1.4.0
+     */
     public enum Type {
         PLAIN("plain"),
         SUBSYSTEM_FEATURE("osgi.subsystem.feature"),
@@ -72,6 +76,9 @@
     /** Feature name. */
     private final String name;
 
+    /** Additional sections. */
+    private final List<Section> additionalSections = new ArrayList<Section>();
+
     /**
      * Construct a new feature.
      * @param name The feature name
@@ -144,12 +151,48 @@
         return result;
     }
 
+    /**
+     * Get the feature type.
+     * @return The feature type.
+     * @since 1.4.0
+     */
     public Type getType() {
         return type;
     }
 
-    public void setType(Type t) {
-        type = t;
+    /**
+     * Set the feature type.
+     * @param t The new type
+     * @since 1.4.0
+     */
+    public void setType(final Type t) {
+        type = ( t == null ? Type.PLAIN : t);
+    }
+
+    /**
+     * Get all additional sections
+     * @return The list of additional sections. It might be empty.
+     * @since 1.4.0
+     */
+    public List<Section> getAdditionalSections() {
+        return this.additionalSections;
+    }
+
+    /**
+     * Get all sections with the given name.
+     * @param name The section name.
+     * @return The list of sections. The list might be empty.
+     * @since 1.4.0
+     */
+    public List<Section> getAdditionalSections(final String name) {
+        final List<Section> result = new ArrayList<Section>();
+
+        for(final Section s : this.additionalSections) {
+            if ( name.equals(s.getName()) ) {
+                result.add(s);
+            }
+        }
+        return result;
     }
 
     @Override
@@ -171,6 +214,7 @@
         return "Feature [runModes=" + runModes + ", variables=" + variables
                 + ", name=" + name
                 + ( type != Type.PLAIN ? ", type=" + type : "" )
+                + ( additionalSections.isEmpty() ? "" : ", additionalSections=" + this.additionalSections)
                 + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
                 + "]";
     }
diff --git a/src/main/java/org/apache/sling/provisioning/model/ModelProcessor.java b/src/main/java/org/apache/sling/provisioning/model/ModelProcessor.java
index ad4fcfc..58cad6d 100644
--- a/src/main/java/org/apache/sling/provisioning/model/ModelProcessor.java
+++ b/src/main/java/org/apache/sling/provisioning/model/ModelProcessor.java
@@ -23,10 +23,10 @@
 /**
  * Allows to process a value. A new value is created and for each part in the model a process
  * method is called. Subclasses can overwrite those methods to inject specific behavior.
- * The processor itself does not change anything in the model. 
+ * The processor itself does not change anything in the model.
  */
 class ModelProcessor {
-    
+
     /**
      * Creates a copy of the model and calls a process method for each part found in the model.
      * This allows to modify the parts content (e.g. replace variables), but not to add or remove parts.
@@ -42,6 +42,7 @@
             newFeature.setType(feature.getType());
             newFeature.setComment(feature.getComment());
             newFeature.setLocation(feature.getLocation());
+            newFeature.getAdditionalSections().addAll(feature.getAdditionalSections());
 
             newFeature.getVariables().setComment(feature.getVariables().getComment());
             newFeature.getVariables().setLocation(feature.getVariables().getLocation());
@@ -86,15 +87,15 @@
         }
         return result;
     }
-    
+
     protected KeyValueMap<String> processVariables(KeyValueMap<String> variables, Feature newFeature) {
         return variables;
     }
-    
+
     protected Artifact processArtifact(Artifact artifact, Feature newFeature, RunMode newRunMode) {
         return artifact;
     }
-    
+
     protected Configuration processConfiguration(Configuration config, Feature newFeature, RunMode newRunMode) {
         return config;
     }
@@ -102,5 +103,5 @@
     protected KeyValueMap<String> processSettings(KeyValueMap<String> settings, Feature newFeature, RunMode newRunMode) {
         return settings;
     }
-    
+
 }
diff --git a/src/main/java/org/apache/sling/provisioning/model/ModelUtility.java b/src/main/java/org/apache/sling/provisioning/model/ModelUtility.java
index 806975d..3a30ab9 100644
--- a/src/main/java/org/apache/sling/provisioning/model/ModelUtility.java
+++ b/src/main/java/org/apache/sling/provisioning/model/ModelUtility.java
@@ -53,6 +53,9 @@
             final Feature baseFeature = base.getOrCreateFeature(feature.getName());
             baseFeature.setType(feature.getType());
 
+            // additional sections
+            baseFeature.getAdditionalSections().addAll(feature.getAdditionalSections());
+
             // variables
             baseFeature.getVariables().putAll(feature.getVariables());
 
@@ -277,33 +280,33 @@
          */
         String resolve(final Artifact artifact);
     }
-    
+
     /**
      * Parameter builder class for {@link ModelUtility#getEffectiveModel(Model, ResolverOptions)} method.
      */
     public static final class ResolverOptions {
-        
+
         private VariableResolver variableResolver;
         private ArtifactVersionResolver artifactVersionResolver;
-        
+
         public VariableResolver getVariableResolver() {
             return variableResolver;
         }
-        
+
         public ResolverOptions variableResolver(VariableResolver variableResolver) {
             this.variableResolver = variableResolver;
             return this;
         }
-        
+
         public ArtifactVersionResolver getArtifactVersionResolver() {
             return artifactVersionResolver;
         }
-        
+
         public ResolverOptions artifactVersionResolver(ArtifactVersionResolver dependencyVersionResolver) {
             this.artifactVersionResolver = dependencyVersionResolver;
             return this;
         }
-        
+
     }
 
     /**
@@ -318,7 +321,7 @@
     public static Model getEffectiveModel(final Model model, final VariableResolver resolver) {
         return getEffectiveModel(model, new ResolverOptions().variableResolver(resolver));
     }
-    
+
     /**
      * Replace all variables in the model and return a new model with the replaced values.
      * @param model The base model.
@@ -329,7 +332,7 @@
     public static Model getEffectiveModel(final Model model) {
         return getEffectiveModel(model, new ResolverOptions());
     }
-    
+
     /**
      * Replace all variables in the model and return a new model with the replaced values.
      * @param model The base model.
@@ -417,7 +420,7 @@
         }
         return errors;
     }
-    
+
     /**
      * Applies a set of variables to the given model.
      * All variables that are referenced anywhere within the model are detected and passed to the given variable resolver.
@@ -430,7 +433,7 @@
      * @since 1.3
      */
     public static Model applyVariables(final Model model, final VariableResolver resolver) {
-        
+
         // define delegating resolver that collects all variable names and value per feature
         final Map<String,Map<String,String>> collectedVars = new HashMap<String, Map<String,String>>();
         VariableResolver variableCollector = new VariableResolver() {
@@ -448,10 +451,10 @@
                 return value;
             }
         };
-        
+
         // use effective model processor to collect variables, but drop the resulting model
         new EffectiveModelProcessor(new ResolverOptions().variableResolver(variableCollector)).process(model);
-        
+
         // define a processor that updates the "variables" sections in the features
         ModelProcessor variablesUpdater = new ModelProcessor() {
             @Override
@@ -466,7 +469,7 @@
                 return newVariables;
             }
         };
-        
+
         // return model with replaced "variables" sections
         return variablesUpdater.process(model);
     }
@@ -482,7 +485,7 @@
      * @since 1.3
      */
     public static Model applyArtifactVersions(final Model model, final ArtifactVersionResolver resolver) {
-        
+
         // define a processor that updates the versions of artifacts
         ModelProcessor versionUpdater = new ModelProcessor() {
             @Override
@@ -501,7 +504,7 @@
                         artifact.getType());
             }
         };
-        
+
         // return model with updated version artifacts
         return versionUpdater.process(model);
     }
diff --git a/src/main/java/org/apache/sling/provisioning/model/Section.java b/src/main/java/org/apache/sling/provisioning/model/Section.java
new file mode 100644
index 0000000..5c4ce38
--- /dev/null
+++ b/src/main/java/org/apache/sling/provisioning/model/Section.java
@@ -0,0 +1,87 @@
+/*
+ * 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;
+
+
+/**
+ * An additional section in the provisioning model.
+ * @since 1.4
+ */
+public class Section
+    extends Commentable {
+
+    /** The section name. */
+    private final String name;
+
+    /** Attributes. */
+    private final Map<String, String> attributes = new HashMap<String, String>();
+
+    /** Contents. */
+    private volatile String contents;
+
+    /**
+     * Construct a new feature.
+     * @param name The feature name
+     */
+    public Section(final String name) {
+        this.name = name;
+    }
+
+    /**
+     * Get the name of the section.
+     * @return The name or {@code null} for an anonymous feature.
+     */
+    public String getName() {
+        return this.name;
+    }
+
+    /**
+     * Get all attributes
+     * @return The map of attributes.
+     */
+    public Map<String, String> getAttributes() {
+        return this.attributes;
+    }
+
+    /**
+     * Get the contents of the section.
+     * @return The contents or {@code null}.
+     */
+    public String getContents() {
+        return contents;
+    }
+
+    /**
+     * Set the contents of the section.
+     * @param contents The new contents.
+     */
+    public void setContents(final String contents) {
+        this.contents = contents;
+    }
+
+    @Override
+    public String toString() {
+        return "Section [name=" + name
+                + ( attributes.isEmpty() ? "": ", attributes=" + attributes )
+                + ( contents == null ? "" : ", contents=" + contents)
+                + ( this.getLocation() != null ? ", location=" + this.getLocation() : "")
+                + "]";
+    }
+}
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
index ec37e48..21ef564 100644
--- a/src/main/java/org/apache/sling/provisioning/model/io/ModelReader.java
+++ b/src/main/java/org/apache/sling/provisioning/model/io/ModelReader.java
@@ -31,6 +31,7 @@
 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.Section;
 
 /**
  * This class offers a method to read a model using a {@code Reader} instance.
@@ -44,7 +45,8 @@
         ARTIFACTS("artifacts", new String[] {"runModes", "startLevel"}),
         SETTINGS("settings", new String[] {"runModes"}),
         CONFIGURATIONS("configurations", new String[] {"runModes"}),
-        CONFIG(null, null);
+        CONFIG(null, null),
+        ADDITIONAL(null, null);
 
         public final String name;
 
@@ -79,6 +81,8 @@
     private ArtifactGroup artifactGroup;
     private Configuration config;
 
+    private Section additionalSection;
+
     private String comment;
 
     private StringBuilder configBuilder;
@@ -109,6 +113,14 @@
 
             // ignore empty line
             if ( line.isEmpty() ) {
+                if ( this.mode == CATEGORY.ADDITIONAL ) {
+                    if ( this.additionalSection.getContents() == null ) {
+                        this.additionalSection.setContents(line);
+                    } else {
+                        this.additionalSection.setContents(this.additionalSection.getContents() + '\n' + line);
+                    }
+                    continue;
+                }
                 checkConfig();
                 continue;
             }
@@ -121,6 +133,14 @@
 
                     continue;
                 }
+                if ( this.mode == CATEGORY.ADDITIONAL ) {
+                    if ( this.additionalSection.getContents() == null ) {
+                        this.additionalSection.setContents(line);
+                    } else {
+                        this.additionalSection.setContents(this.additionalSection.getContents() + '\n' + line);
+                    }
+                    continue;
+                }
                 final String c = line.substring(1).trim();
                 if ( comment == null ) {
                     comment = c;
@@ -138,6 +158,7 @@
             }
 
             if ( line.startsWith("[") ) {
+                additionalSection = null;
                 if ( !line.endsWith("]") ) {
                     throw new IOException(exceptionPrefix + "Illegal category definition in line " + this.lineNumberReader.getLineNumber() + ": " + line);
                 }
@@ -154,7 +175,8 @@
                     }
                 }
                 if ( found == null ) {
-                    throw new IOException(exceptionPrefix + "Unknown category in line " + this.lineNumberReader.getLineNumber() + ": " + line);
+                    // additional section
+                    found = CATEGORY.ADDITIONAL;
                 }
                 this.mode = found;
                 Map<String, String> parameters = Collections.emptyMap();
@@ -207,6 +229,13 @@
                                          checkRunMode(parameters);
                                          this.init(this.runMode.getConfigurations());
                                          break;
+                    case ADDITIONAL: checkFeature();
+                                     this.runMode = null;
+                                     this.artifactGroup = null;
+                                     this.additionalSection = new Section(category);
+                                     this.init(this.additionalSection);
+                                     this.feature.getAdditionalSections().add(this.additionalSection);
+                                     this.additionalSection.getAttributes().putAll(parameters);
                 }
             } else {
                 switch ( this.mode ) {
@@ -288,6 +317,12 @@
                     case CONFIG : configBuilder.append(line);
                                   configBuilder.append('\n');
                                   break;
+                    case ADDITIONAL : if ( this.additionalSection.getContents() == null ) {
+                                          this.additionalSection.setContents(line);
+                                      } else {
+                                          this.additionalSection.setContents(this.additionalSection.getContents() + '\n' + line);
+                                      }
+                                      break;
                 }
             }
 
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
index d1fe473..e6160b3 100644
--- a/src/main/java/org/apache/sling/provisioning/model/io/ModelWriter.java
+++ b/src/main/java/org/apache/sling/provisioning/model/io/ModelWriter.java
@@ -33,6 +33,7 @@
 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.Section;
 
 /**
  * Simple writer for the a model
@@ -236,6 +237,23 @@
                     }
                 }
             }
+
+            // additional sections
+            for(final Section section : feature.getAdditionalSections()) {
+                pw.print("  [");
+                pw.print(section.getName());
+                for(final Map.Entry<String, String> entry : section.getAttributes().entrySet()) {
+                    pw.print(' ');
+                    pw.print(entry.getKey());
+                    pw.print('=');
+                    pw.print(entry.getValue());
+                }
+                pw.println("]");
+                if ( section.getContents() != null ) {
+                    pw.println(section.getContents());
+                }
+                pw.println();
+            }
         }
     }
 }
diff --git a/src/test/java/org/apache/sling/provisioning/model/io/IOTest.java b/src/test/java/org/apache/sling/provisioning/model/io/IOTest.java
index 5fa6a1e..bcfe275 100644
--- a/src/test/java/org/apache/sling/provisioning/model/io/IOTest.java
+++ b/src/test/java/org/apache/sling/provisioning/model/io/IOTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
 import java.io.StringReader;
@@ -27,6 +28,7 @@
 import java.util.Map;
 
 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.ModelUtility;
 import org.apache.sling.provisioning.model.Traceable;
@@ -113,4 +115,12 @@
         assertEquals("C", cfgC.getProperties().get("name"));
         assertArrayEquals(new Integer[] {1,2,3}, (Integer[])cfgC.getProperties().get("array"));
     }
+
+    @Test public void testAddition() throws Exception {
+        final Model model = U.readCompleteTestModel(new String[] {"additional.txt"});
+        final Feature f = model.getFeature("main");
+        assertNotNull(f);
+        assertEquals(1, f.getAdditionalSections().size());
+        assertEquals(1, f.getAdditionalSections("additional").size());
+    }
 }
diff --git a/src/test/resources/additional.txt b/src/test/resources/additional.txt
new file mode 100644
index 0000000..b9b1524
--- /dev/null
+++ b/src/test/resources/additional.txt
@@ -0,0 +1,35 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+[feature name=main]
+
+[variables]
+  ws.version=1.0.2-from-main
+
+[artifacts]
+    commons-io/commons-io/1.4/jar
+
+[additional stuff=free]
+  # Hello
+  world
+  
+  Hello
+
+[configurations]
+  org.apache.test.A
+    name="A"