Support non thread-safe ScriptEngine in ScriptTemplateView

This commit adds a new sharedEngine property to ScriptTemplateConfigurer
and ScriptTemplateView in order to support non thread-safe ScriptEngine
implementations like Nashorn.

When this flag is set to false, the engine is retrieved from a
ThreadLocal<ScriptEngine> field instead of a ScriptEngine one.

Also as part of this commit, all the initialization logic has been moved from
ScriptTemplateConfigurer to ScriptTemplateView since the script engine can
now be lazily initialized multiple time in the view when sharedEngine is
set to false.

Issue: SPR-13034
This commit is contained in:
Sebastien Deleuze
2015-07-15 13:34:16 +02:00
parent 0783a1c667
commit 34de167e59
9 changed files with 407 additions and 284 deletions

View File

@@ -73,6 +73,9 @@ public class ScriptTemplateConfigurerBeanDefinitionParser extends AbstractSimple
if (element.hasAttribute("resource-loader-path")) {
builder.addPropertyValue("resourceLoaderPath", element.getAttribute("resource-loader-path"));
}
if (element.hasAttribute("shared-engine")) {
builder.addPropertyValue("sharedEngine", element.getAttribute("shared-engine"));
}
}
@Override

View File

@@ -19,8 +19,6 @@ package org.springframework.web.servlet.view.script;
import java.nio.charset.Charset;
import javax.script.ScriptEngine;
import org.springframework.core.io.ResourceLoader;
/**
* Interface to be implemented by objects that configure and manage a
* {@link ScriptEngine} for automatic lookup in a web environment.
@@ -33,12 +31,18 @@ public interface ScriptTemplateConfig {
ScriptEngine getEngine();
String getEngineName();
String[] getScripts();
String getRenderObject();
String getRenderFunction();
Charset getCharset();
ResourceLoader getResourceLoader();
String getResourceLoaderPath();
Boolean isShareEngine();
}

View File

@@ -16,28 +16,9 @@
package org.springframework.web.servlet.view.script;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* An implementation of Spring MVC's {@link ScriptTemplateConfig} for creating
@@ -59,37 +40,44 @@ import org.springframework.util.StringUtils;
* }
* </pre>
*
* <p>It is possible to use non thread-safe script engines and templating libraries, like
* Handlebars or React running on Nashorn, by setting the
* {@link #setSharedEngine(Boolean) sharedEngine} property to {@code false}.
*
* @author Sebastien Deleuze
* @since 4.2
* @see ScriptTemplateView
*/
public class ScriptTemplateConfigurer implements ScriptTemplateConfig, ApplicationContextAware, InitializingBean {
public class ScriptTemplateConfigurer implements ScriptTemplateConfig {
private ScriptEngine engine;
private String engineName;
private ApplicationContext applicationContext;
private String[] scripts;
private String renderObject;
private String renderFunction;
private Charset charset = Charset.forName("UTF-8");
private Charset charset;
private ResourceLoader resourceLoader;
private String resourceLoaderPath;
private String resourceLoaderPath = "classpath:";
private Boolean sharedEngine;
/**
* Set the {@link ScriptEngine} to use by the view.
* The script engine must implement {@code Invocable}.
* You must define {@code engine} or {@code engineName}, not both.
*
* <p>When the {@code sharedEngine} flag is set to {@code false}, you should not specify
* the script engine with this setter, but with the {@link #setEngineName(String)}
* one (since it implies multiple lazy instanciations of the script engine).
*
* @see #setEngineName(String)
*/
public void setEngine(ScriptEngine engine) {
Assert.isInstanceOf(Invocable.class, engine);
this.engine = engine;
}
@@ -102,18 +90,15 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig, Applicati
* Set the engine name that will be used to instantiate the {@link ScriptEngine}.
* The script engine must implement {@code Invocable}.
* You must define {@code engine} or {@code engineName}, not both.
* @see #setEngine(ScriptEngine)
*/
public void setEngineName(String engineName) {
this.engineName = engineName;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
protected ApplicationContext getApplicationContext() {
return this.applicationContext;
public String getEngineName() {
return this.engineName;
}
/**
@@ -134,8 +119,8 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig, Applicati
}
@Override
public String getRenderObject() {
return renderObject;
public String[] getScripts() {
return this.scripts;
}
/**
@@ -148,8 +133,8 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig, Applicati
}
@Override
public String getRenderFunction() {
return renderFunction;
public String getRenderObject() {
return this.renderObject;
}
/**
@@ -164,6 +149,11 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig, Applicati
this.renderFunction = renderFunction;
}
@Override
public String getRenderFunction() {
return this.renderFunction;
}
/**
* Set the charset used to read script and template files.
* ({@code UTF-8} by default).
@@ -189,69 +179,30 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig, Applicati
this.resourceLoaderPath = resourceLoaderPath;
}
@Override
public String getResourceLoaderPath() {
return resourceLoaderPath;
return this.resourceLoaderPath;
}
/**
* When set to {@code false}, use thread-local {@link ScriptEngine} instances instead
* of one single shared instance. This flag should be set to {@code false} for those
* using non thread-safe script engines and templating libraries, like Handlebars or
* React running on Nashorn for example.
*
* <p>When this flag is set to {@code false}, the script engine must be specified using
* {@link #setEngineName(String)}. Using {@link #setEngine(ScriptEngine)} is not
* possible because multiple instances of the script engine need to be created lazily
* (one per thread).
* @see <a href="http://docs.oracle.com/javase/8/docs/api/javax/script/ScriptEngineFactory.html#getParameter-java.lang.String-">THREADING ScriptEngine parameter<a/>
*/
public void setSharedEngine(Boolean sharedEngine) {
this.sharedEngine = sharedEngine;
}
@Override
public ResourceLoader getResourceLoader() {
return resourceLoader;
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.engine == null) {
this.engine = createScriptEngine();
}
Assert.state(this.renderFunction != null, "renderFunction property must be defined.");
this.resourceLoader = new DefaultResourceLoader(createClassLoader());
if (this.scripts != null) {
try {
for (String script : this.scripts) {
this.engine.eval(read(script));
}
}
catch (ScriptException e) {
throw new IllegalStateException("could not load script", e);
}
}
}
protected ClassLoader createClassLoader() throws IOException {
String[] paths = StringUtils.commaDelimitedListToStringArray(this.resourceLoaderPath);
List<URL> urls = new ArrayList<URL>();
for (String path : paths) {
Resource[] resources = getApplicationContext().getResources(path);
if (resources.length > 0) {
for (Resource resource : resources) {
if (resource.exists()) {
urls.add(resource.getURL());
}
}
}
}
ClassLoader classLoader = getApplicationContext().getClassLoader();
return (urls.size() > 0 ? new URLClassLoader(urls.toArray(new URL[urls.size()]), classLoader) : classLoader);
}
private Reader read(String path) throws IOException {
Resource resource = this.resourceLoader.getResource(path);
Assert.state(resource.exists(), "Resource " + path + " not found.");
return new InputStreamReader(resource.getInputStream());
}
protected ScriptEngine createScriptEngine() throws IOException {
if (this.engine != null && this.engineName != null) {
throw new IllegalStateException("You should define engine or engineName properties, not both.");
}
if (this.engineName != null) {
ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName(this.engineName);
Assert.state(scriptEngine != null, "No engine \"" + this.engineName + "\" found.");
Assert.state(scriptEngine instanceof Invocable, "Script engine should be instance of Invocable");
this.engine = scriptEngine;
}
Assert.state(this.engine != null, "No script engine found, please specify valid engine or engineName properties.");
return this.engine;
public Boolean isShareEngine() {
return this.sharedEngine;
}
}

View File

@@ -17,10 +17,17 @@
package org.springframework.web.servlet.view.script;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -29,17 +36,26 @@ import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
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.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.view.AbstractUrlBasedView;
/**
* An {@link org.springframework.web.servlet.view.AbstractUrlBasedView AbstractUrlBasedView}
* designed to run any template library based on a JSR-223 script engine.
*
* <p>Nashorn Javascript engine requires Java 8+.
* <p>If not set, each property is auto-detected by looking up up a single
* {@link ScriptTemplateConfig} bean in the web application context and using
* it to obtain the configured properties.
*
* <p>Nashorn Javascript engine requires Java 8+, and may require setting the
* {@code sharedEngine} property to {@code false} in order to run properly. See
* {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} for more details.
*
* @author Sebastien Deleuze
* @since 4.2
@@ -48,8 +64,20 @@ import org.springframework.web.servlet.view.AbstractUrlBasedView;
*/
public class ScriptTemplateView extends AbstractUrlBasedView {
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:";
private ScriptEngine engine;
private final ThreadLocal<ScriptEngine> engineHolder =
new NamedThreadLocal<ScriptEngine>("ScriptTemplateView engine");
private String engineName;
private String[] scripts;
private String renderObject;
private String renderFunction;
@@ -58,58 +86,182 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
private ResourceLoader resourceLoader;
private String resourceLoaderPath;
private Boolean sharedEngine;
/**
* Set the {@link ScriptEngine} to use in this view.
* <p>If not set, the engine is auto-detected by looking up up a single
* {@link ScriptTemplateConfig} bean in the web application context and using
* it to obtain the configured {@code ScriptEngine} instance.
* @see ScriptTemplateConfig
* See {@link ScriptTemplateConfigurer#setEngine(ScriptEngine)} documentation.
*/
public void setEngine(ScriptEngine engine) {
Assert.isInstanceOf(Invocable.class, engine);
this.engine = engine;
}
protected ScriptEngine getEngine() {
if (Boolean.FALSE.equals(this.sharedEngine)) {
ScriptEngine engine = this.engineHolder.get();
if (engine == null) {
engine = createEngineFromName();
this.engineHolder.set(engine);
}
return engine;
}
else if (this.engine == null) {
setEngine(createEngineFromName());
}
return this.engine;
}
protected ScriptEngine createEngineFromName() {
Assert.notNull(this.engineName);
ScriptEngine engine = new ScriptEngineManager().getEngineByName(this.engineName);
Assert.state(engine != null, "No engine \"" + this.engineName + "\" found.");
loadScripts(engine);
return engine;
}
protected void loadScripts(ScriptEngine engine) {
Assert.notNull(engine);
if (this.scripts != null) {
try {
for (String script : this.scripts) {
Resource resource = this.resourceLoader.getResource(script);
Assert.state(resource.exists(), "Resource " + script + " not found.");
engine.eval(new InputStreamReader(resource.getInputStream()));
}
}
catch (ScriptException e) {
throw new IllegalStateException("could not load script", e);
}
catch (IOException e) {
throw new IllegalStateException("could not load script", e);
}
}
}
/**
* Set the render function name. This function will be called with the
* following parameters:
* <ol>
* <li>{@code template}: the view template content (String)</li>
* <li>{@code model}: the view model (Map)</li>
* </ol>
* <p>If not set, the function name is auto-detected by looking up up a single
* {@link ScriptTemplateConfig} bean in the web application context and using
* it to obtain the configured {@code functionName} property.
* @see ScriptTemplateConfig
* See {@link ScriptTemplateConfigurer#setEngineName(String)} documentation.
*/
public void setEngineName(String engineName) {
this.engineName = engineName;
}
/**
* See {@link ScriptTemplateConfigurer#setScripts(String...)} documentation.
*/
public void setScripts(String... scripts) {
this.scripts = scripts;
}
/**
* See {@link ScriptTemplateConfigurer#setRenderObject(String)} documentation.
*/
public void setRenderObject(String renderObject) {
this.renderObject = renderObject;
}
/**
* See {@link ScriptTemplateConfigurer#setRenderFunction(String)} documentation.
*/
public void setRenderFunction(String functionName) {
this.renderFunction = functionName;
}
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
/**
* See {@link ScriptTemplateConfigurer#setCharset(Charset)} documentation.
*/
public void setCharset(Charset charset) {
this.charset = charset;
}
/**
* See {@link ScriptTemplateConfigurer#setResourceLoaderPath(String)} documentation.
*/
public void setResourceLoaderPath(String resourceLoaderPath) {
this.resourceLoaderPath = resourceLoaderPath;
}
/**
* See {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} documentation.
*/
public void setSharedEngine(Boolean sharedEngine) {
this.sharedEngine = sharedEngine;
}
@Override
protected void initApplicationContext(ApplicationContext context) {
super.initApplicationContext(context);
ScriptTemplateConfig viewConfig = autodetectViewConfig();
if (this.engine == null) {
this.engine = viewConfig.getEngine();
Assert.state(this.engine != null, "Script engine should not be null.");
Assert.state(this.engine instanceof Invocable, "Script engine should be instance of Invocable");
if (this.engine == null && viewConfig.getEngine() != null) {
setEngine(viewConfig.getEngine());
}
if (this.resourceLoader == null) {
this.resourceLoader = viewConfig.getResourceLoader();
if (this.engineName == null && viewConfig.getEngineName() != null) {
this.engineName = viewConfig.getEngineName();
}
if (this.renderObject == null) {
if (this.scripts == null && viewConfig.getScripts() != null) {
this.scripts = viewConfig.getScripts();
}
if (this.renderObject == null && viewConfig.getRenderObject() != null) {
this.renderObject = viewConfig.getRenderObject();
}
if (this.renderFunction == null) {
if (this.renderFunction == null && viewConfig.getRenderFunction() != null) {
this.renderFunction = viewConfig.getRenderFunction();
}
if (this.charset == null) {
this.charset = viewConfig.getCharset();
this.charset = viewConfig.getCharset() == null ? DEFAULT_CHARSET : viewConfig.getCharset();
}
if (this.resourceLoaderPath == null) {
this.resourceLoaderPath = viewConfig.getResourceLoaderPath() == null ?
DEFAULT_RESOURCE_LOADER_PATH : viewConfig.getResourceLoaderPath();
}
if (this.resourceLoader == null) {
this.resourceLoader = new DefaultResourceLoader(createClassLoader());
}
if (this.sharedEngine == null && viewConfig.isShareEngine() != null) {
this.sharedEngine = viewConfig.isShareEngine();
}
Assert.state(!(this.engine != null && this.engineName != null),
"You should define engine or engineName properties, not both.");
Assert.state(!(this.engine == null && this.engineName == null),
"No script engine found, please specify valid engine or engineName properties.");
if (Boolean.FALSE.equals(this.sharedEngine)) {
Assert.state(this.engineName != null,
"When sharedEngine property is set to false, you should specify the " +
"script engine using the engineName property, not the engine one.");
}
Assert.state(this.renderFunction != null, "renderFunction property must be defined.");
if (this.engine != null) {
loadScripts(this.engine);
}
else {
setEngine(createEngineFromName());
}
}
protected ClassLoader createClassLoader() {
String[] paths = StringUtils.commaDelimitedListToStringArray(this.resourceLoaderPath);
List<URL> urls = new ArrayList<URL>();
try {
for (String path : paths) {
Resource[] resources = getApplicationContext().getResources(path);
if (resources.length > 0) {
for (Resource resource : resources) {
if (resource.exists()) {
urls.add(resource.getURL());
}
}
}
}
} catch (IOException e) {
throw new IllegalStateException("Cannot create class loader: " + e.getMessage());
}
ClassLoader classLoader = getApplicationContext().getClassLoader();
return (urls.size() > 0 ? new URLClassLoader(urls.toArray(new URL[urls.size()]), classLoader) : classLoader);
}
protected ScriptTemplateConfig autodetectViewConfig() throws BeansException {
@@ -128,13 +280,13 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
Assert.notNull("Render function must not be null", this.renderFunction);
try {
String template = getTemplate(getUrl());
Object html = null;
Object html;
if (this.renderObject != null) {
Object thiz = engine.eval(this.renderObject);
html = ((Invocable)this.engine).invokeMethod(thiz, this.renderFunction, template, model);
html = ((Invocable)getEngine()).invokeMethod(thiz, this.renderFunction, template, model);
}
else {
html = ((Invocable)this.engine).invokeFunction(this.renderFunction, template, model);
html = ((Invocable)getEngine()).invokeFunction(this.renderFunction, template, model);
}
response.getWriter().write(String.valueOf(html));
}

View File

@@ -1260,6 +1260,15 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="shared-engine" type="xsd:boolean">
<xsd:annotation>
<xsd:documentation><![CDATA[
When set to false, use thread-local ScriptEngine instances instead of one single shared
instance. This flag should be set to false for those using non thread-safe script engines
and templating libraries, like Handlebars or React running on Nashorn for example.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>

View File

@@ -807,6 +807,7 @@ public class MvcNamespaceTests {
assertEquals("render", scriptTemplateConfigurer.getRenderFunction());
assertEquals(StandardCharsets.ISO_8859_1, scriptTemplateConfigurer.getCharset());
assertEquals("classpath:", scriptTemplateConfigurer.getResourceLoaderPath());
assertFalse(scriptTemplateConfigurer.isShareEngine());
String[] scripts = { "org/springframework/web/servlet/view/script/nashorn/render.js" };
accessor = new DirectFieldAccessor(scriptTemplateConfigurer);
assertArrayEquals(scripts, (String[]) accessor.getPropertyValue("scripts"));

View File

@@ -1,115 +0,0 @@
/*
* 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.web.servlet.view.script;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import org.hamcrest.Matchers;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import org.springframework.context.support.StaticApplicationContext;
/**
* Unit tests for {@link ScriptTemplateConfigurer}.
*
* @author Sebastien Deleuze
*/
public class ScriptTemplateConfigurerTests {
private static final String RESOURCE_LOADER_PATH = "classpath:org/springframework/web/servlet/view/script/";
private StaticApplicationContext applicationContext;
private ScriptTemplateConfigurer configurer;
@Before
public void setup() throws Exception {
this.applicationContext = new StaticApplicationContext();
this.configurer = new ScriptTemplateConfigurer();
this.configurer.setResourceLoaderPath(RESOURCE_LOADER_PATH);
}
@Test
public void customEngineAndRenderFunction() throws Exception {
this.configurer.setApplicationContext(this.applicationContext);
ScriptEngine engine = mock(InvocableScriptEngine.class);
given(engine.get("key")).willReturn("value");
this.configurer.setEngine(engine);
this.configurer.setRenderFunction("render");
this.configurer.afterPropertiesSet();
engine = this.configurer.getEngine();
assertNotNull(engine);
assertEquals("value", engine.get("key"));
assertNull(this.configurer.getRenderObject());
assertEquals("render", this.configurer.getRenderFunction());
assertEquals(StandardCharsets.UTF_8, this.configurer.getCharset());
}
@Test(expected = IllegalArgumentException.class)
public void nonInvocableScriptEngine() throws Exception {
this.configurer.setApplicationContext(this.applicationContext);
ScriptEngine engine = mock(ScriptEngine.class);
this.configurer.setEngine(engine);
}
@Test(expected = IllegalStateException.class)
public void noRenderFunctionDefined() throws Exception {
this.configurer.setApplicationContext(this.applicationContext);
ScriptEngine engine = mock(InvocableScriptEngine.class);
this.configurer.setEngine(engine);
this.configurer.afterPropertiesSet();
}
@Test
public void parentLoader() throws Exception {
this.configurer.setApplicationContext(this.applicationContext);
ClassLoader classLoader = this.configurer.createClassLoader();
assertNotNull(classLoader);
URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
assertThat(Arrays.asList(urlClassLoader.getURLs()), Matchers.hasSize(1));
assertThat(Arrays.asList(urlClassLoader.getURLs()).get(0).toString(),
Matchers.endsWith("org/springframework/web/servlet/view/script/"));
this.configurer.setResourceLoaderPath(RESOURCE_LOADER_PATH + ",classpath:org/springframework/web/servlet/view/");
classLoader = this.configurer.createClassLoader();
assertNotNull(classLoader);
urlClassLoader = (URLClassLoader) classLoader;
assertThat(Arrays.asList(urlClassLoader.getURLs()), Matchers.hasSize(2));
assertThat(Arrays.asList(urlClassLoader.getURLs()).get(0).toString(),
Matchers.endsWith("org/springframework/web/servlet/view/script/"));
assertThat(Arrays.asList(urlClassLoader.getURLs()).get(1).toString(),
Matchers.endsWith("org/springframework/web/servlet/view/"));
}
private interface InvocableScriptEngine extends ScriptEngine, Invocable {
}
}

View File

@@ -16,16 +16,22 @@
package org.springframework.web.servlet.view.script;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.servlet.ServletContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.hamcrest.Matchers;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.BDDMockito.given;
@@ -33,9 +39,7 @@ import static org.mockito.Mockito.mock;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.ApplicationContextException;
import org.springframework.mock.web.test.MockServletContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.context.support.StaticApplicationContext;
/**
* Unit tests for {@link ScriptTemplateView}.
@@ -44,63 +48,176 @@ import org.springframework.web.context.WebApplicationContext;
*/
public class ScriptTemplateViewTests {
private WebApplicationContext webAppContext;
private ScriptTemplateView view;
private ScriptTemplateConfigurer configurer;
private StaticApplicationContext applicationContext;
private static final String RESOURCE_LOADER_PATH = "classpath:org/springframework/web/servlet/view/script/";
private ServletContext servletContext;
@Before
public void setup() {
this.webAppContext = mock(WebApplicationContext.class);
this.servletContext = new MockServletContext();
this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.webAppContext);
this.configurer = new ScriptTemplateConfigurer();
this.applicationContext = new StaticApplicationContext();
this.applicationContext.getBeanFactory().registerSingleton("scriptTemplateConfigurer", this.configurer);
this.view = new ScriptTemplateView();
this.view.setUrl("sampleView");
}
@Test
public void missingScriptTemplateConfig() throws Exception {
ScriptTemplateView view = new ScriptTemplateView();
given(this.webAppContext.getBeansOfType(ScriptTemplateConfig.class, true, false))
.willReturn(new HashMap<String, ScriptTemplateConfig>());
view.setUrl("sampleView");
try {
view.setApplicationContext(this.webAppContext);
fail();
this.view.setApplicationContext(new StaticApplicationContext());
}
catch (ApplicationContextException ex) {
assertTrue(ex.getMessage().contains("ScriptTemplateConfig"));
return;
}
fail();
}
@Test
public void dectectScriptTemplateConfig() throws Exception {
public void detectScriptTemplateConfigWithEngine() {
InvocableScriptEngine engine = mock(InvocableScriptEngine.class);
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngine(engine);
configurer.setRenderObject("Template");
configurer.setRenderFunction("render");
configurer.setCharset(StandardCharsets.ISO_8859_1);
Map<String, ScriptTemplateConfig> configMap = new HashMap<String, ScriptTemplateConfig>();
configMap.put("scriptTemplateConfigurer", configurer);
ScriptTemplateView view = new ScriptTemplateView();
given(this.webAppContext.getBeansOfType(ScriptTemplateConfig.class, true, false)).willReturn(configMap);
DirectFieldAccessor accessor = new DirectFieldAccessor(view);
view.setUrl("sampleView");
view.setApplicationContext(this.webAppContext);
this.configurer.setEngine(engine);
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setCharset(StandardCharsets.ISO_8859_1);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.applicationContext);
assertEquals(engine, accessor.getPropertyValue("engine"));
assertEquals(StandardCharsets.ISO_8859_1, accessor.getPropertyValue("charset"));
assertEquals("Template", accessor.getPropertyValue("renderObject"));
assertEquals("render", accessor.getPropertyValue("renderFunction"));
assertEquals(StandardCharsets.ISO_8859_1, accessor.getPropertyValue("charset"));
}
@Test(expected = IllegalArgumentException.class)
@Test
public void detectScriptTemplateConfigWithEngineName() {
this.configurer.setEngineName("nashorn");
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setCharset(StandardCharsets.ISO_8859_1);
this.configurer.setSharedEngine(true);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.applicationContext);
assertEquals("nashorn", accessor.getPropertyValue("engineName"));
assertNotNull(accessor.getPropertyValue("engine"));
assertEquals("Template", accessor.getPropertyValue("renderObject"));
assertEquals("render", accessor.getPropertyValue("renderFunction"));
assertEquals(StandardCharsets.ISO_8859_1, accessor.getPropertyValue("charset"));
assertEquals(true, accessor.getPropertyValue("sharedEngine"));
}
@Test
public void customEngineAndRenderFunction() throws Exception {
ScriptEngine engine = mock(InvocableScriptEngine.class);
given(engine.get("key")).willReturn("value");
this.view.setEngine(engine);
this.view.setRenderFunction("render");
this.view.setApplicationContext(this.applicationContext);
engine = this.view.getEngine();
assertNotNull(engine);
assertEquals("value", engine.get("key"));
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
assertNull(accessor.getPropertyValue("renderObject"));
assertEquals("render", accessor.getPropertyValue("renderFunction"));
assertEquals(StandardCharsets.UTF_8, accessor.getPropertyValue("charset"));
}
@Test
public void nonSharedEngine() throws Exception {
int iterations = 20;
this.view.setEngineName("nashorn");
this.view.setRenderFunction("render");
this.view.setSharedEngine(false);
this.view.setApplicationContext(this.applicationContext);
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<Boolean>> results = new ArrayList<>();
for(int i = 0; i < iterations; i++) {
results.add(executor.submit(() -> view.getEngine() != null));
}
assertEquals(iterations, results.size());
for(int i = 0; i < iterations; i++) {
assertTrue(results.get(i).get());
}
executor.shutdown();
}
@Test
public void nonInvocableScriptEngine() throws Exception {
ScriptEngine engine = mock(ScriptEngine.class);
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngine(engine);
try {
this.view.setEngine(mock(ScriptEngine.class));
} catch(IllegalArgumentException ex) {
assertThat(ex.getMessage(), containsString("instance"));
return;
}
fail();
}
@Test
public void noRenderFunctionDefined() {
this.view.setEngine(mock(InvocableScriptEngine.class));
try {
this.view.setApplicationContext(this.applicationContext);
} catch(IllegalStateException ex) {
assertThat(ex.getMessage(), containsString("renderFunction"));
return;
}
fail();
}
@Test
public void engineAndEngineNameBothDefined() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setEngineName("test");
this.view.setRenderFunction("render");
try {
this.view.setApplicationContext(this.applicationContext);
} catch(IllegalStateException ex) {
assertThat(ex.getMessage(), containsString("engine or engineName"));
return;
}
fail();
}
@Test
public void engineSetterAndNonSharedEngine() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setRenderFunction("render");
this.view.setSharedEngine(false);
try {
this.view.setApplicationContext(this.applicationContext);
} catch(IllegalStateException ex) {
assertThat(ex.getMessage(), containsString("sharedEngine"));
return;
}
fail();
}
@Test
public void parentLoader() {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setRenderFunction("render");
this.view.setResourceLoaderPath(RESOURCE_LOADER_PATH);
this.view.setApplicationContext(this.applicationContext);
ClassLoader classLoader = this.view.createClassLoader();
assertNotNull(classLoader);
URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
assertThat(Arrays.asList(urlClassLoader.getURLs()), Matchers.hasSize(1));
assertThat(Arrays.asList(urlClassLoader.getURLs()).get(0).toString(),
Matchers.endsWith("org/springframework/web/servlet/view/script/"));
this.view.setResourceLoaderPath(RESOURCE_LOADER_PATH + ",classpath:org/springframework/web/servlet/view/");
classLoader = this.view.createClassLoader();
assertNotNull(classLoader);
urlClassLoader = (URLClassLoader) classLoader;
assertThat(Arrays.asList(urlClassLoader.getURLs()), Matchers.hasSize(2));
assertThat(Arrays.asList(urlClassLoader.getURLs()).get(0).toString(), Matchers.endsWith("org/springframework/web/servlet/view/script/"));
assertThat(Arrays.asList(urlClassLoader.getURLs()).get(1).toString(), Matchers.endsWith("org/springframework/web/servlet/view/"));
}
private interface InvocableScriptEngine extends ScriptEngine, Invocable {
}

View File

@@ -34,7 +34,8 @@
<mvc:groovy-configurer resource-loader-path="/test" cache-templates="false" auto-indent="true" />
<mvc:script-template-configurer engine-name="nashorn" render-function="render" charset="ISO-8859-1" resource-loader-path="classpath:">
<mvc:script-template-configurer engine-name="nashorn" render-function="render" charset="ISO-8859-1"
resource-loader-path="classpath:" shared-engine="false">
<mvc:script location="org/springframework/web/servlet/view/script/nashorn/render.js" />
</mvc:script-template-configurer>