Skip to content

Commit 1722fa6

Browse files
committed
JSR-223 based StandardScriptFactory (including <lang:std> support)
This commit also completes 4.2 schema variants in spring-context. Issue: SPR-5215
1 parent 4bf3257 commit 1722fa6

File tree

24 files changed

+2438
-37
lines changed

24 files changed

+2438
-37
lines changed

spring-context/src/main/java/org/springframework/scripting/ScriptCompilationException.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -41,8 +41,7 @@ public ScriptCompilationException(String msg) {
4141
/**
4242
* Constructor for ScriptCompilationException.
4343
* @param msg the detail message
44-
* @param cause the root cause (usually from using an underlying
45-
* script compiler API)
44+
* @param cause the root cause (usually from using an underlying script compiler API)
4645
*/
4746
public ScriptCompilationException(String msg, Throwable cause) {
4847
super(msg, cause);
@@ -51,23 +50,32 @@ public ScriptCompilationException(String msg, Throwable cause) {
5150
/**
5251
* Constructor for ScriptCompilationException.
5352
* @param scriptSource the source for the offending script
54-
* @param cause the root cause (usually from using an underlying
55-
* script compiler API)
53+
* @param msg the detail message
54+
* @since 4.2
55+
*/
56+
public ScriptCompilationException(ScriptSource scriptSource, String msg) {
57+
super("Could not compile " + scriptSource + ": " + msg);
58+
this.scriptSource = scriptSource;
59+
}
60+
61+
/**
62+
* Constructor for ScriptCompilationException.
63+
* @param scriptSource the source for the offending script
64+
* @param cause the root cause (usually from using an underlying script compiler API)
5665
*/
5766
public ScriptCompilationException(ScriptSource scriptSource, Throwable cause) {
58-
super("Could not compile script", cause);
67+
super("Could not compile " + scriptSource, cause);
5968
this.scriptSource = scriptSource;
6069
}
6170

6271
/**
6372
* Constructor for ScriptCompilationException.
64-
* @param msg the detail message
6573
* @param scriptSource the source for the offending script
66-
* @param cause the root cause (usually from using an underlying
67-
* script compiler API)
74+
* @param msg the detail message
75+
* @param cause the root cause (usually from using an underlying script compiler API)
6876
*/
6977
public ScriptCompilationException(ScriptSource scriptSource, String msg, Throwable cause) {
70-
super("Could not compile script [" + scriptSource + "]: " + msg, cause);
78+
super("Could not compile " + scriptSource + ": " + msg, cause);
7179
this.scriptSource = scriptSource;
7280
}
7381

spring-context/src/main/java/org/springframework/scripting/config/LangNamespaceHandler.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -44,6 +44,7 @@ public void init() {
4444
registerScriptBeanDefinitionParser("groovy", "org.springframework.scripting.groovy.GroovyScriptFactory");
4545
registerScriptBeanDefinitionParser("jruby", "org.springframework.scripting.jruby.JRubyScriptFactory");
4646
registerScriptBeanDefinitionParser("bsh", "org.springframework.scripting.bsh.BshScriptFactory");
47+
registerScriptBeanDefinitionParser("std", "org.springframework.scripting.support.StandardScriptFactory");
4748
registerBeanDefinitionParser("defaults", new ScriptingDefaultsParser());
4849
}
4950

spring-context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -55,6 +55,8 @@
5555
*/
5656
class ScriptBeanDefinitionParser extends AbstractBeanDefinitionParser {
5757

58+
private static final String ENGINE_ATTRIBUTE = "engine";
59+
5860
private static final String SCRIPT_SOURCE_ATTRIBUTE = "script-source";
5961

6062
private static final String INLINE_SCRIPT_ELEMENT = "inline-script";
@@ -104,6 +106,9 @@ public ScriptBeanDefinitionParser(String scriptFactoryClassName) {
104106
@Override
105107
@SuppressWarnings("deprecation")
106108
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
109+
// Engine attribute only supported for <lang:std>
110+
String engine = element.getAttribute(ENGINE_ATTRIBUTE);
111+
107112
// Resolve the script source.
108113
String value = resolveScriptSource(element, parserContext.getReaderContext());
109114
if (value == null) {
@@ -184,9 +189,13 @@ else if (beanDefinitionDefaults.getDestroyMethodName() != null) {
184189
// Add constructor arguments.
185190
ConstructorArgumentValues cav = bd.getConstructorArgumentValues();
186191
int constructorArgNum = 0;
192+
if (StringUtils.hasLength(engine)) {
193+
cav.addIndexedArgumentValue(constructorArgNum++, engine);
194+
}
187195
cav.addIndexedArgumentValue(constructorArgNum++, value);
188196
if (element.hasAttribute(SCRIPT_INTERFACES_ATTRIBUTE)) {
189-
cav.addIndexedArgumentValue(constructorArgNum++, element.getAttribute(SCRIPT_INTERFACES_ATTRIBUTE));
197+
cav.addIndexedArgumentValue(
198+
constructorArgNum++, element.getAttribute(SCRIPT_INTERFACES_ATTRIBUTE), "java.lang.Class[]");
190199
}
191200

192201
// This is used for Groovy. It's a bean reference to a customizer bean.
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Copyright 2002-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.scripting.support;
18+
19+
import java.io.IOException;
20+
import javax.script.Invocable;
21+
import javax.script.ScriptEngine;
22+
import javax.script.ScriptEngineManager;
23+
24+
import org.springframework.beans.BeanUtils;
25+
import org.springframework.beans.factory.BeanClassLoaderAware;
26+
import org.springframework.scripting.ScriptCompilationException;
27+
import org.springframework.scripting.ScriptFactory;
28+
import org.springframework.scripting.ScriptSource;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.ClassUtils;
31+
import org.springframework.util.ObjectUtils;
32+
import org.springframework.util.StringUtils;
33+
34+
/**
35+
* {@link org.springframework.scripting.ScriptFactory} implementation based
36+
* on the JSR-223 script engine abstraction (as included in Java 6+).
37+
* Supports JavaScript, Groovy, JRuby and other JSR-223 compliant engines.
38+
*
39+
* <p>Typically used in combination with a
40+
* {@link org.springframework.scripting.support.ScriptFactoryPostProcessor};
41+
* see the latter's javadoc for a configuration example.
42+
*
43+
* @author Juergen Hoeller
44+
* @since 4.2
45+
* @see ScriptFactoryPostProcessor
46+
*/
47+
public class StandardScriptFactory implements ScriptFactory, BeanClassLoaderAware {
48+
49+
private final String scriptEngineName;
50+
51+
private final String scriptSourceLocator;
52+
53+
private final Class<?>[] scriptInterfaces;
54+
55+
private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
56+
57+
private volatile ScriptEngine scriptEngine;
58+
59+
60+
/**
61+
* Create a new StandardScriptFactory for the given script source.
62+
* @param scriptSourceLocator a locator that points to the source of the script.
63+
* Interpreted by the post-processor that actually creates the script.
64+
*/
65+
public StandardScriptFactory(String scriptSourceLocator) {
66+
this(null, scriptSourceLocator, (Class<?>[]) null);
67+
}
68+
69+
/**
70+
* Create a new StandardScriptFactory for the given script source.
71+
* @param scriptSourceLocator a locator that points to the source of the script.
72+
* Interpreted by the post-processor that actually creates the script.
73+
* @param scriptInterfaces the Java interfaces that the scripted object
74+
* is supposed to implement
75+
*/
76+
public StandardScriptFactory(String scriptSourceLocator, Class<?>... scriptInterfaces) {
77+
this(null, scriptSourceLocator, scriptInterfaces);
78+
}
79+
80+
/**
81+
* Create a new StandardScriptFactory for the given script source.
82+
* @param scriptEngineName the name of the JSR-223 ScriptEngine to use
83+
* (explicitly given instead of inferred from the script source)
84+
* @param scriptSourceLocator a locator that points to the source of the script.
85+
* Interpreted by the post-processor that actually creates the script.
86+
*/
87+
public StandardScriptFactory(String scriptEngineName, String scriptSourceLocator) {
88+
this(scriptEngineName, scriptSourceLocator, (Class<?>[]) null);
89+
}
90+
91+
/**
92+
* Create a new StandardScriptFactory for the given script source.
93+
* @param scriptEngineName the name of the JSR-223 ScriptEngine to use
94+
* (explicitly given instead of inferred from the script source)
95+
* @param scriptSourceLocator a locator that points to the source of the script.
96+
* Interpreted by the post-processor that actually creates the script.
97+
* @param scriptInterfaces the Java interfaces that the scripted object
98+
* is supposed to implement
99+
*/
100+
public StandardScriptFactory(String scriptEngineName, String scriptSourceLocator, Class<?>... scriptInterfaces) {
101+
Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty");
102+
this.scriptEngineName = scriptEngineName;
103+
this.scriptSourceLocator = scriptSourceLocator;
104+
this.scriptInterfaces = scriptInterfaces;
105+
}
106+
107+
108+
@Override
109+
public void setBeanClassLoader(ClassLoader classLoader) {
110+
this.beanClassLoader = classLoader;
111+
}
112+
113+
protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) {
114+
ScriptEngineManager scriptEngineManager = new ScriptEngineManager(this.beanClassLoader);
115+
if (this.scriptEngineName != null) {
116+
ScriptEngine engine = scriptEngineManager.getEngineByName(this.scriptEngineName);
117+
if (engine == null) {
118+
throw new IllegalStateException("Script engine named '" + this.scriptEngineName + "' not found");
119+
}
120+
return engine;
121+
}
122+
if (scriptSource instanceof ResourceScriptSource) {
123+
String filename = ((ResourceScriptSource) scriptSource).getResource().getFilename();
124+
if (filename != null) {
125+
String extension = StringUtils.getFilenameExtension(filename);
126+
if (extension != null) {
127+
ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension);
128+
if (engine != null) {
129+
return engine;
130+
}
131+
}
132+
}
133+
}
134+
return null;
135+
}
136+
137+
138+
@Override
139+
public String getScriptSourceLocator() {
140+
return this.scriptSourceLocator;
141+
}
142+
143+
@Override
144+
public Class<?>[] getScriptInterfaces() {
145+
return this.scriptInterfaces;
146+
}
147+
148+
@Override
149+
public boolean requiresConfigInterface() {
150+
return false;
151+
}
152+
153+
154+
/**
155+
* Load and parse the script via JSR-223's ScriptEngine.
156+
*/
157+
@Override
158+
public Object getScriptedObject(ScriptSource scriptSource, Class<?>... actualInterfaces)
159+
throws IOException, ScriptCompilationException {
160+
161+
Object script;
162+
163+
try {
164+
if (this.scriptEngine == null) {
165+
this.scriptEngine = retrieveScriptEngine(scriptSource);
166+
if (this.scriptEngine == null) {
167+
throw new IllegalStateException("Could not determine script engine for " + scriptSource);
168+
}
169+
}
170+
script = this.scriptEngine.eval(scriptSource.getScriptAsString());
171+
}
172+
catch (Exception ex) {
173+
throw new ScriptCompilationException(scriptSource, ex);
174+
}
175+
176+
if (!ObjectUtils.isEmpty(actualInterfaces)) {
177+
boolean adaptationRequired = false;
178+
for (Class<?> requestedIfc : actualInterfaces) {
179+
if (!requestedIfc.isInstance(script)) {
180+
adaptationRequired = true;
181+
}
182+
}
183+
if (adaptationRequired) {
184+
Class<?> adaptedIfc;
185+
if (actualInterfaces.length == 1) {
186+
adaptedIfc = actualInterfaces[0];
187+
}
188+
else {
189+
adaptedIfc = ClassUtils.createCompositeInterface(actualInterfaces, this.beanClassLoader);
190+
}
191+
if (adaptedIfc != null) {
192+
if (!(this.scriptEngine instanceof Invocable)) {
193+
throw new ScriptCompilationException(scriptSource,
194+
"ScriptEngine must implement Invocable in order to adapt it to an interface: " +
195+
this.scriptEngine);
196+
}
197+
Invocable invocable = (Invocable) this.scriptEngine;
198+
if (script != null) {
199+
script = invocable.getInterface(script, adaptedIfc);
200+
}
201+
if (script == null) {
202+
script = invocable.getInterface(adaptedIfc);
203+
if (script == null) {
204+
throw new ScriptCompilationException(scriptSource,
205+
"Could not adapt script to interface [" + adaptedIfc.getName() + "]");
206+
}
207+
}
208+
}
209+
}
210+
}
211+
212+
if (script instanceof Class) {
213+
Class<?> scriptClass = (Class<?>) script;
214+
try {
215+
return scriptClass.newInstance();
216+
}
217+
catch (InstantiationException ex) {
218+
throw new ScriptCompilationException(
219+
scriptSource, "Could not instantiate script class: " + scriptClass.getName(), ex);
220+
}
221+
catch (IllegalAccessException ex) {
222+
throw new ScriptCompilationException(
223+
scriptSource, "Could not access script constructor: " + scriptClass.getName(), ex);
224+
}
225+
}
226+
227+
return script;
228+
}
229+
230+
@Override
231+
public Class<?> getScriptedObjectType(ScriptSource scriptSource)
232+
throws IOException, ScriptCompilationException {
233+
234+
return null;
235+
}
236+
237+
@Override
238+
public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) {
239+
return scriptSource.isModified();
240+
}
241+
242+
243+
@Override
244+
public String toString() {
245+
return "StandardScriptFactory: script source locator [" + this.scriptSourceLocator + "]";
246+
}
247+
248+
}

0 commit comments

Comments
 (0)