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