blob: c42b7993e7ce8184ee482dc03545560a6329fc18 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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.apache.brooklyn.camp.brooklyn.spi.dsl;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.BrooklynDslCommon;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.parse.*;
import org.apache.brooklyn.camp.spi.resolve.PlanInterpreter;
import org.apache.brooklyn.camp.spi.resolve.PlanInterpreter.PlanInterpreterAdapter;
import org.apache.brooklyn.camp.spi.resolve.interpret.PlanInterpretationNode;
import org.apache.brooklyn.camp.spi.resolve.interpret.PlanInterpretationNode.Role;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link PlanInterpreter} which understands the $brooklyn DSL
*/
public class BrooklynDslInterpreter extends PlanInterpreterAdapter {
private static final Logger log = LoggerFactory.getLogger(BrooklynDslInterpreter.class);
@Override
public boolean isInterestedIn(PlanInterpretationNode node) {
return node.matchesPrefix("$brooklyn:") || node.getNewValue() instanceof FunctionWithArgs;
}
private static ThreadLocal<PlanInterpretationNode> currentNode = new ThreadLocal<PlanInterpretationNode>();
/** returns the current node, stored in a thread-local, to populate the dsl field of {@link BrooklynDslDeferredSupplier} instances */
public static PlanInterpretationNode currentNode() {
return currentNode.get();
}
/** sets the current node */
public static void currentNode(PlanInterpretationNode node) {
currentNode.set(node);
}
public static void currentNodeClear() {
currentNode.set(null);
}
@Override
public void applyYamlPrimitive(PlanInterpretationNode node) {
String expression = node.getNewValue().toString();
try {
currentNode.set(node);
Object parsedNode = new DslParser(expression).parse();
if (parsedNode instanceof PropertyAccess) {
parsedNode = new FunctionWithArgs(""+((PropertyAccess)parsedNode).getSelector(), null);
}
if ((parsedNode instanceof FunctionWithArgs) && ((FunctionWithArgs)parsedNode).getArgs()==null) {
if (node.getRoleInParent() == Role.MAP_KEY) {
node.setNewValue(parsedNode);
// will be handled later
} else {
throw new IllegalStateException("Invalid function-only expression '"+((FunctionWithArgs)parsedNode).getFunction()+"'");
}
} else {
node.setNewValue( evaluate(parsedNode, true) );
}
} catch (Exception e) {
// we could try parsing it as yaml and if it comes back as a map or a list, reapply the interpreter;
// useful in some contexts where strings are required by the source (eg CFN, TOSCA)
log.warn("Error evaluating node (rethrowing) '"+expression+"': "+e);
Exceptions.propagateIfFatal(e);
throw new IllegalArgumentException("Error evaluating node '"+expression+"'", e);
} finally {
currentNodeClear();
}
}
@Override
public boolean applyMapEntry(PlanInterpretationNode node, Map<Object, Object> mapIn, Map<Object, Object> mapOut,
PlanInterpretationNode key, PlanInterpretationNode value) {
Object knv = key.getNewValue();
if (knv instanceof PropertyAccess) {
// when property access is used as a key, it is a function without args
knv = new FunctionWithArgs(""+((PropertyAccess)knv).getSelector(), null);
}
if (knv instanceof FunctionWithArgs) {
try {
currentNode.set(node);
FunctionWithArgs f = (FunctionWithArgs) knv;
if (f.getArgs()!=null)
throw new IllegalStateException("Invalid map key function "+f.getFunction()+"; should not have arguments if taking arguments from map");
// means evaluation acts on values
List<Object> args = new ArrayList<>();
if (value.getNewValue() instanceof Iterable<?>) {
for (Object vi: (Iterable<?>)value.getNewValue())
args.add(vi);
} else {
args.add(value.getNewValue());
}
try {
// TODO in future we should support functions of the form 'Maps.clear', 'Maps.reset', 'Maps.remove', etc;
// default approach only supported if mapIn has single item and mapOut is empty
if (mapIn.size()!=1)
throw new IllegalStateException("Map-entry DSL syntax only supported with single item in map, not "+mapIn);
if (mapOut.size()!=0)
throw new IllegalStateException("Map-entry DSL syntax only supported with empty output map-so-far, not "+mapOut);
node.setNewValue( evaluate(new FunctionWithArgs(f.getFunction(), args), false) );
return false;
} catch (Exception e) {
log.warn("Error evaluating map-entry (rethrowing) '"+f.getFunction()+args+"': "+e);
Exceptions.propagateIfFatal(e);
throw new IllegalArgumentException("Error evaluating map-entry '"+f.getFunction()+args+"'", e);
}
} finally {
currentNodeClear();
}
}
return super.applyMapEntry(node, mapIn, mapOut, key, value);
}
public Object evaluate(Object f, boolean deepEvaluation) {
if (f instanceof FunctionWithArgs) {
return evaluateOn(BrooklynDslCommon.class, (FunctionWithArgs) f, deepEvaluation);
}
if (f instanceof List) {
Object o = BrooklynDslCommon.class;
for (Object i: (List<?>)f) {
if (i instanceof FunctionWithArgs) {
o = evaluateOn(o, (FunctionWithArgs) i, deepEvaluation);
} else if (i instanceof PropertyAccess) {
o = evaluateOn(o, (PropertyAccess) i);
} else throw new IllegalArgumentException("Unexpected element in parse tree: '"+i+"' (type "+(i!=null ? i.getClass() : null)+")");
}
return o;
}
if (f instanceof QuotedString) {
return ((QuotedString)f).unwrapped();
}
throw new IllegalArgumentException("Unexpected element in parse tree: '"+f+"' (type "+(f!=null ? f.getClass() : null)+")");
}
public Object evaluateOn(Object o, FunctionWithArgs f, boolean deepEvaluation) {
if (f.getArgs()==null)
throw new IllegalStateException("Invalid function-only expression '"+f.getFunction()+"'");
String fn = f.getFunction();
fn = Strings.removeFromStart(fn, BrooklynDslCommon.PREFIX);
if (fn.startsWith("function.")) {
// If the function name starts with 'function.', then we look for the function in BrooklynDslCommon.Functions
// As all functions in BrooklynDslCommon.Functions are static, we don't need to worry whether a class
// or an instance was passed into this method
o = BrooklynDslCommon.Functions.class;
fn = Strings.removeFromStart(fn, "function.");
}
List<Object> args = new ArrayList<>();
for (Object arg: f.getArgs()) {
args.add( deepEvaluation ? evaluate(arg, true) : arg );
}
try {
// TODO Could move argument resolve in DslDeferredFunctionCall freeing each Deffered implementation
// having to handle it separately. The shortcoming is that will lose the eager evaluation we have here.
if (o instanceof BrooklynDslDeferredSupplier && !(o instanceof DslFunctionSource)) {
return new DslDeferredFunctionCall(o, fn, args);
} else {
// Would prefer to keep the invocation logic encapsulated in DslDeferredFunctionCall, but
// for backwards compatibility will evaluate as much as possible eagerly (though it shouldn't matter in theory).
return DslDeferredFunctionCall.invokeOn(o, fn, args).get();
}
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
throw Exceptions.propagate(new InvocationTargetException(e, "Error invoking '"+fn+"' on '"+o+"' with arguments "+args+""));
}
}
public Object evaluateOn(Object o, PropertyAccess propAccess) {
if(propAccess.getSelector() == null) {
throw new IllegalStateException("Invalid property-selector expression!");
}
try {
Object index = propAccess.getSelector();
if (index instanceof QuotedString) {
index = ((QuotedString)index).unwrapped();
}
return new DslDeferredPropertyAccess((BrooklynDslDeferredSupplier) o, index);
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
throw Exceptions.propagate(new InvocationTargetException(e, "Error accessing property " + propAccess.getSelector() + " on " + o));
}
}
}