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}