You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by cz...@apache.org on 2020/11/19 08:34:18 UTC
[sling-org-apache-sling-feature] branch master updated: SLING-9917
: Allow to store metadata alongside framework properties and variables
This is an automated email from the ASF dual-hosted git repository.
cziegeler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature.git
The following commit(s) were added to refs/heads/master by this push:
new df44c46 SLING-9917 : Allow to store metadata alongside framework properties and variables
df44c46 is described below
commit df44c46419998c7717f372ea914e27d52766155d
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Thu Nov 19 09:29:59 2020 +0100
SLING-9917 : Allow to store metadata alongside framework properties and variables
---
.../java/org/apache/sling/feature/Artifact.java | 44 ++++++-
.../org/apache/sling/feature/Configuration.java | 24 ++++
.../java/org/apache/sling/feature/Extension.java | 8 ++
.../java/org/apache/sling/feature/Feature.java | 69 +++++++++-
.../org/apache/sling/feature/MapWithMetadata.java | 137 ++++++++++++++++++++
.../sling/feature/io/json/FeatureJSONReader.java | 40 ++++++
.../sling/feature/io/json/FeatureJSONWriter.java | 116 ++++++++++++-----
.../sling/feature/io/json/JSONConstants.java | 4 +
.../org/apache/sling/feature/package-info.java | 2 +-
.../org/apache/sling/feature/ArtifactTest.java | 61 +++++++++
.../apache/sling/feature/ConfigurationTest.java | 9 ++
.../java/org/apache/sling/feature/FeatureTest.java | 142 +++++++++++++++++++++
.../feature/io/json/FeatureJSONReaderTest.java | 18 +++
.../feature/io/json/FeatureJSONWriterTest.java | 16 +++
src/test/resources/features/test-metadata.json | 22 ++++
15 files changed, 669 insertions(+), 43 deletions(-)
diff --git a/src/main/java/org/apache/sling/feature/Artifact.java b/src/main/java/org/apache/sling/feature/Artifact.java
index 90e416c..76e15f8 100644
--- a/src/main/java/org/apache/sling/feature/Artifact.java
+++ b/src/main/java/org/apache/sling/feature/Artifact.java
@@ -189,13 +189,18 @@ public class Artifact implements Comparable<Artifact> {
}
}
+ /**
+ * Get the feature origins - if recorded
+ *
+ * @return A array of feature artifact ids - array might be empty
+ * @throws IllegalArgumentException If the stored values are not valid artifact ids
+ */
public ArtifactId[] getFeatureOrigins() {
String origins = this.getMetadata().get(KEY_FEATURE_ORIGINS);
Set<ArtifactId> originFeatures;
if (origins == null || origins.trim().isEmpty()) {
originFeatures = Collections.emptySet();
- }
- else {
+ } else {
originFeatures = new LinkedHashSet<>();
for (String origin : origins.split(",")) {
if (!origin.trim().isEmpty()) {
@@ -206,18 +211,45 @@ public class Artifact implements Comparable<Artifact> {
return originFeatures.toArray(new ArtifactId[0]);
}
+ /**
+ * Get the feature origins
+ * If no origins are recorded, the provided artifact id is returned
+ *
+ * @param self The id of the current feature
+ * @return An array of feature artifact ids
+ * @throws IllegalArgumentException If the stored values are not valid artifact ids
+ * @since 1.7.0
+ */
+ public ArtifactId[] getFeatureOrigins(final ArtifactId self) {
+ String origins = this.getMetadata().get(KEY_FEATURE_ORIGINS);
+ Set<ArtifactId> originFeatures;
+ if (origins == null || origins.trim().isEmpty()) {
+ originFeatures = Collections.singleton(self);
+ } else {
+ originFeatures = new LinkedHashSet<>();
+ for (String origin : origins.split(",")) {
+ if (!origin.trim().isEmpty()) {
+ originFeatures.add(ArtifactId.parse(origin));
+ }
+ }
+ }
+ return originFeatures.toArray(new ArtifactId[0]);
+ }
+
+ /**
+ * Set the feature origins
+ * @param featureOrigins the array of artifact ids or null to remove the info from this object
+ */
public void setFeatureOrigins(ArtifactId... featureOrigins) {
String origins;
if (featureOrigins != null && featureOrigins.length > 0) {
origins = Stream.of(featureOrigins).filter(Objects::nonNull).map(ArtifactId::toMvnId).distinct().collect(Collectors.joining(","));
- }
- else {
+ } else {
origins = "";
}
if (!origins.trim().isEmpty()) {
this.getMetadata().put(KEY_FEATURE_ORIGINS, origins);
- }
- else {
+ } else {
this.getMetadata().remove(KEY_FEATURE_ORIGINS);
}
}
diff --git a/src/main/java/org/apache/sling/feature/Configuration.java b/src/main/java/org/apache/sling/feature/Configuration.java
index 97133c5..1ec54eb 100644
--- a/src/main/java/org/apache/sling/feature/Configuration.java
+++ b/src/main/java/org/apache/sling/feature/Configuration.java
@@ -198,6 +198,30 @@ public class Configuration
return Collections.unmodifiableList(list);
}
+ /**
+ * Get the feature origins.
+ * If no origins are recorded, the provided id is returned.
+ *
+ * @param self The id of the current feature
+ * @return A immutable list of feature artifact ids
+ * @since 1.7
+ * @throws IllegalArgumentException If the stored values are not valid artifact ids
+ */
+ public List<ArtifactId> getFeatureOrigins(final ArtifactId self) {
+ final List<ArtifactId> list = new ArrayList<>();
+ final Object origins = this.properties.get(PROP_FEATURE_ORIGINS);
+ if ( origins != null ) {
+ final String[] values = Converters.standardConverter().convert(origins).to(String[].class);
+ for(final String v : values) {
+ list.add(ArtifactId.parse(v));
+ }
+ }
+ if ( list.isEmpty() ) {
+ list.add(self);
+ }
+ return Collections.unmodifiableList(list);
+ }
+
/**
* Set the feature origins
* @param featureOrigins the list of artifact ids or null to remove the info from this object
diff --git a/src/main/java/org/apache/sling/feature/Extension.java b/src/main/java/org/apache/sling/feature/Extension.java
index b9bbb52..2b9873c 100644
--- a/src/main/java/org/apache/sling/feature/Extension.java
+++ b/src/main/java/org/apache/sling/feature/Extension.java
@@ -70,6 +70,14 @@ public class Extension {
*/
public static final String EXTENSION_NAME_ASSEMBLED_FEATURES = "assembled-features";
+ /**
+ * Extension name containing internal data. An extension with this name must not be created by
+ * hand, it is managed by the feature model implementation.
+ * This extension is of type {@link ExtensionType#JSON} and is optional.
+ * @since 1.7.0
+ */
+ public static final String EXTENSION_NAME_INTERNAL_DATA = "feature-internal-data";
+
/** The extension type */
private final ExtensionType type;
diff --git a/src/main/java/org/apache/sling/feature/Feature.java b/src/main/java/org/apache/sling/feature/Feature.java
index a1558be..08a7f8f 100644
--- a/src/main/java/org/apache/sling/feature/Feature.java
+++ b/src/main/java/org/apache/sling/feature/Feature.java
@@ -17,14 +17,16 @@
package org.apache.sling.feature;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
import org.apache.felix.utils.resource.CapabilityImpl;
import org.apache.felix.utils.resource.RequirementImpl;
import org.osgi.resource.Capability;
import org.osgi.resource.Resource;
+import org.osgi.util.converter.Converters;
/**
* A feature consists of
@@ -48,7 +50,7 @@ public class Feature implements Comparable<Feature> {
private final Configurations configurations = new Configurations();
- private final Map<String,String> frameworkProperties = new HashMap<>();
+ private final MapWithMetadata frameworkProperties = new MapWithMetadata();
private final List<MatchingRequirement> requirements = new ArrayList<>();
@@ -56,7 +58,7 @@ public class Feature implements Comparable<Feature> {
private final Extensions extensions = new Extensions();
- private final Map<String,String> variables = new HashMap<>();
+ private final MapWithMetadata variables = new MapWithMetadata();
/** The optional location. */
private volatile String location;
@@ -145,7 +147,7 @@ public class Feature implements Comparable<Feature> {
* The returned object is modifiable.
* @return The framework properties
*/
- public Map<String,String> getFrameworkProperties() {
+ public Map<String, String> getFrameworkProperties() {
return this.frameworkProperties;
}
@@ -321,6 +323,65 @@ public class Feature implements Comparable<Feature> {
}
/**
+ * Return a mutable map of metadata for the framework property
+ * @return A mutable map or {@code null} if the framework property does not exist
+ * @since 1.7.0
+ */
+ public Map<String, Object> getFrameworkPropertyMetadata(final String key) {
+ return this.frameworkProperties.getMetadata(key);
+ }
+
+ /**
+ * Return a mutable map of metadata for the variable
+ * @return A mutable map or {@code null} if the variable does not exist
+ * @since 1.7.0
+ */
+ public Map<String, Object> getVariableMetadata(final String key) {
+ return this.variables.getMetadata(key);
+ }
+
+ /**
+ * Get the feature origins for the metadata- if recorded
+ *
+ * @param The metadata (for a variable or framework property)
+ * @return A immutable list of feature artifact ids - list is never empty
+ * @since 1.7
+ * @throws IllegalArgumentException If the stored values are not valid artifact ids
+ */
+ public List<ArtifactId> getFeatureOrigins(final Map<String, Object> metadata) {
+ final List<ArtifactId> list = new ArrayList<>();
+ final Object origins = metadata.get(Artifact.KEY_FEATURE_ORIGINS);
+ if ( origins != null ) {
+ final String[] values = Converters.standardConverter().convert(origins).to(String[].class);
+ for(final String v : values) {
+ list.add(ArtifactId.parse(v));
+ }
+ }
+ if ( list.isEmpty() ) {
+ list.add(this.getId());
+ }
+ return Collections.unmodifiableList(list);
+ }
+
+ /**
+ * Set the feature origins for the metadata
+ * @param The metadata (for a variable or framework property)
+ * @param featureOrigins the list of artifact ids or null to remove the info from this object
+ * @since 1.7
+ */
+ public void setFeatureOrigins(final Map<String, Object> metadata, final List<ArtifactId> featureOrigins) {
+ if ( featureOrigins == null || featureOrigins.isEmpty() ) {
+ metadata.remove(Artifact.KEY_FEATURE_ORIGINS);
+ } else if ( featureOrigins.size() == 1 && this.getId().equals(featureOrigins.get(0)) ) {
+ metadata.remove(Artifact.KEY_FEATURE_ORIGINS);
+ } else {
+ final List<String> list = featureOrigins.stream().map(ArtifactId::toMvnId).collect(Collectors.toList());
+ final String[] values = Converters.standardConverter().convert(list).to(String[].class);
+ metadata.put(Artifact.KEY_FEATURE_ORIGINS, values);
+ }
+ }
+
+ /**
* Create a copy of the feature
* @return A copy of the feature
*/
diff --git a/src/main/java/org/apache/sling/feature/MapWithMetadata.java b/src/main/java/org/apache/sling/feature/MapWithMetadata.java
new file mode 100644
index 0000000..9cfbb54
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/MapWithMetadata.java
@@ -0,0 +1,137 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Helper class to maintain metadata for a map
+ * @since 1.7.0
+ */
+class MapWithMetadata implements Map<String, String> {
+
+ private final Map<String, String> values = new LinkedHashMap<>();
+
+ private final Map<String, Map<String, Object>> metadata = new HashMap<>();
+
+ public Map<String, Object> getMetadata(final String key) {
+ if (values.containsKey(key) ) {
+ return metadata.computeIfAbsent(key, id -> new LinkedHashMap<>());
+ }
+ metadata.remove(key);
+ return null;
+ }
+
+ @Override
+ public void clear() {
+ this.values.clear();
+ this.metadata.clear();
+ }
+
+ @Override
+ public boolean containsKey(final Object key) {
+ return this.values.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(final Object value) {
+ return this.values.containsValue(value);
+ }
+
+ @Override
+ public Set<Entry<String, String>> entrySet() {
+ return this.values.entrySet();
+ }
+
+ @Override
+ public String get(final Object key) {
+ return this.values.get(key);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.values.isEmpty();
+ }
+
+ @Override
+ public Set<String> keySet() {
+ return this.values.keySet();
+ }
+
+ @Override
+ public String put(final String key, final String value) {
+ return this.values.put(key, value);
+ }
+
+ @Override
+ public void putAll(final Map<? extends String, ? extends String> m) {
+ this.values.putAll(m);
+ }
+
+ @Override
+ public String remove(final Object key) {
+ this.metadata.remove(key);
+ return this.values.remove(key);
+ }
+
+ @Override
+ public int size() {
+ return this.values.size();
+ }
+
+ @Override
+ public Collection<String> values() {
+ return this.values.values();
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return values.hashCode();
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if ( obj instanceof MapWithMetadata ) {
+ return this.values.equals((MapWithMetadata)obj);
+ }
+ if ( !(obj instanceof Map)) {
+ return false;
+ }
+ return this.values.equals(obj);
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return values.toString();
+ }
+}
\ No newline at end of file
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
index 71c1e07..2d3339b 100644
--- a/src/main/java/org/apache/sling/feature/io/json/FeatureJSONReader.java
+++ b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONReader.java
@@ -699,6 +699,15 @@ public class FeatureJSONReader {
JSONConstants.FEATURE_KNOWN_PROPERTIES,
this.feature.getExtensions(), this.feature.getConfigurations());
+ // check for internal metadata extension
+ final Extension internalData = this.feature.getExtensions().getByName(Extension.EXTENSION_NAME_INTERNAL_DATA);
+ if ( internalData != null ) {
+ this.feature.getExtensions().remove(internalData);
+ if ( internalData.getType() != ExtensionType.JSON ) {
+ throw new IOException("Extension " + internalData.getName() + " must be of type JSON");
+ }
+ this.setInternalData(internalData);
+ }
return this.feature;
}
@@ -711,6 +720,37 @@ public class FeatureJSONReader {
throw new IOException(this.exceptionPrefix.concat("Unsupported model version: ").concat(modelVersion));
}
}
+
+ private void setInternalData(final Extension internalData) throws IOException {
+ final JsonValue val = internalData.getJSONStructure();
+ for (final Map.Entry<String, JsonValue> entry : checkTypeObject("Extension ".concat(internalData.getName()), val).entrySet()) {
+ final String key = entry.getKey();
+ if ( JSONConstants.FRAMEWORK_PROPERTIES_METADATA.equals(key) ) {
+ for (final Map.Entry<String, JsonValue> propEntry : checkTypeObject(key, entry.getValue()).entrySet()) {
+ final Map<String, Object> metadata = this.feature.getFrameworkPropertyMetadata(propEntry.getKey());
+ if ( metadata == null ) {
+ throw new IOException("Framework property " + propEntry.getKey() + " does not exists (metadata)");
+ }
+ for(final Map.Entry<String, JsonValue> ve : checkTypeObject(JSONConstants.FRAMEWORK_PROPERTIES_METADATA.concat(".").concat(propEntry.getKey()), propEntry.getValue()).entrySet()) {
+ metadata.put(ve.getKey(), org.apache.felix.cm.json.Configurations.convertToObject(ve.getValue()));
+ }
+ }
+ } else if ( JSONConstants.VARIABLES_METADATA.equals(key) ) {
+ for (final Map.Entry<String, JsonValue> varEntry : checkTypeObject(key, entry.getValue()).entrySet()) {
+ final Map<String, Object> metadata = this.feature.getVariableMetadata(varEntry.getKey());
+ if ( metadata == null ) {
+ throw new IOException("Variable " + varEntry.getKey() + " does not exists (metadata)");
+ }
+ for(final Map.Entry<String, JsonValue> ve : checkTypeObject(JSONConstants.VARIABLES_METADATA.concat(".").concat(varEntry.getKey()), varEntry.getValue()).entrySet()) {
+ metadata.put(ve.getKey(), org.apache.felix.cm.json.Configurations.convertToObject(ve.getValue()));
+ }
+ }
+
+ } else {
+ throw new IOException("Unknown data in " + internalData.getName() + " : " + key);
+ }
+ }
+ }
}
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
index cf33b13..fa7db6c 100644
--- a/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java
+++ b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java
@@ -22,6 +22,7 @@ import java.io.Writer;
import java.util.Collection;
import java.util.Collections;
import java.util.Hashtable;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -37,6 +38,7 @@ 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.ExtensionState;
import org.apache.sling.feature.ExtensionType;
import org.apache.sling.feature.Feature;
import org.apache.sling.feature.MatchingRequirement;
@@ -180,8 +182,16 @@ public class FeatureJSONWriter {
final List<Extension> extensions,
final Configurations allConfigs) throws IOException {
for(final Extension ext : extensions) {
- final String state;
- switch (ext.getState()) {
+ writeExtension(generator, ext, allConfigs);
+ }
+ }
+
+ private void writeExtension(final JsonGenerator generator,
+ final Extension ext,
+ final Configurations allConfigs) throws IOException {
+
+ final String state;
+ switch (ext.getState()) {
case OPTIONAL:
state = "false";
break;
@@ -190,43 +200,42 @@ public class FeatureJSONWriter {
break;
default:
state = ext.getState().name();
+ }
+ final String key = ext.getName().concat(":").concat(ext.getType().name()).concat("|").concat(state);
+ if ( ext.getType() == ExtensionType.JSON ) {
+ generator.write(key, ext.getJSONStructure());
+ } else if ( ext.getType() == ExtensionType.TEXT ) {
+ generator.writeStartArray(key);
+ for(String line : ext.getText().split("\n")) {
+ generator.write(line);
}
- final String key = ext.getName().concat(":").concat(ext.getType().name()).concat("|").concat(state);
- if ( ext.getType() == ExtensionType.JSON ) {
- generator.write(key, ext.getJSONStructure());
- } else if ( ext.getType() == ExtensionType.TEXT ) {
- generator.writeStartArray(key);
- for(String line : ext.getText().split("\n")) {
- generator.write(line);
- }
- generator.writeEnd();
- } else {
- generator.writeStartArray(key);
- 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_ID);
- if ( artifact.getId().toMvnId().equals(artifactProp) ) {
- artifactCfgs.add(cfg);
- }
+ generator.writeEnd();
+ } else {
+ generator.writeStartArray(key);
+ 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_ID);
+ if ( artifact.getId().toMvnId().equals(artifactProp) ) {
+ artifactCfgs.add(cfg);
}
- if ( artifact.getMetadata().isEmpty() && artifactCfgs.isEmpty() ) {
- generator.write(artifact.getId().toMvnId());
- } else {
- generator.writeStartObject();
- generator.write(JSONConstants.ARTIFACT_ID, artifact.getId().toMvnId());
+ }
+ if ( artifact.getMetadata().isEmpty() && artifactCfgs.isEmpty() ) {
+ generator.write(artifact.getId().toMvnId());
+ } else {
+ generator.writeStartObject();
+ generator.write(JSONConstants.ARTIFACT_ID, artifact.getId().toMvnId());
- for(final Map.Entry<String, String> me : artifact.getMetadata().entrySet()) {
- generator.write(me.getKey(), me.getValue());
- }
+ for(final Map.Entry<String, String> me : artifact.getMetadata().entrySet()) {
+ generator.write(me.getKey(), me.getValue());
+ }
- writeConfigurations(generator, artifactCfgs);
+ writeConfigurations(generator, artifactCfgs);
- generator.writeEnd();
- }
+ generator.writeEnd();
}
- generator.writeEnd();
}
+ generator.writeEnd();
}
}
@@ -401,6 +410,12 @@ public class FeatureJSONWriter {
// framework properties
writeFrameworkProperties(generator, feature.getFrameworkProperties());
+ // write metadata for variables and framework properties
+ if ( feature.getExtensions().getByName(Extension.EXTENSION_NAME_INTERNAL_DATA) != null ) {
+ throw new IOException("Feature must not contain internal data extension");
+ }
+ writeInternalData(generator, feature);
+
// extensions
writeExtensions(generator, feature.getExtensions(), feature.getConfigurations());
@@ -412,4 +427,41 @@ public class FeatureJSONWriter {
writeProperty(generator, JSONConstants.FEATURE_ID, feature.getId().toMvnId());
}
+ /**
+ * Write metadata for variables and framework properties in the internal extension
+ */
+ private void writeInternalData(final JsonGenerator generator,
+ final Feature feature) throws IOException {
+
+ final Map<String, Object> output = new LinkedHashMap<>();
+ if ( !feature.getFrameworkProperties().isEmpty() ) {
+ final Map<String, Map<String, Object>> fwkMetadata = new LinkedHashMap<>();
+ for(final String fwkPropName : feature.getFrameworkProperties().keySet()) {
+ final Map<String, Object> metadata = feature.getFrameworkPropertyMetadata(fwkPropName);
+ if ( !metadata.isEmpty() ) {
+ fwkMetadata.put(fwkPropName, metadata);
+ }
+ }
+ if ( !fwkMetadata.isEmpty() ) {
+ output.put(JSONConstants.FRAMEWORK_PROPERTIES_METADATA, fwkMetadata);
+ }
+ }
+ if ( !feature.getVariables().isEmpty() ) {
+ final Map<String, Map<String, Object>> varMetadata = new LinkedHashMap<>();
+ for(final String varName : feature.getVariables().keySet()) {
+ final Map<String, Object> metadata = feature.getVariableMetadata(varName);
+ if ( !metadata.isEmpty() ) {
+ varMetadata.put(varName, metadata);
+ }
+ }
+ if ( !varMetadata.isEmpty() ) {
+ output.put(JSONConstants.VARIABLES_METADATA, varMetadata);
+ }
+ }
+ if ( !output.isEmpty() ) {
+ final Extension ext = new Extension(ExtensionType.JSON, Extension.EXTENSION_NAME_INTERNAL_DATA, ExtensionState.REQUIRED);
+ ext.setJSONStructure(org.apache.felix.cm.json.Configurations.convertToJsonValue(output).asJsonObject());
+ this.writeExtension(generator, ext, null);
+ }
+ }
}
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
index 9d665fb..c48c9c1 100644
--- a/src/main/java/org/apache/sling/feature/io/json/JSONConstants.java
+++ b/src/main/java/org/apache/sling/feature/io/json/JSONConstants.java
@@ -82,4 +82,8 @@ public abstract class JSONConstants {
static final String REQCAP_NAMESPACE = "namespace";
static final String REQCAP_ATTRIBUTES = "attributes";
static final String REQCAP_DIRECTIVES = "directives";
+
+ static final String FRAMEWORK_PROPERTIES_METADATA = "framework-properties-metadata";
+
+ static final String VARIABLES_METADATA = "variables-metadata";
}
diff --git a/src/main/java/org/apache/sling/feature/package-info.java b/src/main/java/org/apache/sling/feature/package-info.java
index 9d1cc7f..4ac3d3a 100644
--- a/src/main/java/org/apache/sling/feature/package-info.java
+++ b/src/main/java/org/apache/sling/feature/package-info.java
@@ -17,7 +17,7 @@
* under the License.
*/
-@org.osgi.annotation.versioning.Version("1.6.0")
+@org.osgi.annotation.versioning.Version("1.7.0")
package org.apache.sling.feature;
diff --git a/src/test/java/org/apache/sling/feature/ArtifactTest.java b/src/test/java/org/apache/sling/feature/ArtifactTest.java
new file mode 100644
index 0000000..fca5d92
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/ArtifactTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+public class ArtifactTest {
+
+ @Test
+ public void testFeatureOrigins() {
+ final ArtifactId self = ArtifactId.parse("self:self:1");
+
+ final Artifact art = new Artifact(ArtifactId.parse("art:art:1"));
+ assertEquals(0, art.getFeatureOrigins().length);
+ assertNull(art.getMetadata().get(Artifact.KEY_FEATURE_ORIGINS));
+ assertEquals(1, art.getFeatureOrigins(self).length);
+ assertEquals(self, art.getFeatureOrigins(self)[0]);
+
+ // single id
+ final ArtifactId id = ArtifactId.parse("g:a:1");
+ art.setFeatureOrigins(id);
+ assertEquals(1, art.getFeatureOrigins().length);
+ assertEquals(id, art.getFeatureOrigins()[0]);
+ assertEquals(1, art.getFeatureOrigins(self).length);
+ assertEquals(id, art.getFeatureOrigins(self)[0]);
+
+ assertNotNull(art.getMetadata().get(Artifact.KEY_FEATURE_ORIGINS));
+ assertEquals(id.toMvnId(), art.getMetadata().get(Artifact.KEY_FEATURE_ORIGINS));
+
+ // add another id
+ final ArtifactId id2 = ArtifactId.parse("g:b:2");
+ art.setFeatureOrigins(id, id2);
+ assertEquals(2, art.getFeatureOrigins().length);
+ assertEquals(id, art.getFeatureOrigins()[0]);
+ assertEquals(id2, art.getFeatureOrigins()[1]);
+ assertEquals(2, art.getFeatureOrigins(self).length);
+ assertEquals(id, art.getFeatureOrigins(self)[0]);
+ assertEquals(id2, art.getFeatureOrigins(self)[1]);
+
+ assertNotNull(art.getMetadata().get(Artifact.KEY_FEATURE_ORIGINS));
+ assertEquals(id.toMvnId().concat(",").concat(id2.toMvnId()), art.getMetadata().get(Artifact.KEY_FEATURE_ORIGINS));
+ }
+}
diff --git a/src/test/java/org/apache/sling/feature/ConfigurationTest.java b/src/test/java/org/apache/sling/feature/ConfigurationTest.java
index 2e28cb3..3f8e222 100644
--- a/src/test/java/org/apache/sling/feature/ConfigurationTest.java
+++ b/src/test/java/org/apache/sling/feature/ConfigurationTest.java
@@ -75,16 +75,22 @@ public class ConfigurationTest {
@Test
public void testFeatureOrigins() {
+ final ArtifactId self = ArtifactId.parse("self:self:1");
+
final Configuration cfg = new Configuration("foo");
assertTrue(cfg.getFeatureOrigins().isEmpty());
assertNull(cfg.getConfigurationProperties().get(Configuration.PROP_FEATURE_ORIGINS));
assertNull(cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS));
+ assertEquals(1, cfg.getFeatureOrigins(self).size());
+ assertEquals(self, cfg.getFeatureOrigins(self).get(0));
// single id
final ArtifactId id = ArtifactId.parse("g:a:1");
cfg.setFeatureOrigins(Collections.singletonList(id));
assertEquals(1, cfg.getFeatureOrigins().size());
assertEquals(id, cfg.getFeatureOrigins().get(0));
+ assertEquals(1, cfg.getFeatureOrigins(self).size());
+ assertEquals(id, cfg.getFeatureOrigins(self).get(0));
assertNull(cfg.getConfigurationProperties().get(Configuration.PROP_FEATURE_ORIGINS));
assertNotNull(cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS));
@@ -97,6 +103,9 @@ public class ConfigurationTest {
assertEquals(2, cfg.getFeatureOrigins().size());
assertEquals(id, cfg.getFeatureOrigins().get(0));
assertEquals(id2, cfg.getFeatureOrigins().get(1));
+ assertEquals(2, cfg.getFeatureOrigins(self).size());
+ assertEquals(id, cfg.getFeatureOrigins(self).get(0));
+ assertEquals(id2, cfg.getFeatureOrigins(self).get(1));
assertNull(cfg.getConfigurationProperties().get(Configuration.PROP_FEATURE_ORIGINS));
assertNotNull(cfg.getProperties().get(Configuration.PROP_FEATURE_ORIGINS));
diff --git a/src/test/java/org/apache/sling/feature/FeatureTest.java b/src/test/java/org/apache/sling/feature/FeatureTest.java
new file mode 100644
index 0000000..a8fd2a2
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/FeatureTest.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;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class FeatureTest {
+
+ @Test public void testVariableMetadata() {
+ final ArtifactId self = ArtifactId.parse("self:self:1");
+ final Feature f = new Feature(self);
+
+ f.getVariables().put("a", "foo");
+ final Map<String, Object> metadata = f.getVariableMetadata("a");
+ assertNotNull(metadata);
+
+ assertNull(f.getVariableMetadata("b"));
+ f.getVariables().put("b", "bar");
+ assertNotNull(f.getVariableMetadata("b"));
+
+ metadata.put("hello", "world");
+
+ assertEquals(1, f.getVariableMetadata("a").size());
+
+ f.getVariables().remove("b");
+ assertNull(f.getVariableMetadata("b"));
+ }
+
+ @Test public void testFrameworkPropertiesMetadata() {
+ final ArtifactId self = ArtifactId.parse("self:self:1");
+ final Feature f = new Feature(self);
+
+ f.getFrameworkProperties().put("a", "foo");
+ final Map<String, Object> metadata = f.getFrameworkPropertyMetadata("a");
+ assertNotNull(metadata);
+
+ assertNull(f.getFrameworkPropertyMetadata("b"));
+ f.getFrameworkProperties().put("b", "bar");
+ assertNotNull(f.getFrameworkPropertyMetadata("b"));
+
+ metadata.put("hello", "world");
+
+ assertEquals(1, f.getFrameworkPropertyMetadata("a").size());
+
+ f.getFrameworkProperties().remove("b");
+ assertNull(f.getFrameworkPropertyMetadata("b"));
+ }
+
+ @Test public void testVariableOrigins() {
+ final ArtifactId self = ArtifactId.parse("self:self:1");
+ final Feature f = new Feature(self);
+
+ f.getVariables().put("a", "foo");
+ final Map<String, Object> metadata = f.getVariableMetadata("a");
+
+ assertNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS));
+ assertEquals(1, f.getFeatureOrigins(metadata).size());
+ assertEquals(self, f.getFeatureOrigins(metadata).get(0));
+
+ // single id
+ final ArtifactId id = ArtifactId.parse("g:a:1");
+ f.setFeatureOrigins(metadata, Collections.singletonList(id));
+ assertEquals(1, f.getFeatureOrigins(metadata).size());
+ assertEquals(id, f.getFeatureOrigins(metadata).get(0));
+
+ assertNotNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS));
+ final String[] array = (String[]) metadata.get(Artifact.KEY_FEATURE_ORIGINS);
+ assertArrayEquals(new String[] {id.toMvnId()}, array);
+
+ // add another id
+ final ArtifactId id2 = ArtifactId.parse("g:b:2");
+ f.setFeatureOrigins(metadata, Arrays.asList(id, id2));
+ assertEquals(2, f.getFeatureOrigins(metadata).size());
+ assertEquals(id, f.getFeatureOrigins(metadata).get(0));
+ assertEquals(id2, f.getFeatureOrigins(metadata).get(1));
+
+ assertNotNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS));
+ final String[] array2 = (String[]) metadata.get(Artifact.KEY_FEATURE_ORIGINS);
+ assertArrayEquals(new String[] {id.toMvnId(), id2.toMvnId()}, array2);
+
+ assertSame(metadata, f.getVariableMetadata("a"));
+ }
+
+ @Test public void testFrameworkPropertiesOrigins() {
+ final ArtifactId self = ArtifactId.parse("self:self:1");
+ final Feature f = new Feature(self);
+
+ f.getFrameworkProperties().put("a", "foo");
+ final Map<String, Object> metadata = f.getFrameworkPropertyMetadata("a");
+
+ assertNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS));
+ assertEquals(1, f.getFeatureOrigins(metadata).size());
+ assertEquals(self, f.getFeatureOrigins(metadata).get(0));
+
+ // single id
+ final ArtifactId id = ArtifactId.parse("g:a:1");
+ f.setFeatureOrigins(metadata, Collections.singletonList(id));
+ assertEquals(1, f.getFeatureOrigins(metadata).size());
+ assertEquals(id, f.getFeatureOrigins(metadata).get(0));
+
+ assertNotNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS));
+ final String[] array = (String[]) metadata.get(Artifact.KEY_FEATURE_ORIGINS);
+ assertArrayEquals(new String[] {id.toMvnId()}, array);
+
+ // add another id
+ final ArtifactId id2 = ArtifactId.parse("g:b:2");
+ f.setFeatureOrigins(metadata, Arrays.asList(id, id2));
+ assertEquals(2, f.getFeatureOrigins(metadata).size());
+ assertEquals(id, f.getFeatureOrigins(metadata).get(0));
+ assertEquals(id2, f.getFeatureOrigins(metadata).get(1));
+
+ assertNotNull(metadata.get(Artifact.KEY_FEATURE_ORIGINS));
+ final String[] array2 = (String[]) metadata.get(Artifact.KEY_FEATURE_ORIGINS);
+ assertArrayEquals(new String[] {id.toMvnId(), id2.toMvnId()}, array2);
+
+ assertSame(metadata, f.getFrameworkPropertyMetadata("a"));
+ }
+}
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
index d2f6872..2ad7f55 100644
--- a/src/test/java/org/apache/sling/feature/io/json/FeatureJSONReaderTest.java
+++ b/src/test/java/org/apache/sling/feature/io/json/FeatureJSONReaderTest.java
@@ -113,4 +113,22 @@ public class FeatureJSONReaderTest {
// we only test whether the feature can be read without problems
U.readFeature("feature-model");
}
+
+ @Test
+ public void testReadInternalData() throws Exception {
+ final Feature feature = U.readFeature("test-metadata");
+ assertNotNull(feature);
+ assertNotNull(feature.getId());
+
+ assertEquals("1", feature.getFrameworkProperties().get("foo"));
+ assertEquals("hello", feature.getVariables().get("bar"));
+
+ assertEquals(1, feature.getFrameworkPropertyMetadata("foo").size());
+ assertEquals(true, feature.getFrameworkPropertyMetadata("foo").get("bool"));
+
+ assertEquals(1, feature.getVariableMetadata("bar").size());
+ assertEquals("hello world", feature.getVariableMetadata("bar").get("string"));
+
+ assertNull(feature.getExtensions().getByName(Extension.EXTENSION_NAME_INTERNAL_DATA));
+ }
}
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
index 4d3feca..43b35a6 100644
--- a/src/test/java/org/apache/sling/feature/io/json/FeatureJSONWriterTest.java
+++ b/src/test/java/org/apache/sling/feature/io/json/FeatureJSONWriterTest.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.io.InputStreamReader;
+import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
@@ -132,4 +133,19 @@ public class FeatureJSONWriterTest {
assertEquals(ValueType.TRUE, val.getValueType());
}
}
+
+ @Test
+ public void testWriteInternalData() throws Exception {
+ try ( final Reader reader = new InputStreamReader(U.class.getResourceAsStream("/features/test-metadata.json"), "UTF-8") ) {
+ final JsonObject origJson = Json.createReader(reader).readObject();
+
+ final Feature feature = U.readFeature("test-metadata");
+ try (final StringWriter writer = new StringWriter()) {
+ FeatureJSONWriter.write(writer, feature);
+ final JsonObject resultJson = Json.createReader(new StringReader(writer.toString())).readObject();
+
+ assertEquals(origJson, resultJson);
+ }
+ }
+ }
}
diff --git a/src/test/resources/features/test-metadata.json b/src/test/resources/features/test-metadata.json
new file mode 100644
index 0000000..13dde35
--- /dev/null
+++ b/src/test/resources/features/test-metadata.json
@@ -0,0 +1,22 @@
+{
+ "id" : "org.apache.sling:test-feature:1.1",
+
+ "variables" : {
+ "bar" : "hello"
+ },
+ "framework-properties" : {
+ "foo" : "1"
+ },
+ "feature-internal-data:JSON|true" : {
+ "framework-properties-metadata" : {
+ "foo" : {
+ "bool" : true
+ }
+ },
+ "variables-metadata" : {
+ "bar" : {
+ "string" : "hello world"
+ }
+ }
+ }
+}
\ No newline at end of file