/*
* 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.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonObject;
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.TypeConverter;
import org.apache.felix.configurator.impl.Util;
import org.apache.felix.configurator.impl.logger.SystemLogger;
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.framework.Bundle;
public class JSONUtil {
private static final String INTERNAL_PREFIX = ":configurator:";
private static final String PROP_VERSION = INTERNAL_PREFIX + "json-version";
private static final String PROP_RANKING = "ranking";
private static final String PROP_POLICY = "policy";
public static BundleState readConfigurationsFromBundle(final Bundle bundle, final Set<String> paths) {
final BundleState config = new BundleState();
final List<ConfigurationFile> allFiles = new ArrayList<>();
for(final String path : paths) {
final List<ConfigurationFile> files = readJSON(bundle, path);
allFiles.addAll(files);
}
Collections.sort(allFiles);
config.addFiles(allFiles);
return config;
}
/**
* Read all json files from a given path in the bundle
* @param bundle The bundle
* @param path The path
* @return A list of configuration files - sorted by url, might be empty.
*/
public static List<ConfigurationFile> readJSON(final Bundle bundle, final String path) {
final List<ConfigurationFile> result = new ArrayList<>();
final Enumeration<URL> urls = bundle.findEntries(path, "*.json", false);
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);
final String contents = Util.getResource(name, url);
if ( contents != null ) {
boolean done = false;
final TypeConverter converter = new TypeConverter(bundle);
try {
final ConfigurationFile file = readJSON(converter, name, url, bundle.getBundleId(), contents);
if ( file != null ) {
result.add(file);
done = true;
}
} finally {
if ( !done ) {
converter.cleanupFiles();
}
}
}
}
Collections.sort(result);
} else {
SystemLogger.error("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
* @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 String identifier = (url == null ? name : url.toString());
final JsonObject json = parseJSON(name, contents);
final Map<String, ?> configs = verifyJSON(name, json);
if ( configs != null ) {
final List<Config> configurations = new ArrayList<>();
for(final Map.Entry<String, ?> entry : configs.entrySet()) {
if ( ! (entry.getValue() instanceof JsonObject) ) {
SystemLogger.error("Ignoring configuration in '" + identifier + "' (not a configuration) : " + entry.getKey());
} else {
final JsonObject mainMap = (JsonObject)entry.getValue();
final int envIndex = entry.getKey().indexOf('[');
if ( envIndex != -1 && !entry.getKey().endsWith("]") ) {
SystemLogger.error("Ignoring configuration in '" + identifier + "' (invalid environments definition) : " + entry.getKey());
continue;
}
final String pid;
final Set<String> environments;
if ( envIndex == -1 ) {
pid = entry.getKey();
environments = null;
} else {
pid = entry.getKey().substring(0, envIndex);
environments = new HashSet<>(Arrays.asList(entry.getKey().substring(envIndex + 1, entry.getKey().length()).split(",")));
if ( environments.isEmpty() ) {
SystemLogger.warning("Invalid environments for configuration in '" + identifier + "' : " + pid);
}
}
int ranking = 0;
ConfigPolicy policy = ConfigPolicy.DEFAULT;
final Dictionary<String, Object> properties = new Hashtable<>();
boolean valid = true;
for(final String mapKey : mainMap.keySet()) {
final Object value = getValue(mainMap, 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 ) {
SystemLogger.warning("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 ) {
SystemLogger.error("Invalid policy for configuration in '" + identifier + "' : " + pid + " - " + value);
} else {
if ( value.equals("default") || value.equals("force") ) {
policy = ConfigPolicy.valueOf(stringVal.toUpperCase());
} else {
SystemLogger.error("Invalid policy for configuration in '" + identifier + "' : " + pid + " - " + value);
}
}
}
} else {
try {
Object convertedVal = converter.convert(pid, value, typeInfo);
if ( convertedVal == null ) {
convertedVal = value.toString();
}
properties.put(mapKey, convertedVal);
} catch ( final IOException io ) {
SystemLogger.error("Invalid value/type for configuration in '" + identifier + "' : " + pid + " - " + mapKey);
valid = false;
break;
}
}
}
if ( valid ) {
final Config c = new Config(pid, environments, properties, bundleId, ranking, policy);
c.setFiles(converter.flushFiles());
configurations.add(c);
}
}
}
final ConfigurationFile file = new ConfigurationFile(url, configurations);
return file;
}
return null;
}
/**
* Parse a JSON content
* @param name The name of the file
* @param contents The contents
* @return The parsed JSON object or {@code null} on failure,
*/
public static JsonObject parseJSON(final String name, String contents) {
// 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) {
SystemLogger.error("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;
}
SystemLogger.error("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 object
case OBJECT : return value;
}
return null;
}
/**
* Verify the JSON according to the rules
* @param name The JSON name
* @param root The JSON root object.
* @return JSON map with configurations or {@code null}
*/
@SuppressWarnings("unchecked")
public static Map<String, ?> verifyJSON(final String name, final JsonObject root) {
if ( root == null ) {
return null;
}
final Object version = getValue(root, PROP_VERSION);
if ( version != null ) {
final int v = TypeConverter.getConverter().convert(version).defaultValue(-1).to(Integer.class);
if ( v == -1 ) {
SystemLogger.error("Invalid version information in " + name + " : " + version);
return null;
}
// we only support version 1
if ( v != 1 ) {
SystemLogger.error("Invalid version number in " + name + " : " + version);
return null;
}
}
final Object configs = getValue(root, "configurations");
if ( configs == null ) {
// short cut, we just return false as we don't have to process this file
return null;
}
if ( !(configs instanceof Map) ) {
SystemLogger.error("Configurations must be a map of configurations in " + name);
return null;
}
return (Map<String, ?>) configs;
}
}