001/* 002 * Copyright 2002-2016 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.bsh; 018 019import java.io.IOException; 020 021import bsh.EvalError; 022 023import org.springframework.beans.factory.BeanClassLoaderAware; 024import org.springframework.lang.Nullable; 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.ReflectionUtils; 031 032/** 033 * {@link org.springframework.scripting.ScriptFactory} implementation 034 * for a BeanShell script. 035 * 036 * <p>Typically used in combination with a 037 * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor}; 038 * see the latter's javadoc for a configuration example. 039 * 040 * @author Juergen Hoeller 041 * @author Rob Harrop 042 * @since 2.0 043 * @see BshScriptUtils 044 * @see org.springframework.scripting.support.ScriptFactoryPostProcessor 045 */ 046public class BshScriptFactory implements ScriptFactory, BeanClassLoaderAware { 047 048 private final String scriptSourceLocator; 049 050 @Nullable 051 private final Class<?>[] scriptInterfaces; 052 053 @Nullable 054 private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); 055 056 @Nullable 057 private Class<?> scriptClass; 058 059 private final Object scriptClassMonitor = new Object(); 060 061 private boolean wasModifiedForTypeCheck = false; 062 063 064 /** 065 * Create a new BshScriptFactory for the given script source. 066 * <p>With this {@code BshScriptFactory} variant, the script needs to 067 * declare a full class or return an actual instance of the scripted object. 068 * @param scriptSourceLocator a locator that points to the source of the script. 069 * Interpreted by the post-processor that actually creates the script. 070 */ 071 public BshScriptFactory(String scriptSourceLocator) { 072 Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); 073 this.scriptSourceLocator = scriptSourceLocator; 074 this.scriptInterfaces = null; 075 } 076 077 /** 078 * Create a new BshScriptFactory for the given script source. 079 * <p>The script may either be a simple script that needs a corresponding proxy 080 * generated (implementing the specified interfaces), or declare a full class 081 * or return an actual instance of the scripted object (in which case the 082 * specified interfaces, if any, need to be implemented by that class/instance). 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 * @param scriptInterfaces the Java interfaces that the scripted object 086 * is supposed to implement (may be {@code null}) 087 */ 088 public BshScriptFactory(String scriptSourceLocator, @Nullable Class<?>... scriptInterfaces) { 089 Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); 090 this.scriptSourceLocator = scriptSourceLocator; 091 this.scriptInterfaces = scriptInterfaces; 092 } 093 094 095 @Override 096 public void setBeanClassLoader(ClassLoader classLoader) { 097 this.beanClassLoader = classLoader; 098 } 099 100 101 @Override 102 public String getScriptSourceLocator() { 103 return this.scriptSourceLocator; 104 } 105 106 @Override 107 @Nullable 108 public Class<?>[] getScriptInterfaces() { 109 return this.scriptInterfaces; 110 } 111 112 /** 113 * BeanShell scripts do require a config interface. 114 */ 115 @Override 116 public boolean requiresConfigInterface() { 117 return true; 118 } 119 120 /** 121 * Load and parse the BeanShell script via {@link BshScriptUtils}. 122 * @see BshScriptUtils#createBshObject(String, Class[], ClassLoader) 123 */ 124 @Override 125 @Nullable 126 public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class<?>... actualInterfaces) 127 throws IOException, ScriptCompilationException { 128 129 Class<?> clazz; 130 131 try { 132 synchronized (this.scriptClassMonitor) { 133 boolean requiresScriptEvaluation = (this.wasModifiedForTypeCheck && this.scriptClass == null); 134 this.wasModifiedForTypeCheck = false; 135 136 if (scriptSource.isModified() || requiresScriptEvaluation) { 137 // New script content: Let's check whether it evaluates to a Class. 138 Object result = BshScriptUtils.evaluateBshScript( 139 scriptSource.getScriptAsString(), actualInterfaces, this.beanClassLoader); 140 if (result instanceof Class) { 141 // A Class: We'll cache the Class here and create an instance 142 // outside of the synchronized block. 143 this.scriptClass = (Class<?>) result; 144 } 145 else { 146 // Not a Class: OK, we'll simply create BeanShell objects 147 // through evaluating the script for every call later on. 148 // For this first-time check, let's simply return the 149 // already evaluated object. 150 return result; 151 } 152 } 153 clazz = this.scriptClass; 154 } 155 } 156 catch (EvalError ex) { 157 this.scriptClass = null; 158 throw new ScriptCompilationException(scriptSource, ex); 159 } 160 161 if (clazz != null) { 162 // A Class: We need to create an instance for every call. 163 try { 164 return ReflectionUtils.accessibleConstructor(clazz).newInstance(); 165 } 166 catch (Throwable ex) { 167 throw new ScriptCompilationException( 168 scriptSource, "Could not instantiate script class: " + clazz.getName(), ex); 169 } 170 } 171 else { 172 // Not a Class: We need to evaluate the script for every call. 173 try { 174 return BshScriptUtils.createBshObject( 175 scriptSource.getScriptAsString(), actualInterfaces, this.beanClassLoader); 176 } 177 catch (EvalError ex) { 178 throw new ScriptCompilationException(scriptSource, ex); 179 } 180 } 181 } 182 183 @Override 184 @Nullable 185 public Class<?> getScriptedObjectType(ScriptSource scriptSource) 186 throws IOException, ScriptCompilationException { 187 188 synchronized (this.scriptClassMonitor) { 189 try { 190 if (scriptSource.isModified()) { 191 // New script content: Let's check whether it evaluates to a Class. 192 this.wasModifiedForTypeCheck = true; 193 this.scriptClass = BshScriptUtils.determineBshObjectType( 194 scriptSource.getScriptAsString(), this.beanClassLoader); 195 } 196 return this.scriptClass; 197 } 198 catch (EvalError ex) { 199 this.scriptClass = null; 200 throw new ScriptCompilationException(scriptSource, ex); 201 } 202 } 203 } 204 205 @Override 206 public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) { 207 synchronized (this.scriptClassMonitor) { 208 return (scriptSource.isModified() || this.wasModifiedForTypeCheck); 209 } 210 } 211 212 213 @Override 214 public String toString() { 215 return "BshScriptFactory: script source locator [" + this.scriptSourceLocator + "]"; 216 } 217 218}