blob: 10095c660e398a6925cce561892424a996c02b9a [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.utils;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.brooklyn.core.workflow.WorkflowExpressionResolution.WorkflowExpressionStage;
import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
import org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.CharactersCollectingParseMode;
import org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.ParseNode;
import org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.ParseNodeOrValue;
import org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.ParseValue;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** convenience class for the complexities of setting values, especially nested values */
public class WorkflowSettingItemsUtils {
/**
* variable indices are not obvious -- currently you should write x['${index}'] for maps and x[${index}] for lists.
* but the quotes on the former might suggest it should be a literal value.
* if you want a literal (unresolved) key of the form ${...}
* you have to put that into another variable, or use $brooklyn:literal.
*
* furthermore x[index] is also ambiguous -- probably that should be the way to imply using index as a variable.
* but currently (and historially) we have allowed it as a string literal "index".
* we now warn on that usage, but we might want to switch it, and respect its type as a string or integer (for map or list).
*
* this constant can be used to find usages.
*/
public static final boolean TODO_ALLOW_VARIABLES_IN_INDICES = false;
public static final String VALUE_SET = "Value set";
public static final String PREVIOUS_VALUE = "Previous value";
public static final String PREVIOUS_VALUE_OUTER = "Previous value (outer)";
private static final Logger log = LoggerFactory.getLogger(WorkflowSettingItemsUtils.class);
public static Pair<String, List<Object>> resolveNameAndBracketedIndices(WorkflowStepInstanceExecutionContext context, String expression, boolean treatDotAsSubkeySeparator) {
if (TODO_ALLOW_VARIABLES_IN_INDICES) {
// this should be a more sophisicated resolution, using ExpressionParser
// values before a bracket should be taken as literals, interpolated expressions resolved, maybe quotes unquoted without resolving;
// and more importantly in the brackets unquoted values should be taken as expressions and resolved;
// not sure what to do for quotes, there is an argument to allow "count_${n}" but also to a quote as a literal.
throw new UnsupportedOperationException();
}
String resolved = context.resolve(WorkflowExpressionStage.STEP_INPUT, expression, String.class);
return expressionParseNameAndIndices(resolved, treatDotAsSubkeySeparator);
}
public static Pair<String, List<Object>> extractNameAndDotOrBracketedIndices(String nameFull) {
if (TODO_ALLOW_VARIABLES_IN_INDICES) {
// callers to this need to be updated
throw new UnsupportedOperationException();
}
return expressionParseNameAndIndices(nameFull, true);
}
public static Pair<String, List<Object>> expressionParseNameAndIndices(String nameFull, boolean treatDotAsSubkeySeparator) {
CharactersCollectingParseMode DOT = new CharactersCollectingParseMode("dot", '.');
ExpressionParserImpl ep = ExpressionParser
.newDefaultAllowingUnquotedLiteralValues()
.includeGroupingBracketsAtUsualPlaces(ExpressionParser.SQUARE_BRACKET);
if (treatDotAsSubkeySeparator) ep.includeAllowedTopLevelTransition(DOT);
ParseNode p = ep.parse(nameFull).get();
Iterator<ParseNodeOrValue> contents = p.getContents().iterator();
if (!contents.hasNext()) throw new IllegalArgumentException("Initial identifier is required");
ParseNodeOrValue nameBaseC = contents.next();
if (nameBaseC.isParseNodeMode(ExpressionParser.INTERPOLATED, ExpressionParser.SQUARE_BRACKET))
throw new IllegalArgumentException("Initial part of identifier cannot be an expression or reference");
String nameBase = ExpressionParser.getUnquoted(nameBaseC).trim();
List<Object> indices = MutableList.of();
while (contents.hasNext()) {
ParseNodeOrValue t = contents.next();
if (t.isParseNodeMode(DOT)) {
if (!contents.hasNext()) throw new IllegalArgumentException("Cannot end with a dot");
ParseNodeOrValue next = contents.next();
if (next.isParseNodeMode(ExpressionParser.SQUARE_BRACKET)) {
// continue to next block; allow foo.['x'].[adfsads]
t = next;
} else if (next.isParseNodeMode(ParseValue.MODE)) {
// only a simple value is allowed
indices.add(((ParseValue)next).getContents());
t = null;
} else {
throw new IllegalArgumentException("Cannot contain this type of object: "+next);
}
}
if (t!=null && t.isParseNodeMode(ExpressionParser.SQUARE_BRACKET)) {
List<ParseNodeOrValue> nest = ExpressionParser.trimWhitespace( ((ParseNode) t).getContents() );
Object index;
if (nest.size()>1) throw new IllegalArgumentException("Bracketed expression must contain a single string or number");
if (nest.isEmpty()) index = "";
else {
ParseNodeOrValue n = nest.get(0);
if (n instanceof ParseValue) {
String v = ((ParseValue) n).getContents().trim();
index = asInteger(v).asType(Object.class).or(() -> {
if (TODO_ALLOW_VARIABLES_IN_INDICES) {
// resolve?
throw new UnsupportedOperationException();
} else {
log.warn("Index to " + nameFull + " should be quoted; allowing unquoted for legacy compatibility");
}
return v;
});
} else if (n.isParseNodeMode(ExpressionParser.SINGLE_QUOTE, ExpressionParser.DOUBLE_QUOTE)) {
index = ExpressionParser.getUnquoted(n);
} else {
throw new IllegalArgumentException("Cannot contain this type of object bracketed: " + n);
}
}
indices.add(index);
}
}
return Pair.of(nameBase, indices);
}
public static Maybe<Integer> asInteger(Object x) {
if (x instanceof Integer) return Maybe.of((Integer)x);
if (x instanceof String) {
if (((String)x).matches("-? *[0-9]+")) return Maybe.of(Integer.parseInt((String)x));
}
return Maybe.absent("Cannot make an integer out of: "+x);
}
public static <T> Maybe<T> ensureMutable(T x) {
return makeMutable(x, false);
}
public static <T> Maybe<T> makeMutableCopy(T x) {
return makeMutable(x, true);
}
private static <T> Maybe<T> makeMutable(T x, boolean alwaysCopyEvenIfMutable) {
Object result = makeMutable(x, alwaysCopyEvenIfMutable, () -> Maybe.absent("Cannot make a mutable object out of null"), (y) -> Maybe.absent("Cannot make a mutable object out of " + y.getClass()));
if (result instanceof Maybe) return (Maybe)result;
return Maybe.of((T)result);
}
public static Object makeMutableOrUnchangedDefaultingToMap(Object x) {
return makeMutable(x, false, () -> MutableMap.of(), v -> v);
}
public static Maybe<Object> makeMutableOrUnchangedForIndex(Object x, boolean alwaysCopyEvenIfMutable, Object index) {
return Maybe.ofDisallowingNull(makeMutable(x, alwaysCopyEvenIfMutable, () -> {
// number or empty string means list
if (index instanceof Integer || "".equals(index)) return MutableList.of();
// string is a map
if (index instanceof String) return MutableMap.of();
// other things not supported
return null;
}, v -> v));
}
public static Object makeMutable(@Nullable Object x, boolean alwaysCopyEvenIfMutable, @Nonnull Supplier<Object> ifNull, @Nonnull Function<Object,Object> ifNotIterableOrMap) {
if (x==null) {
return ifNull.get();
}
if (x instanceof Set) return (!alwaysCopyEvenIfMutable && x instanceof MutableSet) ? x : MutableSet.copyOf((Set) x);
if (x instanceof Map) return (!alwaysCopyEvenIfMutable && x instanceof MutableMap) ? x : MutableMap.copyOf((Map) x);
if (x instanceof Iterable) return (!alwaysCopyEvenIfMutable && x instanceof MutableList) ? x : MutableList.copyOf((Iterable) x);
return ifNotIterableOrMap.apply(x);
}
/** returns pair containing outermost updated object and innermost updated object.
* will be the same if there are no indices. */
public static Pair<Object,Object> setAtIndex(Pair<String,List<Object>> nameAndIndices, boolean allowToCreateIntermediate, Function<Object,Object> valueModifierOrSupplier, Function<String, Object> getter0, BiFunction<String, Object, Object> setter0) {
String name = nameAndIndices.getLeft();
List<Object> indices = nameAndIndices.getRight();
Object key = name;
List<Object> oldValuesReplacedOutermostFirst = MutableList.of();
if (indices!=null && !indices.isEmpty()) {
if ("output".equals(name))
throw new IllegalArgumentException("It is not permitted to set a subfield of the output"); // catch common error
}
String path = "";
Function<Object, Object> getter = (Function) getter0;
Function<Object,Consumer<Object>> setterCurried = k -> v -> {
oldValuesReplacedOutermostFirst.add(0, setter0.apply((String)k, v));
};
Consumer<Object> setter = null;
Object last = null;
for (Object index : MutableList.<Object>of(name).appendAll(indices)) {
path += (path.length()>0 ? "/" : "") + index;
setter = setterCurried.apply(index);
final String pathF = path;
final Consumer<Object> prevSetter = setter;
final Object next = getter.apply(index);
last = next;
if (next == null) {
if (!allowToCreateIntermediate) {
throw new IllegalArgumentException("Cannot set index '" + index + "' at '" + pathF + "' because that is undefined");
} else {
// create
getter = k -> null;
setterCurried = k -> v -> {
Object target = makeMutableOrUnchangedForIndex(null, true, k).orThrow(
() -> new IllegalArgumentException("Cannot set index '" + k + "' at '" + pathF + "' because that is undefined and key type unknown"));
if (k instanceof Integer && (((Integer)k)<-1 || ((Integer)k)>0)) {
throw new IllegalArgumentException("Cannot set index '" + k + "' at '" + pathF + "' because that is undefined and key type out of range");
}
Object oldV = target instanceof List ? ((List)target).add(v) : ((Map)target).put(k, v);
oldValuesReplacedOutermostFirst.add(0, oldV);
prevSetter.accept(target);
};
}
} else if (next instanceof Map) {
getter = ((Map)next)::get;
setterCurried = k -> v -> {
Map target = makeMutableCopy((Map)next).get();
Object oldV = target.put(k, v);
// we could be stricter and block this, in case they thought it was a list?
// if (oldV==null) {
// if (!(k instanceof String))
// throw new IllegalArgumentException("Cannot set non-string index '" + k + "' in map at '" + pathF + "' unless it is replacing (map insertion only supported for string keys)");
// }
oldValuesReplacedOutermostFirst.add(0, oldV);
prevSetter.accept(target);
};
} else if (next instanceof List) {
getter = k -> {
k = asInteger(k).asType(Object.class).or(k);
if ("".equals(k)) return null; // empty string means to append
if (!(k instanceof Integer))
throw new IllegalArgumentException("Cannot get index '" + k + "' at '" + pathF + "' because that is a list");
Integer kn = (Integer) k;
if (kn==-1 || kn==((List)next).size()) return null; // -1 or N means to append
return ((List) next).get((Integer) k);
};
setterCurried = k -> v -> {
List target = makeMutableCopy((List)next).get();
Integer kn = asInteger(k).or(() -> {
if ("".equals(k)) return -1;
throw new IllegalArgumentException("Cannot set index '" + k + "' at '" + pathF + "' because that is a list");
});
// -1 or size appends, or empty string
// (might be nice for negative numbers to reference from the end also -
// but the freemarker getter doesn't support that, so it would cause an odd asymmetry;
// however use of the empty string, or size, to add gives easy ways to add that don't need this)
oldValuesReplacedOutermostFirst.add(0, kn==-1 || kn==target.size() ? target.add(v) : target.set(kn, v));
prevSetter.accept(target);
};
} else {
getter = k -> {
throw new IllegalArgumentException("Cannot set sub-index at '" + k + "' at '" + pathF +"' because that is a " + next.getClass());
};
setterCurried = null;
}
}
setter.accept(valueModifierOrSupplier.apply(last));
return Pair.of(oldValuesReplacedOutermostFirst.get(0), oldValuesReplacedOutermostFirst.get(oldValuesReplacedOutermostFirst.size()-1));
}
public static void noteValueSetMetadata(WorkflowStepInstanceExecutionContext context, Object newValue, Object oldValue) {
context.noteOtherMetadata(WorkflowSettingItemsUtils.VALUE_SET, newValue);
if (oldValue!=null) {
context.noteOtherMetadata(WorkflowSettingItemsUtils.PREVIOUS_VALUE, oldValue);
}
}
public static void noteValueSetNestedMetadata(WorkflowStepInstanceExecutionContext context, Pair<String, List<Object>> nameAndIndices, Object newNestedValue, Pair<Object, Object> oldOuterAndInnerValues) {
noteValueSetMetadata(context, newNestedValue, oldOuterAndInnerValues.getRight());
if (!nameAndIndices.getRight().isEmpty()) {
Object oldValueOuter = oldOuterAndInnerValues.getLeft();
if (oldValueOuter != null) {
context.noteOtherMetadata(WorkflowSettingItemsUtils.PREVIOUS_VALUE_OUTER, oldValueOuter);
}
}
}
}