blob: 7f7c2f82542f728ae45a506b8e8520841c88ad6e [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.collect.Iterables;
import com.google.common.reflect.TypeToken;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.typereg.RegisteredType;
import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts;
import org.apache.brooklyn.core.typereg.RegisteredTypes;
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.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.apache.brooklyn.core.workflow.WorkflowExecutionContext.STEP_TARGET_NAME_FOR_END;
import static org.apache.brooklyn.core.workflow.steps.variables.SetVariableWorkflowStep.setWorkflowScratchVariableDotSeparated;
public class TransformVariableWorkflowStep extends WorkflowStepDefinition {
private static final Logger log = LoggerFactory.getLogger(TransformVariableWorkflowStep.class);
public static final String[] SHORTHAND_OPTIONS = {
"[ ${variable.type} ] " +
"\"variable\" ${variable.name} " +
// "[ \"=\" ${value...} ] " + // no point in allowing this
"[ \"|\" ${transform...} ]",
"[ ${variable.type} ] " +
"\"value\" ${value} " +
"[ \"|\" ${transform...} ]",
"[ ${variable.type} ] " +
"${vv_auto} " +
"[ \"=\" ${value...} ] " +
"[ \"|\" ${transform...} ]",
};
public static final ConfigKey<TypedValueToSet> VARIABLE = ConfigKeys.newConfigKey(TypedValueToSet.class, "variable");
public static final ConfigKey<Object> VALUE = ConfigKeys.newConfigKey(Object.class, "value");
public static final ConfigKey<Object> VV_AUTO = ConfigKeys.newConfigKey(Object.class, "vv_auto");
public static final ConfigKey<Object> TRANSFORM = ConfigKeys.newConfigKey(Object.class, "transform");
@Override
public void populateFromShorthand(String expression) {
Map<String, Object> match = null;
for (int i=0; i<SHORTHAND_OPTIONS.length && match==null; i++)
match = populateFromShorthandTemplate(SHORTHAND_OPTIONS[i], expression, false, false);
if (match==null && Strings.isNonBlank(expression)) {
throw new IllegalArgumentException("Invalid shorthand expression for transform: '" + expression + "'");
}
}
@Override
public void validateStep(@Nullable ManagementContext mgmt, @Nullable WorkflowExecutionContext workflow) {
super.validateStep(mgmt, workflow);
if (!input.containsKey(TRANSFORM.getName())) {
throw new IllegalArgumentException("Transform is required");
}
}
@Override
protected Object doTaskBody(WorkflowStepInstanceExecutionContext context) {
TypedValueToSet variable = context.getInput(VARIABLE);
Object transformO = context.getInputRaw(TRANSFORM.getName());
if (!(transformO instanceof Iterable)) transformO = MutableList.of(transformO);
List<String> transforms = MutableList.of();
for (Object t: (Iterable)transformO) {
if (t instanceof String) transforms.addAll(MutableList.copyOf(Arrays.stream( ((String)t).split("\\|") ).map(String::trim).collect(Collectors.toList())));
else throw new IllegalArgumentException("Argument to transform should be a string or list of strings, not: "+t);
}
String varName = null;
Object vv_auto = input.get(VV_AUTO.getName());
boolean variableNameSpecified = variable != null && Strings.isNonBlank(variable.name);
Object valueExpressionToTransform = null;
boolean setVariableAtEnd = variableNameSpecified;
Boolean typeCoercionAtStart = null;
if (Strings.isNonBlank((String)vv_auto)) {
boolean isVariable = input.containsKey(VALUE.getName());
boolean isValue = variableNameSpecified;
if (!isVariable && !isValue) {
isValue = true;
// in future, just do the line above
// ...
// but for now we need to exclude it if it matches a workflow variable because that could be legacy syntax
boolean isDisallowedUndecoratedVariable = false;
try {
Object vvAutoResolved = context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_INPUT, "${" + vv_auto + "}", Object.class);
isDisallowedUndecoratedVariable = true;
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
// otherwise normal, it does not match a variable name, so treat it as the value
}
if (isDisallowedUndecoratedVariable)
throw new IllegalArgumentException("Legacy transform syntax `transform var_name | ...` disallowed (" + vv_auto + "); use '${"+vv_auto+"}' if you don't want to update it or insert the keyword 'variable' if you do");
}
if (isVariable) {
if (isValue) throw new IllegalArgumentException("Legacy transform identified as both value and variable");
varName = (String) vv_auto;
setVariableAtEnd = true;
typeCoercionAtStart = false;
} else {
if (!isValue) throw new IllegalArgumentException("Legacy transform cannot identify as value or variable");
valueExpressionToTransform = vv_auto;
typeCoercionAtStart = !variableNameSpecified;
}
}
if (variableNameSpecified) {
if (varName!=null) throw new IllegalStateException("Variable name specified twice"); //shouldn't happen
varName = variable.name;
setVariableAtEnd = true;
}
if (valueExpressionToTransform==null) {
// value not set from auto; check if specified elsewhere
valueExpressionToTransform = input.get(VALUE.getName());
if (typeCoercionAtStart==null && valueExpressionToTransform!=null) typeCoercionAtStart = !variableNameSpecified;
}
if (valueExpressionToTransform==null) {
if (!variableNameSpecified) throw new IllegalArgumentException("Transform needs a variable or value to transform");
valueExpressionToTransform = "${"+variable.name+"}";
if (typeCoercionAtStart!=null) throw new IllegalStateException("Unclear whether to do type coercion; should be unset"); // shouldn't come here
typeCoercionAtStart = true;
}
if (typeCoercionAtStart==null) {
throw new IllegalStateException("Unclear whether to do type coercion; should be set"); // shouldn't come here
}
if (variable!=null && Strings.isNonBlank(variable.type)) {
// if type is specified with a variable used to start the transform, we coerce to that type at the start and at the end
if (typeCoercionAtStart) transforms.add(0, "type " + variable.type);
if (setVariableAtEnd) transforms.add("type " + variable.type);
}
boolean isResolved = false;
Object v = valueExpressionToTransform;
for (String t: transforms) {
WorkflowTransformWithContext tt = getTransform(context, t);
Boolean req = tt.resolvedValueRequirement();
if (req==null) { /* no requirement */ }
else if (req && !isResolved) {
v = context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_RUNNING, v, TypeToken.of(Object.class));
isResolved = true;
} else if (!req && isResolved) {
throw new IllegalArgumentException("Transform '" + t + "' must be first or follow a resolve_expression transform as it requires unresolved input");
} else {
// requirement matches resolution status, both done, or both not
}
v = tt.apply(v);
Boolean ret = tt.resolvedValueReturned();
if (ret!=null) isResolved = ret;
}
if (!isResolved) {
// in case "resolve" was supplied at the end
v = context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_RUNNING, v, TypeToken.of(Object.class));
isResolved = true;
}
if (setVariableAtEnd) {
if (varName==null) throw new IllegalStateException("Variable name not specified when setting variable"); // shouldn't happen
setWorkflowScratchVariableDotSeparated(context, varName, v);
// easily inferred from workflow vars, now that updates are stored separately
// Object oldValue =
// <above> setWorkflowScratchVariableDotSeparated(context, varName, v);
// context.noteOtherMetadata("Value set", v);
// if (oldValue != null) context.noteOtherMetadata("Previous value", oldValue);
if (context.getOutput()!=null) throw new IllegalStateException("Transform that produces output results cannot be used when setting a variable");
if (STEP_TARGET_NAME_FOR_END.equals(context.next)) throw new IllegalStateException("Return transform cannot be used when setting a variable");
return context.getPreviousStepOutput();
} else {
return v;
}
}
static WorkflowTransformWithContext transformOf(final Function f) {
if (f instanceof WorkflowTransformWithContext) return (WorkflowTransformWithContext) f;
return new WorkflowTransformDefault() {
@Override
public Object apply(Object o) {
return f.apply(o);
}
};
}
WorkflowTransformWithContext getTransform(WorkflowStepInstanceExecutionContext context, String transformDef) {
List<String> transformWords;
// old
// transformWords = Arrays.asList(transformDef.split(" "));
// new, respecting quotes
transformWords = QuotedStringTokenizer.builder().includeQuotes(false).includeDelimiters(false).expectQuotesDelimited(true).failOnOpenQuote(true).build(transformDef).remainderAsList();
String transformType = transformWords.get(0);
Function t = Maybe.ofDisallowingNull(TRANSFORMATIONS.get(transformType)).map(Supplier::get).orNull();
if (t==null) {
RegisteredTypeLoadingContext lc = RegisteredTypeLoadingContexts.withLoader(
RegisteredTypeLoadingContexts.bean(WorkflowTransform.class),
RegisteredTypes.getClassLoadingContext(context.getEntity()));
RegisteredType rt = context.getManagementContext().getTypeRegistry().get(transformType, lc);
if (rt!=null) t = context.getManagementContext().getTypeRegistry().create(rt, lc, WorkflowTransform.class);
}
if (t==null) throw new IllegalStateException("Unknown transform '"+transformType+"'");
WorkflowTransformWithContext ta = transformOf(t);
ta.init(context.getWorkflowExectionContext(), context, transformWords, transformDef);
return ta;
}
private final static Map<String, Supplier<Function>> TRANSFORMATIONS = MutableMap.of();
static {
TRANSFORMATIONS.put("trim", () -> new TransformTrim());
TRANSFORMATIONS.put("merge", () -> new TransformMerge());
TRANSFORMATIONS.put("prepend", () -> new TransformPrependAppend(false));
TRANSFORMATIONS.put("append", () -> new TransformPrependAppend(true));
TRANSFORMATIONS.put("join", () -> new TransformJoin());
TRANSFORMATIONS.put("split", () -> new TransformSplit());
TRANSFORMATIONS.put("slice", () -> new TransformSlice());
TRANSFORMATIONS.put("remove", () -> new TransformRemove());
TRANSFORMATIONS.put("json", () -> new TransformJsonish(true, false, false));
TRANSFORMATIONS.put("yaml", () -> new TransformJsonish(false, true, false));
TRANSFORMATIONS.put("bash", () -> new TransformJsonish(true, false, true));
TRANSFORMATIONS.put("replace", () -> new TransformReplace());
TRANSFORMATIONS.put("wait", () -> new TransformWait());
TRANSFORMATIONS.put("type", () -> new TransformType());
TRANSFORMATIONS.put("first", () -> x -> {
if (x==null) return null;
if (!(x instanceof Iterable)) throw new IllegalArgumentException("Cannot take first of non-list type "+x);
Iterator xi = ((Iterable) x).iterator();
if (xi.hasNext()) return xi.next();
return null;
});
TRANSFORMATIONS.put("last", () -> x -> {
if (x==null) return null;
if (!(x instanceof Iterable)) throw new IllegalArgumentException("Cannot take first of non-list type "+x);
Iterator xi = ((Iterable) x).iterator();
Object last = null;
while (xi.hasNext()) last = xi.next();
return last;
});
TRANSFORMATIONS.put("max", () -> v -> minmax(v, "max", i -> i>0));
TRANSFORMATIONS.put("min", () -> v -> minmax(v, "min", i -> i<0));
TRANSFORMATIONS.put("sum", () -> v -> sum(v, "sum"));
TRANSFORMATIONS.put("average", () -> v -> average(v, "average"));
TRANSFORMATIONS.put("size", () -> v -> size(v, "size"));
TRANSFORMATIONS.put("to_string", () -> v -> Strings.toString(v));
TRANSFORMATIONS.put("to_upper_case", () -> v -> ((String)v).toUpperCase());
TRANSFORMATIONS.put("to_lower_case", () -> v -> ((String)v).toLowerCase());
TRANSFORMATIONS.put("return", () -> new TransformReturn());
TRANSFORMATIONS.put("set", () -> new TransformSetWorkflowVariable());
TRANSFORMATIONS.put("resolve_expression", () -> new TransformResolveExpression());
// 'get' is added downstream when DSL is initialized
//TRANSFORMATIONS.put("get", () -> new TransformGet());
}
public static void registerTransformation(String key, Supplier<Function> xform) {
TRANSFORMATIONS.put(key, xform);
}
static final Object minmax(Object v, String word, Predicate<Integer> test) {
if (v==null) return null;
if (!(v instanceof Iterable)) throw new IllegalArgumentException("Value is not an iterable; cannot take "+word);
Object result = null;
for (Object vi: (Iterable)v) {
if (result==null) result = vi;
else if (!(vi instanceof Comparable)) throw new IllegalArgumentException("Argument is not comparable; cannot take "+word);
else if (test.test( ((Comparable)vi).compareTo(result) )) result = vi;
}
return result;
}
static final Object sum(Object v, String word) {
if (v==null) return null;
if (!(v instanceof Iterable)) throw new IllegalArgumentException("Value is not an iterable; cannot take "+word);
Iterable<?> vi = (Iterable<?>) v;
if (vi.iterator().hasNext() && vi.iterator().next() instanceof Duration) {
Duration result = Duration.ZERO;
for (Object vii: vi) {
result = result.add(TypeCoercions.coerce(vii, Duration.class));
}
return result;
}
double result = 0;
for (Object vii: vi) {
result += asDouble(vii).get();
}
return simplifiedToIntOrLongIfPossible(result);
}
static 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;
}
static Number simplifiedToIntOrLongIfPossible(Number x) {
if (x instanceof Integer) return x;
if (x instanceof Long) {
if (x.longValue() == x.intValue()) return x.intValue();
return x;
}
if (x instanceof Double || x instanceof Float) {
double xd = x.doubleValue();
if (Math.abs(xd - Math.round(xd)) < 0.000000001) {
return simplifiedToIntOrLongIfPossible(Math.round(xd));
}
}
return x;
}
static final Integer size(Object v, String word) {
if (v==null) return null;
if (v instanceof Iterable) return Iterables.size((Iterable<?>) v);
if (v instanceof Map) return ((Map)v).size();
throw new IllegalArgumentException("Argument is not a set or map; cannot compute "+word);
}
static final Object average(Object v, String word) {
if (v==null) return null;
Integer count = size(v, "average");
if (count==null || count==0) throw new IllegalArgumentException("Value is empty; cannot take "+word);
Object sum = sum(v, "average");
if (sum instanceof Duration) {
return Duration.millis(Math.round(asDouble( ((Duration)sum).toMilliseconds() ).get() / count) );
}
return simplifiedToIntOrLongIfPossible(asDouble(sum).get() / count);
}
@Override protected Boolean isDefaultIdempotent() { return true; }
}