001/* 002 * Copyright 2002-2013 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.lang.reflect.InvocationHandler; 020import java.lang.reflect.Method; 021import java.lang.reflect.Proxy; 022 023import bsh.EvalError; 024import bsh.Interpreter; 025import bsh.Primitive; 026import bsh.XThis; 027 028import org.springframework.core.NestedRuntimeException; 029import org.springframework.util.Assert; 030import org.springframework.util.ClassUtils; 031import org.springframework.util.ReflectionUtils; 032 033/** 034 * Utility methods for handling BeanShell-scripted objects. 035 * 036 * @author Rob Harrop 037 * @author Juergen Hoeller 038 * @since 2.0 039 */ 040public abstract class BshScriptUtils { 041 042 /** 043 * Create a new BeanShell-scripted object from the given script source. 044 * <p>With this {@code createBshObject} variant, the script needs to 045 * declare a full class or return an actual instance of the scripted object. 046 * @param scriptSource the script source text 047 * @return the scripted Java object 048 * @throws EvalError in case of BeanShell parsing failure 049 */ 050 public static Object createBshObject(String scriptSource) throws EvalError { 051 return createBshObject(scriptSource, null, null); 052 } 053 054 /** 055 * Create a new BeanShell-scripted object from the given script source, 056 * using the default ClassLoader. 057 * <p>The script may either be a simple script that needs a corresponding proxy 058 * generated (implementing the specified interfaces), or declare a full class 059 * or return an actual instance of the scripted object (in which case the 060 * specified interfaces, if any, need to be implemented by that class/instance). 061 * @param scriptSource the script source text 062 * @param scriptInterfaces the interfaces that the scripted Java object is 063 * supposed to implement (may be {@code null} or empty if the script itself 064 * declares a full class or returns an actual instance of the scripted object) 065 * @return the scripted Java object 066 * @throws EvalError in case of BeanShell parsing failure 067 * @see #createBshObject(String, Class[], ClassLoader) 068 */ 069 public static Object createBshObject(String scriptSource, Class<?>... scriptInterfaces) throws EvalError { 070 return createBshObject(scriptSource, scriptInterfaces, ClassUtils.getDefaultClassLoader()); 071 } 072 073 /** 074 * Create a new BeanShell-scripted object from the given script source. 075 * <p>The script may either be a simple script that needs a corresponding proxy 076 * generated (implementing the specified interfaces), or declare a full class 077 * or return an actual instance of the scripted object (in which case the 078 * specified interfaces, if any, need to be implemented by that class/instance). 079 * @param scriptSource the script source text 080 * @param scriptInterfaces the interfaces that the scripted Java object is 081 * supposed to implement (may be {@code null} or empty if the script itself 082 * declares a full class or returns an actual instance of the scripted object) 083 * @param classLoader the ClassLoader to use for evaluating the script 084 * @return the scripted Java object 085 * @throws EvalError in case of BeanShell parsing failure 086 */ 087 public static Object createBshObject(String scriptSource, Class<?>[] scriptInterfaces, ClassLoader classLoader) 088 throws EvalError { 089 090 Object result = evaluateBshScript(scriptSource, scriptInterfaces, classLoader); 091 if (result instanceof Class) { 092 Class<?> clazz = (Class<?>) result; 093 try { 094 return clazz.newInstance(); 095 } 096 catch (Throwable ex) { 097 throw new IllegalStateException("Could not instantiate script class: " + clazz.getName(), ex); 098 } 099 } 100 else { 101 return result; 102 } 103 } 104 105 /** 106 * Evaluate the specified BeanShell script based on the given script source, 107 * returning the Class defined by the script. 108 * <p>The script may either declare a full class or return an actual instance of 109 * the scripted object (in which case the Class of the object will be returned). 110 * In any other case, the returned Class will be {@code null}. 111 * @param scriptSource the script source text 112 * @param classLoader the ClassLoader to use for evaluating the script 113 * @return the scripted Java class, or {@code null} if none could be determined 114 * @throws EvalError in case of BeanShell parsing failure 115 */ 116 static Class<?> determineBshObjectType(String scriptSource, ClassLoader classLoader) throws EvalError { 117 Assert.hasText(scriptSource, "Script source must not be empty"); 118 Interpreter interpreter = new Interpreter(); 119 interpreter.setClassLoader(classLoader); 120 Object result = interpreter.eval(scriptSource); 121 if (result instanceof Class) { 122 return (Class<?>) result; 123 } 124 else if (result != null) { 125 return result.getClass(); 126 } 127 else { 128 return null; 129 } 130 } 131 132 /** 133 * Evaluate the specified BeanShell script based on the given script source, 134 * keeping a returned script Class or script Object as-is. 135 * <p>The script may either be a simple script that needs a corresponding proxy 136 * generated (implementing the specified interfaces), or declare a full class 137 * or return an actual instance of the scripted object (in which case the 138 * specified interfaces, if any, need to be implemented by that class/instance). 139 * @param scriptSource the script source text 140 * @param scriptInterfaces the interfaces that the scripted Java object is 141 * supposed to implement (may be {@code null} or empty if the script itself 142 * declares a full class or returns an actual instance of the scripted object) 143 * @param classLoader the ClassLoader to use for evaluating the script 144 * @return the scripted Java class or Java object 145 * @throws EvalError in case of BeanShell parsing failure 146 */ 147 static Object evaluateBshScript(String scriptSource, Class<?>[] scriptInterfaces, ClassLoader classLoader) 148 throws EvalError { 149 150 Assert.hasText(scriptSource, "Script source must not be empty"); 151 Interpreter interpreter = new Interpreter(); 152 interpreter.setClassLoader(classLoader); 153 Object result = interpreter.eval(scriptSource); 154 if (result != null) { 155 return result; 156 } 157 else { 158 // Simple BeanShell script: Let's create a proxy for it, implementing the given interfaces. 159 Assert.notEmpty(scriptInterfaces, 160 "Given script requires a script proxy: At least one script interface is required."); 161 XThis xt = (XThis) interpreter.eval("return this"); 162 return Proxy.newProxyInstance(classLoader, scriptInterfaces, new BshObjectInvocationHandler(xt)); 163 } 164 } 165 166 167 /** 168 * InvocationHandler that invokes a BeanShell script method. 169 */ 170 private static class BshObjectInvocationHandler implements InvocationHandler { 171 172 private final XThis xt; 173 174 public BshObjectInvocationHandler(XThis xt) { 175 this.xt = xt; 176 } 177 178 @Override 179 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 180 if (ReflectionUtils.isEqualsMethod(method)) { 181 return (isProxyForSameBshObject(args[0])); 182 } 183 else if (ReflectionUtils.isHashCodeMethod(method)) { 184 return this.xt.hashCode(); 185 } 186 else if (ReflectionUtils.isToStringMethod(method)) { 187 return "BeanShell object [" + this.xt + "]"; 188 } 189 try { 190 Object result = this.xt.invokeMethod(method.getName(), args); 191 if (result == Primitive.NULL || result == Primitive.VOID) { 192 return null; 193 } 194 if (result instanceof Primitive) { 195 return ((Primitive) result).getValue(); 196 } 197 return result; 198 } 199 catch (EvalError ex) { 200 throw new BshExecutionException(ex); 201 } 202 } 203 204 private boolean isProxyForSameBshObject(Object other) { 205 if (!Proxy.isProxyClass(other.getClass())) { 206 return false; 207 } 208 InvocationHandler ih = Proxy.getInvocationHandler(other); 209 return (ih instanceof BshObjectInvocationHandler && 210 this.xt.equals(((BshObjectInvocationHandler) ih).xt)); 211 } 212 } 213 214 215 /** 216 * Exception to be thrown on script execution failure. 217 */ 218 @SuppressWarnings("serial") 219 public static class BshExecutionException extends NestedRuntimeException { 220 221 private BshExecutionException(EvalError ex) { 222 super("BeanShell script execution failed", ex); 223 } 224 } 225 226}