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}