Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add painless method getByPath, get value from nested collections with dotted path (#43170) #43606

Merged
merged 2 commits into from
Jun 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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