This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit a0a5266c42bbd6a268c9f80a93ae5ab838a3a673
Author: Alex Heneveld <[email protected]>
AuthorDate: Tue Dec 6 14:32:12 2022 +0000

    support map_sensor['key'] syntax for set-sensor and clear-sensor
    
    and use for service.problems map in test
---
 .../steps/appmodel/ClearSensorWorkflowStep.java    |  96 +++++++++++-
 .../steps/appmodel/SetSensorWorkflowStep.java      | 162 +++++++++++++++++++--
 .../core/workflow/WorkflowBeefyStepTest.java       |  36 ++++-
 .../entity/stock/WorkflowStartableTest.java        |   6 +-
 .../apache/brooklyn/util/text/StringEscapes.java   |  32 ++--
 .../brooklyn/util/text/StringEscapesTest.java      |   5 +
 6 files changed, 297 insertions(+), 40 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 bc3be24cb1..713735b1b8 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,7 @@
  */
 package org.apache.brooklyn.core.workflow.steps.appmodel;
 
+import com.google.common.collect.Iterables;
 import com.google.common.reflect.TypeToken;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.config.ConfigKey;
@@ -27,8 +28,17 @@ 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.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;
+
 public class ClearSensorWorkflowStep extends WorkflowStepDefinition {
 
     public static final String SHORTHAND = "[ ${sensor.type} ] ${sensor.name}";
@@ -44,12 +54,92 @@ 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 sensorName = 
context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_INPUT,
 sensor.name, String.class);
-        if (Strings.isBlank(sensorName)) 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);
+
         TypeToken<?> type = context.lookupType(sensor.type, () -> 
TypeToken.of(Object.class));
         Entity entity = sensor.entity;
         if (entity==null) entity = context.getEntity();
-        
((EntityInternal)entity).sensors().remove(Sensors.newSensor(Object.class, 
sensorName));
+
+        if (sensorNameIndexes.isEmpty()) {
+            ((EntityInternal) 
entity).sensors().remove(Sensors.newSensor(Object.class, sensorNameFull));
+        } else {
+            ((EntityInternal) 
entity).sensors().modify(Sensors.newSensor(Object.class, sensorNameBase), old 
-> {
+
+                boolean setLast = false;
+
+                Object newTarget = SetSensorWorkflowStep.makeMutable(old, 
sensorNameIndexes);
+                Object target = newTarget;
+
+                MutableList<Object> indexes = 
MutableList.copyOf(sensorNameIndexes);
+                while (!indexes.isEmpty()) {
+                    Object i = indexes.remove(0);
+                    boolean isLast = indexes.isEmpty();
+                    Object nextTarget;
+
+                    if (target==null) {
+                        // not found, exit
+                        break;
+                    }
+
+                    if (target instanceof Map) {
+                        if (isLast) {
+                            setLast = true;
+                            ((Map) target).remove(i);
+                            nextTarget = null;
+                        } else {
+                            nextTarget = ((Map) target).get(i);
+                            if (nextTarget==null) break;
+                            ((Map) target).put(i, 
SetSensorWorkflowStep.makeMutable(nextTarget, indexes));
+                        }
+
+                    } 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;
+
+                        if (outOfBounds) {
+                            nextTarget = null;
+                            break;
+                        } else if (isLast) {
+                            setLast = true;
+                            if (target instanceof List) {
+                                ((List) target).remove(ii);
+                            } else {
+                                Iterator ti = ((Iterable) target).iterator();
+                                for (int j=0; j<ii; j++) {
+                                    ti.next();
+                                }
+                                ti.remove();
+                            }
+
+                            nextTarget = null;
+                            break;
+                        } else {
+                            Object t0 = Iterables.get((Iterable) target, ii);
+                            nextTarget = SetSensorWorkflowStep.makeMutable(t0, 
indexes);
+                            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);
+                            }
+                        }
+
+                    } else {
+                        throw new IllegalArgumentException("Cannot find 
argument '" + i + "' in " + target);
+                    }
+
+                    target = nextTarget;
+                }
+
+                if (setLast) return Maybe.of(newTarget);
+                else return Maybe.ofDisallowingNull(old);
+            });
+        }
+
         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 c758a41785..93af80a119 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,6 +18,7 @@
  */
 package org.apache.brooklyn.core.workflow.steps.appmodel;
 
+import com.google.common.collect.Iterables;
 import com.google.common.reflect.TypeToken;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
