blob: dc1820e56dd417b1808b9a281e10dd30f166cf42 [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.felix.configurator.impl.json;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonStructure;
import javax.json.JsonValue;
import javax.json.JsonValue.ValueType;
import org.apache.felix.configurator.impl.model.BundleState;
import org.apache.felix.configurator.impl.model.Config;
import org.apache.felix.configurator.impl.model.ConfigPolicy;
import org.apache.felix.configurator.impl.model.ConfigurationFile;
import org.osgi.service.configurator.ConfiguratorConstants;
public class JSONUtil {
private static final String INTERNAL_PREFIX = ":configurator:";
private static final String PROP_RANKING = "ranking";
private static final String PROP_POLICY = "policy";
public static final class Report {
public final List<String> warnings = new ArrayList<>();
public final List<String> errors = new ArrayList<>();
}
/**
* Read all configurations from a bundle
* @param provider The bundle provider
* @param paths The paths to read from
* @param report The report for errors and warnings
* @return The bundle state.
*/
public static BundleState readConfigurationsFromBundle(final BinUtil.ResourceProvider provider,
final Set<String> paths,
final Report report) {
final BundleState config = new BundleState();
final List<ConfigurationFile> allFiles = new ArrayList<>();
for(final String path : paths) {
final List<ConfigurationFile> files = readJSON(provider, path, report);
allFiles.addAll(files);
}
Collections.sort(allFiles);
config.addFiles(allFiles);
return config;
}
/**
* Read all json files from a given path in the bundle
*
* @param provider The bundle provider
* @param path The path
* @param report The report for errors and warnings
* @return A list of configuration files - sorted by url, might be empty.
*/
public static List<ConfigurationFile> readJSON(final BinUtil.ResourceProvider provider,
final String path,
final Report report) {
final List<ConfigurationFile> result = new ArrayList<>();
final Enumeration<URL> urls = provider.findEntries(path, "*.json");
if ( urls != null ) {
while ( urls.hasMoreElements() ) {
final URL url = urls.nextElement();
final String filePath = url.getPath();
final int pos = filePath.lastIndexOf('/');
final String name = path + filePath.substring(pos);
try {
final String contents = getResource(name, url);
boolean done = false;
final TypeConverter converter = new TypeConverter(provider);
try {
final ConfigurationFile file = readJSON(converter, name, url, provider.getBundleId(), contents, report);
if ( file != null ) {
result.add(file);
done = true;
}
} finally {
if ( !done ) {
converter.cleanupFiles();
}
}
} catch ( final IOException ioe ) {
report.errors.add("Unable to read " + name + " : " + ioe.getMessage());
}
}
Collections.sort(result);
} else {
report.errors.add("No configurations found at path " + path);
}
return result;
}
/**
* Read a single JSON file
* @param converter type converter
* @param name The name of the file
* @param url The url to that file or {@code null}
* @param bundleId The bundle id of the bundle containing the file
* @param contents The contents of the file
* @param report The report for errors and warnings
* @return The configuration file or {@code null}.
*/
public static ConfigurationFile readJSON(
final TypeConverter converter,
final String name,
final URL url,
final long bundleId,
final String contents,
final Report report) {
final String identifier = (url == null ? name : url.toString());
final JsonObject json = parseJSON(name, contents, report);
final Map<String, ?> configs = verifyJSON(name, json, url != null, report);
if ( configs != null ) {
final List<Config> list = readConfigurationsJSON(converter, bundleId, identifier, configs, report);
if ( !list.isEmpty() ) {
final ConfigurationFile file = new ConfigurationFile(url, list);
return file;
}
}
return null;
}
/**
* Read the configurations JSON
* @param converter The converter to use
* @param bundleId The bundle id
* @param identifier The identifier
* @param configs The map containing the configurations
* @param report The report for errors and warnings
* @return The list of {@code Config}s or {@code null}
*/
public static List<Config> readConfigurationsJSON(final TypeConverter converter,
final long bundleId,
final String identifier,
final Map<String, ?> configs,
final Report report) {
final List<Config> configurations = new ArrayList<>();
for(final Map.Entry<String, ?> entry : configs.entrySet()) {
if ( ! (entry.getValue() instanceof Map) ) {
if ( !entry.getKey().startsWith(INTERNAL_PREFIX) ) {
report.errors.add("Ignoring configuration in '" + identifier + "' (not a configuration) : " + entry.getKey());
}
} else {
@SuppressWarnings("unchecked")
final Map<String, ?> mainMap = (Map<String, ?>)entry.getValue();
final String pid = entry.getKey();
int ranking = 0;
ConfigPolicy policy = ConfigPolicy.DEFAULT;
final Dictionary<String, Object> properties = new OrderedDictionary();
boolean valid = true;
for(final String mapKey : mainMap.keySet()) {
final Object value = mainMap.get(mapKey);
final boolean internalKey = mapKey.startsWith(INTERNAL_PREFIX);
String key = mapKey;
if ( internalKey ) {
key = key.substring(INTERNAL_PREFIX.length());
}
final int pos = key.indexOf(':');
String typeInfo = null;
if ( pos != -1 ) {
typeInfo = key.substring(pos + 1);
key = key.substring(0, pos);
}
if ( internalKey ) {
// no need to do type conversion based on typeInfo for internal props, type conversion is done directly below
if ( key.equals(PROP_RANKING) ) {
final Integer intObj = TypeConverter.getConverter().convert(value).defaultValue(null).to(Integer.class);
if ( intObj == null ) {
report.warnings.add("Invalid ranking for configuration in '" + identifier + "' : " + pid + " - " + value);
} else {
ranking = intObj.intValue();
}
} else if ( key.equals(PROP_POLICY) ) {
final String stringVal = TypeConverter.getConverter().convert(value).defaultValue(null).to(String.class);
if ( stringVal == null ) {
report.errors.add("Invalid policy for configuration in '" + identifier + "' : " + pid + " - " + value);
} else {
if ( value.equals("default") || value.equals("force") ) {
policy = ConfigPolicy.valueOf(stringVal.toUpperCase());
} else {
report.errors.add("Invalid policy for configuration in '" + identifier + "' : " + pid + " - " + value);
}
}
}
} else {
try {
final Object convertedVal = getTypedValue(converter, pid, value, typeInfo);
properties.put(key, convertedVal);
} catch ( final IOException io ) {
report.errors.add("Invalid value/type for configuration in '" + identifier + "' : " + pid + " - " + mapKey + " : " + io.getMessage());
valid = false;
break;
}
}
}
if ( valid ) {
final Config c = new Config(pid, properties, bundleId, ranking, policy);
c.setFiles(converter.flushFiles());
configurations.add(c);
}
}
}
return configurations;
}
public static JsonStructure build(final Object value) {
if (value instanceof List) {
@SuppressWarnings("unchecked")
final List<Object> list = (List<Object>) value;
final JsonArrayBuilder builder = Json.createArrayBuilder();
for (final Object obj : list) {
if (obj instanceof String) {
builder.add(obj.toString());
} else if (obj instanceof Long) {
builder.add((Long) obj);
} else if (obj instanceof Double) {
builder.add((Double) obj);
} else if (obj instanceof Boolean) {
builder.add((Boolean) obj);
} else if (obj instanceof Map) {
builder.add(build(obj));
} else if (obj instanceof List) {
builder.add(build(obj));
}
}
return builder.build();
} else if (value instanceof Map) {
@SuppressWarnings("unchecked")
final Map<String, Object> map = (Map<String, Object>) value;
final JsonObjectBuilder builder = Json.createObjectBuilder();
for (final Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof String) {
builder.add(entry.getKey(), entry.getValue().toString());
} else if (entry.getValue() instanceof Long) {
builder.add(entry.getKey(), (Long) entry.getValue());
} else if (entry.getValue() instanceof Double) {
builder.add(entry.getKey(), (Double) entry.getValue());
} else if (entry.getValue() instanceof Boolean) {
builder.add(entry.getKey(), (Boolean) entry.getValue());
} else if (entry.getValue() instanceof Map) {
builder.add(entry.getKey(), build(entry.getValue()));
} else if (entry.getValue() instanceof List) {
builder.add(entry.getKey(), build(entry.getValue()));
}
}
return builder.build();
}
return null;
}
/**
* Parse a JSON content
* @param name The name of the file
* @param contents The contents
* @param report The report for errors and warnings
* @return The parsed JSON object or {@code null} on failure,
*/
public static JsonObject parseJSON(final String name,
String contents,
final Report report) {
// minify JSON first (remove comments)
try (final Reader in = new StringReader(contents);
final Writer out = new StringWriter()) {
final JSMin min = new JSMin(in, out);
min.jsmin();
contents = out.toString();
} catch ( final IOException ioe) {
report.errors.add("Invalid JSON from " + name);
return null;
}
try (final JsonReader reader = Json.createReader(new StringReader(contents))) {
final JsonStructure obj = reader.read();
if (obj != null && obj.getValueType() == ValueType.OBJECT) {
return (JsonObject) obj;
}
report.errors.add("Invalid JSON from " + name);
}
return null;
}
/**
* Get the value of a JSON property
* @param root The JSON Object
* @param key The key in the JSON Obejct
* @return The value or {@code null}
*/
public static Object getValue(final JsonObject root, final String key) {
if ( !root.containsKey(key) ) {
return null;
}
final JsonValue value = root.get(key);
return getValue(value);
}
public static Object getValue(final JsonValue value) {
switch ( value.getValueType() ) {
// type NULL -> return null
case NULL : return null;
// type TRUE or FALSE -> return boolean
case FALSE : return false;
case TRUE : return true;
// type String -> return String
case STRING : return ((JsonString)value).getString();
// type Number -> return long or double
case NUMBER : final JsonNumber num = (JsonNumber)value;
if (num.isIntegral()) {
return num.longValue();
}
return num.doubleValue();
// type ARRAY -> return list and call this method for each value
case ARRAY : final List<Object> array = new ArrayList<>();
for(final JsonValue x : ((JsonArray)value)) {
array.add(getValue(x));
}
return array;
// type OBJECT -> return map
case OBJECT : final Map<String, Object> map = new LinkedHashMap<>();
final JsonObject obj = (JsonObject)value;
for(final Map.Entry<String, JsonValue> entry : obj.entrySet()) {
map.put(entry.getKey(), getValue(entry.getValue()));
}
return map;
}
return null;
}
public static Object getTypedValue(final TypeConverter converter,
final String pid,
final Object value,
final String typeInfo) throws IOException {
Object convertedVal = converter.convert(pid, value, typeInfo);
if ( convertedVal == null ) {
if ( typeInfo != null ) {
throw new IOException("Unable to convert to type " + typeInfo);
}
JsonStructure json = build(value);
if ( json == null ) {
convertedVal = value.toString();
} else {
// JSON Structure, this will result in a String or in an array of Strings
if ( json.getValueType() == ValueType.ARRAY ) {
final JsonArray arr = (JsonArray)json;
final String[] val = new String[arr.size()];
for(int i=0;i<val.length;i++) {
val[i] = TypeConverter.getConverter().convert(arr.get(i)).to(String.class);
}
convertedVal = val;
} else {
convertedVal = TypeConverter.getConverter().convert(value).to(String.class);
}
}
}
return convertedVal;
}
/**
* Verify the JSON according to the rules
* @param name The JSON name
* @param root The JSON root object.
* @param report The report for errors and warnings
* @return JSON map with configurations or {@code null}
*/
@SuppressWarnings("unchecked")
public static Map<String, ?> verifyJSON(final String name,
final JsonObject root,
final boolean bundleResource,
final Report report) {
if ( root == null ) {
return null;
}
final Object version = getValue(root, ConfiguratorConstants.PROPERTY_RESOURCE_VERSION);
if ( version != null ) {
final int v = TypeConverter.getConverter().convert(version).defaultValue(-1).to(Integer.class);
if ( v == -1 ) {
report.errors.add("Invalid resource version information in " + name + " : " + version);
return null;
}
// we only support version 1
if ( v != 1 ) {
report.errors.add("Invalid resource version number in " + name + " : " + version);
return null;
}
}
if ( !bundleResource) {
// if this is not a bundle resource
// then version and symbolic name must be set
final Object rsrcVersion = getValue(root, ConfiguratorConstants.PROPERTY_VERSION);
if ( rsrcVersion == null ) {
report.errors.add("Missing version information in " + name);
return null;
}
if ( !(rsrcVersion instanceof String) ) {
report.errors.add("Invalid version information in " + name + " : " + rsrcVersion);
return null;
}
final Object rsrcName = getValue(root, ConfiguratorConstants.PROPERTY_SYMBOLIC_NAME);
if ( rsrcName == null ) {
report.errors.add("Missing symbolic name information in " + name);
return null;
}
if ( !(rsrcName instanceof String) ) {
report.errors.add("Invalid symbolic name information in " + name + " : " + rsrcName);
return null;
}
}
return (Map<String, ?>) getValue(root);
}
/**
* Read the contents of a resource, encoded as UTF-8
* @param name The resource name
* @param url The resource URL
* @return The contents
* @throws IOException If anything goes wrong
*/
public static String getResource(final String name, final URL url)
throws IOException {
final URLConnection connection = url.openConnection();
try(final BufferedReader in = new BufferedReader(
new InputStreamReader(
connection.getInputStream(), "UTF-8"))) {
final StringBuilder sb = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
sb.append(line);
sb.append('\n');
}
return sb.toString();
}
}
}