update shorthand processor to use expression processor
allows us to respect spaces and other things inside brackets
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 32ba2f8..0adbaff 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/ShorthandProcessorEpToQst.java b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorEpToQst.java
new file mode 100644
index 0000000..cb94600
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorEpToQst.java
@@ -0,0 +1,513 @@
+/*
+ * 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 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 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
+ * { 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)
+ */
+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 ShorthandProcessorEpToQst(String template) {
+ this.template = template;
+ }
+
+ public Maybe<Map<String,Object>> process(String input) {
+ return new ShorthandProcessorQstAttempt(this, input).call();
+ }
+
+ /** whether the last match should preserve quotes and spaces; default false */
+ public ShorthandProcessorEpToQst withFinalMatchRaw(boolean finalMatchRaw) {
+ this.finalMatchRaw = finalMatchRaw;
+ return this;
+ }
+
+ /** whether to fail on mismatched quotes in the input, default true */
+ public ShorthandProcessorEpToQst withFailOnMismatch(boolean failOnMismatch) {
+ this.failOnMismatch = failOnMismatch;
+ return this;
+ }
+
+ static class ShorthandProcessorQstAttempt {
+ private final List<String> templateTokens;
+ private final String inputOriginal;
+ private final QuotedStringTokenizer qst0;
+ private final String template;
+ private final ShorthandProcessorEpToQst options;
+ int optionalDepth = 0;
+ int optionalSkippingInput = 0;
+ private String inputRemaining;
+ Map<String, Object> result;
+ Consumer<String> valueUpdater;
+
+ ShorthandProcessorQstAttempt(ShorthandProcessorEpToQst proc, String input) {
+ this.template = proc.template;
+ this.options = proc;
+ this.qst0 = qst0(template);
+ this.templateTokens = qst0.remainderAsList(); // QST works fine for the template
+ this.inputOriginal = input;
+ }
+
+ private QuotedStringTokenizer qst0(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(inputRemaining));
+ } 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 (qst0.isQuoted(t)) {
+ if (optionalSkippingInput>0) continue;
+
+ 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);
+ 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) {
+ if (inputRemaining.isEmpty()) return Maybe.absent("Literal '"+literal+"' expected, when end of input tokens reached");
+ String value = getNextInputTokenUpToPossibleExpectedLiteral(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();
+ 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(inputRemaining);
+ inputRemaining = "";
+
+ } else {
+ value = getNextInputTokenUpToPossibleExpectedLiteral(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 getRemainderPossiblyRawQst(QuotedStringTokenizer qstInput) {
+ String value;
+ value = Strings.join(qstInput.remainderRaw(), "");
+ if (!options.finalMatchRaw) {
+ return qstInput.unwrapIfQuoted(value);
+ }
+ return value;
+ }
+
+ 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"
+ 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;
+ }
+
+ 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 0000000..ae76e31
--- /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/ShorthandProcessorQst.java b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorQst.java
new file mode 100644
index 0000000..99da682
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorQst.java
@@ -0,0 +1,357 @@
+/*
+ * 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 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;
+
+/**
+ * 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
+ * { 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)
+ */
+public class ShorthandProcessorQst {
+
+ private final String template;
+ boolean finalMatchRaw = false;
+ boolean failOnMismatch = true;
+
+ public ShorthandProcessorQst(String template) {
+ this.template = template;
+ }
+
+ public Maybe<Map<String,Object>> process(String input) {
+ return new ShorthandProcessorQstAttempt(this, input).call();
+ }
+
+ /** whether the last match should preserve quotes and spaces; default false */
+ public ShorthandProcessorQst withFinalMatchRaw(boolean finalMatchRaw) {
+ this.finalMatchRaw = finalMatchRaw;
+ return this;
+ }
+
+ /** whether to fail on mismatched quotes in the input, default true */
+ public ShorthandProcessorQst withFailOnMismatch(boolean failOnMismatch) {
+ this.failOnMismatch = failOnMismatch;
+ return this;
+ }
+
+ static class ShorthandProcessorQstAttempt {
+ private final List<String> templateTokens;
+ private final String inputOriginal;
+ private final QuotedStringTokenizer qst;
+ private final String template;
+ private final ShorthandProcessorQst options;
+ int optionalDepth = 0;
+ int optionalSkippingInput = 0;
+ private String inputRemaining;
+ Map<String, Object> result;
+ Consumer<String> valueUpdater;
+
+ ShorthandProcessorQstAttempt(ShorthandProcessorQst 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/WorkflowStepDefinition.java b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepDefinition.java
index f51d962..72f04fc 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.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 @@
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 27ed3d0..0db1cf6 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 @@
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 e809e76..f363261 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 @@
@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 16f8b49..f073cbd 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 @@
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 d5b1779..1f9b3bb 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 @@
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 fc59c55..7f7c2f8 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 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 79%
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 bb52a32..bfe9308 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 @@
// 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 @@
// 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 @@
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 bb52a32..dbb63d1 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 @@
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"));
+ }
+
}