/* * 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.DslParser; import org.apache.brooklyn.camp.brooklyn.spi.dsl.parse.FunctionWithArgs; import org.apache.brooklyn.camp.brooklyn.spi.dsl.parse.QuotedString; 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.javalang.Reflections; import org.apache.brooklyn.util.text.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; /** * {@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 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) { 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) { if (key.getNewValue() instanceof FunctionWithArgs) { try { currentNode.set(node); FunctionWithArgs f = (FunctionWithArgs) key.getNewValue(); 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<Object>(); 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) { o = evaluateOn( o, (FunctionWithArgs)i, deepEvaluation ); } 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()+"'"); Class<?> clazz; if (o instanceof Class) { clazz = (Class<?>)o; } else { clazz = o.getClass(); } if (!(clazz.getPackage().getName().startsWith(BrooklynDslCommon.class.getPackage().getName()))) throw new IllegalArgumentException("Not permitted to invoke function on '"+clazz+"' (outside allowed package scope)"); String fn = f.getFunction(); fn = Strings.removeFromStart(fn, "$brooklyn:"); 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."); } try { List<Object> args = new ArrayList<>(); for (Object arg: f.getArgs()) { args.add( deepEvaluation ? evaluate(arg, true) : arg ); } Optional<Object> v = Reflections.invokeMethodWithArgs(o, fn, args); if (v.isPresent()) return v.get(); } catch (Exception e) { Exceptions.propagateIfFatal(e); throw Exceptions.propagate(new InvocationTargetException(e, "Error invoking '"+fn+"' on '"+o+"'")); } throw new IllegalArgumentException("No such function '"+fn+"' on "+o); } }