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

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

commit 0c6b44172adc1e76caf4b84f6a4ec3c15d6afb1b
Author: Alex Heneveld <a...@cloudsoft.io>
AuthorDate: Wed Feb 7 14:36:49 2024 +0000

    update shorthand processor to use expression processor
    
    allows us to respect spaces and other things inside brackets
---
 .../brooklyn/core/workflow/ShorthandProcessor.java | 319 +-------------
 ...ocessor.java => ShorthandProcessorEpToQst.java} | 225 ++++++++--
 .../workflow/ShorthandProcessorExprParser.java     | 463 +++++++++++++++++++++
 ...ndProcessor.java => ShorthandProcessorQst.java} |  33 +-
 .../core/workflow/WorkflowStepDefinition.java      |   7 +-
 .../steps/appmodel/UpdateChildrenWorkflowStep.java |   6 +-
 .../steps/variables/SetVariableWorkflowStep.java   |   2 +-
 .../workflow/steps/variables/TransformReplace.java |   3 -
 .../workflow/steps/variables/TransformSplit.java   |   9 -
 .../variables/TransformVariableWorkflowStep.java   |   2 +-
 ...est.java => ShorthandProcessorEpToQstTest.java} |  50 ++-
 ...sorTest.java => ShorthandProcessorQstTest.java} |  26 +-
 12 files changed, 747 insertions(+), 398 deletions(-)

diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessor.java 
b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessor.java
index 32ba2f835c..0adbaffc23 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessor.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessor.java
@@ -18,337 +18,36 @@
  */
 package org.apache.brooklyn.core.workflow;
 
