This is an automated email from the ASF dual-hosted git repository. heneveld pushed a commit to branch expression-parsing in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git
commit 68597ff34b3a8e720a8eb78d1c94c1ece4ed615c Author: Alex Heneveld <a...@cloudsoft.io> AuthorDate: Wed Feb 7 17:06:43 2024 +0000 refactor how map sensors are updated, consistent between let and set-sensor --- .../steps/appmodel/ClearSensorWorkflowStep.java | 36 ++- .../steps/appmodel/SetConfigWorkflowStep.java | 22 +- .../steps/appmodel/SetSensorWorkflowStep.java | 238 +++++----------- .../steps/variables/SetVariableWorkflowStep.java | 57 +--- .../variables/TransformSetWorkflowVariable.java | 2 +- .../variables/TransformVariableWorkflowStep.java | 27 +- .../workflow/utils/WorkflowSettingItemsUtils.java | 312 +++++++++++++++++++++ .../core/workflow/WorkflowMapAndListTest.java | 243 +++++++++++++--- .../WorkflowNestedAndCustomExtensionTest.java | 3 +- 9 files changed, 651 insertions(+), 289 deletions(-) diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearSensorWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearSensorWorkflowStep.java index 713735b1b8..e04b47b591 100644 --- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearSensorWorkflowStep.java +++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ClearSensorWorkflowStep.java @@ -18,6 +18,10 @@ */ package org.apache.brooklyn.core.workflow.steps.appmodel; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + import com.google.common.collect.Iterables; import com.google.common.reflect.TypeToken; import org.apache.brooklyn.api.entity.Entity; @@ -25,19 +29,12 @@ import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.sensor.Sensors; -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.core.workflow.utils.WorkflowSettingItemsUtils; 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.brooklyn.util.text.Strings; - -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import org.apache.commons.lang3.tuple.Pair; public class ClearSensorWorkflowStep extends WorkflowStepDefinition { @@ -54,27 +51,26 @@ public class ClearSensorWorkflowStep extends WorkflowStepDefinition { protected Object doTaskBody(WorkflowStepInstanceExecutionContext context) { EntityValueToSet sensor = context.getInput(SENSOR); if (sensor==null) throw new IllegalArgumentException("Sensor name is required"); - String sensorNameFull = context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_INPUT, sensor.name, String.class); - if (Strings.isBlank(sensorNameFull)) throw new IllegalArgumentException("Sensor name is required"); - List<Object> sensorNameIndexes = MutableList.of(); - String sensorNameBase = SetSensorWorkflowStep.extractSensorNameBaseAndPopulateIndices(sensorNameFull, sensorNameIndexes); + Pair<String, List<Object>> sensorNameAndIndices = WorkflowSettingItemsUtils.resolveNameAndBracketedIndices(context, sensor.name, false); + if (sensorNameAndIndices==null) throw new IllegalArgumentException("Sensor name is required"); TypeToken<?> type = context.lookupType(sensor.type, () -> TypeToken.of(Object.class)); Entity entity = sensor.entity; if (entity==null) entity = context.getEntity(); - if (sensorNameIndexes.isEmpty()) { - ((EntityInternal) entity).sensors().remove(Sensors.newSensor(Object.class, sensorNameFull)); + // TODO use WorkflowSettingItemsUtils + if (sensorNameAndIndices.getRight().isEmpty()) { + ((EntityInternal) entity).sensors().remove(Sensors.newSensor(Object.class, sensorNameAndIndices.getLeft())); } else { - ((EntityInternal) entity).sensors().modify(Sensors.newSensor(Object.class, sensorNameBase), old -> { + ((EntityInternal) entity).sensors().modify(Sensors.newSensor(Object.class, sensorNameAndIndices.getLeft()), old -> { boolean setLast = false; - Object newTarget = SetSensorWorkflowStep.makeMutable(old, sensorNameIndexes); + Object newTarget = WorkflowSettingItemsUtils.makeMutableOrUnchangedDefaultingToMap(old); Object target = newTarget; - MutableList<Object> indexes = MutableList.copyOf(sensorNameIndexes); + MutableList<Object> indexes = MutableList.copyOf(sensorNameAndIndices.getRight()); while (!indexes.isEmpty()) { Object i = indexes.remove(0); boolean isLast = indexes.isEmpty(); @@ -93,7 +89,7 @@ public class ClearSensorWorkflowStep extends WorkflowStepDefinition { } else { nextTarget = ((Map) target).get(i); if (nextTarget==null) break; - ((Map) target).put(i, SetSensorWorkflowStep.makeMutable(nextTarget, indexes)); + ((Map) target).put(i, WorkflowSettingItemsUtils.makeMutableOrUnchangedDefaultingToMap(nextTarget)); } } else if (target instanceof Iterable && i instanceof Integer) { @@ -121,7 +117,7 @@ public class ClearSensorWorkflowStep extends WorkflowStepDefinition { break; } else { Object t0 = Iterables.get((Iterable) target, ii); - nextTarget = SetSensorWorkflowStep.makeMutable(t0, indexes); + nextTarget = WorkflowSettingItemsUtils.makeMutableOrUnchangedDefaultingToMap(t0); if (t0!=nextTarget) { if (!(target instanceof List)) throw new IllegalStateException("Cannot set numerical position index in a non-list collection (and was not otherwise known as mutable; e.g. use MutableSet): "+target); ((List) target).set(ii, nextTarget); diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetConfigWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetConfigWorkflowStep.java index 958f94adb4..73b1e9fd4c 100644 --- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetConfigWorkflowStep.java +++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetConfigWorkflowStep.java @@ -18,14 +18,16 @@ */ package org.apache.brooklyn.core.workflow.steps.appmodel; +import java.util.List; + import com.google.common.reflect.TypeToken; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; -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.text.Strings; +import org.apache.brooklyn.core.workflow.utils.WorkflowSettingItemsUtils; +import org.apache.commons.lang3.tuple.Pair; public class SetConfigWorkflowStep extends WorkflowStepDefinition { @@ -43,17 +45,19 @@ public class SetConfigWorkflowStep extends WorkflowStepDefinition { protected Object doTaskBody(WorkflowStepInstanceExecutionContext context) { EntityValueToSet config = context.getInput(CONFIG); if (config ==null) throw new IllegalArgumentException("Config key name is required"); - String configName = context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_INPUT, config.name, String.class); - if (Strings.isBlank(configName)) throw new IllegalArgumentException("Config key name is required"); + + Pair<String, List<Object>> nameAndIndices = WorkflowSettingItemsUtils.resolveNameAndBracketedIndices(context, config.name, false); + if (nameAndIndices==null) throw new IllegalArgumentException("Config key name is required"); + // see note on type in SetSensorWorkflowStep TypeToken<?> type = context.lookupType(config.type, () -> TypeToken.of(Object.class)); Object resolvedValue = context.getInput(VALUE.getName(), type); - Entity entity = config.entity; - if (entity==null) entity = context.getEntity(); - Object oldValue = entity.config().set((ConfigKey<Object>) ConfigKeys.newConfigKey(type, configName), resolvedValue); + Entity entity = config.entity!=null ? config.entity : context.getEntity(); - context.noteOtherMetadata("Value set", resolvedValue); - if (oldValue!=null) context.noteOtherMetadata("Previous value", oldValue); + Pair<Object, Object> oldValues = WorkflowSettingItemsUtils.setAtIndex(nameAndIndices, true, (_oldValue) -> resolvedValue, + name -> entity.config().get((ConfigKey<Object>) ConfigKeys.newConfigKey(type, name)), + (name, value) -> entity.config().set((ConfigKey<Object>) ConfigKeys.newConfigKey(type, name), value)); + WorkflowSettingItemsUtils.noteValueSetNestedMetadata(context, nameAndIndices, resolvedValue, oldValues); return context.getPreviousStepOutput(); } diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetSensorWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetSensorWorkflowStep.java index 72d7a11ebb..3b0d512b52 100644 --- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetSensorWorkflowStep.java +++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/SetSensorWorkflowStep.java @@ -18,43 +18,41 @@ */ package org.apache.brooklyn.core.workflow.steps.appmodel; -import com.google.common.collect.Iterables; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import com.google.common.base.Suppliers; import com.google.common.reflect.TypeToken; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; -import org.apache.brooklyn.core.entity.AbstractEntity; +import org.apache.brooklyn.core.entity.AbstractEntity.BasicSensorSupport; import org.apache.brooklyn.core.resolve.jackson.WrappedValue; import org.apache.brooklyn.core.sensor.Sensors; -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.collections.MutableSet; +import org.apache.brooklyn.core.workflow.utils.WorkflowSettingItemsUtils; import org.apache.brooklyn.util.core.predicates.DslPredicates; +import org.apache.brooklyn.util.core.predicates.DslPredicates.DslEntityPredicateDefault; import org.apache.brooklyn.util.guava.Maybe; -import org.apache.brooklyn.util.text.StringEscapes; -import org.apache.brooklyn.util.text.Strings; +import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; - public class SetSensorWorkflowStep extends WorkflowStepDefinition { private static final Logger LOG = LoggerFactory.getLogger(SetSensorWorkflowStep.class); + static final boolean ALWAYS_USE_SENSOR_MODIFY = true; + public static final String SHORTHAND = "[ ${sensor.type} ] ${sensor.name} [ \"=\" ${value...} ]"; public static final ConfigKey<EntityValueToSet> SENSOR = ConfigKeys.newConfigKey(EntityValueToSet.class, "sensor"); public static final ConfigKey<Object> VALUE = ConfigKeys.newConfigKey(Object.class, "value"); - public static final ConfigKey<DslPredicates.DslPredicate> REQUIRE = ConfigKeys.newConfigKey(DslPredicates.DslPredicate.class, "require"); + public static final ConfigKey<DslPredicates.DslPredicate> REQUIRE = ConfigKeys.newConfigKey(DslPredicates.DslPredicate.class, "require", + "Require a condition in order to set the value; if the condition is not satisfied, the sensor is not set"); @Override public void populateFromShorthand(String expression) { @@ -84,173 +82,83 @@ public class SetSensorWorkflowStep extends WorkflowStepDefinition { EntityValueToSet sensor = context.getInput(SENSOR); if (sensor==null) throw new IllegalArgumentException("Sensor name is required"); - String sensorNameFull = context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_INPUT, sensor.name, String.class); - if (Strings.isBlank(sensorNameFull)) throw new IllegalArgumentException("Sensor name is required"); - - List<Object> sensorNameIndexes = MutableList.of(); - String sensorNameBase = extractSensorNameBaseAndPopulateIndices(sensorNameFull, sensorNameIndexes); + Pair<String, List<Object>> sensorNameAndIndices = WorkflowSettingItemsUtils.resolveNameAndBracketedIndices(context, sensor.name, false); + if (sensorNameAndIndices==null) throw new IllegalArgumentException("Sensor name is required"); - TypeToken<?> type = context.lookupType(sensor.type, () -> TypeToken.of(Object.class)); + final TypeToken<?> type = context.lookupType(sensor.type, () -> TypeToken.of(Object.class)); final Entity entity = sensor.entity!=null ? sensor.entity : context.getEntity(); - AttributeSensor<Object> sensorBase = (AttributeSensor<Object>) Sensors.newSensor(type, sensorNameBase); - AtomicReference<Object> resolvedValue = new AtomicReference<>(); - Object oldValue; - Runnable resolve = () -> { + Supplier<Object> resolveOnceValueSupplier = Suppliers.memoize(() -> { // // might need to be careful if defined type is different or more generic than type specified here; // // but that should be fine as XML persistence preserves types // Sensor<?> sd = entity.getEntityType().getSensor(sensorName); // if (!type.isSupertypeOf(sd.getTypeToken())) { // ... // } + return context.getInput(VALUE.getName(), type); + }); - resolvedValue.set(context.getInput(VALUE.getName(), type)); - }; - + AttributeSensor<Object> sensorBase = (AttributeSensor<Object>) Sensors.newSensor(type, sensorNameAndIndices.getLeft()); DslPredicates.DslPredicate require = context.getInput(REQUIRE); - if (require==null && sensorNameIndexes.isEmpty()) { - resolve.run(); - oldValue = entity.sensors().set(sensorBase, resolvedValue.get()); - } else { - oldValue = entity.sensors().modify(sensorBase, oldBase -> { - if (require!=null) { - Object old = oldBase; - MutableList<Object> indexes = MutableList.copyOf(sensorNameIndexes); - while (!indexes.isEmpty()) { - Object i = indexes.remove(0); - if (old == null) break; - if (old instanceof Map) old = ((Map) old).get(i); - else if (old instanceof Iterable && i instanceof Integer) { - int ii = (Integer)i; - int size = Iterables.size((Iterable) old); - if (ii==-1) ii = size-1; - old = (ii<0 || ii>=size) ? null : Iterables.get((Iterable) old, ii); - } else { - throw new IllegalArgumentException("Cannot find argument '" + i + "' in " + old); - } - } - - if (old == null && !((AbstractEntity.BasicSensorSupport) entity.sensors()).contains(sensorBase.getName())) { - DslPredicates.DslEntityPredicateDefault requireTweaked = new DslPredicates.DslEntityPredicateDefault(); - requireTweaked.sensor = sensorNameFull; - requireTweaked.check = WrappedValue.of(require); - if (!requireTweaked.apply(entity)) { - throw new SensorRequirementFailedAbsent("Sensor " + sensorNameFull + " unset or unavailable when there is a non-absent requirement"); - } - } else { - if (!require.apply(old)) { - throw new SensorRequirementFailed("Sensor " + sensorNameFull + " value does not match requirement", old); - } - } - } - - resolve.run(); - - // now set - Object result; - - if (!sensorNameIndexes.isEmpty()) { - result = oldBase; - - // ensure mutable - result = makeMutable(result, sensorNameIndexes); - - Object target = result; - MutableList<Object> indexes = MutableList.copyOf(sensorNameIndexes); - while (!indexes.isEmpty()) { - Object i = indexes.remove(0); - boolean isLast = indexes.isEmpty(); - Object nextTarget; + AtomicReference<Pair<Object, Object>> oldValues = new AtomicReference<>(); + if (!ALWAYS_USE_SENSOR_MODIFY && require == null && sensorNameAndIndices.getRight().isEmpty()) { + // can do simple if not using 'require' and no indices - though there is no reason not to + // and if there is some special value which does an operation on the sensor, it's nice if we do + Pair<Object, Object> oldValuesP = WorkflowSettingItemsUtils.setAtIndex(sensorNameAndIndices, true, + _oldInner -> resolveOnceValueSupplier.get(), + // outer getter + _name -> entity.sensors().get(sensorBase), + // outer setter + (_name, nv) -> entity.sensors().set(sensorBase, nv) + ); + oldValues.set(oldValuesP); - if (target instanceof Map) { - nextTarget = ((Map) target).get(i); - if (nextTarget==null || isLast || !(nextTarget instanceof MutableMap)) { - // ensure mutable - nextTarget = isLast ? resolvedValue.get() : makeMutable(nextTarget, indexes); - ((Map) target).put(i, nextTarget); - } - - } else if (target instanceof Iterable && i instanceof Integer) { - int ii = (Integer)i; - int size = Iterables.size((Iterable) target); - if (ii==-1) ii = size-1; - boolean outOfBounds = ii < 0 || ii >= size; - nextTarget = outOfBounds ? null : Iterables.get((Iterable) target, ii); - - if (nextTarget==null || isLast || (!(nextTarget instanceof MutableMap) && !(nextTarget instanceof MutableSet) && !(nextTarget instanceof MutableList))) { - nextTarget = isLast ? resolvedValue.get() : makeMutable(nextTarget, indexes); - if (outOfBounds) { - ((Collection) target).add(nextTarget); - } else { - if (!(target instanceof List)) throw new IllegalStateException("Cannot set numerical position index in a non-list collection (and was not otherwise known as mutable; e.g. use MutableSet): "+target); - ((List) target).set(ii, nextTarget); + } else { + // with 'require' we contractually need to do it all in the modify loop, + // performing the check (on the inner value if there are indices); + // even without, there might be concurrent blocks updating the same map/list if indexes are supplied + + // see testBeefySensorRequireForAtomicityAndConfigCountsMap for a thorough test + // (look for references to SetSensorWorkflowStep.REQUIRE) + + entity.sensors().modify(sensorBase, + (oldOuterX) -> { + AtomicReference<Object> newValue = new AtomicReference<>(); + Pair<Object, Object> oldValuesP = WorkflowSettingItemsUtils.setAtIndex(sensorNameAndIndices, true, + oldInner -> { + if (require!=null) { + if (oldInner == null && !((BasicSensorSupport) entity.sensors()).contains(sensorBase.getName())) { + DslEntityPredicateDefault requireTweaked = new DslEntityPredicateDefault(); + requireTweaked.sensor = sensorNameAndIndices.getLeft(); + requireTweaked.check = WrappedValue.of(require); + if (!requireTweaked.apply(entity)) { + throw new SensorRequirementFailedAbsent("Sensor " + sensorNameAndIndices.getLeft() + " unset or unavailable when there is a non-absent requirement"); + } + } else { + if (!require.apply(oldInner)) { + throw new SensorRequirementFailed("Sensor " + sensorNameAndIndices.getLeft() + " value does not match requirement", oldInner); + } + } + } + + return resolveOnceValueSupplier.get(); + }, + // outer getter + _name -> oldOuterX, + // outer setter + (_name, nv) -> { + newValue.set(nv); + return oldOuterX; } - } - - } else { - throw new IllegalArgumentException("Cannot find argument '" + i + "' in " + target); - } - target = nextTarget; - } - } else { - result = resolvedValue.get(); - } - - return Maybe.of(result); - }); + ); + oldValues.set(oldValuesP); + return Maybe.of(newValue.get()); + }); } - context.noteOtherMetadata("Value set", resolvedValue.get()); - if (oldValue!=null) context.noteOtherMetadata("Previous value", oldValue); - + WorkflowSettingItemsUtils.noteValueSetNestedMetadata(context, sensorNameAndIndices, resolveOnceValueSupplier.get(), oldValues.get()); return context.getPreviousStepOutput(); } - static String extractSensorNameBaseAndPopulateIndices(String sensorNameFull, List<Object> sensorNameIndexes) { - int bracket = sensorNameFull.indexOf('['); - String sensorNameBase; - if (bracket > 0) { - sensorNameBase = sensorNameFull.substring(0, bracket); - String brackets = sensorNameFull.substring(bracket); - while (!brackets.isEmpty()) { - if (!brackets.startsWith("[")) throw new IllegalArgumentException("Expected '[' for sensor index"); - brackets = brackets.substring(1).trim(); - int bi = brackets.indexOf(']'); - if (bi<0) throw new IllegalArgumentException("Mismatched ']' in sensor name"); - String bs = brackets.substring(0, bi).trim(); - if (bs.startsWith("\"") || bs.startsWith("\'")) bs = StringEscapes.BashStringEscapes.unwrapBashQuotesAndEscapes(bs); - else if (bs.matches("-? *[0-9]+")) { - sensorNameIndexes.add(Integer.parseInt(bs)); - bs = null; - } - if (bs!=null) { - sensorNameIndexes.add(bs); - } - brackets = brackets.substring(bi+1).trim(); - } - } else if (bracket == 0) { - throw new IllegalArgumentException("Sensor name cannot start with '['"); - } else { - sensorNameBase = sensorNameFull; - } - return sensorNameBase; - } - - static Object makeMutable(Object x, List<Object> indexesRemaining) { - if (x==null) { - // look ahead to see if it should be a list - if (indexesRemaining!=null && !indexesRemaining.isEmpty() && indexesRemaining.get(0) instanceof Integer) return MutableList.of(); - return MutableMap.of(); - } - - if (x instanceof Set) { - if (!(x instanceof MutableSet)) return MutableSet.copyOf((Set) x); - return x; - } - if (x instanceof Map && !(x instanceof MutableMap)) return MutableMap.copyOf((Map) x); - else if (x instanceof Iterable && !(x instanceof MutableList)) return MutableList.copyOf((Iterable) x); - return x; - } - @Override protected Boolean isDefaultIdempotent() { return true; } } diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/SetVariableWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/SetVariableWorkflowStep.java index f363261e1f..2aad2bbeeb 100644 --- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/SetVariableWorkflowStep.java +++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/SetVariableWorkflowStep.java @@ -27,6 +27,7 @@ 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.core.workflow.utils.WorkflowSettingItemsUtils; import org.apache.brooklyn.util.collections.*; import org.apache.brooklyn.util.core.flags.TypeCoercions; import org.apache.brooklyn.util.core.predicates.DslPredicates; @@ -38,6 +39,7 @@ 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.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,52 +106,23 @@ public class SetVariableWorkflowStep extends WorkflowStepDefinition { 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); + setWorkflowScratchVariableDotSeparated(context, name, resolvedValue, + // metadata is easily inferred from workflow vars, and they can use a lot of persistence space, so skip + false); 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); + static Pair<Object,Object> setWorkflowScratchVariableDotSeparated(WorkflowStepInstanceExecutionContext context, String nameFull, Object resolvedValue, boolean noteMetadata) { + Pair<String, List<Object>> np = WorkflowSettingItemsUtils.extractNameAndDotOrBracketedIndices(nameFull); + + Pair<Object, Object> result = WorkflowSettingItemsUtils.setAtIndex(np, true, (_prevValue) -> resolvedValue, + (k) -> context.getWorkflowExectionContext().getWorkflowScratchVariables().get(k), + (k, v) -> context.getWorkflowExectionContext().updateWorkflowScratchVariable(k, v)); + if (noteMetadata) { + // not usually done, see call sites of this method + WorkflowSettingItemsUtils.noteValueSetNestedMetadata(context, np, resolvedValue, result); } - return oldValue; + return result; } private enum LetMergeMode { NONE, SHALLOW, DEEP } diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSetWorkflowVariable.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSetWorkflowVariable.java index 73cc122ec2..10d7d80ab2 100644 --- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSetWorkflowVariable.java +++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSetWorkflowVariable.java @@ -39,7 +39,7 @@ public class TransformSetWorkflowVariable extends WorkflowTransformDefault { @Override public Object apply(Object v) { String nameToSet = context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_RUNNING, name, String.class); - SetVariableWorkflowStep.setWorkflowScratchVariableDotSeparated(stepContext, nameToSet, v); + SetVariableWorkflowStep.setWorkflowScratchVariableDotSeparated(stepContext, nameToSet, v, false); return v; } diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformVariableWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformVariableWorkflowStep.java index 7f7c2f8254..09907ffe38 100644 --- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformVariableWorkflowStep.java +++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformVariableWorkflowStep.java @@ -18,6 +18,16 @@ */ package org.apache.brooklyn.core.workflow.steps.variables; +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 javax.annotation.Nullable; + import com.google.common.collect.Iterables; import com.google.common.reflect.TypeToken; import org.apache.brooklyn.api.mgmt.ManagementContext; @@ -42,16 +52,6 @@ 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; @@ -218,12 +218,7 @@ public class TransformVariableWorkflowStep extends WorkflowStepDefinition { 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); + setWorkflowScratchVariableDotSeparated(context, varName, v, false); 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"); diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/utils/WorkflowSettingItemsUtils.java b/core/src/main/java/org/apache/brooklyn/core/workflow/utils/WorkflowSettingItemsUtils.java new file mode 100644 index 0000000000..10095c660e --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/workflow/utils/WorkflowSettingItemsUtils.java @@ -0,0 +1,312 @@ +/* + * 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); + } + } + } + +} diff --git a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowMapAndListTest.java b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowMapAndListTest.java index 0d9616c1d9..c42bd46223 100644 --- a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowMapAndListTest.java +++ b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowMapAndListTest.java @@ -18,20 +18,31 @@ */ package org.apache.brooklyn.core.workflow; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + import com.google.common.collect.ImmutableMap; import org.apache.brooklyn.api.entity.EntityLocal; import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.entity.Dumper; +import org.apache.brooklyn.core.entity.EntityAsserts; +import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.core.test.BrooklynMgmtUnitTestSupport; +import org.apache.brooklyn.core.workflow.steps.appmodel.SetSensorWorkflowStep; import org.apache.brooklyn.entity.stock.BasicApplication; import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.config.ConfigBag; -import org.testng.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testng.annotations.Test; -import java.util.List; +public class WorkflowMapAndListTest extends BrooklynMgmtUnitTestSupport { -public class WorkflowMapAndListTest extends BrooklynMgmtUnitTestSupport { + private static final Logger log = LoggerFactory.getLogger(WorkflowMapAndListTest.class); private BasicApplication app; @@ -49,7 +60,7 @@ public class WorkflowMapAndListTest extends BrooklynMgmtUnitTestSupport { } @Test - public void testMapDirect() { + public void testSetWorkflowInMapWithDot() { Object result = runSteps(MutableList.of( "let map myMap = {}", "let myMap.a = 1", @@ -57,62 +68,224 @@ public class WorkflowMapAndListTest extends BrooklynMgmtUnitTestSupport { )); Asserts.assertEquals(result, "1"); } + @Test + public void testSetWorkflowInMapWithBrackets() { + Object result = runSteps(MutableList.of( + "let map myMap = {}", + "let myMap['a'] = 1", + "return ${myMap.a}" + )); + Asserts.assertEquals(result, "1"); + } + @Test + public void testSetWorkflowInListWithType() { + Object result = runSteps(MutableList.of( + "let list myList = []", + "let int myList[0] = 1", + "return ${myList[0]}" + )); + Asserts.assertEquals(result, 1); + } + + @Test + public void testSetWorkflowCreateMapAndList() { + Object result = runSteps(MutableList.of( + "let myMap['a'] = 1", + "return ${myMap.a}" + )); + Asserts.assertEquals(result, "1"); + + result = runSteps(MutableList.of( + "let int myList[0] = 1", + "return ${myList}" + )); + Asserts.assertEquals(result, Arrays.asList(1)); + } + + @Test + public void testBasicSensorAndConfig() { + runSteps(MutableList.of( + "set-config my_config['a'] = 1", + "set-sensor int my_sensor['a'] = 1" + )); + EntityAsserts.assertConfigEquals(app, ConfigKeys.newConfigKey(Object.class, "my_config"), MutableMap.of("a", "1")); + EntityAsserts.assertAttributeEquals(app, Sensors.newSensor(Object.class, "my_sensor"), MutableMap.of("a", 1)); + } + + @Test + public void testMapBracketsMore() { + Object result = runSteps(MutableList.of( + "set-sensor service.problems['latency'] = \"too slow\"", + "let x = ${entity.sensor['service.problems']}", + "return ${x}" + )); + Asserts.assertEquals(result, ImmutableMap.of("latency", "too slow")); + + result = runSteps(MutableList.of( + "let map myMap = {}", + "let myMap['a'] = 1", + "return ${myMap.a}" + )); + Asserts.assertEquals(result, "1"); + } @Test - public void testSetSensorMap() { + public void testMapBracketsUnquotedSyntaxLegacy() { + // this will warn but will work, for legacy compatibility reasons + Object result = runSteps(MutableList.of( "set-sensor service.problems[latency] = \"too slow\"", "let x = ${entity.sensor['service.problems']}", "return ${x}" )); Asserts.assertEquals(result, ImmutableMap.of("latency", "too slow")); + + result = runSteps(MutableList.of( + "let map myMap = {}", + "let myMap[a] = 1", + "return ${myMap.a}" + )); + Asserts.assertEquals(result, "1"); } @Test - public void testListIndex() { + public void testMoreListByIndexInsertionCreationAndErrors() { Object result = runSteps(MutableList.of( "let list mylist = [1, 2, 3]", "let mylist[1] = 4", "return ${mylist[1]}" )); Asserts.assertEquals(result, "4"); + + // 0 allowed to create (as does -1, further below) + result = runSteps(MutableList.of( + "let mylist[0] = 4", + "return ${mylist[0]}" + )); + Asserts.assertEquals(result, "4"); + + // other numbers not allowed to create + Asserts.assertFailsWith(() -> runSteps(MutableList.of( + "let mylist[123] = 4" + )), Asserts.expectedFailureContainsIgnoreCase("cannot set index", "123", "mylist", "undefined")); + + // -1 puts at end + result = runSteps(MutableList.of( + "let list mylist = [1, 2, 3]", + "let mylist[-1] = 4", + "return ${mylist[3]}" + )); + Asserts.assertEquals(result, "4"); + + // also works if we insert + result = runSteps(MutableList.of( + "let list mylist = [1, 2, 3]", + "let mylist[-1]['a'][-1] = 4", + "return ${mylist[3]['a'][0]}" + )); + Asserts.assertEquals(result, "4"); + // as does empty string + result = runSteps(MutableList.of( + "let list mylist = [1, 2, 3]", + "let mylist[]['a'][] = 4", + "return ${mylist[3]['a'][0]}" + )); + Asserts.assertEquals(result, "4"); + + // note: getting -1 is not supported + Asserts.assertFailsWith(() -> runSteps(MutableList.of( + "let list mylist = [1,2,3]", + "return ${mylist[-1]}" + )), Asserts.expectedFailureContainsIgnoreCase("invalid", "reference", "-1", "mylist")); } @Test public void testUndefinedList() { - Object result = null; - try { - result = runSteps(MutableList.of( - "let list mylist = [1, 2, 3]", - "let anotherList[1] = 4", - "return ${anotherList[1]}" - )); - } catch (Exception e) { - // We can't use expectedExceptionsMessageRegExp as the error message is in the `Cause` exception - if (e.getCause() == null || !e.getCause().getMessage().contains("Cannot set anotherList[1] because anotherList is unset")) { - Assert.fail("Expected cause exception to contain 'Cannot set anotherList[1] because anotherList is unset'"); - } - return; - } - Assert.fail("Expected IllegalArgumentException"); + Asserts.assertFailsWith(() -> runSteps(MutableList.of( + "let anotherList[123] = 4" + )), + e -> Asserts.expectedFailureContainsIgnoreCase(e.getCause(), + "cannot", "index", "anotherList", "123", "undefined")); } @Test public void testInvalidListSpecifier() { - Object result = null; - try { - result = runSteps(MutableList.of( - "let list mylist = [1, 2, 3]", - "let mylist[1 = 4", - "return ${mylist[1]}" - )); - } catch (Exception e) { - // We can't use expectedExceptionsMessageRegExp as the error message is in the `Cause` exception - if (e.getCause() == null || !e.getCause().getMessage().contains("Invalid list index specifier mylist[1")) { - Assert.fail("Expected cause exception to contain 'Invalid list index specifier mylist[1'"); - } - return; + Asserts.assertFailsWith(() -> runSteps(MutableList.of( + "let list mylizt = [1, 2, 3]", + "let mylizt['abc'] = 4", + "return ${mylizt[1]}" + )), + e -> Asserts.expectedFailureContainsIgnoreCase(e.getCause(), "cannot", "mylizt", "abc", "list")); + } + + @Test(groups = "Integration", invocationCount = 20) + public void testBeefySensorRequireForAtomicityAndConfigCountsMapManyTimes() { + testBeefySensorRequireForAtomicityAndConfigCountsMap(); + } + + @Test + public void testBeefySensorRequireForAtomicityAndConfigCountsMap() { + runSteps(MutableList.of( + "set-sensor sum['total'] = 0", //needed for the 'last' check + "set-sensor map counts = {}", //needed for the 'last' check + MutableMap.of( + "step", "foreach n in 1..20", + "concurrency", 10, + "steps", MutableList.of( + "let last = ${entity.sensor['sum']['total']}", + + // keep counts using sensors and config - observe config might get mismatches but no errors, + // and sensors update perfectly atomically + + "let ck = count_${n}", + "let last_count = ${entity.sensor.counts[ck]} ?? 0", + "let count = ${last_count} + 1", + // races in setting config known; we might miss some here (see check) + "set-config counts['${ck}'] = ${count}", + // setting sensors however will be mutexed on the sensor (assuming it exists or is defined) + "set-sensor counts['${ck}'] = ${count}", + // append to a list + "set-config ${ck}[] = ${count}", + + "let next = ${last} + ${n}", + + "log trying to set ${n} to ${next}, attempt ${count}", + // also check require with retries + MutableMap.of( + "step", "set-sensor sum['total']", + // reference so we can easily find this test + SetSensorWorkflowStep.REQUIRE.getName(), "${last}", + "value", "${next}", + "on-error", "retry from start" // if condition not met, will replay + ), + "log succeeded setting ${n} to ${next}, attempt ${count}" + ) + ))); + + Dumper.dumpInfo(app); + EntityAsserts.assertAttributeEquals(app, Sensors.newSensor(Object.class, "sum"), MutableMap.of("total", 210)); + + Map counts; + counts = app.sensors().get(Sensors.newSensor(Map.class, "counts")); + Asserts.assertEquals(counts.size(), 20); + Asserts.assertThat(counts.get("count_15"), x -> ((Integer) x) >= 1); + + counts = app.config().get(ConfigKeys.newConfigKey(Map.class, "counts")); + Asserts.assertThat(counts.size(), x -> x >= 15); // we don't guarantee concurrency on config map writes, so might lose a few + if (counts.size()==20) { + log.warn("ODD!!! config for counts updated perfectly"); // doesn't usually happen, unless slow machine + } + int nn = 0; + for (int n=1; n<=20; n++) { + List count_n = app.config().get(ConfigKeys.newConfigKey(List.class, "count_"+n)); + Asserts.assertThat(count_n.size(), x -> x >= 1); + for (int i = 0; i < count_n.size(); i++) + Asserts.assertEquals(count_n.get(i), i + 1); + nn += count_n.size(); + } + if (nn<=20) { + log.warn("ODD!!! no retries"); // per odd comment above } - Assert.fail("Expected IllegalArgumentException"); } + } diff --git a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowNestedAndCustomExtensionTest.java b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowNestedAndCustomExtensionTest.java index 9df6e26cc7..36667e971d 100644 --- a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowNestedAndCustomExtensionTest.java +++ b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowNestedAndCustomExtensionTest.java @@ -37,6 +37,7 @@ import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; import org.apache.brooklyn.core.test.entity.TestApplication; import org.apache.brooklyn.core.test.entity.TestEntity; import org.apache.brooklyn.core.typereg.BasicTypeImplementationPlan; +import org.apache.brooklyn.core.workflow.steps.appmodel.SetSensorWorkflowStep; import org.apache.brooklyn.core.workflow.steps.flow.LogWorkflowStep; import org.apache.brooklyn.core.workflow.store.WorkflowRetentionAndExpiration; import org.apache.brooklyn.core.workflow.store.WorkflowStatePersistenceViaSensors; @@ -424,7 +425,7 @@ public class WorkflowNestedAndCustomExtensionTest extends RebindTestFixture<Test " - let count = ${entity.parent.sensor.count}", " - let inc = ${count} + 1", " - step: set-sensor count = ${inc}", - " require: ${count}", + " "+ SetSensorWorkflowStep.REQUIRE.getName()+": ${count}", " sensor:", " entity: ${entity.parent}", " on-error:",