Skip to content

Commit c7fd4cc

Browse files
committed
StandardScriptUtils.retrieveEngineByName for lookup with descriptive exception message
Also revised StandardScriptFactory for finer-grained template methods, added further configuration variants to StandardScriptEvaluator, and identified thread-local ScriptEngine instances in ScriptTemplateView by appropriate key. Issue: SPR-13491 Issue: SPR-13487
1 parent fe3aad4 commit c7fd4cc

File tree

4 files changed

+278
-96
lines changed

4 files changed

+278
-96
lines changed

spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java

Lines changed: 61 additions & 21 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.
@@ -22,7 +22,6 @@
2222
import javax.script.ScriptEngine;
2323
import javax.script.ScriptEngineManager;
2424
import javax.script.ScriptException;
25-
import javax.script.SimpleBindings;
2625

2726
import org.springframework.beans.factory.BeanClassLoaderAware;
2827
import org.springframework.core.io.Resource;
@@ -45,34 +44,74 @@ public class StandardScriptEvaluator implements ScriptEvaluator, BeanClassLoader
4544

4645
private volatile ScriptEngineManager scriptEngineManager;
4746

48-
private String language;
47+
private String engineName;
4948

5049

5150
/**
52-
* Construct a new StandardScriptEvaluator.
51+
* Construct a new {@code StandardScriptEvaluator}.
5352
*/
5453
public StandardScriptEvaluator() {
5554
}
5655

5756
/**
58-
* Construct a new StandardScriptEvaluator.
57+
* Construct a new {@code StandardScriptEvaluator} for the given class loader.
5958
* @param classLoader the class loader to use for script engine detection
6059
*/
6160
public StandardScriptEvaluator(ClassLoader classLoader) {
6261
this.scriptEngineManager = new ScriptEngineManager(classLoader);
6362
}
6463

