From 54eef7b4c7a58c25342358993a63d0b21adf5f0a Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 16 Feb 2023 23:28:05 -0500 Subject: [PATCH] Allow array access via indices in configuration paths - Fixes #30 - Adds and modifies peek and find methods to allow indexing of ConfigList objects - Enables nested array and object access using indices in paths: listConfig.getString("ofObject.0.byteObj.byteVal") listConfig.getString("ofArray.2.2") --- .../config/impl/AbstractConfigObject.java | 34 +++++++++++ .../typesafe/config/impl/SimpleConfig.java | 58 +++++++++++++++++-- .../config/impl/SimpleConfigList.java | 1 + .../config/impl/ConfigBeanFactoryTest.scala | 32 +++++++++- 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java b/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java index a98515de6..e9e335cda 100644 --- a/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java +++ b/config/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java @@ -10,6 +10,7 @@ import java.util.Map; import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigList; import com.typesafe.config.ConfigMergeable; import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigOrigin; @@ -104,6 +105,8 @@ private static AbstractConfigValue peekPath(AbstractConfigObject self, Path path } else { if (v instanceof AbstractConfigObject) { return peekPath((AbstractConfigObject) v, next); + } else if (v instanceof ConfigList) { + return peekListPath((ConfigList) v, next); } else { return null; } @@ -113,6 +116,37 @@ private static AbstractConfigValue peekPath(AbstractConfigObject self, Path path } } + private static AbstractConfigValue peekListPath(ConfigList self, Path path) + { + try { + Integer current = Integer.valueOf(path.first()); + if(path.remainder() == null) { + return (AbstractConfigValue) self.get(current); + } + + AbstractConfigValue v = + self.get(current) instanceof AbstractConfigObject ? + ((AbstractConfigObject) self.get(current)).attemptPeekWithPartialResolve(path.remainder().first()) : + peekListPath((ConfigList) self.get(current), path.remainder()); + + Path next = path.remainder().remainder(); + if (next == null) { + return v; + } else { + if (v instanceof AbstractConfigObject) { + return peekPath((AbstractConfigObject) v, next); + } else if (v instanceof ConfigList) { + return peekListPath((ConfigList) v, next); + } else { + return next.remainder() == null ? v : null; + } + } + } catch (ConfigException.NotResolved e) { + throw ConfigImpl.improveNotResolved(path, e); + } + } + + @Override public ConfigValueType valueType() { return ConfigValueType.OBJECT; diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java index 12c0d3f7a..cf9fcb6a7 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java @@ -24,6 +24,8 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigException.BadPath; +import com.typesafe.config.ConfigException.NotResolved; import com.typesafe.config.ConfigList; import com.typesafe.config.ConfigMemorySize; import com.typesafe.config.ConfigMergeable; @@ -174,11 +176,21 @@ static private AbstractConfigValue findOrNull(AbstractConfigObject self, Path pa if (next == null) { return findKeyOrNull(self, key, expected, originalPath); } else { - AbstractConfigObject o = (AbstractConfigObject) findKey(self, key, - ConfigValueType.OBJECT, - originalPath.subPath(0, originalPath.length() - next.length())); - assert (o != null); // missing was supposed to throw - return findOrNull(o, next, expected, originalPath); + + AbstractConfigValue nextValue = + findKey(self, + key, + null, + originalPath.subPath(0, originalPath.length() - next.length())); + + if (nextValue instanceof AbstractConfigObject) { + AbstractConfigObject o = (AbstractConfigObject) nextValue; + return findOrNull(o, next, expected, originalPath); + } else if (nextValue instanceof SimpleConfigList) { + SimpleConfigList o = (SimpleConfigList) nextValue; + return findOrNull(o, next, expected, originalPath); + } + return nextValue; } } catch (ConfigException.NotResolved e) { throw ConfigImpl.improveNotResolved(path, e); @@ -194,6 +206,42 @@ AbstractConfigValue find(String pathExpression, ConfigValueType expected) { return find(path, expected, path); } + static private AbstractConfigValue findOrNull(SimpleConfigList self, + Path path, + ConfigValueType expected, + Path originalPath) + { + try { + Integer index = Integer.valueOf(path.first()); + Path next = path.remainder(); + if (next == null) { + try { + return self.get(index); + } catch (IndexOutOfBoundsException e) { + throw new BadPath(originalPath.render(), + String.format("Path index %d exceeds the configuration list length %d", + index, self.size())); + } + } + + if (self.get(index) instanceof AbstractConfigObject) { + + return findOrNull((AbstractConfigObject) self.get(index), next, expected, originalPath); + + } else if (self.get(index) instanceof SimpleConfigList) { + + return findOrNull((SimpleConfigList) self.get(index), next, expected, originalPath); + } + throw new BadPath(originalPath.render(), + String.format("Attempted to retrieve key %s on non-object %s", + self.get(index), + self)); + } catch (ConfigException.NotResolved e) { + throw ConfigImpl.improveNotResolved(path, e); + } + } + + private AbstractConfigValue findOrNull(Path pathExpression, ConfigValueType expected, Path originalPath) { return findOrNull(object, pathExpression, expected, originalPath); } diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java index a8d8e366c..fcd51399a 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigList.java @@ -12,6 +12,7 @@ import java.util.ListIterator; import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigException.BadPath; import com.typesafe.config.ConfigList; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigRenderOptions; diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala index b7e56ebf7..98549b915 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala @@ -3,16 +3,18 @@ */ package com.typesafe.config.impl -import beanconfig.EnumsConfig.{ Solution, Problem } +import beanconfig.EnumsConfig.{ Problem, Solution } import com.typesafe.config._ import java.io.{ InputStream, InputStreamReader } import java.time.Duration - import beanconfig._ +import com.typesafe.config.ConfigException.BadPath import org.junit.Assert._ import org.junit._ +import java.util +import java.util.Collections import scala.collection.JavaConverters._ import scala.collection.mutable.ArrayBuffer @@ -131,6 +133,32 @@ class ConfigBeanFactoryTest extends TestUtils { assertEquals(List(stringsConfigOne, stringsConfigTwo).asJava, beanConfig.getOfStringBean) } + @Test + def testCanAccessListByElement() { + val listConfig: Config = loadConfig().getConfig("arrays") + Assert.assertTrue(listConfig.getList("empty").isEmpty) + Assert.assertTrue(listConfig.hasPath("ofObject.0")) + Assert.assertEquals(2, listConfig.getInt("ofInt.1")) + Assert.assertEquals("c", listConfig.getString("ofString.2")) + Assert.assertEquals("c", listConfig.getString("ofArray.2.2")) + Assert.assertEquals(listConfig.getList("ofArray.2").unwrapped(), List("a", "b", "c").asJava); + Assert.assertTrue(listConfig.hasPath("ofArray.2")) + Assert.assertTrue(listConfig.hasPath("ofArray.2.2")) + Assert.assertTrue(listConfig.hasPath("ofObject.0.byteObj.byteVal")) + Assert.assertEquals("1", listConfig.getString("ofObject.0.byteObj.byteVal")) + Assert.assertFalse(listConfig.hasPath("ofObject.0.byteObj.byteVal.non")) + Assert.assertTrue(listConfig.getIsNull("ofNull.0")) + Assert.assertFalse(listConfig.hasPath("ofNull.0")) + + try { + listConfig.getString("empty.1") + Assert.fail(); + } catch { + case e: BadPath => {} + case e: Exception => Assert.fail() + } + + } @Test def testCreateSet() {