This is an automated email from the ASF dual-hosted git repository. duncangrant pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git
The following commit(s) were added to refs/heads/master by this push: new 51ee4a1c21 Adds Workflow Replace Transform new 1098e61808 Merge pull request #1392 from nakomis/workflow-transform-regex 51ee4a1c21 is described below commit 51ee4a1c218ac497dbc4e0edb9251a3b35c6bf90 Author: Martin Harris <git...@nakomis.com> AuthorDate: Wed Apr 19 11:03:19 2023 +0100 Adds Workflow Replace Transform --- .../workflow/steps/variables/TransformReplace.java | 196 ++++++++++++++++++++ .../variables/TransformVariableWorkflowStep.java | 3 +- .../steps/variables/WorkflowTransformDefault.java | 5 + .../variables/WorkflowTransformWithContext.java | 2 + .../core/workflow/WorkflowTransformTest.java | 202 +++++++++++++++++++++ 5 files changed, 407 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformReplace.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformReplace.java new file mode 100644 index 0000000000..65b9b07fc1 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/TransformReplace.java @@ -0,0 +1,196 @@ +/* + * 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 org.apache.brooklyn.core.workflow.ShorthandProcessor; +import org.apache.brooklyn.core.workflow.WorkflowExecutionContext; +import org.apache.brooklyn.util.guava.Maybe; + +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class TransformReplace extends WorkflowTransformDefault { + boolean regex; + boolean glob; + boolean literal; + boolean all; + String patternToMatch; + String replacement; + + String SHORTHAND = "\"replace\" [ ?${all} \"all\" ] [ ?${regex} \"regex\" ] [ ?${glob} \"glob\" ] [ ?${literal} \"literal\" ] ${patternToMatch} ${replacement}"; + + @Override + public void init(WorkflowExecutionContext context, List<String> definition, String transformDef) { + super.init(context, null); + + Maybe<Map<String, Object>> maybeResult = new ShorthandProcessor(SHORTHAND) + .withFinalMatchRaw(true) + .withFailOnMismatch(true) + .process(transformDef); + + if (maybeResult.isPresent()) { + // TODO: Ability to parse Brooklyn DSL + Map<String, Object> result = maybeResult.get(); + all = Boolean.TRUE.equals(result.get("all")); + regex = Boolean.TRUE.equals(result.get("regex")); + glob = Boolean.TRUE.equals(result.get("glob")); + literal = Boolean.TRUE.equals(result.get("literal")); + patternToMatch = String.valueOf(result.get("patternToMatch")); + replacement = String.valueOf(result.get("replacement")); + + int replaceTypeCount = (regex?1:0) + (glob?1:0) + (literal?1:0); + + if (replaceTypeCount > 1) { + throw new IllegalArgumentException("Only one of regex, glob, and literal can be set"); + } else if (replaceTypeCount == 0) { + // Set the default if none provided + literal = true; + } + } else { + throw new IllegalArgumentException("Expression must be of the form 'replace [all] [regex|glob|literal] patternToMatch replacement'"); + } + } + + @Override + public Object apply(Object o) { + if (o == null) + return null; + if (!(o instanceof String)) { + throw new IllegalArgumentException("Expression must be of the form replace [regex|glob|literal] pattern_to_match replacement"); + } + String input = (String)o; + + if (regex) { + return all ? input.replaceAll(patternToMatch, replacement) + : input.replaceFirst(patternToMatch, replacement); + } + if (glob) { + String globToRegex = convertGlobToRegex(patternToMatch); + + return all ? input.replaceAll(globToRegex, replacement) + : input.replaceFirst(globToRegex, replacement); + } + if (literal) { + return all ? Pattern.compile(patternToMatch, Pattern.LITERAL).matcher(input).replaceAll(replacement) + : Pattern.compile(patternToMatch, Pattern.LITERAL).matcher(input).replaceFirst(replacement); + } + // Should never get here + throw new IllegalArgumentException("Expression must be of the form replace [regex|glob|literal] pattern_to_match replacement"); + } + + /** + * Converts a standard POSIX Shell globbing pattern into a regular expression + * + * Copied from Neil Traft's answer in https://stackoverflow.com/questions/1247772/is-there-an-equivalent-of-java-util-regex-for-glob-type-patterns/17369948#17369948 + * See public domain statement in comments on the above + * + * TODO: Want to use library utility, but cant' find a suitable one other than the now-retired Apache Jakarta ORO + * https://jakarta.apache.org/oro/ + * + */ + private String convertGlobToRegex(String pattern) { + StringBuilder sb = new StringBuilder(pattern.length()); + int inGroup = 0; + int inClass = 0; + int firstIndexInClass = -1; + char[] arr = pattern.toCharArray(); + for (int i = 0; i < arr.length; i++) { + char ch = arr[i]; + switch (ch) { + case '\\': + if (++i >= arr.length) { + sb.append('\\'); + } else { + char next = arr[i]; + switch (next) { + case ',': + // escape not needed + break; + case 'Q': + case 'E': + // extra escape needed + sb.append('\\'); + default: + sb.append('\\'); + } + sb.append(next); + } + break; + case '*': + if (inClass == 0) + sb.append(".*"); + else + sb.append('*'); + break; + case '?': + if (inClass == 0) + sb.append('.'); + else + sb.append('?'); + break; + case '[': + inClass++; + firstIndexInClass = i+1; + sb.append('['); + break; + case ']': + inClass--; + sb.append(']'); + break; + case '.': + case '(': + case ')': + case '+': + case '|': + case '^': + case '$': + case '@': + case '%': + if (inClass == 0 || (firstIndexInClass == i && ch == '^')) + sb.append('\\'); + sb.append(ch); + break; + case '!': + if (firstIndexInClass == i) + sb.append('^'); + else + sb.append('!'); + break; + case '{': + inGroup++; + sb.append('('); + break; + case '}': + inGroup--; + sb.append(')'); + break; + case ',': + if (inGroup > 0) + sb.append('|'); + else + sb.append(','); + break; + default: + sb.append(ch); + } + } + return sb.toString(); + } +} 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 a8d0c05fb8..f95d20cba6 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 @@ -137,7 +137,7 @@ public class TransformVariableWorkflowStep extends WorkflowStepDefinition { } if (t==null) throw new IllegalStateException("Unknown transform '"+transformType+"'"); WorkflowTransformWithContext ta = transformOf(t); - ta.init(context.getWorkflowExectionContext(), transformWords); + ta.init(context.getWorkflowExectionContext(), transformWords, transformDef); return ta; } @@ -148,6 +148,7 @@ public class TransformVariableWorkflowStep extends WorkflowStepDefinition { TRANSFORMATIONS.put("json", () -> new TransformJsonish(true, false, false)); TRANSFORMATIONS.put("yaml", () -> new TransformJsonish(false, true, false)); TRANSFORMATIONS.put("bash", () -> new TransformJsonish(true, false, true)); + TRANSFORMATIONS.put("replace", () -> new TransformReplace()); TRANSFORMATIONS.put("wait", () -> new TransformWait()); TRANSFORMATIONS.put("type", () -> new TransformType()); TRANSFORMATIONS.put("first", () -> x -> { diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/WorkflowTransformDefault.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/WorkflowTransformDefault.java index 3b7de118dc..386f4fa74f 100644 --- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/WorkflowTransformDefault.java +++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/WorkflowTransformDefault.java @@ -25,6 +25,11 @@ import java.util.List; public abstract class WorkflowTransformDefault implements WorkflowTransformWithContext { protected WorkflowExecutionContext context; + @Override + public void init(WorkflowExecutionContext context, List<String> definition, String transformDef) { + init(context, definition); + } + @Override public void init(WorkflowExecutionContext context, List<String> definition) { this.context = context; diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/WorkflowTransformWithContext.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/WorkflowTransformWithContext.java index ccc9bb1788..4819feacd1 100644 --- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/WorkflowTransformWithContext.java +++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/WorkflowTransformWithContext.java @@ -25,5 +25,7 @@ import java.util.List; public interface WorkflowTransformWithContext extends WorkflowTransform { void init(WorkflowExecutionContext context, List<String> definition); + void init(WorkflowExecutionContext context, List<String> definition, String transformDef); + boolean isResolver(); } 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 new file mode 100644 index 0000000000..0e66f257a5 --- /dev/null +++ b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowTransformTest.java @@ -0,0 +1,202 @@ +/* + * 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; + +import org.apache.brooklyn.api.entity.EntityLocal; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.core.entity.EntityAsserts; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.core.test.BrooklynMgmtUnitTestSupport; +import org.apache.brooklyn.core.workflow.steps.flow.LogWorkflowStep; +import org.apache.brooklyn.entity.stock.BasicApplication; +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.config.ConfigBag; +import org.testng.annotations.Test; + +import java.util.List; + +public class WorkflowTransformTest extends BrooklynMgmtUnitTestSupport { + + protected void loadTypes() { + WorkflowBasicTest.addWorkflowStepTypes(mgmt); + } + + @Test + public void testTransformTrim() throws Exception { + loadTypes(); + + String untrimmed = "Hello, World! "; + String trimmed = untrimmed.trim(); + + BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); + + WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() + .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") + .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("p1", MutableMap.of("defaultValue", "p1v"))) + .configure(WorkflowEffector.STEPS, MutableList.<Object>of() + .append("let mystring = '"+untrimmed+"'") + .append("transform mytrimmedstring = ${mystring} | trim") + .append("return ${mytrimmedstring}") + ) + ); + eff.apply((EntityLocal)app); + + Task<?> invocation = app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null); + Object result = invocation.getUnchecked(); + Asserts.assertNotNull(result); + Asserts.assertEquals(result, trimmed); + } + + @Test + public void testTransformRegex() throws Exception { + loadTypes(); + + BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); + + WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() + .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") + .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("p1", MutableMap.of("defaultValue", "p1v"))) + .configure(WorkflowEffector.STEPS, MutableList.<Object>of() + .append("transform x = 'silly world' | replace regex l. k") + .append("return ${x}") + ) + ); + eff.apply((EntityLocal)app); + + Task<?> invocation = app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null); + Object result = invocation.getUnchecked(); + Asserts.assertNotNull(result); + Asserts.assertEquals(result, "siky world"); + } + + @Test + public void testTransformAllRegex() throws Exception { + loadTypes(); + + BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); + + WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() + .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") + .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("p1", MutableMap.of("defaultValue", "p1v"))) + .configure(WorkflowEffector.STEPS, MutableList.<Object>of() + .append("transform x = 'silly world' | replace all regex l. k") + .append("return ${x}") + ) + ); + eff.apply((EntityLocal)app); + + Task<?> invocation = app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null); + Object result = invocation.getUnchecked(); + Asserts.assertNotNull(result); + Asserts.assertEquals(result, "siky work"); + } + + @Test + public void testTransformRegexWithBackslash() throws Exception { + loadTypes(); + + BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); + + WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() + .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") + .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("p1", MutableMap.of("defaultValue", "p1v"))) + .configure(WorkflowEffector.STEPS, MutableList.<Object>of() + .append("transform x = 'abc/def/ghi' | replace regex 'c/d' XXX") + .append("return ${x}") + ) + ); + eff.apply((EntityLocal)app); + + Task<?> invocation = app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null); + Object result = invocation.getUnchecked(); + Asserts.assertNotNull(result); + Asserts.assertEquals(result, "abXXXef/ghi"); + } + + @Test + public void testTransformRegexWithSpace() throws Exception { + loadTypes(); + + BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); + + WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() + .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") + .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("p1", MutableMap.of("defaultValue", "p1v"))) + .configure(WorkflowEffector.STEPS, MutableList.<Object>of() + .append("transform x = 'abc def ghi' | replace regex 'c d' XXX") + .append("return ${x}") + ) + ); + eff.apply((EntityLocal)app); + + Task<?> invocation = app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null); + Object result = invocation.getUnchecked(); + Asserts.assertNotNull(result); + Asserts.assertEquals(result, "abXXXef ghi"); + } + + @Test + public void testTransformLiteral() throws Exception { + loadTypes(); + + BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); + + WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() + .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") + .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("p1", MutableMap.of("defaultValue", "p1v"))) + .configure(WorkflowEffector.STEPS, MutableList.<Object>of() + .append("transform x = 'abc.*def ghi' | replace literal c.*d XXX") + .append("return ${x}") + ) + ); + eff.apply((EntityLocal)app); + + Task<?> invocation = app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null); + Object result = invocation.getUnchecked(); + Asserts.assertNotNull(result); + Asserts.assertEquals(result, "abXXXef ghi"); + } + + @Test + public void testTransformGlob() throws Exception { + loadTypes(); + + BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); + + WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() + .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") + .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("p1", MutableMap.of("defaultValue", "p1v"))) + .configure(WorkflowEffector.STEPS, MutableList.<Object>of() + .append("transform x = 'abc.*def ghi' | replace glob c*e XXX") + .append("return ${x}") + ) + ); + eff.apply((EntityLocal)app); + + Task<?> invocation = app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null); + Object result = invocation.getUnchecked(); + Asserts.assertNotNull(result); + Asserts.assertEquals(result, "abXXXf ghi"); + } + +}