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