@@ -28,12 +29,20 @@ 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.util.core.predicates.DslPredicates;
 import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.text.StringEscapes;
 import org.apache.brooklyn.util.text.Strings;
 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 {
@@ -73,11 +82,16 @@ public class SetSensorWorkflowStep extends 
WorkflowStepDefinition {
     protected Object doTaskBody(WorkflowStepInstanceExecutionContext context) {
         EntityValueToSet sensor = context.getInput(SENSOR);
         if (sensor==null) throw new IllegalArgumentException("Sensor name is 
required");
-        String sensorName = 
context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_INPUT,
 sensor.name, String.class);
-        if (Strings.isBlank(sensorName)) 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);
+
         TypeToken<?> type = context.lookupType(sensor.type, () -> 
TypeToken.of(Object.class));
         final Entity entity = sensor.entity!=null ? sensor.entity : 
context.getEntity();
-        AttributeSensor<Object> s = (AttributeSensor<Object>) 
Sensors.newSensor(type, sensorName);
+        AttributeSensor<Object> sensorBase = (AttributeSensor<Object>) 
Sensors.newSensor(type, sensorNameBase);
         AtomicReference<Object> resolvedValue = new AtomicReference<>();
         Object oldValue;
 
@@ -93,25 +107,95 @@ public class SetSensorWorkflowStep extends 
WorkflowStepDefinition {
         };
 
         DslPredicates.DslPredicate require = context.getInput(REQUIRE);
-        if (require==null) {
+        if (require==null && sensorNameIndexes.isEmpty()) {
             resolve.run();
-            oldValue = entity.sensors().set(s, resolvedValue.get());
+            oldValue = entity.sensors().set(sensorBase, resolvedValue.get());
         } else {
-            oldValue = entity.sensors().modify(s, old -> {
-                if (old==null && 
!((AbstractEntity.BasicSensorSupport)entity.sensors()).contains(s.getName())) {
-                    DslPredicates.DslEntityPredicateDefault requireTweaked = 
new DslPredicates.DslEntityPredicateDefault();
-                    requireTweaked.sensor = s.getName();
-                    requireTweaked.check = require;
-                    if (!requireTweaked.apply(entity)) {
-                        throw new SensorRequirementFailedAbsent("Sensor 
"+s.getName()+" unset or unavailable when there is a non-absent requirement");
+            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);
+                        }
                     }
-                } else {
-                    if (!require.apply(old)) {
-                        throw new SensorRequirementFailed("Sensor 
"+s.getName()+" value does not match requirement", old);
+
+                    if (old == null && !((AbstractEntity.BasicSensorSupport) 
entity.sensors()).contains(sensorBase.getName())) {
+                        DslPredicates.DslEntityPredicateDefault requireTweaked 
= new DslPredicates.DslEntityPredicateDefault();
+                        requireTweaked.sensor = sensorNameFull;
+                        requireTweaked.check = 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();
-                return Maybe.of(resolvedValue.get());
+
+                // 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);
+                                }
+                            }
+
+                        } else {
+                            throw new IllegalArgumentException("Cannot find 
argument '" + i + "' in " + target);
+                        }
+                        target = nextTarget;
+                    }
+                } else {
+                    result = resolvedValue.get();
+                }
+
+                return Maybe.of(result);
             });
         }
 
@@ -121,5 +205,51 @@ public class SetSensorWorkflowStep extends 
WorkflowStepDefinition {
         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/test/java/org/apache/brooklyn/core/workflow/WorkflowBeefyStepTest.java
 
b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowBeefyStepTest.java
index e3c05dc984..cf46c0857a 100644
--- 
a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowBeefyStepTest.java
+++ 
b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowBeefyStepTest.java
@@ -38,14 +38,17 @@ import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.config.ConfigBag;
 import org.apache.brooklyn.util.core.http.BetterMockWebServer;
 import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool;
+import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.http.executor.HttpConfig;
 import org.apache.brooklyn.util.net.Networking;
+import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.time.Duration;
 import org.testng.annotations.Test;
 
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -63,8 +66,14 @@ public class WorkflowBeefyStepTest extends 
BrooklynMgmtUnitTestSupport {
         return runSteps(steps, appFunction, null);
     }
     Object runSteps(List<Object> steps, Consumer<BasicApplication> 
appFunction, ConfigBag defaultConfig) {
+        return runSteps(true, steps, appFunction, defaultConfig);
+    }
+    Object runMoreSteps(List<Object> steps) {
+        return runSteps(false, steps, null, null);
+    }
+    Object runSteps(boolean reset, List<Object> steps, 
Consumer<BasicApplication> appFunction, ConfigBag defaultConfig) {
         loadTypes();
-        BasicApplication app = 
mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class));
+        BasicApplication app = reset || lastApp==null ? 
mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)) 
: lastApp;
         this.lastApp = app;
         WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance()
                 .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow")
