blob: 962756484118559e5cae01f46c8dc7050bca52da [file] [log] [blame]
/*
* 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.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonException;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonStructure;
import jakarta.json.JsonValue;
import jakarta.json.JsonValue.ValueType;
import org.apache.felix.cm.json.io.ConfigurationReader;
import org.apache.felix.cm.json.io.ConfigurationReader.ConfiguratorPropertyHandler;
import org.apache.felix.cm.json.io.ConfigurationResource;
import org.apache.felix.utils.resource.CapabilityImpl;
import org.apache.felix.utils.resource.RequirementImpl;
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.ExtensionState;
import org.apache.sling.feature.ExtensionType;
import org.apache.sling.feature.Extensions;
import org.apache.sling.feature.Feature;
import org.apache.sling.feature.MatchingRequirement;
import org.apache.sling.feature.Prototype;
import org.osgi.resource.Capability;
import org.osgi.resource.Resource;
/**
* This class offers a static method to read a {@code Feature} using a {@code Reader} instance.
*/
public class FeatureJSONReader {
/**
* 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)
throws IOException {
try {
final FeatureJSONReader mr = new FeatureJSONReader(location);
return mr.readFeature(reader);
} catch (final IllegalStateException | IllegalArgumentException | JsonException e) {
throw new IOException(e);
}
}
/** The read feature. */
private Feature feature;
/** The optional location. */
private final String location;
/** Exception prefix containing the location (if set) */
private final String exceptionPrefix;
/**
* private constructor
* @param location Optional location
*/
private FeatureJSONReader(final String location) {
this.location = location;
if ( location == null ) {
exceptionPrefix = "";
} else {
exceptionPrefix = location.concat(" : ");
}
}
/**
* Get the feature id
* @param json The feature json
* @return The artifact id
* @throws IOException If the id is missing
*/
private ArtifactId getFeatureId(final JsonObject json) throws IOException {
if ( !json.containsKey(JSONConstants.FEATURE_ID) ) {
throw new IOException(this.exceptionPrefix.concat("Feature id is missing"));
}
return checkTypeArtifactId(JSONConstants.FEATURE_ID, json.get(JSONConstants.FEATURE_ID));
}
private String getProperty(final JsonObject json, final String key) throws IOException {
final JsonValue val = json.get(key);
if ( val != null ) {
return checkTypeString(key, val);
}
return null;
}
/**
* Read the variables section
* @param json The json describing the feature or application
* @param kvMap The variables will be written to this Key Value Map
* @throws IOException If the json is invalid.
*/
private void readVariables(JsonObject json, Map<String,String> kvMap) throws IOException {
if (json.containsKey(JSONConstants.FEATURE_VARIABLES)) {
final JsonValue variablesObj = json.get(JSONConstants.FEATURE_VARIABLES);
for (final Map.Entry<String, JsonValue> entry : checkTypeObject(JSONConstants.FEATURE_VARIABLES, variablesObj).entrySet()) {
final String key = entry.getKey();
// skip comments
if ( !key.startsWith("#") ) {
if (kvMap.get(key) != null) {
throw new IOException(this.exceptionPrefix.concat("Duplicate variable ").concat(key));
}
final String value = checkScalarType("variable value", entry.getValue(), true);
kvMap.put(key, value);
}
}
}
}
/**
* Read the bundles / start levels section
* @param json The json object describing the feature
* @param container The bundles container
* @param configContainer The configurations container
* @throws IOException If the json is invalid.
*/
private void readBundles(
final JsonObject json,
final Bundles container,
final Configurations configContainer) throws IOException {
if ( json.containsKey(JSONConstants.FEATURE_BUNDLES)) {
final List<Artifact> list = new ArrayList<>();
readArtifacts(JSONConstants.FEATURE_BUNDLES, "bundle", list, json.get(JSONConstants.FEATURE_BUNDLES), configContainer);
for(final Artifact a : list) {
if ( container.containsExact(a.getId())) {
throw new IOException(exceptionPrefix + "Duplicate identical 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);
}
}
}
private void readArtifacts(final String section,
final String artifactType,
final List<Artifact> artifacts,
final JsonValue listObj,
final Configurations container)
throws IOException {
for(final JsonValue entry : checkTypeArray(section, listObj)) {
final Artifact artifact;
checkTypeObjectOrString(artifactType, entry);
if ( entry.getValueType() == ValueType.STRING ) {
// skip comments
if ( ((JsonString)entry).getString().startsWith("#") ) {
continue;
}
artifact = new Artifact(checkTypeArtifactId(artifactType, entry));
} else {
final JsonObject bundleObj = (JsonObject) entry;
if ( !bundleObj.containsKey(JSONConstants.ARTIFACT_ID) ) {
throw new IOException(exceptionPrefix.concat(" ").concat(artifactType).concat(" is missing required artifact id"));
}
final ArtifactId id = checkTypeArtifactId(artifactType.concat(" ").concat(JSONConstants.ARTIFACT_ID), bundleObj.get(JSONConstants.ARTIFACT_ID));
artifact = new Artifact(id);
for(final Map.Entry<String, JsonValue> metadataEntry : bundleObj.entrySet()) {
final String key = metadataEntry.getKey();
// skip comments
if ( key.startsWith("#") ) {
continue;
}
if ( JSONConstants.ARTIFACT_KNOWN_PROPERTIES.contains(key) ) {
continue;
}
final String mval = checkScalarType(artifactType.concat(" metadata ").concat(key), metadataEntry.getValue(), false);
artifact.getMetadata().put(key, mval);
}
if ( bundleObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
final JsonObject cfgs = checkTypeObject(artifactType.concat(" configurations"), bundleObj.get(JSONConstants.FEATURE_CONFIGURATIONS));
addConfigurations(cfgs, artifact, container);
}
}
artifacts.add(artifact);
}
}
private void addConfigurations(final JsonObject json,
final Artifact artifact,
final Configurations container) throws IOException {
final ConfigurationReader reader = org.apache.felix.cm.json.io.Configurations.buildReader()
.verifyAsBundleResource(true)
.withIdentifier(this.location)
.withConfiguratorPropertyHandler(new ConfiguratorPropertyHandler(){
@Override
public void handleConfiguratorProperty(final String pid, final String property, final Object value) {
Configuration cfg = container.getConfiguration(pid);
if ( cfg == null ) {
cfg = new Configuration(pid);
container.add(cfg);
}
cfg.getProperties().put(Configuration.CONFIGURATOR_PREFIX.concat(property), value);
}
})
.build(json);
final ConfigurationResource rsrc = reader.readConfigurationResource();
if ( !reader.getIgnoredErrors().isEmpty() ) {
final StringBuilder builder = new StringBuilder(exceptionPrefix);
builder.append("Errors in configurations:");
for(final String w : reader.getIgnoredErrors()) {
builder.append("\n");
builder.append(w);
}
throw new IOException(builder.toString());
}
for(final Map.Entry<String, Hashtable<String, Object>> c : rsrc.getConfigurations().entrySet()) {
Configuration config = container.getConfiguration(c.getKey());
if ( config == null ) {
config = new Configuration(c.getKey());
container.add(config);
}
for(final Map.Entry<String, Object> prop : c.getValue().entrySet()) {
config.getProperties().put(prop.getKey(), prop.getValue());
}
if ( config.getProperties().get(Configuration.PROP_ARTIFACT_ID) != null ) {
throw new IOException(exceptionPrefix.concat("Configuration must not define property ").concat(Configuration.PROP_ARTIFACT_ID));
}
if ( artifact != null ) {
config.getProperties().put(Configuration.PROP_ARTIFACT_ID, artifact.getId().toMvnId());
}
}
}
private void readConfigurations(final JsonObject json,
final Configurations container) throws IOException {
if ( json.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
final JsonObject cfgs = checkTypeObject(JSONConstants.FEATURE_CONFIGURATIONS, json.get(JSONConstants.FEATURE_CONFIGURATIONS));
addConfigurations(cfgs, null, container);
}
}
private void readFrameworkProperties(final JsonObject json,
final Map<String,String> container) throws IOException {
if ( json.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) {
final JsonValue propsObj= json.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
for(final Map.Entry<String, JsonValue> entry : checkTypeObject(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES, propsObj).entrySet()) {
// skip comments
if ( entry.getKey().startsWith("#") ) {
continue;
}
if ( container.get(entry.getKey()) != null ) {
throw new IOException(this.exceptionPrefix.concat("Duplicate framework property ").concat(entry.getKey()));
}
final String value = checkScalarType("framework property value", entry.getValue(), false);
container.put(entry.getKey(), value);
}
}
}
private void readExtensions(final JsonObject json,
final List<String> keywords,
final Extensions container,
final Configurations configContainer) throws IOException {
final Set<String> keySet = new HashSet<>(json.keySet());
keySet.removeAll(keywords);
// the remaining keys are considered extensions!
for(final String key : keySet) {
if ( key.startsWith("#") ) {
// skip comments
continue;
}
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 state;
if ( pos == -1 ) {
type = ExtensionType.ARTIFACTS.name();
if ( sep == -1 ) {
name = key;
state = ExtensionState.OPTIONAL.name();
} else {
name = key.substring(0, sep);
state = key.substring(sep + 1);
}
} else {
name = key.substring(0, pos);
if ( sep == -1 ) {
type = postfix;
state = ExtensionState.OPTIONAL.name();
} else {
type = postfix.substring(0, sep);
state = postfix.substring(sep + 1);
}
}
if ( JSONConstants.FEATURE_KNOWN_PROPERTIES.contains(name) ) {
throw new IOException(this.exceptionPrefix.concat("Extension is using reserved name : ").concat(name));
}
if ( container.getByName(name) != null ) {
throw new IOException(exceptionPrefix.concat("Duplicate extension with name ").concat(name));
}
final ExtensionType extType = ExtensionType.valueOf(type);
final ExtensionState extState;
if (ExtensionState.OPTIONAL.name().equalsIgnoreCase(state)) {
extState = ExtensionState.OPTIONAL;
} else if (ExtensionState.REQUIRED.name().equalsIgnoreCase(state)) {
extState = ExtensionState.REQUIRED;
} else if (ExtensionState.TRANSIENT.name().equalsIgnoreCase(state)) {
extState = ExtensionState.TRANSIENT;
} else {
final boolean opt = Boolean.valueOf(state).booleanValue();
extState = opt ? ExtensionState.REQUIRED : ExtensionState.OPTIONAL;
}
final Extension ext = new Extension(extType, name, extState);
final JsonValue value = json.get(key);
switch ( extType ) {
case ARTIFACTS : final List<Artifact> list = new ArrayList<>();
readArtifacts("Extension ".concat(name), "artifact", list, value, configContainer);
for(final Artifact a : list) {
if ( ext.getArtifacts().contains(a) ) {
throw new IOException(exceptionPrefix.concat("Duplicate artifact in extension ").concat(name).concat(" : ").concat(a.getId().toMvnId()));
}
ext.getArtifacts().add(a);
}
break;
case JSON : if ( value.getValueType() != ValueType.ARRAY && value.getValueType() != ValueType.OBJECT ) {
throw new IOException(this.exceptionPrefix.concat("JSON Extension ").concat(name).concat(" is neither an object nor an array : ").concat(value.getValueType().name()));
}
ext.setJSONStructure((JsonStructure)value);
break;
case TEXT : if ( value.getValueType() != ValueType.ARRAY && value.getValueType() != ValueType.STRING ) {
throw new IOException(this.exceptionPrefix.concat("Text Extension ").concat(name).concat(" is neither a string nor an array : ").concat(value.getValueType().name()));
}
if ( value.getValueType() == ValueType.STRING ) {
// string
ext.setText(((JsonString)value).getString());
} else {
// list (array of strings)
final StringBuilder sb = new StringBuilder();
for(final JsonValue o : value.asJsonArray()) {
final String textValue = checkTypeString("Text Extension ".concat(name).concat(", value ").concat(o.toString()), o);
sb.append(textValue);
sb.append('\n');
}
ext.setText(sb.toString());
}
break;
}
container.add(ext);
}
}
/**
* Check if the value is a scalar type
* @param key A key for the error message
* @param value The value to check
* @param allowNull Whether null is allowed as value
* @return A string representing the value or {@code null}
* @throws IOException If the value is not of the specified types
*/
private String checkScalarType(final String key, final JsonValue value, boolean allowNull) throws IOException {
if ( allowNull && value.getValueType() == ValueType.NULL ) {
return null;
}
if ( value.getValueType() == ValueType.STRING || value.getValueType() == ValueType.NUMBER || value.getValueType() == ValueType.FALSE || value.getValueType() == ValueType.TRUE ) {
return org.apache.felix.cm.json.io.Configurations.convertToObject(value).toString();
}
throw new IOException(this.exceptionPrefix.concat("Key ").concat(key).concat(" is not one of the allowed types string, number or boolean : ").concat(value.getValueType().name()));
}
/**
* Check if the value is an object or a string
* @param key A key for the error message
* @param value The value to check
* @throws IOException If the value is not of the specified types
*/
private void checkTypeObjectOrString(final String key, final JsonValue value) throws IOException {
if ( value.getValueType() != ValueType.STRING && value.getValueType() != ValueType.OBJECT ) {
throw new IOException(this.exceptionPrefix.concat("Key ").concat(key).concat(" is neither a string nor an object : ").concat(value.getValueType().name()));
}
}
/**
* Check if the value is a boolean
* @param key A key for the error message
* @param value The value to check
* @return The boolean value
* @throws IOException If the value is not of the specified types
*/
private boolean checkTypeBoolean(final String key, final JsonValue value) throws IOException {
if ( value.getValueType() == ValueType.TRUE || value.getValueType() == ValueType.FALSE ) {
return (Boolean)org.apache.felix.cm.json.io.Configurations.convertToObject(value);
}
throw new IOException(this.exceptionPrefix.concat("Key ").concat(key).concat(" is not of type boolean : ").concat(value.getValueType().name()));
}
/**
* Check if the value is an artifact id
* @param key A key for the error message
* @param value The value to check
* @return The artifact id
* @throws IOException If the value is not a string and not a valid artifact id
*/
private ArtifactId checkTypeArtifactId(final String key, final JsonValue value) throws IOException {
final String textValue = checkTypeString(key, value);
try {
return ArtifactId.parse(textValue);
} catch ( final IllegalArgumentException iae) {
throw new IOException(this.exceptionPrefix.concat("Key ").concat(key).concat(" is not a valid artifact id : ").concat(textValue));
}
}
/**
* Check if the value is a string
* @param key A key for the error message
* @param value The value to check
* @return The string value
* @throws IOException If the value is not a string
*/
private String checkTypeString(final String key, final JsonValue value) throws IOException {
if ( value.getValueType() == ValueType.STRING) {
return ((JsonString)value).getString();
}
throw new IOException(this.exceptionPrefix.concat("Key ").concat(key).concat(" is not of type string : ").concat(value.getValueType().name()));
}
/**
* Check if the value is an object
* @param key A key for the error message
* @param value The value to check
* @return The object
* @throws IOException If the value is not an object
*/
private JsonObject checkTypeObject(final String key, final JsonValue value) throws IOException {
if ( value.getValueType() == ValueType.OBJECT) {
return value.asJsonObject();
}
throw new IOException(this.exceptionPrefix.concat("Key ").concat(key).concat(" is not of type object : ").concat(value.getValueType().name()));
}
/**
* Check if the value is an array
* @param key A key for the error message
* @param value The value to check
* @return The array
* @throws IOException If the value is not of the specified types
*/
private JsonArray checkTypeArray(final String key, final JsonValue value) throws IOException {
if ( value.getValueType() == ValueType.ARRAY) {
return value.asJsonArray();
}
throw new IOException(this.exceptionPrefix.concat("Key ").concat(key).concat(" is not of type array : ").concat(value.getValueType().name()));
}
private Prototype readPrototype(final JsonObject json) throws IOException {
if ( json.containsKey(JSONConstants.FEATURE_PROTOTYPE)) {
final JsonValue prototypeObj = json.get(JSONConstants.FEATURE_PROTOTYPE);
checkTypeObjectOrString(JSONConstants.FEATURE_PROTOTYPE, prototypeObj);
final Prototype prototype;
if ( prototypeObj.getValueType() == ValueType.STRING ) {
prototype = new Prototype(checkTypeArtifactId(JSONConstants.FEATURE_PROTOTYPE, prototypeObj));
} else {
final JsonObject obj = (JsonObject) prototypeObj;
if ( !obj.containsKey(JSONConstants.ARTIFACT_ID) ) {
throw new IOException(exceptionPrefix.concat(" prototype is missing required artifact id"));
}
prototype = new Prototype(checkTypeArtifactId("Prototype ".concat(JSONConstants.ARTIFACT_ID), obj.get(JSONConstants.ARTIFACT_ID)));
if ( obj.containsKey(JSONConstants.PROTOTYPE_REMOVALS) ) {
final JsonObject removalObj = checkTypeObject("Prototype removals", obj.get(JSONConstants.PROTOTYPE_REMOVALS));
if ( removalObj.containsKey(JSONConstants.FEATURE_BUNDLES) ) {
for(final JsonValue val : checkTypeArray("Prototype removal bundles", removalObj.get(JSONConstants.FEATURE_BUNDLES))) {
if ( checkTypeString("Prototype removal bundles", val).startsWith("#")) {
continue;
}
prototype.getBundleRemovals().add(checkTypeArtifactId("Prototype removal bundles", val));
}
}
if ( removalObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
for(final JsonValue val : checkTypeArray("Prototype removal configuration", removalObj.get(JSONConstants.FEATURE_CONFIGURATIONS))) {
final String propVal = checkTypeString("Prototype removal configuration", val);
if ( propVal.startsWith("#") ) {
continue;
}
prototype.getConfigurationRemovals().add(propVal);
}
}
if ( removalObj.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) {
for(final JsonValue val : checkTypeArray("Prototype removal framework properties", removalObj.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES))) {
final String propVal = checkTypeString("Prototype removal framework properties", val);
if ( propVal.startsWith("#") ) {
continue;
}
prototype.getFrameworkPropertiesRemovals().add(propVal);
}
}
if ( removalObj.containsKey(JSONConstants.PROTOTYPE_EXTENSION_REMOVALS) ) {
for(final JsonValue val : checkTypeArray("Prototype removal extensions", removalObj.get(JSONConstants.PROTOTYPE_EXTENSION_REMOVALS))) {
checkTypeObjectOrString("Prototype removal extension", val);
if ( val.getValueType() == ValueType.STRING ) {
final String propVal = org.apache.felix.cm.json.io.Configurations.convertToObject(val).toString();
if ( propVal.startsWith("#")) {
continue;
}
prototype.getExtensionRemovals().add(propVal);
} else {
final JsonObject removalMap = (JsonObject)val;
final JsonValue nameObj = removalMap.get("name");
final String name = checkTypeString("Prototype removal extension", nameObj);
if ( removalMap.containsKey("artifacts") ) {
final List<ArtifactId> ids = new ArrayList<>();
for(final JsonValue aid : checkTypeArray("Prototype removal extension artifacts", removalMap.get("artifacts"))) {
if ( checkTypeString("Prototype removal extension artifact", aid).startsWith("#")) {
continue;
}
ids.add(checkTypeArtifactId("Prototype removal extension artifact", aid));
}
prototype.getArtifactExtensionRemovals().put(name, ids);
} else {
prototype.getExtensionRemovals().add(name);
}
}
}
}
readRequirements(removalObj, prototype.getRequirementRemovals());
readCapabilities(removalObj, prototype.getCapabilityRemovals());
}
}
return prototype;
}
return null;
}
private void readRequirements(final JsonObject json, final List<MatchingRequirement> container)
throws IOException {
if ( json.containsKey(JSONConstants.FEATURE_REQUIREMENTS)) {
for(final JsonValue req : checkTypeArray(JSONConstants.FEATURE_REQUIREMENTS, json.get(JSONConstants.FEATURE_REQUIREMENTS))) {
final JsonObject obj = checkTypeObject("Requirement", req);
if ( !obj.containsKey(JSONConstants.REQCAP_NAMESPACE) ) {
throw new IOException(this.exceptionPrefix.concat("Namespace is missing for requirement"));
}
final String namespace = checkTypeString("Requirement namespace", obj.get(JSONConstants.REQCAP_NAMESPACE));
Map<String, Object> attrMap = new HashMap<>();
if ( obj.containsKey(JSONConstants.REQCAP_ATTRIBUTES) ) {
final JsonObject attrs = checkTypeObject("Requirement attributes", obj.get(JSONConstants.REQCAP_ATTRIBUTES));
attrs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalAttribute(key, value, attrMap::put)));
}
Map<String, String> dirMap = new HashMap<>();
if ( obj.containsKey(JSONConstants.REQCAP_DIRECTIVES) ) {
final JsonObject dirs = checkTypeObject("Requirement directives", obj.get(JSONConstants.REQCAP_DIRECTIVES));
dirs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalDirective(key, value, dirMap::put)));
}
final MatchingRequirement r = new MatchingRequirementImpl(null,
namespace, dirMap, attrMap);
container.add(r);
}
}
}
private void readCapabilities(final JsonObject json, final List<Capability> container) throws IOException {
if ( json.containsKey(JSONConstants.FEATURE_CAPABILITIES)) {
for(final JsonValue cap : checkTypeArray(JSONConstants.FEATURE_REQUIREMENTS, json.get(JSONConstants.FEATURE_CAPABILITIES))) {
final JsonObject obj = checkTypeObject("Capability", cap);
if ( !obj.containsKey(JSONConstants.REQCAP_NAMESPACE) ) {
throw new IOException(this.exceptionPrefix.concat("Namespace is missing for capability"));
}
final String namespace = checkTypeString("Capability namespace", obj.get(JSONConstants.REQCAP_NAMESPACE));
Map<String, Object> attrMap = new HashMap<>();
if ( obj.containsKey(JSONConstants.REQCAP_ATTRIBUTES) ) {
final JsonObject attrs = checkTypeObject("Capability attributes", obj.get(JSONConstants.REQCAP_ATTRIBUTES));
attrs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalAttribute(key, value, attrMap::put)));
}
Map<String, String> dirMap = new HashMap<>();
if ( obj.containsKey(JSONConstants.REQCAP_DIRECTIVES) ) {
final JsonObject dirs = checkTypeObject("Capability directives", obj.get(JSONConstants.REQCAP_DIRECTIVES));
dirs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalDirective(key, value, dirMap::put)));
}
final Capability c = new CapabilityImpl(null, namespace, dirMap, attrMap);
container.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;
}
private static class MatchingRequirementImpl extends RequirementImpl implements MatchingRequirement {
public MatchingRequirementImpl(Resource res, String ns, Map<String, String> dirs, Map<String, Object> attrs) {
super(res, ns, dirs, attrs);
}
}
/**
* 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(org.apache.felix.cm.json.io.Configurations.jsonCommentAwareReader(reader)).readObject();
checkModelVersion(json);
final ArtifactId featureId = this.getFeatureId(json);
this.feature = new Feature(featureId);
this.feature.setLocation(this.location);
// final flag
if (json.containsKey(JSONConstants.FEATURE_FINAL)) {
this.feature.setFinal(checkTypeBoolean(JSONConstants.FEATURE_FINAL, json.get(JSONConstants.FEATURE_FINAL)));
}
// complete flag
if (json.containsKey(JSONConstants.FEATURE_COMPLETE)) {
this.feature.setComplete(checkTypeBoolean(JSONConstants.FEATURE_COMPLETE, json.get(JSONConstants.FEATURE_COMPLETE)));
}
// title, description, vendor and license, docURL, scmInfo
this.feature.setTitle(getProperty(json, JSONConstants.FEATURE_TITLE));
this.feature.setDescription(getProperty(json, JSONConstants.FEATURE_DESCRIPTION));
this.feature.setVendor(getProperty(json, JSONConstants.FEATURE_VENDOR));
this.feature.setLicense(getProperty(json, JSONConstants.FEATURE_LICENSE));
this.feature.setDocURL(getProperty(json, JSONConstants.FEATURE_DOC_URL));
this.feature.setSCMInfo(getProperty(json, JSONConstants.FEATURE_SCM_INFO));
// categories
if ( json.containsKey(JSONConstants.FEATURE_CATEGORIES) ) {
for(final JsonValue val : checkTypeArray(JSONConstants.FEATURE_CATEGORIES, json.get(JSONConstants.FEATURE_CATEGORIES))) {
this.feature.getCategories().add(checkTypeString("Categories", val));
}
}
this.readVariables(json, feature.getVariables());
this.readBundles(json, feature.getBundles(), feature.getConfigurations());
this.readFrameworkProperties(json, feature.getFrameworkProperties());
this.readConfigurations(json, feature.getConfigurations());
this.readCapabilities(json, feature.getCapabilities());
this.readRequirements(json, feature.getRequirements());
this.feature.setPrototype(this.readPrototype(json));
this.readExtensions(json,
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;
}
private void checkModelVersion(final JsonObject json) throws IOException {
String modelVersion = getProperty(json, JSONConstants.FEATURE_MODEL_VERSION);
if (modelVersion == null) {
modelVersion = "1";
}
if (!"1".equals(modelVersion)) {
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.io.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.io.Configurations.convertToObject(ve.getValue()));
}
}
} else {
throw new IOException("Unknown data in " + internalData.getName() + " : " + key);
}
}
}
}