refactor how map sensors are updated, consistent between let and set-sensor
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 713735b..e04b47b 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.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 @@
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 @@
} 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 @@
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 958f94a..73b1e9f 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 @@
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 72d7a11..3b0d512 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 @@
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");
+ Pair<String, List<Object>> sensorNameAndIndices = WorkflowSettingItemsUtils.resolveNameAndBracketedIndices(context, sensor.name, false);
+ if (sensorNameAndIndices==null) throw new IllegalArgumentException("Sensor name is required");
- List<Object> sensorNameIndexes = MutableList.of();
- String sensorNameBase = extractSensorNameBaseAndPopulateIndices(sensorNameFull, sensorNameIndexes);
-
- 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());
+ 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);
+
} 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);
- }
- }
+ // 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
- 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);
- }
- }
- }
+ // see testBeefySensorRequireForAtomicityAndConfigCountsMap for a thorough test
+ // (look for references to SetSensorWorkflowStep.REQUIRE)
- resolve.run();
+ 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);
+ }
+ }
+ }
- // 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;
-
- 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);
+ 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 f363261..2aad2bb 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.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.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 @@
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"));
+ static Pair<Object,Object> setWorkflowScratchVariableDotSeparated(WorkflowStepInstanceExecutionContext context, String nameFull, Object resolvedValue, boolean noteMetadata) {
+ Pair<String, List<Object>> np = WorkflowSettingItemsUtils.extractNameAndDotOrBracketedIndices(nameFull);
- 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);
+ 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 73cc122..10d7d80 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 @@
@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 7f7c2f8..09907ff 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.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 @@
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 0000000..10095c6
--- /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 0d9616c..c42bd46 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 @@
}
@Test
- public void testMapDirect() {
+ public void testSetWorkflowInMapWithDot() {
Object result = runSteps(MutableList.of(
"let map myMap = {}",
"let myMap.a = 1",
@@ -57,62 +68,224 @@
));
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 testSetSensorMap() {
+ 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 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;
- }
- Assert.fail("Expected IllegalArgumentException");
+ 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
+ }
+ }
+
}
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 9df6e26..36667e9 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.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 @@
" - 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:",