diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java index eb640e2aa0..3a726af77f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java @@ -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 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java index 4919492b15..da3d7c8e63 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java @@ -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(); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java index 5e3c1b755a..374d95f76d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java @@ -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; * } * * + *

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. + * + *

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. + * + *

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 THREADING ScriptEngine parameter + */ + 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 urls = new ArrayList(); - 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; } } 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 1d306e9f2e..0f9e7b9b1f 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 @@ -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. * - *

Nashorn Javascript engine requires Java 8+. + *

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. + * + *

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 engineHolder = + new NamedThreadLocal("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. - *

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: - *

    - *
  1. {@code template}: the view template content (String)
  2. - *
  3. {@code model}: the view model (Map)
  4. - *
- *

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 urls = new ArrayList(); + 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)); } diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd index ccb04b50b3..b2ecd06d04 100644 --- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd +++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd @@ -1260,6 +1260,15 @@ ]]> + + + + + diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index fe7ba2f77f..fc40bd4b13 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -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")); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurerTests.java deleted file mode 100644 index e5c46c45df..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurerTests.java +++ /dev/null @@ -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 { - } - -} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewTests.java index 1ae173d5c9..708eace985 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewTests.java @@ -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()); - - 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 configMap = new HashMap(); - 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> 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 { } diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution.xml index 222650f8ee..3488a15b48 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution.xml @@ -34,7 +34,8 @@ - +