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() { + }