From c7fd4ccf4848c39d68524f68a668833f2fa4a07e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 23 Sep 2015 22:37:35 +0200 Subject: [PATCH] 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 --- .../support/StandardScriptEvaluator.java | 82 ++++++++--- .../support/StandardScriptFactory.java | 138 +++++++++--------- .../support/StandardScriptUtils.java | 79 ++++++++++ .../view/script/ScriptTemplateView.java | 75 ++++++++-- 4 files changed, 278 insertions(+), 96 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/scripting/support/StandardScriptUtils.java diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java index d2fd038b27..484a4a2cc3 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import javax.script.Bindings; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; -import javax.script.SimpleBindings; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.io.Resource; @@ -45,34 +44,74 @@ public class StandardScriptEvaluator implements ScriptEvaluator, BeanClassLoader private volatile ScriptEngineManager scriptEngineManager; - private String language; + private String engineName; /** - * Construct a new StandardScriptEvaluator. + * Construct a new {@code StandardScriptEvaluator}. */ public StandardScriptEvaluator() { } /** - * Construct a new StandardScriptEvaluator. + * Construct a new {@code StandardScriptEvaluator} for the given class loader. * @param classLoader the class loader to use for script engine detection */ public StandardScriptEvaluator(ClassLoader classLoader) { this.scriptEngineManager = new ScriptEngineManager(classLoader); } + /** + * Construct a new {@code StandardScriptEvaluator} for the given JSR-223 + * {@link ScriptEngineManager} to obtain script engines from. + * @param scriptEngineManager the ScriptEngineManager (or subclass thereof) to use + * @since 4.2.2 + */ + public StandardScriptEvaluator(ScriptEngineManager scriptEngineManager) { + this.scriptEngineManager = scriptEngineManager; + } - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - this.scriptEngineManager = new ScriptEngineManager(classLoader); + + /** + * Set the name of the language meant for evaluating the scripts (e.g. "Groovy"). + *

This is effectively an alias for {@link #setEngineName "engineName"}, + * potentially (but not yet) providing common abbreviations for certain languages + * beyond what the JSR-223 script engine factory exposes. + * @see #setEngineName + */ + public void setLanguage(String language) { + this.engineName = language; } /** - * Set the name of language meant for evaluation the scripts (e.g. "Groovy"). + * Set the name of the script engine for evaluating the scripts (e.g. "Groovy"), + * as exposed by the JSR-223 script engine factory. + * @since 4.2.2 + * @see #setLanguage */ - public void setLanguage(String language) { - this.language = language; + public void setEngineName(String engineName) { + this.engineName = engineName; + } + + /** + * Set the globally scoped bindings on the underlying script engine manager, + * shared by all scripts, as an alternative to script argument bindings. + * @since 4.2.2 + * @see #evaluate(ScriptSource, Map) + * @see javax.script.ScriptEngineManager#setBindings(Bindings) + * @see javax.script.SimpleBindings + */ + public void setGlobalBindings(Map globalBindings) { + if (globalBindings != null) { + this.scriptEngineManager.setBindings(StandardScriptUtils.getBindings(globalBindings)); + } + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + if (this.scriptEngineManager == null) { + this.scriptEngineManager = new ScriptEngineManager(classLoader); + } } @@ -82,12 +121,16 @@ public class StandardScriptEvaluator implements ScriptEvaluator, BeanClassLoader } @Override - public Object evaluate(ScriptSource script, Map arguments) { + public Object evaluate(ScriptSource script, Map argumentBindings) { ScriptEngine engine = getScriptEngine(script); - Bindings bindings = (!CollectionUtils.isEmpty(arguments) ? new SimpleBindings(arguments) : null); try { - return (bindings != null ? engine.eval(script.getScriptAsString(), bindings) : - engine.eval(script.getScriptAsString())); + if (CollectionUtils.isEmpty(argumentBindings)) { + return engine.eval(script.getScriptAsString()); + } + else { + Bindings bindings = StandardScriptUtils.getBindings(argumentBindings); + return engine.eval(script.getScriptAsString(), bindings); + } } catch (IOException ex) { throw new ScriptCompilationException(script, "Cannot access script", ex); @@ -106,12 +149,9 @@ public class StandardScriptEvaluator implements ScriptEvaluator, BeanClassLoader if (this.scriptEngineManager == null) { this.scriptEngineManager = new ScriptEngineManager(); } - if (StringUtils.hasText(this.language)) { - ScriptEngine engine = this.scriptEngineManager.getEngineByName(this.language); - if (engine == null) { - throw new IllegalStateException("No matching engine found for language '" + this.language + "'"); - } - return engine; + + if (StringUtils.hasText(this.engineName)) { + return StandardScriptUtils.retrieveEngineByName(this.scriptEngineManager, this.engineName); } else if (script instanceof ResourceScriptSource) { Resource resource = ((ResourceScriptSource) script).getResource(); diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java index a01991b41b..bff51a79ea 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java @@ -109,31 +109,6 @@ public class StandardScriptFactory implements ScriptFactory, BeanClassLoaderAwar this.beanClassLoader = classLoader; } - protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) { - ScriptEngineManager scriptEngineManager = new ScriptEngineManager(this.beanClassLoader); - if (this.scriptEngineName != null) { - ScriptEngine engine = scriptEngineManager.getEngineByName(this.scriptEngineName); - if (engine == null) { - throw new IllegalStateException("Script engine named '" + this.scriptEngineName + "' not found"); - } - return engine; - } - if (scriptSource instanceof ResourceScriptSource) { - String filename = ((ResourceScriptSource) scriptSource).getResource().getFilename(); - if (filename != null) { - String extension = StringUtils.getFilenameExtension(filename); - if (extension != null) { - ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension); - if (engine != null) { - return engine; - } - } - } - } - return null; - } - - @Override public String getScriptSourceLocator() { return this.scriptSourceLocator; @@ -157,54 +132,18 @@ public class StandardScriptFactory implements ScriptFactory, BeanClassLoaderAwar public Object getScriptedObject(ScriptSource scriptSource, Class... actualInterfaces) throws IOException, ScriptCompilationException { - Object script; - - try { - if (this.scriptEngine == null) { - this.scriptEngine = retrieveScriptEngine(scriptSource); - if (this.scriptEngine == null) { - throw new IllegalStateException("Could not determine script engine for " + scriptSource); - } - } - script = this.scriptEngine.eval(scriptSource.getScriptAsString()); - } - catch (Exception ex) { - throw new ScriptCompilationException(scriptSource, ex); - } + Object script = evaluateScript(scriptSource); if (!ObjectUtils.isEmpty(actualInterfaces)) { boolean adaptationRequired = false; for (Class requestedIfc : actualInterfaces) { - if (!requestedIfc.isInstance(script)) { + if (script instanceof Class ? !requestedIfc.isAssignableFrom((Class) script) : + !requestedIfc.isInstance(script)) { adaptationRequired = true; } } if (adaptationRequired) { - Class adaptedIfc; - if (actualInterfaces.length == 1) { - adaptedIfc = actualInterfaces[0]; - } - else { - adaptedIfc = ClassUtils.createCompositeInterface(actualInterfaces, this.beanClassLoader); - } - if (adaptedIfc != null) { - if (!(this.scriptEngine instanceof Invocable)) { - throw new ScriptCompilationException(scriptSource, - "ScriptEngine must implement Invocable in order to adapt it to an interface: " + - this.scriptEngine); - } - Invocable invocable = (Invocable) this.scriptEngine; - if (script != null) { - script = invocable.getInterface(script, adaptedIfc); - } - if (script == null) { - script = invocable.getInterface(adaptedIfc); - if (script == null) { - throw new ScriptCompilationException(scriptSource, - "Could not adapt script to interface [" + adaptedIfc.getName() + "]"); - } - } - } + script = adaptToInterfaces(script, scriptSource, actualInterfaces); } } @@ -226,6 +165,75 @@ public class StandardScriptFactory implements ScriptFactory, BeanClassLoaderAwar return script; } + protected Object evaluateScript(ScriptSource scriptSource) { + try { + if (this.scriptEngine == null) { + this.scriptEngine = retrieveScriptEngine(scriptSource); + if (this.scriptEngine == null) { + throw new IllegalStateException("Could not determine script engine for " + scriptSource); + } + } + return this.scriptEngine.eval(scriptSource.getScriptAsString()); + } + catch (Exception ex) { + throw new ScriptCompilationException(scriptSource, ex); + } + } + + protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) { + ScriptEngineManager scriptEngineManager = new ScriptEngineManager(this.beanClassLoader); + + if (this.scriptEngineName != null) { + return StandardScriptUtils.retrieveEngineByName(scriptEngineManager, this.scriptEngineName); + } + + if (scriptSource instanceof ResourceScriptSource) { + String filename = ((ResourceScriptSource) scriptSource).getResource().getFilename(); + if (filename != null) { + String extension = StringUtils.getFilenameExtension(filename); + if (extension != null) { + ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension); + if (engine != null) { + return engine; + } + } + } + } + + return null; + } + + protected Object adaptToInterfaces(Object script, ScriptSource scriptSource, Class... actualInterfaces) { + Class adaptedIfc; + if (actualInterfaces.length == 1) { + adaptedIfc = actualInterfaces[0]; + } + else { + adaptedIfc = ClassUtils.createCompositeInterface(actualInterfaces, this.beanClassLoader); + } + + if (adaptedIfc != null) { + if (!(this.scriptEngine instanceof Invocable)) { + throw new ScriptCompilationException(scriptSource, + "ScriptEngine must implement Invocable in order to adapt it to an interface: " + + this.scriptEngine); + } + Invocable invocable = (Invocable) this.scriptEngine; + if (script != null) { + script = invocable.getInterface(script, adaptedIfc); + } + if (script == null) { + script = invocable.getInterface(adaptedIfc); + if (script == null) { + throw new ScriptCompilationException(scriptSource, + "Could not adapt script to interface [" + adaptedIfc.getName() + "]"); + } + } + } + + return script; + } + @Override public Class getScriptedObjectType(ScriptSource scriptSource) throws IOException, ScriptCompilationException { diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptUtils.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptUtils.java new file mode 100644 index 0000000000..50206a7db4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed 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.springframework.scripting.support; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; +import javax.script.SimpleBindings; + +/** + * Common operations for dealing with a JSR-223 {@link ScriptEngine}. + * + * @author Juergen Hoeller + * @since 4.2.2 + */ +public abstract class StandardScriptUtils { + + /** + * Retrieve a {@link ScriptEngine} from the given {@link ScriptEngineManager} + * by name, delegating to {@link ScriptEngineManager#getEngineByName} but + * throwing a descriptive exception if not found or if initialization failed. + * @param scriptEngineManager the ScriptEngineManager to use + * @param engineName the name of the engine + * @return a corresponding ScriptEngine (never {@code null}) + * @throws IllegalArgumentException if no matching engine has been found + * @throws IllegalStateException if no matching engine has been found or if + */ + public static ScriptEngine retrieveEngineByName(ScriptEngineManager scriptEngineManager, String engineName) { + ScriptEngine engine = scriptEngineManager.getEngineByName(engineName); + if (engine == null) { + Set engineNames = new LinkedHashSet(); + for (ScriptEngineFactory engineFactory : scriptEngineManager.getEngineFactories()) { + List factoryNames = engineFactory.getNames(); + if (factoryNames.contains(engineName)) { + // Special case: getEngineByName returned null but engine is present... + // Let's assume it failed to initialize (which ScriptEngineManager silently swallows). + // If it happens to initialize fine now, alright, but we really expect an exception. + try { + engine = engineFactory.getScriptEngine(); + engine.setBindings(scriptEngineManager.getBindings(), ScriptContext.GLOBAL_SCOPE); + } + catch (Throwable ex) { + throw new IllegalStateException("Script engine with name '" + engineName + + "' failed to initialize", ex); + } + } + engineNames.addAll(factoryNames); + } + throw new IllegalArgumentException("Script engine with name '" + engineName + + "' not found; registered engine names: " + engineNames); + } + return engine; + } + + static Bindings getBindings(Map bindings) { + return (bindings instanceof Bindings ? (Bindings) bindings : new SimpleBindings(bindings)); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java index e9a5096cc7..8430b7e6d6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java @@ -22,6 +22,8 @@ import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import javax.script.Invocable; @@ -39,8 +41,10 @@ import org.springframework.core.NamedThreadLocal; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.scripting.support.StandardScriptUtils; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.servlet.view.AbstractUrlBasedView; @@ -57,6 +61,7 @@ import org.springframework.web.servlet.view.AbstractUrlBasedView; * {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} for more details. * * @author Sebastien Deleuze + * @author Juergen Hoeller * @since 4.2 * @see ScriptTemplateConfigurer * @see ScriptTemplateViewResolver @@ -69,8 +74,9 @@ public class ScriptTemplateView extends AbstractUrlBasedView { private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:"; - private static final ThreadLocal engineHolder = - new NamedThreadLocal("ScriptTemplateView engine"); + + private static final ThreadLocal> enginesHolder = + new NamedThreadLocal>("ScriptTemplateView engines"); private ScriptEngine engine; @@ -87,9 +93,11 @@ public class ScriptTemplateView extends AbstractUrlBasedView { private Charset charset; + private String resourceLoaderPath; + private ResourceLoader resourceLoader; - private String resourceLoaderPath; + private volatile ScriptEngineManager scriptEngineManager; /** @@ -234,12 +242,20 @@ public class ScriptTemplateView extends AbstractUrlBasedView { Assert.isTrue(this.renderFunction != null, "The 'renderFunction' property must be defined."); } + protected ScriptEngine getEngine() { if (Boolean.FALSE.equals(this.sharedEngine)) { - ScriptEngine engine = engineHolder.get(); + Map engines = enginesHolder.get(); + if (engines == null) { + engines = new HashMap(4); + enginesHolder.set(engines); + } + Object engineKey = (!ObjectUtils.isEmpty(this.scripts) ? + new EngineKey(this.engineName, this.scripts) : this.engineName); + ScriptEngine engine = engines.get(engineKey); if (engine == null) { engine = createEngineFromName(); - engineHolder.set(engine); + engines.put(engineKey, engine); } return engine; } @@ -250,21 +266,22 @@ public class ScriptTemplateView extends AbstractUrlBasedView { } protected ScriptEngine createEngineFromName() { - ScriptEngine engine = new ScriptEngineManager().getEngineByName(this.engineName); - if (engine == null) { - throw new IllegalStateException("No engine with name '" + this.engineName + "' found"); + if (this.scriptEngineManager == null) { + this.scriptEngineManager = new ScriptEngineManager(getApplicationContext().getClassLoader()); } + + ScriptEngine engine = StandardScriptUtils.retrieveEngineByName(this.scriptEngineManager, this.engineName); loadScripts(engine); return engine; } protected void loadScripts(ScriptEngine engine) { - if (this.scripts != null) { + if (!ObjectUtils.isEmpty(this.scripts)) { try { for (String script : this.scripts) { Resource resource = this.resourceLoader.getResource(script); if (!resource.exists()) { - throw new IllegalStateException("Resource " + script + " not found"); + throw new IllegalStateException("Script resource [" + script + "] not found"); } engine.eval(new InputStreamReader(resource.getInputStream())); } @@ -309,6 +326,7 @@ public class ScriptTemplateView extends AbstractUrlBasedView { } } + @Override protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { super.prepareResponse(request, response); @@ -326,6 +344,7 @@ public class ScriptTemplateView extends AbstractUrlBasedView { Invocable invocable = (Invocable) engine; String url = getUrl(); String template = getTemplate(url); + Object html; if (this.renderObject != null) { Object thiz = engine.eval(this.renderObject); @@ -334,6 +353,7 @@ public class ScriptTemplateView extends AbstractUrlBasedView { else { html = invocable.invokeFunction(this.renderFunction, template, model, url); } + response.getWriter().write(String.valueOf(html)); } catch (Exception ex) { @@ -347,4 +367,39 @@ public class ScriptTemplateView extends AbstractUrlBasedView { return FileCopyUtils.copyToString(reader); } + + /** + * Key class for the {@code enginesHolder ThreadLocal}. + * Only used if scripts have been specified; otherwise, the + * {@code engineName String} will be used as cache key directly. + */ + private static class EngineKey { + + private final String engineName; + + private final String[] scripts; + + public EngineKey(String engineName, String[] scripts) { + this.engineName = engineName; + this.scripts = scripts; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof EngineKey)) { + return false; + } + EngineKey otherKey = (EngineKey) other; + return (this.engineName.equals(otherKey.engineName) && Arrays.equals(this.scripts, otherKey.scripts)); + } + + @Override + public int hashCode() { + return (this.engineName.hashCode() * 29 + Arrays.hashCode(this.scripts)); + } + } + }