001package com.identityworksllc.iiq.common.task; 002 003import com.fasterxml.jackson.core.type.TypeReference; 004import com.identityworksllc.iiq.common.Ref; 005import com.identityworksllc.iiq.common.Utilities; 006import sailpoint.api.SailPointContext; 007import sailpoint.object.*; 008import sailpoint.task.TaskMonitor; 009import sailpoint.tools.GeneralException; 010import sailpoint.tools.Util; 011 012import java.util.HashMap; 013import java.util.List; 014import java.util.Map; 015import java.util.Optional; 016import java.util.concurrent.ConcurrentHashMap; 017import java.util.function.Supplier; 018 019/** 020 * A threaded task executor to run an iterator rule or script. The rule or script will 021 * be invoked in parallel for each of the objects returned by the iterator query. 022 * The object will be passed as 'object' to Beanshell. 023 * 024 * NOTE that if you are iterating over a very large set of Sailpoint objects, it 025 * will be vastly more efficient to return a string ID from your object retriever 026 * and then look up each object in the iterator rule or script. This will also avoid 027 * problems with multiple contexts acting on the same object. The task option 028 * 'extractReferences' will do this for you automatically and is true by default. 029 */ 030public class ThreadedRuleRunner extends AbstractThreadedObjectIteratorTask<Object> { 031 032 /** 033 * The rule action type 034 */ 035 enum RuleActionType { 036 rule, 037 script 038 } 039 /** 040 * An optional rule to run after the batch 041 */ 042 protected Rule afterBatchRule; 043 /** 044 * An optional script to run after the batch 045 */ 046 protected Script afterBatchScript; 047 /** 048 * An optional rule to run before the batch 049 */ 050 protected Rule beforeBatchRule; 051 /** 052 * An optional script to run prior to the batch 053 */ 054 protected Script beforeBatchScript; 055 /** 056 * True if we should extract references to the objects 057 */ 058 private boolean extractReferences; 059 /** 060 * The iterator rule, if specified 061 */ 062 protected Rule iteratorRule; 063 /** 064 * The iterator script, if specified 065 */ 066 protected Script iteratorScript; 067 068 /** 069 * JSON arguments 070 */ 071 protected Map<String, Object> extraArguments; 072 073 /** 074 * The state that is shared between each item in a batch 075 */ 076 protected ThreadLocal<Map<String, Object>> threadState = ThreadLocal.withInitial(ConcurrentHashMap::new); 077 078 /** 079 * Invoked by the worker thread after each batch 080 * @param threadContext The context for this thread 081 * @throws GeneralException if anything fails 082 */ 083 @Override 084 public void afterBatch(SailPointContext threadContext) throws GeneralException { 085 super.afterBatch(threadContext); 086 087 Map<String, Object> arguments = new HashMap<>(); 088 if (extraArguments != null) { 089 arguments.putAll(this.extraArguments); 090 } 091 092 arguments.put("context", threadContext); 093 arguments.put("log", log); 094 arguments.put("logger", log); 095 arguments.put("state", threadState.get()); 096 097 if (this.afterBatchRule != null) { 098 threadContext.runRule(this.afterBatchRule, arguments); 099 } else if (this.afterBatchScript != null) { 100 Script tempScript = Utilities.getAsScript(this.afterBatchScript); 101 threadContext.runScript(tempScript, arguments); 102 } 103 } 104 105 /** 106 * Invoked by the worker thread before each batch 107 * @param threadContext The context for this thread 108 * @throws GeneralException if anything fails 109 */ 110 @Override 111 public void beforeBatch(SailPointContext threadContext) throws GeneralException { 112 super.beforeBatch(threadContext); 113 threadState.get().clear(); 114 115 Map<String, Object> arguments = new HashMap<>(); 116 if (extraArguments != null) { 117 arguments.putAll(this.extraArguments); 118 } 119 120 arguments.put("context", threadContext); 121 arguments.put("log", log); 122 arguments.put("logger", log); 123 arguments.put("state", threadState.get()); 124 125 if (this.beforeBatchRule != null) { 126 threadContext.runRule(this.beforeBatchRule, arguments); 127 } else if (this.beforeBatchScript != null) { 128 Script tempScript = Utilities.getAsScript(this.beforeBatchScript); 129 threadContext.runScript(tempScript, arguments); 130 } 131 } 132 133 /** 134 * If the extract reference flag is set, and this is a SailPointObject, transforms it 135 * into a {@link Reference} object instead. This will make everything 136 * enormously more efficient and avoid weird issues with Hibernate context. 137 * 138 * @param input The input 139 * @return The reference, or the original object 140 */ 141 @Override 142 protected Object convertObject(Object input) { 143 if (this.extractReferences && input instanceof SailPointObject) { 144 return Ref.of((SailPointObject) input); 145 } else { 146 return input; 147 } 148 } 149 150 /** 151 * Extracts the arguments passed to this task. This is the ONLY place that 152 * you ought to use the parent context. 153 * 154 * @param args The arguments to read 155 * @throws Exception if there are any failures during parsing 156 */ 157 @Override 158 protected void parseArgs(Attributes<String, Object> args) throws Exception { 159 // Mandatory 160 super.parseArgs(args); 161 162 Object ruleConfig = args.get("ruleConfig"); 163 if (ruleConfig instanceof Map) { 164 this.extraArguments = new HashMap<>((Map<String, Object>) ruleConfig); 165 } else if (ruleConfig instanceof String) { 166 String ruleConfigStr = Util.otoa(ruleConfig).trim(); 167 if (ruleConfigStr.startsWith("{")) { 168 com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); 169 Map<String, Object> jsonMap = mapper.readValue(ruleConfigStr, new TypeReference<>() {}); 170 if (jsonMap != null) { 171 this.extraArguments = jsonMap; 172 } 173 } else { 174 this.extraArguments = new HashMap<>(); 175 List<String> values = Util.csvToList(ruleConfigStr); 176 if ((values.size() % 2) == 1) { 177 throw new IllegalArgumentException("If you specify a 'ruleConfig' in CSV format, there must be an even number of key-value pairs"); 178 } 179 for(int i = 0; i < values.size(); i++) { 180 String key = values.get(i); 181 String value = values.get(i + 1); 182 183 if (Util.isNotNullOrEmpty(key) && Util.isNotNullOrEmpty(value)) { 184 this.extraArguments.put(key, value); 185 } 186 } 187 } 188 } else if (ruleConfig != null) { 189 throw new IllegalArgumentException("If specified, a 'ruleConfig' must be either a String or a Map"); 190 } 191 192 String actionTypeName = args.getString("iteratorType"); 193 if (Util.isNullOrEmpty(actionTypeName)) { 194 throw new IllegalArgumentException("A non-null 'iteratorType' must be set either 'rule' or 'script'"); 195 } 196 197 this.extractReferences = args.getBoolean("extractReferences", true); 198 199 if (args.containsKey("beforeBatchRule")) { 200 this.beforeBatchRule = context.getObject(Rule.class, args.getString("beforeBatchRule")); 201 if (this.beforeBatchRule == null) { 202 throw new IllegalArgumentException("The after batch rule specified (" + args.get("beforeBatchRule") + ") does not exist"); 203 } 204 this.beforeBatchRule.load(); 205 } 206 207 if (args.containsKey("beforeBatchScript")) { 208 this.beforeBatchScript = new Script(); 209 this.beforeBatchScript.setSource(args.getString("beforeBatchScript")); 210 } 211 212 if (args.containsKey("afterBatchRule")) { 213 this.afterBatchRule = context.getObject(Rule.class, args.getString("afterBatchRule")); 214 if (this.afterBatchRule == null) { 215 throw new IllegalArgumentException("The after batch rule specified (" + args.get("afterBatchRule") + ") does not exist"); 216 } 217 this.afterBatchRule.load(); 218 } 219 220 if (args.containsKey("afterBatchScript")) { 221 this.afterBatchScript = new Script(); 222 this.afterBatchScript.setSource(args.getString("afterBatchScript")); 223 } 224 225 RuleActionType actionType = RuleActionType.valueOf(actionTypeName); 226 227 if (actionType == RuleActionType.rule) { 228 String ruleNameOrId = args.getString("iteratorRule"); 229 if (Util.isNotNullOrEmpty(ruleNameOrId)) { 230 this.iteratorRule = context.getObject(Rule.class, ruleNameOrId); 231 if (this.iteratorRule == null) { 232 throw new IllegalArgumentException("The iterator rule specified (" + ruleNameOrId + ") does not exist"); 233 } 234 this.iteratorRule.load(); 235 } else { 236 throw new IllegalArgumentException("You must specify a value for iteratorRule for type = rule"); 237 } 238 } else if (actionType == RuleActionType.script) { 239 if (Util.isNotNullOrEmpty(args.getString("iteratorScript"))) { 240 this.iteratorScript = new Script(); 241 iteratorScript.setSource(args.getString("iteratorScript")); 242 } else { 243 throw new IllegalArgumentException("You must specify a value for iteratorScript for type = script"); 244 } 245 } else { 246 throw new IllegalArgumentException("Unsupported action type: " + actionType); 247 } 248 } 249 250 /** 251 * Executes this rule or script against the given object 252 * @param threadContext The context created for this specific thread 253 * @param parameters The parameters created for this thread 254 * @param obj The input object for this thread 255 * @return Always null (no meaningful result) 256 * @throws GeneralException if any failures occur 257 */ 258 public Object threadExecute(SailPointContext threadContext, Map<String, Object> parameters, Object obj) throws GeneralException { 259 if (log.isDebugEnabled()) { 260 log.debug("Processing object " + obj); 261 } 262 TaskMonitor monitor = new TaskMonitor(threadContext, taskResult); 263 264 if (obj instanceof Reference && this.extractReferences) { 265 obj = ((Reference)obj).resolve(threadContext); 266 if (log.isDebugEnabled()) { 267 log.debug("Object reference resolved to " + obj); 268 } 269 } 270 if (!terminated.get()) { 271 Map<String, Object> arguments = new HashMap<>(); 272 if (extraArguments != null) { 273 arguments.putAll(this.extraArguments); 274 } 275 276 arguments.put("context", threadContext); 277 arguments.put("log", Optional.ofNullable(Util.get(parameters, "log")).orElse(log)); 278 arguments.put("logger", Optional.ofNullable(Util.get(parameters, "log")).orElse(log)); 279 arguments.put("object", obj); 280 arguments.put("state", threadState.get()); 281 arguments.put("monitor", monitor); 282 arguments.put("worker", parameters.get("worker")); 283 arguments.put("taskListener", parameters.get("taskListener")); 284 arguments.put("terminated", (Supplier<Boolean>) terminated::get); 285 if (iteratorRule != null) { 286 threadContext.runRule(iteratorRule, arguments); 287 } else { 288 Script tempScript = Utilities.getAsScript(iteratorScript); 289 threadContext.runScript(tempScript, arguments); 290 } 291 } 292 return null; 293 } 294 295}