001/* 002 * Copyright 2002-2019 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.scripting.support; 018 019import java.io.IOException; 020import java.lang.reflect.InvocationTargetException; 021 022import javax.script.Invocable; 023import javax.script.ScriptEngine; 024import javax.script.ScriptEngineManager; 025 026import org.springframework.beans.factory.BeanClassLoaderAware; 027import org.springframework.lang.Nullable; 028import org.springframework.scripting.ScriptCompilationException; 029import org.springframework.scripting.ScriptFactory; 030import org.springframework.scripting.ScriptSource; 031import org.springframework.util.Assert; 032import org.springframework.util.ClassUtils; 033import org.springframework.util.ObjectUtils; 034import org.springframework.util.ReflectionUtils; 035import org.springframework.util.StringUtils; 036 037/** 038 * {@link org.springframework.scripting.ScriptFactory} implementation based 039 * on the JSR-223 script engine abstraction (as included in Java 6+). 040 * Supports JavaScript, Groovy, JRuby, and other JSR-223 compliant engines. 041 * 042 * <p>Typically used in combination with a 043 * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor}; 044 * see the latter's javadoc for a configuration example. 045 * 046 * @author Juergen Hoeller 047 * @since 4.2 048 * @see ScriptFactoryPostProcessor 049 */ 050public class StandardScriptFactory implements ScriptFactory, BeanClassLoaderAware { 051 052 @Nullable 053 private final String scriptEngineName; 054 055 private final String scriptSourceLocator; 056 057 @Nullable 058 private final Class<?>[] scriptInterfaces; 059 060 @Nullable 061 private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); 062 063 @Nullable 064 private volatile ScriptEngine scriptEngine; 065 066 067 /** 068 * Create a new StandardScriptFactory for the given script source. 069 * @param scriptSourceLocator a locator that points to the source of the script. 070 * Interpreted by the post-processor that actually creates the script. 071 */ 072 public StandardScriptFactory(String scriptSourceLocator) { 073 this(null, scriptSourceLocator, (Class<?>[]) null); 074 } 075 076 /** 077 * Create a new StandardScriptFactory for the given script source. 078 * @param scriptSourceLocator a locator that points to the source of the script. 079 * Interpreted by the post-processor that actually creates the script. 080 * @param scriptInterfaces the Java interfaces that the scripted object 081 * is supposed to implement 082 */ 083 public StandardScriptFactory(String scriptSourceLocator, Class<?>... scriptInterfaces) { 084 this(null, scriptSourceLocator, scriptInterfaces); 085 } 086 087 /** 088 * Create a new StandardScriptFactory for the given script source. 089 * @param scriptEngineName the name of the JSR-223 ScriptEngine to use 090 * (explicitly given instead of inferred from the script source) 091 * @param scriptSourceLocator a locator that points to the source of the script. 092 * Interpreted by the post-processor that actually creates the script. 093 */ 094 public StandardScriptFactory(String scriptEngineName, String scriptSourceLocator) { 095 this(scriptEngineName, scriptSourceLocator, (Class<?>[]) null); 096 } 097 098 /** 099 * Create a new StandardScriptFactory for the given script source. 100 * @param scriptEngineName the name of the JSR-223 ScriptEngine to use 101 * (explicitly given instead of inferred from the script source) 102 * @param scriptSourceLocator a locator that points to the source of the script. 103 * Interpreted by the post-processor that actually creates the script. 104 * @param scriptInterfaces the Java interfaces that the scripted object 105 * is supposed to implement 106 */ 107 public StandardScriptFactory( 108 @Nullable String scriptEngineName, String scriptSourceLocator, @Nullable Class<?>... scriptInterfaces) { 109 110 Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); 111 this.scriptEngineName = scriptEngineName; 112 this.scriptSourceLocator = scriptSourceLocator; 113 this.scriptInterfaces = scriptInterfaces; 114 } 115 116 117 @Override 118 public void setBeanClassLoader(ClassLoader classLoader) { 119 this.beanClassLoader = classLoader; 120 } 121 122 @Override 123 public String getScriptSourceLocator() { 124 return this.scriptSourceLocator; 125 } 126 127 @Override 128 @Nullable 129 public Class<?>[] getScriptInterfaces() { 130 return this.scriptInterfaces; 131 } 132 133 @Override 134 public boolean requiresConfigInterface() { 135 return false; 136 } 137 138 139 /** 140 * Load and parse the script via JSR-223's ScriptEngine. 141 */ 142 @Override 143 @Nullable 144 public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class<?>... actualInterfaces) 145 throws IOException, ScriptCompilationException { 146 147 Object script = evaluateScript(scriptSource); 148 149 if (!ObjectUtils.isEmpty(actualInterfaces)) { 150 boolean adaptationRequired = false; 151 for (Class<?> requestedIfc : actualInterfaces) { 152 if (script instanceof Class ? !requestedIfc.isAssignableFrom((Class<?>) script) : 153 !requestedIfc.isInstance(script)) { 154 adaptationRequired = true; 155 break; 156 } 157 } 158 if (adaptationRequired) { 159 script = adaptToInterfaces(script, scriptSource, actualInterfaces); 160 } 161 } 162 163 if (script instanceof Class) { 164 Class<?> scriptClass = (Class<?>) script; 165 try { 166 return ReflectionUtils.accessibleConstructor(scriptClass).newInstance(); 167 } 168 catch (NoSuchMethodException ex) { 169 throw new ScriptCompilationException( 170 "No default constructor on script class: " + scriptClass.getName(), ex); 171 } 172 catch (InstantiationException ex) { 173 throw new ScriptCompilationException( 174 scriptSource, "Unable to instantiate script class: " + scriptClass.getName(), ex); 175 } 176 catch (IllegalAccessException ex) { 177 throw new ScriptCompilationException( 178 scriptSource, "Could not access script constructor: " + scriptClass.getName(), ex); 179 } 180 catch (InvocationTargetException ex) { 181 throw new ScriptCompilationException( 182 "Failed to invoke script constructor: " + scriptClass.getName(), ex.getTargetException()); 183 } 184 } 185 186 return script; 187 } 188 189 protected Object evaluateScript(ScriptSource scriptSource) { 190 try { 191 ScriptEngine scriptEngine = this.scriptEngine; 192 if (scriptEngine == null) { 193 scriptEngine = retrieveScriptEngine(scriptSource); 194 if (scriptEngine == null) { 195 throw new IllegalStateException("Could not determine script engine for " + scriptSource); 196 } 197 this.scriptEngine = scriptEngine; 198 } 199 return scriptEngine.eval(scriptSource.getScriptAsString()); 200 } 201 catch (Exception ex) { 202 throw new ScriptCompilationException(scriptSource, ex); 203 } 204 } 205 206 @Nullable 207 protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) { 208 ScriptEngineManager scriptEngineManager = new ScriptEngineManager(this.beanClassLoader); 209 210 if (this.scriptEngineName != null) { 211 return StandardScriptUtils.retrieveEngineByName(scriptEngineManager, this.scriptEngineName); 212 } 213 214 if (scriptSource instanceof ResourceScriptSource) { 215 String filename = ((ResourceScriptSource) scriptSource).getResource().getFilename(); 216 if (filename != null) { 217 String extension = StringUtils.getFilenameExtension(filename); 218 if (extension != null) { 219 ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension); 220 if (engine != null) { 221 return engine; 222 } 223 } 224 } 225 } 226 227 return null; 228 } 229 230 @Nullable 231 protected Object adaptToInterfaces( 232 @Nullable Object script, ScriptSource scriptSource, Class<?>... actualInterfaces) { 233 234 Class<?> adaptedIfc; 235 if (actualInterfaces.length == 1) { 236 adaptedIfc = actualInterfaces[0]; 237 } 238 else { 239 adaptedIfc = ClassUtils.createCompositeInterface(actualInterfaces, this.beanClassLoader); 240 } 241 242 if (adaptedIfc != null) { 243 ScriptEngine scriptEngine = this.scriptEngine; 244 if (!(scriptEngine instanceof Invocable)) { 245 throw new ScriptCompilationException(scriptSource, 246 "ScriptEngine must implement Invocable in order to adapt it to an interface: " + scriptEngine); 247 } 248 Invocable invocable = (Invocable) scriptEngine; 249 if (script != null) { 250 script = invocable.getInterface(script, adaptedIfc); 251 } 252 if (script == null) { 253 script = invocable.getInterface(adaptedIfc); 254 if (script == null) { 255 throw new ScriptCompilationException(scriptSource, 256 "Could not adapt script to interface [" + adaptedIfc.getName() + "]"); 257 } 258 } 259 } 260 261 return script; 262 } 263 264 @Override 265 @Nullable 266 public Class<?> getScriptedObjectType(ScriptSource scriptSource) 267 throws IOException, ScriptCompilationException { 268 269 return null; 270 } 271 272 @Override 273 public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) { 274 return scriptSource.isModified(); 275 } 276 277 278 @Override 279 public String toString() { 280 return "StandardScriptFactory: script source locator [" + this.scriptSourceLocator + "]"; 281 } 282 283}