support dot property access in DSL, and transform get, join, split
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 9481512..c42b799 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
@@ -66,6 +66,9 @@
try {
currentNode.set(node);
Object parsedNode = new DslParser(expression).parse();
+ if (parsedNode instanceof PropertyAccess) {
+ parsedNode = new FunctionWithArgs(""+((PropertyAccess)parsedNode).getSelector(), null);
+ }
if ((parsedNode instanceof FunctionWithArgs) && ((FunctionWithArgs)parsedNode).getArgs()==null) {
if (node.getRoleInParent() == Role.MAP_KEY) {
node.setNewValue(parsedNode);
@@ -90,11 +93,16 @@
@Override
public boolean applyMapEntry(PlanInterpretationNode node, Map<Object, Object> mapIn, Map<Object, Object> mapOut,
PlanInterpretationNode key, PlanInterpretationNode value) {
- if (key.getNewValue() instanceof FunctionWithArgs) {
+ Object knv = key.getNewValue();
+ if (knv instanceof PropertyAccess) {
+ // when property access is used as a key, it is a function without args
+ knv = new FunctionWithArgs(""+((PropertyAccess)knv).getSelector(), null);
+ }
+ if (knv instanceof FunctionWithArgs) {
try {
currentNode.set(node);
- FunctionWithArgs f = (FunctionWithArgs) key.getNewValue();
+ FunctionWithArgs f = (FunctionWithArgs) knv;
if (f.getArgs()!=null)
throw new IllegalStateException("Invalid map key function "+f.getFunction()+"; should not have arguments if taking arguments from map");
diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
index 0468930..6dde3ff 100644
--- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
+++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
@@ -48,6 +48,7 @@
import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslAccessible;
import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent.Scope;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.parse.WorkflowTransformGet;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.config.external.ExternalConfigSupplier;
@@ -65,6 +66,7 @@
import org.apache.brooklyn.core.sensor.DependentConfiguration;
import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts;
import org.apache.brooklyn.core.typereg.RegisteredTypes;
+import org.apache.brooklyn.core.workflow.steps.variables.TransformVariableWorkflowStep;
import org.apache.brooklyn.util.collections.Jsonya;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
@@ -116,6 +118,7 @@
BrooklynJacksonSerializationUtils.JsonDeserializerForCommonBrooklynThings.BROOKLYN_PARSE_DSL_FUNCTION = DslUtils::parseBrooklynDsl;
BrooklynObjectsJsonMapper.DslToStringSerialization.BROOKLYN_DSL_INTERFACE = BrooklynDslDeferredSupplier.class;
registerSpecCoercionAdapter();
+ registerWorkflowTransforms();
INITIALIZED = true;
}
private static boolean INITIALIZED = false;
@@ -163,6 +166,9 @@
}
});
}
+ public static void registerWorkflowTransforms() {
+ TransformVariableWorkflowStep.registerTransformation("get", () -> new WorkflowTransformGet());
+ }
// Access specific entities
@@ -463,7 +469,7 @@
return new DslLiteral(expression);
}
- protected final static class DslLiteral extends BrooklynDslDeferredSupplier<Object> {
+ public final static class DslLiteral extends BrooklynDslDeferredSupplier<Object> {
final String literalString;
final String literalObjectJson;
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 71b4677..846a1ef 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
@@ -21,6 +21,8 @@
import java.util.Collection;
import java.util.List;
+import com.fasterxml.jackson.databind.annotation.JsonAppend;
+import com.fasterxml.jackson.databind.annotation.JsonAppend.Prop;
import org.apache.brooklyn.util.collections.MutableList;
public class DslParser {
@@ -83,8 +85,10 @@
char c = expression.charAt(index);
if (Character.isJavaIdentifierPart(c)) ;
// these chars also permitted
- else if (".:".indexOf(c)>=0) ;
- // other things e.g. whitespace, parentheses, etc, skip
+ else if (c==':') ;
+ else if (c=='.' && expression.substring(0, index).endsWith("function")) ; // function.xxx used for some static DslAccessible functions
+
+ // other things e.g. whitespace, parentheses, etc, beak on
else break;
index++;
} while (true);
@@ -98,7 +102,12 @@
// collect arguments
int parenStart = index;
List<Object> args = new MutableList<>();
- if (expression.charAt(index)=='[' && expression.charAt(index +1)=='"') { index ++;} // for ["x"] syntax needs to be increased to extract the name of the property correctly
+ if (expression.charAt(index)=='[') {
+ 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
+ }
index ++;
do {
skipWhitespace();
@@ -120,7 +129,9 @@
if (fn.isEmpty()) {
Object arg = args.get(0);
- if (arg instanceof FunctionWithArgs) {
+ if (arg instanceof PropertyAccess) {
+ result.add(arg);
+ } else if (arg instanceof FunctionWithArgs) {
FunctionWithArgs holder = (FunctionWithArgs) arg;
if(holder.getArgs() == null || holder.getArgs().isEmpty()) {
result.add(new PropertyAccess(holder.getFunction()));
@@ -135,37 +146,43 @@
}
index++;
- skipWhitespace();
- if (index >= expression.length())
- return result;
- char c = expression.charAt(index);
- if (c=='.') {
- // chained expression
- int chainStart = index;
- index++;
- Object next = next();
- if (next instanceof List) {
- result.addAll((Collection<? extends FunctionWithArgs>) next);
+ do {
+ skipWhitespace();
+ if (index >= expression.length())
return result;
+ char c = expression.charAt(index);
+ if (c == '.') {
+ // chained expression
+ int chainStart = index;
+ index++;
+ Object next = next();
+ if (next instanceof List) {
+ result.addAll((Collection<?>) next);
+ } else if (next instanceof PropertyAccess) {
+ result.add(next);
+ } else {
+ throw new IllegalStateException("Expected functions following position " + chainStart);
+ }
+ } else if (c == '[') {
+ int selectorsStart = index;
+ Object next = next();
+ if (next instanceof List) {
+ result.addAll((Collection<? extends PropertyAccess>) next);
+ skipWhitespace();
+ if (index >= expression.length())
+ return result;
+ } else {
+ throw new IllegalStateException("Expected property selectors following position " + selectorsStart);
+ }
} else {
- throw new IllegalStateException("Expected functions following position "+chainStart);
- }
- } else if (c=='[') {
- int selectorsStart = index;
- Object next = next();
- if (next instanceof List) {
- result.addAll((Collection<? extends PropertyAccess>) next);
+ // following word not something handled at this level; assume parent will handle (or throw) - e.g. a , or extra )
return result;
- } else {
- throw new IllegalStateException("Expected property selectors following position "+selectorsStart);
}
- } else {
- // following word not something handled at this level; assume parent will handle (or throw) - e.g. a , or extra )
- return result;
- }
+ } while (true);
} else {
- // it is just a word; return it with args as null;
- return new FunctionWithArgs(fn, null);
+ // 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)
+ return new PropertyAccess(fn);
}
}
diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/WorkflowTransformGet.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/WorkflowTransformGet.java
new file mode 100644
index 0000000..66ae498
--- /dev/null
+++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/WorkflowTransformGet.java
@@ -0,0 +1,78 @@
+/*
+ * 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.camp.brooklyn.spi.dsl.parse;
+
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslInterpreter;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.BrooklynDslCommon.DslLiteral;
+import org.apache.brooklyn.core.workflow.WorkflowExpressionResolution;
+import org.apache.brooklyn.core.workflow.steps.variables.WorkflowTransformDefault;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.core.text.TemplateProcessor;
+
+public class WorkflowTransformGet extends WorkflowTransformDefault {
+
+ String modifier;
+
+ @Override
+ protected void initCheckingDefinition() {
+ List<String> d = MutableList.copyOf(definition.subList(1, definition.size()));
+ if (d.size()>1) throw new IllegalArgumentException("Transform requires at most a single argument being the index or modifier to get");
+ if (!d.isEmpty()) modifier = d.get(0);
+ }
+
+ @Override
+ public Object apply(Object v) {
+ String modifierResolved = context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_RUNNING, modifier, String.class);
+ if (modifierResolved==null) {
+ if (v instanceof Supplier) return ((Supplier)v).get();
+ return v;
+ }
+ modifierResolved = modifierResolved.trim();
+ if (modifierResolved.startsWith("[") || modifierResolved.startsWith(".")) {
+ // already in modifier form
+ } else {
+ if (modifierResolved.contains(".") || modifierResolved.contains("[") || modifierResolved.contains(" ")) {
+ throw new IllegalArgumentException("Argument to 'get' must be a simple key (no spaces, dots, or brackets) or a bracketed string expression or start with an initial dot");
+ } else {
+ modifierResolved = "[\"" + modifierResolved+"\"]";
+ }
+ }
+
+ String m = "$brooklyn:literal(\"ignored\")" + modifierResolved;
+ List parse = (List) new DslParser(m).parse();
+ parse = parse.subList(1, parse.size());
+ // should be a bunch of property access
+ BrooklynDslInterpreter ip = new BrooklynDslInterpreter();
+ Object result = new DslLiteral(v);
+ for (Object p: parse) {
+ if (p instanceof PropertyAccess) {
+ result = ip.evaluateOn(result, (PropertyAccess) p);
+ } else {
+ throw new IllegalArgumentException("Invalid entry in 'get' transform argument; should be property access/modifiers");
+ }
+ }
+ return ((BrooklynDslDeferredSupplier) result).get();
+ }
+
+}
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 0b11eab..10c76f5 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
@@ -18,11 +18,13 @@
*/
package org.apache.brooklyn.camp.brooklyn;
+import com.google.common.base.Suppliers;
import com.google.common.collect.Iterables;
import com.google.common.reflect.TypeToken;
import org.apache.brooklyn.api.effector.Effector;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.BrooklynDslCommon;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.Entities;
@@ -36,6 +38,8 @@
import org.apache.brooklyn.entity.stock.BasicEntityImpl;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.test.ClassLogWatcher;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.flags.TypeCoercions;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
@@ -49,6 +53,7 @@
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
import java.util.function.Function;
public class WorkflowExpressionsYamlTest extends AbstractYamlTest {
@@ -340,4 +345,30 @@
coercedFromMissingId = Entities.submit(lastEntity, Tasks.of("test", () -> TypeCoercions.tryCoerce("does_not_exist", Entity.class))).get();
Asserts.assertThat(coercedFromMissingId, Maybe::isAbsent);
}
+
+
+ @Test
+ public void testTransformGet() throws Exception {
+ BrooklynDslCommon.registerSerializationHooks();
+ Entity app = createAndStartApplication(
+ "services:",
+ "- type: " + BasicEntity.class.getName());
+
+ BiFunction<Object,String,Object> get = (input, command) -> {
+ app.config().set(ConfigKeys.newConfigKey(Object.class, "x"), input);
+ return WorkflowBasicTest.runWorkflow(app, " - transform $brooklyn:config(\"x\") | get " + command, "test").getTask(false).get().getUnchecked();
+ };
+
+ Asserts.assertEquals( get.apply(Suppliers.ofInstance(42), ""), 42);
+ Asserts.assertEquals( get.apply(42, ""), 42);
+
+ Asserts.assertEquals( get.apply(MutableList.of(42), "0"), 42);
+ Asserts.assertEquals( get.apply(MutableList.of(42), "[0]"), 42);
+ 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\"][0]"), 42);
+ Asserts.assertEquals( get.apply(MutableMap.of("k", MutableList.of(42)), ".k[0]"), 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 ac7f555..79d2b67 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
@@ -136,22 +136,35 @@
@Test
public void testParseObjectAttribute() {
- Object fx = new DslParser("$brooklyn:object(\"[brooklyn.obj.TestObject,host]\").attributeWhenReady(\"ips_container\")[\"ips\"][0]").parse();
- assertEquals(((List<?>) fx).size(), 4, "" + fx);
+ List fx = (List) new DslParser("$brooklyn:object(\"[brooklyn.obj.TestObject,host]\").attributeWhenReady(\"ips_container\")[\"ips\"][0]").parse();
+ assertEquals(fx.size(), 4, "" + fx);
- Object fx1 = ((List<?>)fx).get(0);
- assertEquals( ((FunctionWithArgs)fx1).getFunction(), "$brooklyn:object" );
- assertEquals( ((FunctionWithArgs)fx1).getArgs(), ImmutableList.of(new QuotedString("\"[brooklyn.obj.TestObject,host]\"")) );
+ assertEquals( ((FunctionWithArgs)fx.get(0)).getFunction(), "$brooklyn:object" );
+ assertEquals( ((FunctionWithArgs)fx.get(0)).getArgs(), ImmutableList.of(new QuotedString("\"[brooklyn.obj.TestObject,host]\"")) );
- Object fx2 = ((List<?>)fx).get(1);
- assertEquals( ((FunctionWithArgs)fx2).getFunction(), "attributeWhenReady" );
- assertEquals( ((FunctionWithArgs)fx2).getArgs(), ImmutableList.of(new QuotedString("\"ips_container\"")) );
+ assertEquals( ((FunctionWithArgs)fx.get(1)).getFunction(), "attributeWhenReady" );
+ assertEquals( ((FunctionWithArgs)fx.get(1)).getArgs(), ImmutableList.of(new QuotedString("\"ips_container\"")) );
- Object fx3 = ((List<?>)fx).get(2);
- assertEquals( ((PropertyAccess)fx3).getSelector(), "ips" );
+ assertEquals( ((PropertyAccess)fx.get(2)).getSelector(), "ips" );
+ assertEquals( ((PropertyAccess)fx.get(3)).getSelector(), "0" );
- Object fx4 = ((List<?>)fx).get(3);
- assertEquals( ((PropertyAccess)fx4).getSelector(), "0" );
+ fx = (List) new DslParser("$brooklyn:object(\"[brooklyn.obj.TestObject,host]\").attributeWhenReady(\"ips_container\").ips[0]").parse();
+ assertEquals(fx.size(), 4, "" + fx);
+ assertEquals( ((PropertyAccess)fx.get(2)).getSelector(), "ips" );
+
+ fx = (List) new DslParser("$brooklyn:object(\"[brooklyn.obj.TestObject,host]\").attributeWhenReady(\"ips_container\").a.b[0].c.d[1]").parse();
+ assertEquals(fx.size(), 8, "" + fx);
+ assertEquals( ((PropertyAccess)fx.get(3)).getSelector(), "b" );
+ assertEquals( ((PropertyAccess)fx.get(4)).getSelector(), "0" );
+ assertEquals( ((PropertyAccess)fx.get(6)).getSelector(), "d" );
+ assertEquals( ((PropertyAccess)fx.get(7)).getSelector(), "1" );
+ }
+
+ @Test
+ public void testParseFunctionExplicit() {
+ List fx = (List) new DslParser("$brooklyn:function.foo()").parse();
+ assertEquals( ((FunctionWithArgs)fx.get(0)).getFunction(), "$brooklyn:function.foo" );
+ assertEquals( ((FunctionWithArgs)fx.get(0)).getArgs(), ImmutableList.of() );
}
}
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformJoin.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformJoin.java
new file mode 100644
index 0000000..b14e8b7
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformJoin.java
@@ -0,0 +1,60 @@
+/*
+ * 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.steps.variables;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.brooklyn.core.workflow.WorkflowExpressionResolution;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.javalang.Boxing;
+import org.apache.brooklyn.util.text.Strings;
+
+public class TransformJoin extends WorkflowTransformDefault {
+
+ String separator;
+
+ @Override
+ protected void initCheckingDefinition() {
+ List<String> d = MutableList.copyOf(definition.subList(1, definition.size()));
+ if (d.size()>1) throw new IllegalArgumentException("Transform requires zero or one arguments being a token to insert between elements");
+ if (!d.isEmpty()) separator = d.get(0);
+ }
+
+ @Override
+ public Object apply(Object v) {
+ Object separatorResolvedO = separator==null ? "" : context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_RUNNING, separator, Object.class);
+ if (!(separatorResolvedO instanceof String || Boxing.isPrimitiveOrBoxedObject(separatorResolvedO))) {
+ throw new IllegalStateException("Argument must be a string or primitive to use as the separator");
+ }
+ String separatorResolved = ""+separatorResolvedO;
+ if (v instanceof Iterable) {
+ List list = MutableList.copyOf((Iterable)v);
+ return list.stream().map(x -> {
+ if (!(x instanceof String || Boxing.isPrimitiveOrBoxedObject(x))) {
+ throw new IllegalStateException("Elements in the list to join must be a strings or primitives");
+ }
+ return ""+x;
+ }).collect(Collectors.joining(separatorResolved));
+ } else {
+ throw new IllegalStateException("Input must be a list to join");
+ }
+ }
+
+}
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSlice.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSlice.java
index f8b357a..d77d965 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSlice.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSlice.java
@@ -71,7 +71,7 @@
return list.subList(from, to);
}
- static <T> T resolveAs(Object expression, WorkflowExecutionContext context, String errorPrefix, boolean failIfNull, Class<T> type, String typeName) {
+ public static <T> T resolveAs(Object expression, WorkflowExecutionContext context, String errorPrefix, boolean failIfNull, Class<T> type, String typeName) {
T result = null;
try {
if (expression!=null) result = context.resolve(WorkflowExpressionResolution.WorkflowExpressionStage.STEP_RUNNING, expression, type);
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
new file mode 100644
index 0000000..d5b1779
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformSplit.java
@@ -0,0 +1,129 @@
+/*
+ * 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.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 {
+
+ String SHORTHAND = "\"split\" [ \"limit\" ${limit} ] [ ?${keep_delimiters} \"keep_delimiters\" ] [ ?${literal} \"literal\" ] [ ?${regex} \"regex\" ] ${delimiter}";
+
+ Integer limit;
+ String delimiter;
+ boolean keep_delimiters, literal, regex;
+
+ @Override
+ protected void initCheckingDefinition() {
+ Maybe<Map<String, Object>> maybeResult = new ShorthandProcessor(SHORTHAND)
+ .withFinalMatchRaw(false)
+ .withFailOnMismatch(true)
+ .process(transformDef);
+
+ if (maybeResult.isPresent()) {
+ Map<String, Object> result = maybeResult.get();
+ keep_delimiters = Boolean.TRUE.equals(result.get("keep_delimiters"));
+ literal = Boolean.TRUE.equals(result.get("literal"));
+ regex = Boolean.TRUE.equals(result.get("regex"));
+ limit = TransformSlice.resolveAs(result.get("limit"), context, "First argument 'limit'", false, Integer.class, "an integer");
+ delimiter = TransformSlice.resolveAs(result.get("delimiter"), context, "Last argument 'delimiter'", true, String.class, "a string");
+
+ // could disallow this, but it makes sense and works so we allow it;
+ //if (Strings.isEmpty(delimiter)) throw new IllegalArgumentException("Delimiter to split must not be empty");
+
+ if (regex && literal) throw new IllegalArgumentException("Only one of regex and literal can be set");
+ if (!regex && !literal) literal = true;
+ } else {
+ throw new IllegalArgumentException("Expression must be of the form 'split [limit L] [keep_delimiters] [literal|regex] DELIMITER");
+ }
+ }
+
+ @Override
+ public Object apply(Object v) {
+ if (v instanceof String) {
+ List<String> split = MutableList.of();
+
+ final String s = (String)v;
+ Matcher m = regex ? Pattern.compile(delimiter).matcher((String) v) : null;
+
+ int lastEnd = 0;
+ while (true) {
+ if (m==null) {
+ int index = s.indexOf(delimiter, lastEnd);
+
+ if (delimiter.isEmpty()) {
+ if (split.isEmpty()) {
+ split.add("");
+ if (s.isEmpty()) break;
+ if (keep_delimiters) split.add("");
+ }
+ index++;
+ }
+
+ if (index >= 0 && index<=s.length() && !s.isEmpty()) {
+ split.add(s.substring(lastEnd, index));
+ if (keep_delimiters) split.add(delimiter);
+ lastEnd = index + delimiter.length();
+ } else {
+ split.add(s.substring(lastEnd));
+ break;
+ }
+ } else {
+ if (m.find() && !s.isEmpty()) {
+ if (m.start()<lastEnd) continue;
+ if (lastEnd==m.end() && !split.isEmpty()) {
+ // Matcher.find should increment, so this shouldn't happen, but double check;
+ // we do match at start and end, deliberately
+ throw new IllegalStateException("Regex match repeats splitting on empty string at same position");
+ }
+ split.add(s.substring(lastEnd, m.start()));
+ if (keep_delimiters) split.add(s.substring(m.start(), m.end()));
+ lastEnd = m.end();
+ } else {
+ split.add(s.substring(lastEnd));
+ break;
+ }
+ }
+ if (limit!=null && split.size() >= limit) {
+ split = split.subList(0, limit);
+ break;
+ }
+ }
+ return split;
+
+ } else {
+ throw new IllegalStateException("Input must be a string to split");
+ }
+ }
+
+}
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformTrim.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformTrim.java
index 7a38f6c..bfb3a84 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformTrim.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformTrim.java
@@ -30,10 +30,9 @@
public Object apply(Object v) {
if (v == null) return null;
if (v instanceof String) return ((String) v).trim();
- if (v instanceof Set) return ((Set) v).stream().filter(x -> x != null).collect(Collectors.toSet());
+ if (v instanceof Set) return ((Set) v).stream().filter(TransformTrim::shouldTrimKeepInList).collect(Collectors.toSet());
if (v instanceof Collection)
- return ((Collection) v).stream().filter(x -> x != null).collect(Collectors.toList());
- ;
+ return ((Collection) v).stream().filter(TransformTrim::shouldTrimKeepInList).collect(Collectors.toList());
if (v instanceof Map) {
Map<Object, Object> result = MutableMap.of();
((Map) v).forEach((k, vi) -> {
@@ -43,4 +42,11 @@
}
return v;
}
+
+ public static boolean shouldTrimKeepInList(Object x) {
+ if (x==null) return false;
+ if (x instanceof String) return !((String) x).isEmpty();
+ return true;
+ }
+
}
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 86bbb0f..656e067 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
@@ -272,6 +272,8 @@
TRANSFORMATIONS.put("merge", () -> new TransformMerge());
TRANSFORMATIONS.put("prepend", () -> new TransformPrependAppend(false));
TRANSFORMATIONS.put("append", () -> new TransformPrependAppend(true));
+ TRANSFORMATIONS.put("join", () -> new TransformJoin());
+ TRANSFORMATIONS.put("split", () -> new TransformSplit());
TRANSFORMATIONS.put("slice", () -> new TransformSlice());
TRANSFORMATIONS.put("remove", () -> new TransformRemove());
TRANSFORMATIONS.put("json", () -> new TransformJsonish(true, false, false));
@@ -300,17 +302,18 @@
TRANSFORMATIONS.put("sum", () -> v -> sum(v, "sum"));
TRANSFORMATIONS.put("average", () -> v -> average(v, "average"));
TRANSFORMATIONS.put("size", () -> v -> size(v, "size"));
- TRANSFORMATIONS.put("get", () -> v -> {
- // TODO should this be able to get indexes etc
- if (v instanceof Supplier) return ((Supplier)v).get();
- return v;
- });
TRANSFORMATIONS.put("to_string", () -> v -> Strings.toString(v));
TRANSFORMATIONS.put("to_upper_case", () -> v -> ((String)v).toUpperCase());
TRANSFORMATIONS.put("to_lower_case", () -> v -> ((String)v).toLowerCase());
TRANSFORMATIONS.put("return", () -> new TransformReturn());
TRANSFORMATIONS.put("set", () -> new TransformSetWorkflowVariable());
TRANSFORMATIONS.put("resolve_expression", () -> new TransformResolveExpression());
+
+ // 'get' is added downstream when DSL is initialized
+ //TRANSFORMATIONS.put("get", () -> new TransformGet());
+ }
+ public static void registerTransformation(String key, Supplier<Function> xform) {
+ TRANSFORMATIONS.put(key, xform);
}
static final Object minmax(Object v, String word, Predicate<Integer> test) {
diff --git a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowTransformTest.java b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowTransformTest.java
index af4746c..4a29156 100644
--- a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowTransformTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowTransformTest.java
@@ -18,11 +18,16 @@
*/
package org.apache.brooklyn.core.workflow;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.google.common.base.Suppliers;
import org.apache.brooklyn.api.entity.Entity;
-import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.entity.EntitySpec;
-import org.apache.brooklyn.api.mgmt.ManagementContext;
-import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.test.BrooklynMgmtUnitTestSupport;
import org.apache.brooklyn.core.workflow.steps.variables.TransformVariableWorkflowStep;
@@ -30,17 +35,11 @@
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
-import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.text.Strings;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
public class WorkflowTransformTest extends BrooklynMgmtUnitTestSupport {
protected void loadTypes() {
@@ -65,7 +64,7 @@
}
static Object runWorkflowSteps(Entity entity, String ...steps) {
- return WorkflowBasicTest.runWorkflow(entity, Arrays.asList(steps).stream().map(s -> "- "+Strings.indent(2, s).trim()).collect(Collectors.joining("\n")), "test").getTask(false).get().getUnchecked();
+ return WorkflowBasicTest.runWorkflow(entity, Arrays.stream(steps).map(s -> "- "+Strings.indent(2, s).trim()).collect(Collectors.joining("\n")), "test").getTask(false).get().getUnchecked();
}
Object runWorkflowSteps(String ...steps) {
return runWorkflowSteps(app, steps);
@@ -119,7 +118,7 @@
@Test
- public void testTransformTrim() throws Exception {
+ public void testTransformTrim() {
String untrimmed = "Hello, World! ";
String trimmed = untrimmed.trim();
@@ -132,7 +131,7 @@
}
@Test
- public void testTransformRegex() throws Exception {
+ public void testTransformRegex() {
Asserts.assertEquals(transform("value 'silly world' | replace regex l. k"), "siky world");
Asserts.assertEquals(transform("value 'silly world' | replace all regex l. k"), "siky work");
// with slash
@@ -151,14 +150,14 @@
}
@Test
- public void testTransformLiteral() throws Exception {
+ public void testTransformLiteral() {
Asserts.assertEquals(transform("value 'abc def ghi' | replace literal c.*d XXX"), "abc def ghi");
Asserts.assertEquals(transform("value 'abc.*def ghi c.*d' | replace literal c.*d XXX"), "abXXXef ghi c.*d");
Asserts.assertEquals(transform("value 'abc.*def ghi c.*d' | replace all literal c.*d XXX"), "abXXXef ghi XXX");
}
@Test
- public void testTransformGlob() throws Exception {
+ public void testTransformGlob() {
Asserts.assertEquals(transform("value 'abc def ghi' | replace glob c*e XXX"), "abXXXf ghi");
// glob is greedy, unless all is specified where it is not
Asserts.assertEquals(transform("value 'abc def ghi c2e' | replace glob c*e XXX"), "abXXX");
@@ -275,4 +274,58 @@
MutableMap.of("a", 1));
}
+ @Test
+ public void testTransformJoin() {
+ Asserts.assertEquals( runWorkflowSteps(
+ "let list words = [ \"hello\" , 1, \"world\" ]",
+ "transform ${words} | join \" \" | return"),
+ "hello 1 world");
+ }
+
+ @Test
+ public void testTransformSplit() {
+ BiFunction<String,String,Object> split = (input, command) -> runWorkflowSteps(
+ "let words = "+input,
+ "transform ${words} | split "+command);
+
+ Asserts.assertEquals( split.apply("\"hello 1 world\"", "\" \""), MutableList.of("hello", "1", "world"));
+ Asserts.assertEquals( split.apply("\"hello 1 world\"", "\" \""), MutableList.of("hello", "1 world"));
+ Asserts.assertEquals( split.apply("\"hello 1 world\"", "literal \" \""), MutableList.of("hello", "1 world"));
+ Asserts.assertEquals( split.apply("\"hello 1 world\"", "regex \" +\""), MutableList.of("hello", "1", "world"));
+ Asserts.assertEquals( split.apply("\"hello 1 world\"", "\" +\""), MutableList.of("hello 1 world"));
+ Asserts.assertEquals( split.apply("\"hello 1 world\"", "keep_delimiters regex \" +\""), MutableList.of("hello", " ", "1", " ", "world"));
+ Asserts.assertEquals( split.apply("\"hello 1 world\"", "limit 4 keep_delimiters regex \" +\""), MutableList.of("hello", " ", "1", " "));
+ Asserts.assertEquals( split.apply("\"hello 1 world\"", "keep_delimiters \" \""), MutableList.of("hello", " ", "", " ", "1", " ", "world"));
+ Asserts.assertEquals( split.apply("\"hello 1 world\"", "keep_delimiters regex \" \""), MutableList.of("hello", " ", "", " ", "1", " ", "world"));
+
+ Asserts.assertEquals( split.apply("\"hello 1 world\"", "\" \""), MutableList.of("hello", "", "1", "world"));
+ Asserts.assertEquals( split.apply("\" hello 1 world \"", "\" \" | trim"), MutableList.of("hello", "1", "world"));
+
+ // leading and trailing matches generate an empty string in the output list
+ Asserts.assertEquals( split.apply("\" h ey \"", "\" \""), MutableList.of("", "h", "ey", ""));
+ Asserts.assertEquals( split.apply("\" h ey \"", "regex \" \""), MutableList.of("", "h", "ey", ""));
+
+ // empty string matches every break, including start and end, once
+ Asserts.assertEquals( split.apply("\"hey\"", "regex \"\""), MutableList.of("", "h", "e", "y", ""));
+ Asserts.assertEquals( split.apply("\"hey\"", "\"\""), MutableList.of("", "h", "e", "y", ""));
+ Asserts.assertEquals( split.apply("\"\"", "regex \"\""), MutableList.of(""));
+ Asserts.assertEquals( split.apply("\"\"", "\"\""), MutableList.of(""));
+ }
+
+ @Test
+ public void testTransformSum() {
+ Asserts.assertEquals( runWorkflowSteps(
+ "let list nums = [ 1, 2, 3]",
+ "transform ${nums} | sum | return"),
+ 6);
+
+ Asserts.assertFailsWith( () -> runWorkflowSteps(
+ "let list nums = [ 1, 2, 3]",
+ "transform ${nums} | sum 4 | return"),
+ e -> Asserts.expectedFailureContainsIgnoreCase(e, "sum", "does not accept args", "4"));
+ }
+
+ // done in camp project, WorkflowExpressionsYamlTest
+ //public void testTransformGet() {
+
}