Author: oheger Date: Wed Jun 21 20:00:25 2017 New Revision: 1799501 URL: http://svn.apache.org/viewvc?rev=1799501&view=rev Log: [CONFIGURATION-258] Support for JSON and YAML configurations.
Thanks to The Alchemist (kap4020 at gmail dot com) for the patch. Added: commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/AbstractMapBasedConfiguration.java commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/JSONConfiguration.java commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/YAMLConfiguration.java commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration2/TestJSONConfiguration.java commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration2/TestYAMLConfiguration.java commons/proper/configuration/trunk/src/test/resources/test.json commons/proper/configuration/trunk/src/test/resources/test.yaml Modified: commons/proper/configuration/trunk/pom.xml Modified: commons/proper/configuration/trunk/pom.xml URL: http://svn.apache.org/viewvc/commons/proper/configuration/trunk/pom.xml?rev=1799501&r1=1799500&r2=1799501&view=diff ============================================================================== --- commons/proper/configuration/trunk/pom.xml (original) +++ commons/proper/configuration/trunk/pom.xml Wed Jun 21 20:00:25 2017 @@ -469,6 +469,22 @@ <version>${slf4j.version}</version> <scope>test</scope> </dependency> + + + <dependency> + <groupId>org.yaml</groupId> + <artifactId>snakeyaml</artifactId> + <version>1.18</version> + <optional>true</optional> + </dependency> + + + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>2.5.3</version> + <optional>true</optional> + </dependency> </dependencies> <properties> Added: commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/AbstractMapBasedConfiguration.java URL: http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/AbstractMapBasedConfiguration.java?rev=1799501&view=auto ============================================================================== --- commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/AbstractMapBasedConfiguration.java (added) +++ commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/AbstractMapBasedConfiguration.java Wed Jun 21 20:00:25 2017 @@ -0,0 +1,92 @@ +package org.apache.commons.configuration2; + +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.commons.configuration2.io.ConfigurationLogger; +import org.apache.commons.configuration2.tree.ImmutableNode; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author The-Alchemist + */ +public class AbstractMapBasedConfiguration extends BaseHierarchicalConfiguration { + + public AbstractMapBasedConfiguration() { + super(); + + initLogger(new ConfigurationLogger(getClass())); + } + + protected void load(Map<String, Object> map) { + ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder(); + ImmutableNode top = constructHierarchy(rootBuilder, map); + getNodeModel().setRootNode(top); + } + + /** + * Constructs a YAML map, i.e. String -> Object from a given + * configuration node. + * + * @param node The configuration node to create a map from. + * @return A Map that contains the configuration node information. + */ + protected Map<String, Object> constructMap(ImmutableNode node) + { + Map<String, Object> map = new HashMap<String, Object>(node.getChildren().size()); + for (ImmutableNode cNode : node.getChildren()) + { + if (cNode.getChildren().isEmpty()) + { + map.put(cNode.getNodeName(), cNode.getValue()); + } + else + { + map.put(cNode.getNodeName(), constructMap(cNode)); + } + } + return map; + } + + /** + * Constructs the internal configuration nodes hierarchy. + * @param parent The configuration node that is the root of the current configuration section. + * @param map The map with the yaml configurations nodes, i.e. String -> Object. + */ + protected ImmutableNode constructHierarchy(ImmutableNode.Builder parent, Map<String, Object> map) + { + for (Map.Entry<String, Object> entry : map.entrySet()) + { + String key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof Map) + { + ImmutableNode.Builder subtree = new ImmutableNode.Builder() + .name(key); + ImmutableNode children = constructHierarchy(subtree, (Map) value); + parent.addChild(children); + } + else + { + ImmutableNode leaf = new ImmutableNode.Builder() + .name(key) + .value(value) + .create(); + parent.addChild(leaf); + } + } + return parent.create(); + } + + + static void rethrowException(Exception e) throws ConfigurationException { + if(e instanceof ClassCastException) + { + throw new ConfigurationException("Error parsing", e); + } + else + { + throw new ConfigurationException("Unable to load the configuration", e); + } + } +} Added: commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/JSONConfiguration.java URL: http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/JSONConfiguration.java?rev=1799501&view=auto ============================================================================== --- commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/JSONConfiguration.java (added) +++ commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/JSONConfiguration.java Wed Jun 21 20:00:25 2017 @@ -0,0 +1,117 @@ +/* + * 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.commons.configuration2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.MapType; +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.commons.configuration2.io.InputStreamSupport; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.Map; + +/** + * <p> + * A specialized hierarchical configuration class that is able to parse JSON + * documents. + * </p> + * + * @author The-Alchemist + * + * @since commons-configuration2 2.2.? + * @version $$ + */ + +public class JSONConfiguration extends AbstractMapBasedConfiguration implements + FileBasedConfiguration, InputStreamSupport +{ + + private final ObjectMapper mapper = new ObjectMapper(); + private final MapType type = mapper.getTypeFactory().constructMapType(Map.class, String.class, Object.class); + + + /** + * Creates a new instance of {@code YAMLConfiguration}. + */ + public JSONConfiguration() { + super(); + } + + @Override + public void read(Reader in) throws ConfigurationException + { + try + { + Map<String, Object> map = mapper.readValue(in, this.type); + load(map); + } + catch (Exception e) + { + rethrowException(e); + } + } + + private String readFully(Reader in) { + try (BufferedReader r = new BufferedReader(in)) { + String str = null; + StringBuilder sb = new StringBuilder(); + while((str =r.readLine())!=null) + { + sb.append(str); + } + return sb.toString(); + } catch(IOException e) { + throw new RuntimeException(e); + } + + } + + @Override + public void write(Writer out) throws ConfigurationException, IOException + { + this.mapper.writer().writeValue(out, constructMap(this.getNodeModel().getNodeHandler().getRootNode())); + } + + + /** + * Loads the configuration from the given input stream. + * + * @param in the input stream + * @throws ConfigurationException if an error occurs + */ + @Override + public void read(InputStream in) throws ConfigurationException + { + try + { + Map<String, Object> map = mapper.readValue(in, this.type); + load(map); + } + catch (Exception e) + { + rethrowException(e); + } + } + + + +} \ No newline at end of file Added: commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/YAMLConfiguration.java URL: http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/YAMLConfiguration.java?rev=1799501&view=auto ============================================================================== --- commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/YAMLConfiguration.java (added) +++ commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration2/YAMLConfiguration.java Wed Jun 21 20:00:25 2017 @@ -0,0 +1,141 @@ +/* + * 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.commons.configuration2; + +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.commons.configuration2.io.ConfigurationLogger; +import org.apache.commons.configuration2.io.InputStreamSupport; +import org.apache.commons.configuration2.tree.ImmutableNode; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; + +/** + * <p> + * A specialized hierarchical configuration class that is able to parse YAML + * documents. + * </p> + * + * @author The-Alchemist + * + * @since commons-configuration2 2.2.? + * @version $$ + */ + +public class YAMLConfiguration extends AbstractMapBasedConfiguration implements + FileBasedConfiguration, InputStreamSupport +{ + + /** + * Creates a new instance of {@code YAMLConfiguration}. + */ + public YAMLConfiguration() + { + super(); + } + + + @Override + public void read(Reader in) throws ConfigurationException + { + try + { + Yaml yaml = new Yaml(); + Map<String, Object> map = (Map) yaml.load(in); + load(map); + } + catch (Exception e) + { + rethrowException(e); + } + } + + + public void read(Reader in, LoaderOptions options) throws ConfigurationException + { + try + { + Yaml yaml = new Yaml(options); + Map<String, Object> map = (Map) yaml.load(in); + load(map); + } + catch (Exception e) + { + rethrowException(e); + } + } + + @Override + public void write(Writer out) throws ConfigurationException, IOException + { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + dump(out, options); + } + + public void dump(Writer out, DumperOptions options) throws ConfigurationException, IOException + { + Yaml yaml = new Yaml(options); + yaml.dump(constructMap(getNodeModel().getNodeHandler().getRootNode()), out); + } + + + /** + * Loads the configuration from the given input stream. + * + * @param in the input stream + * @throws ConfigurationException if an error occurs + */ + @Override + public void read(InputStream in) throws ConfigurationException + { + try + { + Yaml yaml = new Yaml(); + Map<String, Object> map = (Map) yaml.load(in); + load(map); + } + catch (Exception e) + { + rethrowException(e); + } + } + + public void read(InputStream in, LoaderOptions options) throws ConfigurationException + { + try + { + Yaml yaml = new Yaml(options); + Map<String, Object> map = (Map) yaml.load(in); + load(map); + } + catch (Exception e) + { + rethrowException(e); + } + } + + +} \ No newline at end of file Added: commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration2/TestJSONConfiguration.java URL: http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration2/TestJSONConfiguration.java?rev=1799501&view=auto ============================================================================== --- commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration2/TestJSONConfiguration.java (added) +++ commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration2/TestJSONConfiguration.java Wed Jun 21 20:00:25 2017 @@ -0,0 +1,123 @@ +package org.apache.commons.configuration2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.MapType; +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Unit test for {@link JSONConfiguration} + * + * Not ideal: it uses the Jackson JSON parser just like {@link JSONConfiguration} itself + * @author The-Alchemist + */ +public class TestJSONConfiguration { + /** The files that we test with. */ + private String testJson = ConfigurationAssert.getTestFile("test.json").getAbsolutePath(); + private File testSaveConf = ConfigurationAssert.getOutFile("testsave.json"); + + private JSONConfiguration jsonConfiguration; + + @Before + public void setUp() throws Exception + { + jsonConfiguration = new JSONConfiguration(); + jsonConfiguration.read(new FileReader(testJson)); + removeTestFile(); + } + + @Test + public void testGetProperty_simple() + { + assertEquals("value1", jsonConfiguration.getProperty("key1")); + } + + @Test + public void testGetProperty_nested() + { + assertEquals("value23", jsonConfiguration.getProperty("key2.key3")); + } + + @Test + public void testGetProperty_nested_with_list() + { + assertEquals(Arrays.asList("col1", "col2"), jsonConfiguration.getProperty("key4.key5")); + } + + @Test + public void testGetProperty_subset() + { + Configuration subset = jsonConfiguration.subset("key4"); + assertEquals(Arrays.asList("col1", "col2"), subset.getProperty("key5")); + } + + + @Test + public void testGetProperty_very_nested_properties() + { + Object property = jsonConfiguration.getProperty("very.nested.properties"); + assertEquals(Arrays.asList("nested1", "nested2", "nested3"), property); + } + + @Test + public void testGetProperty_integer() + { + Object property = jsonConfiguration.getProperty("int1"); + assertTrue("property should be an Integer", property instanceof Integer); + assertEquals(37, property); + } + + @Test + public void testSave() throws IOException, ConfigurationException { + // save the Configuration as a String... + StringWriter sw = new StringWriter(); + jsonConfiguration.write(sw); + String output = sw.toString(); + + // ..and then try parsing it back + ObjectMapper mapper = new ObjectMapper(); + MapType type = mapper.getTypeFactory().constructMapType(Map.class, String.class, Object.class); + Map<String, Object> parsed = mapper.readValue(output, type); + assertEquals(6, parsed.entrySet().size()); + assertEquals("value1", parsed.get("key1")); + + Map key2 = (Map) parsed.get("key2"); + assertEquals("value23", key2.get("key3")); + + List<String> key5 = (List<String>) ((Map) parsed.get("key4")).get("key5"); + assertEquals(2, key5.size()); + assertEquals("col1", key5.get(0)); + assertEquals("col2", key5.get(1)); + } + + @Test + public void testGetProperty_dictionary() + { + assertEquals("Martin D'vloper", jsonConfiguration.getProperty("martin.name")); + assertEquals("Developer", jsonConfiguration.getProperty("martin.job")); + assertEquals("Elite", jsonConfiguration.getProperty("martin.skill")); + } + + /** + * Removes the test output file if it exists. + */ + private void removeTestFile() + { + if (testSaveConf.exists()) + { + assertTrue(testSaveConf.delete()); + } + } + +} \ No newline at end of file Added: commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration2/TestYAMLConfiguration.java URL: http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration2/TestYAMLConfiguration.java?rev=1799501&view=auto ============================================================================== --- commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration2/TestYAMLConfiguration.java (added) +++ commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration2/TestYAMLConfiguration.java Wed Jun 21 20:00:25 2017 @@ -0,0 +1,123 @@ +package org.apache.commons.configuration2; + +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.junit.Before; +import org.junit.Test; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +/** + * Unit test for {@link YAMLConfiguration} + * + * @author The-Alchemist + */ +public class TestYAMLConfiguration +{ + /** The files that we test with. */ + private String testYaml = ConfigurationAssert.getTestFile("test.yaml").getAbsolutePath(); + private File testSaveConf = ConfigurationAssert.getOutFile("testsave.yaml"); + + private YAMLConfiguration yamlConfiguration; + + @Before + public void setUp() throws Exception + { + yamlConfiguration = new YAMLConfiguration(); + yamlConfiguration.read(new FileReader(testYaml)); + removeTestFile(); + } + + @Test + public void testGetProperty_simple() + { + assertEquals("value1", yamlConfiguration.getProperty("key1")); + } + + @Test + public void testGetProperty_nested() + { + assertEquals("value23", yamlConfiguration.getProperty("key2.key3")); + } + + @Test + public void testGetProperty_nested_with_list() + { + assertEquals(Arrays.asList("col1", "col2"), yamlConfiguration.getProperty("key4.key5")); + } + + @Test + public void testGetProperty_subset() + { + Configuration subset = yamlConfiguration.subset("key4"); + assertEquals(Arrays.asList("col1", "col2"), subset.getProperty("key5")); + } + + + @Test + public void testGetProperty_very_nested_properties() + { + Object property = yamlConfiguration.getProperty("very.nested.properties"); + assertEquals(Arrays.asList("nested1", "nested2", "nested3"), property); + } + + @Test + public void testGetProperty_integer() + { + Object property = yamlConfiguration.getProperty("int1"); + assertTrue("property should be an Integer", property instanceof Integer); + assertEquals(37, property); + } + + + @Test + public void testSave() throws IOException, ConfigurationException + { + // save the YAMLConfiguration as a String... + StringWriter sw = new StringWriter(); + yamlConfiguration.write(sw); + String output = sw.toString(); + + // ..and then try parsing it back as using SnakeYAML + Map parsed = new Yaml().loadAs(output, Map.class); + assertEquals(6, parsed.entrySet().size()); + assertEquals("value1", parsed.get("key1")); + + Map key2 = (Map) parsed.get("key2"); + assertEquals("value23", key2.get("key3")); + + List<String> key5 = (List<String>) ((Map) parsed.get("key4")).get("key5"); + assertEquals(2, key5.size()); + assertEquals("col1", key5.get(0)); + assertEquals("col2", key5.get(1)); + } + + @Test + public void testGetProperty_dictionary() + { + assertEquals("Martin D'vloper", yamlConfiguration.getProperty("martin.name")); + assertEquals("Developer", yamlConfiguration.getProperty("martin.job")); + assertEquals("Elite", yamlConfiguration.getProperty("martin.skill")); + } + + /** + * Removes the test output file if it exists. + */ + private void removeTestFile() + { + if (testSaveConf.exists()) + { + assertTrue(testSaveConf.delete()); + } + } +} Added: commons/proper/configuration/trunk/src/test/resources/test.json URL: http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/test/resources/test.json?rev=1799501&view=auto ============================================================================== --- commons/proper/configuration/trunk/src/test/resources/test.json (added) +++ commons/proper/configuration/trunk/src/test/resources/test.json Wed Jun 21 20:00:25 2017 @@ -0,0 +1,27 @@ +{ + "key1": "value1", + "key2": { + "key3": "value23" + }, + "key4": { + "key5": [ + "col1", + "col2" + ] + }, + "very": { + "nested": { + "properties": [ + "nested1", + "nested2", + "nested3" + ] + } + }, + "int1": 37, + "martin": { + "name": "Martin D'vloper", + "job": "Developer", + "skill": "Elite" + } +} Added: commons/proper/configuration/trunk/src/test/resources/test.yaml URL: http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/test/resources/test.yaml?rev=1799501&view=auto ============================================================================== --- commons/proper/configuration/trunk/src/test/resources/test.yaml (added) +++ commons/proper/configuration/trunk/src/test/resources/test.yaml Wed Jun 21 20:00:25 2017 @@ -0,0 +1,26 @@ +--- +key1: + value1 + +key2: + key3: value23 + +key4: + key5: + - col1 + - col2 + +very: + nested: + properties: + - nested1 + - nested2 + - nested3 + +int1: + 37 + +martin: + name: Martin D'vloper + job: Developer + skill: Elite \ No newline at end of file