org.elasticsearch.groovy.ClosureToMapConverter.groovy Maven / Gradle / Ivy
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch.groovy
/**
* {@code ClosureToMapConverter} serves as a utility to convert {@link Closure}s into {@link Map}s with {@link String}
* keys and {@link Object} values. The values can be {@link List}s, {@link Map}s, or other values (normal objects). In
* general, this serves as a convenient way to load JSON-like syntax in Groovy and convert it into a format usable by
* Elasticsearch.
*
* Map<String, Object> map = mapClosure {
* name {
* first = "Example"
* middle = ["First", "Second"]
* last = "Name"
* }
* details {
* nested {
* user_id = 1234
* timestamp = new Date()
* }
* }
* }
*
* This class is added to {@link Closure}s automatically via the {@link ClosureExtensions} class, so the above code can
* be slightly simplified by avoiding the static import of {@link ClosureToMapConverter#mapClosure} and instead using
* the {@link ClosureExtensions#asMap(Closure)} extension method.
*
* Map<String, Object> map = {
* name {
* first = "Example"
* middle = ["First", "Second"]
* last = "Name"
* }
* details {
* nested {
* user_id = 1234
* timestamp = new Date()
* }
* }
* }.asMap()
*
* It's important to note that the field name's can be specified as methods or properties. This means that
*
* {
* name {
* first "Example"
* }
* }
*
* is treated the same way as
*
* {
* name = {
* first = "Example"
* }
* }
*
* Mixing the two styles is allowed, but consistency is naturally very important for code readability.
*
* Instances should never be reused. Instances of this class are not thread safe, but separate instances do not share
* any state and therefore multiple instances can run in parallel.
* @see ClosureExtensions#asMap(Closure)
*/
class ClosureToMapConverter {
/**
* Convert the {@code closure} into a {@link Map}.
*
* @param closure The closure to convert.
* @return Never {@code null}. Can be {@link Map#isEmpty() empty}.
*/
static Map mapClosure(Closure closure) {
mapClosureWithOwner(closure, closure.owner)
}
/**
* Convert the {@code closure} into a {@link Map}.
*
* The {@code owner} comes up when users specify variables within the {@link Closure}.
*
* @param closure The closure to convert.
* @Param owner The owner of the {@code closure}.
* @return Never {@code null}. Can be {@link Map#isEmpty() empty}.
*/
private static Map mapClosureWithOwner(Closure closure, Object owner) {
new ClosureToMapConverter(closure, owner).convert()
}
/**
* The "buildName" is used to maintain state for names like "field.innerField.nested", which
* is actually 3 separate fields without recognizing it.
*
* Map<String, Object> map = mapClosure {
* field.innerField.nested = value
* }
*
* This allows us to parse the field name as literally "field.innerField.nested" by tracking requests
* for the field "field", followed by "innerField" and finally attempting to set "nested".
*
* This format should only be used when for things like changing settings or searching (e.g.,
* a match request against an inner field or a nested query).
*/
private String buildName = null
/**
* The unraveled {@link Closure} after calling {@link #convert()}.
*/
private final Map map = [:]
/**
* The closure that is unraveled into the {@link #map}.
*/
final Closure closure
/**
* The owner of the root {@link #closure}.
*
* Inner {@link Closure}s handle their owner differently than the root one, so it's important to track the root's
* owner.
*/
final Object rootOwner
/**
* Construct a new {@link ClosureToMapConverter} that delegates the {@code closure} to call the constructed
* instance ({@code this}) when it is invoked. These calls are used to unravel the {@code closure} into the
* {@link #map}.
*
* @param closure The {@link Closure} to convert into a {@link Map}.
* @throws NullPointerException if {@code closure} is {@code null}
*/
private ClosureToMapConverter(Closure closure, Object owner) {
// required
this.closure = (Closure)closure.clone()
this.rootOwner = owner
// When looking up properties and invoking methods, it first looks at the delegate (THIS) for the value. If
// the delegate (THIS) does not have it, then it will check the owner for the value (effectively the closure).
this.closure.delegate = this
// Note: Using OWNER_FIRST (the default) does not work except for non-nested closures.
// This class will take it over entirely, as the delegate, for property access, but not method invocation.
this.closure.resolveStrategy = Closure.DELEGATE_FIRST
}
/**
* Trigger the conversion of the {@link #closure} to the {@link #map}.
*
* This method should only be invoked once.
*
* @return Never {@code null}. {@link Map#isEmpty() Empty} if the {@code closure} does not assign any properties.
*/
Map convert() {
// invoke the closure, thus triggering calls to the delegate (this)
closure.call()
map
}
/**
* Called when the {@link #closure} is delegating to {@code this} instance for method invocations. For example
*
* { username "kimchy" }
*
* The above {@link Closure} would pass a {@code methodName} set to "username" and {@code args} as "kimchy"
* within a single element {@code Object[]}.
*
* {
* user {
* id 1234
* name "kimchy"
* }
* }
*
* This {@link Closure} would pass a {@code methodName} set to "user" and {@code args} as the user closure within
* a single element {@code Object[]}. A nested invocation of that closure would pass a {@code methodName} of
* "id" and {@code args} as 1234 within a single element {@code Object[]}. A separate nested invocation of that
* closure would handle the "name" field.
*/
@Override
Object invokeMethod(String methodName, Object args) {
Object value = args
// single element arrays are the most expected form:
// Map map = {
// name {
// value = "xyz"
// }
// }
// methodName would be "name" and args would be the closure
if (args instanceof Object[] && args.size() == 1) {
value = args[0]
}
// assign the value of the would-be method
setProperty(methodName, value)
// return the value
getProperty(methodName)
}
/**
* Get the value defined in the {@link #map} with the {@code propertyName}.
*
* This method has a side-effect if you are requesting a {@code propertyName} that is unrecognized to both
* {@code this} (its internal {@link #map}) or the {@link #rootOwner}. In that case, the this method will remember
* the {@code propertyName} for future use with {@link #setProperty} or {@link #invokeMethod}.
*
* The reason that it keeps track of this the {@code propertyName} is because this method is only called when a
* value is sought. In general, that only occurs on the right hand side (e.g., x = y, where y is the right
* hand side). However, if the value is unrecognized, then it's either an error or, more likely, it's being
* used in the form of "x.y.z = a" rather than "x { y { z = a } }". Doing this should is considered an error.
*
* Map<String, Object> map = mapClosure {
* x.y.z = 123
* a = x.y.z // BAD!!
* }
*
* If you really need similar functionality, then define a temporary variable outside of the
* {@code Closure} and use it.
*
* int value = 123
*
* Map<String, Object> map = mapClosure {
* x.y.z = value
* a = value // GOOD!!
* }
*
*
* Perhaps unexpectedly, this will never return property values unless given the full name. When Groovy
* is unraveling the shorthand version, this method is called twice in the above example. Once with "x", again with
* "y", and finally the setter is called against "y" for "z". By tracking "x" and "y", we can appropriately create
* the "x.y.z" key that is intended. Because we are building it, we don't want to return any value that we happen
* across "along the way" because each key in this format is effectively not associated with others.
*/
@Override
Object getProperty(String propertyName) {
Object returned = this
// if we know about it, then return the value
if (map.containsKey(propertyName)) {
returned = map[propertyName]
}
// NOTE: The following blocks are here to handle the less common "key1.key2" on the left-hand side
// This is meant to be less common
// if we don't know about it, then start building the actual key name
// (key is in form of "key1.key2.key3" and this will be "key1"
else if (buildName == null) {
// If the OWNER of the closure has the value, then we want to use that value because it will get assigned.
// NOTE: This "owner" is only the same as "closure.owner" for the root closure.
if (rootOwner.hasProperty(propertyName)) {
// the owner has it, so just return that value (this _should_ be on the right hand side used as a value)
returned = rootOwner.getProperty(propertyName)
}
else {
buildName = propertyName
}
}
// continuation from above where it's now "key2"; this method would never get "key3" unless it was on the
else {
buildName += '.' + propertyName
}
returned
}
/**
* Called when the {@link #closure} is delegating to {@code this} instance. For example
*
* { username = "kimchy" }
*
* The above {@link Closure} would pass a {@code propertyName} set to "username" and a {@code newValue} as "kimchy".
*/
@Override
void setProperty(String propertyName, Object newValue) {
String fullName = propertyName
// If we are maintaining state trying to load the name
if (buildName != null) {
fullName = buildName + "." + propertyName
// We just now saw "key3", so we can throw away the name now
buildName = null
}
map[fullName] = convertValue(newValue)
}
/**
* Convert the passed in {@code value} into an object that is not a {@link Closure}, and convert any
* {@link Collection}s into {@link List}s.
*
* This will recursively call itself as necessary.
*
* @param value The incoming parameter to convert if necessary ({@link Closure}s and {@link Collection}s)
* @return {@code value} as-is unless it is a {@link Closure} or {@link Collection}. Otherwise the unraveled
* value of those objects.
* @throws IllegalArgumentException if you attempt to reuse a shorthand property on the right hand side (e.g.,
* "x.y.z = a; b = x.y.z;" where "x.y.z" is the shorthand property)
*/
private Object convertValue(Object value) {
Object ret = value
// avoid handling shorthand assignments (technically we could check for this, then build the name here, but
// this is a very bad code smell; just use a temporary variable!)
if (value instanceof ClosureToMapConverter) {
throw new IllegalArgumentException(
"value is a ClosureToMapConverter. This means that you are trying to reuse a shorthand " +
"property! (For example, { x.y.z = 123; a = x.y.z }. 'x.y.z' cannot be referenced on the right " +
"hand side! Use a temporary variable from outside the closure instead.)")
}
// enable nested objects
else if (value instanceof Closure) {
// avoid overwriting this instance's map; maintain the owner!
ret = mapClosureWithOwner(value, rootOwner)
}
// unravel collections into a List
else if (value instanceof Collection) {
// use _this_ method by passing its method address to be invoked
ret = ((Collection)value).collect(this.&convertValue)
}
// unravel arrays into a List (note: this hits native arrays like int[])
else if (value instanceof Object[] || (value != null && value.getClass().isArray())) {
ret = (value as List).collect(this.&convertValue)
}
ret
}
}