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