Skip to content

Commit 0eb870f

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

File tree

4 files changed

+274
-0
lines changed

4 files changed

+274
-0
lines changed

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: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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.Bindings;
24+
import javax.script.ScriptContext;
25+
import javax.script.ScriptEngine;
26+
import javax.script.ScriptEngineManager;
27+
import javax.script.ScriptException;
28+
import javax.script.SimpleScriptContext;
29+
30+
import com.google.common.base.Preconditions;
31+
import com.google.common.reflect.TypeToken;
32+
33+
import org.apache.brooklyn.api.effector.Effector;
34+
import org.apache.brooklyn.api.effector.ParameterType;
35+
import org.apache.brooklyn.config.ConfigKey;
36+
import org.apache.brooklyn.core.config.ConfigKeys;
37+
import org.apache.brooklyn.core.effector.AddEffector;
38+
import org.apache.brooklyn.core.effector.EffectorBody;
39+
import org.apache.brooklyn.core.effector.Effectors;
40+
import org.apache.brooklyn.core.effector.Effectors.EffectorBuilder;
41+
import org.apache.brooklyn.util.core.ResourceUtils;
42+
import org.apache.brooklyn.util.core.config.ConfigBag;
43+
import org.apache.brooklyn.util.core.flags.SetFromFlag;
44+
import org.apache.brooklyn.util.core.flags.TypeCoercions;
45+
import org.apache.brooklyn.util.core.task.Tasks;
46+
import org.apache.brooklyn.util.exceptions.Exceptions;
47+
import org.apache.brooklyn.util.text.Strings;
48+
49+
public final class ScriptEffector<T> extends AddEffector {
50+
51+
@SetFromFlag("lang")
52+
public static final ConfigKey<String> EFFECTOR_SCRIPT_LANGUAGE = ConfigKeys.newStringConfigKey(
53+
"script.language", "The scripting language the effector is written in", "JavaScript");
54+
55+
@SetFromFlag("content")
56+
public static final ConfigKey<String> EFFECTOR_SCRIPT_CONTENT = ConfigKeys.newStringConfigKey(
57+
"script.content", "The script code to evaluate for the effector");
58+
59+
@SetFromFlag("script")
60+
public static final ConfigKey<String> EFFECTOR_SCRIPT_URL = ConfigKeys.newStringConfigKey(
61+
"script.url", "A URL for the script to evaluate for the effector");
62+
63+
@SetFromFlag("return")
64+
public static final ConfigKey<String> EFFECTOR_SCRIPT_RETURN_VAR = ConfigKeys.newStringConfigKey(
65+
"script.return.var", "An optional script variable to return from the effector");
66+
67+
@SetFromFlag("type")
68+
public static final ConfigKey<Class<?>> EFFECTOR_SCRIPT_RETURN_TYPE = ConfigKeys.newConfigKey(
69+
new TypeToken<Class<?>>() { },
70+
"script.return.type", "The type of the return value from the effector", Object.class);
71+
72+
public ScriptEffector(ConfigBag params) {
73+
super(newEffectorBuilder(params).build());
74+
}
75+
76+
public ScriptEffector(Map<String,?> params) {
77+
this(ConfigBag.newInstance(params));
78+
}
79+
80+
public static EffectorBuilder<Object> newEffectorBuilder(ConfigBag params) {
81+
EffectorBuilder<Object> eff = AddEffector.newEffectorBuilder(Object.class, params);
82+
eff.impl(new Body(eff.buildAbstract(), params));
83+
return eff;
84+
}
85+
86+
protected static class Body extends EffectorBody<Object> {
87+
private final Effector<?> effector;
88+
private final String script;
89+
private final String language;
90+
private final String returnVar;
91+
private final Class<?> returnType;
92+
private final ScriptEngineManager factory = new ScriptEngineManager();
93+
private final ScriptEngine engine;
94+
95+
public Body(Effector<?> eff, ConfigBag params) {
96+
this.effector = eff;
97+
String content = params.get(EFFECTOR_SCRIPT_CONTENT);
98+
String url = params.get(EFFECTOR_SCRIPT_URL);
99+
if (Strings.isNonBlank(content)) {
100+
this.script = content;
101+
} else {
102+
this.script = ResourceUtils.create().getResourceAsString(Preconditions.checkNotNull(url, "Script URL or content must be specified"));
103+
}
104+
this.language = params.get(EFFECTOR_SCRIPT_LANGUAGE);
105+
this.returnVar = params.get(EFFECTOR_SCRIPT_RETURN_VAR);
106+
this.returnType = params.get(EFFECTOR_SCRIPT_RETURN_TYPE);
107+
this.engine = Preconditions.checkNotNull(factory.getEngineByName(language), "Engine for requested language does not exist");
108+
}
109+
110+
@Override
111+
public Object call(ConfigBag params) {
112+
ScriptContext context = new SimpleScriptContext();
113+
114+
// Store effector arguments as engine scope bindings
115+
for (ParameterType<?> param: effector.getParameters()) {
116+
context.setAttribute(param.getName(), params.get(Effectors.asConfigKey(param)), ScriptContext.ENGINE_SCOPE);
117+
}
118+
119+
// Add global scope object bindings
120+
context.setAttribute("entity", entity(), ScriptContext.ENGINE_SCOPE);
121+
context.setAttribute("managementContext", entity().getManagementContext(), ScriptContext.ENGINE_SCOPE);
122+
context.setAttribute("task", Tasks.current(), ScriptContext.ENGINE_SCOPE);
123+
context.setAttribute("config", params.getAllConfig(), ScriptContext.ENGINE_SCOPE);
124+
125+
try {
126+
// Execute the script and return result
127+
Object result = engine.eval(script, context);
128+
if (Strings.isNonBlank(returnVar)) {
129+
result = context.getAttribute(returnVar, ScriptContext.ENGINE_SCOPE);
130+
}
131+
return TypeCoercions.coerce(result, returnType);
132+
} catch (ScriptException e) {
133+
throw Exceptions.propagate(e);
134+
}
135+
}
136+
}
137+
}
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, "jython",
59+
ScriptEffector.EFFECTOR_SCRIPT_RETURN_VAR, "o",
60+
ScriptEffector.EFFECTOR_SCRIPT_RETURN_TYPE, String.class,
61+
ScriptEffector.EFFECTOR_SCRIPT_CONTENT, "var 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)