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"); + } }
