From 2f99d11640f9c14235ddf82ad5f5c86dbd274f0d Mon Sep 17 00:00:00 2001 From: Andrew Donald Kennedy Date: Fri, 20 May 2016 13:50:49 +0100 Subject: [PATCH 1/2] Add a ScriptEffector using JSR-223 embedded scripting --- core/pom.xml | 5 + .../core/effector/script/ScriptEffector.java | 147 ++++++++++++++++++ .../effector/script/ScriptEffectorTest.java | 68 ++++++++ .../script/script-effector-example.yaml | 88 +++++++++++ 4 files changed, 308 insertions(+) create mode 100644 core/src/main/java/org/apache/brooklyn/core/effector/script/ScriptEffector.java create mode 100644 core/src/test/java/org/apache/brooklyn/core/effector/script/ScriptEffectorTest.java create mode 100644 core/src/test/resources/org/apache/brooklyn/core/effector/script/script-effector-example.yaml diff --git a/core/pom.xml b/core/pom.xml index ef3e850f7d..7a67dc27dc 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 0000000000..aca88d3a93 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/effector/script/ScriptEffector.java @@ -0,0 +1,147 @@ +/* + * 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; + +import sun.org.mozilla.javascript.internal.NativeJavaObject; + +public final class ScriptEffector extends AddEffector { + + static { + Options.importSite = false; // Workaround for Jython + } + + @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); + } + if (result instanceof NativeJavaObject) { // Unwarap JavaScript return values + result = ((NativeJavaObject) result).unwrap(); + } + 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 0000000000..099cdb9ee4 --- /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 = \"jsval\";"))))); + 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, "jsval", "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 = \"pyval\""))))); + 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, "pyval", "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 0000000000..94b281cf11 --- /dev/null +++ b/core/src/test/resources/org/apache/brooklyn/core/effector/script/script-effector-example.yaml @@ -0,0 +1,88 @@ +# 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 int(x) * int(x) + + s = Square().square(n) + - type: org.apache.brooklyn.core.effector.script.ScriptEffector + brooklyn.config: + name: renameEntity + description: | + A JavaScript effector manipulating entity data + parameters: + name: + type: java.lang.String + description: "The new entity name" + script.language: "JavaScript" + script.content: | + entity.setDisplayName(name); + - type: org.apache.brooklyn.core.effector.script.ScriptEffector + brooklyn.config: + name: getId + description: | + A Python effector returning entity data + script.language: "python" + script.return.var: "id" + script.return.type: java.lang.String + script.content: | + id = entity.getId() From 60e734b1f0ba237bc6d3a257b262c3d6dcd2b240 Mon Sep 17 00:00:00 2001 From: Andrew Donald Kennedy Date: Wed, 25 May 2016 18:07:05 +0100 Subject: [PATCH 2/2] Updated effector to handle Java 8 --- core/pom.xml | 11 +- .../effector/script/ScriptClassLoader.java | 69 +++++++++ .../core/effector/script/ScriptEffector.java | 77 ++++++++-- .../effector/script/ScriptEffectorTest.java | 40 ++++- .../script/script-effector-example.yaml | 145 +++++++++--------- 5 files changed, 249 insertions(+), 93 deletions(-) create mode 100644 core/src/main/java/org/apache/brooklyn/core/effector/script/ScriptClassLoader.java diff --git a/core/pom.xml b/core/pom.xml index 7a67dc27dc..47a3ab14e7 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -150,11 +150,6 @@ org.apache.commons commons-lang3 - - org.python - jython - 2.7.0 - org.testng @@ -200,6 +195,12 @@ jcommander test + + org.python + jython + 2.7.0 + test + org.apache.httpcomponents httpclient diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/script/ScriptClassLoader.java b/core/src/main/java/org/apache/brooklyn/core/effector/script/ScriptClassLoader.java new file mode 100644 index 0000000000..090bd160e7 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/effector/script/ScriptClassLoader.java @@ -0,0 +1,69 @@ +/* + * 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.List; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +/** + * Blocks access to any {@code org.apache.brooklyn} classes + * with an entitlements check. + */ +public class ScriptClassLoader extends ClassLoader { + + private static final Logger LOG = LoggerFactory.getLogger(ScriptClassLoader.class); + + private List blacklist = ImmutableList.of(); + + public ScriptClassLoader(ClassLoader parent, String...blacklist) { + super(parent); + this.blacklist = compileBlacklist(blacklist); + } + + private List compileBlacklist(String...blacklist) { + List patterns = Lists.newArrayList(); + for (String entry : blacklist) { + patterns.add(Pattern.compile(entry)); + } + return ImmutableList.copyOf(patterns); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + LOG.info("Script class loader: {}", name); + for (Pattern pattern : blacklist) { + if (pattern.matcher(name).matches()) { + throw new ClassNotFoundException(String.format("Class %s is blacklisted: %s", name, pattern.pattern())); + } + } + return super.loadClass(name, resolve); + } + + @Override + public String toString() { + return String.format("ScriptClassLoader %s", Iterables.toString(blacklist)); + } +} 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 index aca88d3a93..d48a77c667 100644 --- 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 @@ -18,17 +18,25 @@ */ package org.apache.brooklyn.core.effector.script; +import java.lang.reflect.InvocationTargetException; +import java.util.List; import java.util.Map; import javax.script.ScriptContext; import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import javax.script.SimpleScriptContext; -import org.python.core.Options; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.google.common.reflect.TypeToken; import org.apache.brooklyn.api.effector.Effector; @@ -45,15 +53,12 @@ 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.javalang.Reflections; import org.apache.brooklyn.util.text.Strings; -import sun.org.mozilla.javascript.internal.NativeJavaObject; - public final class ScriptEffector extends AddEffector { - static { - Options.importSite = false; // Workaround for Jython - } + private static final Logger LOG = LoggerFactory.getLogger(ScriptEffector.class); @SetFromFlag("lang") public static final ConfigKey EFFECTOR_SCRIPT_LANGUAGE = ConfigKeys.newStringConfigKey( @@ -96,8 +101,6 @@ protected static class Body extends EffectorBody { 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; @@ -106,24 +109,44 @@ public Body(Effector eff, ConfigBag params) { if (Strings.isNonBlank(content)) { this.script = content; } else { - this.script = ResourceUtils.create().getResourceAsString(Preconditions.checkNotNull(url, "Script URL or content must be specified")); + Preconditions.checkNotNull(url, "Script URL or content must be specified"); + this.script = ResourceUtils.create().getResourceAsString(url); } 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"); + + // Check the language is supported by trying to create a ScriptEngine + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(language); + if (engine == null) { + String message = "Script language not supported: " + language; + LOG.warn(message); + if (LOG.isDebugEnabled()) { + List supported = getScriptLanguages(manager); + LOG.debug("Supported languages for scripts: " + Joiner.on(',').join(supported)); + } + throw new IllegalStateException(message); + } } @Override public Object call(ConfigBag params) { + ClassLoader parentLoader = Thread.currentThread().getContextClassLoader(); + ClassLoader scriptLoader = new ScriptClassLoader(parentLoader, "org.apache.brooklyn.*"); + Thread.currentThread().setContextClassLoader(scriptLoader); + ScriptEngineManager manager = new ScriptEngineManager(scriptLoader); + ScriptEngine engine = manager.getEngineByName(language); + ScriptContext defaultContext = engine.getContext(); ScriptContext context = new SimpleScriptContext(); + context.setBindings(defaultContext.getBindings(ScriptContext.ENGINE_SCOPE), ScriptContext.ENGINE_SCOPE); // 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 + // Add 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); @@ -135,13 +158,39 @@ public Object call(ConfigBag params) { if (Strings.isNonBlank(returnVar)) { result = context.getAttribute(returnVar, ScriptContext.ENGINE_SCOPE); } - if (result instanceof NativeJavaObject) { // Unwarap JavaScript return values - result = ((NativeJavaObject) result).unwrap(); + String resultClass = result.getClass().getName(); + + // Unwrap JavaScript return values to underlying Java object + if (resultClass.endsWith("ScriptObjectMirror")) { + // JDK 8.0 Nashorn interpreter + Class scriptUtils = Class.forName("jdk.nashorn.api.scripting.ScriptUtils"); + Optional unwrapped = Reflections.invokeMethodWithArgs(scriptUtils, "unwrap", ImmutableList.of(result)); + result = unwrapped.get(); + } + if (resultClass.endsWith("NativeJavaObject")) { + // JDK 7.0 Rhino interpreter + Optional unwrapped = Reflections.invokeMethodWithArgs(result, "unwrap", ImmutableList.of()); + result = unwrapped.get(); } return TypeCoercions.coerce(result, returnType); - } catch (ScriptException e) { + } catch (ScriptException | InvocationTargetException | IllegalAccessException | ClassNotFoundException e) { throw Exceptions.propagate(e); + } finally { + Thread.currentThread().setContextClassLoader(parentLoader); + } + } + + /** Returns a list of all JSR-223 script language names. */ + private List getScriptLanguages(ScriptEngineManager manager) { + List languages = Lists.newArrayList(); + List factories = manager.getEngineFactories(); + for (ScriptEngineFactory factory : factories) { + List engNames = factory.getNames(); + for (String name : engNames) { + languages.add(name); + } } + return ImmutableList.copyOf(languages); } } } 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 index 099cdb9ee4..ff2e13672a 100644 --- 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 @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap; import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.core.effector.AddEffector; import org.apache.brooklyn.core.entity.Entities; @@ -31,6 +32,7 @@ import org.apache.brooklyn.entity.stock.BasicEntity; import org.apache.brooklyn.util.core.config.ConfigBag; import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.text.Strings; public class ScriptEffectorTest extends BrooklynAppUnitTestSupport { @@ -44,25 +46,53 @@ public void testAddJavaScriptEffector() { ScriptEffector.EFFECTOR_SCRIPT_RETURN_TYPE, String.class, ScriptEffector.EFFECTOR_SCRIPT_CONTENT, "var o; o = \"jsval\";"))))); Maybe> javaScriptEffector = entity.getEntityType().getEffectorByName("javaScriptEffector"); - Assert.assertTrue(javaScriptEffector.isPresentAndNonNull(), "The JavaScript effector does not exist"); + Assert.assertTrue(javaScriptEffector.isPresentAndNonNull(), "The 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.assertTrue(result instanceof String, "Returned value is not of type String"); Assert.assertEquals(result, "jsval", "Returned value is not correct"); } @Test + public void testJavaScriptEffectorEntityAccess() { + BasicEntity entity = app.createAndManageChild(EntitySpec.create(BasicEntity.class) + .addInitializer(new ScriptEffector(ConfigBag.newInstance(ImmutableMap.of( + AddEffector.EFFECTOR_NAME, "entityEffector", + ScriptEffector.EFFECTOR_SCRIPT_LANGUAGE, "js", + ScriptEffector.EFFECTOR_SCRIPT_RETURN_VAR, "a", + ScriptEffector.EFFECTOR_SCRIPT_RETURN_TYPE, Entity.class, + ScriptEffector.EFFECTOR_SCRIPT_CONTENT, "var a; a = entity.getApplication();"))))); + Maybe> entityEffector = entity.getEntityType().getEffectorByName("entityEffector"); + Assert.assertTrue(entityEffector.isPresentAndNonNull(), "The effector does not exist"); + Object result = Entities.invokeEffector(entity, entity, entityEffector.get()).getUnchecked(); + Assert.assertTrue(result instanceof Entity, "Returned value is not of type Entity"); + Assert.assertEquals(result, app, "Returned value is not correct"); + } + + @Test(expectedExceptions = IllegalStateException.class, + expectedExceptionsMessageRegExp = "Script language not supported: ruby") + public void testAddRubyEffector() { + app.createAndManageChild(EntitySpec.create(BasicEntity.class) + .addInitializer(new ScriptEffector(ConfigBag.newInstance(ImmutableMap.of( + AddEffector.EFFECTOR_NAME, "rubyEffector", + ScriptEffector.EFFECTOR_SCRIPT_LANGUAGE, "ruby", + ScriptEffector.EFFECTOR_SCRIPT_CONTENT, "print \"Ruby\\n\""))))); + } + + // TODO requires python.path to be set as JVM property + @Test(enabled = false) public void testAddPythonEffector() { + // System.setProperty("python.path", "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/"); 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_LANGUAGE, "jython", ScriptEffector.EFFECTOR_SCRIPT_RETURN_VAR, "o", ScriptEffector.EFFECTOR_SCRIPT_RETURN_TYPE, String.class, ScriptEffector.EFFECTOR_SCRIPT_CONTENT, "o = \"pyval\""))))); Maybe> pythonEffector = entity.getEntityType().getEffectorByName("pythonEffector"); - Assert.assertTrue(pythonEffector.isPresentAndNonNull(), "The Python effector does not exist"); + Assert.assertTrue(pythonEffector.isPresentAndNonNull(), "The 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.assertTrue(result instanceof String, "Returned value is not of type String"); Assert.assertEquals(result, "pyval", "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 index 94b281cf11..7d3c568d45 100644 --- 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 @@ -15,74 +15,81 @@ # specific language governing permissions and limitations # under the License. -location: - localhost +brooklyn.catalog: + version: "0.10.0-SNAPSHOT" # BROOKLYN_VERSION -services: -- type: org.apache.brooklyn.entity.stock.BasicStartable - brooklyn.initializers: + brooklyn.libraries: + - https://oss.sonatype.org/service/local/repositories/releases/content/org/python/jython/2.7.0/jython-2.7.0.jar - - 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 int(x) * int(x) - - s = Square().square(n) - - type: org.apache.brooklyn.core.effector.script.ScriptEffector - brooklyn.config: - name: renameEntity - description: | - A JavaScript effector manipulating entity data - parameters: - name: - type: java.lang.String - description: "The new entity name" - script.language: "JavaScript" - script.content: | - entity.setDisplayName(name); - - type: org.apache.brooklyn.core.effector.script.ScriptEffector - brooklyn.config: - name: getId - description: | - A Python effector returning entity data - script.language: "python" - script.return.var: "id" - script.return.type: java.lang.String - script.content: | - id = entity.getId() + items: + - id: script-effector-example + name: "Script Effector Example" + description: | + An example entity with multiple effectors implemented using JSR-223 + scripting. Includes Jython library for Python effectors. + itemType: entity + item: + 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 int(x) * int(x) + s = Square().square(n) + - type: org.apache.brooklyn.core.effector.script.ScriptEffector + brooklyn.config: + name: renameEntity + description: | + A JavaScript effector manipulating entity data + parameters: + name: + type: java.lang.String + description: "The new entity name" + script.language: "JavaScript" + script.content: | + entity.setDisplayName(name); + - type: org.apache.brooklyn.core.effector.script.ScriptEffector + brooklyn.config: + name: getId + description: | + A Python effector returning entity data + script.language: "python" + script.return.var: "id" + script.return.type: java.lang.String \ No newline at end of file