blob: ae76e31659ebf5db967e0106c988427f4d579a00 [file] [log] [blame]
/*
* 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);
}
}
}