You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

236 lines
6.6 KiB

package org.wikimedia.integration
import com.cloudbees.groovy.cps.NonCPS
import org.codehaus.groovy.GroovyException
import org.wikimedia.integration.ExecutionGraph
/**
* Provides execution context and value bindings to graph nodes. Each node's
* context (see {@link ofNode()}) provides methods for saving its own values
* and bindings for accessing values saved by ancestor nodes.
*
* These contexts can be used during graph stack evaluation to safely pass
* values from ancestor nodes to descendent nodes, and to keep nodes from
* different branches of execution to access each other's values.
*/
class ExecutionContext implements Serializable {
private Map globals = [:]
private ExecutionGraph graph
ExecutionContext(executionGraph) {
graph = executionGraph
}
/**
* Create and return an execution context for the given node.
*/
NodeContext ofNode(node) {
new NodeContext(node, graph.ancestorsOf(node))
}
/**
* Returns the names of all values bound by node contexts.
*/
List getAllKeys() {
def keys = []
for (def ns in globals) {
for (def key in globals[ns]) {
keys.add("${ns}.${key}")
}
}
keys
}
/**
* Provides an execution context for a single given node that can resolve
* bindings for values stored by ancestor nodes, set its own values, and
* safely interpolate user-provided strings.
*
* @example
* Given a graph:
* <pre><code>
* def context = new ExecutionContext(new ExecutionGraph([
* ["a", "b", "c", "d", "e", "f"],
* ["x", "d", "y", "f"],
* ]))
* </code></pre>
*
* @example
* Values can be bound to any existing node.
* <pre><code>
* context.ofNode("a")["foo"] = "afoo"
* context.ofNode("a")["bar"] = "abar"
* context.ofNode("b")["foo"] = "bfoo"
* context.ofNode("x")["bar"] = "xbar"
* </code></pre>
*
* @example
* Those same values can be accessed in contexts of descendent nodes, but
* not in contexts of unrelated nodes.
* <pre><code>
* assert context.ofNode("c").binding("a", "foo") == "afoo"
* assert context.ofNode("c").binding("a", "bar") == "abar"
* assert context.ofNode("c").binding("b", "foo") == "bfoo"
*
* assert context.ofNode("c").binding("x", "bar") == null
*
* assert context.ofNode("e").binding("a", "foo") == "afoo"
* assert context.ofNode("e").binding("a", "bar") == "abar"
* assert context.ofNode("e").binding("b", "foo") == "bfoo"
* assert context.ofNode("e").binding("x", "bar") == "xbar"
* </code></pre>
*
* @example
* Leveraging all of the above, user-provided configuration can be safely
* interpolated.
* <pre><code>
* assert (context.ofNode("c") % 'w-t-${a.foo} ${b.foo}') == "w-t-afoo bfoo"
* assert (context.ofNode("c") % 'w-t-${a.bar}') == "w-t-abar"
* assert (context.ofNode("x") % 'w-t-${x.bar}') == 'w-t-${x.bar}'
* </code></pre>
*/
class NodeContext implements Serializable {
final VAR_EXPRESSION = /\$\{\w*\.\w+\}/
final def node
final Set ancestors
NodeContext(contextNode, contextAncestors) {
globals[contextNode] = globals[contextNode] ?: [:]
node = contextNode
ancestors = contextAncestors
}
/**
* Binds a value to a name in the globals store under the node's
* namespace. The value may later be retrieved by any descendent node's
* context using {@link binding()}.
*/
@NonCPS
void bind(String key, value)
throws NameAlreadyBoundException {
if (globals[node].containsKey(key)) {
throw new NameAlreadyBoundException(key: key)
}
globals[node][key] = value
}
/**
* Retrieves a value previously bound using {@link bind()} to this node's
* context. If the given key is not found under this node's namespace, a
* {@link NameNotFoundException} is thrown.
*/
def binding(String key)
throws NameNotFoundException {
if (!globals[node].containsKey(key)) {
throw new NameNotFoundException(ns: node, key: key)
}
globals[node][key]
}
/**
* Retrieves a value previously bound using {@link bind()} under the given
* ancestor node's namespace and name.
*/
def binding(ancestorNode, String key)
throws NameNotFoundException, AncestorNotFoundException {
if (!(ancestorNode in ancestors)) {
throw new AncestorNotFoundException(ancestor: ancestorNode, node: node)
}
if (!globals[ancestorNode].containsKey(key)) {
throw new NameNotFoundException(ns: ancestorNode, key: key)
}
globals[ancestorNode][key]
}
/**
* Returns all objects bound to the given name under any node namespace,
* as well as the node under which it is found.
*
* This should only be used at the end of an execution graph.
*/
Map getAll(String key) {
globals.findAll { it.value[key] != null }.collectEntries { [it.key, it.value[key]] }
}
/**
* Operator alias for {@link binding(String)} or, if a "namespace.key" is
* given, {@link binding(def, String)}.
*/
def getAt(String key) {
def keys = key.split(/\./)
if (keys.size() > 1) {
if (keys[0] == "") {
return binding(keys[1])
}
return binding(keys[0], keys[1])
}
return binding(key)
}
/**
* Interpolates the given string by substituting all symbol expressions
* with values previously bound by ancestor nodes.
*/
String interpolate(String str) {
// NOTE call to replaceAll does not rely on its sub matching feature as
// Groovy CPS does not implement it correctly, and marking this method
// as NonCPS causes it to only ever return the first substitution.
str.replaceAll(VAR_EXPRESSION) {
this[it[2..-2]]
}
}
/**
* Operator alias for {@link interpolate()}.
*/
String mod(String str) {
interpolate(str)
}
/**
* Operator alias for {@link bind()}.
*/
void putAt(String key, value) {
bind(key, value)
}
}
class AncestorNotFoundException extends GroovyException {
def ancestor, node
String getMessage() {
"cannot access '${ancestor}.*' values since '${node}' does not follow it in the graph '${graph}'"
}
}
class NameNotFoundException extends GroovyException {
def ns, key
String getMessage() {
"no value bound for '${ns}.${key}'; all bound names are: ${getAllKeys().join(", ")}"
}
}
class NameAlreadyBoundException extends GroovyException {
def key
String getMessage() {
"'${node}' already has a value assigned to '${key}'"
}
}
}