65-
66-
@Override
67-
public void setBeanClassLoader(ClassLoader classLoader) {
68-
this.scriptEngineManager = new ScriptEngineManager(classLoader);
64+
/**
65+
* Construct a new {@code StandardScriptEvaluator} for the given JSR-223
66+
* {@link ScriptEngineManager} to obtain script engines from.
67+
* @param scriptEngineManager the ScriptEngineManager (or subclass thereof) to use
68+
* @since 4.2.2
69+
*/
70+
public StandardScriptEvaluator(ScriptEngineManager scriptEngineManager) {
71+
this.scriptEngineManager = scriptEngineManager;
6972
}
7073

74+
7175
/**
72-
* Set the name of language meant for evaluation the scripts (e.g. "Groovy").
76+
* Set the name of the language meant for evaluating the scripts (e.g. "Groovy").
77+
* <p>This is effectively an alias for {@link #setEngineName "engineName"},
78+
* potentially (but not yet) providing common abbreviations for certain languages
79+
* beyond what the JSR-223 script engine factory exposes.
80+
* @see #setEngineName
7381
*/
7482
public void setLanguage(String language) {
75-
this.language = language;
83+
this.engineName = language;
84+
}
85+
86+
/**
87+
* Set the name of the script engine for evaluating the scripts (e.g. "Groovy"),
88+
* as exposed by the JSR-223 script engine factory.
89+
* @since 4.2.2
90+
* @see #setLanguage
91+
*/
92+
public void setEngineName(String engineName) {
93+
this.engineName = engineName;
94+
}
95+
96+
/**
97+
* Set the globally scoped bindings on the underlying script engine manager,
98+
* shared by all scripts, as an alternative to script argument bindings.
99+
* @since 4.2.2
100+
* @see #evaluate(ScriptSource, Map)
101+
* @see javax.script.ScriptEngineManager#setBindings(Bindings)
102+
* @see javax.script.SimpleBindings
103+
*/
104+
public void setGlobalBindings(Map<String, Object> globalBindings) {
105+
if (globalBindings != null) {
106+
this.scriptEngineManager.setBindings(StandardScriptUtils.getBindings(globalBindings));
107+
}
108+
}
109+
110+
@Override
111+
public void setBeanClassLoader(ClassLoader classLoader) {
112+
if (this.scriptEngineManager == null) {
113+
this.scriptEngineManager = new ScriptEngineManager(classLoader);
114+
}
76115
}
77116

78117

@@ -82,12 +121,16 @@ public Object evaluate(ScriptSource script) {
82121
}
83122

84123
@Override
85-
public Object evaluate(ScriptSource script, Map<String, Object> arguments) {
124+
public Object evaluate(ScriptSource script, Map<String, Object> argumentBindings) {
86125
ScriptEngine engine = getScriptEngine(script);
87-
Bindings bindings = (!CollectionUtils.isEmpty(arguments) ? new SimpleBindings(arguments) : null);
88126
try {
89-
return (bindings != null ? engine.eval(script.getScriptAsString(), bindings) :
90-
engine.eval(script.getScriptAsString()));
127+
if (CollectionUtils.isEmpty(argumentBindings)) {
128+
return engine.eval(script.getScriptAsString());
129+
}
130+
else {
131+
Bindings bindings = StandardScriptUtils.getBindings(argumentBindings);
132+
return engine.eval(script.getScriptAsString(), bindings);
133+
}
91134
}
92135
catch (IOException ex) {
93136
throw new ScriptCompilationException(script, "Cannot access script", ex);
@@ -106,12 +149,9 @@ protected ScriptEngine getScriptEngine(ScriptSource script) {
106149
if (this.scriptEngineManager == null) {
107150
this.scriptEngineManager = new ScriptEngineManager();
108151
}
109-
if (StringUtils.hasText(this.language)) {
110-
ScriptEngine engine = this.scriptEngineManager.getEngineByName(this.language);
111-
if (engine == null) {
112-
throw new IllegalStateException("No matching engine found for language '" + this.language + "'");
113-
}
114-
return engine;
152+
153+
if (StringUtils.hasText(this.engineName)) {
154+
return StandardScriptUtils.retrieveEngineByName(this.scriptEngineManager, this.engineName);
115155
}
116156
else if (script instanceof ResourceScriptSource) {
117157
Resource resource = ((ResourceScriptSource) script).getResource();

spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java

Lines changed: 73 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -109,31 +109,6 @@ public void setBeanClassLoader(ClassLoader classLoader) {
109109
this.beanClassLoader = classLoader;
110110
}
111111

112-
protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) {
113-
ScriptEngineManager scriptEngineManager = new ScriptEngineManager(this.beanClassLoader);
114-
if (this.scriptEngineName != null) {
115-
ScriptEngine engine = scriptEngineManager.getEngineByName(this.scriptEngineName);
116-
if (engine == null) {
117-
throw new IllegalStateException("Script engine named '" + this.scriptEngineName + "' not found");
118-
}
119-
return engine;
120-
}
121-
if (scriptSource instanceof ResourceScriptSource) {
122-
String filename = ((ResourceScriptSource) scriptSource).getResource().getFilename();
123-
if (filename != null) {
124-
String extension = StringUtils.getFilenameExtension(filename);
125-
if (extension != null) {
126-
ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension);
127-
if (engine != null) {
128-
return engine;
129-
}
130-
}
131-
}
132-
}
133-
return null;
134-
}
135-
136-
137112
@Override
138113
public String getScriptSourceLocator() {
139114
return this.scriptSourceLocator;
@@ -157,54 +132,18 @@ public boolean requiresConfigInterface() {
157132
public Object getScriptedObject(ScriptSource scriptSource, Class<?>... actualInterfaces)
158133
throws IOException, ScriptCompilationException {
159134

160-
Object script;
161-
162-
try {
163-
if (this.scriptEngine == null) {
164-
this.scriptEngine = retrieveScriptEngine(scriptSource);
165-
if (this.scriptEngine == null) {
166-
throw new IllegalStateException("Could not determine script engine for " + scriptSource);
167-
}
168-
}
169-
script = this.scriptEngine.eval(scriptSource.getScriptAsString());
170-
}
171-
catch (Exception ex) {
172-
throw new ScriptCompilationException(scriptSource, ex);
173-
}
135+
Object script = evaluateScript(scriptSource);
174136

175137
if (!ObjectUtils.isEmpty(actualInterfaces)) {
176138
boolean adaptationRequired = false;
177139
for (Class<?> requestedIfc : actualInterfaces) {
178-
if (!requestedIfc.isInstance(script)) {
140+
if (script instanceof Class ? !requestedIfc.isAssignableFrom((Class<?>) script) :
141+
!requestedIfc.isInstance(script)) {
179142
adaptationRequired = true;
180143
}
181144
}
182145
if (adaptationRequired) {
183-
Class<?> adaptedIfc;
184-
if (actualInterfaces.length == 1) {
185-
adaptedIfc = actualInterfaces[0];
186-
}
187-
else {
188-
adaptedIfc = ClassUtils.createCompositeInterface(actualInterfaces, this.beanClassLoader);
189-
}
190-
if (adaptedIfc != null) {
191-
if (!(this.scriptEngine instanceof Invocable)) {
192-
throw new ScriptCompilationException(scriptSource,
193-
"ScriptEngine must implement Invocable in order to adapt it to an interface: " +
194-
this.scriptEngine);
195-
}
196-
Invocable invocable = (Invocable) this.scriptEngine;
197-
if (script != null) {
198-
script = invocable.getInterface(script, adaptedIfc);
199-
}
200-
if (script == null) {
201-
script = invocable.getInterface(adaptedIfc);
202-
if (script == null) {
203-
throw new ScriptCompilationException(scriptSource,
204-
"Could not adapt script to interface [" + adaptedIfc.getName() + "]");
205-
}
206-
}
207-
}
146+
script = adaptToInterfaces(script, scriptSource, actualInterfaces);
208147
}
209148
}
210149

@@ -226,6 +165,75 @@ public Object getScriptedObject(ScriptSource scriptSource, Class<?>... actualInt
226165
return script;
227166
}
228167

168+
protected Object evaluateScript(ScriptSource scriptSource) {
169+
try {
170+
if (this.scriptEngine == null) {
171+
this.scriptEngine = retrieveScriptEngine(scriptSource);
172+
if (this.scriptEngine == null) {
173+
throw new IllegalStateException("Could not determine script engine for " + scriptSource);
174+
}
175+
}
176+
return this.scriptEngine.eval(scriptSource.getScriptAsString());
177+
}
178+
catch (Exception ex) {
179+
throw new ScriptCompilationException(scriptSource, ex);
180+
}
181+
}
182+
183+
protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) {
184+
ScriptEngineManager scriptEngineManager = new ScriptEngineManager(this.beanClassLoader);
185+
186+
if (this.scriptEngineName != null) {
187+
return StandardScriptUtils.retrieveEngineByName(scriptEngineManager, this.scriptEngineName);
188+
}
189+
190+
if (scriptSource instanceof ResourceScriptSource) {
191+
String filename = ((ResourceScriptSource) scriptSource).getResource().getFilename();
192+
if (filename != null) {
193+
String extension = StringUtils.getFilenameExtension(filename);
194+
if (extension != null) {
195+
ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension);
196+
if (engine != null) {
197+
return engine;
198+
}
199+
}
200+
}
201+
}
202+
203+
return null;
204+
}
205+
206+
protected Object adaptToInterfaces(Object script, ScriptSource scriptSource, Class<?>... actualInterfaces) {
207+
Class<?> adaptedIfc;
208+
if (actualInterfaces.length == 1) {
209+
adaptedIfc = actualInterfaces[0];
210+
}
211+
else {
212+
adaptedIfc = ClassUtils.createCompositeInterface(actualInterfaces, this.beanClassLoader);
213+
}
214+
215+
if (adaptedIfc != null) {
216+
if (!(this.scriptEngine instanceof Invocable)) {
217+
throw new ScriptCompilationException(scriptSource,
218+
"ScriptEngine must implement Invocable in order to adapt it to an interface: " +
219+
this.scriptEngine);
220+
}
221+
Invocable invocable = (Invocable) this.scriptEngine;
222+
if (script != null) {
223+
script = invocable.getInterface(script, adaptedIfc);
224+
}
225+
if (script == null) {
226+
script = invocable.getInterface(adaptedIfc);
227+
if (script == null) {
228+
throw new ScriptCompilationException(scriptSource,
229+
"Could not adapt script to interface [" + adaptedIfc.getName() + "]");
230+
}
231+
}
232+
}
233+
234+
return script;
235+
}
236+
229237
@Override
230238
public Class<?> getScriptedObjectType(ScriptSource scriptSource)
231239
throws IOException, ScriptCompilationException {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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.util.LinkedHashSet;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Set;
23+
import javax.script.Bindings;
24+
import javax.script.ScriptContext;
25+
import javax.script.ScriptEngine;
26+
import javax.script.ScriptEngineFactory;
27+
import javax.script.ScriptEngineManager;
28+
import javax.script.SimpleBindings;
29+
30+
/**
31+
* Common operations for dealing with a JSR-223 {@link ScriptEngine}.
32+
*
33+
* @author Juergen Hoeller
34+
* @since 4.2.2
35+
*/
36+
public abstract class StandardScriptUtils {
37+
38+
/**
39+
* Retrieve a {@link ScriptEngine} from the given {@link ScriptEngineManager}
40+
* by name, delegating to {@link ScriptEngineManager#getEngineByName} but
41+
* throwing a descriptive exception if not found or if initialization failed.
42+
* @param scriptEngineManager the ScriptEngineManager to use
43+
* @param engineName the name of the engine
44+
* @return a corresponding ScriptEngine (never {@code null})
45+
* @throws IllegalArgumentException if no matching engine has been found
46+
* @throws IllegalStateException if no matching engine has been found or if
47+
*/
48+
public static ScriptEngine retrieveEngineByName(ScriptEngineManager scriptEngineManager, String engineName) {
49+
ScriptEngine engine = scriptEngineManager.getEngineByName(engineName);
50+
if (engine == null) {
51+
Set<String> engineNames = new LinkedHashSet<String>();
52+
for (ScriptEngineFactory engineFactory : scriptEngineManager.getEngineFactories()) {
53+
List<String> factoryNames = engineFactory.getNames();
54+
if (factoryNames.contains(engineName)) {
55+
// Special case: getEngineByName returned null but engine is present...
56+
// Let's assume it failed to initialize (which ScriptEngineManager silently swallows).
57+
// If it happens to initialize fine now, alright, but we really expect an exception.
58+
try {
59+
engine = engineFactory.getScriptEngine();
60+
engine.setBindings(scriptEngineManager.getBindings(), ScriptContext.GLOBAL_SCOPE);
61+
}
62+
catch (Throwable ex) {
63+
throw new IllegalStateException("Script engine with name '" + engineName +
64+
"' failed to initialize", ex);
65+
}
66+
}
67+
engineNames.addAll(factoryNames);
68+
}
69+
throw new IllegalArgumentException("Script engine with name '" + engineName +
70+
"' not found; registered engine names: " + engineNames);
71+
}
72+
return engine;
73+
}
74+
75+
static Bindings getBindings(Map<String, Object> bindings) {
76+
return (bindings instanceof Bindings ? (Bindings) bindings : new SimpleBindings(bindings));
77+
}
78+
79+
}

0 commit comments

Comments
 (0)