Skip to content

Commit

Permalink
Updated effector to handle Java 8
Browse files Browse the repository at this point in the history
  • Loading branch information
grkvlt committed Jun 1, 2016
1 parent 2f99d11 commit 60e734b
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 93 deletions.
11 changes: 6 additions & 5 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,6 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.python</groupId>
<artifactId>jython</artifactId>
<version>2.7.0</version>
</dependency>

<dependency>
<groupId>org.testng</groupId>
Expand Down Expand Up @@ -200,6 +195,12 @@
<artifactId>jcommander</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.python</groupId>
<artifactId>jython</artifactId>
<version>2.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Pattern> blacklist = ImmutableList.of();

public ScriptClassLoader(ClassLoader parent, String...blacklist) {
super(parent);
this.blacklist = compileBlacklist(blacklist);
}

private List<Pattern> compileBlacklist(String...blacklist) {
List<Pattern> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<T> extends AddEffector {

static {
Options.importSite = false; // Workaround for Jython
}
private static final Logger LOG = LoggerFactory.getLogger(ScriptEffector.class);

@SetFromFlag("lang")
public static final ConfigKey<String> EFFECTOR_SCRIPT_LANGUAGE = ConfigKeys.newStringConfigKey(
Expand Down Expand Up @@ -96,8 +101,6 @@ protected static class Body extends EffectorBody<Object> {
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;
Expand All @@ -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<String> 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);
Expand All @@ -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<Object> unwrapped = Reflections.invokeMethodWithArgs(scriptUtils, "unwrap", ImmutableList.of(result));
result = unwrapped.get();
}
if (resultClass.endsWith("NativeJavaObject")) {
// JDK 7.0 Rhino interpreter
Optional<Object> 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<String> getScriptLanguages(ScriptEngineManager manager) {
List<String> languages = Lists.newArrayList();
List<ScriptEngineFactory> factories = manager.getEngineFactories();
for (ScriptEngineFactory factory : factories) {
List<String> engNames = factory.getNames();
for (String name : engNames) {
languages.add(name);
}
}
return ImmutableList.copyOf(languages);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
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;
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;
import org.apache.brooklyn.util.text.Strings;

public class ScriptEffectorTest extends BrooklynAppUnitTestSupport {

Expand All @@ -44,25 +46,53 @@ public void testAddJavaScriptEffector() {
ScriptEffector.EFFECTOR_SCRIPT_RETURN_TYPE, String.class,
ScriptEffector.EFFECTOR_SCRIPT_CONTENT, "var o; o = \"jsval\";")))));
Maybe<Effector<?>> 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<Effector<?>> 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<Effector<?>> 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");
}
}
Loading

0 comments on commit 60e734b

Please sign in to comment.