@@ -97,6 +106,31 @@ public class WorkflowBeefyStepTest extends 
BrooklynMgmtUnitTestSupport {
         EntityAsserts.assertAttributeEquals(lastApp, 
Sensors.newSensor(Object.class, "last-param"), "from-invocation");
     }
 
+    @Test
+    public void testSensorMap() throws Exception {
+        Object r;
+        r = runSteps(MutableList.of("set-sensor some.map['key'] = x", "return 
${entity.sensor['some.map']}"), null);
+        Asserts.assertEquals(r, MutableMap.of("key", "x"));
+
+        r = runMoreSteps(MutableList.of("set-sensor some.map[key2] = y", 
"return ${entity.sensor['some.map']}"));
+        Asserts.assertEquals(r, MutableMap.of("key", "x", "key2", "y"));
+
+        r = runMoreSteps(MutableList.of("set-sensor some.new['a'][\"b\"][-1] = 
ab0", "return ${entity.sensor['some.new']}"));
+        Asserts.assertEquals(r, MutableMap.of("a", MutableMap.of("b", 
MutableList.of("ab0"))));
+
+        r = runMoreSteps(MutableList.of("set-sensor some.new[\"a\"][\"b\"][1] 
= ab1", "return ${entity.sensor['some.new']}"));
+        Asserts.assertEquals(r, MutableMap.of("a", MutableMap.of("b", 
MutableList.of("ab0", "ab1"))));
+
+        r = runMoreSteps(MutableList.of("clear-sensor 
some.new[\"a\"][\"b\"][0]", "return ${entity.sensor['some.new']}"));
+        Asserts.assertEquals(r, MutableMap.of("a", MutableMap.of("b", 
MutableList.of("ab1"))));
+
+        r = runMoreSteps(MutableList.of("clear-sensor 
some.new[\"a\"][\"b\"][999]", "return ${entity.sensor['some.new']}"));
+        Asserts.assertEquals(r, MutableMap.of("a", MutableMap.of("b", 
MutableList.of("ab1"))));
+
+        r = runMoreSteps(MutableList.of("clear-sensor some.new[\"a\"]", 
"return ${entity.sensor['some.new']}"));
+        Asserts.assertEquals(r, MutableMap.of());
+    }
+
     @Test
     public void testSshLocalhost() throws NoMachinesAvailableException {
         LocalhostMachineProvisioningLocation loc = 
mgmt.getLocationManager().createLocation(LocationSpec.create(LocalhostMachineProvisioningLocation.class)
diff --git 
a/core/src/test/java/org/apache/brooklyn/entity/stock/WorkflowStartableTest.java
 
b/core/src/test/java/org/apache/brooklyn/entity/stock/WorkflowStartableTest.java
index dbd4918fa8..aa54ba42cc 100644
--- 
a/core/src/test/java/org/apache/brooklyn/entity/stock/WorkflowStartableTest.java
+++ 
b/core/src/test/java/org/apache/brooklyn/entity/stock/WorkflowStartableTest.java
@@ -90,8 +90,7 @@ public class WorkflowStartableTest extends 
BrooklynAppUnitTestSupport {
 
         WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance()
                 .configure(WorkflowEffector.EFFECTOR_NAME, "make-problem")
-                .configure(WorkflowEffector.STEPS, MutableList.of("set-sensor 
service.problems = { some_problem: Testing }")) );
-//                .configure(WorkflowEffector.STEPS, 
MutableList.of("set-sensor service.problems['some_problem'] = Testing a 
problem")) );
+                .configure(WorkflowEffector.STEPS, MutableList.of("set-sensor 
service.problems['some_problem'] = Testing a problem")) );
         eff.apply((EntityLocal)entity);
         
entity.invoke(entity.getEntityType().getEffectorByName("make-problem").get(), 
null).getUnchecked();
 
@@ -101,8 +100,7 @@ public class WorkflowStartableTest extends 
BrooklynAppUnitTestSupport {
 
         eff = new WorkflowEffector(ConfigBag.newInstance()
                 .configure(WorkflowEffector.EFFECTOR_NAME, "fix-problem")
-                .configure(WorkflowEffector.STEPS, MutableList.of("set-sensor 
service.problems = {}")) );
-//                .configure(WorkflowEffector.STEPS, 
MutableList.of("clear-sensor service.problems['some_problem']")) );
+                .configure(WorkflowEffector.STEPS, 
MutableList.of("clear-sensor service.problems['some_problem']")) );
         eff.apply((EntityLocal)entity);
         
entity.invoke(entity.getEntityType().getEffectorByName("fix-problem").get(), 
null).getUnchecked();
 
diff --git 
a/utils/common/src/main/java/org/apache/brooklyn/util/text/StringEscapes.java 
b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringEscapes.java
index 98e183ed69..08d3442937 100644
--- 
a/utils/common/src/main/java/org/apache/brooklyn/util/text/StringEscapes.java
+++ 
b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringEscapes.java
@@ -194,7 +194,7 @@ public class StringEscapes {
 
         /** given a string in bash notation, e.g. with quoted portions needing 
unescaped, returns the unescaped and unquoted version */
         public static String unwrapBashQuotesAndEscapes(String s) {
-            return applyUnquoteAndUnescape(s, "Bash", true);
+            return applyUnquoteAndUnescape(s, "Bash", true, true);
         }
     }
     
@@ -270,7 +270,7 @@ public class StringEscapes {
         /** given a string in java syntax, e.g. wrapped in quotes and with 
backslash escapes, returns the literal value,
          * without the surrounding quotes and unescaped; throws 
IllegalArgumentException if not a valid java string */
         public static String unwrapJavaString(String s) {
-            return applyUnquoteAndUnescape(s, "Java", false);
+            return applyUnquoteAndUnescape(s, "Java", false, false);
         }
         
         /**
@@ -501,18 +501,19 @@ public class StringEscapes {
         out.append('\\');
         out.append(c);
     }
-    private static String applyUnquoteAndUnescape(String s, String mode, 
boolean allowMultipleQuotes) {
+    private static String applyUnquoteAndUnescape(String s, String mode, 
boolean allowMultipleQuotes, boolean allowSingleQuotes) {
         StringBuilder result = new StringBuilder();
         boolean escaped = false;
-        boolean quoted = false;
+        Character quote = null;
         for (int i=0; i<s.length(); i++) {
             char c = s.charAt(i);
-            if (!quoted) {
+            if (quote==null) {
                 assert (i==0 || allowMultipleQuotes);
                 assert !escaped;
-                if (c=='"') quoted = true;
+                if (c=='"') quote = c;
+                else if (c=='\'' && allowSingleQuotes) quote = c;
                 else if (!allowMultipleQuotes)
-                    throw new IllegalArgumentException("String '"+s+"' is not 
a valid "+mode+" string (must start with double quote)");
+                    throw new IllegalArgumentException("String '"+s+"' is not 
a valid "+mode+" string (must start with quote)");
                 else result.append(c);
             } else {
                 if (escaped) {
@@ -522,17 +523,16 @@ public class StringEscapes {
                     else if (c=='r') result.append('\r');
                     else throw new IllegalArgumentException("String '"+s+"' is 
not a valid "+mode+" string (unsupported escape char '"+c+"' at position 
"+i+")");
                     escaped = false;
-                } else {
-                    if (c=='\\') escaped = true;
-                    else if (c=='\"') {
-                        quoted = false;
-                        if (!allowMultipleQuotes && i<s.length()-1)
-                            throw new IllegalArgumentException("String '"+s+"' 
is not a valid "+mode+" string (unescaped interior double quote at position 
"+i+")");
-                    } else result.append(c); 
-                }
+                } else if (c=='\\') {
+                    escaped = true;
+                } else if (c == quote && (c=='\'' || c=='"')) {
+                    quote = null;
+                    if (!allowMultipleQuotes && i < s.length() - 1)
+                        throw new IllegalArgumentException("String '" + s + "' 
is not a valid " + mode + " string (unescaped interior double quote at position 
" + i + ")");
+                } else result.append(c);
             }
         }
-        if (quoted)
+        if (quote!=null)
             throw new IllegalArgumentException("String '"+s+"' is not a valid 
"+mode+" string (unterminated string)");
         assert !escaped;
         return result.toString();
diff --git 
a/utils/common/src/test/java/org/apache/brooklyn/util/text/StringEscapesTest.java
 
b/utils/common/src/test/java/org/apache/brooklyn/util/text/StringEscapesTest.java
index 613bc6c557..d27c0f84fe 100644
--- 
a/utils/common/src/test/java/org/apache/brooklyn/util/text/StringEscapesTest.java
+++ 
b/utils/common/src/test/java/org/apache/brooklyn/util/text/StringEscapesTest.java
@@ -228,4 +228,9 @@ public class StringEscapesTest {
                 MutableList.of(MutableMap.of("a", MutableList.<Object>of("b", 
2)), "world"));
     }
 
+    @Test
+    public void testBashUnwrap() {
+        
Assert.assertEquals(StringEscapes.BashStringEscapes.unwrapBashQuotesAndEscapes("\"back\\\\slash\""),
 "back\\slash");
+        
Assert.assertEquals(StringEscapes.BashStringEscapes.unwrapBashQuotesAndEscapes("\'back\\\\slash\'"),
 "back\\slash");
+    }
 }

Reply via email to