blob: f363261e1f83db801dcdad5add3b4b80006edc31 [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.core.workflow.steps.variables;
import com.google.common.reflect.TypeToken;
import freemarker.core.ParseException;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.workflow.WorkflowExecutionContext;
import org.apache.brooklyn.core.workflow.WorkflowExpressionResolution;
import org.apache.brooklyn.core.workflow.WorkflowStepDefinition;
import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
import org.apache.brooklyn.util.collections.*;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.core.predicates.DslPredicates;
import org.apache.brooklyn.util.core.text.TemplateProcessor;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.text.QuotedStringTokenizer;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Timestamp;
import org.apache.commons.lang3.tuple.MutablePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.time.Instant;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
public class SetVariableWorkflowStep extends WorkflowStepDefinition {
private static final Logger log = LoggerFactory.getLogger(SetVariableWorkflowStep.class);
public static final String SHORTHAND =
"[ [ ${variable.type} ] ${variable.name} [ \"=\" ${value...} ] ]";
public static final ConfigKey<TypedValueToSet> VARIABLE = ConfigKeys.newConfigKey(TypedValueToSet.class, "variable");
public static final ConfigKey<Object> VALUE = ConfigKeys.newConfigKey(Object.class, "value");
public enum InterpolationMode {
WORDS,
DISABLED,
FULL,
}
public static final ConfigKey<InterpolationMode> INTERPOLATION_MODE = ConfigKeys.newConfigKey(InterpolationMode.class, "interpolation_mode",
"Whether interpolation runs on the full value (not touching quotes; the default in most places), " +
"on words (if unquoted, unquoting others; the default for 'let var = value' shorthand), " +
"or is disabled (not applied at all)");
public static final ConfigKey<TemplateProcessor.InterpolationErrorMode> INTERPOLATION_ERRORS = ConfigKeys.newConfigKey(TemplateProcessor.InterpolationErrorMode.class, "interpolation_errors",
"Whether unresolvable interpolated expressions fail and return an error (the default for 'let'), " +
"ignore the expression leaving it in place (the default for 'load'), " +
"or replace the expression with a blank string");
@Override
public void populateFromShorthand(String expression) {
Map<String, Object> newInput = populateFromShorthandTemplate(SHORTHAND, expression, true, true);
if (newInput.get(VALUE.getName())!=null && input.get(INTERPOLATION_MODE.getName())==null) {
setInput(INTERPOLATION_MODE, InterpolationMode.WORDS);
}
}
@Override
public void validateStep(@Nullable ManagementContext mgmt, @Nullable WorkflowExecutionContext workflow) {
super.validateStep(mgmt, workflow);
if (input.get(VARIABLE.getName())==null) {
throw new IllegalArgumentException("Variable name is required");
}
if (input.get(VALUE.getName())==null) {
throw new IllegalArgumentException("Value is required");
}
}
@Override
protected Object doTaskBody(WorkflowStepInstanceExecutionContext context) {
TypedValueToSet variable = context.getInput(VARIABLE);
if (variable ==null) throw new IllegalArgumentException("Variable name is required");
String name = context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_INPUT, variable.name, String.class);
if (Strings.isBlank(name)) throw new IllegalArgumentException("Variable name is required");
TypeToken<?> type = context.lookupType(variable.type, () -> null);
Object unresolvedValue = input.get(VALUE.getName());
Object resolvedValue = new ConfigurableInterpolationEvaluation(context, type, unresolvedValue, context.getInputOrDefault(INTERPOLATION_MODE), context.getInputOrDefault(INTERPOLATION_ERRORS)).evaluate();
Object oldValue = setWorkflowScratchVariableDotSeparated(context, name, resolvedValue);
// these are easily inferred from workflow vars
// context.noteOtherMetadata("Value set", resolvedValue);
// if (oldValue!=null) context.noteOtherMetadata("Previous value", oldValue);
return context.getPreviousStepOutput();
}
static Object setWorkflowScratchVariableDotSeparated(WorkflowStepInstanceExecutionContext context, String name, Object resolvedValue) {
Object oldValue;
if (name.contains(".")) {
String[] names = name.split("\\.");
String names0 = names[0];
if ("output".equals(names0)) throw new IllegalArgumentException("Cannot set subfield in output"); // catch common error
Object h = context.getWorkflowExectionContext().getWorkflowScratchVariables().get(names0);
if (!(h instanceof Map)) throw new IllegalArgumentException("Cannot set " + name + " because " + names0 + " is " + (h == null ? "unset" : "not a map"));
for (int i = 1; i < names.length - 1; i++) {
Object hi = ((Map<?, ?>) h).get(names[i]);
if (hi == null) {
hi = MutableMap.of();
((Map) h).put(names[i], hi);
} else if (!(hi instanceof Map))
throw new IllegalArgumentException("Cannot set " + name + " because " + names[i] + " is not a map");
h = hi;
}
oldValue = ((Map) h).put(names[names.length - 1], resolvedValue);
} else if (name.contains("[")) {
String[] names = name.split("((?<=\\[|\\])|(?=\\[|\\]))");
if (names.length != 4 || !"[".equals(names[1]) || !"]".equals(names[3])) {
throw new IllegalArgumentException("Invalid list index specifier " + name);
}
String listName = names[0];
int listIndex = Integer.parseInt(names[2]);
Object o = context.getWorkflowExectionContext().getWorkflowScratchVariables().get(listName);
if (!(o instanceof List))
throw new IllegalArgumentException("Cannot set " + name + " because " + listName + " is " + (o == null ? "unset" : "not a list"));
List l = MutableList.copyOf(((List)o));
if (listIndex < 0 || listIndex >= l.size()) {
throw new IllegalArgumentException("Invalid list index " + listIndex);
}
oldValue = l.set(listIndex, resolvedValue);
context.getWorkflowExectionContext().updateWorkflowScratchVariable(listName, l);
} else {
oldValue = context.getWorkflowExectionContext().updateWorkflowScratchVariable(name, resolvedValue);
}
return oldValue;
}
private enum LetMergeMode { NONE, SHALLOW, DEEP }
public static class ConfigurableInterpolationEvaluation<T> {
protected final WorkflowStepInstanceExecutionContext context;
protected final TypeToken<T> type;
protected final Object unresolvedValue;
protected final InterpolationMode interpolationMode;
protected final TemplateProcessor.InterpolationErrorMode errorMode;
public ConfigurableInterpolationEvaluation(WorkflowStepInstanceExecutionContext context, TypeToken<T> type, Object unresolvedValue) {
this(context, type, unresolvedValue, null, null);
}
public ConfigurableInterpolationEvaluation(WorkflowStepInstanceExecutionContext context, TypeToken<T> type, Object unresolvedValue, InterpolationMode interpolationMode, TemplateProcessor.InterpolationErrorMode errorMode) {
this.context = context;
this.unresolvedValue = unresolvedValue;
this.type = type;
this.interpolationMode = interpolationMode;
this.errorMode = errorMode;
}
public boolean unquotedStartsWith(String s, char c) {
if (s==null) return false;
s = s.trim();
s = Strings.removeFromStart(s, "\"");
s = Strings.removeFromStart(s, "\'");
s = s.trim();
return (s.startsWith(""+c));
}
public boolean trimmedEndsWithQuote(String s) {
if (s==null) return false;
s = s.trim();
return s.endsWith("\"") || s.endsWith("\'");
}
public Function<String,TypeToken<?>> ifUnquotedStartsWithThen(char c, Class<?> clazz) {
return s -> (!unquotedStartsWith(s, c)) ? null : TypeToken.of(clazz);
}
public T evaluate() {
try {
Object result = unresolvedValue;
Object resultCoerced;
TypeToken<? extends Object> typeIntermediate = type == null ? TypeToken.of(Object.class) : type;
if (interpolationMode == InterpolationMode.DISABLED) {
resultCoerced = context.getWorkflowExectionContext().resolveCoercingOnly(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_RUNNING, result, typeIntermediate);
} else if (result instanceof String && interpolationMode == InterpolationMode.WORDS) {
result = process((String) result);
resultCoerced = context.getWorkflowExectionContext().resolveCoercingOnly(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_RUNNING, result, typeIntermediate);
} else {
// full, or null the default
resultCoerced = resolveSubPart(result, typeIntermediate);
}
return (T) resultCoerced;
} catch (Exception e) {
throw e;
}
}
<T> T resolveSubPart(Object v, TypeToken<T> type) {
return new WorkflowExpressionResolution(context.getWorkflowExectionContext(), WorkflowExpressionResolution.WorkflowExpressionStage.STEP_RUNNING, false, WorkflowExpressionResolution.WrappingMode.NONE, errorMode)
.resolveWithTemplates(v, type);
}
Object process(String input) {
if (Strings.isBlank(input)) return input;
List<String> wordsByQuote = null;
try {
// first deal with internal quotes
QuotedStringTokenizer qst = qst(input);
if (qst.isQuoted(input)) {
// special treatment if whole line is quoted
wordsByQuote = MutableList.of(input);
} else {
wordsByQuote = qst.remainderAsList();
}
// then look for operators etc
return process(wordsByQuote);
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
if (wordsByQuote==null || wordsByQuote.size()>1) {
if (Exceptions.getCausalChain(e).stream().anyMatch(cause -> cause instanceof ParseException || cause instanceof IllegalArgumentException)) {
// try again with the whole thing as tokens, if it is an interpolated string with spaces inside it or mismatched quotes
try {
return process(MutableList.of(input));
} catch (Exception e2) {
log.debug("Failed to process expression as tokens or as string; preferring error from former, but error from latter was: " + e2);
}
}
}
throw Exceptions.propagate(e);
}
}
QuotedStringTokenizer qst(String input) {
return QuotedStringTokenizer.builder().includeQuotes(true).includeDelimiters(true).expectQuotesDelimited(true).failOnOpenQuote(true).build(input);
}
Object process(List<String> w) {
return process(w, false);
}
Object process(List<String> w, boolean ternaryColonAllowed) {
// if no tokens, treat as null
if (w.isEmpty()) return null;
Maybe<Object> result;
// Order of operations:
// not used: () [] -> . :: (i.e. Function call, scope, array/member access)
// not used: ! ~ - + & ++ -- (i.e. unary operators)
// #__: ?: (i.e. ternary)
result = handleTokenIfPresent(w, false,
ternaryColonAllowed ? MutableMap.of(
"?", this::handleTernaryCondition,
":", this::handleTernaryArms)
: MutableMap.of("?", this::handleTernaryCondition)
);
if (result.isPresent()) return result.get();
// ?: ?? (nullish - treat as an unary operator)
result = handleTokenIfPresent(w, false, MutableMap.of("??", this::handleNullish));
if (result.isPresent()) return result.get();
//NOTE: levels 4 and 3 are out of order (by the C Order or operations)
// #4: + -
result = handleTokenIfPresent(w, true, MutableMap.of("+", this::handleAdd, "-", this::handleSubtract));
if (result.isPresent()) return result.get();
// #3: - * / % MOD
result = handleTokenIfPresent(w, true, MutableMap.of("*", this::handleMultiply, "/", this::handleDivide));
if (result.isPresent()) return result.get();
result = handleTokenIfPresent(w, true, MutableMap.of("%", this::handleModulo));
if (result.isPresent()) return result.get();
// #5: << >> (i.e. bitwise shift left and right
// #6: < <= > >=
result = handleTokenIfPresent(w, false, MutableMap.of(
"==", this::handleEquals,
"<", this::handleOrderedLessThan,
"<=", this::handleOrderedLessThanOrEqual,
">", this::handleOrderedGreaterThan,
">=", this::handleOrderedGreaterThanOrEqual
));
if (result.isPresent()) return result.get();
// #7: == !=
// #8: & (i.e. bitwise AND)
// #9: ^ (i.e. bitwise XOR)
// #10: | (i.e. bitwise OR)
// #11: && (i.e. logical AND)
result = handleTokenIfPresent(w, false, MutableMap.of("&&", this::handleBooleanAnd));
if (result.isPresent()) return result.get();
// #12: || (i.e. logical OR)
result = handleTokenIfPresent(w, false, MutableMap.of("||", this::handleBooleanOr));
if (result.isPresent()) return result.get();
// #14: = += -= *= /= %= &= |= ^= <<= >>= (i.e. assignment operators)
// #15: ,
// tokens include space delimiters and are still quotes, so unwrap then just stitch together. will preserve spaces.
boolean resolveToString = w.size()>1;
QuotedStringTokenizer qst = qst("");
List<Object> objs = w.stream().map(t -> {
if (qst.isQuoted(t)) return qst.unwrapIfQuoted(t);
TypeToken<?> target = resolveToString ? TypeToken.of(String.class) : TypeToken.of(Object.class);
return resolveSubPart(t, target);
}).collect(Collectors.toList());
if (!resolveToString) return objs.get(0);
return ((List<String>)(List)objs).stream().collect(Collectors.joining());
}
private Maybe<Object> handleTokenIfPresent(List<String> tokens, boolean startAtRight, Map<String, BiFunction<List<String>,List<String>,Object>> tokenProcessors) {
for (int i0=0; i0<tokens.size(); i0++) {
int i = startAtRight ? tokens.size()-1-i0 : i0;
String t = tokens.get(i);
BiFunction<List<String>, List<String>, Object> p = tokenProcessors.get(t);
if (p!=null) {
List<String> lhs = trim(tokens.subList(0, i));
List<String> rhs = trim(tokens.subList(i + 1, tokens.size()));
return Maybe.of(p.apply(lhs, rhs));
}
}
return Maybe.absent();
}
private List<String> trim(List<String> l) {
if (!l.isEmpty() && Strings.isBlank(l.get(0))) l = l.subList(1, l.size());
if (!l.isEmpty() && Strings.isBlank(l.get(l.size()-1))) l = l.subList(0, l.size()-1);
return l;
}
Object handleNullish(List<String> lhs, List<String> rhs) {
return processMaybe(lhs, null).or(() -> process(rhs));
}
Maybe<Object> processMaybe(List<String> lhs, Function<String,TypeToken<?>> explicitType) {
try {
Object result = process(lhs);
if (result!=null) return Maybe.of(result);
return Maybe.absent("null");
} catch (Exception e) {
if (Exceptions.isCausedByInterruptInAnyThread(e) && !Thread.currentThread().isInterrupted()) {
// value was unavailable, pass through to RHS
return Maybe.absent("unavailable");
} else {
Exceptions.propagateIfFatal(e);
}
if (log.isTraceEnabled()) {
log.trace("Non-fatal exception processing expression "+ lhs +" (in context where there is an alterantive)", e);
}
return Maybe.absent(e);
}
}
Maybe<Integer> asInteger(Object x) {
Maybe<Integer> xi = TypeCoercions.tryCoerce(x, Integer.class);
Maybe<Double> xd = asDouble(x);
if (xi.isAbsent() || xd.isAbsent()) return xi;
if (Math.abs(xd.get() - xi.get())<0.0000000001) return xi;
return Maybe.absent("Double value does not match integer value");
}
Maybe<Double> asDouble(Object x) {
Maybe<Double> v = TypeCoercions.tryCoerce(x, Double.class);
if (v.isPresent() && !Double.isFinite(v.get())) return Maybe.absent(() -> new IllegalArgumentException("Value cannot be coerced to double: "+v));
return v;
}
Object applyMathOperator(String op, List<String> lhs0, List<String> rhs0, BiFunction<Integer,Integer,Number> ifInt, BiFunction<Double,Double,Number> ifDouble) {
Object lhs = process(lhs0);
Object rhs = process(rhs0);
if ("+".equals(op)) {
if (lhs instanceof Duration) {
if (rhs instanceof Instant || rhs instanceof Date) {
Object newRhs = lhs;
lhs = rhs;
rhs = newRhs;
// fall through to below
} else {
return TypeCoercions.coerce(rhs, Duration.class).add((Duration) lhs);
}
}
if (lhs instanceof Instant) return TypeCoercions.coerce(rhs, Duration.class).addTo((Instant) lhs);
if (lhs instanceof Date)
return new Timestamp((Instant) TypeCoercions.coerce(rhs, Duration.class).addTo(((Date) lhs).toInstant()));
} else if ("-".equals(op)) {
if (lhs instanceof Duration) {
return ((Duration)lhs).subtract(TypeCoercions.coerce(rhs, Duration.class));
}
if (lhs instanceof Instant) {
if (rhs instanceof Instant) return Duration.between((Instant)rhs, (Instant)lhs);
return TypeCoercions.coerce(rhs, Duration.class).multiply(-1).addTo((Instant) lhs);
}
if (lhs instanceof Date) {
if (rhs instanceof Date) return Duration.between(((Date)rhs).toInstant(), ((Date)lhs).toInstant());
return new Timestamp((Instant) TypeCoercions.coerce(rhs, Duration.class).multiply(-1).addTo(((Date) lhs).toInstant()));
}
}
Maybe<Integer> lhsI = asInteger(lhs);
Maybe<Integer> rhsI = asInteger(rhs);
if (lhsI.isPresent() && rhsI.isPresent()) {
Number x = ifInt.apply(lhsI.get(), rhsI.get());
return ((Maybe)asInteger(x)).orMaybe(() -> asDouble(x)).get();
}
if (ifDouble!=null) {
Maybe<Double> lhsD = asDouble(lhs);
Maybe<Double> rhsD = asDouble(rhs);
if (lhsD.isPresent() && rhsD.isPresent()) return asDouble(ifDouble.apply(lhsD.get(), rhsD.get())).get();
if (lhsD.isAbsent()) failOnInvalidArgument("left", op, lhs0, lhs);
if (rhsD.isAbsent()) failOnInvalidArgument("right", op, rhs0, rhs);
} else {
if (lhsI.isAbsent()) failOnInvalidArgument("left", op, lhs0, lhs);
if (rhsI.isAbsent()) failOnInvalidArgument("right", op, rhs0, rhs);
}
throw new IllegalArgumentException("Should not come here");
}
Object applyBooleanOperator(List<String> lhs0, List<String> rhs0, BiFunction<Boolean, Boolean, Boolean> biFn) {
Object lhs = process(lhs0);
Object rhs = process(rhs0);
Maybe<Boolean> lhsB = asBoolean(lhs);
Maybe<Boolean> rhsB = asBoolean(rhs);
if (lhsB.isPresent() && rhsB.isPresent()) {
return biFn.apply(lhsB.get(), rhsB.get());
}
throw new IllegalArgumentException("Should not come here");
}
boolean applyComparison(List<String> lhs0, List<String> rhs0, Function<Integer, Boolean> test) {
Object lhs = process(lhs0);
Object rhs = process(rhs0);
return DslPredicates.coercedCompare(lhs, rhs, test);
}
Maybe<Boolean> asBoolean(Object x) {
return TypeCoercions.tryCoerce(x, Boolean.class);
}
private IllegalArgumentException failOnInvalidArgument(String side, String op, Object pre, Object post) {
String msg = "Invalid "+side+" argument to operation '"+op+"'";
String postS = ""+post;
if (postS.contains("*") || postS.contains("+") || postS.contains("+") || postS.contains("/")) {
// we could weaken this contraint but for now at least make it clear
msg += "; mathematical operations must have spaces around them for disambiguation";
}
throw new IllegalArgumentException(msg + ": "+pre+" => "+post);
}
Object handleMultiply(List<String> lhs, List<String> rhs) {
return applyMathOperator("*", lhs, rhs, (a,b)->a*b, (a,b)->a*b);
}
Object handleDivide(List<String> lhs, List<String> rhs) {
return applyMathOperator("/", lhs, rhs, (a,b)->1.0*a/b, (a,b)->a/b);
}
Object handleBooleanAnd(List<String> lhs, List<String> rhs) {
return applyBooleanOperator(lhs, rhs, (a, b) -> a && b);
}
Object handleBooleanOr(List<String> lhs, List<String> rhs) {
return applyBooleanOperator(lhs, rhs, (a, b) -> a || b);
}
Object handleOrderedGreaterThan(List<String> lhs, List<String> rhs) {
return applyComparison(lhs, rhs, v -> v>0);
}
Object handleOrderedGreaterThanOrEqual(List<String> lhs, List<String> rhs) {
return applyComparison(lhs, rhs, v -> v>=0);
}
Object handleOrderedLessThan(List<String> lhs, List<String> rhs) {
return applyComparison(lhs, rhs, v -> v<0);
}
Object handleOrderedLessThanOrEqual(List<String> lhs, List<String> rhs) {
return applyComparison(lhs, rhs, v -> v<=0);
}
boolean handleEquals(List<String> lhs, List<String> rhs) {
return DslPredicates.coercedEqual(process(lhs), process(rhs));
}
Object handleTernaryCondition(List<String> lhs0, List<String> rhs0) {
//log.info(String.format("Ternary Condition 0: [lhs:%s][rhs:%s]", lhs0, rhs0));
Object lhs = process(lhs0);
Object rhs;
int questionIndex = rhs0.indexOf("?");
int colonIndex = rhs0.indexOf(":");
if (questionIndex > -1 && questionIndex < colonIndex) {
// Nested ternary
rhs = handleNestedTernaryRhs(rhs0);
} else if (questionIndex > -1 && colonIndex < questionIndex) {
// Chained ternary
rhs = handleChainedTernaryRhs(rhs0);
} else {
// non-nested or chained
rhs = process(rhs0, true);
}
//log.info(String.format("Ternary Condition 1: [lhs:%s][rhs:%s]", lhs, rhs));
if (!(rhs instanceof TernaryArms)) throw new IllegalArgumentException("Mismatched ternary ':' operator");
Maybe<Boolean> condition = asBoolean(lhs);
if (condition.isPresent()){
if (condition.get()) {
// ? left : right -- rhs length is 5 [left, ,:, ,right]
// ? true && true : false || false -- rhs length is ???
return process( ((TernaryArms)rhs).getLeft());
//throw new IllegalArgumentException("TERNARY CONDITION IS TRUE");
} else {
return process( ((TernaryArms)rhs).getRight());
//throw new IllegalArgumentException("TERNARY CONDITION IS FALSE");
}
}
throw new IllegalArgumentException("Leading term of ternary '"+lhs+"' does not evaluate to a boolean");
}
public TernaryArms handleNestedTernaryRhs(List<String> rhs) {
int lastColonIndex = rhs.lastIndexOf(":");
if (lastColonIndex == -1) {
throw new IllegalArgumentException("Mismatched ternary ':' operator");
}
int firstColonIndex = rhs.indexOf(":");
if (firstColonIndex == lastColonIndex) {
return (TernaryArms) process(rhs);
}
return new TernaryArms(trim(rhs.subList(0, lastColonIndex)), trim(rhs.subList(lastColonIndex + 1, rhs.size())));
}
public TernaryArms handleChainedTernaryRhs(List<String> rhs) {
int colonIndex = rhs.indexOf(":");
if (colonIndex == -1) {
throw new IllegalArgumentException("Mismatched ternary ':' operator");
}
return new TernaryArms(trim(rhs.subList(0, colonIndex)), trim(rhs.subList(colonIndex + 1, rhs.size())));
}
static class TernaryArms extends MutablePair<List<String>,List<String>> {
public TernaryArms(List<String> lhs, List<String> rhs) {
super(lhs, rhs);
}
}
public Object handleTernaryArms(List<String> lhs0, List<String> rhs0) {
//log.info(String.format("Ternary 0: [lhs:%s][rhs:%s]", lhs0, rhs0));
return new TernaryArms(lhs0, rhs0);
}
Object handleAdd(List<String> lhs, List<String> rhs) {
return applyMathOperator("+", lhs, rhs, (a,b)->a+b, (a,b)->a+b);
}
Object handleSubtract(List<String> lhs, List<String> rhs) {
return applyMathOperator("-", lhs, rhs, (a,b)->a-b, (a,b)->a-b);
}
Object handleModulo(List<String> lhs, List<String> rhs) {
return applyMathOperator("%", lhs, rhs, (a,b)->a%b, null);
}
}
@Override protected Boolean isDefaultIdempotent() { return true; }
}