-import org.apache.brooklyn.util.collections.CollectionMerger;
-import org.apache.brooklyn.util.collections.MutableList;
-import org.apache.brooklyn.util.collections.MutableMap;
-import org.apache.brooklyn.util.guava.Maybe;
-import org.apache.brooklyn.util.text.QuotedStringTokenizer;
-import org.apache.brooklyn.util.text.Strings;
-
-import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
-import java.util.function.Consumer;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
+
+import org.apache.brooklyn.util.guava.Maybe;
 
 /**
- * Accepts a shorthand template, and converts it to a map of values,
- * e.g. given template "[ ?${type_set} ${sensor.type} ] ${sensor.name} \"=\" 
${value}"
- * and input "integer foo=3", this will return
- * { sensor: { type: integer, name: foo }, value: 3, type_set: true }.
- *
- * Expects space-separated TOKEN where TOKEN is either:
- *
- * ${VAR} - to set VAR, which should be of the regex 
[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*, with dot separation used to set nested maps;
- *   will match a quoted string if supplied, else up to the next literal if 
the next token is a literal, else the next work.
- * ${VAR...} - as above, but will collect multiple args if needed (if the next 
token is a literal matched further on, or if at end of word)
- * "LITERAL" - to expect a literal expression. this must include the quotation 
marks and should include spaces if spaces are required.
- * [ TOKEN ] - to indicate TOKEN is optional, where TOKEN is one of the above 
sections. parsing is attempted first with it, then without it.
- * [ ?${VAR} TOKEN ] - as `[ TOKEN ]` but VAR is set true or false depending 
whether this optional section was matched.
- *
- * Would be nice to support A | B (exclusive or) for A or B but not both 
(where A might contain a literal for disambiguation),
- * and ( X ) for X required but grouped (for use with | (exclusive or) where 
one option is required).
- * Would also be nice to support any order, which could be ( A & B ) to allow 
A B or B A.
- *
- * But for now we've made do without it, with some compromises:
- * * keywords must follow the order indicated
- * * exclusive alternatives are disallowed by code subsequently or checked 
separately (eg Transform)
+ * This impl delegates to one of the various classes that do this -- see notes 
in individual ones.
  */
 public class ShorthandProcessor {
 
-    private final String template;
-    boolean finalMatchRaw = false;
-    boolean failOnMismatch = true;
+    ShorthandProcessorEpToQst delegate;
 
     public ShorthandProcessor(String template) {
-        this.template = template;
+        delegate = new ShorthandProcessorEpToQst(template);
     }
 
     public Maybe<Map<String,Object>> process(String input) {
-        return new ShorthandProcessorAttempt(this, input).call();
+        return delegate.process(input);
     }
 
     /** whether the last match should preserve quotes and spaces; default 
false */
     public ShorthandProcessor withFinalMatchRaw(boolean finalMatchRaw) {
-        this.finalMatchRaw = finalMatchRaw;
+        delegate.withFinalMatchRaw(finalMatchRaw);
         return this;
     }
 
     /** whether to fail on mismatched quotes in the input, default true */
     public ShorthandProcessor withFailOnMismatch(boolean failOnMismatch) {
-        this.failOnMismatch = failOnMismatch;
+        // only supported for some
+        delegate.withFailOnMismatch(failOnMismatch);
         return this;
     }
 
-    static class ShorthandProcessorAttempt {
-        private final List<String> templateTokens;
-        private final String inputOriginal;
-        private final QuotedStringTokenizer qst;
-        private final String template;
-        private final ShorthandProcessor options;
-        int optionalDepth = 0;
-        int optionalSkippingInput = 0;
-        private String inputRemaining;
-        Map<String, Object> result;
-        Consumer<String> valueUpdater;
-
-        ShorthandProcessorAttempt(ShorthandProcessor proc, String input) {
-            this.template = proc.template;
-            this.options = proc;
-            this.qst = qst(template);
-            this.templateTokens = qst.remainderAsList();
-            this.inputOriginal = input;
-        }
-
-        private QuotedStringTokenizer qst(String x) {
-            return 
QuotedStringTokenizer.builder().includeQuotes(true).includeDelimiters(false).expectQuotesDelimited(true).failOnOpenQuote(options.failOnMismatch).build(x);
-        }
-
-        public synchronized Maybe<Map<String,Object>> call() {
-            if (result == null) {
-                result = MutableMap.of();
-                inputRemaining = inputOriginal;
-            } else {
-                throw new IllegalStateException("Only allowed to use once");
-            }
-            Maybe<Object> error = doCall();
-            if (error.isAbsent()) return Maybe.Absent.castAbsent(error);
-            inputRemaining = Strings.trimStart(inputRemaining);
-            if (Strings.isNonBlank(inputRemaining)) {
-                if (valueUpdater!=null) {
-                    QuotedStringTokenizer qstInput = qst(inputRemaining);
-                    valueUpdater.accept(getRemainderPossiblyRaw(qstInput));
-                } else {
-                    // shouldn't come here
-                    return Maybe.absent("Input has trailing characters after 
template is matched: '" + inputRemaining + "'");
-                }
-            }
-            return Maybe.of(result);
-        }
-
-        protected Maybe<Object> doCall() {
-            boolean isEndOfOptional = false;
-            outer: while (true) {
-                if (isEndOfOptional) {
-                    if (optionalDepth <= 0) {
-                        throw new IllegalStateException("Unexpected optional 
block closure");
-                    }
-                    optionalDepth--;
-                    if (optionalSkippingInput>0) {
-                        // we were in a block where we skipped something 
optional because it couldn't be matched; outer parser is now canonical,
-                        // and should stop skipping
-                        return Maybe.of(true);
-                    }
-                    isEndOfOptional = false;
-                }
-
-                if (templateTokens.isEmpty()) {
-                    if (Strings.isNonBlank(inputRemaining) && 
valueUpdater==null) {
-                        return Maybe.absent("Input has trailing characters 
after template is matched: '" + inputRemaining + "'");
-                    }
-                    if (optionalDepth>0)
-                        return Maybe.absent("Mismatched optional marker in 
template");
-                    return Maybe.of(true);
-                }
-                String t = templateTokens.remove(0);
-
-                if (t.startsWith("[")) {
-                    t = t.substring(1);
-                    if (!t.isEmpty()) {
-                        templateTokens.add(0, t);
-                    }
-                    String optionalPresentVar = null;
-                    if (!templateTokens.isEmpty() && 
templateTokens.get(0).startsWith("?")) {
-                        String v = templateTokens.remove(0);
-                        if (v.startsWith("?${") && v.endsWith("}")) {
-                            optionalPresentVar = v.substring(3, v.length() - 
1);
-                        } else {
-                            throw new IllegalStateException("? after [ should 
indicate optional presence variable using syntax '?${var}', not '"+v+"'");
-                        }
-                    }
-                    Maybe<Object> cr;
-                    if (optionalSkippingInput<=0) {
-                        // make a deep copy so that valueUpdater writes get 
replayed
-                        Map<String, Object> backupResult = (Map) 
CollectionMerger.builder().deep(true).build().merge(MutableMap.of(), result);
-                        Consumer<String> backupValueUpdater = valueUpdater;
-                        String backupInputRemaining = inputRemaining;
-                        List<String> backupTemplateTokens = 
MutableList.copyOf(templateTokens);
-                        int oldDepth = optionalDepth;
-                        int oldSkippingDepth = optionalSkippingInput;
-
-                        optionalDepth++;
-                        cr = doCall();
-                        if (cr.isPresent()) {
-                            // succeeded
-                            if (optionalPresentVar!=null) 
result.put(optionalPresentVar, true);
-                            continue;
-
-                        } else {
-                            // restore
-                            result = backupResult;
-                            valueUpdater = backupValueUpdater;
-                            if (optionalPresentVar!=null) 
result.put(optionalPresentVar, false);
-                            inputRemaining = backupInputRemaining;
-                            templateTokens.clear();
-                            templateTokens.addAll(backupTemplateTokens);
-                            optionalDepth = oldDepth;
-                            optionalSkippingInput = oldSkippingDepth;
-
-                            optionalSkippingInput++;
-                            optionalDepth++;
-                            cr = doCall();
-                            if (cr.isPresent()) {
-                                optionalSkippingInput--;
-                                continue;
-                            }
-                        }
-                    } else {
-                        if (optionalPresentVar!=null) {
-                            result.put(optionalPresentVar, false);
-                            valueUpdater = null;
-                        }
-                        optionalDepth++;
-                        cr = doCall();
-                        if (cr.isPresent()) {
-                            continue;
-                        }
-                    }
-                    return cr;
-                }
-
-                isEndOfOptional = t.endsWith("]");
-
-                if (isEndOfOptional) {
-                    t = t.substring(0, t.length() - 1);
-                    if (t.isEmpty()) continue;
-                    // next loop will process the end of the optionality
-                }
-
-                if (qst.isQuoted(t)) {
-                    if (optionalSkippingInput>0) continue;
-
-                    String literal = qst.unwrapIfQuoted(t);
-                    do {
-                        // ignore leading spaces (since the quoted string 
tokenizer will have done that anyway); but their _absence_ can be significant 
for intra-token searching when matching a var
-                        inputRemaining = Strings.trimStart(inputRemaining);
-                        if 
(inputRemaining.startsWith(Strings.trimStart(literal))) {
-                            // literal found
-                            inputRemaining = 
inputRemaining.substring(Strings.trimStart(literal).length());
-                            continue outer;
-                        }
-                        if (inputRemaining.isEmpty()) return 
Maybe.absent("Literal '"+literal+"' expected, when end of input reached");
-                        if (valueUpdater!=null) {
-                            QuotedStringTokenizer qstInput = 
qst(inputRemaining);
-                            if (!qstInput.hasMoreTokens()) return 
Maybe.absent("Literal '"+literal+"' expected, when end of input tokens 
reached");
-                            String value = 
getNextInputTokenUpToPossibleExpectedLiteral(qstInput, literal);
-                            valueUpdater.accept(value);
-                            continue;
-                        }
-                        return Maybe.absent("Literal '"+literal+"' expected, 
when encountered '"+inputRemaining+"'");
-                    } while (true);
-                }
-
-                if (t.startsWith("${") && t.endsWith("}")) {
-                    if (optionalSkippingInput>0) continue;
-
-                    t = t.substring(2, t.length()-1);
-                    String value;
-
-                    inputRemaining = inputRemaining.trim();
-                    QuotedStringTokenizer qstInput = qst(inputRemaining);
-                    if (!qstInput.hasMoreTokens()) return Maybe.absent("End of 
input when looking for variable "+t);
-
-                    if (!templateTokens.stream().filter(x -> 
!x.equals("]")).findFirst().isPresent()) {
-                        // last word (whether optional or not) takes everything
-                        value = getRemainderPossiblyRaw(qstInput);
-                        inputRemaining = "";
-
-                    } else {
-                        value = 
getNextInputTokenUpToPossibleExpectedLiteral(qstInput, null);
-                    }
-                    boolean multiMatch = t.endsWith("...");
-                    if (multiMatch) t = Strings.removeFromEnd(t, "...");
-                    String keys[] = t.split("\\.");
-                    final String tt = t;
-                    valueUpdater = v2 -> {
-                        Map target = result;
-                        for (int i=0; i<keys.length; i++) {
-                            if 
(!Pattern.compile("[A-Za-z0-9_-]+").matcher(keys[i]).matches()) {
-                                throw new IllegalArgumentException("Invalid 
variable '"+tt+"'");
-                            }
-                            if (i == keys.length - 1) {
-                                target.compute(keys[i], (k, v) -> v == null ? 
v2 : v + " " + v2);
-                            } else {
-                                // need to make sure we have a map or null
-                                target = (Map) target.compute(keys[i], (k, v) 
-> {
-                                    if (v == null) return MutableMap.of();
-                                    if (v instanceof Map) return v;
-                                    return Maybe.absent("Cannot process 
shorthand for " + Arrays.asList(keys) + " because entry '" + k + "' is not a 
map (" + v + ")");
-                                });
-                            }
-                        }
-                    };
-                    valueUpdater.accept(value);
-                    if (!multiMatch) valueUpdater = null;
-                    continue;
-                }
-
-                // unexpected token
-                return Maybe.absent("Unexpected token in shorthand pattern 
'"+template+"' at position "+(template.lastIndexOf(t)+1));
-            }
-        }
-
-        private String getRemainderPossiblyRaw(QuotedStringTokenizer qstInput) 
{
-            String value;
-            value = Strings.join(qstInput.remainderRaw(), "");
-            if (!options.finalMatchRaw) {
-                return qstInput.unwrapIfQuoted(value);
-            }
-            return value;
-        }
-
-        private String 
getNextInputTokenUpToPossibleExpectedLiteral(QuotedStringTokenizer qstInput, 
String nextLiteral) {
-            String value;
-            String v = qstInput.nextToken();
-            if (qstInput.isQuoted(v)) {
-                // input was quoted, eg "\"foo=b\" ..." -- ignore the = in 
"foo=b"
-                value = qstInput.unwrapIfQuoted(v);
-                inputRemaining = inputRemaining.substring(v.length());
-            } else {
-                // input not quoted, if next template token is literal, look 
for it
-                boolean isLiteralExpected;
-                if (nextLiteral==null) {
-                    nextLiteral = templateTokens.get(0);
-                    if (qstInput.isQuoted(nextLiteral)) {
-                        nextLiteral = qstInput.unwrapIfQuoted(nextLiteral);
-                        isLiteralExpected = true;
-                    } else {
-                        isLiteralExpected = false;
-                    }
-                } else {
-                    isLiteralExpected = true;
-                }
-                if (isLiteralExpected) {
-                    int nli = v.indexOf(nextLiteral);
-                    if (nli>0) {
-                        // literal found in unquoted string, eg "foo=bar" when 
literal is =
-                        value = v.substring(0, nli);
-                        inputRemaining = 
inputRemaining.substring(value.length());
-                    } else {
-                        // literal not found
-                        value = v;
-                        inputRemaining = 
inputRemaining.substring(value.length());
-                    }
-                } else {
-                    // next is not a literal, so the whole token is the value
-                    value = v;
-                    inputRemaining = inputRemaining.substring(value.length());
-                }
-            }
-            return value;
-        }
-    }
-
-
 }
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessor.java 
b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorEpToQst.java
similarity index 62%
copy from 
core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessor.java
copy to 
core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorEpToQst.java
index 32ba2f835c..cb94600636 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessor.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorEpToQst.java
@@ -18,21 +18,31 @@
  */
 package org.apache.brooklyn.core.workflow;
 
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+
+import org.apache.brooklyn.core.workflow.utils.ExpressionParser;
+import org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.ParseNode;
+import 
org.apache.brooklyn.core.workflow.utils.ExpressionParserImpl.ParseNodeOrValue;
 import org.apache.brooklyn.util.collections.CollectionMerger;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.text.QuotedStringTokenizer;
 import org.apache.brooklyn.util.text.Strings;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
+ * This is the latest version of the SP which uses the EP for input, and the 
QST for templates,
+ * to keep backwards compatibility but allow better handling for internal 
double quotes and interpolated expressions with spaces.
+ * It falls back to QST when there are errors to maximize compatibility.
+ *
+ * ===
+ *
  * Accepts a shorthand template, and converts it to a map of values,
  * e.g. given template "[ ?${type_set} ${sensor.type} ] ${sensor.name} \"=\" 
${value}"
  * and input "integer foo=3", this will return
@@ -55,53 +65,57 @@ import java.util.stream.Collectors;
  * * keywords must follow the order indicated
  * * exclusive alternatives are disallowed by code subsequently or checked 
separately (eg Transform)
  */
-public class ShorthandProcessor {
+public class ShorthandProcessorEpToQst {
+
+    private static final Logger log = 
LoggerFactory.getLogger(ShorthandProcessorEpToQst.class);
+
+    static final boolean TRY_HARDER_FOR_QST_COMPATIBILITY = true;
 
     private final String template;
     boolean finalMatchRaw = false;
     boolean failOnMismatch = true;
 
-    public ShorthandProcessor(String template) {
+    public ShorthandProcessorEpToQst(String template) {
         this.template = template;
     }
 
     public Maybe<Map<String,Object>> process(String input) {
-        return new ShorthandProcessorAttempt(this, input).call();
+        return new ShorthandProcessorQstAttempt(this, input).call();
     }
 
     /** whether the last match should preserve quotes and spaces; default 
false */
-    public ShorthandProcessor withFinalMatchRaw(boolean finalMatchRaw) {
+    public ShorthandProcessorEpToQst withFinalMatchRaw(boolean finalMatchRaw) {
         this.finalMatchRaw = finalMatchRaw;
         return this;
     }
 
     /** whether to fail on mismatched quotes in the input, default true */
-    public ShorthandProcessor withFailOnMismatch(boolean failOnMismatch) {
+    public ShorthandProcessorEpToQst withFailOnMismatch(boolean 
failOnMismatch) {
         this.failOnMismatch = failOnMismatch;
         return this;
     }
 
-    static class ShorthandProcessorAttempt {
+    static class ShorthandProcessorQstAttempt {
         private final List<String> templateTokens;
         private final String inputOriginal;
-        private final QuotedStringTokenizer qst;
+        private final QuotedStringTokenizer qst0;
         private final String template;
-        private final ShorthandProcessor options;
+        private final ShorthandProcessorEpToQst options;
         int optionalDepth = 0;
         int optionalSkippingInput = 0;
         private String inputRemaining;
         Map<String, Object> result;
         Consumer<String> valueUpdater;
 
-        ShorthandProcessorAttempt(ShorthandProcessor proc, String input) {
+        ShorthandProcessorQstAttempt(ShorthandProcessorEpToQst proc, String 
input) {
             this.template = proc.template;
             this.options = proc;
-            this.qst = qst(template);
-            this.templateTokens = qst.remainderAsList();
+            this.qst0 = qst0(template);
+            this.templateTokens = qst0.remainderAsList();  // QST works fine 
for the template
             this.inputOriginal = input;
         }
 
-        private QuotedStringTokenizer qst(String x) {
+        private QuotedStringTokenizer qst0(String x) {
             return 
QuotedStringTokenizer.builder().includeQuotes(true).includeDelimiters(false).expectQuotesDelimited(true).failOnOpenQuote(options.failOnMismatch).build(x);
         }
 
@@ -117,8 +131,8 @@ public class ShorthandProcessor {
             inputRemaining = Strings.trimStart(inputRemaining);
             if (Strings.isNonBlank(inputRemaining)) {
                 if (valueUpdater!=null) {
-                    QuotedStringTokenizer qstInput = qst(inputRemaining);
-                    valueUpdater.accept(getRemainderPossiblyRaw(qstInput));
+                    //QuotedStringTokenizer qstInput = qst(inputRemaining);
+                    
valueUpdater.accept(getRemainderPossiblyRaw(inputRemaining));
                 } else {
                     // shouldn't come here
                     return Maybe.absent("Input has trailing characters after 
template is matched: '" + inputRemaining + "'");
@@ -225,10 +239,10 @@ public class ShorthandProcessor {
                     // next loop will process the end of the optionality
                 }
 
-                if (qst.isQuoted(t)) {
+                if (qst0.isQuoted(t)) {
                     if (optionalSkippingInput>0) continue;
 
-                    String literal = qst.unwrapIfQuoted(t);
+                    String literal = qst0.unwrapIfQuoted(t);
                     do {
                         // ignore leading spaces (since the quoted string 
tokenizer will have done that anyway); but their _absence_ can be significant 
for intra-token searching when matching a var
                         inputRemaining = Strings.trimStart(inputRemaining);
@@ -239,9 +253,8 @@ public class ShorthandProcessor {
                         }
                         if (inputRemaining.isEmpty()) return 
Maybe.absent("Literal '"+literal+"' expected, when end of input reached");
                         if (valueUpdater!=null) {
-                            QuotedStringTokenizer qstInput = 
qst(inputRemaining);
-                            if (!qstInput.hasMoreTokens()) return 
Maybe.absent("Literal '"+literal+"' expected, when end of input tokens 
reached");
-                            String value = 
getNextInputTokenUpToPossibleExpectedLiteral(qstInput, literal);
+                            if (inputRemaining.isEmpty()) return 
Maybe.absent("Literal '"+literal+"' expected, when end of input tokens 
reached");
+                            String value = 
getNextInputTokenUpToPossibleExpectedLiteral(literal);
                             valueUpdater.accept(value);
                             continue;
                         }
@@ -256,16 +269,15 @@ public class ShorthandProcessor {
                     String value;
 
                     inputRemaining = inputRemaining.trim();
-                    QuotedStringTokenizer qstInput = qst(inputRemaining);
-                    if (!qstInput.hasMoreTokens()) return Maybe.absent("End of 
input when looking for variable "+t);
+                    if (inputRemaining.isEmpty()) return Maybe.absent("End of 
input when looking for variable "+t);
 
                     if (!templateTokens.stream().filter(x -> 
!x.equals("]")).findFirst().isPresent()) {
                         // last word (whether optional or not) takes everything
-                        value = getRemainderPossiblyRaw(qstInput);
+                        value = getRemainderPossiblyRaw(inputRemaining);
                         inputRemaining = "";
 
                     } else {
-                        value = 
getNextInputTokenUpToPossibleExpectedLiteral(qstInput, null);
+                        value = 
getNextInputTokenUpToPossibleExpectedLiteral(null);
                     }
                     boolean multiMatch = t.endsWith("...");
                     if (multiMatch) t = Strings.removeFromEnd(t, "...");
@@ -299,7 +311,7 @@ public class ShorthandProcessor {
             }
         }
 
-        private String getRemainderPossiblyRaw(QuotedStringTokenizer qstInput) 
{
+        private String getRemainderPossiblyRawQst(QuotedStringTokenizer 
qstInput) {
             String value;
             value = Strings.join(qstInput.remainderRaw(), "");
             if (!options.finalMatchRaw) {
@@ -308,8 +320,46 @@ public class ShorthandProcessor {
             return value;
         }
 
-        private String 
getNextInputTokenUpToPossibleExpectedLiteral(QuotedStringTokenizer qstInput, 
String nextLiteral) {
+        private String getRemainderPossiblyRaw(String inputRemaining) {
+            Maybe<String> mp = getRemainderPossiblyRawEp(inputRemaining);
+
+            if (TRY_HARDER_FOR_QST_COMPATIBILITY || (mp.isAbsent() && 
!options.failOnMismatch)) {
+                String qstResult = 
getRemainderPossiblyRawQst(qst0(inputRemaining));
+                if (mp.isPresent() && !mp.get().equals(qstResult)) {
+                    log.debug("Shorthand parsing semantics change for: 
"+inputOriginal+"\n" +
+                            "  old qst: "+qstResult+"\n"+
+                            "  new exp: "+mp.get());
+                    // to debug
+//                    getRemainderPossiblyRawEp(inputRemaining);
+                }
+                if (mp.isAbsent()) {
+                    return qstResult;
+                }
+            }
+
+            return mp.get();
+        }
+        private Maybe<String> getRemainderPossiblyRawEp(String inputRemaining) 
{
+            if (options.finalMatchRaw) {
+                return Maybe.of(inputRemaining);
+            }
+            Maybe<List<ParseNodeOrValue>> mp = 
ShorthandProcessorExprParser.tokenizer().parseEverything(inputRemaining);
+            return mp.map(pnl -> {
+                final boolean UNQUOTE_INDIVIDUAL_WORDS = false;  // legacy 
behaviour
+
+                if (pnl.size()==1 || UNQUOTE_INDIVIDUAL_WORDS) {
+                    return ExpressionParser.getAllUnquoted(pnl);
+                } else {
+                    return ExpressionParser.getUnescapedButNotUnquoted(pnl);
+                }
+            });
+        }
+
+        private String 
getNextInputTokenUpToPossibleExpectedLiteralQst(QuotedStringTokenizer qstInput, 
String nextLiteral) {
             String value;
+            if (!qstInput.hasMoreTokens()) {
+                return "";  // shouldn't happen
+            }
             String v = qstInput.nextToken();
             if (qstInput.isQuoted(v)) {
                 // input was quoted, eg "\"foo=b\" ..." -- ignore the = in 
"foo=b"
@@ -348,7 +398,116 @@ public class ShorthandProcessor {
             }
             return value;
         }
-    }
 
+        private String getNextInputTokenUpToPossibleExpectedLiteral(String 
nextLiteral) {
+            String oi = inputRemaining;
+            Maybe<String> v1 = 
getNextInputTokenUpToPossibleExpectedLiteralEp(nextLiteral);
+
+            if (TRY_HARDER_FOR_QST_COMPATIBILITY || (v1.isAbsent() && 
!options.failOnMismatch)) {
+                String ni = inputRemaining;
+
+                inputRemaining = oi;
+                String qstResult = 
getNextInputTokenUpToPossibleExpectedLiteralQst(qst0(inputRemaining), 
nextLiteral);
+                if (v1.isPresent() && !v1.get().equals(qstResult)) {
+                    log.debug("Shorthand parsing semantics change for literal 
" + nextLiteral + ": " + inputOriginal + "\n" +
+                            "  old qst: " + qstResult + "\n" +
+                            "  new exp: " + v1.get());
+
+//                    // to debug differences
+//                    inputRemaining = oi;
+////                    
getNextInputTokenUpToPossibleExpectedLiteralQst(qst0(inputRemaining), 
nextLiteral);
+//                    
getNextInputTokenUpToPossibleExpectedLiteralEp(nextLiteral);
+                }
+                if (v1.isAbsent()) {
+                    return qstResult;
+                }
+                inputRemaining = ni;
+            }
+
+            return v1.get();
+        }
+
+        private Maybe<String> 
getNextInputTokenUpToPossibleExpectedLiteralEp(String nextLiteral) {
+            String result = "";
+            boolean canRepeat = true;
+
+            Maybe<ParseNode> parseM = 
ShorthandProcessorExprParser.tokenizer().parse(inputRemaining);
+            while (canRepeat) {
+                String value;
+                if (parseM.isAbsent()) return Maybe.castAbsent(parseM);
+
+                List<ParseNodeOrValue> tokens = parseM.get().getContents();
+                ParseNodeOrValue t = tokens.iterator().next();
+
+                if (ExpressionParser.isQuotedExpressionNode(t)) {
+                    // input was quoted, eg "\"foo=b\" ..." -- ignore the = in 
"foo=b"
+                    value = ExpressionParser.getUnquoted(t);
+                    inputRemaining = 
inputRemaining.substring(t.getSource().length());
+
+                } else {
+                    // input not quoted, if next template token is literal, 
look for it
+                    boolean isLiteralExpected;
+                    if (nextLiteral == null) {
+                        String nl = templateTokens.get(0);
+                        if (qst0.isQuoted(nl)) {
+                            nextLiteral = qst0.unwrapIfQuoted(nl);
+                            isLiteralExpected = true;
+                        } else {
+                            isLiteralExpected = false;
+                        }
+                    } else {
+                        isLiteralExpected = true;
+                    }
+                    if (isLiteralExpected) {
+                        int nli =
+                                // previously took next QST token
+                                
qst0(inputRemaining).nextToken().indexOf(nextLiteral);
+
+//                            // this is simple, slightly greedier;
+//                            // slightly too greedy, in that nextLiteral 
inside quotes further away will match
+//                            // and it breaks backwards compatibility
+//                            inputRemaining.indexOf(nextLiteral);
+
+//                            // this parse node is probably too short
+//                            t.getSource().indexOf(nextLiteral);
+
+                        final boolean ALLOW_NOTHING_BEFORE_LITERAL = false;
+                        if ((nli == 0 && ALLOW_NOTHING_BEFORE_LITERAL) || nli 
> 0) {
+                            // literal found in unquoted string, eg "foo=bar" 
when literal is =
+                            if 
(!qst0(inputRemaining).nextToken().startsWith(inputRemaining.substring(0, 
nli))) {
+                                String v = qst0(inputRemaining).nextToken();
+                            }
+                            value = inputRemaining.substring(0, nli);
+                            inputRemaining = inputRemaining.substring(nli);
+                            canRepeat = false;
+
+                        } else {
+                            // literal not found
+                            value = t.getSource();  // since we know it isn't 
quoted
+                            inputRemaining = 
inputRemaining.substring(value.length());
+                        }
+                    } else {
+                        // next is not a literal, so the whole token is the 
value - not unquoted
+                        value = t.getSource();
+                        inputRemaining = 
inputRemaining.substring(value.length());
+                    }
+                }
+                result += value;
+
+                canRepeat &= !inputRemaining.isEmpty();
+                if (canRepeat) {
+                    parseM = 
ShorthandProcessorExprParser.tokenizer().parse(inputRemaining);
+                    if (parseM.isAbsent()) canRepeat = false;
+                    else {
+                        if 
(ExpressionParser.startsWithWhitespace(parseM.get())) canRepeat = false;
+                        // otherwise, to act like QST, we treat 
non-whitespace-separated things all as one token
+                    }
+                }
+            }
+
+            return Maybe.of(result);
+        }
+
+    }
 
 }
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorExprParser.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorExprParser.java
new file mode 100644
index 0000000000..ae76e31659
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorExprParser.java
@@ -0,0 +1,463 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.brooklyn.core.workflow.utils.ExpressionParser;
+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.CollectionMerger;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.commons.lang3.tuple.Pair;
+
+/**
+ * An implentation of {@link ShorthandProcessor} which tries to use the 
ExpressionParser for everything.
+ * However the semantics of a parse tree vs a linear string are too different 
so this is deprecated.
+ *
+ * It would be better to write this anew, and accept a breakage of semantics 
-- or use SPEpToQst (which we do)
+ */
+@Deprecated
+public class ShorthandProcessorExprParser {
+
+    private final String template;
+    boolean finalMatchRaw = false;
+
+    public ShorthandProcessorExprParser(String template) {
+        this.template = template;
+    }
+
+    public Maybe<Map<String,Object>> process(String input) {
+        return new ShorthandProcessorAttempt(this, input).call();
+    }
+
+    /** whether the last match should preserve quotes and spaces; default 
false */
+    public ShorthandProcessorExprParser withFinalMatchRaw(boolean 
finalMatchRaw) {
+        this.finalMatchRaw = finalMatchRaw;
+        return this;
+    }
+
+    public static ExpressionParser tokenizer() {
+        return ExpressionParser
+                .newDefaultAllowingUnquotedAndSplittingOnWhitespace()
+                .includeGroupingBracketsAtUsualPlaces()
+
+                // including = means we can handle let x=1 !
+                .includeAllowedTopLevelTransition(new 
CharactersCollectingParseMode("equals", '='))
+
+                // whitespace in square brackets might be interesting -- 
though i don't think it is
+//                
.includeAllowedSubmodeTransition(ExpressionParser.SQUARE_BRACKET, 
ExpressionParser.WHITESPACE)
+                ;
+    }
+    private static Maybe<List<ParseNodeOrValue>> tokenized(String x) {
+        return tokenizer().parseEverything(x);
+    }
+
+    static class ShorthandProcessorAttempt {
+        private final List<ParseNodeOrValue> templateTokensOriginal;
+        private final String inputOriginal2;
+        private final String template;
+        private final ShorthandProcessorExprParser options;
+        int optionalDepth = 0;
+        int optionalSkippingInput = 0;
+        Map<String, Object> result;
+        Consumer<String> valueUpdater;
+
+        ShorthandProcessorAttempt(ShorthandProcessorExprParser proc, String 
input) {
+            this.template = proc.template;
+            this.options = proc;
+            this.templateTokensOriginal = tokenized(template).get();
+            this.inputOriginal2 = input;
+        }
+
+        public synchronized Maybe<Map<String,Object>> call() {
+            if (result != null) {
+                throw new IllegalStateException("Only allowed to use once");
+            }
+            result = MutableMap.of();
+            List<ParseNodeOrValue> templateTokens = 
MutableList.copyOf(templateTokensOriginal);
+            List<ParseNodeOrValue> inputTokens = 
tokenized(inputOriginal2).get();
+            Maybe<Boolean> error = doCall(templateTokens, inputTokens, 0);
+            if (error.isAbsent()) return Maybe.Absent.castAbsent(error);
+            if (!error.get()) return Maybe.absent("Template could not be 
matched."); // shouldn't happen
+            chompWhitespace(inputTokens);
+            if (!inputTokens.isEmpty()) {
+                if (valueUpdater!=null) {
+                    valueUpdater.accept(getRemainderPossiblyRaw(inputTokens));
+                } else {
+                    // shouldn't come here
+                    return Maybe.absent("Input has trailing characters after 
template is matched: '" + inputTokens.stream().map(x -> 
x.getSource()).collect(Collectors.joining()) + "'");
+                }
+            }
+            return Maybe.of(result);
+        }
+
+        private static final Object EMPTY = "empty";
+
+        protected Maybe<Boolean> doCall(List<ParseNodeOrValue> templateTokens, 
List<ParseNodeOrValue> inputTokens, int depth) {
+//            boolean isEndOfOptional = false;
+            outer: while (true) {
+//                if (isEndOfOptional) {
+//                    if (optionalDepth <= 0) {
+//                        throw new IllegalStateException("Unexpected optional 
block closure");
+//                    }
+//                    optionalDepth--;
+//                    if (optionalSkippingInput>0) {
+//                        // we were in a block where we skipped something 
optional because it couldn't be matched; outer parser is now canonical,
+//                        // and should stop skipping
+//                        return Maybe.of(true);
+//                    }
+//                    isEndOfOptional = false;
+//                }
+
+                if (templateTokens.isEmpty()) {
+////                    if (Strings.isNonBlank(inputRemaining) && 
valueUpdater==null) {
+////                        return Maybe.absent("Input has trailing characters 
after template is matched: '" + inputRemaining + "'");
+////                    }
+//                    if (optionalDepth>0)
+//                        return Maybe.absent("Mismatched optional marker in 
template");
+                    return Maybe.of(true);
+                }
+                ParseNodeOrValue tnv = templateTokens.remove(0);
+
+                if (tnv.isParseNodeMode(ExpressionParser.SQUARE_BRACKET)) {
+                    List<ParseNodeOrValue> tt = ((ParseNode) 
tnv).getContents();
+                    String optionalPresentVar = null;
+                    chompWhitespace(tt);
+                    if (!tt.isEmpty() && 
tt.get(0).getParseNodeMode().equals(ParseValue.MODE) &&
+                            ((String)tt.get(0).getContents()).startsWith("?")) 
{
+                        ParseNodeOrValue vt = tt.remove(0);
+                        ParseNodeOrValue vt2 = 
tt.stream().findFirst().orElse(null);
+                        if (vt2!=null && 
vt2.isParseNodeMode(ExpressionParser.INTERPOLATED)) {
+                            ParseValue vt2c = ((ParseNode) 
vt2).getOnlyContent()
+                                    .mapMaybe(x -> x instanceof ParseValue ? 
Maybe.of((ParseValue)x) : Maybe.absent())
+                                    .orThrow(() -> new 
IllegalStateException("? after [ should be followed by optional presence 
variable using syntax '?${var}', not '" + vt2.getSource() + "'"));
+                            optionalPresentVar = vt2c.getContents();
+                            tt.remove(0);
+                        } else {
+                            throw new IllegalStateException("? after [ should 
indicate optional presence variable using syntax '?${var}', not 
'"+vt.getSource()+"'");
+                        }
+                    }
+                    Maybe<Boolean> cr;
+                    if (optionalSkippingInput<=0) {
+                        // make a deep copy so that valueUpdater writes get 
replayed
+                        Map<String, Object> backupResult = (Map) 
CollectionMerger.builder().deep(true).build().merge(MutableMap.of(), result);
+                        Consumer<String> backupValueUpdater = valueUpdater;
+                        List<ParseNodeOrValue> backupInputRemaining = 
MutableList.copyOf(inputTokens);
+                        List<ParseNodeOrValue> backupTT = tt;
+                        int oldDepth = optionalDepth;
+                        int oldSkippingDepth = optionalSkippingInput;
+
+                        optionalDepth++;
+                        tt = MutableList.copyOf(tt);
+                        tt.addAll(templateTokens);
+                        // this nested call handles the bracketed nodes, and 
everything after, to make sure the inclusion of the optional works to the end
+                        cr = doCall(tt, inputTokens, depth+1);
+                        if (cr.isPresent()) {
+                            if (cr.get()) {
+                                // succeeded
+                                templateTokens.clear();  // because the 
subcall handled them
+                                if (optionalPresentVar != null) 
result.put(optionalPresentVar, true);
+                                continue;
+                            } else {
+                                // go into next block, we'll re-run, but with 
optional skipping turned on
+                            }
+                        }
+                        // restore
+                        result = backupResult;
+                        valueUpdater = backupValueUpdater;
+                        if (optionalPresentVar!=null) 
result.put(optionalPresentVar, false);
+                        inputTokens.clear();
+                        inputTokens.addAll(backupInputRemaining);
+                        tt = backupTT;
+                        optionalDepth = oldDepth;
+                        optionalSkippingInput = oldSkippingDepth;
+
+                        optionalSkippingInput++;
+                        optionalDepth++;
+//                        tt.add(0, tnv); // put our bracket back on the head 
so we come back in to this block
+                        cr = doCall(tt, inputTokens, depth+1);
+                        if (cr.isPresent()) {
+                            optionalSkippingInput--;
+                            continue;
+                        }
+                    } else {
+                        if (optionalPresentVar!=null) {
+                            result.put(optionalPresentVar, false);
+                            valueUpdater = null;
+                        }
+                        optionalDepth++;
+                        cr = doCall(tt, inputTokens, depth+1);
+                        if (cr.isPresent()) {
+                            continue;
+                        }
+                    }
+                    // failed
+                    return cr;
+                }
+
+                if (ExpressionParser.isQuotedExpressionNode(tnv)) {
+                    if (optionalSkippingInput>0) continue;
+
+                    Maybe<String> literalM = getSingleStringContents(tnv, 
"inside shorthand template quote");
+                    if (literalM.isAbsent()) return Maybe.castAbsent(literalM);
+                    String literal = Strings.trimStart(literalM.get());
+//                    boolean foundSomething = false;
+                    valueUpdater: do {
+                        chompWhitespace(inputTokens);
+                        literal: while (!inputTokens.isEmpty()) {
+                            if (!literal.isEmpty()) {
+                                Maybe<String> nextInputM = 
getSingleStringContents(inputTokens.stream().findFirst().orElse(null), 
"matching literal '" + literal + "'");
+                                String nextInput = 
Strings.trimStart(nextInputM.orNull());
+                                if (nextInput == null) {
+                                    break; // shouldn't happen, but if so, 
fall through to error below
+                                }
+                                if (literal.startsWith(nextInput)) {
+                                    literal = Strings.removeFromStart(literal, 
nextInput.trim());
+                                    inputTokens.remove(0);
+                                    chompWhitespace(inputTokens);
+                                    literal = Strings.trimStart(literal);
+                                    continue literal;
+                                }
+                                if (nextInput.startsWith(literal)) {
+                                    String putBackOnInput = 
nextInput.substring(literal.length());
+                                    literal = "";
+                                    inputTokens.remove(0);
+                                    if (!putBackOnInput.isEmpty()) 
inputTokens.add(0, new ParseValue(putBackOnInput));
+                                    else chompWhitespace(inputTokens);
+                                    // go into next block
+                                }
+                            }
+                            if (literal.isEmpty()) {
+                                // literal found
+                                continue outer;
+                            }
+                            break;
+                        }
+                        if (inputTokens.isEmpty()) {
+                            return Maybe.absent("Literal '" + literalM.get() + 
"' expected, when end of input reached");
+                        }
+                        if (valueUpdater!=null) {
+                            if (inputTokens.isEmpty()) return 
Maybe.absent("Literal '"+literal+"' expected, when end of input tokens 
reached");
+                            Pair<String,Boolean> value = 
getNextInputTokenUpToPossibleExpectedLiteral(inputTokens, templateTokens, 
literal, false);
+                            if (value.getRight()) 
valueUpdater.accept(value.getLeft());
+                            // always continue, until we see the literal
+//                            if (!value.getRight()) {
+//                                return Maybe.of(foundSomething);
+//                            }
+//                            foundSomething = true;
+                            continue valueUpdater;
+                        }
+                        return Maybe.absent("Literal '"+literal+"' expected, 
when encountered '"+inputTokens+"'");
+                    } while (true);
+                }
+
+                if (tnv.isParseNodeMode(ExpressionParser.INTERPOLATED)) {
+                    if (optionalSkippingInput>0) continue;
+
+                    Maybe<String> varNameM = getSingleStringContents(tnv, "in 
template interpolated variable definition");
+                    if (varNameM.isAbsent()) return Maybe.castAbsent(varNameM);
+                    String varName = varNameM.get();
+
+                    if (inputTokens.isEmpty()) return Maybe.absent("End of 
input when looking for variable "+varName);
+
+                    chompWhitespace(inputTokens);
+                    String value;
+                    if (templateTokens.isEmpty()) {
+                        // last word (whether optional or not) takes everything
+                        value = getRemainderPossiblyRaw(inputTokens);
+                        inputTokens.clear();
+
+                    } else {
+                        Pair<String,Boolean> valueM = 
getNextInputTokenUpToPossibleExpectedLiteral(inputTokens, templateTokens, null, 
false);
+                        if (!valueM.getRight()) {
+                            // if we didn't find an expression, bail out
+                            return Maybe.absent("Did not find expression prior 
to expected literal");
+                        }
+                        value = valueM.getLeft();
+                    }
+                    boolean dotsMultipleWordMatch = varName.endsWith("...");
+                    if (dotsMultipleWordMatch) varName = 
Strings.removeFromEnd(varName, "...");
+                    String keys[] = varName.split("\\.");
+                    final String tt = varName;
+                    valueUpdater = v2 -> {
+                        Map target = result;
+                        for (int i=0; i<keys.length; i++) {
+                            if 
(!Pattern.compile("[A-Za-z0-9_-]+").matcher(keys[i]).matches()) {
+                                throw new IllegalArgumentException("Invalid 
variable '"+tt+"'");
+                            }
+                            if (i == keys.length - 1) {
+                                target.compute(keys[i], (k, v) -> v == null ? 
v2 : v + " " + v2);
+                            } else {
+                                // need to make sure we have a map or null
+                                target = (Map) target.compute(keys[i], (k, v) 
-> {
+                                    if (v == null) return MutableMap.of();
+                                    if (v instanceof Map) return v;
+                                    return Maybe.absent("Cannot process 
shorthand for " + Arrays.asList(keys) + " because entry '" + k + "' is not a 
map (" + v + ")");
+                                });
+                            }
+                        }
+                    };
+                    valueUpdater.accept(value);
+                    if (!dotsMultipleWordMatch && !templateTokens.isEmpty()) 
valueUpdater = null;
+                    continue;
+                }
+
+                if (tnv.isParseNodeMode(ExpressionParser.WHITESPACE)) {
+                    chompWhitespace(templateTokens);
+                    continue;
+                }
+
+                // unexpected token
+                return Maybe.absent("Unexpected token in shorthand pattern 
'"+template+"' at "+tnv+", followed by "+templateTokens);
+            }
+        }
+
+        private static Maybe<String> getSingleStringContents(ParseNodeOrValue 
tnv, String where) {
+            if (tnv==null)
+                Maybe.absent(()-> new IllegalArgumentException("No remaining 
tokens "+where));
+            if (tnv instanceof ParseValue) return 
Maybe.of(((ParseValue)tnv).getContents());
+            Maybe<ParseNodeOrValue> c = ((ParseNode) tnv).getOnlyContent();
+            if (c.isAbsent()) Maybe.absent(()-> new 
IllegalArgumentException("Expected single string "+where));
+            return getSingleStringContents(c.get(), where);
+        }
+
+        private static String getEscapedForInsideQuotedString(ParseNodeOrValue 
tnv, String where) {
+            if (tnv==null) throw new IllegalArgumentException("No remaining 
tokens "+where);
+            if (tnv instanceof ParseValue) return 
((ParseValue)tnv).getContents();
+            if (ExpressionParser.isQuotedExpressionNode(tnv))
+                return ((ParseNode) tnv).getContents().stream().map(x -> 
getEscapedForInsideQuotedString(x, where)).collect(Collectors.joining());
+            else
+                return ((ParseNode) tnv).getContents().stream().map(x -> 
x.getSource()).collect(Collectors.joining());
+        }
+
+        private void chompWhitespace(List<ParseNodeOrValue> tt) {
+            while (!tt.isEmpty() && 
tt.get(0).isParseNodeMode(ExpressionParser.WHITESPACE)) tt.remove(0);
+        }
+
+        private String getRemainderPossiblyRaw(List<ParseNodeOrValue> 
remainder) {
+            if (options.finalMatchRaw) {
+                return 
remainder.stream().map(ParseNodeOrValue::getSource).collect(Collectors.joining());
+            } else {
+                return remainder.stream().map(x -> {
+                    if (x instanceof ParseValue) return 
((ParseValue)x).getContents();
+                    return 
getRemainderPossiblyRaw(((ParseNode)x).getContents());
+                }).collect(Collectors.joining());
+            }
+        }
+
+        private Pair<String,Boolean> 
getNextInputTokenUpToPossibleExpectedLiteral(List<ParseNodeOrValue> 
inputTokens, List<ParseNodeOrValue> templateTokens, String nextLiteral, boolean 
removeLiteralFromInput) {
+            String value;
+            ParseNodeOrValue nextInput = inputTokens.iterator().next();
+            Boolean foundLiteral;
+            Boolean foundContentsPriorToLiteral;
+            if (ExpressionParser.isQuotedExpressionNode(nextInput)) {
+                // quoted expressions in input won't match template literals
+                value = getEscapedForInsideQuotedString(nextInput, "reading 
quoted input");
+                inputTokens.remove(0);
+                foundLiteral = false;
+                foundContentsPriorToLiteral = true;
+            } else {
+                boolean isLiteralExpected;
+                if (nextLiteral==null) {
+                    // input not quoted, no literal supplied; if next template 
token is literal, look for it
+                    chompWhitespace(templateTokens);
+                    ParseNodeOrValue nextTT = templateTokens.isEmpty() ? null 
: templateTokens.get(0);
+                    if (ExpressionParser.isQuotedExpressionNode(nextTT)) {
+                        Maybe<String> nextLiteralM = 
getSingleStringContents(nextTT, "identifying expected literal");
+                        isLiteralExpected = nextLiteralM.isPresent();
+                        nextLiteral = nextLiteralM.orNull();
+                    } else {
+                        isLiteralExpected = false;
+                    }
+                } else {
+                    isLiteralExpected = true;
+                }
+                if (isLiteralExpected) {
+                    value = "";
+                    while (true) {
+                        Maybe<String> vsm = getSingleStringContents(nextInput, 
"looking for literal '" + nextLiteral + "'");
+                        if (vsm.isAbsent()) {
+                            foundLiteral = false;
+                            break;
+                        }
+                        String vs = vsm.get();
+                        int nli = (" "+vs+" ").indexOf(nextLiteral); // wrap 
the token we got in spaces in case there is a quoted space in the next literal
+                        if (nli >= 0) {
+                            // literal found in unquoted string, eg "foo=bar" 
when literal is =
+                            if (nli>0) nli--; // walk back the space
+                            value += vs.substring(0, nli);
+                            value = Strings.trimEnd(value);
+                            if (removeLiteralFromInput) nli += 
nextLiteral.length();
+                            String putBackOnInput = vs.substring(nli);
+                            inputTokens.remove(0);
+                            if (!putBackOnInput.isEmpty()) inputTokens.add(0, 
new ParseValue(putBackOnInput));
+                            foundLiteral = true;
+                            break;
+                        } else {
+                            nli = (" "+value + vs).indexOf(nextLiteral);
+                            if (nli >= 0) {
+                                // found in concatenation
+                                if (nli>0) nli--; // walk back the space
+                                String putBackOnInput = (value + 
vs).substring(nli);
+                                value = (value + vs).substring(0, nli);
+                                value = Strings.trimEnd(value);
+                                inputTokens.remove(0);
+                                if (removeLiteralFromInput) putBackOnInput = 
putBackOnInput.substring(nextLiteral.length());
+                                if (!putBackOnInput.isEmpty()) 
inputTokens.add(0, new ParseValue(putBackOnInput));
+                                foundLiteral = true;
+                                break;
+                            } else {
+                                // literal not found
+                                value += vs;
+                                inputTokens.remove(0);
+                                if (inputTokens.isEmpty()) {
+                                    foundLiteral = false;
+                                    break;
+                                } else {
+                                    nextInput = inputTokens.iterator().next();
+                                    continue;
+                                }
+                            }
+                        }
+                    }
+                    foundContentsPriorToLiteral = Strings.isNonEmpty(value);
+                } else {
+                    // next is not a literal, so take the next, and caller 
will probably recurse, taking the entire rest as the value
+                    value = getSingleStringContents(nextInput, "taking 
remainder when no literal").or("");
+                    foundLiteral = false;
+                    foundContentsPriorToLiteral = true;
+                    inputTokens.remove(0);
+                }
+            }
+            return Pair.of(value, foundContentsPriorToLiteral);
+        }
+    }
+
+}
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessor.java 
b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorQst.java
similarity index 96%
copy from 
core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessor.java
copy to 
core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorQst.java
index 32ba2f835c..99da682e11 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessor.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorQst.java
@@ -18,6 +18,12 @@
  */
 package org.apache.brooklyn.core.workflow;
 
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.regex.Pattern;
+
 import org.apache.brooklyn.util.collections.CollectionMerger;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
@@ -25,14 +31,11 @@ import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.text.QuotedStringTokenizer;
 import org.apache.brooklyn.util.text.Strings;
 
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
 /**
+ * This is the original processor which relied purely on the QST.
+ *
+ * ===
+ *
  * Accepts a shorthand template, and converts it to a map of values,
  * e.g. given template "[ ?${type_set} ${sensor.type} ] ${sensor.name} \"=\" 
${value}"
  * and input "integer foo=3", this will return
@@ -55,45 +58,45 @@ import java.util.stream.Collectors;
  * * keywords must follow the order indicated
  * * exclusive alternatives are disallowed by code subsequently or checked 
separately (eg Transform)
  */
-public class ShorthandProcessor {
+public class ShorthandProcessorQst {
 
     private final String template;
     boolean finalMatchRaw = false;
     boolean failOnMismatch = true;
 
-    public ShorthandProcessor(String template) {
+    public ShorthandProcessorQst(String template) {
         this.template = template;
     }
 
     public Maybe<Map<String,Object>> process(String input) {
-        return new ShorthandProcessorAttempt(this, input).call();
+        return new ShorthandProcessorQstAttempt(this, input).call();
     }
 
     /** whether the last match should preserve quotes and spaces; default 
false */
-    public ShorthandProcessor withFinalMatchRaw(boolean finalMatchRaw) {
+    public ShorthandProcessorQst withFinalMatchRaw(boolean finalMatchRaw) {
         this.finalMatchRaw = finalMatchRaw;
         return this;
     }
 
     /** whether to fail on mismatched quotes in the input, default true */
-    public ShorthandProcessor withFailOnMismatch(boolean failOnMismatch) {
+    public ShorthandProcessorQst withFailOnMismatch(boolean failOnMismatch) {
         this.failOnMismatch = failOnMismatch;
         return this;
     }
 
-    static class ShorthandProcessorAttempt {
+    static class ShorthandProcessorQstAttempt {
         private final List<String> templateTokens;
         private final String inputOriginal;
         private final QuotedStringTokenizer qst;
         private final String template;
-        private final ShorthandProcessor options;
+        private final ShorthandProcessorQst options;
         int optionalDepth = 0;
         int optionalSkippingInput = 0;
         private String inputRemaining;
         Map<String, Object> result;
         Consumer<String> valueUpdater;
 
-        ShorthandProcessorAttempt(ShorthandProcessor proc, String input) {
+        ShorthandProcessorQstAttempt(ShorthandProcessorQst proc, String input) 
{
             this.template = proc.template;
             this.options = proc;
             this.qst = qst(template);
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepDefinition.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepDefinition.java
index f51d962208..72f04fc002 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepDefinition.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepDefinition.java
@@ -23,7 +23,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.Preconditions;
-import com.google.common.reflect.TypeToken;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
 import org.apache.brooklyn.api.mgmt.Task;
 import org.apache.brooklyn.config.ConfigKey;
@@ -159,11 +158,11 @@ public abstract class WorkflowStepDefinition {
     abstract public void populateFromShorthand(String value);
 
     protected void populateFromShorthandTemplate(String template, String 
value) {
-        populateFromShorthandTemplate(template, value, false, true, true);
+        populateFromShorthandTemplate(template, value, false, true);
     }
 
-    protected Map<String, Object> populateFromShorthandTemplate(String 
template, String value, boolean finalMatchRaw, boolean failOnMismatch, boolean 
failOnError) {
-        Maybe<Map<String, Object>> result = new 
ShorthandProcessor(template).withFinalMatchRaw(finalMatchRaw).withFailOnMismatch(failOnMismatch).process(value);
+    protected Map<String, Object> populateFromShorthandTemplate(String 
template, String value, boolean finalMatchRaw, boolean failOnError) {
+        Maybe<Map<String, Object>> result = new 
ShorthandProcessor(template).withFinalMatchRaw(finalMatchRaw).process(value);
         if (result.isAbsent()) {
             if (failOnError) throw new IllegalArgumentException("Invalid 
shorthand expression: '" + value + "'", Maybe.Absent.getException(result));
             return null;
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/UpdateChildrenWorkflowStep.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/UpdateChildrenWorkflowStep.java
index 27ed3d0c68..0db1cf6a88 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/UpdateChildrenWorkflowStep.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/UpdateChildrenWorkflowStep.java
@@ -241,7 +241,11 @@ public class UpdateChildrenWorkflowStep extends 
WorkflowStepDefinition implement
             stepState.parent = parentId!=null ? 
WorkflowStepResolution.findEntity(context, parentId).get() : 
context.getEntity();
 
             stepState.identifier_expression = 
TypeCoercions.coerce(context.getInputRaw(IDENTIFIER_EXRPESSION.getName()), 
String.class);
-            stepState.items = context.getInput(ITEMS);
+            try {
+                stepState.items = context.getInput(ITEMS);
+            } catch (Exception e) {
+                throw new IllegalStateException("Cannot resolve items as a 
list", e);
+            }
             if (stepState.items==null) throw new IllegalStateException("Items 
cannot be null");
             setStepState(context, stepState);
         } else {
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 e809e76431..f363261e1f 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
@@ -74,7 +74,7 @@ public class SetVariableWorkflowStep extends 
WorkflowStepDefinition {
 
     @Override
     public void populateFromShorthand(String expression) {
-        Map<String, Object> newInput = 
populateFromShorthandTemplate(SHORTHAND, expression, true, true, true);
+        Map<String, Object> newInput = 
populateFromShorthandTemplate(SHORTHAND, expression, true, true);
         if (newInput.get(VALUE.getName())!=null && 
input.get(INTERPOLATION_MODE.getName())==null) {
             setInput(INTERPOLATION_MODE, InterpolationMode.WORDS);
         }
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformReplace.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformReplace.java
index 16f8b491e3..f073cbdec3 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformReplace.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformReplace.java
@@ -19,10 +19,8 @@
 package org.apache.brooklyn.core.workflow.steps.variables;
 
 import org.apache.brooklyn.core.workflow.ShorthandProcessor;
-import org.apache.brooklyn.core.workflow.WorkflowExecutionContext;
 import org.apache.brooklyn.util.guava.Maybe;
 
-import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
 
@@ -40,7 +38,6 @@ public class TransformReplace extends 
WorkflowTransformDefault {
     protected void initCheckingDefinition() {
         Maybe<Map<String, Object>> maybeResult = new 
ShorthandProcessor(SHORTHAND)
                 .withFinalMatchRaw(false)
-                .withFailOnMismatch(true)
                 .process(transformDef);
 
         if (maybeResult.isPresent()) {
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSplit.java
 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSplit.java
index d5b17793bd..1f9b3bba5d 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSplit.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSplit.java
@@ -18,22 +18,14 @@
  */
 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.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 
-import com.google.common.base.Splitter;
 import org.apache.brooklyn.core.workflow.ShorthandProcessor;
-import org.apache.brooklyn.core.workflow.WorkflowExpressionResolution;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.guava.Maybe;
-import org.apache.brooklyn.util.javalang.Boxing;
-import org.apache.brooklyn.util.text.Strings;
-import org.apache.commons.lang3.StringUtils;
 
 public class TransformSplit extends WorkflowTransformDefault {
 
@@ -47,7 +39,6 @@ public class TransformSplit extends WorkflowTransformDefault {
     protected void initCheckingDefinition() {
         Maybe<Map<String, Object>> maybeResult = new 
ShorthandProcessor(SHORTHAND)
                 .withFinalMatchRaw(false)
-                .withFailOnMismatch(true)
                 .process(transformDef);
 
         if (maybeResult.isPresent()) {
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 fc59c55edb..7f7c2f8254 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
@@ -86,7 +86,7 @@ public class TransformVariableWorkflowStep extends 
WorkflowStepDefinition {
     public void populateFromShorthand(String expression) {
         Map<String, Object> match = null;
         for (int i=0; i<SHORTHAND_OPTIONS.length && match==null; i++)
-            match = populateFromShorthandTemplate(SHORTHAND_OPTIONS[i], 
expression, false, true, false);
+            match = populateFromShorthandTemplate(SHORTHAND_OPTIONS[i], 
expression, false, false);
 
         if (match==null && Strings.isNonBlank(expression)) {
             throw new IllegalArgumentException("Invalid shorthand expression 
for transform: '" + expression + "'");
diff --git 
a/core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorTest.java
 
b/core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorEpToQstTest.java
similarity index 80%
copy from 
core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorTest.java
copy to 
core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorEpToQstTest.java
index bb52a32ade..bfe930890f 100644
--- 
a/core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorTest.java
+++ 
b/core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorEpToQstTest.java
@@ -18,17 +18,15 @@
  */
 package org.apache.brooklyn.core.workflow;
 
+import java.util.Map;
+import java.util.function.Consumer;
+
 import org.apache.brooklyn.core.test.BrooklynMgmtUnitTestSupport;
-import 
org.apache.brooklyn.core.workflow.steps.variables.TransformVariableWorkflowStep;
 import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.testng.annotations.Test;
 
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-public class ShorthandProcessorTest extends BrooklynMgmtUnitTestSupport {
+public class ShorthandProcessorEpToQstTest extends BrooklynMgmtUnitTestSupport 
{
 
     void assertShorthandOfGives(String template, String input, 
Map<String,Object> expected) {
         Asserts.assertEquals(new 
ShorthandProcessor(template).process(input).get(), expected);
@@ -72,8 +70,20 @@ public class ShorthandProcessorTest extends 
BrooklynMgmtUnitTestSupport {
         // if you want quotes, you have to wrap them in quotes
         assertShorthandOfGives("${x} \" is \" ${y}", "\"this is b\" is \"\\\"c 
is is quoted\\\"\"", MutableMap.of("x", "this is b", "y", "\"c is is 
quoted\""));
         // and only quotes at word end are considered, per below
-        assertShorthandOfGivesError("${x} \" is \" ${y}", "\"this is b\" is 
\"\\\"c is \"is  quoted\"\\\"\"", MutableMap.of("x", "this is b", "y", "\"c is 
\"is  quoted\"\""));
-        assertShorthandOfGivesError("${x} \" is \" ${y}", "\"this is b\" is 
\"\\\"c is \"is  quoted\"\\\"\"  too", MutableMap.of("x", "this is b", "y", 
"\"\\\"c is \"is  quoted\"\\\"\"  too"));
+
+        // but unlike pure QST we recognize double quotes like everyone else 
now, so the following behaviour is changed
+        if (ShorthandProcessorEpToQst.TRY_HARDER_FOR_QST_COMPATIBILITY) {
+            assertShorthandOfGivesError("${x} \" is \" ${y}", "\"this is b\" 
is \"\\\"c is \"is  quoted\"\\\"\"",
+                    MutableMap.of("x", "this is b", "y",
+//                            "\"c is \"is  quoted\"\""
+                            "\"\\\"c is \"is  quoted\"\\\"\""
+                    ));
+            assertShorthandOfGivesError("${x} \" is \" ${y}", "\"this is b\" 
is \"\\\"c is \"is  quoted\"\\\"\"  too",
+                    MutableMap.of("x", "this is b", "y", "\"\\\"c is \"is  
quoted\"\\\"\"  too"));
+        }
+        // and this is new
+        assertShorthandOfGives("${x} \" is \" ${y}", "\"this is b\" is \"\\\"c 
is \\\"is  quoted\"", MutableMap.of("x", "this is b", "y", "\"c is \"is  
quoted"));
+        assertShorthandOfGives("${x} \" is \" ${y}", "\"this is b\" is \"\\\"c 
is \\\"is  quoted\"  too", MutableMap.of("x", "this is b", "y", "\"\\\"c is 
\\\"is  quoted\"  too"));
 
         // preserve spaces in a word
         assertShorthandOfGives("${x}", "\"  sp a  ces \"", MutableMap.of("x", 
"  sp a  ces "));
@@ -86,8 +96,12 @@ public class ShorthandProcessorTest extends 
BrooklynMgmtUnitTestSupport {
         // a close quote must come at a word end to be considered
         // so this gives an error
         assertShorthandFailsWith("${x}", "\"c is  \"is", e -> 
Asserts.expectedFailureContainsIgnoreCase(e, "mismatched", "quot"));
-        // and this is treated as one quoted string
-        assertShorthandOfGivesError("${x}", "\"\\\"c  is \"is  
quoted\"\\\"\"", MutableMap.of("x", "\"c  is \"is  quoted\"\""));
+
+        // and this WAS treated as one quoted string
+        assertShorthandOfGivesError("${x}", "\"\\\"c  is \"is  quoted\"\\\"\"",
+//                MutableMap.of("x", "\"c  is \"is  quoted\"\""));
+                // but now two better, as two quoted strings
+                MutableMap.of("x", "\"\\\"c  is \"is  quoted\"\\\"\""));
     }
 
     @Test
@@ -162,4 +176,20 @@ the logic will take the one which matches the optional 
word1 but as minimally as
         assertShorthandOfGives("[ [ ${a} ] ${b} [ \"=\" ${c...} ] ]", "a b = 
c", MutableMap.of("a", "a", "b", "b", "c", "c"));
     }
 
+    @Test
+    public void testTokensVsWords() {
+        assertShorthandOfGives("[ [ ${type} ] ${var} [ \"=\" ${val...} ] ]",
+                "int foo['bar'] = baz", MutableMap.of("type", "int", "var", 
"foo['bar']", "val", "baz"));
+
+        // THIS IS STILL NOT DONE BECAUSE ${var} matches a "word";
+        // to support that, we need to delay setting a variable when followed 
by an optional literal until we are processing that literal;
+        // like "multi-match" mode but "single match" which calls back to the 
getUpToLiteral once the literal is known.
+        // with valueUpdater that shouldn't be too hard.
+//        assertShorthandOfGives("[ [ ${type} ] ${var} [ \"=\" ${val...} ] ]",
+//                "int foo['bar']=baz", MutableMap.of("type", "int",
+//                        "var", "foo['bar']", "val", "baz"
+//                        "var", "foo['bar']=baz"
+//                ));
+    }
+
 }
diff --git 
a/core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorTest.java
 
b/core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorQstTest.java
similarity index 91%
rename from 
core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorTest.java
rename to 
core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorQstTest.java
index bb52a32ade..dbb63d11d8 100644
--- 
a/core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorTest.java
+++ 
b/core/src/test/java/org/apache/brooklyn/core/workflow/ShorthandProcessorQstTest.java
@@ -18,34 +18,32 @@
  */
 package org.apache.brooklyn.core.workflow;
 
+import java.util.Map;
+import java.util.function.Consumer;
+
 import org.apache.brooklyn.core.test.BrooklynMgmtUnitTestSupport;
-import 
org.apache.brooklyn.core.workflow.steps.variables.TransformVariableWorkflowStep;
 import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.testng.annotations.Test;
 
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-public class ShorthandProcessorTest extends BrooklynMgmtUnitTestSupport {
+public class ShorthandProcessorQstTest extends BrooklynMgmtUnitTestSupport {
 
     void assertShorthandOfGives(String template, String input, 
Map<String,Object> expected) {
-        Asserts.assertEquals(new 
ShorthandProcessor(template).process(input).get(), expected);
+        Asserts.assertEquals(new 
ShorthandProcessorQst(template).process(input).get(), expected);
     }
 
     void assertShorthandOfGivesError(String template, String input, 
Map<String,Object> expected) {
-        Asserts.assertEquals(new 
ShorthandProcessor(template).withFailOnMismatch(false).process(input).get(), 
expected);
-        Asserts.assertFails(() -> new 
ShorthandProcessor(template).withFailOnMismatch(true).process(input).get());
+        Asserts.assertEquals(new 
ShorthandProcessorQst(template).withFailOnMismatch(false).process(input).get(), 
expected);
+        Asserts.assertFails(() -> new 
ShorthandProcessorQst(template).withFailOnMismatch(true).process(input).get());
     }
 
     void assertShorthandFinalMatchRawOfGives(String template, String input, 
Map<String,Object> expected) {
-        Asserts.assertEquals(new 
ShorthandProcessor(template).withFinalMatchRaw(true).process(input).get(), 
expected);
+        Asserts.assertEquals(new 
ShorthandProcessorQst(template).withFinalMatchRaw(true).process(input).get(), 
expected);
     }
 
     void assertShorthandFailsWith(String template, String input, 
Consumer<Exception> check) {
         try {
-            new ShorthandProcessor(template).process(input).get();
+            new ShorthandProcessorQst(template).process(input).get();
             Asserts.shouldHaveFailedPreviously();
         } catch (Exception e) {
             check.accept(e);
@@ -162,4 +160,10 @@ the logic will take the one which matches the optional 
word1 but as minimally as
         assertShorthandOfGives("[ [ ${a} ] ${b} [ \"=\" ${c...} ] ]", "a b = 
c", MutableMap.of("a", "a", "b", "b", "c", "c"));
     }
 
+    @Test
+    public void testTokensVsWords() {
+        assertShorthandOfGives("[ [ ${type} ] ${var} [ \"=\" ${val...} ] ]",
+                "int foo['bar'] = baz", MutableMap.of("type", "int", "var", 
"foo['bar']", "val", "baz"));
+    }
+
 }

Reply via email to