SLING-9087 : 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
new file mode 100644
index 0000000..df00ced
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/archive/ArchiveReader.java
@@ -0,0 +1,129 @@
+/*
+ * 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.feature.io.archive;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.io.json.FeatureJSONReader;
+
+/**
+ * The feature archive reader can be used to read an archive based on a feature
+ * model. The archive contains the model and all artifacts.
+ */
+public class ArchiveReader {
+
+ public interface ArtifactConsumer {
+
+ /**
+ * Consume the artifact from the archive The input stream must not be closed by
+ * the consumer.
+ *
+ * @param artifactId The artifact id
+ * @param is The input stream for the artifact
+ * @throws IOException If the artifact can't be consumed
+ */
+ void consume(ArtifactId artifactId, final InputStream is) throws IOException;
+ }
+
+ /**
+ * 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
+ * @throws IOException If anything goes wrong
+ */
+ @SuppressWarnings("resource")
+ public static Feature read(final InputStream in,
+ final ArtifactConsumer consumer)
+ throws IOException {
+ Feature feature = null;
+
+ final JarInputStream jis = new JarInputStream(in);
+
+ // check manifest
+ final Manifest manifest = jis.getManifest();
+ if ( manifest == null ) {
+ throw new IOException("Not a feature model archive - manifest is missing.");
+ }
+ // check manifest header
+ final String version = manifest.getMainAttributes().getValue(ArchiveWriter.MANIFEST_HEADER);
+ if ( version == null ) {
+ throw new IOException("Not a feature model archive - manifest header is missing.");
+ }
+ // validate manifest header
+ try {
+ final int number = Integer.valueOf(version);
+ if ( number < 1 || number > ArchiveWriter.ARCHIVE_VERSION ) {
+ throw new IOException("Not a feature model archive - invalid manifest header value: " + version);
+ }
+ } catch (final NumberFormatException nfe) {
+ throw new IOException("Not a feature model archive - invalid manifest header value: " + version);
+ }
+
+ final Set<ArtifactId> artifacts = new HashSet<>();
+
+ // read contents
+ JarEntry entry = null;
+ while ( ( entry = jis.getNextJarEntry() ) != null ) {
+ if ( ArchiveWriter.MODEL_NAME.equals(entry.getName()) ) {
+ feature = FeatureJSONReader.read(new InputStreamReader(jis, "UTF-8"), null);
+ } 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);
+ artifacts.add(id);
+ }
+ jis.closeEntry();
+ }
+ if (feature == null) {
+ 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");
+ }
+ }
+
+ 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;
+ }
+}
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
new file mode 100644
index 0000000..f6c18c0
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/archive/ArchiveWriter.java
@@ -0,0 +1,178 @@
+/*
+ * 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.feature.io.archive;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.URL;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.Deflater;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.ArtifactProvider;
+import org.apache.sling.feature.io.json.FeatureJSONWriter;
+
+/**
+ * The feature archive writer can be used to create an archive based on a
+ * feature model. The archive contains the feature model file and all artifacts.
+ */
+public class ArchiveWriter {
+
+ /** The manifest header marking an archive as a feature archive. */
+ public static final String MANIFEST_HEADER = "Feature-Archive-Version";
+
+ /** Current support version of the feature model archive. */
+ public static final int ARCHIVE_VERSION = 1;
+
+ /** Default extension for feature model archives. */
+ public static final String DEFAULT_EXTENSION = "far";
+
+ /** Model name. */
+ public static final String MODEL_NAME = "models/feature.json";
+
+ /** Artifacts prefix. */
+ public static final String ARTIFACTS_PREFIX = "artifacts/";
+
+ /**
+ * Options are used to control the creation of an archive.
+ */
+ public static final class ArchiveOptions {
+
+ private int level = Deflater.DEFAULT_COMPRESSION;
+
+ /**
+ * Get the compression level.
+ *
+ * @return The compression level
+ */
+ public int getLevel() {
+ return level;
+ }
+
+ /**
+ * Set the compression level
+ *
+ * @param level The compression level
+ * @return This object
+ * @see [{@link Deflater#setLevel(int)}
+ * @exception IllegalArgumentException if the compression level is invalid
+ */
+ public ArchiveOptions setLevel(final int level) {
+ if ((level < 0 || level > 9) && level != Deflater.DEFAULT_COMPRESSION) {
+ throw new IllegalArgumentException("invalid compression level");
+ }
+ this.level = level;
+ return this;
+ }
+ }
+
+ /**
+ * Create a feature model archive. The output stream will not be closed by this
+ * method. The caller must call {@link JarOutputStream#close()} or
+ * {@link JarOutputStream#finish()} on the return output stream. The caller can
+ * add additional files through the return stream.
+ *
+ * A feature model can be in different states: it might be a partial feature
+ * model, a complete feature model or an assembled feature model. This method
+ * takes the feature model as provided and only writes the listed bundles and
+ * artifacts of this feature model into the archive. In general, the best
+ * approach for sharing features is to archive {@link Feature#isComplete()
+ * 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 options Optional options to further control the compression
+ * @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 ArchiveOptions options)
+ throws IOException {
+ // create manifest
+ final Manifest manifest = (baseManifest == null ? new Manifest() : new Manifest(baseManifest));
+ manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
+ manifest.getMainAttributes().putValue(MANIFEST_HEADER, String.valueOf(ARCHIVE_VERSION));
+
+ // create archive
+ final JarOutputStream jos = new JarOutputStream(out, manifest);
+ if (options != null) {
+ jos.setLevel(options.getLevel());
+ }
+ // write model first
+ 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();
+
+ final byte[] buffer = new byte[1024*1024*256];
+
+ final Set<ArtifactId> artifacts = new HashSet<>();
+
+ 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);
+ }
+ }
+ }
+ return jos;
+ }
+
+ private static void writeArtifact(final Set<ArtifactId> artifacts,
+ final ArtifactProvider provider,
+ final Artifact artifact,
+ final JarOutputStream jos,
+ final byte[] buffer) throws IOException {
+ if ( artifacts.add(artifact.getId())) {
+ final JarEntry artifactEntry = new JarEntry(ARTIFACTS_PREFIX + artifact.getId().toMvnPath());
+ jos.putNextEntry(artifactEntry);
+
+ final URL url = provider.provide(artifact.getId());
+ if (url == null) {
+ throw new IOException("Unable to find artifact " + artifact.getId().toMvnId());
+ }
+ try (final InputStream is = url.openStream()) {
+ int l = 0;
+ while ( (l = is.read(buffer)) > 0 ) {
+ jos.write(buffer, 0, l);
+ }
+ }
+ jos.closeEntry();
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/io/archive/package-info.java b/src/main/java/org/apache/sling/feature/io/archive/package-info.java
new file mode 100644
index 0000000..46413c3
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/archive/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.feature.io.archive;
+
+