This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit fe2b3248dbcab5d15324894e00966f9777fa6922
Author: Alex Heneveld <a...@cloudsoft.io>
AuthorDate: Tue Oct 10 14:27:57 2023 +0100

    support dot property access in DSL, and transform get, join, split
---
 .../brooklyn/spi/dsl/BrooklynDslInterpreter.java   |  12 +-
 .../spi/dsl/methods/BrooklynDslCommon.java         |   8 +-
 .../camp/brooklyn/spi/dsl/parse/DslParser.java     |  77 +++++++-----
 .../spi/dsl/parse/WorkflowTransformGet.java        |  78 +++++++++++++
 .../camp/brooklyn/WorkflowExpressionsYamlTest.java |  31 +++++
 .../camp/brooklyn/spi/dsl/DslParseTest.java        |  37 ++++--
 .../workflow/steps/variables/TransformJoin.java    |  60 ++++++++++
 .../workflow/steps/variables/TransformSlice.java   |   2 +-
 .../workflow/steps/variables/TransformSplit.java   | 129 +++++++++++++++++++++
 .../workflow/steps/variables/TransformTrim.java    |  12 +-
 .../variables/TransformVariableWorkflowStep.java   |  13 ++-
 .../core/workflow/WorkflowTransformTest.java       |  81 ++++++++++---
 12 files changed, 472 insertions(+), 68 deletions(-)

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 948151274f..c42b7993e7 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 @@ public class BrooklynDslInterpreter extends 
PlanInterpreterAdapter {
         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 @@ public class BrooklynDslInterpreter extends 
PlanInterpreterAdapter {
     @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 0468930717..6dde3ff5b3 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.creation.EntitySpecConfiguration;
 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.resolve.jackson.BrooklynJacksonSerializationUtil
 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 @@ public class BrooklynDslCommon {
         
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 class BrooklynDslCommon {
             }
         });
     }
+    public static void registerWorkflowTransforms() {
+        TransformVariableWorkflowStep.registerTransformation("get", () -> new 
WorkflowTransformGet());
+    }
     
     // Access specific entities
 
@@ -463,7 +469,7 @@ public class BrooklynDslCommon {
         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 71b4677b0e..846a1efad1 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 @@ package org.apache.brooklyn.camp.brooklyn.spi.dsl.parse;
 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 @@ public class DslParser {
                 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 @@ public class DslParser {
             // 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 @@ public class DslParser {
 
             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 @@ public class DslParser {
             }
 
             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 0000000000..66ae498679
--- /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 0b11eabba1..10c76f5a79 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.BasicEntity;
 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 org.testng.annotations.Test;
 
 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 @@ public class WorkflowExpressionsYamlTest extends 
AbstractYamlTest {
         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 ac7f555abe..79d2b67925 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 @@ public class DslParseTest {
 
     @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 0000000000..b14e8b77d9
--- /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 f8b357a4ac..d77d965df8 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 @@ public class TransformSlice extends WorkflowTransformDefault {
         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 0000000000..d5b17793bd
--- /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 7a38f6cc26..bfb3a84bb0 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 class TransformTrim extends WorkflowTransformDefault {
     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 @@ public class TransformTrim extends WorkflowTransformDefault {
         }
         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 86bbb0f58a..656e067c5c 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 @@ public class TransformVariableWorkflowStep extends 
WorkflowStepDefinition {
         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 @@ public class TransformVariableWorkflowStep extends 
WorkflowStepDefinition {
         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 af4746cd90..4a29156f4a 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.entity.stock.BasicApplication;
 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 @@ public class WorkflowTransformTest extends 
BrooklynMgmtUnitTestSupport {
     }
 
     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 @@ public class WorkflowTransformTest extends 
BrooklynMgmtUnitTestSupport {
 
 
     @Test
-    public void testTransformTrim() throws Exception {
+    public void testTransformTrim() {
         String untrimmed = "Hello, World!   ";
         String trimmed = untrimmed.trim();
 
@@ -132,7 +131,7 @@ public class WorkflowTransformTest extends 
BrooklynMgmtUnitTestSupport {
     }
 
     @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 @@ public class WorkflowTransformTest extends 
BrooklynMgmtUnitTestSupport {
     }
 
     @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 @@ public class WorkflowTransformTest extends 
BrooklynMgmtUnitTestSupport {
                 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() {
+
 }


Reply via email to