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}