/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.configuration.internal; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.node.ArrayNode; import org.codehaus.jackson.node.JsonNodeFactory; import org.codehaus.jackson.node.ObjectNode; import de.rcenvironment.core.configuration.ConfigurationException; import de.rcenvironment.core.configuration.ConfigurationSegment; import de.rcenvironment.core.configuration.WritableConfigurationSegment; import de.rcenvironment.core.utils.common.JsonUtils; /** * Default {@link WritableConfigurationSegment} implementation. * * @author Robert Mischke * @author David Scholz */ class WritableConfigurationSegmentImpl implements WritableConfigurationSegment { private WritableConfigurationSegmentImpl rootSegment; // note: may be null in case of a non-existing segment; private JsonNode segmentRootNode; @SuppressWarnings("unused") private final Log log = LogFactory.getLog(getClass()); WritableConfigurationSegmentImpl(JsonNode treeRoot) { this.segmentRootNode = treeRoot; this.rootSegment = this; } WritableConfigurationSegmentImpl(JsonNode treeRoot, WritableConfigurationSegmentImpl rootSegment) { this.segmentRootNode = treeRoot; this.rootSegment = rootSegment; } @Override public ConfigurationSegment getSubSegment(String relativePath) { JsonNode treeLocation = navigatePath(relativePath); return new WritableConfigurationSegmentImpl(treeLocation, rootSegment); } @Override public WritableConfigurationSegment getOrCreateWritableSubSegment(String relativePath) throws ConfigurationException { JsonNode treeLocation = createPath(relativePath); // create missing elements return new WritableConfigurationSegmentImpl(treeLocation, rootSegment); } @Override public WritableConfigurationSegment createElement(String id) throws ConfigurationException { // at the moment, this is the same, so just delegate // FIXME 7.0.0: check for an existing element for this id, and fail if there is one return getOrCreateWritableSubSegment(id); } @Override public boolean deleteElement(String id) throws ConfigurationException { if (!(segmentRootNode instanceof ObjectNode)) { throw new ConfigurationException("Consistency error: segment node for deleteElement() is not an object node, but " + segmentRootNode.getClass()); } JsonNode removed = ((ObjectNode) segmentRootNode).remove(id); return (removed != null); } @Override public boolean isPresentInCurrentConfiguration() { return segmentRootNode != null; } @Override public String getString(String relativePath) { return getString(relativePath, null); } @Override public String getString(String relativePath, String defaultValue) { JsonNode treeLocation = navigatePath(relativePath); if (treeLocation != null) { return useDefaultValueIfNull(treeLocation.getTextValue(), defaultValue); } else { return defaultValue; } } @Override public void setString(String name, String value) throws ConfigurationException { validateNodeExistsAndIsAnObjectNode(); ((ObjectNode) segmentRootNode).put(name, value); } @Override public Long getLong(String relativePath) { return getLong(relativePath, null); } @Override public Long getLong(String relativePath, Long defaultValue) { JsonNode treeLocation = navigatePath(relativePath); if (treeLocation != null) { if (!treeLocation.isIntegralNumber()) { // TODO (p2) improve: print configuration tree location to make this more user-friendly log.error( "Expected an integer configuration value, but found \"" + treeLocation.asText() + "\"; treating it as the default value \"" + defaultValue + "\""); return defaultValue; } return treeLocation.getLongValue(); } else { return defaultValue; } } @Override public Integer getInteger(String relativePath) { return getInteger(relativePath, null); } @Override public Integer getInteger(String relativePath, Integer defaultValue) { JsonNode treeLocation = navigatePath(relativePath); if (treeLocation != null) { // TODO (p1) >8.0.0 check how this reacts to integer over-/underflows if (!treeLocation.isIntegralNumber()) { // TODO (p2) improve: print configuration tree location to make this more user-friendly log.error( "Expected an integer configuration value, but found \"" + treeLocation.asText() + "\"; treating it as the default value \"" + defaultValue + "\""); return defaultValue; } return treeLocation.getIntValue(); } else { return defaultValue; } } @Override public Double getDouble(String relativePath) { return getDouble(relativePath, null); } @Override public Double getDouble(String relativePath, Double defaultValue) { JsonNode treeLocation = navigatePath(relativePath); if (treeLocation != null) { return useDefaultValueIfNull(treeLocation.getDoubleValue(), defaultValue); } else { return defaultValue; } } @Override public Boolean getBoolean(String relativePath) { return getBoolean(relativePath, null); } @Override public Boolean getBoolean(String relativePath, Boolean defaultValue) { JsonNode treeLocation = navigatePath(relativePath); if (treeLocation != null) { return useDefaultValueIfNull(treeLocation.getBooleanValue(), defaultValue); } else { return defaultValue; } } @Override public void setBoolean(String key, boolean value) throws ConfigurationException { validateNodeExistsAndIsAnObjectNode(); ((ObjectNode) segmentRootNode).put(key, value); } @Override public void setStringArray(String key, String[] value) throws ConfigurationException { validateNodeExistsAndIsAnObjectNode(); final ArrayNode newArrayNode = JsonNodeFactory.instance.arrayNode(); for (String element : value) { newArrayNode.add(element); } ((ObjectNode) segmentRootNode).put(key, newArrayNode); } @Override public void setInteger(String key, Integer value) throws ConfigurationException { validateNodeExistsAndIsAnObjectNode(); ((ObjectNode) segmentRootNode).put(key, value); } @Override public void setLong(String key, Long value) throws ConfigurationException { validateNodeExistsAndIsAnObjectNode(); ((ObjectNode) segmentRootNode).put(key, value); } @Override public List<String> getStringArray(String relativePath) throws ConfigurationException { validateNodeExistsAndIsAnObjectNode(); List<String> list = new ArrayList<String>(); for (JsonNode node : segmentRootNode.path(relativePath)) { list.add(node.asText()); } return list; } @Override public Map<String, ConfigurationSegment> listElements(String relativePath) { Map<String, ConfigurationSegment> resultMap = new HashMap<>(); JsonNode treeLocation = navigatePath(relativePath); if (treeLocation == null) { return new HashMap<>(); // see JavaDoc } Iterator<Entry<String, JsonNode>> iterator = treeLocation.getFields(); while (iterator.hasNext()) { Entry<String, JsonNode> entry = iterator.next(); resultMap.put(entry.getKey(), new WritableConfigurationSegmentImpl(entry.getValue(), rootSegment)); } return resultMap; } @Override public <T> T mapToObject(Class<T> clazz) throws IOException { try { if (segmentRootNode == null) { return clazz.newInstance(); } return JsonUtils.getDefaultObjectMapper().treeToValue(segmentRootNode, clazz); } catch (RuntimeException | InstantiationException | IllegalAccessException e) { throw new IOException("Error parsing configuration", e); } } protected WritableConfigurationSegmentImpl getRootSegment() { return rootSegment; } @Override public String toString() { if (segmentRootNode != null) { return segmentRootNode.toString(); } else { return "<null root node>"; } } protected JsonNode getSegmentRootNode() { return segmentRootNode; } private JsonNode navigatePath(String relativePath) { if (segmentRootNode == null) { return null; } JsonNode treeLocation = segmentRootNode; for (String pathSegment : relativePath.split("/")) { @SuppressWarnings("unused") JsonNode oldTreeLocation = treeLocation; // only for debug output treeLocation = treeLocation.get(pathSegment); // log.debug(StringUtils.format("Traversing JSON tree by path segment '%s': %s -> %s", pathSegment, oldTreeLocation, // treeLocation)); if (treeLocation == null) { return null; } } return treeLocation; } private JsonNode createPath(String relativePath) throws ConfigurationException { if (segmentRootNode == null) { throw new ConfigurationException("Tried to create a new configuration segment from a non-existing one"); } JsonNode treeLocation = segmentRootNode; for (String pathSegment : relativePath.split("/")) { final JsonNode oldTreeLocation = treeLocation; treeLocation = oldTreeLocation.get(pathSegment); // log.debug(StringUtils.format("Traversing JSON tree by path segment '%s': %s -> %s", pathSegment, oldTreeLocation, // treeLocation)); if (treeLocation == null) { final ObjectNode newObjectNode = JsonNodeFactory.instance.objectNode(); ((ObjectNode) oldTreeLocation).put(pathSegment, newObjectNode); treeLocation = newObjectNode; } } return treeLocation; } private void validateNodeExistsAndIsAnObjectNode() throws ConfigurationException { if (!isPresentInCurrentConfiguration()) { throw new ConfigurationException("The parent segment must exist before new fields can be added"); } if (!segmentRootNode.isObject()) { throw new ConfigurationException("The parent segment does not point to a valid configuration (JSON) node"); } } private <T> T useDefaultValueIfNull(T value, T defaultValue) { if (value != null) { return value; } else { return defaultValue; } } }