Skip to content

Commit a77cb7a

Browse files
committed
Add a ScriptEffector using JSR-223 embedded scripting
1 parent dee1074 commit a77cb7a

4 files changed

Lines changed: 279 additions & 0 deletions

File tree

core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@
150150
<groupId>org.apache.commons</groupId>
151151
<artifactId>commons-lang3</artifactId>
152152
</dependency>
153+
<dependency>
154+
<groupId>org.python</groupId>
155+
<artifactId>jython</artifactId>
156+
<version>2.7.0</version>
157+
</dependency>
153158

154159
<dependency>
155160
<groupId>org.testng</groupId>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.brooklyn.core.effector.script;
20+
21+
import java.util.Map;
22+
23+
import javax.script.ScriptContext;
24+
import javax.script.ScriptEngine;
25+
import javax.script.ScriptEngineManager;
26+
import javax.script.ScriptException;
27+
import javax.script.SimpleScriptContext;
28+
29+
import org.python.core.Options;
30+
31+
import com.google.common.base.Preconditions;
32+
import com.google.common.reflect.TypeToken;
33+
34+
import org.apache.brooklyn.api.effector.Effector;
35+
import org.apache.brooklyn.api.effector.ParameterType;
36+
import org.apache.brooklyn.config.ConfigKey;
37+
import org.apache.brooklyn.core.config.ConfigKeys;
38+
import org.apache.brooklyn.core.effector.AddEffector;
39+
import org.apache.brooklyn.core.effector.EffectorBody;
40+
import org.apache.brooklyn.core.effector.Effectors;
41+
import org.apache.brooklyn.core.effector.Effectors.EffectorBuilder;
42+
import org.apache.brooklyn.util.core.ResourceUtils;
43+
import org.apache.brooklyn.util.core.config.ConfigBag;
44+
import org.apache.brooklyn.util.core.flags.SetFromFlag;
45+
import org.apache.brooklyn.util.core.flags.TypeCoercions;
46+
import org.apache.brooklyn.util.core.task.Tasks;
47+
import org.apache.brooklyn.util.exceptions.Exceptions;
48+
import org.apache.brooklyn.util.text.Strings;
49+
50+
public final class ScriptEffector<T> extends AddEffector {
51+
52+
static {
53+
Options.importSite = false;
54+
}
55+
56+
@SetFromFlag("lang")
57+
public static final ConfigKey<String> EFFECTOR_SCRIPT_LANGUAGE = ConfigKeys.newStringConfigKey(
58+
"script.language", "The scripting language the effector is written in", "JavaScript");
59+
60+
@SetFromFlag("content")
61+
public static final ConfigKey<String> EFFECTOR_SCRIPT_CONTENT = ConfigKeys.newStringConfigKey(
62+
"script.content", "The script code to evaluate for the effector");
63+
64+
@SetFromFlag("script")
65+
public static final ConfigKey<String> EFFECTOR_SCRIPT_URL = ConfigKeys.newStringConfigKey(
66+
"script.url", "A URL for the script to evaluate for the effector");
67+
68+
@SetFromFlag("return")
69+
public static final ConfigKey<String> EFFECTOR_SCRIPT_RETURN_VAR = ConfigKeys.newStringConfigKey(
70+
"script.return.var", "An optional script variable to return from the effector");
71+
72+
@SetFromFlag("type")
73+
public static final ConfigKey<Class<?>> EFFECTOR_SCRIPT_RETURN_TYPE = ConfigKeys.newConfigKey(
74+
new TypeToken<Class<?>>() { },
75+
"script.return.type", "The type of the return value from the effector", Object.class);
76+
77+
public ScriptEffector(ConfigBag params) {
78+
super(newEffectorBuilder(params).build());
79+
}
80+
81+
public ScriptEffector(Map<String,?> params) {
82+
this(ConfigBag.newInstance(params));
83+
}
84+
85+
public static EffectorBuilder<Object> newEffectorBuilder(ConfigBag params) {
86+
EffectorBuilder<Object> eff = AddEffector.newEffectorBuilder(Object.class, params);
87+
eff.impl(new Body(eff.buildAbstract(), params));
88+
return eff;
89+
}
90+
91+
protected static class Body extends EffectorBody<Object> {
92+
private final Effector<?> effector;
93+
private final String script;
94+
private final String language;
95+
private final String returnVar;
96+
private final Class<?> returnType;
97+
private final ScriptEngineManager factory = new ScriptEngineManager();
98+
private final ScriptEngine engine;
99+
100+
public Body(Effector<?> eff, ConfigBag params) {
101+
this.effector = eff;
102+
String content = params.get(EFFECTOR_SCRIPT_CONTENT);
103+
String url = params.get(EFFECTOR_SCRIPT_URL);
104+
if (Strings.isNonBlank(content)) {
105+
this.script = content;
106+
} else {
107+
this.script = ResourceUtils.create().getResourceAsString(Preconditions.checkNotNull(url, "Script URL or content must be specified"));
108+
}
109+
this.language = params.get(EFFECTOR_SCRIPT_LANGUAGE);
110+
this.returnVar = params.get(EFFECTOR_SCRIPT_RETURN_VAR);
111+
this.returnType = params.get(EFFECTOR_SCRIPT_RETURN_TYPE);
112+
this.engine = Preconditions.checkNotNull(factory.getEngineByName(language), "Engine for requested language does not exist");
113+
}
114+
115+
@Override
116+
public Object call(ConfigBag params) {
117+
ScriptContext context = new SimpleScriptContext();
118+
119+
// Store effector arguments as engine scope bindings
120+
for (ParameterType<?> param: effector.getParameters()) {
121+
context.setAttribute(param.getName(), params.get(Effectors.asConfigKey(param)), ScriptContext.ENGINE_SCOPE);
122+
}
123+
124+
// Add global scope object bindings
125+
context.setAttribute("entity", entity(), ScriptContext.ENGINE_SCOPE);
126+
context.setAttribute("managementContext", entity().getManagementContext(), ScriptContext.ENGINE_SCOPE);
127+
context.setAttribute("task", Tasks.current(), ScriptContext.ENGINE_SCOPE);
128+
context.setAttribute("config", params.getAllConfig(), ScriptContext.ENGINE_SCOPE);
129+
130+
try {
131+
// Execute the script and return result
132+
Object result = engine.eval(script, context);
133+
if (Strings.isNonBlank(returnVar)) {
134+
result = context.getAttribute(returnVar, ScriptContext.ENGINE_SCOPE);
135+
}
136+
return TypeCoercions.coerce(result, returnType);
137+
} catch (ScriptException e) {
138+
throw Exceptions.propagate(e);
139+
}
140+
}
141+
}
142+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.brooklyn.core.effector.script;
20+
21+
import org.testng.Assert;
22+
import org.testng.annotations.Test;
23+
24+
import com.google.common.collect.ImmutableMap;
25+
26+
import org.apache.brooklyn.api.effector.Effector;
27+
import org.apache.brooklyn.api.entity.EntitySpec;
28+
import org.apache.brooklyn.core.effector.AddEffector;
29+
import org.apache.brooklyn.core.entity.Entities;
30+
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
31+
import org.apache.brooklyn.entity.stock.BasicEntity;
32+
import org.apache.brooklyn.util.core.config.ConfigBag;
33+
import org.apache.brooklyn.util.guava.Maybe;
34+
35+
public class ScriptEffectorTest extends BrooklynAppUnitTestSupport {
36+
37+
@Test
38+
public void testAddJavaScriptEffector() {
39+
BasicEntity entity = app.createAndManageChild(EntitySpec.create(BasicEntity.class)
40+
.addInitializer(new ScriptEffector(ConfigBag.newInstance(ImmutableMap.of(
41+
AddEffector.EFFECTOR_NAME, "javaScriptEffector",
42+
ScriptEffector.EFFECTOR_SCRIPT_LANGUAGE, "js",
43+
ScriptEffector.EFFECTOR_SCRIPT_RETURN_VAR, "o",
44+
ScriptEffector.EFFECTOR_SCRIPT_RETURN_TYPE, String.class,
45+
ScriptEffector.EFFECTOR_SCRIPT_CONTENT, "var o; o = \"myval\";")))));
46+
Maybe<Effector<?>> javaScriptEffector = entity.getEntityType().getEffectorByName("javaScriptEffector");
47+
Assert.assertTrue(javaScriptEffector.isPresentAndNonNull(), "The JavaScript effector does not exist");
48+
Object result = Entities.invokeEffector(entity, entity, javaScriptEffector.get()).getUnchecked();
49+
Assert.assertTrue(result instanceof String, "Returned value is of type String");
50+
Assert.assertEquals(result, "myval", "Returned value is not correct");
51+
}
52+
53+
@Test
54+
public void testAddPythonEffector() {
55+
BasicEntity entity = app.createAndManageChild(EntitySpec.create(BasicEntity.class)
56+
.addInitializer(new ScriptEffector(ConfigBag.newInstance(ImmutableMap.of(
57+
AddEffector.EFFECTOR_NAME, "pythonEffector",
58+
ScriptEffector.EFFECTOR_SCRIPT_LANGUAGE, "python",
59+
ScriptEffector.EFFECTOR_SCRIPT_RETURN_VAR, "o",
60+
ScriptEffector.EFFECTOR_SCRIPT_RETURN_TYPE, String.class,
61+
ScriptEffector.EFFECTOR_SCRIPT_CONTENT, "o = \"myval\"")))));
62+
Maybe<Effector<?>> pythonEffector = entity.getEntityType().getEffectorByName("pythonEffector");
63+
Assert.assertTrue(pythonEffector.isPresentAndNonNull(), "The Python effector does not exist");
64+
Object result = Entities.invokeEffector(entity, entity, pythonEffector.get()).getUnchecked();
65+
Assert.assertTrue(result instanceof String, "Returned value is of type String");
66+
Assert.assertEquals(result, "myval", "Returned value is not correct");
67+
}
68+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
location:
19+
localhost
20+
21+
services:
22+
- type: org.apache.brooklyn.entity.stock.BasicStartable
23+
brooklyn.initializers:
24+
- type: org.apache.brooklyn.core.effector.script.ScriptEffector
25+
brooklyn.config:
26+
name: javascript
27+
description: |
28+
An effector implemented in JavaScript
29+
parameters:
30+
one:
31+
type: java.lang.String
32+
description: "A string argument"
33+
defaultValue: "one"
34+
two:
35+
type: java.lang.Integer
36+
description: "An integer argument"
37+
defaultValue: 2
38+
script.language: "JavaScript"
39+
script.return.type: java.lang.String
40+
script.content: |
41+
var n = 0;
42+
var out = "";
43+
while (n < two) {
44+
out += one;
45+
n++;
46+
}
47+
out;
48+
- type: org.apache.brooklyn.core.effector.script.ScriptEffector
49+
brooklyn.config:
50+
name: python
51+
description: |
52+
An effector implemented in Python
53+
parameters:
54+
n:
55+
type: java.lang.Integer
56+
defaultValue: 3
57+
script.language: "python"
58+
script.return.var: "s"
59+
script.return.type: java.lang.Integer
60+
script.content: |
61+
class Square:
62+
def square(self, x):
63+
return x * x
64+
s = Square().square(n)

0 commit comments

Comments
 (0)