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