You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by da...@apache.org on 2018/04/27 09:56:00 UTC
[sling-org-apache-sling-feature-io] 01/02: [Sling Feature Model]
Split off IO packages into separate module.
This is an automated email from the ASF dual-hosted git repository.
davidb pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-io.git
commit 168c956f7d2ef3d0c22072d9779362b05d88d51d
Author: David Bosschaert <da...@gmail.com>
AuthorDate: Wed Apr 25 10:58:43 2018 +0100
[Sling Feature Model] Split off IO packages into separate module.
---
pom.xml | 120 +++++
.../apache/sling/feature/io/ArtifactHandler.java | 42 ++
.../apache/sling/feature/io/ArtifactManager.java | 366 +++++++++++++++
.../sling/feature/io/ArtifactManagerConfig.java | 142 ++++++
.../org/apache/sling/feature/io/FileUtils.java | 207 +++++++++
.../feature/io/json/ApplicationJSONReader.java | 92 ++++
.../feature/io/json/ApplicationJSONWriter.java | 97 ++++
.../feature/io/json/ConfigurationJSONReader.java | 78 ++++
.../feature/io/json/ConfigurationJSONWriter.java | 64 +++
.../sling/feature/io/json/FeatureJSONReader.java | 431 +++++++++++++++++
.../sling/feature/io/json/FeatureJSONWriter.java | 207 +++++++++
.../sling/feature/io/json/JSONConstants.java | 88 ++++
.../sling/feature/io/json/JSONReaderBase.java | 479 +++++++++++++++++++
.../sling/feature/io/json/JSONWriterBase.java | 259 +++++++++++
.../sling/feature/io/json/ManifestUtils.java | 515 +++++++++++++++++++++
.../apache/sling/feature/io/json/package-info.java | 23 +
.../org/apache/sling/feature/io/package-info.java | 23 +
.../sling/feature/io/spi/ArtifactProvider.java | 55 +++
.../feature/io/spi/ArtifactProviderContext.java | 46 ++
.../apache/sling/feature/io/spi/package-info.java | 23 +
.../sling/feature/io/ArtifactManagerTest.java | 102 ++++
.../apache/sling/feature/io/FeatureUtilTest.java | 50 ++
.../feature/io/json/FeatureJSONReaderTest.java | 222 +++++++++
.../feature/io/json/FeatureJSONWriterTest.java | 50 ++
.../java/org/apache/sling/feature/io/json/U.java | 89 ++++
src/test/resources/features/repoinit.json | 4 +
src/test/resources/features/repoinit2.json | 7 +
src/test/resources/features/test.json | 92 ++++
src/test/resources/features/test2.json | 107 +++++
src/test/resources/features/test3.json | 116 +++++
30 files changed, 4196 insertions(+)
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..8053a89
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+ <!--
+ 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.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>sling</artifactId>
+ <version>33</version>
+ <relativePath />
+ </parent>
+
+ <artifactId>org.apache.sling.feature.io</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <packaging>bundle</packaging>
+
+ <name>Apache Sling Feature IO Module</name>
+ <description>
+ IO functionality for the Feature Model
+ </description>
+
+ <properties>
+ <sling.java.version>8</sling.java.version>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Conditional-Package>
+ org.apache.felix.configurator.impl.json,
+ org.apache.felix.configurator.impl.model
+ </Conditional-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.converter</artifactId>
+ <version>0.1.0-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.configurator</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.utils</artifactId>
+ <version>1.11.0-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.geronimo.specs</groupId>
+ <artifactId>geronimo-json_1.0_spec</artifactId>
+ <version>1.0-alpha-1</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.feature</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.annotation.versioning</artifactId>
+ <version>1.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.core</artifactId>
+ <version>6.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+
+ <!-- Testing -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>2.8.9</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.johnzon</groupId>
+ <artifactId>johnzon-core</artifactId>
+ <version>1.0.0</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/feature/io/ArtifactHandler.java b/src/main/java/org/apache/sling/feature/io/ArtifactHandler.java
new file mode 100644
index 0000000..45f27be
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/ArtifactHandler.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+import java.io.File;
+
+/**
+ * A handler provides a file object for an artifact.
+ */
+public class ArtifactHandler {
+
+ private final String url;
+
+ private final File file;
+
+ public ArtifactHandler(final String url, final File file) {
+ this.url = url;
+ this.file = file;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public File getFile() {
+ return file;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/io/ArtifactManager.java b/src/main/java/org/apache/sling/feature/io/ArtifactManager.java
new file mode 100644
index 0000000..954ac1a
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/ArtifactManager.java
@@ -0,0 +1,366 @@
+/*
+ * 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;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.io.spi.ArtifactProvider;
+import org.apache.sling.feature.io.spi.ArtifactProviderContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+/**
+ * The artifact manager is the central service to get artifacts.
+ * It uses {@link ArtifactProvider}s to get artifacts. The
+ * providers are loaded using the service loader.
+ */
+public class ArtifactManager {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ /** The map of providers. */
+ private final Map<String, ArtifactProvider> providers;
+
+ /** The configuration */
+ private final ArtifactManagerConfig config;
+
+ /**
+ * Get an artifact manager based on the configuration
+ * @param config The configuration
+ * @return The artifact manager
+ * @throws IOException If the manager can't be initialized
+ */
+ public static ArtifactManager getArtifactManager(final ArtifactManagerConfig config) throws IOException {
+ final ServiceLoader<ArtifactProvider> loader = ServiceLoader.load(ArtifactProvider.class);
+ final Map<String, ArtifactProvider> providers = new HashMap<>();
+ for(final ArtifactProvider provider : loader) {
+ providers.put(provider.getProtocol(), provider);
+ }
+
+ final String[] repositoryURLs = new String[config.getRepositoryUrls().length];
+ int index = 0;
+ for(final String urlString : config.getRepositoryUrls()) {
+ repositoryURLs[index] = urlString;
+ index++;
+ }
+ // default
+ if ( !providers.containsKey("*") ) {
+ providers.put("*", new DefaultArtifactHandler());
+ }
+
+ return new ArtifactManager(config, providers);
+ }
+
+ ArtifactManager(final ArtifactManagerConfig config, final Map<String, ArtifactProvider> providers)
+ throws IOException {
+ this.config = config;
+ this.providers = providers;
+ try {
+ for(final ArtifactProvider provider : this.providers.values()) {
+ provider.init(config);
+ }
+ } catch ( final IOException io) {
+ shutdown();
+ throw io;
+ }
+ }
+
+ /**
+ * Shutdown the artifact manager.
+ */
+ public void shutdown() {
+ for(final ArtifactProvider provider : this.providers.values()) {
+ provider.shutdown();
+ }
+ this.providers.clear();
+ }
+
+ private final File getArtifactFromProviders(final String url, final String relativeCachePath) throws IOException {
+ final int pos = url.indexOf(":");
+ final String scheme = url.substring(0, pos);
+
+ ArtifactProvider provider = this.providers.get(scheme);
+ if ( provider == null ) {
+ provider = this.providers.get("*");
+ }
+ if ( provider == null ) {
+ throw new IOException("No URL provider found for " + url);
+ }
+ return provider.getArtifact(url, relativeCachePath);
+ }
+
+ /**
+ * Get the full artifact url and file for an artifact.
+ * @param url Artifact url or relative path.
+ * @return Absolute url and file in the form of a handler.
+ * @throws IOException If something goes wrong.
+ */
+ public ArtifactHandler getArtifactHandler(final String url) throws IOException {
+ logger.debug("Trying to get artifact for {}", url);
+
+ final String path;
+
+ if ( url.startsWith("mvn:") ) {
+ // mvn url
+ path = ArtifactId.fromMvnUrl(url).toMvnPath();
+
+ } else if ( url.startsWith(":") ) {
+ // repository path
+ path = url.substring(1);
+
+ } else if ( url.indexOf(":/") > 0 ) {
+
+ // absolute URL
+ int pos = url.indexOf(":/") + 2;
+ while ( url.charAt(pos) == '/') {
+ pos++;
+ }
+ final File file = this.getArtifactFromProviders(url, url.substring(pos));
+ if ( file == null || !file.exists()) {
+ throw new IOException("Artifact " + url + " not found.");
+ }
+ return new ArtifactHandler(url, file);
+
+ } else {
+ // file (either relative or absolute)
+ final File f = new File(url);
+ if ( !f.exists()) {
+ throw new IOException("Artifact " + url + " not found.");
+ }
+ return new ArtifactHandler(f.toURI().toString(), f);
+ }
+ logger.debug("Querying repositories for {}", path);
+
+ for(final String repoUrl : this.config.getRepositoryUrls()) {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(repoUrl);
+ builder.append('/');
+ builder.append(path);
+
+ final String artifactUrl = builder.toString();
+ final int pos = artifactUrl.indexOf(":");
+ final String scheme = artifactUrl.substring(0, pos);
+
+ ArtifactProvider handler = this.providers.get(scheme);
+ if ( handler == null ) {
+ handler = this.providers.get("*");
+ }
+ if ( handler == null ) {
+ throw new IOException("No URL handler found for " + artifactUrl);
+ }
+
+ logger.debug("Checking {} to get artifact from {}", handler, artifactUrl);
+
+ final File file = handler.getArtifact(artifactUrl, path);
+ if ( file != null ) {
+ logger.debug("Found artifact {}", artifactUrl);
+ return new ArtifactHandler(artifactUrl, file);
+ }
+
+ // check for SNAPSHOT
+ final int lastSlash = artifactUrl.lastIndexOf('/');
+ final int startSnapshot = artifactUrl.indexOf("-SNAPSHOT", lastSlash + 1);
+
+ if ( startSnapshot > -1 ) {
+ // special snapshot handling
+ final String metadataUrl = artifactUrl.substring(0, lastSlash) + "/maven-metadata.xml";
+ try {
+ final ArtifactHandler metadataHandler = this.getArtifactHandler(metadataUrl);
+
+ final String contents = getFileContents(metadataHandler);
+
+ final String latestVersion = getLatestSnapshot(contents);
+ if ( latestVersion != null ) {
+ final String name = artifactUrl.substring(lastSlash); // includes slash
+ final String fullURL = artifactUrl.substring(0, lastSlash) + name.replace("SNAPSHOT", latestVersion);
+ int pos2 = fullURL.indexOf(":/") + 2;
+ while ( fullURL.charAt(pos2) == '/') {
+ pos2++;
+ }
+ final File file2 = this.getArtifactFromProviders(fullURL, path);
+ if ( file2 == null || !file2.exists()) {
+ throw new IOException("Artifact " + fullURL + " not found.");
+ }
+ return new ArtifactHandler(artifactUrl, file2);
+ }
+ } catch ( final IOException ignore ) {
+ // we ignore this but report the original 404
+ }
+ }
+ }
+
+ throw new IOException("Artifact " + url + " not found in any repository.");
+ }
+
+ protected String getFileContents(final ArtifactHandler handler) throws IOException {
+ final StringBuilder sb = new StringBuilder();
+ for(final String line : Files.readAllLines(handler.getFile().toPath())) {
+ sb.append(line).append('\n');
+ }
+
+ return sb.toString();
+ }
+
+ public static String getValue(final String xml, final String[] xpath) {
+ String value = null;
+ int pos = 0;
+ for(final String name : xpath) {
+ final String element = '<' + name + '>';
+
+ pos = xml.indexOf(element, pos);
+ if ( pos == -1 ) {
+ final String elementWithAttributes = '<' + name + ' ';
+ pos = xml.indexOf(elementWithAttributes, pos);
+ if ( pos == -1 ) {
+ break;
+ }
+ }
+ pos = xml.indexOf('>', pos) + 1;
+ }
+ if ( pos != -1 ) {
+ final int endPos = xml.indexOf("</", pos);
+ if ( endPos != -1 ) {
+ value = xml.substring(pos, endPos).trim();
+ }
+ }
+ return value;
+ }
+ public static String getLatestSnapshot(final String mavenMetadata) {
+ final String timestamp = getValue(mavenMetadata, new String[] {"metadata", "versioning", "snapshot", "timestamp"});
+ final String buildNumber = getValue(mavenMetadata, new String[] {"metadata", "versioning", "snapshot", "buildNumber"});
+
+ if ( timestamp != null && buildNumber != null ) {
+ return timestamp + '-' + buildNumber;
+ }
+
+ return null;
+ }
+
+ private static final class DefaultArtifactHandler implements ArtifactProvider {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private volatile File cacheDir;
+
+ private volatile ArtifactProviderContext config;
+
+ @Override
+ public String getProtocol() {
+ return "*";
+ }
+
+ @Override
+ public void init(final ArtifactProviderContext config) throws IOException {
+ this.cacheDir = config.getCacheDirectory();
+ this.config = config;
+ }
+
+ @Override
+ public void shutdown() {
+ this.config = null;
+ this.cacheDir = null;
+ }
+
+ @Override
+ public File getArtifact(final String url, final String relativeCachePath) {
+ logger.debug("Checking url to be local file {}", url);
+ // check if this is already a local file
+ try {
+ final File f = new File(new URL(url).toURI());
+ if ( f.exists() ) {
+ this.config.incLocalArtifacts();
+ return f;
+ }
+ return null;
+ } catch ( final URISyntaxException ise) {
+ // ignore
+ } catch ( final IllegalArgumentException iae) {
+ // ignore
+ } catch ( final MalformedURLException mue) {
+ // ignore
+ }
+ logger.debug("Checking remote url {}", url);
+ try {
+ // check for url
+ if ( url.indexOf(":") == -1 ) {
+ return null;
+ }
+
+ final String filePath = (this.cacheDir.getAbsolutePath() + File.separatorChar + relativeCachePath).replace('/', File.separatorChar);
+ final File cacheFile = new File(filePath);
+
+ if ( !cacheFile.exists() ) {
+ cacheFile.getParentFile().mkdirs();
+ final URL u = new URL(url);
+ final URLConnection con = u.openConnection();
+ con.connect();
+
+ final InputStream readIS = con.getInputStream();
+ final byte[] buffer = new byte[32768];
+ int l;
+ OutputStream os = null;
+ try {
+ os = new FileOutputStream(cacheFile);
+ while ( (l = readIS.read(buffer)) >= 0 ) {
+ os.write(buffer, 0, l);
+ }
+ } finally {
+ try {
+ readIS.close();
+ } catch ( final IOException ignore) {
+ // ignore
+ }
+ if ( os != null ) {
+ try {
+ os.close();
+ } catch ( final IOException ignore ) {
+ // ignore
+
+ }
+ }
+ }
+ this.config.incDownloadedArtifacts();
+ } else {
+ this.config.incCachedArtifacts();
+ }
+ return cacheFile;
+ } catch ( final Exception e) {
+ logger.info("Artifact not found in one repository", e);
+ // ignore for now
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "DefaultArtifactHandler";
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/io/ArtifactManagerConfig.java b/src/main/java/org/apache/sling/feature/io/ArtifactManagerConfig.java
new file mode 100644
index 0000000..5df9ed8
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/ArtifactManagerConfig.java
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import org.apache.sling.feature.io.spi.ArtifactProviderContext;
+
+/**
+ * This class holds the configuration of artifact manager.
+ */
+public class ArtifactManagerConfig implements ArtifactProviderContext {
+
+ /** The repository urls. */
+ private volatile String[] repositoryUrls;
+
+ /** The cache directory. */
+ private volatile File cacheDirectory;
+
+ private volatile long cachedArtifacts;
+
+ private volatile long downloadedArtifacts;
+
+ private volatile long localArtifacts;
+
+ /**
+ * Create a new configuration object.
+ * Set the default values
+ */
+ public ArtifactManagerConfig() {
+ // set defaults
+ this.repositoryUrls = new String[] {
+ "file://" + System.getProperty("user.home") + "/.m2/repository",
+ "https://repo.maven.apache.org/maven2",
+ "https://repository.apache.org/content/groups/snapshots"
+ };
+ try {
+ this.cacheDirectory = Files.createTempDirectory("slingfeature").toFile();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Set the repository urls
+ * @param urls The repository urls
+ */
+ public void setRepositoryUrls(final String[] urls) {
+ if ( urls == null || urls.length == 0 ) {
+ this.repositoryUrls = null;
+ } else {
+ this.repositoryUrls = new String[urls.length];
+ System.arraycopy(urls, 0, this.repositoryUrls, 0, urls.length);
+ for(int i=0; i<this.repositoryUrls.length; i++) {
+ if ( this.repositoryUrls[i].endsWith("/") ) {
+ this.repositoryUrls[i] = this.repositoryUrls[i].substring(0, this.repositoryUrls[i].length() - 1);
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the repository urls.
+ * A repository url does not end with a slash.
+ * @return The repository urls.
+ */
+ public String[] getRepositoryUrls() {
+ return repositoryUrls;
+ }
+
+ /**
+ * Get the cache directory
+ * @return The cache directory.
+ */
+ @Override
+ public File getCacheDirectory() {
+ return cacheDirectory;
+ }
+
+ /**
+ * Set the cache directory
+ * @param dir The cache directory
+ */
+ public void setCacheDirectory(final File dir) {
+ this.cacheDirectory = dir;
+ }
+
+ @Override
+ public void incCachedArtifacts() {
+ this.cachedArtifacts++;
+ }
+
+ @Override
+ public void incDownloadedArtifacts() {
+ this.downloadedArtifacts++;
+ }
+
+ @Override
+ public void incLocalArtifacts() {
+ this.localArtifacts++;
+ }
+
+ /**
+ * Get the number of cached artifacts
+ * @return The number of cached artifacts
+ */
+ public long getCachedArtifacts() {
+ return this.cachedArtifacts;
+ }
+
+ /**
+ * Get the number of downloaded artifacts
+ * @return The number of downloaded artifacts
+ */
+ public long getDownloadedArtifacts() {
+ return this.downloadedArtifacts;
+ }
+
+ /**
+ * Get the number of local artifacts
+ * @return The number of local artifacts
+ */
+ public long getLocalArtifacts() {
+ return this.localArtifacts;
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/io/FileUtils.java b/src/main/java/org/apache/sling/feature/io/FileUtils.java
new file mode 100644
index 0000000..a54d933
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/FileUtils.java
@@ -0,0 +1,207 @@
+/*
+ * 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;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class FileUtils {
+
+ /** The extension for a reference file. */
+ public static final String EXTENSION_REF_FILE = ".ref";
+
+ /** The extension for a feature file. */
+ public static final String EXTENSION_FEATURE_FILE = ".json";
+
+ /** The default directory to search for features. */
+ public static final String DEFAULT_DIRECTORY = "features";
+
+ /** The default name of the feature file. */
+ public static final String DEFAULT_FEATURE_FILE = "feature" + EXTENSION_FEATURE_FILE;
+
+ /**
+ * Parse a feature reference file
+ * @param file The file
+ * @return The referenced features
+ * @throws IOException If reading fails
+ */
+ public static List<String> parseFeatureRefFile(final File file)
+ throws IOException {
+ final List<String> result = new ArrayList<>();
+ final List<String> lines = Files.readAllLines(file.toPath());
+ for(String line : lines) {
+ line = line.trim();
+ if ( !line.isEmpty() && !line.startsWith("#") ) {
+ if ( line.indexOf(':') == -1 ) {
+ result.add(new File(line).getAbsolutePath());
+ } else {
+ result.add(line);
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Get the list of feature files.
+ * If the provided list of files is {@code null} or an empty array, the default is used.
+ * The default checks for the following places, the first one found is used. If none is
+ * found an empty list is returned.
+ * <ol>
+ * <li>A directory named {@link #DEFAULT_DIRECTORY} in the current directory
+ * <li>A file named {@link #DEFAULT_FEATURE_FILE} in the current directory
+ * <li>A directory named {@link #DEFAULT_DIRECTORY} in the home directory
+ * <li>A file named {@link #DEFAULT_FEATURE_FILE} in the home directory
+ * </ol>
+ *
+ * The list of files is processed one after the other. If it is relative, it is
+ * first tried to be resolved against the current directory and then against the
+ * home directory.
+ * If an entry denotes a directory, all children ending in {@link #EXTENSION_FEATURE_FILE} or
+ * {@link #EXTENSION_REF_FILE} of that directory are read.
+ * If a file ends in {@link #EXTENSION_REF_FILE} the contents is read and every line not
+ * starting with the hash sign is considered a reference to a feature artifact.
+ *
+ * @param homeDirectory If relative files should be resolved, this is the directory to use
+ * @param files Optional list of files. If none is provided, a default is used.
+ * @return The list of files.
+ * @throws IOException If an error occurs.
+ */
+ public static List<String> getFeatureFiles(final File homeDirectory, final String... files)
+ throws IOException {
+ String[] featureFiles = files;
+ if ( featureFiles == null || featureFiles.length == 0 ) {
+ // Default value - check feature directory otherwise features file
+ final File[] candidates = new File[] {
+ new File(homeDirectory, DEFAULT_DIRECTORY),
+ new File(homeDirectory, DEFAULT_FEATURE_FILE),
+ new File(DEFAULT_DIRECTORY),
+ new File(DEFAULT_FEATURE_FILE)
+ };
+ File f = null;
+ for(final File c : candidates) {
+ if ( c.exists() ) {
+ f = c;
+ break;
+ }
+ }
+ // nothing found, we default to the first candidate and fail later
+ if ( f == null ) {
+ f = candidates[0];
+ }
+
+ featureFiles = new String[] {f.getAbsolutePath()};
+ }
+
+ final List<String> paths = new ArrayList<>();
+ for(final String name : featureFiles) {
+ // check for absolute
+ if ( name.indexOf(':') > 1 ) {
+ paths.add(name);
+ } else {
+ // file or relative
+ File f = null;
+ final File test = new File(name);
+ if ( test.isAbsolute() ) {
+ f = test;
+ } else {
+ final File[] candidates = {
+ new File(homeDirectory, name),
+ new File(homeDirectory, DEFAULT_DIRECTORY + File.separatorChar + name),
+ new File(name),
+ new File(DEFAULT_DIRECTORY + File.separatorChar + name),
+ };
+ for(final File c : candidates) {
+ if ( c.exists() && c.isFile() ) {
+ f = c;
+ break;
+ }
+ }
+ }
+
+ if ( f != null && f.exists() ) {
+ if ( f.isFile() ) {
+ processFile(paths, f);
+ } else {
+ processDir(paths, f);
+ }
+ } else {
+ // we simply add the path and fail later on
+ paths.add(new File(name).getAbsolutePath());
+ }
+ }
+ }
+
+ Collections.sort(paths, FEATURE_PATH_COMP);
+ return paths;
+ }
+
+ static final Comparator<String> FEATURE_PATH_COMP = new Comparator<String>() {
+
+ @Override
+ public int compare(final String o1, final String o2) {
+ // windows path conversion
+ final String key1 = o1.replace(File.separatorChar, '/');
+ final String key2 = o2.replace(File.separatorChar, '/');
+
+ final int lastSlash1 = key1.lastIndexOf('/');
+ final int lastSlash2 = key2.lastIndexOf('/');
+ if ( lastSlash1 == -1 || lastSlash2 == -1 ) {
+ return o1.compareTo(o2);
+ }
+ final String path1 = key1.substring(0, lastSlash1 + 1);
+ final String path2 = key2.substring(0, lastSlash2 + 1);
+ if ( path1.equals(path2) ) {
+ return o1.compareTo(o2);
+ }
+ if ( path1.startsWith(path2) ) {
+ return 1;
+ } else if ( path2.startsWith(path1) ) {
+ return -1;
+ }
+ return o1.compareTo(o2);
+ }
+ };
+
+ private static void processDir(final List<String> paths, final File dir)
+ throws IOException {
+ for(final File f : dir.listFiles()) {
+ if ( f.isFile() && !f.getName().startsWith(".")) {
+ // check if file is a reference
+ if ( f.getName().endsWith(EXTENSION_REF_FILE) || f.getName().endsWith(EXTENSION_FEATURE_FILE) ) {
+ processFile(paths, f);
+ }
+ }
+ }
+ }
+
+
+
+ private static void processFile(final List<String> paths, final File f)
+ throws IOException {
+ if ( f.getName().endsWith(EXTENSION_REF_FILE) ) {
+ paths.addAll(parseFeatureRefFile(f));
+ } else {
+ paths.add(f.getAbsolutePath());
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/io/json/ApplicationJSONReader.java b/src/main/java/org/apache/sling/feature/io/json/ApplicationJSONReader.java
new file mode 100644
index 0000000..88d3bc5
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/ApplicationJSONReader.java
@@ -0,0 +1,92 @@
+/*
+ * 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.json;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+
+import org.apache.felix.configurator.impl.json.JSONUtil;
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.ArtifactId;
+
+/**
+ * This class offers a method to read an {@code Application} using a {@code Reader} instance.
+ */
+public class ApplicationJSONReader extends JSONReaderBase {
+
+ /**
+ * Read a new application from the reader
+ * The reader is not closed. It is up to the caller to close the reader.
+ *
+ * @param reader The reader for the feature
+ * @return The application
+ * @throws IOException If an IO errors occurs or the JSON is invalid.
+ */
+ public static Application read(final Reader reader)
+ throws IOException {
+ try {
+ final ApplicationJSONReader mr = new ApplicationJSONReader();
+ mr.readApplication(reader);
+ return mr.app;
+ } catch (final IllegalStateException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /** The read application. */
+ private final Application app;
+
+ /**
+ * Private constructor
+ */
+ private ApplicationJSONReader() {
+ super(null);
+ this.app = new Application();
+ }
+
+ /**
+ * Read a full application
+ * @param reader The reader
+ * @throws IOException If an IO error occurs or the JSON is not valid.
+ */
+ private void readApplication(final Reader reader)
+ throws IOException {
+ final JsonObject json = Json.createReader(new StringReader(minify(reader))).readObject();
+
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> map = (Map<String, Object>) JSONUtil.getValue(json);
+
+ final String frameworkId = this.getProperty(map, JSONConstants.APP_FRAMEWORK);
+ if ( frameworkId != null ) {
+ app.setFramework(ArtifactId.parse(frameworkId));
+ }
+ this.readBundles(map, app.getBundles(), app.getConfigurations());
+ this.readFrameworkProperties(map, app.getFrameworkProperties());
+ this.readConfigurations(map, app.getConfigurations());
+
+ this.readExtensions(map,
+ JSONConstants.APP_KNOWN_PROPERTIES,
+ this.app.getExtensions(), this.app.getConfigurations());
+ }
+}
+
+
diff --git a/src/main/java/org/apache/sling/feature/io/json/ApplicationJSONWriter.java b/src/main/java/org/apache/sling/feature/io/json/ApplicationJSONWriter.java
new file mode 100644
index 0000000..bdf4902
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/ApplicationJSONWriter.java
@@ -0,0 +1,97 @@
+/*
+ * 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.json;
+
+import org.apache.sling.feature.Application;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collections;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonWriter;
+import javax.json.JsonWriterFactory;
+import javax.json.stream.JsonGenerator;
+
+/**
+ * Simple JSON writer for an application
+ */
+public class ApplicationJSONWriter extends JSONWriterBase {
+
+ /**
+ * Writes the application to the writer.
+ * The writer is not closed.
+ * @param writer Writer
+ * @param app The application
+ * @throws IOException If writing fails
+ */
+ public static void write(final Writer writer, final Application app)
+ throws IOException {
+ final ApplicationJSONWriter w = new ApplicationJSONWriter();
+ w.writeApp(writer, app);
+ }
+
+ private void writeApp(final Writer writer, final Application app)
+ throws IOException {
+ JsonObjectBuilder ob = Json.createObjectBuilder();
+
+ // framework
+ if ( app.getFramework() != null ) {
+ ob.add(JSONConstants.APP_FRAMEWORK, app.getFramework().toMvnId());
+ }
+
+ // features
+ if ( !app.getFeatureIds().isEmpty() ) {
+ JsonArrayBuilder featuresArr = Json.createArrayBuilder();
+
+ for(final ArtifactId id : app.getFeatureIds()) {
+ featuresArr.add(id.toMvnId());
+ }
+ ob.add(JSONConstants.APP_FEATURES, featuresArr.build());
+ }
+
+ // bundles
+ writeBundles(ob, app.getBundles(), app.getConfigurations());
+
+ // configurations
+ final Configurations cfgs = new Configurations();
+ for(final Configuration cfg : app.getConfigurations()) {
+ final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT);
+ if ( artifactProp == null ) {
+ cfgs.add(cfg);
+ }
+ }
+ writeConfigurations(ob, cfgs);
+
+ // framework properties
+ writeFrameworkProperties(ob, app.getFrameworkProperties());
+
+ // extensions
+ writeExtensions(ob, app.getExtensions(), app.getConfigurations());
+
+ JsonWriterFactory writerFactory = Json.createWriterFactory(
+ Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true));
+ JsonWriter jw = writerFactory.createWriter(writer);
+ jw.writeObject(ob.build());
+ jw.close();
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONReader.java b/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONReader.java
new file mode 100644
index 0000000..5fca482
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONReader.java
@@ -0,0 +1,78 @@
+/*
+ * 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.json;
+
+import org.apache.felix.configurator.impl.json.JSONUtil;
+import org.apache.sling.feature.Configurations;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * JSON Reader for configurations.
+ */
+public class ConfigurationJSONReader extends JSONReaderBase {
+
+ /**
+ * Read a map of configurations from the reader
+ * The reader is not closed. It is up to the caller to close the reader.
+ *
+ * @param reader The reader for the configuration
+ * @param location Optional location
+ * @return The read configurations
+ * @throws IOException If an IO errors occurs or the JSON is invalid.
+ */
+ public static Configurations read(final Reader reader, final String location)
+ throws IOException {
+ try {
+ final ConfigurationJSONReader mr = new ConfigurationJSONReader(location);
+ return mr.readConfigurations(reader);
+ } catch (final IllegalStateException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Private constructor
+ * @param location Optional location
+ */
+ ConfigurationJSONReader(final String location) {
+ super(location);
+ }
+
+ Configurations readConfigurations(final Reader reader) throws IOException {
+ final Configurations result = new Configurations();
+
+ final JsonObject json = Json.createReader(new StringReader(minify(reader))).readObject();
+
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> map = (Map<String, Object>) JSONUtil.getValue(json);
+
+ final Map<String, Object> objMap = Collections.singletonMap(JSONConstants.FEATURE_CONFIGURATIONS, (Object)map);
+
+ readConfigurations(objMap, result);
+
+ return result;
+ }
+}
+
+
diff --git a/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java b/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java
new file mode 100644
index 0000000..64e937d
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java
@@ -0,0 +1,64 @@
+/*
+ * 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.json;
+
+import org.apache.sling.feature.Configurations;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collections;
+
+import javax.json.Json;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonWriter;
+import javax.json.JsonWriterFactory;
+import javax.json.stream.JsonGenerator;
+
+
+/**
+ * JSON writer for configurations
+ */
+public class ConfigurationJSONWriter extends JSONWriterBase {
+
+ /**
+ * Writes the configurations to the writer.
+ * The writer is not closed.
+ * @param writer Writer
+ * @param configs List of configurations
+ * @throws IOException If writing fails
+ */
+ public static void write(final Writer writer, final Configurations configs)
+ throws IOException {
+ final ConfigurationJSONWriter w = new ConfigurationJSONWriter();
+ w.writeConfigurations(writer, configs);
+ }
+
+ private void writeConfigurations(final Writer writer, final Configurations configs)
+ throws IOException {
+ JsonObjectBuilder ob = Json.createObjectBuilder();
+
+ // TODO is this correct?
+ ob.add(JSONConstants.FEATURE_CONFIGURATIONS,
+ writeConfigurationsMap(configs));
+
+ JsonWriterFactory writerFactory = Json.createWriterFactory(
+ Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true));
+ JsonWriter jw = writerFactory.createWriter(writer);
+ jw.writeObject(ob.build());
+ jw.close();
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/io/json/FeatureJSONReader.java b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONReader.java
new file mode 100644
index 0000000..0d21598
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONReader.java
@@ -0,0 +1,431 @@
+/*
+ * 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.json;
+
+import org.apache.felix.utils.resource.CapabilityImpl;
+import org.apache.felix.utils.resource.RequirementImpl;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Include;
+import org.apache.sling.feature.KeyValueMap;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+
+/**
+ * This class offers a method to read a {@code Feature} using a {@code Reader} instance.
+ */
+public class FeatureJSONReader extends JSONReaderBase {
+ public enum SubstituteVariables { NONE, RESOLVE, LAUNCH }
+
+ // The pattern that variables in Feature JSON follow
+ private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{[a-zA-Z0-9.-_]+\\}");
+
+ /**
+ * Read a new feature from the reader
+ * The reader is not closed. It is up to the caller to close the reader.
+ *
+ * @param reader The reader for the feature
+ * @param location Optional location
+ * @return The read feature
+ * @throws IOException If an IO errors occurs or the JSON is invalid.
+ */
+ public static Feature read(final Reader reader, final String location, final SubstituteVariables phase)
+ throws IOException {
+ return read(reader, null, location, phase);
+ }
+
+ /**
+ * Read a new feature from the reader
+ * The reader is not closed. It is up to the caller to close the reader.
+ *
+ * @param reader The reader for the feature
+ * @param providedId Optional artifact id
+ * @param location Optional location
+ * @return The read feature
+ * @throws IOException If an IO errors occurs or the JSON is invalid.
+ */
+ public static Feature read(final Reader reader,
+ final ArtifactId providedId,
+ final String location,
+ final SubstituteVariables phase)
+ throws IOException {
+ try {
+ final FeatureJSONReader mr = new FeatureJSONReader(providedId, location, phase);
+ return mr.readFeature(reader);
+ } catch (final IllegalStateException | IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /** The read feature. */
+ private Feature feature;
+
+ /** The provided id. */
+ private final ArtifactId providedId;
+
+ /** The variables from the JSON. */
+ private Map<String, String> variables;
+
+ /** The current reading phase. */
+ private final SubstituteVariables phase;
+
+ /**
+ * Private constructor
+ * @param pId Optional id
+ * @param location Optional location
+ */
+ FeatureJSONReader(final ArtifactId pId, final String location, final SubstituteVariables phase) {
+ super(location);
+ this.providedId = pId;
+ this.phase = phase;
+ }
+
+ /**
+ * Read a full feature
+ * @param reader The reader
+ * @return The feature object
+ * @throws IOException If an IO error occurs or the JSON is not valid.
+ */
+ private Feature readFeature(final Reader reader)
+ throws IOException {
+ final JsonObject json = Json.createReader(new StringReader(minify(reader))).readObject();
+ final Map<String, Object> map = getJsonMap(json);
+
+ checkModelVersion(map);
+
+ final ArtifactId fId;
+ if ( !map.containsKey(JSONConstants.FEATURE_ID) ) {
+ if ( this.providedId == null ) {
+ throw new IOException(this.exceptionPrefix + "Feature id is missing");
+ }
+ fId = this.providedId;
+ } else {
+ final Object idObj = map.get(JSONConstants.FEATURE_ID);
+ checkType(JSONConstants.FEATURE_ID, idObj, String.class);
+ fId = ArtifactId.parse(idObj.toString());
+ }
+ this.feature = new Feature(fId);
+ this.feature.setLocation(this.location);
+
+ // title, description, vendor and license
+ this.feature.setTitle(getProperty(map, JSONConstants.FEATURE_TITLE));
+ this.feature.setDescription(getProperty(map, JSONConstants.FEATURE_DESCRIPTION));
+ this.feature.setVendor(getProperty(map, JSONConstants.FEATURE_VENDOR));
+ this.feature.setLicense(getProperty(map, JSONConstants.FEATURE_LICENSE));
+
+ this.readVariables(map, feature.getVariables());
+ this.readBundles(map, feature.getBundles(), feature.getConfigurations());
+ this.readFrameworkProperties(map, feature.getFrameworkProperties());
+ this.readConfigurations(map, feature.getConfigurations());
+
+ this.readCapabilities(map);
+ this.readRequirements(map);
+ this.readIncludes(map);
+
+ this.readExtensions(map,
+ JSONConstants.FEATURE_KNOWN_PROPERTIES,
+ this.feature.getExtensions(), this.feature.getConfigurations());
+
+ return feature;
+ }
+
+ private void checkModelVersion(final Map<String, Object> map) throws IOException {
+ String modelVersion = getProperty(map, JSONConstants.FEATURE_MODEL_VERSION);
+ if (modelVersion == null) {
+ modelVersion = "1";
+ }
+ if (!"1".equals(modelVersion)) {
+ throw new IOException("Unsupported model version: " + modelVersion);
+ }
+ }
+
+ @Override
+ protected Object handleResolveVars(Object val) {
+ if (phase == SubstituteVariables.RESOLVE) {
+ return handleVars(val);
+ } else {
+ return val;
+ }
+ }
+
+ @Override
+ protected Object handleLaunchVars(Object val) {
+ if (phase == SubstituteVariables.LAUNCH) {
+ return handleVars(val);
+ }
+ return val;
+ }
+
+ private Object handleVars(Object value) {
+ if (!(value instanceof String)) {
+ return value;
+ }
+
+ String textWithVars = (String) value;
+
+ Matcher m = VARIABLE_PATTERN.matcher(textWithVars.toString());
+ StringBuffer sb = new StringBuffer();
+ while (m.find()) {
+ String var = m.group();
+
+ int len = var.length();
+ String name = var.substring(2, len - 1);
+ String val = variables.get(name);
+ if (val != null) {
+ m.appendReplacement(sb, Matcher.quoteReplacement(val));
+ } else {
+ throw new IllegalStateException("Undefined variable: " + name);
+ }
+ }
+ m.appendTail(sb);
+
+ return sb.toString();
+ }
+
+ private void readVariables(Map<String, Object> map, KeyValueMap kvMap) throws IOException {
+ variables = new HashMap<>();
+
+ if (map.containsKey(JSONConstants.FEATURE_VARIABLES)) {
+ final Object variablesObj = map.get(JSONConstants.FEATURE_VARIABLES);
+ checkType(JSONConstants.FEATURE_VARIABLES, variablesObj, Map.class);
+
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> vars = (Map<String, Object>) variablesObj;
+ for (final Map.Entry<String, Object> entry : vars.entrySet()) {
+ checkType("variable value", entry.getValue(), String.class, Boolean.class, Number.class);
+
+ String key = entry.getKey();
+ if (kvMap.get(key) != null) {
+ throw new IOException(this.exceptionPrefix + "Duplicate variable " + key);
+ }
+ String value = "" + entry.getValue();
+ kvMap.put(key, value);
+ variables.put(key, value);
+ }
+ }
+ }
+
+ private void readIncludes(final Map<String, Object> map) throws IOException {
+ if ( map.containsKey(JSONConstants.FEATURE_INCLUDES)) {
+ final Object includesObj = map.get(JSONConstants.FEATURE_INCLUDES);
+ checkType(JSONConstants.FEATURE_INCLUDES, includesObj, List.class);
+
+ @SuppressWarnings("unchecked")
+ final List<Object> includes = (List<Object>)includesObj;
+ for(final Object inc : includes) {
+ checkType("Include", inc, Map.class, String.class);
+ final Include include;
+ if ( inc instanceof String ) {
+ final ArtifactId id = ArtifactId.parse(inc.toString());
+ include = new Include(id);
+ } else {
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> obj = (Map<String, Object>) inc;
+ if ( !obj.containsKey(JSONConstants.ARTIFACT_ID) ) {
+ throw new IOException(exceptionPrefix + " include is missing required artifact id");
+ }
+ checkType("Include " + JSONConstants.ARTIFACT_ID, obj.get(JSONConstants.ARTIFACT_ID), String.class);
+ final ArtifactId id = ArtifactId.parse(handleResolveVars(obj.get(JSONConstants.ARTIFACT_ID)).toString());
+ include = new Include(id);
+
+ if ( obj.containsKey(JSONConstants.INCLUDE_REMOVALS) ) {
+ checkType("Include removals", obj.get(JSONConstants.INCLUDE_REMOVALS), Map.class);
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> removalObj = (Map<String, Object>) obj.get(JSONConstants.INCLUDE_REMOVALS);
+ if ( removalObj.containsKey(JSONConstants.FEATURE_BUNDLES) ) {
+ checkType("Include removal bundles", removalObj.get(JSONConstants.FEATURE_BUNDLES), List.class);
+ @SuppressWarnings("unchecked")
+ final List<Object> list = (List<Object>)removalObj.get(JSONConstants.FEATURE_BUNDLES);
+ for(final Object val : list) {
+ checkType("Include removal bundles", val, String.class);
+ include.getBundleRemovals().add(ArtifactId.parse(val.toString()));
+ }
+ }
+ if ( removalObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
+ checkType("Include removal configuration", removalObj.get(JSONConstants.FEATURE_CONFIGURATIONS), List.class);
+ @SuppressWarnings("unchecked")
+ final List<Object> list = (List<Object>)removalObj.get(JSONConstants.FEATURE_CONFIGURATIONS);
+ for(final Object val : list) {
+ checkType("Include removal bundles", val, String.class);
+ include.getConfigurationRemovals().add(val.toString());
+ }
+ }
+ if ( removalObj.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) {
+ checkType("Include removal framework properties", removalObj.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES), List.class);
+ @SuppressWarnings("unchecked")
+ final List<Object> list = (List<Object>)removalObj.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
+ for(final Object val : list) {
+ checkType("Include removal bundles", val, String.class);
+ include.getFrameworkPropertiesRemovals().add(val.toString());
+ }
+ }
+ if ( removalObj.containsKey(JSONConstants.INCLUDE_EXTENSION_REMOVALS) ) {
+ checkType("Include removal extensions", removalObj.get(JSONConstants.INCLUDE_EXTENSION_REMOVALS), List.class);
+ @SuppressWarnings("unchecked")
+ final List<Object> list = (List<Object>)removalObj.get(JSONConstants.INCLUDE_EXTENSION_REMOVALS);
+ for(final Object val : list) {
+ checkType("Include removal extension", val, String.class, Map.class);
+ if ( val instanceof String ) {
+ include.getExtensionRemovals().add(val.toString());
+ } else {
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> removalMap = (Map<String, Object>)val;
+ final Object nameObj = removalMap.get("name");
+ checkType("Include removal extension", nameObj, String.class);
+ if ( removalMap.containsKey("artifacts") ) {
+ checkType("Include removal extension artifacts", removalMap.get("artifacts"), List.class);
+ @SuppressWarnings("unchecked")
+ final List<Object> artifactList = (List<Object>)removalMap.get("artifacts");
+ final List<ArtifactId> ids = new ArrayList<>();
+ for(final Object aid : artifactList) {
+ checkType("Include removal extension artifact", aid, String.class);
+ ids.add(ArtifactId.parse(aid.toString()));
+ }
+ include.getArtifactExtensionRemovals().put(nameObj.toString(), ids);
+ } else {
+ include.getExtensionRemovals().add(nameObj.toString());
+ }
+ }
+ }
+ }
+
+ }
+ }
+ for(final Include i : feature.getIncludes()) {
+ if ( i.getId().equals(include.getId()) ) {
+ throw new IOException(exceptionPrefix + "Duplicate include of " + include.getId());
+ }
+ }
+ feature.getIncludes().add(include);
+ }
+ }
+ }
+
+ private void readRequirements(Map<String, Object> map) throws IOException {
+ if ( map.containsKey(JSONConstants.FEATURE_REQUIREMENTS)) {
+ final Object reqObj = map.get(JSONConstants.FEATURE_REQUIREMENTS);
+ checkType(JSONConstants.FEATURE_REQUIREMENTS, reqObj, List.class);
+
+ @SuppressWarnings("unchecked")
+ final List<Object> requirements = (List<Object>)reqObj;
+ for(final Object req : requirements) {
+ checkType("Requirement", req, Map.class);
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> obj = (Map<String, Object>) req;
+
+ if ( !obj.containsKey(JSONConstants.REQCAP_NAMESPACE) ) {
+ throw new IOException(this.exceptionPrefix + "Namespace is missing for requirement");
+ }
+ checkType("Requirement namespace", obj.get(JSONConstants.REQCAP_NAMESPACE), String.class);
+
+ Map<String, Object> attrMap = new HashMap<>();
+ if ( obj.containsKey(JSONConstants.REQCAP_ATTRIBUTES) ) {
+ checkType("Requirement attributes", obj.get(JSONConstants.REQCAP_ATTRIBUTES), Map.class);
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> attrs = (Map<String, Object>)obj.get(JSONConstants.REQCAP_ATTRIBUTES);
+ attrs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalAttribute(key, handleResolveVars(value), attrMap::put)));
+ }
+
+ Map<String, String> dirMap = new HashMap<>();
+ if ( obj.containsKey(JSONConstants.REQCAP_DIRECTIVES) ) {
+ checkType("Requirement directives", obj.get(JSONConstants.REQCAP_DIRECTIVES), Map.class);
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> dirs = (Map<String, Object>)obj.get(JSONConstants.REQCAP_DIRECTIVES);
+ dirs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalDirective(key, handleResolveVars(value), dirMap::put)));
+ }
+
+ final Requirement r = new RequirementImpl(null, handleResolveVars(obj.get(JSONConstants.REQCAP_NAMESPACE)).toString(), dirMap, attrMap);
+ feature.getRequirements().add(r);
+ }
+ }
+ }
+
+ private void readCapabilities(Map<String, Object> map) throws IOException {
+ if ( map.containsKey(JSONConstants.FEATURE_CAPABILITIES)) {
+ final Object capObj = map.get(JSONConstants.FEATURE_CAPABILITIES);
+ checkType(JSONConstants.FEATURE_CAPABILITIES, capObj, List.class);
+
+ @SuppressWarnings("unchecked")
+ final List<Object> capabilities = (List<Object>)capObj;
+ for(final Object cap : capabilities) {
+ checkType("Capability", cap, Map.class);
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> obj = (Map<String, Object>) cap;
+
+ if ( !obj.containsKey(JSONConstants.REQCAP_NAMESPACE) ) {
+ throw new IOException(this.exceptionPrefix + "Namespace is missing for capability");
+ }
+ checkType("Capability namespace", obj.get(JSONConstants.REQCAP_NAMESPACE), String.class);
+
+ Map<String, Object> attrMap = new HashMap<>();
+ if ( obj.containsKey(JSONConstants.REQCAP_ATTRIBUTES) ) {
+ checkType("Capability attributes", obj.get(JSONConstants.REQCAP_ATTRIBUTES), Map.class);
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> attrs = (Map<String, Object>)obj.get(JSONConstants.REQCAP_ATTRIBUTES);
+ attrs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalAttribute(key, handleResolveVars(value), attrMap::put)));
+ }
+
+ Map<String, String> dirMap = new HashMap<>();
+ if ( obj.containsKey(JSONConstants.REQCAP_DIRECTIVES) ) {
+ checkType("Capability directives", obj.get(JSONConstants.REQCAP_DIRECTIVES), Map.class);
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> dirs = (Map<String, Object>) obj.get(JSONConstants.REQCAP_DIRECTIVES);
+ dirs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalDirective(key, handleResolveVars(value), dirMap::put)));
+ }
+
+ final Capability c = new CapabilityImpl(null, handleResolveVars(obj.get(JSONConstants.REQCAP_NAMESPACE)).toString(), dirMap, attrMap);
+ feature.getCapabilities().add(c);
+ }
+ }
+ }
+
+ @FunctionalInterface
+ private interface BiConsumer_WithExceptions<T, V, E extends Exception> {
+ void accept(T t, V u) throws E;
+ }
+
+ private static <T, V, E extends Exception> BiConsumer<T, V> rethrowBiConsumer(BiConsumer_WithExceptions<T, V, E> biConsumer) {
+ return (t, u) -> {
+ try {
+ biConsumer.accept(t, u);
+ } catch (Exception exception) {
+ throwAsUnchecked(exception);
+ }
+ };
+ }
+
+ @SuppressWarnings ("unchecked")
+ private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E {
+ throw (E) exception;
+ }
+}
+
+
diff --git a/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java
new file mode 100644
index 0000000..57428e1
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java
@@ -0,0 +1,207 @@
+/*
+ * 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.json;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Include;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonWriter;
+import javax.json.JsonWriterFactory;
+import javax.json.stream.JsonGenerator;
+
+/**
+ * Simple JSON writer for a feature
+ */
+public class FeatureJSONWriter extends JSONWriterBase {
+ private FeatureJSONWriter() {}
+
+ /**
+ * Writes the feature to the writer.
+ * The writer is not closed.
+ * @param writer Writer
+ * @param feature Feature
+ * @throws IOException If writing fails
+ */
+ public static void write(final Writer writer, final Feature feature)
+ throws IOException {
+ final FeatureJSONWriter w = new FeatureJSONWriter();
+ w.writeFeature(writer, feature);
+ }
+
+ private void writeProperty(final JsonObjectBuilder ob, final String key, final String value) {
+ if ( value != null ) {
+ ob.add(key, value);
+ }
+ }
+
+ private void writeFeature(final Writer writer, final Feature feature)
+ throws IOException {
+ JsonObjectBuilder ob = Json.createObjectBuilder();
+ ob.add(JSONConstants.FEATURE_ID, feature.getId().toMvnId());
+
+ // title, description, vendor, license
+ writeProperty(ob, JSONConstants.FEATURE_TITLE, feature.getTitle());
+ writeProperty(ob, JSONConstants.FEATURE_DESCRIPTION, feature.getDescription());
+ writeProperty(ob, JSONConstants.FEATURE_VENDOR, feature.getVendor());
+ writeProperty(ob, JSONConstants.FEATURE_LICENSE, feature.getLicense());
+
+ // variables
+ writeVariables(ob, feature.getVariables());
+
+ // includes
+ if ( !feature.getIncludes().isEmpty() ) {
+ JsonArrayBuilder incArray = Json.createArrayBuilder();
+ for(final Include inc : feature.getIncludes()) {
+ if ( inc.getArtifactExtensionRemovals().isEmpty()
+ && inc.getBundleRemovals().isEmpty()
+ && inc.getConfigurationRemovals().isEmpty()
+ && inc.getFrameworkPropertiesRemovals().isEmpty() ) {
+ incArray.add(inc.getId().toMvnId());
+ } else {
+ JsonObjectBuilder includeObj = Json.createObjectBuilder();
+ includeObj.add(JSONConstants.ARTIFACT_ID, inc.getId().toMvnId());
+
+ JsonObjectBuilder removalsObj = Json.createObjectBuilder();
+ if ( !inc.getArtifactExtensionRemovals().isEmpty()
+ || inc.getExtensionRemovals().isEmpty() ) {
+ JsonArrayBuilder extRemovals = Json.createArrayBuilder();
+ for(final String id : inc.getExtensionRemovals()) {
+ extRemovals.add(id);
+ }
+ for(final Map.Entry<String, List<ArtifactId>> entry : inc.getArtifactExtensionRemovals().entrySet()) {
+ JsonArrayBuilder ab = Json.createArrayBuilder();
+ for(final ArtifactId id : entry.getValue()) {
+ ab.add(id.toMvnId());
+ }
+ extRemovals.add(Json.createObjectBuilder().add(entry.getKey(),
+ ab.build()).build());
+ }
+ removalsObj.add(JSONConstants.INCLUDE_EXTENSION_REMOVALS, extRemovals.build());
+ }
+ if ( !inc.getConfigurationRemovals().isEmpty() ) {
+ JsonArrayBuilder cfgRemovals = Json.createArrayBuilder();
+ for(final String val : inc.getConfigurationRemovals()) {
+ cfgRemovals.add(val);
+ }
+ removalsObj.add(JSONConstants.FEATURE_CONFIGURATIONS, cfgRemovals.build());
+ }
+ if ( !inc.getBundleRemovals().isEmpty() ) {
+ JsonArrayBuilder bundleRemovals = Json.createArrayBuilder();
+ for(final ArtifactId val : inc.getBundleRemovals()) {
+ bundleRemovals.add(val.toMvnId());
+ }
+ removalsObj.add(JSONConstants.FEATURE_BUNDLES, bundleRemovals.build());
+ }
+ if ( !inc.getFrameworkPropertiesRemovals().isEmpty() ) {
+ JsonArrayBuilder propRemovals = Json.createArrayBuilder();
+ for(final String val : inc.getFrameworkPropertiesRemovals()) {
+ propRemovals.add(val);
+ }
+ removalsObj.add(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES, propRemovals.build());
+ }
+ includeObj.add(JSONConstants.INCLUDE_REMOVALS, removalsObj.build());
+
+ incArray.add(includeObj.build());
+ }
+ }
+ ob.add(JSONConstants.FEATURE_INCLUDES, incArray.build());
+ }
+
+ // requirements
+ if ( !feature.getRequirements().isEmpty() ) {
+ JsonArrayBuilder requirements = Json.createArrayBuilder();
+
+ for(final Requirement req : feature.getRequirements()) {
+ JsonObjectBuilder requirementObj = Json.createObjectBuilder();
+ requirementObj.add(JSONConstants.REQCAP_NAMESPACE, req.getNamespace());
+ if ( !req.getAttributes().isEmpty() ) {
+ JsonObjectBuilder attrObj = Json.createObjectBuilder();
+ req.getAttributes().forEach((key, value) -> ManifestUtils.marshalAttribute(key, value, attrObj::add));
+ requirementObj.add(JSONConstants.REQCAP_ATTRIBUTES, attrObj.build());
+ }
+ if ( !req.getDirectives().isEmpty() ) {
+ JsonObjectBuilder reqObj = Json.createObjectBuilder();
+ req.getDirectives().forEach((key, value) -> ManifestUtils.marshalDirective(key, value, reqObj::add));
+ requirementObj.add(JSONConstants.REQCAP_DIRECTIVES, reqObj.build());
+ }
+ requirements.add(requirementObj.build());
+ }
+ ob.add(JSONConstants.FEATURE_REQUIREMENTS, requirements.build());
+ }
+
+ // capabilities
+ if ( !feature.getCapabilities().isEmpty() ) {
+ JsonArrayBuilder capabilities = Json.createArrayBuilder();
+
+ for(final Capability cap : feature.getCapabilities()) {
+ JsonObjectBuilder capabilityObj = Json.createObjectBuilder();
+ capabilityObj.add(JSONConstants.REQCAP_NAMESPACE, cap.getNamespace());
+ if ( !cap.getAttributes().isEmpty() ) {
+ JsonObjectBuilder attrObj = Json.createObjectBuilder();
+ cap.getAttributes().forEach((key, value) -> ManifestUtils.marshalAttribute(key, value, attrObj::add));
+ capabilityObj.add(JSONConstants.REQCAP_ATTRIBUTES, attrObj.build());
+ }
+ if ( !cap.getDirectives().isEmpty() ) {
+ JsonObjectBuilder reqObj = Json.createObjectBuilder();
+ cap.getDirectives().forEach((key, value) -> ManifestUtils.marshalDirective(key, value, reqObj::add));
+ capabilityObj.add(JSONConstants.REQCAP_DIRECTIVES, reqObj.build());
+ }
+ capabilities.add(capabilityObj.build());
+ }
+ ob.add(JSONConstants.FEATURE_CAPABILITIES, capabilities.build());
+ }
+
+ // bundles
+ writeBundles(ob, feature.getBundles(), feature.getConfigurations());
+
+ // configurations
+ final Configurations cfgs = new Configurations();
+ for(final Configuration cfg : feature.getConfigurations()) {
+ final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT);
+ if ( artifactProp == null ) {
+ cfgs.add(cfg);
+ }
+ }
+ writeConfigurations(ob, cfgs);
+
+ // framework properties
+ writeFrameworkProperties(ob, feature.getFrameworkProperties());
+
+ // extensions
+ writeExtensions(ob, feature.getExtensions(), feature.getConfigurations());
+
+ JsonWriterFactory writerFactory = Json.createWriterFactory(
+ Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true));
+ JsonWriter jw = writerFactory.createWriter(writer);
+ jw.writeObject(ob.build());
+ jw.close();
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/io/json/JSONConstants.java b/src/main/java/org/apache/sling/feature/io/json/JSONConstants.java
new file mode 100644
index 0000000..3b6f707
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/JSONConstants.java
@@ -0,0 +1,88 @@
+/*
+ * 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.json;
+
+import org.apache.sling.feature.Configuration;
+
+import java.util.Arrays;
+import java.util.List;
+
+public abstract class JSONConstants {
+
+ public static final String FEATURE_ID = "id";
+
+ public static final String FEATURE_VARIABLES = "variables";
+
+ public static final String FEATURE_BUNDLES = "bundles";
+
+ public static final String FEATURE_FRAMEWORK_PROPERTIES = "framework-properties";
+
+ public static final String FEATURE_CONFIGURATIONS = "configurations";
+
+ public static final String FEATURE_INCLUDES = "includes";
+
+ public static final String FEATURE_REQUIREMENTS = "requirements";
+
+ public static final String FEATURE_CAPABILITIES = "capabilities";
+
+ public static final String FEATURE_TITLE = "title";
+
+ public static final String FEATURE_DESCRIPTION = "description";
+
+ public static final String FEATURE_VENDOR = "vendor";
+
+ public static final String FEATURE_LICENSE = "license";
+
+ public static final String FEATURE_MODEL_VERSION = "model-version";
+
+ public static final List<String> FEATURE_KNOWN_PROPERTIES = Arrays.asList(FEATURE_ID,
+ FEATURE_MODEL_VERSION,
+ FEATURE_VARIABLES,
+ FEATURE_BUNDLES,
+ FEATURE_FRAMEWORK_PROPERTIES,
+ FEATURE_CONFIGURATIONS,
+ FEATURE_INCLUDES,
+ FEATURE_REQUIREMENTS,
+ FEATURE_CAPABILITIES,
+ FEATURE_TITLE,
+ FEATURE_DESCRIPTION,
+ FEATURE_VENDOR,
+ FEATURE_LICENSE);
+
+ public static final String ARTIFACT_ID = "id";
+
+ public static final List<String> ARTIFACT_KNOWN_PROPERTIES = Arrays.asList(ARTIFACT_ID,
+ Configuration.PROP_ARTIFACT,
+ FEATURE_CONFIGURATIONS);
+
+ public static final String INCLUDE_REMOVALS = "removals";
+
+ public static final String INCLUDE_EXTENSION_REMOVALS = "extensions";
+
+ public static final String REQCAP_NAMESPACE = "namespace";
+ public static final String REQCAP_ATTRIBUTES = "attributes";
+ public static final String REQCAP_DIRECTIVES = "directives";
+
+ public static final String APP_FRAMEWORK = "frameworkId";
+ public static final String APP_FEATURES = "features";
+
+ public static final List<String> APP_KNOWN_PROPERTIES = Arrays.asList(APP_FRAMEWORK,
+ FEATURE_BUNDLES,
+ FEATURE_FRAMEWORK_PROPERTIES,
+ FEATURE_CONFIGURATIONS,
+ APP_FEATURES);
+}
diff --git a/src/main/java/org/apache/sling/feature/io/json/JSONReaderBase.java b/src/main/java/org/apache/sling/feature/io/json/JSONReaderBase.java
new file mode 100644
index 0000000..fe3b89e
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/JSONReaderBase.java
@@ -0,0 +1,479 @@
+/*
+ * 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.json;
+
+import org.apache.felix.configurator.impl.json.JSMin;
+import org.apache.felix.configurator.impl.json.JSONUtil;
+import org.apache.felix.configurator.impl.json.TypeConverter;
+import org.apache.felix.configurator.impl.model.Config;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Extensions;
+import org.apache.sling.feature.KeyValueMap;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonStructure;
+import javax.json.JsonWriter;
+
+/**
+ * Common methods for JSON reading.
+ */
+abstract class JSONReaderBase {
+
+ /** The optional location. */
+ protected final String location;
+
+ /** Exception prefix containing the location (if set) */
+ protected final String exceptionPrefix;
+
+ /**
+ * Private constructor
+ * @param location Optional location
+ */
+ JSONReaderBase(final String location) {
+ this.location = location;
+ if ( location == null ) {
+ exceptionPrefix = "";
+ } else {
+ exceptionPrefix = location + " : ";
+ }
+ }
+
+ protected String minify(final Reader reader) throws IOException {
+ // minify JSON (remove comments)
+ final String contents;
+ try ( final Writer out = new StringWriter()) {
+ final JSMin min = new JSMin(reader, out);
+ min.jsmin();
+ contents = out.toString();
+ }
+ return contents;
+ }
+
+ /** Get the JSON object as a map, removing all comments that start with a '#' character
+ */
+ protected Map<String, Object> getJsonMap(JsonObject json) {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> m = (Map<String, Object>) JSONUtil.getValue(json);
+
+ removeComments(m);
+ return m;
+ }
+
+ private void removeComments(Map<String, Object> m) {
+ for(Iterator<Map.Entry<String, Object>> it = m.entrySet().iterator(); it.hasNext(); ) {
+ Entry<String, ?> entry = it.next();
+ if (entry.getKey().startsWith("#")) {
+ it.remove();
+ } else if (entry.getValue() instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> embedded = (Map<String, Object>) entry.getValue();
+ removeComments(embedded);
+ } else if (entry.getValue() instanceof Collection) {
+ Collection<?> embedded = (Collection<?>) entry.getValue();
+ removeComments(embedded);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void removeComments(Collection<?> embedded) {
+ for (Object el : embedded) {
+ if (el instanceof Collection) {
+ removeComments((Collection<?>) el);
+ } else if (el instanceof Map) {
+ removeComments((Map<String, Object>) el);
+ }
+ }
+ }
+
+ protected String getProperty(final Map<String, Object> map, final String key) throws IOException {
+ final Object val = map.get(key);
+ if ( val != null ) {
+ checkType(key, val, String.class);
+ return val.toString();
+ }
+ return null;
+ }
+
+ /**
+ * Read the bundles / start levels section
+ * @param map The map describing the feature
+ * @param container The bundles container
+ * @param configContainer The configurations container
+ * @throws IOException If the json is invalid.
+ */
+ protected void readBundles(
+ final Map<String, Object> map,
+ final Bundles container,
+ final Configurations configContainer) throws IOException {
+ if ( map.containsKey(JSONConstants.FEATURE_BUNDLES)) {
+ final Object bundlesObj = map.get(JSONConstants.FEATURE_BUNDLES);
+ checkType(JSONConstants.FEATURE_BUNDLES, bundlesObj, List.class);
+
+ final List<Artifact> list = new ArrayList<>();
+ readArtifacts(JSONConstants.FEATURE_BUNDLES, "bundle", list, bundlesObj, configContainer);
+
+ for(final Artifact a : list) {
+ Artifact sameFound = container.getSame(a.getId());
+ if ( sameFound != null) {
+ String str1 = a.getMetadata().get("run-modes");
+ String str2 = sameFound.getMetadata().get("run-modes");
+
+ if (str1 == null ? str2 == null : str1.equals(str2)) {
+ throw new IOException(exceptionPrefix + "Duplicate bundle " + a.getId().toMvnId());
+ }
+ }
+ try {
+ // check start order
+ a.getStartOrder();
+ } catch ( final IllegalArgumentException nfe) {
+ throw new IOException(exceptionPrefix + "Illegal start order '" + a.getMetadata().get(Artifact.KEY_START_ORDER) + "'");
+ }
+ container.add(a);
+ }
+ }
+ }
+
+ protected void readArtifacts(final String section,
+ final String artifactType,
+ final List<Artifact> artifacts,
+ final Object listObj,
+ final Configurations container)
+ throws IOException {
+ checkType(section, listObj, List.class);
+ @SuppressWarnings("unchecked")
+ final List<Object> list = (List<Object>) listObj;
+ for(final Object entry : list) {
+ final Artifact artifact;
+ checkType(artifactType, entry, Map.class, String.class);
+ if ( entry instanceof String ) {
+ artifact = new Artifact(ArtifactId.parse(handleResolveVars(entry).toString()));
+ } else {
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> bundleObj = (Map<String, Object>) entry;
+ if ( !bundleObj.containsKey(JSONConstants.ARTIFACT_ID) ) {
+ throw new IOException(exceptionPrefix + " " + artifactType + " is missing required artifact id");
+ }
+ checkType(artifactType + " " + JSONConstants.ARTIFACT_ID, bundleObj.get(JSONConstants.ARTIFACT_ID), String.class);
+ final ArtifactId id = ArtifactId.parse(handleResolveVars(bundleObj.get(JSONConstants.ARTIFACT_ID)).toString());
+
+ artifact = new Artifact(id);
+ for(final Map.Entry<String, Object> metadataEntry : bundleObj.entrySet()) {
+ final String key = metadataEntry.getKey();
+ if ( JSONConstants.ARTIFACT_KNOWN_PROPERTIES.contains(key) ) {
+ continue;
+ }
+ checkType(artifactType + " metadata " + key, metadataEntry.getValue(), String.class, Number.class, Boolean.class);
+ artifact.getMetadata().put(key, metadataEntry.getValue().toString());
+ }
+ if ( bundleObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
+ checkType(artifactType + " configurations", bundleObj.get(JSONConstants.FEATURE_CONFIGURATIONS), Map.class);
+ List<Configuration> bundleConfigs = addConfigurations(bundleObj, artifact, container);
+ artifact.getMetadata().put(JSONConstants.FEATURE_CONFIGURATIONS, bundleConfigs);
+ }
+ }
+ artifacts.add(artifact);
+ }
+ }
+
+ /** Substitutes variables that need to be specified before the resolver executes.
+ * These are variables in features, artifacts (such as bundles), requirements
+ * and capabilities.
+ * @param val The value that may contain a variable.
+ * @return The value with the variable substitiuted.
+ */
+ protected Object handleResolveVars(Object val) {
+ // No variable substitution at this level, but subclasses can add this in
+ return val;
+ }
+
+ /** Substitutes variables that need to be substituted at launch time.
+ * These are all variables that are not needed by the resolver.
+ * @param val The value that may contain a variable.
+ * @return The value with the variable substitiuted.
+ */
+ protected Object handleLaunchVars(Object val) {
+ // No variable substitution at this level, but subclasses can add this in
+ return val;
+ }
+
+ protected List<Configuration> addConfigurations(final Map<String, Object> map,
+ final Artifact artifact,
+ final Configurations container) throws IOException {
+ final JSONUtil.Report report = new JSONUtil.Report();
+ @SuppressWarnings("unchecked")
+ final List<Config> configs = JSONUtil.readConfigurationsJSON(new TypeConverter(null),
+ 0, "", (Map<String, ?>)map.get(JSONConstants.FEATURE_CONFIGURATIONS), report);
+ if ( !report.errors.isEmpty() || !report.warnings.isEmpty() ) {
+ final StringBuilder builder = new StringBuilder(exceptionPrefix);
+ builder.append("Errors in configurations:");
+ for(final String w : report.warnings) {
+ builder.append("\n");
+ builder.append(w);
+ }
+ for(final String e : report.errors) {
+ builder.append("\n");
+ builder.append(e);
+ }
+ throw new IOException(builder.toString());
+ }
+
+ List<Configuration> newConfigs = new ArrayList<>();
+ for(final Config c : configs) {
+ final int pos = c.getPid().indexOf('~');
+ final Configuration config;
+ if ( pos != -1 ) {
+ config = new Configuration(c.getPid().substring(0, pos), c.getPid().substring(pos + 1));
+ } else {
+ config = new Configuration(c.getPid());
+ }
+ final Enumeration<String> keyEnum = c.getProperties().keys();
+ while ( keyEnum.hasMoreElements() ) {
+ final String key = keyEnum.nextElement();
+ if ( key.startsWith(":configurator:") ) {
+ throw new IOException(exceptionPrefix + "Configuration must not define configurator property " + key);
+ }
+ final Object val = c.getProperties().get(key);
+ config.getProperties().put(key, handleLaunchVars(val));
+ }
+ if ( config.getProperties().get(Configuration.PROP_ARTIFACT) != null ) {
+ throw new IOException(exceptionPrefix + "Configuration must not define property " + Configuration.PROP_ARTIFACT);
+ }
+ if ( artifact != null ) {
+ config.getProperties().put(Configuration.PROP_ARTIFACT, artifact.getId().toMvnId());
+ }
+ for(final Configuration current : container) {
+ if ( current.equals(config) ) {
+ throw new IOException(exceptionPrefix + "Duplicate configuration " + config);
+ }
+ }
+ container.add(config);
+ newConfigs.add(config);
+ }
+ return newConfigs;
+ }
+
+
+ protected void readConfigurations(final Map<String, Object> map,
+ final Configurations container) throws IOException {
+ if ( map.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
+ checkType(JSONConstants.FEATURE_CONFIGURATIONS, map.get(JSONConstants.FEATURE_CONFIGURATIONS), Map.class);
+ addConfigurations(map, null, container);
+ }
+ }
+
+ protected void readFrameworkProperties(final Map<String, Object> map,
+ final KeyValueMap container) throws IOException {
+ if ( map.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) {
+ final Object propsObj= map.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
+ checkType(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES, propsObj, Map.class);
+
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> props = (Map<String, Object>) propsObj;
+ for(final Map.Entry<String, Object> entry : props.entrySet()) {
+ checkType("framework property value", entry.getValue(), String.class, Boolean.class, Number.class);
+ if ( container.get(entry.getKey()) != null ) {
+ throw new IOException(this.exceptionPrefix + "Duplicate framework property " + entry.getKey());
+ }
+ container.put(entry.getKey(), handleLaunchVars(entry.getValue()).toString());
+ }
+
+ }
+ }
+
+ protected void readExtensions(final Map<String, Object> map,
+ final List<String> keywords,
+ final Extensions container,
+ final Configurations configContainer) throws IOException {
+ final Set<String> keySet = new HashSet<>(map.keySet());
+ keySet.removeAll(keywords);
+ // the remaining keys are considered extensions!
+ for(final String key : keySet) {
+ final int pos = key.indexOf(':');
+ final String postfix = pos == -1 ? null : key.substring(pos + 1);
+ final int sep = (postfix == null ? key.indexOf('|') : postfix.indexOf('|'));
+ final String name;
+ final String type;
+ final String optional;
+ if ( pos == -1 ) {
+ type = ExtensionType.ARTIFACTS.name();
+ if ( sep == -1 ) {
+ name = key;
+ optional = Boolean.FALSE.toString();
+ } else {
+ name = key.substring(0, sep);
+ optional = key.substring(sep + 1);
+ }
+ } else {
+ name = key.substring(0, pos);
+ if ( sep == -1 ) {
+ type = postfix;
+ optional = Boolean.FALSE.toString();
+ } else {
+ type = postfix.substring(0, sep);
+ optional = postfix.substring(sep + 1);
+ }
+ }
+ if ( JSONConstants.APP_KNOWN_PROPERTIES.contains(name) ) {
+ throw new IOException(this.exceptionPrefix + "Extension is using reserved name : " + name);
+ }
+ if ( JSONConstants.FEATURE_KNOWN_PROPERTIES.contains(name) ) {
+ throw new IOException(this.exceptionPrefix + "Extension is using reserved name : " + name);
+ }
+ if ( container.getByName(name) != null ) {
+ throw new IOException(exceptionPrefix + "Duplicate extension with name " + name);
+ }
+
+ final ExtensionType extType = ExtensionType.valueOf(type);
+ final boolean opt = Boolean.valueOf(optional).booleanValue();
+
+ final Extension ext = new Extension(extType, name, opt);
+ final Object value = map.get(key);
+ switch ( extType ) {
+ case ARTIFACTS : final List<Artifact> list = new ArrayList<>();
+ readArtifacts("Extension " + name, "artifact", list, value, configContainer);
+ for(final Artifact a : list) {
+ if ( ext.getArtifacts().contains(a) ) {
+ throw new IOException(exceptionPrefix + "Duplicate artifact in extension " + name + " : " + a.getId().toMvnId());
+ }
+ ext.getArtifacts().add(a);
+ }
+ break;
+ case JSON : checkType("JSON Extension " + name, value, Map.class, List.class);
+ final JsonStructure struct = build(value);
+ try ( final StringWriter w = new StringWriter()) {
+ final JsonWriter jw = Json.createWriter(w);
+ jw.write(struct);
+ w.flush();
+ ext.setJSON(w.toString());
+ }
+ break;
+ case TEXT : checkType("Text Extension " + name, value, String.class, List.class);
+ if ( value instanceof String ) {
+ // string
+ ext.setText(value.toString());
+ } else {
+ // list (array of strings)
+ @SuppressWarnings("unchecked")
+ final List<Object> l = (List<Object>)value;
+ final StringBuilder sb = new StringBuilder();
+ for(final Object o : l) {
+ checkType("Text Extension " + name + ", value " + o, o, String.class);
+ sb.append(o.toString());
+ sb.append('\n');
+ }
+ ext.setText(sb.toString());
+ }
+ break;
+ }
+
+ container.add(ext);
+ }
+ }
+
+ private JsonStructure build(final Object value) {
+ if ( value instanceof List ) {
+ @SuppressWarnings("unchecked")
+ final List<Object> list = (List<Object>)value;
+ final JsonArrayBuilder builder = Json.createArrayBuilder();
+ for(final Object obj : list) {
+ if ( obj instanceof String ) {
+ builder.add(obj.toString());
+ } else if ( obj instanceof Long ) {
+ builder.add((Long)obj);
+ } else if ( obj instanceof Double ) {
+ builder.add((Double)obj);
+ } else if (obj instanceof Boolean ) {
+ builder.add((Boolean)obj);
+ } else if ( obj instanceof Map ) {
+ builder.add(build(obj));
+ } else if ( obj instanceof List ) {
+ builder.add(build(obj));
+ }
+
+ }
+ return builder.build();
+ } else if ( value instanceof Map ) {
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> map = (Map<String, Object>)value;
+ final JsonObjectBuilder builder = Json.createObjectBuilder();
+ for(final Map.Entry<String, Object> entry : map.entrySet()) {
+ if ( entry.getValue() instanceof String ) {
+ builder.add(entry.getKey(), entry.getValue().toString());
+ } else if ( entry.getValue() instanceof Long ) {
+ builder.add(entry.getKey(), (Long)entry.getValue());
+ } else if ( entry.getValue() instanceof Double ) {
+ builder.add(entry.getKey(), (Double)entry.getValue());
+ } else if ( entry.getValue() instanceof Boolean ) {
+ builder.add(entry.getKey(), (Boolean)entry.getValue());
+ } else if ( entry.getValue() instanceof Map ) {
+ builder.add(entry.getKey(), build(entry.getValue()));
+ } else if ( entry.getValue() instanceof List ) {
+ builder.add(entry.getKey(), build(entry.getValue()));
+ }
+ }
+ return builder.build();
+ }
+ return null;
+ }
+
+ /**
+ * Check if the value is one of the provided types
+ * @param key A key for the error message
+ * @param val The value to check
+ * @param types The allowed types
+ * @throws IOException If the val is not of the specified types
+ */
+ protected void checkType(final String key, final Object val, Class<?>...types) throws IOException {
+ boolean valid = false;
+ for(final Class<?> c : types) {
+ if ( c.isInstance(val) ) {
+ valid = true;
+ break;
+ }
+ }
+ if ( !valid ) {
+ throw new IOException(this.exceptionPrefix + "Key " + key + " is not one of the allowed types " + Arrays.toString(types) + " : " + val.getClass());
+ }
+ }
+}
+
+
diff --git a/src/main/java/org/apache/sling/feature/io/json/JSONWriterBase.java b/src/main/java/org/apache/sling/feature/io/json/JSONWriterBase.java
new file mode 100644
index 0000000..ff502af
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/JSONWriterBase.java
@@ -0,0 +1,259 @@
+/*
+ * 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.json;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.KeyValueMap;
+
+import java.io.StringReader;
+import java.lang.reflect.Array;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonStructure;
+
+/**
+ * Common functionality for writing JSON
+ */
+abstract class JSONWriterBase {
+ protected void writeBundles(final JsonObjectBuilder ob,
+ final Bundles bundles,
+ final Configurations allConfigs) {
+ // bundles
+ if ( !bundles.isEmpty() ) {
+ JsonArrayBuilder bundleArray = Json.createArrayBuilder();
+
+ for(final Artifact artifact : bundles) {
+ final Configurations cfgs = new Configurations();
+ for(final Configuration cfg : allConfigs) {
+ String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT);
+ if (artifactProp != null) {
+ if (artifactProp.startsWith("mvn:")) {
+ // Change Maven URL to maven GAV syntax
+ artifactProp = artifactProp.substring("mvn:".length());
+ artifactProp = artifactProp.replace('/', ':');
+ }
+ if (artifact.getId().toMvnId().equals(artifactProp)) {
+ cfgs.add(cfg);
+ }
+ }
+ }
+ KeyValueMap md = artifact.getMetadata();
+ if ( md.isEmpty() && cfgs.isEmpty() ) {
+ bundleArray.add(artifact.getId().toMvnId());
+ } else {
+ JsonObjectBuilder bundleObj = Json.createObjectBuilder();
+ bundleObj.add(JSONConstants.ARTIFACT_ID, artifact.getId().toMvnId());
+
+ if (md.get("start-level") == null) {
+ String so = md.get("start-order");
+ if (so != null) {
+ md.put("start-level", so);
+ }
+ }
+
+ Object runmodes = md.remove("runmodes");
+ if (runmodes instanceof String) {
+ md.put("run-modes", runmodes);
+ }
+
+ for(final Map.Entry<String, String> me : md) {
+ bundleObj.add(me.getKey(), me.getValue());
+ }
+
+ writeConfigurations(bundleObj, cfgs);
+
+ bundleArray.add(bundleObj.build());
+ }
+ }
+ ob.add(JSONConstants.FEATURE_BUNDLES, bundleArray.build());
+ }
+ }
+
+ /**
+ * Write the list of configurations into a "configurations" element
+ * @param ob The json generator
+ * @param cfgs The list of configurations
+ */
+ protected void writeConfigurations(final JsonObjectBuilder ob, final Configurations cfgs) {
+ if ( !cfgs.isEmpty() ) {
+ ob.add(JSONConstants.FEATURE_CONFIGURATIONS,
+ writeConfigurationsMap(cfgs));
+ }
+ }
+
+ /**
+ * Write the list of configurations into a "configurations" element
+ * @param w The json generator
+ * @param cfgs The list of configurations
+ * @return
+ */
+ protected JsonObject writeConfigurationsMap(final Configurations cfgs) {
+ JsonObjectBuilder configObj = Json.createObjectBuilder();
+ for(final Configuration cfg : cfgs) {
+ final String key;
+ if ( cfg.isFactoryConfiguration() ) {
+ key = cfg.getFactoryPid() + "~" + cfg.getName();
+ } else {
+ key = cfg.getPid();
+ }
+ JsonObjectBuilder cfgValObj = Json.createObjectBuilder();
+
+ final Enumeration<String> e = cfg.getProperties().keys();
+ while ( e.hasMoreElements() ) {
+ final String name = e.nextElement();
+ if ( Configuration.PROP_ARTIFACT.equals(name) ) {
+ continue;
+ }
+
+ final Object val = cfg.getProperties().get(name);
+
+ String typePostFix = null;
+ final Object typeCheck;
+ if ( val.getClass().isArray() ) {
+ if ( Array.getLength(val) > 0 ) {
+ typeCheck = Array.get(val, 0);
+ } else {
+ typeCheck = null;
+ }
+ } else {
+ typeCheck = val;
+ }
+
+ if ( typeCheck instanceof Integer ) {
+ typePostFix = ":Integer";
+ } else if ( typeCheck instanceof Byte ) {
+ typePostFix = ":Byte";
+ } else if ( typeCheck instanceof Character ) {
+ typePostFix = ":Character";
+ } else if ( typeCheck instanceof Float ) {
+ typePostFix = ":Float";
+ }
+
+ if ( val.getClass().isArray() ) {
+ JsonArrayBuilder ab = Json.createArrayBuilder();
+ for(int i=0; i<Array.getLength(val);i++ ) {
+ final Object obj = Array.get(val, i);
+ if ( typePostFix == null ) {
+ if ( obj instanceof String ) {
+ ab.add((String)obj);
+ } else if ( obj instanceof Boolean ) {
+ ab.add((Boolean)obj);
+ } else if ( obj instanceof Long ) {
+ ab.add((Long)obj);
+ } else if ( obj instanceof Double ) {
+ ab.add((Double)obj);
+ }
+ } else {
+ ab.add(obj.toString());
+ }
+ }
+ cfgValObj.add(name, ab.build());
+ } else {
+ if ( typePostFix == null ) {
+ if ( val instanceof String ) {
+ cfgValObj.add(name, (String)val);
+ } else if ( val instanceof Boolean ) {
+ cfgValObj.add(name, (Boolean)val);
+ } else if ( val instanceof Long ) {
+ cfgValObj.add(name, (Long)val);
+ } else if ( val instanceof Double ) {
+ cfgValObj.add(name, (Double)val);
+ }
+ } else {
+ cfgValObj.add(name + typePostFix, val.toString());
+ }
+ }
+ }
+ configObj.add(key, cfgValObj.build());
+ }
+ return configObj.build();
+ }
+
+ protected void writeVariables(final JsonObjectBuilder ob, final KeyValueMap vars) {
+ if ( !vars.isEmpty()) {
+ JsonObjectBuilder varsObj = Json.createObjectBuilder();
+ for (final Map.Entry<String, String> entry : vars) {
+ varsObj.add(entry.getKey(), entry.getValue());
+ }
+ ob.add(JSONConstants.FEATURE_VARIABLES, varsObj.build());
+ }
+ }
+
+ protected void writeFrameworkProperties(final JsonObjectBuilder ob, final KeyValueMap props) {
+ // framework properties
+ if ( !props.isEmpty() ) {
+ JsonObjectBuilder propsObj = Json.createObjectBuilder();
+ for(final Map.Entry<String, String> entry : props) {
+ propsObj.add(entry.getKey(), entry.getValue());
+ }
+ ob.add(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES, propsObj.build());
+ }
+ }
+
+ protected void writeExtensions(final JsonObjectBuilder ob,
+ final List<Extension> extensions,
+ final Configurations allConfigs) {
+ for(final Extension ext : extensions) {
+ final String key = ext.getName() + ":" + ext.getType().name() + "|" + ext.isOptional();
+ if ( ext.getType() == ExtensionType.JSON ) {
+ final JsonStructure struct;
+ try ( final StringReader reader = new StringReader(ext.getJSON()) ) {
+ struct = Json.createReader(reader).read();
+ }
+ ob.add(key, struct);
+ } else if ( ext.getType() == ExtensionType.TEXT ) {
+ ob.add(key, ext.getText());
+ } else {
+ JsonArrayBuilder extensionArr = Json.createArrayBuilder();
+ for(final Artifact artifact : ext.getArtifacts()) {
+ final Configurations artifactCfgs = new Configurations();
+ for(final Configuration cfg : allConfigs) {
+ final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT);
+ if ( artifact.getId().toMvnId().equals(artifactProp) ) {
+ artifactCfgs.add(cfg);
+ }
+ }
+ if ( artifact.getMetadata().isEmpty() && artifactCfgs.isEmpty() ) {
+ extensionArr.add(artifact.getId().toMvnId());
+ } else {
+ JsonObjectBuilder extObj = Json.createObjectBuilder();
+ extObj.add(JSONConstants.ARTIFACT_ID, artifact.getId().toMvnId());
+
+ for(final Map.Entry<String, String> me : artifact.getMetadata()) {
+ extObj.add(me.getKey(), me.getValue());
+ }
+
+ writeConfigurations(ob, artifactCfgs);
+ extensionArr.add(extObj.build());
+ }
+ }
+ ob.add(key, extensionArr.build());
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/io/json/ManifestUtils.java b/src/main/java/org/apache/sling/feature/io/json/ManifestUtils.java
new file mode 100644
index 0000000..ee6585f
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/ManifestUtils.java
@@ -0,0 +1,515 @@
+/*
+ * 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.json;
+
+import org.apache.felix.utils.resource.CapabilityImpl;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Version;
+import org.osgi.resource.Capability;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+// This class can be picked up from Felix Utils once it has been moved there. At that point
+// this class can be removed.
+class ManifestUtils {
+ public static void unmarshalAttribute(String key, Object value, BiConsumer<String, Object> sink) throws IOException {
+ unmarshal(key + "=" + value, Capability::getAttributes, sink);
+ }
+
+ public static void unmarshalDirective(String key, Object value, BiConsumer<String, String> sink) throws IOException {
+ unmarshal(key + ":=" + value, Capability::getDirectives, sink);
+ }
+
+ private static <T> void unmarshal(String header, Function<Capability, Map<String, T>> lookup, BiConsumer<String, T> sink) throws IOException {
+ try {
+ convertProvideCapabilities(
+ normalizeCapabilityClauses(parseStandardHeader("foo;" + header), "2"))
+ .forEach(capability -> lookup.apply(capability).forEach(sink));
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ }
+
+ public static void marshalAttribute(String key, Object value, BiConsumer<String, String> sink) {
+ marshal(key, value, sink);
+ }
+
+ public static void marshalDirective(String key, Object value, BiConsumer<String, String> sink) {
+ marshal(key, value, sink);
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private static void marshal(String key, Object value, BiConsumer<String, String> sink) {
+ StringBuilder keyBuilder = new StringBuilder(key);
+ if (value instanceof List) {
+ List list = (List) value;
+ keyBuilder.append(":List");
+ if (!list.isEmpty()) {
+ String type = type(list.get(0));
+ if (!type.equals("String")) {
+ keyBuilder.append('<').append(type).append('>');
+ }
+ value = list.stream().map(
+ v -> v.toString().replace(",", "\\,")
+ ).collect(Collectors.joining(","));
+ }
+ else {
+ value = "";
+ }
+ }
+ else {
+ String type = type(value);
+ if (!type.equals("String")) {
+ keyBuilder.append(':').append(type);
+ }
+ }
+ sink.accept(keyBuilder.toString(), value.toString());
+ }
+
+ private static String type(Object value) {
+ if (value instanceof Long) {
+ return "Long";
+ }
+ else if (value instanceof Double)
+ {
+ return "Double";
+ }
+ else if (value instanceof Version)
+ {
+ return "Version";
+ }
+ else
+ {
+ return "String";
+ }
+ }
+
+ public static List<Capability> convertProvideCapabilities(
+ List<ParsedHeaderClause> clauses)
+ throws BundleException
+ {
+ List<Capability> capList = new ArrayList<>();
+ for (ParsedHeaderClause clause : clauses)
+ {
+ for (String path : clause.m_paths)
+ {
+ if (path.startsWith("osgi.wiring."))
+ {
+ throw new BundleException("Manifest cannot use Provide-Capability for '"
+ + path
+ + "' namespace.");
+ }
+
+ Capability capability = new CapabilityImpl(null, path, clause.m_dirs, clause.m_attrs);
+ // Create package capability and add to capability list.
+ capList.add(capability);
+ }
+ }
+
+ return capList;
+ }
+
+ public static List<ParsedHeaderClause> normalizeCapabilityClauses(
+ List<ParsedHeaderClause> clauses, String mv)
+ throws BundleException
+ {
+
+ if (!mv.equals("2") && !clauses.isEmpty())
+ {
+ // Should we error here if we are not an R4 bundle?
+ }
+
+ // Convert attributes into specified types.
+ for (ParsedHeaderClause clause : clauses)
+ {
+ for (Entry<String, String> entry : clause.m_types.entrySet())
+ {
+ String type = entry.getValue();
+ if (!type.equals("String"))
+ {
+ if (type.equals("Double"))
+ {
+ clause.m_attrs.put(
+ entry.getKey(),
+ new Double(clause.m_attrs.get(entry.getKey()).toString().trim()));
+ }
+ else if (type.equals("Version"))
+ {
+ clause.m_attrs.put(
+ entry.getKey(),
+ new Version(clause.m_attrs.get(entry.getKey()).toString().trim()));
+ }
+ else if (type.equals("Long"))
+ {
+ clause.m_attrs.put(
+ entry.getKey(),
+ new Long(clause.m_attrs.get(entry.getKey()).toString().trim()));
+ }
+ else if (type.startsWith("List"))
+ {
+ int startIdx = type.indexOf('<');
+ int endIdx = type.indexOf('>');
+ if (((startIdx > 0) && (endIdx <= startIdx))
+ || ((startIdx < 0) && (endIdx > 0)))
+ {
+ throw new BundleException(
+ "Invalid Provide-Capability attribute list type for '"
+ + entry.getKey()
+ + "' : "
+ + type);
+ }
+
+ String listType = "String";
+ if (endIdx > startIdx)
+ {
+ listType = type.substring(startIdx + 1, endIdx).trim();
+ }
+
+ List<String> tokens = parseDelimitedString(
+ clause.m_attrs.get(entry.getKey()).toString(), ",", false);
+ List<Object> values = new ArrayList<>(tokens.size());
+ for (String token : tokens)
+ {
+ if (listType.equals("String"))
+ {
+ values.add(token);
+ }
+ else if (listType.equals("Double"))
+ {
+ values.add(new Double(token.trim()));
+ }
+ else if (listType.equals("Version"))
+ {
+ values.add(new Version(token.trim()));
+ }
+ else if (listType.equals("Long"))
+ {
+ values.add(new Long(token.trim()));
+ }
+ else
+ {
+ throw new BundleException(
+ "Unknown Provide-Capability attribute list type for '"
+ + entry.getKey()
+ + "' : "
+ + type);
+ }
+ }
+ clause.m_attrs.put(
+ entry.getKey(),
+ values);
+ }
+ else
+ {
+ throw new BundleException(
+ "Unknown Provide-Capability attribute type for '"
+ + entry.getKey()
+ + "' : "
+ + type);
+ }
+ }
+ }
+ }
+
+ return clauses;
+ }
+
+ 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" })
+ public static List<ParsedHeaderClause> parseStandardHeader(String header)
+ {
+ List<ParsedHeaderClause> clauses = new ArrayList<>();
+ 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<>(),
+ new HashMap<>(),
+ new HashMap<>(),
+ new HashMap<>());
+ 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;
+ }
+
+ /**
+ * Parses delimited string and returns an array containing the tokens. This
+ * parser obeys quotes, so the delimiter character will be ignored if it is
+ * inside of a quote. This method assumes that the quote character is not
+ * included in the set of delimiter characters.
+ * @param value the delimited string to parse.
+ * @param delim the characters delimiting the tokens.
+ * @return a list of string or an empty list if there are none.
+ **/
+ public static List<String> parseDelimitedString(String value, String delim, boolean trim)
+ {
+ if (value == null)
+ {
+ value = "";
+ }
+
+ List<String> list = new ArrayList<>();
+
+ int CHAR = 1;
+ int DELIMITER = 2;
+ int STARTQUOTE = 4;
+ int ENDQUOTE = 8;
+
+ StringBuffer sb = new StringBuffer();
+
+ int expecting = (CHAR | DELIMITER | STARTQUOTE);
+
+ boolean isEscaped = false;
+ for (int i = 0; i < value.length(); i++)
+ {
+ char c = value.charAt(i);
+
+ boolean isDelimiter = (delim.indexOf(c) >= 0);
+
+ if (!isEscaped && (c == '\\'))
+ {
+ isEscaped = true;
+ continue;
+ }
+
+ if (isEscaped)
+ {
+ sb.append(c);
+ }
+ else if (isDelimiter && ((expecting & DELIMITER) > 0))
+ {
+ if (trim)
+ {
+ list.add(sb.toString().trim());
+ }
+ else
+ {
+ list.add(sb.toString());
+ }
+ sb.delete(0, sb.length());
+ expecting = (CHAR | DELIMITER | STARTQUOTE);
+ }
+ else if ((c == '"') && ((expecting & STARTQUOTE) > 0))
+ {
+ sb.append(c);
+ expecting = CHAR | ENDQUOTE;
+ }
+ else if ((c == '"') && ((expecting & ENDQUOTE) > 0))
+ {
+ sb.append(c);
+ expecting = (CHAR | STARTQUOTE | DELIMITER);
+ }
+ else if ((expecting & CHAR) > 0)
+ {
+ sb.append(c);
+ }
+ else
+ {
+ throw new IllegalArgumentException("Invalid delimited string: " + value);
+ }
+
+ isEscaped = false;
+ }
+
+ if (sb.length() > 0)
+ {
+ if (trim)
+ {
+ list.add(sb.toString().trim());
+ }
+ else
+ {
+ list.add(sb.toString());
+ }
+ }
+
+ return list;
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/feature/io/json/package-info.java b/src/main/java/org/apache/sling/feature/io/json/package-info.java
new file mode 100644
index 0000000..6b84931
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/json/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.json;
+
+
diff --git a/src/main/java/org/apache/sling/feature/io/package-info.java b/src/main/java/org/apache/sling/feature/io/package-info.java
new file mode 100644
index 0000000..25e2f46
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/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;
+
+
diff --git a/src/main/java/org/apache/sling/feature/io/spi/ArtifactProvider.java b/src/main/java/org/apache/sling/feature/io/spi/ArtifactProvider.java
new file mode 100644
index 0000000..5f250d7
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/spi/ArtifactProvider.java
@@ -0,0 +1,55 @@
+/*
+ * 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.spi;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * The artifact provider is an extension point for providing artifacts
+ * from different sources, like for example s3.
+ */
+public interface ArtifactProvider {
+
+ /**
+ * The protocol name of the provider, e.g. "s3"
+ * @return The protocol name.
+ */
+ String getProtocol();
+
+ /**
+ * Initialize the provider.
+ * @param context The context
+ * @throws IOException If the provider can't be initialized.
+ */
+ void init(ArtifactProviderContext context) throws IOException;
+
+ /**
+ * Shutdown the provider.
+ */
+ void shutdown();
+
+ /**
+ * Get a local file for the artifact URL.
+ *
+ * @param url Artifact url
+ * @param relativeCachePath A relative path that can be used as a cache path
+ * by the provider. The path does not start with a slash.
+ * @return A file if the artifact exists or {@code null}
+ */
+ File getArtifact(String url, String relativeCachePath);
+}
diff --git a/src/main/java/org/apache/sling/feature/io/spi/ArtifactProviderContext.java b/src/main/java/org/apache/sling/feature/io/spi/ArtifactProviderContext.java
new file mode 100644
index 0000000..c837548
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/spi/ArtifactProviderContext.java
@@ -0,0 +1,46 @@
+/*
+ * 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.spi;
+
+import java.io.File;
+
+/**
+ * This is the context for the artifact providers
+ */
+public interface ArtifactProviderContext {
+
+ /**
+ * Get the cache directory
+ * @return The cache directory.
+ */
+ File getCacheDirectory();
+
+ /**
+ * Inform about an artifact found in the cache.
+ */
+ void incCachedArtifacts();
+
+ /**
+ * Inform about an artifact being downloaded
+ */
+ void incDownloadedArtifacts();
+
+ /**
+ * Inform about an artifact found locally.
+ */
+ void incLocalArtifacts();
+}
diff --git a/src/main/java/org/apache/sling/feature/io/spi/package-info.java b/src/main/java/org/apache/sling/feature/io/spi/package-info.java
new file mode 100644
index 0000000..b69692b
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/spi/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.spi;
+
+
diff --git a/src/test/java/org/apache/sling/feature/io/ArtifactManagerTest.java b/src/test/java/org/apache/sling/feature/io/ArtifactManagerTest.java
new file mode 100644
index 0000000..f89c40b
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/io/ArtifactManagerTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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;
+
+import org.apache.sling.feature.io.ArtifactHandler;
+import org.apache.sling.feature.io.ArtifactManager;
+import org.apache.sling.feature.io.ArtifactManagerConfig;
+import org.apache.sling.feature.io.spi.ArtifactProvider;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ArtifactManagerTest {
+
+ private static final String METADATA = "<metadata modelVersion=\"1.1.0\">\n" +
+ "<groupId>org.apache.sling.samples</groupId>\n" +
+ "<artifactId>slingshot</artifactId>\n" +
+ "<version>0-DEFAULT-SNAPSHOT</version>\n" +
+ "<versioning>\n" +
+ "<snapshot>\n" +
+ "<timestamp>20160321.103951</timestamp>\n" +
+ "<buildNumber>1</buildNumber>\n" +
+ "</snapshot>\n" +
+ "<lastUpdated>20160321103951</lastUpdated>\n" +
+ "<snapshotVersions>\n" +
+ "<snapshotVersion>\n" +
+ "<extension>txt</extension>\n" +
+ "<value>0-DEFAULT-20160321.103951-1</value>\n" +
+ "<updated>20160321103951</updated>\n" +
+ "</snapshotVersion>\n" +
+ "<snapshotVersion>\n" +
+ "<extension>pom</extension>\n" +
+ "<value>0-DEFAULT-20160321.103951-1</value>\n" +
+ "<updated>20160321103951</updated>\n" +
+ "</snapshotVersion>\n" +
+ "</snapshotVersions>\n" +
+ "</versioning></metadata>";
+
+ @Test public void testMetadataParsing() {
+ final String version = ArtifactManager.getLatestSnapshot(METADATA);
+ assertEquals("20160321.103951-1", version);
+ }
+
+ @Test public void testSnapshotHandling() throws IOException {
+ final String REPO = "http://org.apache.sling";
+ final ArtifactManagerConfig config = mock(ArtifactManagerConfig.class);
+ when(config.getRepositoryUrls()).thenReturn(new String[] {REPO});
+
+ final File metadataFile = mock(File.class);
+ when(metadataFile.exists()).thenReturn(true);
+ when(metadataFile.getPath()).thenReturn("/maven-metadata.xml");
+
+ final File artifactFile = mock(File.class);
+ when(artifactFile.exists()).thenReturn(true);
+
+ final ArtifactProvider provider = mock(ArtifactProvider.class);
+ when(provider.getArtifact(REPO + "/group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-SNAPSHOT.txt", "group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-SNAPSHOT.txt")).thenReturn(null);
+ when(provider.getArtifact(REPO + "/group/artifact/1.0.0-SNAPSHOT/maven-metadata.xml", "org.apache.sling/group/artifact/1.0.0-SNAPSHOT/maven-metadata.xml")).thenReturn(metadataFile);
+ when(provider.getArtifact(REPO + "/group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-20160321.103951-1.txt", "group/artifact/1.0.0-SNAPSHOT/artifact-1.0.0-SNAPSHOT.txt")).thenReturn(artifactFile);
+
+ final Map<String, ArtifactProvider> providers = new HashMap<>();
+ providers.put("*", provider);
+
+ final ArtifactManager mgr = new ArtifactManager(config, providers) {
+
+ @Override
+ protected String getFileContents(final ArtifactHandler handler) throws IOException {
+ final String path = handler.getFile().getPath();
+ if ( "/maven-metadata.xml".equals(path) ) {
+ return METADATA;
+ }
+ return super.getFileContents(handler);
+ }
+ };
+
+ final ArtifactHandler handler = mgr.getArtifactHandler("mvn:group/artifact/1.0.0-SNAPSHOT/txt");
+ assertNotNull(handler);
+ assertEquals(artifactFile, handler.getFile());
+ }
+}
diff --git a/src/test/java/org/apache/sling/feature/io/FeatureUtilTest.java b/src/test/java/org/apache/sling/feature/io/FeatureUtilTest.java
new file mode 100644
index 0000000..e06b175
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/io/FeatureUtilTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sling.feature.io.FileUtils;
+import org.junit.Test;
+
+public class FeatureUtilTest {
+
+ @Test public void testFileSort() {
+ final String[] files = new String[] {
+ "/different/path/app.json",
+ "/path/to/base.json",
+ "/path/to/feature.json",
+ "/path/to/amode/feature.json",
+ "/path/to/later/feature.json",
+ "http://sling.apache.org/features/one.json",
+ "http://sling.apache.org/features/two.json",
+ "http://sling.apache.org/features/amode/feature.json"
+ };
+
+ final List<String> l = new ArrayList<>(Arrays.asList(files));
+ Collections.sort(l, FileUtils.FEATURE_PATH_COMP);
+ for(int i=0; i<files.length; i++) {
+ assertEquals(files[i], l.get(i));
+ }
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/feature/io/json/FeatureJSONReaderTest.java b/src/test/java/org/apache/sling/feature/io/json/FeatureJSONReaderTest.java
new file mode 100644
index 0000000..47ab83b
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/io/json/FeatureJSONReaderTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.json;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.Extensions;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Include;
+import org.apache.sling.feature.KeyValueMap;
+import org.apache.sling.feature.io.json.FeatureJSONReader;
+import org.apache.sling.feature.io.json.FeatureJSONReader.SubstituteVariables;
+import org.junit.Test;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class FeatureJSONReaderTest {
+
+ @Test public void testRead() throws Exception {
+ final Feature feature = U.readFeature("test");
+ assertNotNull(feature);
+ assertNotNull(feature.getId());
+ assertEquals("org.apache.sling", feature.getId().getGroupId());
+ assertEquals("test-feature", feature.getId().getArtifactId());
+ assertEquals("1.1", feature.getId().getVersion());
+ assertEquals("jar", feature.getId().getType());
+ assertNull(feature.getId().getClassifier());
+
+ assertEquals(2, feature.getConfigurations().size());
+ final Configuration cfg1 = U.findConfiguration(feature.getConfigurations(), "my.pid");
+ assertEquals(7, cfg1.getProperties().get("number"));
+ final Configuration cfg2 = U.findFactoryConfiguration(feature.getConfigurations(), "my.factory.pid", "name");
+ assertEquals("yeah", cfg2.getProperties().get("a.value"));
+
+ assertEquals(3, feature.getCapabilities().size());
+ Capability capability = U.findCapability(feature.getCapabilities(),"osgi.service");
+ assertNotNull(capability.getAttributes().get("objectClass"));
+
+ assertEquals(Arrays.asList("org.osgi.service.http.runtime.HttpServiceRuntime"), capability.getAttributes().get("objectClass"));
+
+ }
+
+ @Test public void testReadWithVariablesResolve() throws Exception {
+ final Feature feature = U.readFeature("test2");
+
+ List<Include> includes = feature.getIncludes();
+ assertEquals(1, includes.size());
+ Include include = includes.get(0);
+ assertEquals("org.apache.sling:sling:9", include.getId().toMvnId());
+
+ List<Requirement> reqs = feature.getRequirements();
+ Requirement req = reqs.get(0);
+ assertEquals("osgi.contract", req.getNamespace());
+ assertEquals("(&(osgi.contract=JavaServlet)(&(version>=3.0)(!(version>=4.0))))",
+ req.getDirectives().get("filter"));
+
+ List<Capability> caps = feature.getCapabilities();
+ Capability cap = null;
+ for (Capability c : caps) {
+ if ("osgi.service".equals(c.getNamespace())) {
+ cap = c;
+ break;
+ }
+ }
+ assertEquals(Collections.singletonList("org.osgi.service.http.runtime.HttpServiceRuntime"),
+ cap.getAttributes().get("objectClass"));
+ assertEquals("org.osgi.service.http.runtime",
+ cap.getDirectives().get("uses"));
+ // TODO this seems quite broken: fix!
+ // assertEquals("org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto",
+ // cap.getDirectives().get("uses"));
+
+ KeyValueMap fwProps = feature.getFrameworkProperties();
+ assertEquals("Framework property substitution should not happen at resolve time",
+ "${something}", fwProps.get("brave"));
+
+ Bundles bundles = feature.getBundles();
+ ArtifactId id = new ArtifactId("org.apache.sling", "foo-xyz", "1.2.3", null, null);
+ assertTrue(bundles.containsExact(id));
+ ArtifactId id2 = new ArtifactId("org.apache.sling", "bar-xyz", "1.2.3", null, null);
+ assertTrue(bundles.containsExact(id2));
+
+ Configurations configurations = feature.getConfigurations();
+ Configuration config = configurations.getConfiguration("my.pid2");
+ Dictionary<String, Object> props = config.getProperties();
+ assertEquals("Configuration substitution should not happen at resolve time",
+ "aa${ab_config}", props.get("a.value"));
+ assertEquals("${ab_config}bb", props.get("b.value"));
+ assertEquals("c${c_config}c", props.get("c.value"));
+ }
+
+ @Test public void testReadWithVariablesLaunch() throws Exception {
+ final Feature feature = U.readFeature("test3", SubstituteVariables.LAUNCH);
+
+ List<Include> includes = feature.getIncludes();
+ assertEquals(1, includes.size());
+ Include include = includes.get(0);
+ assertEquals("Include substitution should not happen at launch time",
+ "${sling.gid}:sling:10", include.getId().toMvnId());
+
+ List<Requirement> reqs = feature.getRequirements();
+ Requirement req = reqs.get(0);
+ assertEquals("Requirement substitution should not happen at launch time",
+ "osgi.${ns}", req.getNamespace());
+ assertEquals("Requirement substitution should not happen at launch time",
+ "(&(osgi.contract=${contract.name})(&(version>=3.0)(!(version>=4.0))))",
+ req.getDirectives().get("filter"));
+ assertEquals("There should be 1 directive, comments should be ignored",
+ 1, req.getDirectives().size());
+
+ List<Capability> caps = feature.getCapabilities();
+ Capability cap = null;
+ for (Capability c : caps) {
+ if ("osgi.${svc}".equals(c.getNamespace())) {
+ cap = c;
+ break;
+ }
+ }
+ assertEquals("Capability substitution should not happen at launch time",
+ Collections.singletonList("org.osgi.${svc}.http.runtime.HttpServiceRuntime"),
+ cap.getAttributes().get("objectClass"));
+ assertEquals("There should be 1 attribute, comments should be ignored",
+ 1, cap.getAttributes().size());
+
+ KeyValueMap fwProps = feature.getFrameworkProperties();
+ assertEquals("something", fwProps.get("brave"));
+ assertEquals("There should be 3 framework properties, comments not included",
+ 3, fwProps.size());
+
+ Configurations configurations = feature.getConfigurations();
+ assertEquals("There should be 2 configurations, comments not included",
+ 2, configurations.size());
+
+ Configuration config1 = configurations.getConfiguration("my.pid2");
+ for (Enumeration<?> en = config1.getProperties().elements(); en.hasMoreElements(); ) {
+ assertFalse("The comment should not show up in the configuration",
+ "comment".equals(en.nextElement()));
+ }
+
+ Configuration config2 = configurations.getConfiguration("my.pid2");
+ Dictionary<String, Object> props = config2.getProperties();
+ assertEquals("aaright!", props.get("a.value"));
+ assertEquals("right!bb", props.get("b.value"));
+ assertEquals("creally?c", props.get("c.value"));
+ assertEquals("The variable definition looks like a variable definition, this escaping mechanism should work",
+ "${refvar}", props.get("refvar"));
+ }
+
+ @Test public void testReadRepoInitExtension() throws Exception {
+ Feature feature = U.readFeature("repoinit");
+ Extensions extensions = feature.getExtensions();
+ assertEquals(1, extensions.size());
+ Extension ext = extensions.iterator().next();
+ assertEquals("some repo init\ntext", ext.getText());
+ }
+
+ @Test public void testReadRepoInitExtensionArray() throws Exception {
+ Feature feature = U.readFeature("repoinit2");
+ Extensions extensions = feature.getExtensions();
+ assertEquals(1, extensions.size());
+ Extension ext = extensions.iterator().next();
+ assertEquals("some repo init\ntext\n", ext.getText());
+ }
+
+ @Test public void testHandleVars() throws Exception {
+ FeatureJSONReader reader = new FeatureJSONReader(null, null, SubstituteVariables.LAUNCH);
+ Map<String, Object> vars = new HashMap<>();
+ vars.put("var1", "bar");
+ vars.put("varvariable", "${myvar}");
+ vars.put("var.2", "2");
+ setPrivateField(reader, "variables", vars);
+
+ assertEquals("foobarfoo", reader.handleLaunchVars("foo${var1}foo"));
+ assertEquals("barbarbar", reader.handleLaunchVars("${var1}${var1}${var1}"));
+ assertEquals("${}test${myvar}2", reader.handleLaunchVars("${}test${varvariable}${var.2}"));
+ try {
+ reader.handleLaunchVars("${undefined}");
+ fail("Should throw an exception on the undefined variable");
+ } catch (IllegalStateException ise) {
+ // good
+ }
+ }
+
+ private void setPrivateField(Object obj, String name, Object value) throws Exception {
+ Field field = obj.getClass().getDeclaredField(name);
+ field.setAccessible(true);
+ field.set(obj, value);
+ }
+}
diff --git a/src/test/java/org/apache/sling/feature/io/json/FeatureJSONWriterTest.java b/src/test/java/org/apache/sling/feature/io/json/FeatureJSONWriterTest.java
new file mode 100644
index 0000000..81db5e6
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/io/json/FeatureJSONWriterTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.json;
+
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.io.json.FeatureJSONReader;
+import org.apache.sling.feature.io.json.FeatureJSONWriter;
+import org.apache.sling.feature.io.json.FeatureJSONReader.SubstituteVariables;
+import org.junit.Test;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Arrays;
+
+import static org.junit.Assert.assertEquals;
+
+public class FeatureJSONWriterTest {
+
+ @Test public void testRead() throws Exception {
+ final Feature f = U.readFeature("test");
+ final Feature rf;
+ try ( final StringWriter writer = new StringWriter() ) {
+ FeatureJSONWriter.write(writer, f);
+ try ( final StringReader reader = new StringReader(writer.toString()) ) {
+ rf = FeatureJSONReader.read(reader, null, SubstituteVariables.RESOLVE);
+ }
+ }
+ assertEquals(f.getId(), rf.getId());
+ assertEquals("org.apache.sling:test-feature:1.1", rf.getId().toMvnId());
+ assertEquals("The feature description", rf.getDescription());
+
+ assertEquals(Arrays.asList("org.osgi.service.http.runtime.HttpServiceRuntime"),
+ U.findCapability(rf.getCapabilities(), "osgi.service").getAttributes().get("objectClass"));
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/feature/io/json/U.java b/src/test/java/org/apache/sling/feature/io/json/U.java
new file mode 100644
index 0000000..904f158
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/io/json/U.java
@@ -0,0 +1,89 @@
+/*
+ * 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.json;
+
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.io.json.FeatureJSONReader;
+import org.apache.sling.feature.io.json.FeatureJSONReader.SubstituteVariables;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.List;
+
+import static org.junit.Assert.fail;
+
+/** Test utilities */
+public class U {
+
+ /** Read the feature from the provided resource
+ */
+ public static Feature readFeature(final String name) throws Exception {
+ return readFeature(name, SubstituteVariables.RESOLVE);
+ }
+
+ public static Feature readFeature(final String name, final SubstituteVariables phase) throws Exception {
+ try ( final Reader reader = new InputStreamReader(U.class.getResourceAsStream("/features/" + name + ".json"),
+ "UTF-8") ) {
+ return FeatureJSONReader.read(reader, name, phase);
+ }
+ }
+
+ public static Configuration findConfiguration(final List<Configuration> cfgs, final String pid) {
+ for(final Configuration c : cfgs) {
+ if ( !c.isFactoryConfiguration() && pid.equals(c.getPid()) ) {
+ return c;
+ }
+ }
+ fail("Configuration not found " + pid);
+ return null;
+ }
+
+ public static Configuration findFactoryConfiguration(final List<Configuration> cfgs, final String factoryid, final String name) {
+ for(final Configuration c : cfgs) {
+ if ( c.isFactoryConfiguration() && factoryid.equals(c.getFactoryPid()) && name.equals(c.getName())) {
+ return c;
+ }
+ }
+ fail("Factory Configuration not found " + factoryid + "~" + name);
+ return null;
+ }
+
+ public static Capability findCapability(List<Capability> capabilities, final String namespace) {
+ for (Capability capability : capabilities) {
+ if (capability.getNamespace().equals(namespace)) {
+ return capability;
+ }
+ }
+
+ fail(String.format("No Capability with namespace '%s' found", namespace));
+ return null;
+ }
+
+ public static Requirement findRequirement(List<Requirement> requirements, final String namespace) {
+ for (Requirement requirement : requirements) {
+ if (requirement.getNamespace().equals(namespace)) {
+ return requirement;
+ }
+ }
+
+ fail(String.format("No Requirement with namespace '%s' found", namespace));
+ return null;
+ }
+}
diff --git a/src/test/resources/features/repoinit.json b/src/test/resources/features/repoinit.json
new file mode 100644
index 0000000..2177702
--- /dev/null
+++ b/src/test/resources/features/repoinit.json
@@ -0,0 +1,4 @@
+{
+ "id": "test/repoinit/1.0.0",
+ "repoinit:TEXT|false": "some repo init\ntext"
+}
diff --git a/src/test/resources/features/repoinit2.json b/src/test/resources/features/repoinit2.json
new file mode 100644
index 0000000..beef986
--- /dev/null
+++ b/src/test/resources/features/repoinit2.json
@@ -0,0 +1,7 @@
+{
+ "id": "test/repoinit2/1.0.0",
+ "repoinit:TEXT|false": [
+ "some repo init",
+ "text"
+ ]
+}
diff --git a/src/test/resources/features/test.json b/src/test/resources/features/test.json
new file mode 100644
index 0000000..8ae346c
--- /dev/null
+++ b/src/test/resources/features/test.json
@@ -0,0 +1,92 @@
+{
+ "id" : "org.apache.sling/test-feature/1.1",
+ "description": "The feature description",
+
+ "includes" : [
+ {
+ "id" : "org.apache.sling/sling/9",
+ "removals" : {
+ "configurations" : [
+ ],
+ "bundles" : [
+ ],
+ "framework-properties" : [
+ ]
+ }
+ }
+ ],
+ "requirements" : [
+ {
+ "namespace" : "osgi.contract",
+ "directives" : {
+ "filter" : "(&(osgi.contract=JavaServlet)(&(version>=3.0)(!(version>=4.0))))"
+ }
+ }
+ ],
+ "capabilities" : [
+ {
+ "namespace" : "osgi.implementation",
+ "attributes" : {
+ "osgi.implementation" : "osgi.http",
+ "version:Version" : "1.1"
+ },
+ "directives" : {
+ "uses" : "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard"
+ }
+ },
+ {
+ "namespace" : "osgi.service",
+ "attributes" : {
+ "objectClass:List<String>" : "org.osgi.service.http.runtime.HttpServiceRuntime"
+ },
+ "directives" : {
+ "uses" : "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto"
+ }
+ },
+ {
+ "namespace" : "osgi.contract",
+ "attributes" : {
+ "osgi.contract" : "JavaServlet",
+ "osgi.implementation" : "osgi.http",
+ "version:Version" : "3.1"
+ },
+ "directives" : {
+ "uses" : "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto"
+ }
+ }
+ ],
+ "framework-properties" : {
+ "foo" : 1,
+ "brave" : "something",
+ "org.apache.felix.scr.directory" : "launchpad/scr"
+ },
+ "bundles" :[
+ {
+ "id" : "org.apache.sling/oak-server/1.0.0",
+ "hash" : "4632463464363646436",
+ "start-order" : 1
+ },
+ {
+ "id" : "org.apache.sling/application-bundle/2.0.0",
+ "start-order" : 1
+ },
+ {
+ "id" : "org.apache.sling/another-bundle/2.1.0",
+ "start-order" : 1
+ },
+ {
+ "id" : "org.apache.sling/foo-xyz/1.2.3",
+ "start-order" : 2
+ }
+ ],
+ "configurations" : {
+ "my.pid" : {
+ "foo" : 5,
+ "bar" : "test",
+ "number:Integer" : 7
+ },
+ "my.factory.pid~name" : {
+ "a.value" : "yeah"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/features/test2.json b/src/test/resources/features/test2.json
new file mode 100644
index 0000000..0e5c1c6
--- /dev/null
+++ b/src/test/resources/features/test2.json
@@ -0,0 +1,107 @@
+{
+ "model-version": "1",
+ "id" : "org.apache.sling/test2/1.1",
+
+ "variables": {
+ "common.version": "1.2.3",
+ "contract.name": "JavaServlet",
+ "ab_config": "right!",
+ "c_config": "really?",
+ "includever": "9",
+ "ns": "contract",
+ "sling.gid": "org.apache.sling",
+ "something": "something",
+ "svc": "service"
+ },
+
+ "includes" : [
+ {
+ "id" : "${sling.gid}/sling/${includever}",
+ "removals" : {
+ "configurations" : [
+ ],
+ "bundles" : [
+ ],
+ "framework-properties" : [
+ ]
+ }
+ }
+ ],
+ "requirements" : [
+ {
+ "namespace" : "osgi.${ns}",
+ "directives" : {
+ "filter" : "(&(osgi.contract=${contract.name})(&(version>=3.0)(!(version>=4.0))))"
+ }
+ }
+ ],
+ "capabilities" : [
+ {
+ "namespace" : "osgi.implementation",
+ "attributes" : {
+ "osgi.implementation" : "osgi.http",
+ "version:Version" : "1.1"
+ },
+ "directives" : {
+ "uses" : "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard"
+ }
+ },
+ {
+ "namespace" : "osgi.${svc}",
+ "attributes" : {
+ "objectClass:List<String>" : "org.osgi.${svc}.http.runtime.HttpServiceRuntime"
+ },
+ "directives" : {
+ "uses" : "org.osgi.${svc}.http.runtime,org.osgi.${svc}.http.runtime.dto"
+ }
+ },
+ {
+ "namespace" : "osgi.contract",
+ "attributes" : {
+ "osgi.contract" : "JavaServlet",
+ "osgi.implementation" : "osgi.http",
+ "version:Version" : "3.1"
+ },
+ "directives" : {
+ "uses" : "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto"
+ }
+ }
+ ],
+ "framework-properties" : {
+ "foo" : 1,
+ "brave" : "${something}",
+ "org.apache.felix.scr.directory" : "launchpad/scr"
+ },
+ "bundles" :[
+ {
+ "id" : "org.apache.sling/oak-server/1.0.0",
+ "hash" : "4632463464363646436",
+ "start-order" : 1
+ },
+ {
+ "id" : "org.apache.sling/application-bundle/2.0.0",
+ "start-order" : 1
+ },
+ {
+ "id" : "org.apache.sling/another-bundle/2.1.0",
+ "start-order" : 1
+ },
+ {
+ "id" : "org.apache.sling/foo-xyz/${common.version}",
+ "start-order" : 2
+ },
+ "org.apache.sling/bar-xyz/${common.version}"
+ ],
+ "configurations" : {
+ "my.pid" : {
+ "foo" : 5,
+ "bar" : "test",
+ "number:Integer" : 7
+ },
+ "my.pid2" : {
+ "a.value" : "aa${ab_config}",
+ "b.value" : "${ab_config}bb",
+ "c.value" : "c${c_config}c"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/features/test3.json b/src/test/resources/features/test3.json
new file mode 100644
index 0000000..630239d
--- /dev/null
+++ b/src/test/resources/features/test3.json
@@ -0,0 +1,116 @@
+{
+ "#": "A comment",
+ "# array": ["array", "comment"],
+ "id": "org.apache.sling/test2/1.1",
+
+ "variables": {
+ "common.version": "1.2.3",
+ "contract.name": "JavaServlet",
+ "ab_config": "right!",
+ "c_config": "really?",
+ "includever": "9",
+ "ns": "contract",
+ "sling.gid": "org.apache.sling",
+ "something": "something",
+ "svc": "service",
+ "refvar": "${refvar}"
+ },
+
+ "includes" : [
+ {
+ "#": "comment",
+ "id" : "${sling.gid}/sling/10",
+ "removals" : {
+ "configurations" : [
+ ],
+ "bundles" : [
+ ],
+ "#": "comment",
+ "framework-properties" : [
+ ]
+ }
+ }
+ ],
+ "requirements" : [
+ {
+ "namespace" : "osgi.${ns}",
+ "#": "comment",
+ "directives" : {
+ "#": "comment",
+ "filter" : "(&(osgi.contract=${contract.name})(&(version>=3.0)(!(version>=4.0))))"
+ }
+ }
+ ],
+ "capabilities" : [
+ {
+ "#": "comment",
+ "namespace" : "osgi.implementation",
+ "attributes" : {
+ "osgi.implementation" : "osgi.http",
+ "version:Version" : "1.1"
+ },
+ "directives" : {
+ "uses" : "javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard"
+ }
+ },
+ {
+ "namespace" : "osgi.${svc}",
+ "attributes" : {
+ "#": "comment",
+ "objectClass:List<String>" : "org.osgi.${svc}.http.runtime.HttpServiceRuntime"
+ },
+ "directives" : {
+ "uses" : "org.osgi.${svc}.http.runtime,org.osgi.${svc}.http.runtime.dto"
+ }
+ },
+ {
+ "namespace" : "osgi.contract",
+ "attributes" : {
+ "osgi.contract" : "JavaServlet",
+ "osgi.implementation" : "osgi.http",
+ "version:Version" : "3.1"
+ },
+ "directives" : {
+ "uses" : "org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto"
+ }
+ }
+ ],
+ "framework-properties" : {
+ "# one": "comment",
+ "# two": "comment",
+ "foo" : 1,
+ "brave" : "${something}",
+ "org.apache.felix.scr.directory" : "launchpad/scr"
+ },
+ "bundles" :[
+ {
+ "id" : "org.apache.sling/oak-server/1.0.0",
+ "hash" : "4632463464363646436",
+ "start-order" : 1,
+ "#": "comment"
+ },
+ {
+ "id" : "org.apache.sling/application-bundle/2.0.0",
+ "start-order" : 1
+ },
+ {
+ "id" : "org.apache.sling/another-bundle/2.1.0",
+ "start-order" : 1
+ }
+ ],
+ "configurations" : {
+ "#": "comment",
+ "my.pid" : {
+ "#": "comment",
+ "foo" : 5,
+ "bar" : "test",
+ "number:Integer" : 7
+ },
+ "my.pid2" : {
+ "a.value" : "aa${ab_config}",
+ "b.value" : "${ab_config}bb",
+ "c.value" : "c${c_config}c",
+ "refvar": "${refvar}"
+ }
+ }
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
davidb@apache.org.