From be63b620f66434ec39989e180d60df442377e1d6 Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Thu, 10 Nov 2011 16:46:44 -0500 Subject: [PATCH] make ConfigObject implement java.util.Map --- .../com/typesafe/config/ConfigObject.java | 36 +++++++------- .../config/impl/AbstractConfigObject.java | 39 ++++++++++++++- .../config/impl/SimpleConfigObject.java | 45 ++++++++++++++++-- .../config/impl/TransformedConfigObject.java | 33 ++++++++++++- .../com/typesafe/config/impl/ConfigTest.scala | 11 ++++- .../config/impl/ConfigValueTest.scala | 47 +++++++++++++++++++ 6 files changed, 186 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/typesafe/config/ConfigObject.java b/src/main/java/com/typesafe/config/ConfigObject.java index 21da5ae1..17f3106b 100644 --- a/src/main/java/com/typesafe/config/ConfigObject.java +++ b/src/main/java/com/typesafe/config/ConfigObject.java @@ -1,13 +1,13 @@ package com.typesafe.config; import java.util.List; -import java.util.Set; +import java.util.Map; /** * A ConfigObject is a read-only configuration object, which may have nested * child objects. Implementations of ConfigObject should be immutable (at least * from the perspective of anyone using this interface). - * + * * The getters all have the same semantics; they throw ConfigException.Missing * if the value is entirely unset, and ConfigException.WrongType if you ask for * a type that the value can't be converted to. ConfigException.Null is a @@ -16,14 +16,14 @@ import java.util.Set; * path "a.b.c" looks for key c in object b in object a in the root object. (The * syntax for paths is the same as in ${} substitution expressions in config * files, sometimes double quotes are needed around special characters.) - * + * + * ConfigObject implements the standard Java Map interface, but the mutator + * methods all throw UnsupportedOperationException. + * * TODO add OrNull variants of all these getters? Or better to avoid convenience * API for that? - * - * TODO should it implement Map with the mutators - * throwing ? */ -public interface ConfigObject extends ConfigValue { +public interface ConfigObject extends ConfigValue, Map { boolean getBoolean(String path); @@ -46,16 +46,14 @@ public interface ConfigObject extends ConfigValue { Object getAny(String path); /** - * Gets the value at the path as a ConfigValue. - * - * TODO conceptually if we want to match a read-only subset of the - * Map interface, we would need to take a key - * instead of a path here and return null instead of throwing an exception. + * Gets the value at the given path, unless the value is a null value or + * missing, in which case it throws just like the other getters. Use get() + * from the Map interface if you want an unprocessed value. * * @param path * @return */ - ConfigValue get(String path); + ConfigValue getValue(String path); /** Get value as a size in bytes (parses special strings like "128M") */ // rename getSizeInBytes ? clearer. allows a megabyte version @@ -76,6 +74,7 @@ public interface ConfigObject extends ConfigValue { */ Long getNanoseconds(String path); + /* TODO should this return an iterator instead? */ List getList(String path); List getBooleanList(String path); @@ -100,7 +99,12 @@ public interface ConfigObject extends ConfigValue { List getNanosecondsList(String path); - boolean containsKey(String key); - - Set keySet(); + /** + * Gets a ConfigValue at the given key, or returns null if there is no + * value. The returned ConfigValue may have ConfigValueType.NULL or any + * other type, and the passed-in key must be a key in this object, rather + * than a path expression. + */ + @Override + ConfigValue get(Object key); } diff --git a/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java b/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java index 2799e95f..4c3a1ade 100644 --- a/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java +++ b/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java @@ -24,6 +24,9 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements this.transformer = transformer; } + @Override + abstract public Map unwrapped(); + /** * This looks up the key with no transformation or type conversion of any * kind, and returns null if the key is not present. @@ -244,7 +247,15 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements } @Override - public ConfigValue get(String path) { + public ConfigValue get(Object key) { + if (key instanceof String) + return peek((String) key); + else + return null; + } + + @Override + public ConfigValue getValue(String path) { return find(path, null, path); } @@ -487,4 +498,30 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements sb.append(")"); return sb.toString(); } + + private static UnsupportedOperationException weAreImmutable(String method) { + return new UnsupportedOperationException( + "ConfigObject is immutable, you can't call Map.'" + method + + "'"); + } + + @Override + public void clear() { + throw weAreImmutable("clear"); + } + + @Override + public ConfigValue put(String arg0, ConfigValue arg1) { + throw weAreImmutable("put"); + } + + @Override + public void putAll(Map arg0) { + throw weAreImmutable("putAll"); + } + + @Override + public ConfigValue remove(Object arg0) { + throw weAreImmutable("remove"); + } } diff --git a/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java b/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java index 970359d8..4ebea7de 100644 --- a/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java +++ b/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java @@ -1,8 +1,11 @@ package com.typesafe.config.impl; +import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -10,6 +13,7 @@ import java.util.Set; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigValue; class SimpleConfigObject extends AbstractConfigObject { @@ -31,16 +35,16 @@ class SimpleConfigObject extends AbstractConfigObject { } @Override - public Object unwrapped() { + public Map unwrapped() { Map m = new HashMap(); - for (String k : value.keySet()) { - m.put(k, value.get(k).unwrapped()); + for (Map.Entry e : value.entrySet()) { + m.put(e.getKey(), e.getValue().unwrapped()); } return m; } @Override - public boolean containsKey(String key) { + public boolean containsKey(Object key) { return value.containsKey(key); } @@ -104,4 +108,37 @@ class SimpleConfigObject extends AbstractConfigObject { // note that "origin" is deliberately NOT part of equality return mapHash(this.value); } + + @Override + public boolean containsValue(Object v) { + return value.containsValue(v); + } + + @Override + public Set> entrySet() { + // total bloat just to work around lack of type variance + + HashSet> entries = new HashSet>(); + for (Map.Entry e : value.entrySet()) { + entries.add(new AbstractMap.SimpleImmutableEntry( + e.getKey(), e + .getValue())); + } + return entries; + } + + @Override + public boolean isEmpty() { + return value.isEmpty(); + } + + @Override + public int size() { + return value.size(); + } + + @Override + public Collection values() { + return new HashSet(value.values()); + } } diff --git a/src/main/java/com/typesafe/config/impl/TransformedConfigObject.java b/src/main/java/com/typesafe/config/impl/TransformedConfigObject.java index 3cd52e77..d989f7cd 100644 --- a/src/main/java/com/typesafe/config/impl/TransformedConfigObject.java +++ b/src/main/java/com/typesafe/config/impl/TransformedConfigObject.java @@ -1,7 +1,11 @@ package com.typesafe.config.impl; +import java.util.Collection; +import java.util.Map; import java.util.Set; +import com.typesafe.config.ConfigValue; + class TransformedConfigObject extends AbstractConfigObject { private AbstractConfigObject underlying; @@ -13,7 +17,7 @@ class TransformedConfigObject extends AbstractConfigObject { } @Override - public boolean containsKey(String key) { + public boolean containsKey(Object key) { return underlying.containsKey(key); } @@ -23,7 +27,7 @@ class TransformedConfigObject extends AbstractConfigObject { } @Override - public Object unwrapped() { + public Map unwrapped() { return underlying.unwrapped(); } @@ -31,4 +35,29 @@ class TransformedConfigObject extends AbstractConfigObject { protected AbstractConfigValue peek(String key) { return underlying.peek(key); } + + @Override + public boolean containsValue(Object value) { + return underlying.containsValue(value); + } + + @Override + public Set> entrySet() { + return underlying.entrySet(); + } + + @Override + public boolean isEmpty() { + return underlying.isEmpty(); + } + + @Override + public int size() { + return underlying.size(); + } + + @Override + public Collection values() { + return underlying.values(); + } } diff --git a/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/src/test/scala/com/typesafe/config/impl/ConfigTest.scala index 88b19b23..28d0770b 100644 --- a/src/test/scala/com/typesafe/config/impl/ConfigTest.scala +++ b/src/test/scala/com/typesafe/config/impl/ConfigTest.scala @@ -156,8 +156,15 @@ class ConfigTest extends TestUtils { assertEquals("null bar 42 baz true 3.14 hi", conf.getString("strings.concatenated")) assertEquals(true, conf.getBoolean("booleans.trueAgain")) assertEquals(false, conf.getBoolean("booleans.falseAgain")) - // FIXME need to add a way to get a null - //assertEquals(null, conf.getAny("nulls.null")) + + // to get null we have to use the get() method from Map, + // which takes a key and not a path + assertEquals(nullValue(), conf.getObject("nulls").get("null")) + assertNull(conf.get("notinthefile")) + + // get stuff with getValue + assertEquals(intValue(42), conf.getValue("ints.fortyTwo")) + assertEquals(stringValue("abcd"), conf.getValue("strings.abcd")) // get stuff with getAny assertEquals(42L, conf.getAny("ints.fortyTwo")) diff --git a/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala b/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala index 3f3da1bd..7f780beb 100644 --- a/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala +++ b/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala @@ -4,6 +4,8 @@ import org.junit.Assert._ import org.junit._ import com.typesafe.config.ConfigValue import java.util.Collections +import scala.collection.JavaConverters._ +import com.typesafe.config.ConfigObject class ConfigValueTest extends TestUtils { @@ -90,4 +92,49 @@ class ConfigValueTest extends TestUtils { subst("a").toString() substInString("b").toString() } + + private def unsupported(body: => Unit) { + intercept[UnsupportedOperationException] { + body + } + } + + @Test + def configObjectUnwraps() { + val m = new SimpleConfigObject(fakeOrigin(), null, configMap("a" -> 1, "b" -> 2, "c" -> 3)) + assertEquals(Map("a" -> 1, "b" -> 2, "c" -> 3), m.unwrapped().asScala) + } + + @Test + def configObjectImplementsMap() { + val m: ConfigObject = new SimpleConfigObject(fakeOrigin(), null, configMap("a" -> 1, "b" -> 2, "c" -> 3)) + + assertEquals(intValue(1), m.get("a")) + assertEquals(intValue(2), m.get("b")) + assertEquals(intValue(3), m.get("c")) + assertNull(m.get("d")) + + assertTrue(m.containsKey("a")) + assertFalse(m.containsKey("z")) + + assertTrue(m.containsValue(intValue(1))) + assertFalse(m.containsValue(intValue(10))) + + assertFalse(m.isEmpty()) + + assertEquals(3, m.size()) + + val values = Set(intValue(1), intValue(2), intValue(3)) + assertEquals(values, m.values().asScala.toSet) + assertEquals(values, m.entrySet().asScala map { _.getValue() } toSet) + + val keys = Set("a", "b", "c") + assertEquals(keys, m.keySet().asScala.toSet) + assertEquals(keys, m.entrySet().asScala map { _.getKey() } toSet) + + unsupported { m.clear() } + unsupported { m.put("hello", intValue(42)) } + unsupported { m.putAll(Collections.emptyMap[String, AbstractConfigValue]()) } + unsupported { m.remove("a") } + } }