SLING-9098 : Support creation of feature archives
diff --git a/src/main/java/org/apache/sling/feature/io/archive/ArchiveReader.java b/src/main/java/org/apache/sling/feature/io/archive/ArchiveReader.java
index df00ced..2bb3bed 100644
--- a/src/main/java/org/apache/sling/feature/io/archive/ArchiveReader.java
+++ b/src/main/java/org/apache/sling/feature/io/archive/ArchiveReader.java
@@ -38,6 +38,7 @@
  */
 public class ArchiveReader {
 
+    @FunctionalInterface
     public interface ArtifactConsumer {
 
         /**
@@ -55,16 +56,16 @@
      * Read a feature model archive. The input stream is not closed. It is up to the
      * caller to close the input stream.
      *
-     * @param in The input stream to read from.
-     * @return The feature model
+     * @param in       The input stream to read from.
+     * @param consumer The plugin consuming the binaries, if {@code null} only the
+     *                 feature models are read
+     * @return The feature models
      * @throws IOException If anything goes wrong
      */
     @SuppressWarnings("resource")
-    public static Feature read(final InputStream in,
+    public static Set<Feature> read(final InputStream in,
                              final ArtifactConsumer consumer)
     throws IOException {
-        Feature feature = null;
-
         final JarInputStream jis = new JarInputStream(in);
 
         // check manifest
@@ -90,40 +91,44 @@
         final Set<ArtifactId> artifacts = new HashSet<>();
 
         // read contents
+        final Set<Feature> features = new HashSet<>();
+
         JarEntry entry = null;
         while ( ( entry = jis.getNextJarEntry() ) != null ) {
-            if ( ArchiveWriter.MODEL_NAME.equals(entry.getName()) ) {
-                feature = FeatureJSONReader.read(new InputStreamReader(jis, "UTF-8"), null);
+            if (!entry.isDirectory() && entry.getName().startsWith(ArchiveWriter.FEATURE_MODEL_PREFIX)) { // feature
+                features.add(FeatureJSONReader.read(new InputStreamReader(jis, "UTF-8"), entry.getName()));
             } else if ( !entry.isDirectory() && entry.getName().startsWith(ArchiveWriter.ARTIFACTS_PREFIX) ) { // artifact
                 final ArtifactId id = ArtifactId
-                        .fromMvnUrl("mvn:" + entry.getName().substring(ArchiveWriter.ARTIFACTS_PREFIX.length()));
-                consumer.consume(id, jis);
+                        .fromMvnUrl("mvn:".concat(entry.getName().substring(ArchiveWriter.ARTIFACTS_PREFIX.length())));
+                if (consumer != null) {
+                    consumer.consume(id, jis);
+                }
                 artifacts.add(id);
             }
             jis.closeEntry();
         }
-        if (feature == null) {
+        if (features.isEmpty()) {
             throw new IOException("Not a feature model archive - feature file is missing.");
         }
 
-        // check whether all artifacts from the model are in the archive
-
-        for (final Artifact a : feature.getBundles()) {
-            if (!artifacts.contains(a.getId())) {
-                throw new IOException("Artifact " + a.getId().toMvnId() + " is missing in archive");
+        // check whether all artifacts from the models are in the archive
+        for (final Feature feature : features) {
+            for (final Artifact a : feature.getBundles()) {
+                if (!artifacts.contains(a.getId())) {
+                    throw new IOException("Artifact " + a.getId().toMvnId() + " is missing in archive");
+                }
             }
-        }
 
-        for (final Extension e : feature.getExtensions()) {
-            if (e.getType() == ExtensionType.ARTIFACTS) {
-                for (final Artifact a : e.getArtifacts()) {
-                    if (!artifacts.contains(a.getId())) {
-                        throw new IOException("Artifact " + a.getId().toMvnId() + " is missing in archive");
+            for (final Extension e : feature.getExtensions()) {
+                if (e.getType() == ExtensionType.ARTIFACTS) {
+                    for (final Artifact a : e.getArtifacts()) {
+                        if (!artifacts.contains(a.getId())) {
+                            throw new IOException("Artifact " + a.getId().toMvnId() + " is missing in archive");
+                        }
                     }
                 }
             }
         }
-
-        return feature;
+        return features;
     }
 }
diff --git a/src/main/java/org/apache/sling/feature/io/archive/ArchiveWriter.java b/src/main/java/org/apache/sling/feature/io/archive/ArchiveWriter.java
index 5e5cafd..6f48967 100644
--- a/src/main/java/org/apache/sling/feature/io/archive/ArchiveWriter.java
+++ b/src/main/java/org/apache/sling/feature/io/archive/ArchiveWriter.java
@@ -49,10 +49,10 @@
     /** Current support version of the feature model archive. */
     public static final int ARCHIVE_VERSION = 1;
 
-    /** Model name. */
-    public static final String MODEL_NAME = "features/feature.json";
+    /** The directory in the archive holding the features */
+    public static final String FEATURE_MODEL_PREFIX = "features/";
 
-    /** Artifacts prefix. */
+    /** The directory in the archive holding the artifacts */
     public static final String ARTIFACTS_PREFIX = "artifacts/";
 
     /**
@@ -69,16 +69,15 @@
      * complete} features.
      *
      * @param out          The output stream to write to
-     * @param feature      The feature model to archive
      * @param baseManifest Optional base manifest used for creating the manifest.
      * @param provider     The artifact provider
+     * @param features     The features model to archive
      * @return The jar output stream.
      * @throws IOException If anything goes wrong
      */
     public static JarOutputStream write(final OutputStream out,
-            final Feature feature,
             final Manifest baseManifest,
-            final ArtifactProvider provider)
+            final ArtifactProvider provider, final Feature... features)
     throws IOException {
         // create manifest
         final Manifest manifest = (baseManifest == null ? new Manifest() : new Manifest(baseManifest));
@@ -88,14 +87,16 @@
         // create archive
         final JarOutputStream jos = new JarOutputStream(out, manifest);
 
-        // write model first with compression enabled
+        // write models first with compression enabled
         jos.setLevel(Deflater.BEST_COMPRESSION);
-        final JarEntry entry = new JarEntry(MODEL_NAME);
-        jos.putNextEntry(entry);
-        final Writer writer = new OutputStreamWriter(jos, "UTF-8");
-        FeatureJSONWriter.write(writer, feature);
-        writer.flush();
-        jos.closeEntry();
+        for (final Feature feature : features) {
+            final JarEntry entry = new JarEntry(FEATURE_MODEL_PREFIX.concat(feature.getId().toMvnPath()));
+            jos.putNextEntry(entry);
+            final Writer writer = new OutputStreamWriter(jos, "UTF-8");
+            FeatureJSONWriter.write(writer, feature);
+            writer.flush();
+            jos.closeEntry();
+        }
 
         // write artifacts with compression disabled
         jos.setLevel(Deflater.NO_COMPRESSION);
@@ -103,14 +104,16 @@
 
         final Set<ArtifactId> artifacts = new HashSet<>();
 
-        for(final Artifact a : feature.getBundles() ) {
-            writeArtifact(artifacts, provider, a, jos, buffer);
-        }
+        for (final Feature feature : features) {
+            for (final Artifact a : feature.getBundles()) {
+                writeArtifact(artifacts, provider, a, jos, buffer);
+            }
 
-        for (final Extension e : feature.getExtensions()) {
-            if (e.getType() == ExtensionType.ARTIFACTS) {
-                for (final Artifact a : e.getArtifacts()) {
-                    writeArtifact(artifacts, provider, a, jos, buffer);
+            for (final Extension e : feature.getExtensions()) {
+                if (e.getType() == ExtensionType.ARTIFACTS) {
+                    for (final Artifact a : e.getArtifacts()) {
+                        writeArtifact(artifacts, provider, a, jos, buffer);
+                    }
                 }
             }
         }