blob: 286e93e70ff44714b935f3175193ae73d310a0ce [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.cm.json.impl;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javax.json.JsonException;
import javax.json.JsonObject;
import javax.json.JsonValue;
import javax.json.JsonValue.ValueType;
import org.apache.felix.cm.json.ConfigurationReader;
import org.apache.felix.cm.json.ConfigurationResource;
import org.apache.felix.cm.json.Configurations;
import org.osgi.service.configurator.ConfiguratorConstants;
import org.osgi.util.converter.Converters;
public class ConfigurationReaderImpl
implements ConfigurationReader, ConfigurationReader.Builder {
private boolean closed = false;
private Reader reader;
private JsonObject jsonObject;
private boolean verifyAsBundleResource = false;
private String identifier;
private final List<String> errors = new ArrayList<>();
private BinaryHandler binaryHandler;
private ConfiguratorPropertyHandler propertyHandler;
@Override
public Builder verifyAsBundleResource(final boolean flag) {
this.verifyAsBundleResource = flag;
return this;
}
@Override
public Builder withIdentifier(final String value) {
this.identifier = value;
return this;
}
@Override
public Builder withBinaryHandler(final BinaryHandler handler) {
this.binaryHandler = handler;
return this;
}
@Override
public Builder withConfiguratorPropertyHandler(final ConfiguratorPropertyHandler handler) {
this.propertyHandler = handler;
return this;
}
@Override
public ConfigurationReader build(final Reader reader) {
this.reader = reader;
return this;
}
@Override
public ConfigurationReader build(final JsonObject object) {
this.jsonObject = object;
return this;
}
@Override
public List<String> getIgnoredErrors() {
return this.errors;
}
private void checkClosed() throws IOException {
if (this.closed) {
throwIOException("Reader already closed");
}
closed = true;
}
@Override
public Hashtable<String, Object> readConfiguration() throws IOException {
checkClosed();
if (this.reader != null) {
try {
this.jsonObject = JsonSupport.parseJson(this.identifier, this.reader);
} catch ( final JsonException jpe) {
throw new IOException("Invalid JSON " + jpe.getMessage(), jpe);
}
}
return readSingleConfiguration("<configuration>", this.jsonObject);
}
@Override
public ConfigurationResource readConfigurationResource() throws IOException {
checkClosed();
if (this.reader != null) {
try {
this.jsonObject = JsonSupport.parseJson(this.identifier, this.reader);
} catch ( final JsonException jpe) {
throw new IOException("Invalid JSON " + jpe.getMessage(), jpe);
}
}
verifyJsonResource();
final ConfigurationResource resource = new ConfigurationResource();
for (final Map.Entry<String, JsonValue> entry : this.jsonObject.entrySet()) {
if (entry.getKey().startsWith(ConfigurationResource.CONFIGURATOR_PROPERTY_PREFIX)) {
// internal property
resource.getProperties().put(entry.getKey(), JsonSupport.convertToObject(entry.getValue()));
} else if (entry.getValue().getValueType() != ValueType.OBJECT) {
addError("Ignoring property (not a configuration) : ".concat(entry.getKey()));
resource.getProperties().put(entry.getKey(), JsonSupport.convertToObject(entry.getValue()));
} else {
final Hashtable<String, Object> properties = readSingleConfiguration(entry.getKey(),
entry.getValue().asJsonObject());
if (properties != null) {
resource.getConfigurations().put(entry.getKey(), properties);
}
}
}
return resource;
}
private void addError(final String msg) {
if (this.identifier == null) {
this.errors.add(msg);
} else {
this.errors.add(identifier.concat(" : ").concat(msg));
}
}
private void throwIOException(final String msg) throws IOException {
if (this.identifier == null) {
throw new IOException(msg);
}
throw new IOException(this.identifier.concat(" : ").concat(msg));
}
/**
* Verify the JSON according to the rules
*
* @param root The JSON root object.
*/
private void verifyJsonResource() throws IOException {
final Object version = JsonSupport
.convertToObject(this.jsonObject.get(ConfiguratorConstants.PROPERTY_RESOURCE_VERSION));
if (version != null) {
final int v = Converters.standardConverter().convert(version).defaultValue(-1).to(Integer.class);
if (v == -1) {
throwIOException("Invalid resource version information : ".concat(version.toString()));
}
// we only support version 1
if (v != 1) {
throwIOException("Unknown resource version : ".concat(version.toString()));
}
}
if (!verifyAsBundleResource) {
// if this is not a bundle resource
// then version and symbolic name must be set
final Object rsrcVersion = JsonSupport
.convertToObject(this.jsonObject.get(ConfiguratorConstants.PROPERTY_VERSION));
if (rsrcVersion == null) {
throwIOException("Missing version information");
}
if (!(rsrcVersion instanceof String)) {
throwIOException("Invalid version information : ".concat(rsrcVersion.toString()));
}
final Object rsrcName = JsonSupport
.convertToObject(this.jsonObject.get(ConfiguratorConstants.PROPERTY_SYMBOLIC_NAME));
if (rsrcName == null) {
throwIOException("Missing symbolic name information");
}
if (!(rsrcName instanceof String)) {
throwIOException("Invalid symbolic name information : ".concat(rsrcVersion.toString()));
}
}
}
public static final class KeyInfo {
public final String propertyKey;
public final String typeInfo;
public final boolean isInternal;
public final boolean isBinary;
public KeyInfo(final String mapKey) {
this.isInternal = mapKey.startsWith(ConfigurationResource.CONFIGURATOR_PROPERTY_PREFIX);
String key = mapKey;
if (isInternal) {
key = key.substring(ConfigurationResource.CONFIGURATOR_PROPERTY_PREFIX.length());
}
final int pos = key.indexOf(':');
String typeInfo = null;
if (pos != -1) {
typeInfo = key.substring(pos + 1);
key = key.substring(0, pos);
}
this.propertyKey = key;
this.typeInfo = typeInfo;
this.isBinary = TypeConverter.TYPE_BINARY.equals(typeInfo) || TypeConverter.TYPE_BINARIES.equals(typeInfo);
}
}
/**
* Read a single configuration
*
* @param pid The configuration pid
* @param propertyMap The configuration map
* @return A valid configuration dictionary or {@code null}
* @throws IOException If reading fails
*/
private Hashtable<String, Object> readSingleConfiguration(final String pid,
final JsonObject propertyMap) throws IOException {
final Hashtable<String, Object> properties = Configurations.newConfiguration();
boolean valid = true;
for (final Map.Entry<String, JsonValue> propEntry : propertyMap.entrySet()) {
final String mapKey = propEntry.getKey();
final KeyInfo keyInfo = new KeyInfo(mapKey);
if ( keyInfo.isBinary ) {
if ( !this.verifyAsBundleResource) {
throwIOException("PID ".concat(pid)
.concat(" : Properties of type binary not allowed for non bundle resource : ")
.concat(keyInfo.propertyKey));
}
if ( this.binaryHandler == null ) {
throwIOException("PID ".concat(pid)
.concat(" : No handler configured for binary property : ")
.concat(keyInfo.propertyKey));
}
if ( keyInfo.isInternal ) {
throwIOException("PID ".concat(pid)
.concat(" : Binary values are not allowed for configurator properties : ")
.concat(keyInfo.propertyKey));
}
}
if ( keyInfo.isInternal ) {
final Object value = JsonSupport.convertToObject(propEntry.getValue());
if ( propertyHandler == null ) {
properties.put(keyInfo.propertyKey, value);
} else {
propertyHandler.handleConfiguratorProperty(pid, keyInfo.propertyKey, value);
}
} else {
// convert value
Object convertedVal = TypeConverter.convertObjectToType(propEntry.getValue(), keyInfo.typeInfo);
if ( convertedVal == TypeConverter.CONVERSION_FAILED ) {
final String msg = "PID ".concat(pid).concat(" : Invalid value/type for configuration : ")
.concat(mapKey).concat(" : ").concat(propEntry.getValue().toString());
if (!this.verifyAsBundleResource) {
throwIOException(msg);
}
addError(msg);
valid = false;
break;
}
// special handling for binary and binary[]
if (keyInfo.isBinary) {
// String or String[] ?
if ( convertedVal instanceof String ) {
final String path = (String)convertedVal;
convertedVal = this.binaryHandler.handleBinaryValue(pid, keyInfo.propertyKey, path);
} else {
final String[] paths = (String[])convertedVal;
final String[] array = new String[paths.length];
convertedVal = array;
for(int i=0;i<paths.length;i++) {
array[i] = this.binaryHandler.handleBinaryValue(pid, keyInfo.propertyKey, paths[i]);
if ( array[i] == null ) {
convertedVal = null;
}
}
}
}
// if ( convertedVal is null) this configuration is invalid
if ( convertedVal == null ) {
valid = false;
} else {
properties.put(keyInfo.propertyKey, convertedVal);
}
}
}
return valid ? properties : null;
}
}