001/* 002 * Copyright 2002-2017 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.view.script; 018 019import java.io.IOException; 020import java.io.InputStreamReader; 021import java.nio.charset.Charset; 022import java.util.Arrays; 023import java.util.HashMap; 024import java.util.Locale; 025import java.util.Map; 026import javax.script.Invocable; 027import javax.script.ScriptEngine; 028import javax.script.ScriptEngineManager; 029import javax.script.ScriptException; 030import javax.servlet.ServletException; 031import javax.servlet.http.HttpServletRequest; 032import javax.servlet.http.HttpServletResponse; 033 034import org.springframework.beans.BeansException; 035import org.springframework.beans.factory.BeanFactoryUtils; 036import org.springframework.beans.factory.NoSuchBeanDefinitionException; 037import org.springframework.context.ApplicationContext; 038import org.springframework.context.ApplicationContextException; 039import org.springframework.core.NamedThreadLocal; 040import org.springframework.core.io.Resource; 041import org.springframework.core.io.ResourceLoader; 042import org.springframework.scripting.support.StandardScriptEvalException; 043import org.springframework.scripting.support.StandardScriptUtils; 044import org.springframework.util.Assert; 045import org.springframework.util.FileCopyUtils; 046import org.springframework.util.ObjectUtils; 047import org.springframework.util.StringUtils; 048import org.springframework.web.servlet.view.AbstractUrlBasedView; 049 050/** 051 * An {@link AbstractUrlBasedView} subclass designed to run any template library 052 * based on a JSR-223 script engine. 053 * 054 * <p>If not set, each property is auto-detected by looking up a single 055 * {@link ScriptTemplateConfig} bean in the web application context and using 056 * it to obtain the configured properties. 057 * 058 * <p>The Nashorn JavaScript engine requires Java 8+ and may require setting the 059 * {@code sharedEngine} property to {@code false} in order to run properly. See 060 * {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} for more details. 061 * 062 * @author Sebastien Deleuze 063 * @author Juergen Hoeller 064 * @since 4.2 065 * @see ScriptTemplateConfigurer 066 * @see ScriptTemplateViewResolver 067 */ 068public class ScriptTemplateView extends AbstractUrlBasedView { 069 070 public static final String DEFAULT_CONTENT_TYPE = "text/html"; 071 072 private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 073 074 private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:"; 075 076 077 private static final ThreadLocal<Map<Object, ScriptEngine>> enginesHolder = 078 new NamedThreadLocal<Map<Object, ScriptEngine>>("ScriptTemplateView engines"); 079 080 081 private ScriptEngine engine; 082 083 private String engineName; 084 085 private Boolean sharedEngine; 086 087 private String[] scripts; 088 089 private String renderObject; 090 091 private String renderFunction; 092 093 private Charset charset; 094 095 private String[] resourceLoaderPaths; 096 097 private ResourceLoader resourceLoader; 098 099 private volatile ScriptEngineManager scriptEngineManager; 100 101 102 /** 103 * Constructor for use as a bean. 104 * @see #setUrl 105 */ 106 public ScriptTemplateView() { 107 setContentType(null); 108 } 109 110 /** 111 * Create a new ScriptTemplateView with the given URL. 112 * @since 4.2.1 113 */ 114 public ScriptTemplateView(String url) { 115 super(url); 116 setContentType(null); 117 } 118 119 120 /** 121 * See {@link ScriptTemplateConfigurer#setEngine(ScriptEngine)} documentation. 122 */ 123 public void setEngine(ScriptEngine engine) { 124 Assert.isInstanceOf(Invocable.class, engine, "ScriptEngine must implement Invocable"); 125 this.engine = engine; 126 } 127 128 /** 129 * See {@link ScriptTemplateConfigurer#setEngineName(String)} documentation. 130 */ 131 public void setEngineName(String engineName) { 132 this.engineName = engineName; 133 } 134 135 /** 136 * See {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} documentation. 137 */ 138 public void setSharedEngine(Boolean sharedEngine) { 139 this.sharedEngine = sharedEngine; 140 } 141 142 /** 143 * See {@link ScriptTemplateConfigurer#setScripts(String...)} documentation. 144 */ 145 public void setScripts(String... scripts) { 146 this.scripts = scripts; 147 } 148 149 /** 150 * See {@link ScriptTemplateConfigurer#setRenderObject(String)} documentation. 151 */ 152 public void setRenderObject(String renderObject) { 153 this.renderObject = renderObject; 154 } 155 156 /** 157 * See {@link ScriptTemplateConfigurer#setRenderFunction(String)} documentation. 158 */ 159 public void setRenderFunction(String functionName) { 160 this.renderFunction = functionName; 161 } 162 163 /** 164 * See {@link ScriptTemplateConfigurer#setContentType(String)}} documentation. 165 * @since 4.2.1 166 */ 167 @Override 168 public void setContentType(String contentType) { 169 super.setContentType(contentType); 170 } 171 172 /** 173 * See {@link ScriptTemplateConfigurer#setCharset(Charset)} documentation. 174 */ 175 public void setCharset(Charset charset) { 176 this.charset = charset; 177 } 178 179 /** 180 * See {@link ScriptTemplateConfigurer#setResourceLoaderPath(String)} documentation. 181 */ 182 public void setResourceLoaderPath(String resourceLoaderPath) { 183 String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath); 184 this.resourceLoaderPaths = new String[paths.length + 1]; 185 this.resourceLoaderPaths[0] = ""; 186 for (int i = 0; i < paths.length; i++) { 187 String path = paths[i]; 188 if (!path.endsWith("/") && !path.endsWith(":")) { 189 path = path + "/"; 190 } 191 this.resourceLoaderPaths[i + 1] = path; 192 } 193 } 194 195 196 @Override 197 protected void initApplicationContext(ApplicationContext context) { 198 super.initApplicationContext(context); 199 200 ScriptTemplateConfig viewConfig = autodetectViewConfig(); 201 if (this.engine == null && viewConfig.getEngine() != null) { 202 setEngine(viewConfig.getEngine()); 203 } 204 if (this.engineName == null && viewConfig.getEngineName() != null) { 205 this.engineName = viewConfig.getEngineName(); 206 } 207 if (this.scripts == null && viewConfig.getScripts() != null) { 208 this.scripts = viewConfig.getScripts(); 209 } 210 if (this.renderObject == null && viewConfig.getRenderObject() != null) { 211 this.renderObject = viewConfig.getRenderObject(); 212 } 213 if (this.renderFunction == null && viewConfig.getRenderFunction() != null) { 214 this.renderFunction = viewConfig.getRenderFunction(); 215 } 216 if (this.getContentType() == null) { 217 setContentType(viewConfig.getContentType() != null ? viewConfig.getContentType() : DEFAULT_CONTENT_TYPE); 218 } 219 if (this.charset == null) { 220 this.charset = (viewConfig.getCharset() != null ? viewConfig.getCharset() : DEFAULT_CHARSET); 221 } 222 if (this.resourceLoaderPaths == null) { 223 String resourceLoaderPath = viewConfig.getResourceLoaderPath(); 224 setResourceLoaderPath(resourceLoaderPath == null ? DEFAULT_RESOURCE_LOADER_PATH : resourceLoaderPath); 225 } 226 if (this.resourceLoader == null) { 227 this.resourceLoader = getApplicationContext(); 228 } 229 if (this.sharedEngine == null && viewConfig.isSharedEngine() != null) { 230 this.sharedEngine = viewConfig.isSharedEngine(); 231 } 232 233 Assert.isTrue(!(this.engine != null && this.engineName != null), 234 "You should define either 'engine' or 'engineName', not both."); 235 Assert.isTrue(!(this.engine == null && this.engineName == null), 236 "No script engine found, please specify either 'engine' or 'engineName'."); 237 238 if (Boolean.FALSE.equals(this.sharedEngine)) { 239 Assert.isTrue(this.engineName != null, 240 "When 'sharedEngine' is set to false, you should specify the " + 241 "script engine using the 'engineName' property, not the 'engine' one."); 242 } 243 else if (this.engine != null) { 244 loadScripts(this.engine); 245 } 246 else { 247 setEngine(createEngineFromName()); 248 } 249 250 Assert.isTrue(this.renderFunction != null, "The 'renderFunction' property must be defined."); 251 } 252 253 protected ScriptEngine getEngine() { 254 if (Boolean.FALSE.equals(this.sharedEngine)) { 255 Map<Object, ScriptEngine> engines = enginesHolder.get(); 256 if (engines == null) { 257 engines = new HashMap<Object, ScriptEngine>(4); 258 enginesHolder.set(engines); 259 } 260 Object engineKey = (!ObjectUtils.isEmpty(this.scripts) ? 261 new EngineKey(this.engineName, this.scripts) : this.engineName); 262 ScriptEngine engine = engines.get(engineKey); 263 if (engine == null) { 264 engine = createEngineFromName(); 265 engines.put(engineKey, engine); 266 } 267 return engine; 268 } 269 else { 270 // Simply return the configured ScriptEngine... 271 return this.engine; 272 } 273 } 274 275 protected ScriptEngine createEngineFromName() { 276 if (this.scriptEngineManager == null) { 277 this.scriptEngineManager = new ScriptEngineManager(getApplicationContext().getClassLoader()); 278 } 279 280 ScriptEngine engine = StandardScriptUtils.retrieveEngineByName(this.scriptEngineManager, this.engineName); 281 loadScripts(engine); 282 return engine; 283 } 284 285 protected void loadScripts(ScriptEngine engine) { 286 if (!ObjectUtils.isEmpty(this.scripts)) { 287 for (String script : this.scripts) { 288 Resource resource = getResource(script); 289 if (resource == null) { 290 throw new IllegalStateException("Script resource [" + script + "] not found"); 291 } 292 try { 293 engine.eval(new InputStreamReader(resource.getInputStream())); 294 } 295 catch (Throwable ex) { 296 throw new IllegalStateException("Failed to evaluate script [" + script + "]", ex); 297 } 298 } 299 } 300 } 301 302 protected Resource getResource(String location) { 303 for (String path : this.resourceLoaderPaths) { 304 Resource resource = this.resourceLoader.getResource(path + location); 305 if (resource.exists()) { 306 return resource; 307 } 308 } 309 return null; 310 } 311 312 protected ScriptTemplateConfig autodetectViewConfig() throws BeansException { 313 try { 314 return BeanFactoryUtils.beanOfTypeIncludingAncestors( 315 getApplicationContext(), ScriptTemplateConfig.class, true, false); 316 } 317 catch (NoSuchBeanDefinitionException ex) { 318 throw new ApplicationContextException("Expected a single ScriptTemplateConfig bean in the current " + 319 "Servlet web application context or the parent root context: ScriptTemplateConfigurer is " + 320 "the usual implementation. This bean may have any name.", ex); 321 } 322 } 323 324 325 @Override 326 public boolean checkResource(Locale locale) throws Exception { 327 return (getResource(getUrl()) != null); 328 } 329 330 @Override 331 protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { 332 super.prepareResponse(request, response); 333 334 setResponseContentType(request, response); 335 response.setCharacterEncoding(this.charset.name()); 336 } 337 338 @Override 339 protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, 340 HttpServletResponse response) throws Exception { 341 342 try { 343 ScriptEngine engine = getEngine(); 344 Invocable invocable = (Invocable) engine; 345 String url = getUrl(); 346 String template = getTemplate(url); 347 348 Object html; 349 if (this.renderObject != null) { 350 Object thiz = engine.eval(this.renderObject); 351 html = invocable.invokeMethod(thiz, this.renderFunction, template, model, url); 352 } 353 else { 354 html = invocable.invokeFunction(this.renderFunction, template, model, url); 355 } 356 357 response.getWriter().write(String.valueOf(html)); 358 } 359 catch (ScriptException ex) { 360 throw new ServletException("Failed to render script template", new StandardScriptEvalException(ex)); 361 } 362 } 363 364 protected String getTemplate(String path) throws IOException { 365 Resource resource = getResource(path); 366 if (resource == null) { 367 throw new IllegalStateException("Template resource [" + path + "] not found"); 368 } 369 InputStreamReader reader = new InputStreamReader(resource.getInputStream(), this.charset); 370 return FileCopyUtils.copyToString(reader); 371 } 372 373 374 /** 375 * Key class for the {@code enginesHolder ThreadLocal}. 376 * Only used if scripts have been specified; otherwise, the 377 * {@code engineName String} will be used as cache key directly. 378 */ 379 private static class EngineKey { 380 381 private final String engineName; 382 383 private final String[] scripts; 384 385 public EngineKey(String engineName, String[] scripts) { 386 this.engineName = engineName; 387 this.scripts = scripts; 388 } 389 390 @Override 391 public boolean equals(Object other) { 392 if (this == other) { 393 return true; 394 } 395 if (!(other instanceof EngineKey)) { 396 return false; 397 } 398 EngineKey otherKey = (EngineKey) other; 399 return (this.engineName.equals(otherKey.engineName) && Arrays.equals(this.scripts, otherKey.scripts)); 400 } 401 402 @Override 403 public int hashCode() { 404 return (this.engineName.hashCode() * 29 + Arrays.hashCode(this.scripts)); 405 } 406 } 407 408}