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}