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