improve DSL parsing
diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java
index c42b799..7e65aa4 100644
--- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java
+++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java
@@ -29,6 +29,7 @@
import org.apache.brooklyn.camp.spi.resolve.PlanInterpreter.PlanInterpreterAdapter;
import org.apache.brooklyn.camp.spi.resolve.interpret.PlanInterpretationNode;
import org.apache.brooklyn.camp.spi.resolve.interpret.PlanInterpretationNode.Role;
+import org.apache.brooklyn.core.resolve.jackson.WrappedValue;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
@@ -159,6 +160,10 @@
return ((QuotedString)f).unwrapped();
}
+ if (f instanceof PropertyAccess) {
+ return ((PropertyAccess)f).getSelector();
+ }
+
throw new IllegalArgumentException("Unexpected element in parse tree: '"+f+"' (type "+(f!=null ? f.getClass() : null)+")");
}
@@ -201,6 +206,9 @@
}
try {
Object index = propAccess.getSelector();
+ while (index instanceof PropertyAccess) {
+ index = ((PropertyAccess)index).getSelector();
+ }
if (index instanceof QuotedString) {
index = ((QuotedString)index).unwrapped();
}
diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/DslParser.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/DslParser.java
index 846a1ef..db82823 100644
--- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/DslParser.java
+++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/DslParser.java
@@ -50,35 +50,32 @@
public Object next() {
int start = index;
- boolean isProperty = (index > 0 && (expression.charAt(index -1) == '[' )) || // for [x] syntax - indexes in lists
+ boolean isAlreadyInBracket = (index > 0 && (expression.charAt(index -1) == '[' )) || // for [x] syntax - indexes in lists
(index > 1 && (expression.charAt(index -1) == '"' && expression.charAt(index -2) == '[')) ; // for ["x"] syntax - string properties and map keys
- if (!isProperty) {
+ if (!isAlreadyInBracket) {
+ // feel like this block shouldn't be used; not sure it is
skipWhitespace();
if (index >= expression.length())
throw new IllegalStateException("Unexpected end of expression to parse, looking for content since position " + start);
if (expression.charAt(index) == '"') {
// assume a string, that is why for property syntax using [x] or ["x"], we skip this part
- int stringStart = index;
- index++;
- do {
- if (index >= expression.length())
- throw new IllegalStateException("Unexpected end of expression to parse, looking for close quote since position " + stringStart);
- char c = expression.charAt(index);
- if (c == '"') break;
- if (c == '\\') index++;
- index++;
- } while (true);
- index++;
- return new QuotedString(expression.substring(stringStart, index));
+ return nextAsQuotedString();
}
}
// not a string, must be a function (or chain thereof)
List<Object> result = new MutableList<Object>();
+ skipWhitespace();
int fnStart = index;
- isProperty = expression.charAt(fnStart) == '[';
- if(!isProperty) {
+ boolean isBracketed = expression.charAt(fnStart) == '[';
+ if (isBracketed) {
+ if (isAlreadyInBracket)
+ throw new IllegalStateException("Nested brackets not permitted, at position "+start);
+ // proceed to below
+
+ } else {
+ // non-bracketed property access, skip separators
do {
if (index >= expression.length())
break;
@@ -93,40 +90,74 @@
index++;
} while (true);
}
- String fn = isProperty ? "" : expression.substring(fnStart, index);
- if (fn.length()==0 && !isProperty)
- throw new IllegalStateException("Expected a function name or double-quoted string at position "+start);
- skipWhitespace();
- if (index < expression.length() && ( expression.charAt(index)=='(' || expression.charAt(index)=='[')) {
+ String fn = isBracketed ? "" : expression.substring(fnStart, index);
+ if (fn.length()==0 && !isBracketed)
+ throw new IllegalStateException("Expected a function name or double-quoted string at position "+start);
+
+ if (index < expression.length() && ( isBracketed || expression.charAt(index)=='(')) {
// collect arguments
int parenStart = index;
List<Object> args = new MutableList<>();
- if (expression.charAt(index)=='[') {
+ if (expression.charAt(index)=='[' && !isBracketed) {
if (!fn.isEmpty()) {
return new PropertyAccess(fn);
}
- if (expression.charAt(index +1)=='"') { index ++;} // for ["x"] syntax needs to be increased to extract the name of the property correctly
+ // not sure comes here
+ if (isBracketed)
+ throw new IllegalStateException("Nested brackets not permitted, at position "+start);
+ isBracketed = true;
}
- index ++;
+ index++;
+ boolean justAdded = false;
do {
skipWhitespace();
if (index >= expression.length())
throw new IllegalStateException("Unexpected end of arguments to function '"+fn+"', no close parenthesis matching character at position "+parenStart);
char c = expression.charAt(index);
- if(isProperty && c =='"') { index++; break; } // increasing the index for ["x"] syntax to account for the presence of the '"'
- if (c==')'|| c == ']') break;
- if (c==',') {
- if (args.isEmpty())
- throw new IllegalStateException("Invalid character at position"+index);
- index++;
- } else {
- if (!args.isEmpty() && !isProperty)
- throw new IllegalStateException("Expected , before position"+index);
+
+ if (c=='"') {
+ if (justAdded)
+ throw new IllegalStateException("Expected , before quoted string at position "+index);
+
+ QuotedString next = nextAsQuotedString();
+ if (isBracketed) args.add(next.unwrapped());
+ else args.add(next);
+ justAdded = true;
+ continue;
}
+
+ if (c == ']') {
+ if (!isBracketed)
+ throw new IllegalStateException("Mismatched close bracket at position "+index);
+ justAdded = false;
+ break;
+ }
+ if (c==')') {
+ justAdded = false;
+ break;
+ }
+ if (c==',') {
+ if (!justAdded)
+ throw new IllegalStateException("Invalid character at position "+index);
+ justAdded = false;
+ index++;
+ continue;
+ }
+
+ if (justAdded) // did have this but don't think need it?: && !isProperty)
+ throw new IllegalStateException("Expected , before position "+index);
+
args.add(next()); // call with first letter of the property
+ justAdded = true;
} while (true);
+ if (justAdded) {
+ if (isBracketed) {
+ throw new IllegalStateException("Expected ] at position " + index);
+ }
+ }
+
if (fn.isEmpty()) {
Object arg = args.get(0);
if (arg instanceof PropertyAccess) {
@@ -182,10 +213,27 @@
} else {
// previously we returned a null-arg function; now we treat as explicit property access,
// and places that need it as a function with arguments from a map key convert it on to new FunctionWithArgs(selector, null)
+
+ // ideally we'd have richer types here; sometimes it is property access, but sometimes just a wrapped non-quoted constant
return new PropertyAccess(fn);
}
}
+ private QuotedString nextAsQuotedString() {
+ int stringStart = index;
+ index++;
+ do {
+ if (index >= expression.length())
+ throw new IllegalStateException("Unexpected end of expression to parse, looking for close quote since position " + stringStart);
+ char c = expression.charAt(index);
+ if (c == '"') break;
+ if (c == '\\') index++;
+ index++;
+ } while (true);
+ index++;
+ return new QuotedString(expression.substring(stringStart, index));
+ }
+
private void skipWhitespace() {
while (index<expression.length() && Character.isWhitespace(expression.charAt(index)))
index++;
diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/PropertyAccess.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/PropertyAccess.java
index 30ff04d..51578e1 100644
--- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/PropertyAccess.java
+++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/PropertyAccess.java
@@ -18,6 +18,9 @@
*/
package org.apache.brooklyn.camp.brooklyn.spi.dsl.parse;
+import java.util.Objects;
+
+/** access a property in a map, position in a list, or otherwise a non-string (simple) expression being passed to a function */
public class PropertyAccess {
private final Object selector;
@@ -33,4 +36,15 @@
public String toString() {
return "PropertyAccess[" +selector + "]";
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PropertyAccess)) return false;
+ return Objects.equals(selector, ((PropertyAccess)obj).selector);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(selector);
+ }
}
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WorkflowExpressionsYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WorkflowExpressionsYamlTest.java
index 10c76f5..8c15898 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WorkflowExpressionsYamlTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WorkflowExpressionsYamlTest.java
@@ -367,8 +367,14 @@
app.config().set(ConfigKeys.newConfigKey(Object.class, "index"), 0);
Asserts.assertEquals( get.apply(MutableList.of(42), "$brooklyn:config(\"index\")"), 42);
+ Asserts.assertEquals( get.apply(MutableMap.of("k", MutableList.of(42)), "k"), MutableList.of(42));
Asserts.assertEquals( get.apply(MutableMap.of("k", MutableList.of(42)), ".k"), MutableList.of(42));
Asserts.assertEquals( get.apply(MutableMap.of("k", MutableList.of(42)), "[\"k\"][0]"), 42);
Asserts.assertEquals( get.apply(MutableMap.of("k", MutableList.of(42)), ".k[0]"), 42);
+
+ Asserts.assertEquals( get.apply(MutableMap.of("w-hyphen", 42), "w-hyphen"), 42);
+ Asserts.assertFailsWith(()->get.apply(MutableMap.of("w-hyphen", 42), ".w-hyphen"),
+ Asserts.expectedFailureContainsIgnoreCase("unexpected character", "position 30"));
+ Asserts.assertEquals( get.apply(MutableMap.of("w-hyphen", 42), "[\"w-hyphen\"]"), 42);
}
}
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslParseTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslParseTest.java
index 79d2b67..62e366c 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslParseTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslParseTest.java
@@ -24,11 +24,13 @@
import org.apache.brooklyn.camp.brooklyn.spi.dsl.parse.FunctionWithArgs;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.parse.PropertyAccess;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.parse.QuotedString;
+import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
import org.testng.annotations.Test;
import java.util.Arrays;
import java.util.List;
+import java.util.function.Function;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
@@ -55,11 +57,20 @@
}
public void testParseMultiArgMultiTypeFunction() {
- // TODO Parsing "f(\"x\", 1)" fails, because it interprets 1 as a function rather than a number. Is that expected?
Object fx = new DslParser("f(\"x\", \"y\")").parse();
fx = Iterables.getOnlyElement( (List<?>)fx );
assertEquals( ((FunctionWithArgs)fx).getFunction(), "f" );
assertEquals( ((FunctionWithArgs)fx).getArgs(), ImmutableList.of(new QuotedString("\"x\""), new QuotedString("\"y\"")));
+
+ fx = new DslParser("f(\"x\", 1)").parse();
+ fx = Iterables.getOnlyElement( (List<?>)fx );
+ assertEquals( ((FunctionWithArgs)fx).getFunction(), "f" );
+ assertEquals( ((FunctionWithArgs)fx).getArgs(), ImmutableList.of(new QuotedString("\"x\""), new PropertyAccess("1")));
+
+ fx = new DslParser("$brooklyn:formatString(\"%s-%s\", parent().attributeWhenReady(\"host.address\"), $brooklyn:attributeWhenReady(\"host.address\"))").parse();
+ fx = Iterables.getOnlyElement( (List<?>)fx );
+ Asserts.assertInstanceOf(fx, FunctionWithArgs.class);
+ Asserts.assertPasses((FunctionWithArgs)fx, fx0 -> Asserts.assertSize(fx0.getArgs(), 3));
}
@@ -161,6 +172,22 @@
}
@Test
+ public void testParseMapValueVariousWays() {
+ Function<String,Object> accessor = suffix -> ((List) new DslParser("$brooklyn:literal(\"ignored\")"+suffix).parse()).get(1);
+ Asserts.assertPasses(accessor.apply("[\"a-b\"]"), v -> Asserts.assertEquals( ((PropertyAccess)v).getSelector(), "a-b" ));
+ Asserts.assertPasses(accessor.apply(".[\"a-b\"]"), v -> Asserts.assertEquals( ((PropertyAccess)v).getSelector(), "a-b" ));
+
+ Asserts.assertPasses(accessor.apply(".a"), v -> Asserts.assertEquals( ((PropertyAccess)v).getSelector(), "a" ));
+ Asserts.assertPasses(accessor.apply("[\"a\"]"), v -> Asserts.assertEquals( ((PropertyAccess)v).getSelector(), "a" ));
+
+ Asserts.assertFailsWith(() -> accessor.apply("a"), Asserts.expectedFailureContainsIgnoreCase(
+ "unexpected character", " 28 ", ")a"));
+ Asserts.assertFailsWith(() -> accessor.apply(".a-b"), Asserts.expectedFailureContainsIgnoreCase(
+ "unexpected character", " 30 ", "a-b"));
+ }
+
+
+ @Test
public void testParseFunctionExplicit() {
List fx = (List) new DslParser("$brooklyn:function.foo()").parse();
assertEquals( ((FunctionWithArgs)fx.get(0)).getFunction(), "$brooklyn:function.foo" );
diff --git a/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java
index fedcd94..25459e8 100644
--- a/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java
+++ b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java
@@ -1188,6 +1188,18 @@
fail(Strings.isBlank(message) ? "Failed "+condition+": "+object : message);
}
+ public static <T> void assertPasses(T object, java.util.function.Consumer<T> condition) {
+ assertPasses(object, condition, null);
+ }
+ public static <T> void assertPasses(T object, java.util.function.Consumer<T> condition, String message) {
+ try {
+ condition.accept(object);
+ } catch (Throwable t) {
+ if (message!=null) throw new AssertionError(message, t);
+ else throw Exceptions.propagate(t);
+ }
+ }
+
public static void assertStringContains(String input, String phrase1ToContain, String ...optionalOtherPhrasesToContain) {
if (input==null) fail("Input is null.");
if (phrase1ToContain!=null) {