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