Skip to content

Commit

Permalink
Add painless method getByPath, get value from nested collections with…
Browse files Browse the repository at this point in the history
… dotted path (#43170)

Given a nested structure composed of Lists and Maps, getByPath will return the value
keyed by path.  getByPath is a method on Lists and Maps.

The path is string Map keys and integer List indices separated by dot. An optional third
argument returns a default value if the path lookup fails due to a missing value.

Eg.
['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key1') = ['c', 'd']
['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key1.0') = 'c'
['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key2', 'x') = 'x'
[['key0': 'value0'], ['key1': 'value1']].getByPath('1.key1') = 'value1'

Throws IllegalArgumentException if an item cannot be found and a default is not given.
Throws NumberFormatException if a path element operating on a List is not an integer.

Fixes #42769
  • Loading branch information
stu-elastic authored Jun 20, 2019
1 parent 0b48f04 commit 2c8e9ae
Show file tree
Hide file tree
Showing 6 changed files with 430 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ See the <<painless-api-reference-score, Score API>> for a high-level overview of
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* def {java11-javadoc}/java.base/java/util/List.html#get(int)[get](int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* Map groupBy(Function)
* int {java11-javadoc}/java.base/java/util/List.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -84,6 +86,8 @@ See the <<painless-api-reference-score, Score API>> for a high-level overview of
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* def {java11-javadoc}/java.base/java/util/List.html#get(int)[get](int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* Map groupBy(Function)
* int {java11-javadoc}/java.base/java/util/List.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -138,6 +142,8 @@ See the <<painless-api-reference-score, Score API>> for a high-level overview of
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* def {java11-javadoc}/java.base/java/util/List.html#get(int)[get](int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* Map groupBy(Function)
* int {java11-javadoc}/java.base/java/util/List.html#hashCode()[hashCode]()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4335,6 +4335,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* def {java11-javadoc}/java.base/java/util/List.html#get(int)[get](int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* Map groupBy(Function)
* int {java11-javadoc}/java.base/java/util/List.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -4386,6 +4388,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(BiFunction)
* void {java11-javadoc}/java.base/java/util/Map.html#forEach(java.util.function.BiConsumer)[forEach](BiConsumer)
* def {java11-javadoc}/java.base/java/util/Map.html#get(java.lang.Object)[get](def)
* Object getByPath(String)
* Object getByPath(String, Object)
* def {java11-javadoc}/java.base/java/util/Map.html#getOrDefault(java.lang.Object,java.lang.Object)[getOrDefault](def, def)
* Map groupBy(BiFunction)
* int {java11-javadoc}/java.base/java/lang/Object.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -4500,6 +4504,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* def {java11-javadoc}/java.base/java/util/List.html#get(int)[get](int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* Map groupBy(Function)
* int {java11-javadoc}/java.base/java/util/List.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -4666,6 +4672,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* def {java11-javadoc}/java.base/java/util/List.html#get(int)[get](int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* Map groupBy(Function)
* int {java11-javadoc}/java.base/java/util/List.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -5367,6 +5375,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(BiFunction)
* void {java11-javadoc}/java.base/java/util/Map.html#forEach(java.util.function.BiConsumer)[forEach](BiConsumer)
* def {java11-javadoc}/java.base/java/util/Map.html#get(java.lang.Object)[get](def)
* Object getByPath(String)
* Object getByPath(String, Object)
* def {java11-javadoc}/java.base/java/util/Map.html#getOrDefault(java.lang.Object,java.lang.Object)[getOrDefault](def, def)
* Map groupBy(BiFunction)
* int {java11-javadoc}/java.base/java/lang/Object.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -5457,6 +5467,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(BiFunction)
* void {java11-javadoc}/java.base/java/util/Map.html#forEach(java.util.function.BiConsumer)[forEach](BiConsumer)
* def {java11-javadoc}/java.base/java/util/Map.html#get(java.lang.Object)[get](def)
* Object getByPath(String)
* Object getByPath(String, Object)
* def {java11-javadoc}/java.base/java/util/Map.html#getOrDefault(java.lang.Object,java.lang.Object)[getOrDefault](def, def)
* Map groupBy(BiFunction)
* int {java11-javadoc}/java.base/java/lang/Object.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -5502,6 +5514,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(BiFunction)
* void {java11-javadoc}/java.base/java/util/Map.html#forEach(java.util.function.BiConsumer)[forEach](BiConsumer)
* def {java11-javadoc}/java.base/java/util/Map.html#get(java.lang.Object)[get](def)
* Object getByPath(String)
* Object getByPath(String, Object)
* def {java11-javadoc}/java.base/java/util/Map.html#getOrDefault(java.lang.Object,java.lang.Object)[getOrDefault](def, def)
* Map groupBy(BiFunction)
* int {java11-javadoc}/java.base/java/lang/Object.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -5668,6 +5682,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(BiFunction)
* void {java11-javadoc}/java.base/java/util/Map.html#forEach(java.util.function.BiConsumer)[forEach](BiConsumer)
* def {java11-javadoc}/java.base/java/util/Map.html#get(java.lang.Object)[get](def)
* Object getByPath(String)
* Object getByPath(String, Object)
* def {java11-javadoc}/java.base/java/util/Map.html#getOrDefault(java.lang.Object,java.lang.Object)[getOrDefault](def, def)
* Map groupBy(BiFunction)
* int {java11-javadoc}/java.base/java/lang/Object.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -5764,6 +5780,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* def {java11-javadoc}/java.base/java/util/List.html#get(int)[get](int)
* Object getByPath(String)
* Object getByPath(String, Object)
* def {java11-javadoc}/java.base/java/util/Deque.html#getFirst()[getFirst]()
* def {java11-javadoc}/java.base/java/util/Deque.html#getLast()[getLast]()
* int getLength()
Expand Down Expand Up @@ -5836,6 +5854,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* def {java11-javadoc}/java.base/java/util/List.html#get(int)[get](int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* Map groupBy(Function)
* int {java11-javadoc}/java.base/java/util/List.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -6056,6 +6076,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(BiFunction)
* void {java11-javadoc}/java.base/java/util/Map.html#forEach(java.util.function.BiConsumer)[forEach](BiConsumer)
* def {java11-javadoc}/java.base/java/util/Map.html#get(java.lang.Object)[get](def)
* Object getByPath(String)
* Object getByPath(String, Object)
* def {java11-javadoc}/java.base/java/util/Map.html#getOrDefault(java.lang.Object,java.lang.Object)[getOrDefault](def, def)
* Map groupBy(BiFunction)
* int {java11-javadoc}/java.base/java/lang/Object.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -6157,6 +6179,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* def {java11-javadoc}/java.base/java/util/NavigableMap.html#floorKey(java.lang.Object)[floorKey](def)
* void {java11-javadoc}/java.base/java/util/Map.html#forEach(java.util.function.BiConsumer)[forEach](BiConsumer)
* def {java11-javadoc}/java.base/java/util/Map.html#get(java.lang.Object)[get](def)
* Object getByPath(String)
* Object getByPath(String, Object)
* def {java11-javadoc}/java.base/java/util/Map.html#getOrDefault(java.lang.Object,java.lang.Object)[getOrDefault](def, def)
* Map groupBy(BiFunction)
* int {java11-javadoc}/java.base/java/lang/Object.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -6642,6 +6666,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* def {java11-javadoc}/java.base/java/util/SortedMap.html#firstKey()[firstKey]()
* void {java11-javadoc}/java.base/java/util/Map.html#forEach(java.util.function.BiConsumer)[forEach](BiConsumer)
* def {java11-javadoc}/java.base/java/util/Map.html#get(java.lang.Object)[get](def)
* Object getByPath(String)
* Object getByPath(String, Object)
* def {java11-javadoc}/java.base/java/util/Map.html#getOrDefault(java.lang.Object,java.lang.Object)[getOrDefault](def, def)
* Map groupBy(BiFunction)
* int {java11-javadoc}/java.base/java/lang/Object.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -6844,6 +6870,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* def {java11-javadoc}/java.base/java/util/Vector.html#firstElement()[firstElement]()
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* def {java11-javadoc}/java.base/java/util/List.html#get(int)[get](int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* Map groupBy(Function)
* int {java11-javadoc}/java.base/java/util/List.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -6988,6 +7016,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* def {java11-javadoc}/java.base/java/util/NavigableMap.html#floorKey(java.lang.Object)[floorKey](def)
* void {java11-javadoc}/java.base/java/util/Map.html#forEach(java.util.function.BiConsumer)[forEach](BiConsumer)
* def {java11-javadoc}/java.base/java/util/Map.html#get(java.lang.Object)[get](def)
* Object getByPath(String)
* Object getByPath(String, Object)
* def {java11-javadoc}/java.base/java/util/Map.html#getOrDefault(java.lang.Object,java.lang.Object)[getOrDefault](def, def)
* Map groupBy(BiFunction)
* int {java11-javadoc}/java.base/java/lang/Object.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -7158,6 +7188,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* def {java11-javadoc}/java.base/java/util/Vector.html#firstElement()[firstElement]()
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* def {java11-javadoc}/java.base/java/util/List.html#get(int)[get](int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* Map groupBy(Function)
* int {java11-javadoc}/java.base/java/util/List.html#hashCode()[hashCode]()
Expand Down Expand Up @@ -8016,6 +8048,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* Boolean get(int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* boolean getValue()
* Map groupBy(Function)
Expand Down Expand Up @@ -8071,6 +8105,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* BytesRef get(int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* BytesRef getValue()
* Map groupBy(Function)
Expand Down Expand Up @@ -8126,6 +8162,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* JodaCompatibleZonedDateTime get(int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* JodaCompatibleZonedDateTime getValue()
* Map groupBy(Function)
Expand Down Expand Up @@ -8181,6 +8219,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* Double get(int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* double getValue()
* Map groupBy(Function)
Expand Down Expand Up @@ -8240,6 +8280,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* double geohashDistance(String)
* double geohashDistanceWithDefault(String, double)
* GeoPoint get(int)
* Object getByPath(String)
* Object getByPath(String, Object)
* double getLat()
* double[] getLats()
* int getLength()
Expand Down Expand Up @@ -8301,6 +8343,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* Long get(int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* long getValue()
* Map groupBy(Function)
Expand Down Expand Up @@ -8356,6 +8400,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* String get(int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* String getValue()
* Map groupBy(Function)
Expand Down Expand Up @@ -8415,6 +8461,8 @@ See the <<painless-api-reference-shared, Shared API>> for a high-level overview
* List findResults(Function)
* void {java11-javadoc}/java.base/java/lang/Iterable.html#forEach(java.util.function.Consumer)[forEach](Consumer)
* String get(int)
* Object getByPath(String)
* Object getByPath(String, Object)
* int getLength()
* String getValue()
* Map groupBy(Function)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.BiConsumer;
Expand All @@ -34,6 +35,7 @@
import java.util.function.Function;
import java.util.function.ObjIntConsumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.ToDoubleFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -552,4 +554,115 @@ public static String[] splitOnToken(String receiver, String token, int limit) {
// O(N) or faster depending on implementation
return result.toArray(new String[0]);
}

/**
* Access values in nested containers with a dot separated path. Path elements are treated
* as strings for Maps and integers for Lists.
* @throws IllegalArgumentException if any of the following:
* - path is empty
* - path contains a trailing '.' or a repeated '.'
* - an element of the path does not exist, ie key or index not present
* - there is a non-container type at a non-terminal path element
* - a path element for a List is not an integer
* @return object at path
*/
public static <E> Object getByPath(List<E> receiver, String path) {
return getByPathDispatch(receiver, splitPath(path), 0, throwCantFindValue(path));
}

/**
* Same as {@link #getByPath(List, String)}, but for Map.
*/
public static <K,V> Object getByPath(Map<K,V> receiver, String path) {
return getByPathDispatch(receiver, splitPath(path), 0, throwCantFindValue(path));
}

/**
* Same as {@link #getByPath(List, String)}, but with a default value.
* @return element at path or {@code defaultValue} if the terminal path element does not exist.
*/
public static <E> Object getByPath(List<E> receiver, String path, Object defaultValue) {
return getByPathDispatch(receiver, splitPath(path), 0, () -> defaultValue);
}

/**
* Same as {@link #getByPath(List, String, Object)}, but for Map.
*/
public static <K,V> Object getByPath(Map<K,V> receiver, String path, Object defaultValue) {
return getByPathDispatch(receiver, splitPath(path), 0, () -> defaultValue);
}

// Dispatches to getByPathMap, getByPathList or returns obj if done. See handleMissing for dealing with missing
// elements.
private static Object getByPathDispatch(Object obj, String[] elements, int i, Supplier<Object> defaultSupplier) {
if (i > elements.length - 1) {
return obj;
} else if (elements[i].length() == 0 ) {
String format = "Extra '.' in path [%s] at index [%d]";
throw new IllegalArgumentException(String.format(Locale.ROOT, format, String.join(".", elements), i));
} else if (obj instanceof Map<?,?>) {
return getByPathMap((Map<?,?>) obj, elements, i, defaultSupplier);
} else if (obj instanceof List<?>) {
return getByPathList((List<?>) obj, elements, i, defaultSupplier);
}
return handleMissing(obj, elements, i, defaultSupplier);
}

// lookup existing key in map, call back to dispatch.
private static <K,V> Object getByPathMap(Map<K,V> map, String[] elements, int i, Supplier<Object> defaultSupplier) {
String element = elements[i];
if (map.containsKey(element)) {
return getByPathDispatch(map.get(element), elements, i + 1, defaultSupplier);
}
return handleMissing(map, elements, i, defaultSupplier);
}

// lookup existing index in list, call back to dispatch. Throws IllegalArgumentException with NumberFormatException
// if index can't be parsed as an int.
private static <E> Object getByPathList(List<E> list, String[] elements, int i, Supplier<Object> defaultSupplier) {
String element = elements[i];
try {
int elemInt = Integer.parseInt(element);
if (list.size() >= elemInt) {
return getByPathDispatch(list.get(elemInt), elements, i + 1, defaultSupplier);
}
} catch (NumberFormatException e) {
String format = "Could not parse [%s] as a int index into list at path [%s] and index [%d]";
throw new IllegalArgumentException(String.format(Locale.ROOT, format, element, String.join(".", elements), i), e);
}
return handleMissing(list, elements, i, defaultSupplier);
}

// Split path on '.', throws IllegalArgumentException for empty paths and paths ending in '.'
private static String[] splitPath(String path) {
if (path.length() == 0) {
throw new IllegalArgumentException("Missing path");
}
if (path.endsWith(".")) {
String format = "Trailing '.' in path [%s]";
throw new IllegalArgumentException(String.format(Locale.ROOT, format, path));
}
return path.split("\\.");
}

// A supplier that throws IllegalArgumentException
private static Supplier<Object> throwCantFindValue(String path) {
return () -> {
throw new IllegalArgumentException(String.format(Locale.ROOT, "Could not find value at path [%s]", path));
};
}

// Use defaultSupplier if at last path element, otherwise throw IllegalArgumentException
private static Object handleMissing(Object obj, String[] elements, int i, Supplier<Object> defaultSupplier) {
if (obj instanceof List || obj instanceof Map) {
if (elements.length - 1 == i) {
return defaultSupplier.get();
}
String format = "Container does not have [%s], for non-terminal index [%d] in path [%s]";
throw new IllegalArgumentException(String.format(Locale.ROOT, format, elements[i], i, String.join(".", elements)));
}
String format = "Non-container [%s] at [%s], index [%d] in path [%s]";
throw new IllegalArgumentException(
String.format(Locale.ROOT, format, obj.getClass().getName(), elements[i], i, String.join(".", elements)));
}
}
Loading

0 comments on commit 2c8e9ae

Please sign in to comment.