diff --git a/core/pom.xml b/core/pom.xml index ef3e850f7dd..7a67dc27dc8 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -150,6 +150,11 @@ org.apache.commons commons-lang3 + + org.python + jython + 2.7.0 + org.testng diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/script/ScriptEffector.java b/core/src/main/java/org/apache/brooklyn/core/effector/script/ScriptEffector.java new file mode 100644 index 00000000000..904b3ac3fc5 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/effector/script/ScriptEffector.java @@ -0,0 +1,142 @@ +/* + * 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.effector.script; + +import java.util.Map; + +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import javax.script.SimpleScriptContext; + +import org.python.core.Options; + +import com.google.common.base.Preconditions; +import com.google.common.reflect.TypeToken; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.effector.ParameterType; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.effector.AddEffector; +import org.apache.brooklyn.core.effector.EffectorBody; +import org.apache.brooklyn.core.effector.Effectors; +import org.apache.brooklyn.core.effector.Effectors.EffectorBuilder; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.flags.SetFromFlag; +import org.apache.brooklyn.util.core.flags.TypeCoercions; +import org.apache.brooklyn.util.core.task.Tasks; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.text.Strings; + +public final class ScriptEffector extends AddEffector { + + static { + Options.importSite = false; + } + + @SetFromFlag("lang") + public static final ConfigKey EFFECTOR_SCRIPT_LANGUAGE = ConfigKeys.newStringConfigKey( + "script.language", "The scripting language the effector is written in", "JavaScript"); + + @SetFromFlag("content") + public static final ConfigKey EFFECTOR_SCRIPT_CONTENT = ConfigKeys.newStringConfigKey( + "script.content", "The script code to evaluate for the effector"); + + @SetFromFlag("script") + public static final ConfigKey EFFECTOR_SCRIPT_URL = ConfigKeys.newStringConfigKey( + "script.url", "A URL for the script to evaluate for the effector"); + + @SetFromFlag("return") + public static final ConfigKey EFFECTOR_SCRIPT_RETURN_VAR = ConfigKeys.newStringConfigKey( + "script.return.var", "An optional script variable to return from the effector"); + + @SetFromFlag("type") + public static final ConfigKey> EFFECTOR_SCRIPT_RETURN_TYPE = ConfigKeys.newConfigKey( + new TypeToken>() { }, + "script.return.type", "The type of the return value from the effector", Object.class); + + public ScriptEffector(ConfigBag params) { + super(newEffectorBuilder(params).build()); + } + + public ScriptEffector(Map params) { + this(ConfigBag.newInstance(params)); + } + + public static EffectorBuilder newEffectorBuilder(ConfigBag params) { + EffectorBuilder eff = AddEffector.newEffectorBuilder(Object.class, params); + eff.impl(new Body(eff.buildAbstract(), params)); + return eff; + } + + protected static class Body extends EffectorBody { + private final Effector effector; + private final String script; + private final String language; + private final String returnVar; + private final Class returnType; + private final ScriptEngineManager factory = new ScriptEngineManager(); + private final ScriptEngine engine; + + public Body(Effector eff, ConfigBag params) { + this.effector = eff; + String content = params.get(EFFECTOR_SCRIPT_CONTENT); + String url = params.get(EFFECTOR_SCRIPT_URL); + if (Strings.isNonBlank(content)) { + this.script = content; + } else { + this.script = ResourceUtils.create().getResourceAsString(Preconditions.checkNotNull(url, "Script URL or content must be specified")); + } + this.language = params.get(EFFECTOR_SCRIPT_LANGUAGE); + this.returnVar = params.get(EFFECTOR_SCRIPT_RETURN_VAR); + this.returnType = params.get(EFFECTOR_SCRIPT_RETURN_TYPE); + this.engine = Preconditions.checkNotNull(factory.getEngineByName(language), "Engine for requested language does not exist"); + } + + @Override + public Object call(ConfigBag params) { + ScriptContext context = new SimpleScriptContext(); + + // Store effector arguments as engine scope bindings + for (ParameterType param: effector.getParameters()) { + context.setAttribute(param.getName(), params.get(Effectors.asConfigKey(param)), ScriptContext.ENGINE_SCOPE); + } + + // Add global scope object bindings + context.setAttribute("entity", entity(), ScriptContext.ENGINE_SCOPE); + context.setAttribute("managementContext", entity().getManagementContext(), ScriptContext.ENGINE_SCOPE); + context.setAttribute("task", Tasks.current(), ScriptContext.ENGINE_SCOPE); + context.setAttribute("config", params.getAllConfig(), ScriptContext.ENGINE_SCOPE); + + try { + // Execute the script and return result + Object result = engine.eval(script, context); + if (Strings.isNonBlank(returnVar)) { + result = context.getAttribute(returnVar, ScriptContext.ENGINE_SCOPE); + } + return TypeCoercions.coerce(result, returnType); + } catch (ScriptException e) { + throw Exceptions.propagate(e); + } + } + } +} diff --git a/core/src/test/java/org/apache/brooklyn/core/effector/script/ScriptEffectorTest.java b/core/src/test/java/org/apache/brooklyn/core/effector/script/ScriptEffectorTest.java new file mode 100644 index 00000000000..51106fa656c --- /dev/null +++ b/core/src/test/java/org/apache/brooklyn/core/effector/script/ScriptEffectorTest.java @@ -0,0 +1,68 @@ +/* + * 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.effector.script; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.core.effector.AddEffector; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; +import org.apache.brooklyn.entity.stock.BasicEntity; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.guava.Maybe; + +public class ScriptEffectorTest extends BrooklynAppUnitTestSupport { + + @Test + public void testAddJavaScriptEffector() { + BasicEntity entity = app.createAndManageChild(EntitySpec.create(BasicEntity.class) + .addInitializer(new ScriptEffector(ConfigBag.newInstance(ImmutableMap.of( + AddEffector.EFFECTOR_NAME, "javaScriptEffector", + ScriptEffector.EFFECTOR_SCRIPT_LANGUAGE, "js", + ScriptEffector.EFFECTOR_SCRIPT_RETURN_VAR, "o", + ScriptEffector.EFFECTOR_SCRIPT_RETURN_TYPE, String.class, + ScriptEffector.EFFECTOR_SCRIPT_CONTENT, "var o; o = \"myval\";"))))); + Maybe> javaScriptEffector = entity.getEntityType().getEffectorByName("javaScriptEffector"); + Assert.assertTrue(javaScriptEffector.isPresentAndNonNull(), "The JavaScript effector does not exist"); + Object result = Entities.invokeEffector(entity, entity, javaScriptEffector.get()).getUnchecked(); + Assert.assertTrue(result instanceof String, "Returned value is of type String"); + Assert.assertEquals(result, "myval", "Returned value is not correct"); + } + + @Test + public void testAddPythonEffector() { + BasicEntity entity = app.createAndManageChild(EntitySpec.create(BasicEntity.class) + .addInitializer(new ScriptEffector(ConfigBag.newInstance(ImmutableMap.of( + AddEffector.EFFECTOR_NAME, "pythonEffector", + ScriptEffector.EFFECTOR_SCRIPT_LANGUAGE, "python", + ScriptEffector.EFFECTOR_SCRIPT_RETURN_VAR, "o", + ScriptEffector.EFFECTOR_SCRIPT_RETURN_TYPE, String.class, + ScriptEffector.EFFECTOR_SCRIPT_CONTENT, "o = \"myval\""))))); + Maybe> pythonEffector = entity.getEntityType().getEffectorByName("pythonEffector"); + Assert.assertTrue(pythonEffector.isPresentAndNonNull(), "The Python effector does not exist"); + Object result = Entities.invokeEffector(entity, entity, pythonEffector.get()).getUnchecked(); + Assert.assertTrue(result instanceof String, "Returned value is of type String"); + Assert.assertEquals(result, "myval", "Returned value is not correct"); + } +} diff --git a/core/src/test/resources/org/apache/brooklyn/core/effector/script/script-effector-example.yaml b/core/src/test/resources/org/apache/brooklyn/core/effector/script/script-effector-example.yaml new file mode 100644 index 00000000000..69c712565ac --- /dev/null +++ b/core/src/test/resources/org/apache/brooklyn/core/effector/script/script-effector-example.yaml @@ -0,0 +1,64 @@ +# 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. + +location: + localhost + +services: +- type: org.apache.brooklyn.entity.stock.BasicStartable + brooklyn.initializers: + - type: org.apache.brooklyn.core.effector.script.ScriptEffector + brooklyn.config: + name: javascript + description: | + An effector implemented in JavaScript + parameters: + one: + type: java.lang.String + description: "A string argument" + defaultValue: "one" + two: + type: java.lang.Integer + description: "An integer argument" + defaultValue: 2 + script.language: "JavaScript" + script.return.type: java.lang.String + script.content: | + var n = 0; + var out = ""; + while (n < two) { + out += one; + n++; + } + out; + - type: org.apache.brooklyn.core.effector.script.ScriptEffector + brooklyn.config: + name: python + description: | + An effector implemented in Python + parameters: + n: + type: java.lang.Integer + defaultValue: 3 + script.language: "python" + script.return.var: "s" + script.return.type: java.lang.Integer + script.content: | + class Square: + def square(self, x): + return x * x + s = Square().square(n)