in core/src/main/java/org/apache/brooklyn/core/workflow/ShorthandProcessorExprParser.java [123:340]
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);
}
}