001package com.identityworksllc.iiq.common;
002
003import bsh.NameSpace;
004import bsh.Primitive;
005import bsh.UtilEvalError;
006import org.apache.bsf.BSFManager;
007import org.apache.bsf.util.BSFFunctions;
008import org.apache.commons.logging.Log;
009import sailpoint.object.TaskResult;
010import sailpoint.tools.GeneralException;
011import sailpoint.tools.Util;
012
013import java.lang.reflect.Field;
014import java.lang.reflect.Proxy;
015import java.util.Objects;
016
017/**
018 * Utilities for working with Beanshell at a language level
019 */
020@SuppressWarnings("unused")
021public class BeanshellUtilities {
022        /**
023         * Create a Proxy implementing the given interface which will invoke a similarly
024         * named Beanshell method for any function call to the interface.
025         *
026         * @param bshThis The 'this' instance in Beanshell
027         * @param types The interface types that the Beanshell context ought to proxy
028         * @return the proxy implementing the interface
029         */
030        public static Object coerce(bsh.This bshThis, Class<?>... types) {
031                return Proxy.newProxyInstance(BeanshellUtilities.class.getClassLoader(), types,
032                                (proxy, method, args) -> bshThis.invokeMethod(method.getName(), args)
033                );
034        }
035
036        /**
037         * Create a Proxy implementing the given class which will invoke the
038         * given Beanshell function for any method call. This should be used
039         * mainly for "functional" interfaces with a single abstract method,
040         * like Runnable or Callable or the various event handlers.
041         *
042         * @param type The proxy type
043         * @param bshThis The 'this' instance in Beanshell
044         * @param methodName The method name to invoke on method call
045         * @param <T> the interface type to implement
046         * @return the proxy implementing the interface
047         */
048        public static <T> T coerce(Class<T> type, bsh.This bshThis, String methodName) {
049                return coerce(type, bshThis, methodName, methodName);
050        }
051
052        /**
053         * Create a Proxy implementing the given class which will invoke the
054         * given Beanshell function for any method call. This should be used
055         * mainly for "functional" interfaces with a single abstract method,
056         * like Runnable or Callable or the various event handlers.
057         *
058         * @param type The proxy type
059         * @param bshThis The 'this' instance in Beanshell
060         * @param targetMethod The method to intercept on the interface (e.g. 'run' on a Runnable)
061         * @param methodName The Beanshell method name to invoke on method call
062         * @param <T> the interface type to implement
063         * @return the proxy implementing the interface
064         */
065        @SuppressWarnings("unchecked")
066        public static <T> T coerce(Class<T> type, bsh.This bshThis, String targetMethod, String methodName) {
067                Class<?>[] interfaces = new Class<?>[] {Objects.requireNonNull(type)};
068                return (T) Proxy.newProxyInstance(BeanshellUtilities.class.getClassLoader(), interfaces,
069                                (proxy, method, args) ->
070                                {
071                                        if (method.getName().equals(targetMethod)) {
072                                                return bshThis.invokeMethod(methodName, args);
073                                        } else {
074                                                return null;
075                                        }
076                                }
077                );
078        }
079
080        /**
081         * Dumps the beanshell namespace passed to this method to the
082         * given logger
083         * @param namespace The beanshell namespace to dump
084         * @throws UtilEvalError On errors getting the variable value
085         * @throws GeneralException If a Sailpoint exception occurs
086         */
087        public static void dump(bsh.This namespace, Log logger) throws UtilEvalError, GeneralException {
088                for(String name : namespace.getNameSpace().getVariableNames()) {
089                        if ("transient".equals(name)) { continue; }
090
091                        Object value = namespace.getNameSpace().getVariable(name);
092
093                        if (value == null) {
094                                logger.warn(name + " = null");
095                        } else {
096                                if (value instanceof sailpoint.object.SailPointObject) {
097                                        sailpoint.object.SailPointObject objValue = (sailpoint.object.SailPointObject)value;
098                                        logger.warn(name + "(" + value.getClass().getSimpleName() + ") = " + objValue.toXml());
099                                } else {
100                                        logger.warn(name + "(" + value.getClass().getSimpleName() + ") = " + value );
101                                }
102                        }
103                }
104        }
105
106        /**
107         * An indirect reference to {@link BeanshellUtilities#exists(bsh.This, String)}
108         * that will allow the Eclipse plugin to compile rules properly. If the bshThis
109         * passed is not a This object, this method will silently return false.
110         *
111         * @param bshThis The Beanshell this variable
112         * @param variableName The variable name to check
113         * @return True if the variable exists, false otherwise
114         */
115        public static boolean exists(Object bshThis, String variableName) {
116                if (bshThis instanceof bsh.This) {
117                        return exists((bsh.This)bshThis, variableName);
118                }
119                return false;
120        }
121
122        /**
123         * Returns true if a Beanshell variable with the given name exists in the
124         * current namespace or one of its parents, returns false otherwise. This
125         * avoids the need for 'void' checks which mess with Beanshell parsing in
126         * the Eclipse plugin.
127         *
128         * @param bshThis The Beanshell 'this'
129         * @param variableName The variable name
130         * @return true if the variable exists in the current namespace
131         */
132        private static boolean exists(bsh.This bshThis, String variableName) {
133                NameSpace bshNamespace = bshThis.getNameSpace();
134                try {
135                        Object value = bshNamespace.getVariable(variableName);
136                        return !Primitive.VOID.equals(value);
137                } catch(UtilEvalError e) {
138                        /* Ignore this */
139                }
140                return false;
141        }
142
143        /**
144         * Extracts the BSFManager from the current Beanshell context using reflection
145         * @param bsf The 'bsf' variable passed to all Beanshell scripts
146         * @return The BSFManager
147         * @throws GeneralException if any issues occur retrieving the value using reflection
148         */
149        public static BSFManager getBSFManager(BSFFunctions bsf) throws GeneralException {
150                try {
151                        Field mgrField = bsf.getClass().getDeclaredField("mgr");
152                        mgrField.setAccessible(true);
153                        try {
154                                return (BSFManager) mgrField.get(bsf);
155                        } finally {
156                                mgrField.setAccessible(false);
157                        }
158                } catch(Exception e) {
159                        throw new GeneralException(e);
160                }
161        }
162
163        /**
164         * Gets the value of the Beanshell variable, if it exists and is the
165         * expected object type, otherwise null.
166         *
167         * @param bshThis The beanshell 'this' object
168         * @param variableName The variable name to retrieve
169         * @param expectedType The expected type of the variable
170         * @param <T> The expected object type
171         * @return The value of the given variable, or null
172         */
173        public static <T> T get(Object bshThis, String variableName, Class<T> expectedType) {
174                if (bshThis instanceof bsh.This) {
175                        return get((bsh.This)bshThis, variableName, expectedType);
176                } else {
177                        return null;
178                }
179        }
180
181        /**
182         * Gets the value of the Beanshell variable, if it exists and is the
183         * expected object type, otherwise null.
184         *
185         * @param bshThis The beanshell 'this' object
186         * @param variableName The variable name to retrieve
187         * @param expectedType The expected type of the variable
188         * @param <T> The expected object type
189         * @return The value of the given variable, or null
190         */
191        public static <T> T get(bsh.This bshThis, String variableName, Class<T> expectedType) {
192                if (bshThis != null && bshThis.getNameSpace() != null) {
193                        try {
194                                Object resultMaybe = bshThis.getNameSpace().getVariable(variableName, true);
195                                if (resultMaybe != null && Functions.isAssignableFrom(expectedType, resultMaybe.getClass())) {
196                                        return (T)resultMaybe;
197                                }
198                        } catch(UtilEvalError e) {
199                                // Ignore this, return null
200                        }
201                }
202                return null;
203        }
204
205        /**
206         * Imports static methods from the given target class into the namespace
207         * @param bshThis The 'this' reference from Beanshell
208         * @param target The target class to import
209         */
210        public static void importStatic(bsh.This bshThis, Class<?> target) {
211                NameSpace bshNamespace = bshThis.getNameSpace();
212                bshNamespace.importStatic(target);
213        }
214
215        /**
216         * Intended to be invoked from a Run Rule task (or code that may be invoked
217         * from one), will check whether the task has been stopped.
218         *
219         * @param bshThis The 'this' object from Beanshell
220         * @return True if the task has been terminated
221         */
222        public static boolean runRuleTerminated(bsh.This bshThis)  {
223                if (bshThis != null && bshThis.getNameSpace() != null) {
224                        try {
225                                Object maybeTaskResult = bshThis.getNameSpace().getVariable("taskResult", true);
226                                if (maybeTaskResult instanceof TaskResult) {
227                                        TaskResult tr = (TaskResult) maybeTaskResult;
228                                        return (tr.isTerminated() || tr.isTerminateRequested());
229                                }
230                        } catch (UtilEvalError e) {
231                                // Ignore this, at least check Thread interrupt
232                        }
233                }
234
235                return Thread.currentThread().isInterrupted();
236        }
237
238        /**
239         * If the given variable does not exist, sets it to null, enabling ordinary
240         * null checks. If the "this" reference is not a Beanshell context, this
241         * method will have no effect.
242         *
243         * @param bshThis The current Beanshell namespace
244         * @param variableName The variable name
245         * @param defaultValue The default value
246         * @throws GeneralException if any failures occur
247         */
248        public static void safe(Object bshThis, String variableName, Object defaultValue) throws GeneralException {
249                if (bshThis instanceof bsh.This) {
250                        safe((bsh.This) bshThis, variableName, defaultValue);
251                }
252        }
253
254        /**
255         * If the given variable does not exist, sets it to the given default value. Otherwise,
256         * the value is retained as-is.
257         *
258         * @param bshThis The current Beanshell namespace
259         * @param variableName The variable name
260         * @param defaultValue The default value
261         * @throws GeneralException if any failures occur
262         */
263        private static void safe(bsh.This bshThis, String variableName, Object defaultValue) throws GeneralException {
264                if (!exists(bshThis, variableName)) {
265                        try {
266                                bshThis.getNameSpace().setVariable(variableName, defaultValue, false);
267                        } catch(UtilEvalError e) {
268                                throw new GeneralException(e);
269                        }
270                }
271        }
272
273        /**
274         * If the given variable does not exist, sets it to null, enabling ordinary
275         * null checks. If the "this" reference is not a Beanshell context, this
276         * method will have no effect.
277         *
278         * @param bshThis The current Beanshell namespace
279         * @param variableName The variable name
280         * @throws GeneralException if any failures occur
281         */
282        public static void safe(Object bshThis, String variableName) throws GeneralException {
283                if (bshThis instanceof bsh.This) {
284                        safe((bsh.This) bshThis, variableName, Primitive.NULL);
285                }
286        }
287
288
289        /**
290         * Returns true if the variable exists and is equal to the expected value. If the variable
291         * is null or void, it will match an expected value of null. If the variable is not null or
292         * void, it will be passed to {@link Util#nullSafeEq(Object, Object)}.
293         *
294         * @param bshThis The 'this' object from Beanshell
295         * @param variableName The variable name to extract
296         * @param expectedValue The value we expect the variable to have
297         * @return True if the variable's value is equal to the expected value
298         * @throws GeneralException if reading the variable fails
299         */
300        public static boolean safeEquals(Object bshThis, String variableName, Object expectedValue) throws GeneralException {
301                if (bshThis instanceof bsh.This) {
302                        return safeEquals((bsh.This) bshThis, variableName, expectedValue);
303                }
304                return false;
305        }
306
307        /**
308         * Returns true if the variable exists and is equal to the expected value. If the variable
309         * is null or void, it will match an expected value of null. If the variable is not null or
310         * void, it will be passed to {@link Util#nullSafeEq(Object, Object)}.
311         *
312         * @param bshThis The 'this' object from Beanshell
313         * @param variableName The variable name to extract
314         * @param expectedValue The value we expect the variable to have
315         * @return True if the variable's value is equal to the expected value
316         * @throws GeneralException if reading the variable fails
317         */
318        private static boolean safeEquals(bsh.This bshThis, String variableName, Object expectedValue) throws GeneralException {
319                try {
320                        Object existingValue = bshThis.getNameSpace().getVariable(variableName, true);
321                        if (existingValue == null || Primitive.NULL.equals(existingValue) || Primitive.VOID.equals(existingValue)) {
322                                return (expectedValue == null);
323                        } else {
324                                return Util.nullSafeEq(existingValue, expectedValue);
325                        }
326                } catch(UtilEvalError e) {
327                        throw new GeneralException(e);
328                }
329        }
330
331        /**
332         * Private utility constructor
333         */
334        private BeanshellUtilities() {
335
336        }
337}