001package com.identityworksllc.iiq.common;
002
003import com.fasterxml.jackson.core.JsonParser;
004import com.fasterxml.jackson.core.util.DefaultIndenter;
005import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
006import com.fasterxml.jackson.databind.DeserializationFeature;
007import com.fasterxml.jackson.databind.MapperFeature;
008import com.fasterxml.jackson.databind.ObjectMapper;
009import com.identityworksllc.iiq.common.logging.SLogger;
010import com.identityworksllc.iiq.common.query.ContextConnectionWrapper;
011import org.apache.commons.beanutils.PropertyUtils;
012import org.apache.commons.logging.Log;
013import org.apache.commons.logging.LogFactory;
014import org.apache.velocity.VelocityContext;
015import org.apache.velocity.app.Velocity;
016import sailpoint.api.MessageAccumulator;
017import sailpoint.api.ObjectAlreadyLockedException;
018import sailpoint.api.ObjectUtil;
019import sailpoint.api.PersistenceManager;
020import sailpoint.api.SailPointContext;
021import sailpoint.api.SailPointFactory;
022import sailpoint.object.*;
023import sailpoint.plugin.PluginsCache;
024import sailpoint.rest.BaseResource;
025import sailpoint.server.AbstractSailPointContext;
026import sailpoint.server.Environment;
027import sailpoint.server.SPKeyStore;
028import sailpoint.server.SailPointConsole;
029import sailpoint.tools.*;
030import sailpoint.tools.xml.ConfigurationException;
031import sailpoint.tools.xml.XMLObjectFactory;
032import sailpoint.web.BaseBean;
033import sailpoint.web.UserContext;
034import sailpoint.web.util.WebUtil;
035
036import javax.faces.context.FacesContext;
037import javax.servlet.http.HttpSession;
038import java.io.IOException;
039import java.io.InputStream;
040import java.io.PrintWriter;
041import java.io.StringWriter;
042import java.lang.reflect.InvocationTargetException;
043import java.lang.reflect.Method;
044import java.sql.Connection;
045import java.sql.SQLException;
046import java.text.MessageFormat;
047import java.text.SimpleDateFormat;
048import java.time.Duration;
049import java.time.Instant;
050import java.time.LocalDate;
051import java.time.LocalDateTime;
052import java.time.Period;
053import java.time.ZoneId;
054import java.time.ZonedDateTime;
055import java.time.format.DateTimeFormatter;
056import java.time.format.DateTimeParseException;
057import java.time.temporal.ChronoUnit;
058import java.util.*;
059import java.util.concurrent.Callable;
060import java.util.concurrent.ExecutorService;
061import java.util.concurrent.ForkJoinPool;
062import java.util.concurrent.TimeUnit;
063import java.util.concurrent.atomic.AtomicBoolean;
064import java.util.concurrent.atomic.AtomicReference;
065import java.util.concurrent.locks.Lock;
066import java.util.function.BiConsumer;
067import java.util.function.Consumer;
068import java.util.function.Function;
069import java.util.function.Supplier;
070import java.util.stream.Collectors;
071import java.util.stream.Stream;
072
073/**
074 * Static utility methods that are useful throughout any IIQ codebase, supplementing
075 * SailPoint's {@link Util} in many places.
076 */
077@SuppressWarnings("unused")
078public class Utilities {
079
080        /**
081         * Used as an indicator that the quick property lookup produced nothing.
082         * All instances of this class are always identical via equals().
083         */
084        public static final class PropertyLookupNone {
085                private PropertyLookupNone() {
086                }
087
088                @Override
089                public boolean equals(Object o) {
090                        if (this == o) return true;
091                        return o instanceof PropertyLookupNone;
092                }
093
094                @Override
095                public int hashCode() {
096                        return Objects.hash("");
097                }
098        }
099        private static final AtomicReference<ObjectMapper> DEFAULT_OBJECT_MAPPER = new AtomicReference<>();
100        /**
101         * The name of the worker pool, stored in CustomGlobal by default
102         */
103        public static final String IDW_WORKER_POOL = "idw.worker.pool";
104        /**
105         * The key used to store the user's most recent locale on their UIPrefs,
106         * captured by {@link #tryCaptureLocationInfo(SailPointContext, Identity)}
107         */
108        public static final String MOST_RECENT_LOCALE = "mostRecentLocale";
109        /**
110         * The key used to store the user's most recent timezone on their UIPrefs,
111         * captured by {@link #tryCaptureLocationInfo(SailPointContext, Identity)}
112         */
113        public static final String MOST_RECENT_TIMEZONE = "mostRecentTimezone";
114        /**
115         * A magic constant for use with the {@link Utilities#getQuickProperty(Object, String)} method.
116         * If the property is not an available 'quick property', this object will be returned.
117         */
118        public static final Object NONE = new PropertyLookupNone();
119        /**
120         * Indicates whether velocity has been initialized in this Utilities class
121         */
122        private static final AtomicBoolean VELOCITY_INITIALIZED = new AtomicBoolean();
123
124        /**
125         * The internal logger
126         */
127        private static final Log logger = LogFactory.getLog(Utilities.class);
128
129        /**
130         * Private utility constructor
131         */
132        private Utilities() {
133
134        }
135
136        /**
137         * Adds the given value to a {@link Collection} at the given key in the map.
138         * <p>
139         * If the map does not have a {@link Collection} at the given key, a new {@link ArrayList} is added, then the value is added to that.
140         * <p>
141         * This method is null safe. If the map or key is null, this method has no effect.
142         *
143         * @param map   The map to modify
144         * @param key   The map key that points to the list to add the value to
145         * @param value The value to add to the list
146         * @param <S>   The key type
147         * @param <T>   The list element type
148         */
149        @SuppressWarnings({"unchecked"})
150        public static <S, T extends Collection<S>> void addMapped(Map<String, T> map, String key, S value) {
151                if (map != null && key != null) {
152                        if (!map.containsKey(key)) {
153                                map.put(key, (T) new ArrayList<>());
154                        }
155                        map.get(key).add(value);
156                }
157        }
158
159        /**
160         * Adds a message banner to the current browser session which will show up at the
161         * top of each page. This requires that a FacesContext exist in the current
162         * session. You can't always assume this to be the case.
163         *
164         * If you're using the BaseCommonPluginResource class, it has a method to construct
165         * a new FacesContext if one doesn't exist.
166         *
167         * @param context The IIQ context
168         * @param message The Message to add
169         * @throws GeneralException if anything goes wrong
170         */
171        public static void addSessionMessage(SailPointContext context, Message message) throws GeneralException {
172                FacesContext fc = FacesContext.getCurrentInstance();
173                if (fc != null) {
174                        try {
175                                BaseBean bean = (BaseBean) fc.getApplication().evaluateExpressionGet(fc, "#{base}", Object.class);
176                                bean.addMessageToSession(message);
177                        } catch (Exception e) {
178                                throw new GeneralException(e);
179                        }
180                }
181        }
182
183        /**
184         * Adds a new MatchTerm as an 'and' to an existing MatchExpression, transforming an
185         * existing 'or' into a sub-expression if needed.
186         *
187         * @param input   The input MatchExpression
188         * @param newTerm The new term to 'and' with the existing expressions
189         * @return The resulting match term
190         */
191        public static IdentitySelector.MatchExpression andMatchTerm(IdentitySelector.MatchExpression input, IdentitySelector.MatchTerm newTerm) {
192                if (input.isAnd()) {
193                        input.addTerm(newTerm);
194                } else {
195                        IdentitySelector.MatchTerm bigOr = new IdentitySelector.MatchTerm();
196                        for (IdentitySelector.MatchTerm existing : Util.safeIterable(input.getTerms())) {
197                                bigOr.addChild(existing);
198                        }
199                        bigOr.setContainer(true);
200                        bigOr.setAnd(false);
201
202                        List<IdentitySelector.MatchTerm> newChildren = new ArrayList<>();
203                        newChildren.add(bigOr);
204                        newChildren.add(newTerm);
205
206                        input.setAnd(true);
207                        input.setTerms(newChildren);
208                }
209                return input;
210        }
211
212        /**
213         * Boxes a primitive type into its java Object type
214         *
215         * @param prim The primitive type class
216         * @return The boxed type
217         */
218        /*package*/
219        static Class<?> box(Class<?> prim) {
220                Objects.requireNonNull(prim, "The class to box must not be null");
221                if (prim.equals(Long.TYPE)) {
222                        return Long.class;
223                } else if (prim.equals(Integer.TYPE)) {
224                        return Integer.class;
225                } else if (prim.equals(Short.TYPE)) {
226                        return Short.class;
227                } else if (prim.equals(Character.TYPE)) {
228                        return Character.class;
229                } else if (prim.equals(Byte.TYPE)) {
230                        return Byte.class;
231                } else if (prim.equals(Boolean.TYPE)) {
232                        return Boolean.class;
233                } else if (prim.equals(Float.TYPE)) {
234                        return Float.class;
235                } else if (prim.equals(Double.TYPE)) {
236                        return Double.class;
237                }
238                throw new IllegalArgumentException("Unrecognized primitive type: " + prim.getName());
239        }
240
241        /**
242         * Returns true if the collection contains the given value ignoring case
243         *
244         * @param collection The collection to check
245         * @param value      The value to check for
246         * @return True if the collection contains the value, comparing case-insensitively
247         */
248        public static boolean caseInsensitiveContains(Collection<? extends Object> collection, Object value) {
249                if (collection == null || collection.isEmpty()) {
250                        return false;
251                }
252                // Most of the Set classes have efficient implementations
253                // of contains which we should check first for case-sensitive matches.
254                if (collection instanceof Set && collection.contains(value)) {
255                        return true;
256                }
257                if (value instanceof String) {
258                        String s2 = (String) value;
259                        for (Object o : collection) {
260                                if (o instanceof String) {
261                                        String s1 = (String) o;
262                                        if (s1.equalsIgnoreCase(s2)) {
263                                                return true;
264                                        }
265                                }
266                        }
267                        return false;
268                }
269                return collection.contains(value);
270        }
271
272        /**
273         * Gets a global singleton value from CustomGlobal. If it doesn't exist, uses
274         * the supplied factory (in a synchronized thread) to create it.
275         * <p>
276         * NOTE: This should NOT be an instance of a class defined in a plugin. If the
277         * plugin is redeployed and its classloader refreshes, it will cause the return
278         * value from this method to NOT match the "new" class in the new classloader,
279         * causing ClassCastExceptions.
280         *
281         * @param key     The key
282         * @param factory The factory to use if the stored value is null
283         * @param <T>     the expected output type
284         * @return the object from the global cache
285         */
286        @SuppressWarnings("unchecked")
287        public static <T> T computeGlobalSingleton(String key, Supplier<T> factory) {
288                T output = (T) CustomGlobal.get(key);
289                if (output == null && factory != null) {
290                        synchronized (CustomGlobal.class) {
291                                output = (T) CustomGlobal.get(key);
292                                if (output == null) {
293                                        output = factory.get();
294                                        if (output != null) {
295                                                CustomGlobal.put(key, output);
296                                        }
297                                }
298                        }
299                }
300                return output;
301        }
302
303        /**
304         * Invokes a command via the IIQ console which will run as though it was
305         * typed at the command prompt
306         *
307         * @param command The command to run
308         * @return the results of the command
309         * @throws Exception if a failure occurs during run
310         */
311        public static String consoleInvoke(String command) throws Exception {
312                final Console console = new SailPointConsole();
313                final SailPointContext context = SailPointFactory.getCurrentContext();
314                try {
315                        SailPointFactory.setContext(null);
316                        try (StringWriter stringWriter = new StringWriter()) {
317                                try (PrintWriter writer = new PrintWriter(stringWriter)) {
318                                        Method doCommand = console.getClass().getSuperclass().getDeclaredMethod("doCommand", String.class, PrintWriter.class);
319                                        doCommand.setAccessible(true);
320                                        doCommand.invoke(console, command, writer);
321                                }
322                                return stringWriter.getBuffer().toString();
323                        }
324                } finally {
325                        SailPointFactory.setContext(context);
326                }
327        }
328
329        /**
330         * Returns true if the Throwable message (or any of its causes) contain the given message
331         *
332         * @param t     The throwable to check
333         * @param cause The message to check for
334         * @return True if the message appears anywhere
335         */
336        public static boolean containsMessage(Throwable t, String cause) {
337                if (t == null || t.toString() == null || cause == null || cause.isEmpty()) {
338                        return false;
339                }
340                if (t.toString().contains(cause)) {
341                        return true;
342                }
343                if (t.getCause() != null) {
344                        return containsMessage(t.getCause(), cause);
345                }
346                return false;
347        }
348
349        /**
350         * Returns true if the given match expression references the given property anywhere. This is
351         * mainly intended for one-off operations to find roles with particular selectors.
352         *
353         * @param input    The filter input
354         * @param property The property to check for
355         * @return True if the MatchExpression references the given property anywhere in its tree
356         */
357        public static boolean containsProperty(IdentitySelector.MatchExpression input, String property) {
358                for (IdentitySelector.MatchTerm term : Util.safeIterable(input.getTerms())) {
359                        boolean contains = containsProperty(term, property);
360                        if (contains) {
361                                return true;
362                        }
363                }
364                return false;
365        }
366
367        /**
368         * Returns true if the given match term references the given property anywhere. This is
369         * mainly intended for one-off operations to find roles with particular selectors.
370         *
371         * @param term     The MatchTerm to check
372         * @param property The property to check for
373         * @return True if the MatchTerm references the given property anywhere in its tree
374         */
375        public static boolean containsProperty(IdentitySelector.MatchTerm term, String property) {
376                if (term.isContainer()) {
377                        for (IdentitySelector.MatchTerm child : Util.safeIterable(term.getChildren())) {
378                                boolean contains = containsProperty(child, property);
379                                if (contains) {
380                                        return true;
381                                }
382                        }
383                } else {
384                        return Util.nullSafeCaseInsensitiveEq(term.getName(), property);
385                }
386                return false;
387        }
388
389        /**
390         * Returns true if the given filter references the given property anywhere. This is
391         * mainly intended for one-off operations to find roles with particular selectors.
392         * <p>
393         * If either the filter or the property is null, returns false.
394         *
395         * @param input    The filter input
396         * @param property The property to check for
397         * @return True if the Filter references the given property anywhere in its tree
398         */
399        public static boolean containsProperty(Filter input, String property) {
400                if (Util.isNullOrEmpty(property)) {
401                        return false;
402                }
403                if (input instanceof Filter.CompositeFilter) {
404                        Filter.CompositeFilter compositeFilter = (Filter.CompositeFilter) input;
405                        for (Filter child : Util.safeIterable(compositeFilter.getChildren())) {
406                                boolean contains = containsProperty(child, property);
407                                if (contains) {
408                                        return true;
409                                }
410                        }
411                } else if (input instanceof Filter.LeafFilter) {
412                        Filter.LeafFilter leafFilter = (Filter.LeafFilter) input;
413                        return Util.nullSafeCaseInsensitiveEq(leafFilter.getProperty(), property) || Util.nullSafeCaseInsensitiveEq(leafFilter.getSubqueryProperty(), property);
414                }
415                return false;
416        }
417
418        /**
419         * Converts the input object using the two date formats provided by invoking
420         * the four-argument {@link #convertDateFormat(Object, String, String, ZoneId)}.
421         *
422         * The system default ZoneId will be used.
423         *
424         * @param something The input object, which can be a string or various date objects
425         * @param inputDateFormat The input date format, which will be applied to a string input
426         * @param outputDateFormat The output date format, which will be applied to the intermediate date
427         * @return The input object formatted according to the output date format
428         * @throws java.time.format.DateTimeParseException if there is a failure parsing the date
429         */
430        public static String convertDateFormat(Object something, String inputDateFormat, String outputDateFormat) {
431                return convertDateFormat(something, inputDateFormat, outputDateFormat, ZoneId.systemDefault());
432        }
433
434        /**
435         * Converts the input object using the two date formats provided.
436         *
437         * If the input object is a String (the most likely case), it will be
438         * transformed into an intermediate date using the inputDateFormat. Date
439         * type inputs (Date, LocalDate, LocalDateTime, and Long) will be
440         * converted directly to an intermediate date.
441         *
442         * JDBC classes like Date and Timestamp extend java.util.Date, so that
443         * logic would apply here.
444         *
445         * The intermediate date will then be formatted using the outputDateFormat
446         * at the appropriate ZoneId.
447         *
448         * @param something The input object, which can be a string or various date objects
449         * @param inputDateFormat The input date format, which will be applied to a string input
450         * @param outputDateFormat The output date format, which will be applied to the intermediate date
451         * @param zoneId The time zone to use for parsing and formatting
452         * @return The input object formatted according to the output date format
453         * @throws java.time.format.DateTimeParseException if there is a failure parsing the date
454         */
455        public static String convertDateFormat(Object something, String inputDateFormat, String outputDateFormat, ZoneId zoneId) {
456                if (something == null) {
457                        return null;
458                }
459
460                LocalDateTime intermediateDate;
461
462                DateTimeFormatter inputFormat = DateTimeFormatter.ofPattern(inputDateFormat).withZone(zoneId);
463                DateTimeFormatter outputFormat = DateTimeFormatter.ofPattern(outputDateFormat).withZone(zoneId);
464
465                if (something instanceof String) {
466                        if (Util.isNullOrEmpty((String) something)) {
467                                return null;
468                        }
469
470                        String inputString = (String) something;
471
472                        intermediateDate = LocalDateTime.parse(inputString, inputFormat);
473                } else if (something instanceof Date) {
474                        Date somethingDate = (Date) something;
475                        intermediateDate = somethingDate.toInstant().atZone(zoneId).toLocalDateTime();
476                } else if (something instanceof LocalDate) {
477                        intermediateDate = ((LocalDate) something).atStartOfDay();
478                } else if (something instanceof LocalDateTime) {
479                        intermediateDate = (LocalDateTime) something;
480                } else if (something instanceof Number) {
481                        long timestamp = ((Number) something).longValue();
482                        intermediateDate = Instant.ofEpochMilli(timestamp).atZone(zoneId).toLocalDateTime();
483                } else {
484                        throw new IllegalArgumentException("The input type is not valid (expected String, Date, LocalDate, LocalDateTime, or Long");
485                }
486
487                return intermediateDate.format(outputFormat);
488        }
489
490        /**
491         * Determines whether the test date is at least N days ago.
492         *
493         * @param testDate The test date
494         * @param days The number of dates
495         * @return True if this date is equal to or earlier than the calendar date N days ago
496         */
497        public static boolean dateAtLeastDaysAgo(Date testDate, int days) {
498                LocalDate ldt1 = testDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
499                LocalDate ldt2 = LocalDate.now().minus(days, ChronoUnit.DAYS);
500
501                return !ldt1.isAfter(ldt2);
502        }
503
504        /**
505         * Determines whether the test date is at least N years ago.
506         *
507         * NOTE: This method checks using actual calendar years, rather than
508         * calculating a number of days and comparing that. It will take into
509         * account leap years and other date weirdness.
510         *
511         * @param testDate The test date
512         * @param years The number of years
513         * @return True if this date is equal to or earlier than the calendar date N years ago
514         */
515        public static boolean dateAtLeastYearsAgo(Date testDate, int years) {
516                LocalDate ldt1 = testDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
517                LocalDate ldt2 = LocalDate.now().minus(years, ChronoUnit.YEARS);
518
519                return !ldt1.isAfter(ldt2);
520        }
521
522        /**
523         * Converts two Date objects to {@link LocalDate} at the system default
524         * time zone and returns the number of days between them.
525         *
526         * If you pass the dates in the wrong order (first parameter is the later
527         * date), they will be silently swapped before returning the Duration.
528         *
529         * @param firstTime The first time to compare
530         * @param secondTime The second time to compare
531         * @return The {@link Period} between the two days
532         */
533        public static Period dateDifference(Date firstTime, Date secondTime) {
534                if (firstTime == null || secondTime == null) {
535                        throw new IllegalArgumentException("Both arguments to dateDifference must be non-null");
536                }
537
538                LocalDate ldt1 = firstTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
539                LocalDate ldt2 = secondTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
540
541                // Swap the dates if they're backwards
542                if (ldt1.isAfter(ldt2)) {
543                        LocalDate tmp = ldt2;
544                        ldt2 = ldt1;
545                        ldt1 = tmp;
546                }
547
548                return Period.between(ldt1, ldt2);
549        }
550
551        /**
552         * Coerces the long millisecond timestamps to Date objects, then returns the
553         * result of {@link #dateDifference(Date, Date)}.
554         *
555         * @param firstTimeMillis The first time to compare
556         * @param secondTimeMillis The second time to compare
557         * @return The {@link Period} between the two days
558         */
559        public static Period dateDifference(long firstTimeMillis, long secondTimeMillis) {
560                return dateDifference(new Date(firstTimeMillis), new Date(secondTimeMillis));
561        }
562
563        /**
564         * Detaches the given object as much as possible from the database context by converting it to XML and back again.
565         * <p>
566         * Converting to XML requires resolving all Hibernate lazy-loaded references.
567         *
568         * @param context The context to use to parse the XML
569         * @param o       The object to detach
570         * @return A reference to the object detached from any Hibernate session
571         * @param <T> A class extending SailPointObject
572         * @throws GeneralException if a parsing failure occurs
573         */
574        public static <T extends SailPointObject> T detach(SailPointContext context, T o) throws GeneralException {
575                @SuppressWarnings("unchecked")
576                T retVal = (T) SailPointObject.parseXml(context, o.toXml());
577                context.decache(o);
578                return retVal;
579        }
580
581        /**
582         * Throws an exception if the string is null or empty
583         *
584         * @param values The strings to test
585         */
586        public static void ensureAllNotNullOrEmpty(String... values) {
587                if (values == null || Util.isAnyNullOrEmpty(values)) {
588                        throw new NullPointerException();
589                }
590        }
591
592        /**
593         * Throws an exception if the string is null or empty
594         *
595         * @param value The string to test
596         */
597        public static void ensureNotNullOrEmpty(String value) {
598                if (Util.isNullOrEmpty(value)) {
599                        throw new NullPointerException();
600                }
601        }
602
603        /**
604         * Uses reflection to evict the RuleRunner pool cache
605         *
606         * @throws Exception if anything goes wrong
607         */
608        public static void evictRuleRunnerPool() throws Exception {
609                RuleRunner ruleRunner = Environment.getEnvironment().getRuleRunner();
610
611                java.lang.reflect.Field _poolField = ruleRunner.getClass().getDeclaredField("_pool");
612                _poolField.setAccessible(true);
613
614                Object poolObject = _poolField.get(ruleRunner);
615
616                _poolField.setAccessible(false);
617
618                java.lang.reflect.Method clearMethod = poolObject.getClass().getMethod("clear");
619                clearMethod.invoke(poolObject);
620        }
621
622        /**
623         * Extracts the value of the given property from each item in the list and returns
624         * a new list containing those property values.
625         * <p>
626         * If the input list is null or empty, an empty list will be returned.
627         * <p>
628         * This is roughly identical to
629         * <p>
630         * input.stream().map(item -> (T)Utilities.getProperty(item, property)).collect(Collectors.toList())
631         *
632         * @param input        The input list
633         * @param property     The property to extract
634         * @param expectedType The expected type of the output objects
635         * @param <S>          The input type
636         * @param <T>          The output type
637         * @return A list of the extracted values
638         * @throws GeneralException if extraction goes wrong
639         */
640        @SuppressWarnings("unchecked")
641        public static <S, T> List<T> extractProperty(List<S> input, String property, Class<T> expectedType) throws GeneralException {
642                List<T> output = new ArrayList<>();
643                for (S inputObject : Util.safeIterable(input)) {
644                        output.add((T) Utilities.getProperty(inputObject, property));
645                }
646                return output;
647        }
648
649        /**
650         * Transfors an input Filter into a MatchExpression
651         *
652         * @param input The Filter
653         * @return The MatchExpression
654         */
655        public static IdentitySelector.MatchExpression filterToMatchExpression(Filter input) {
656                IdentitySelector.MatchExpression expression = new IdentitySelector.MatchExpression();
657                expression.addTerm(filterToMatchTerm(input));
658                return expression;
659        }
660
661        /**
662         * Transfors an input Filter into a MatchTerm
663         *
664         * @param input The Filter
665         * @return The MatchTerm
666         */
667        public static IdentitySelector.MatchTerm filterToMatchTerm(Filter input) {
668                IdentitySelector.MatchTerm matchTerm = null;
669
670                if (input instanceof Filter.CompositeFilter) {
671                        matchTerm = new IdentitySelector.MatchTerm();
672                        matchTerm.setContainer(true);
673
674                        Filter.CompositeFilter compositeFilter = (Filter.CompositeFilter) input;
675                        if (compositeFilter.getOperation().equals(Filter.BooleanOperation.AND)) {
676                                matchTerm.setAnd(true);
677                        } else if (compositeFilter.getOperation().equals(Filter.BooleanOperation.NOT)) {
678                                throw new UnsupportedOperationException("MatchExpressions do not support NOT filters");
679                        }
680
681                        for (Filter child : Util.safeIterable(compositeFilter.getChildren())) {
682                                matchTerm.addChild(filterToMatchTerm(child));
683                        }
684                } else if (input instanceof Filter.LeafFilter) {
685                        matchTerm = new IdentitySelector.MatchTerm();
686
687                        Filter.LeafFilter leafFilter = (Filter.LeafFilter) input;
688                        if (leafFilter.getOperation().equals(Filter.LogicalOperation.IN)) {
689                                matchTerm.setContainer(true);
690                                List<String> values = Util.otol(leafFilter.getValue());
691                                if (values == null) {
692                                        throw new IllegalArgumentException("For IN filters, only List<String> values are accepted");
693                                }
694                                for (String value : values) {
695                                        IdentitySelector.MatchTerm child = new IdentitySelector.MatchTerm();
696                                        child.setName(leafFilter.getProperty());
697                                        child.setValue(value);
698                                        child.setType(IdentitySelector.MatchTerm.Type.Entitlement);
699                                        matchTerm.addChild(child);
700                                }
701                        } else if (leafFilter.getOperation().equals(Filter.LogicalOperation.EQ)) {
702                                matchTerm.setName(leafFilter.getProperty());
703                                matchTerm.setValue(Util.otoa(leafFilter.getValue()));
704                                matchTerm.setType(IdentitySelector.MatchTerm.Type.Entitlement);
705                        } else if (leafFilter.getOperation().equals(Filter.LogicalOperation.ISNULL)) {
706                                matchTerm.setName(leafFilter.getProperty());
707                                matchTerm.setType(IdentitySelector.MatchTerm.Type.Entitlement);
708                        } else {
709                                throw new UnsupportedOperationException("MatchExpressions do not support " + leafFilter.getOperation() + " operations");
710                        }
711
712                }
713
714                return matchTerm;
715        }
716
717        /**
718         * Returns the first item in the list that is not null or empty. If all items are null
719         * or empty, or the input is itself a null or empty array, an empty string will be returned.
720         * This method will never return null.
721         *
722         * @param inputs The input strings
723         * @return The first not null or empty item, or an empty string if none is found
724         */
725        public static String firstNotNullOrEmpty(String... inputs) {
726                if (inputs == null || inputs.length == 0) {
727                        return "";
728                }
729                for (String in : inputs) {
730                        if (Util.isNotNullOrEmpty(in)) {
731                                return in;
732                        }
733                }
734                return "";
735        }
736
737        /**
738         * Formats the input message template using Java's MessageFormat class
739         * and the SLogger.Formatter class.
740         * <p>
741         * If no parameters are provided, the message template is returned as-is.
742         *
743         * @param messageTemplate The message template into which parameters should be injected
744         * @param params          The parameters to be injected
745         * @return The resulting string
746         */
747        public static String format(String messageTemplate, Object... params) {
748                if (params == null || params.length == 0) {
749                        return messageTemplate;
750                }
751
752                Object[] formattedParams = new Object[params.length];
753                for (int p = 0; p < params.length; p++) {
754                        formattedParams[p] = new SLogger.Formatter(params[p]);
755                }
756                return MessageFormat.format(messageTemplate, formattedParams);
757        }
758
759        /**
760         * Retrieves a key from the given Map in a 'fuzzy' way. Keys will be matched
761         * ignoring case and whitespace.
762         * <p>
763         * For example, given the actual key "toolboxConfig", the following inputs
764         * would also match:
765         * <p>
766         * "toolbox config"
767         * "Toolbox Config"
768         * "ToolboxConfig"
769         * <p>
770         * The first matching key will be returned, so it is up to the caller to ensure
771         * that the input does not match more than one actual key in the Map. For some
772         * Map types, "first matching key" may be nondeterministic if more than one key
773         * matches.
774         * <p>
775         * If either the provided key or the map is null, this method will return null.
776         *
777         * @param map      The map from which to query the value
778         * @param fuzzyKey The fuzzy key
779         * @param <T>      The return type, for convenience
780         * @return The value from the map
781         */
782        @SuppressWarnings("unchecked")
783        public static <T> T fuzzyGet(Map<String, Object> map, String fuzzyKey) {
784                if (map == null || map.isEmpty() || Util.isNullOrEmpty(fuzzyKey)) {
785                        return null;
786                }
787
788                // Quick exact match
789                if (map.containsKey(fuzzyKey)) {
790                        return (T) map.get(fuzzyKey);
791                }
792
793                // Case-insensitive match
794                for (String key : map.keySet()) {
795                        if (safeTrim(key).equalsIgnoreCase(safeTrim(fuzzyKey))) {
796                                return (T) map.get(key);
797                        }
798                }
799
800                // Whitespace and case insensitive match
801                String collapsedKey = safeTrim(fuzzyKey.replaceAll("\\s+", ""));
802                for (String key : map.keySet()) {
803                        if (safeTrim(key).equalsIgnoreCase(collapsedKey)) {
804                                return (T) map.get(key);
805                        }
806                }
807
808                return null;
809        }
810
811        /**
812         * Gets the input object as a thread-safe Script. If the input is a String, it
813         * will be interpreted as the source of a Script. If the input is already a Script
814         * object, it will be copied for thread safety and the copy returned.
815         *
816         * @param input The input object, either a string or a script
817         * @return The output
818         */
819        public static Script getAsScript(Object input) {
820                if (input instanceof Script) {
821                        Script copy = new Script();
822                        Script os = (Script) input;
823
824                        copy.setSource(os.getSource());
825                        copy.setIncludes(os.getIncludes());
826                        copy.setLanguage(os.getLanguage());
827                        return copy;
828                } else if (input instanceof String) {
829                        Script tempScript = new Script();
830                        tempScript.setSource(Util.otoa(input));
831                        return tempScript;
832                }
833                return null;
834        }
835
836        /**
837         * Gets the attributes of the given source object. If the source is not a Sailpoint
838         * object, or if it's one of the objects without attributes, this method returns
839         * null.
840         *
841         * @param source The source object, which may implement an Attributes container method
842         * @return The attributes, if any, or null
843         */
844        public static Attributes<String, Object> getAttributes(Object source) {
845                if (source instanceof Identity) {
846                        Attributes<String, Object> attributes = ((Identity) source).getAttributes();
847                        if (attributes == null) {
848                                return new Attributes<>();
849                        }
850                        return attributes;
851                } else if (source instanceof LinkInterface) {
852                        Attributes<String, Object> attributes = ((LinkInterface) source).getAttributes();
853                        if (attributes == null) {
854                                return new Attributes<>();
855                        }
856                        return attributes;
857                } else if (source instanceof Bundle) {
858                        return ((Bundle) source).getAttributes();
859                } else if (source instanceof Custom) {
860                        return ((Custom) source).getAttributes();
861                } else if (source instanceof Configuration) {
862                        return ((Configuration) source).getAttributes();
863                } else if (source instanceof Application) {
864                        return ((Application) source).getAttributes();
865                } else if (source instanceof CertificationItem) {
866                        // This one returns a Map for some reason
867                        return new Attributes<>(((CertificationItem) source).getAttributes());
868                } else if (source instanceof CertificationEntity) {
869                        return ((CertificationEntity) source).getAttributes();
870                } else if (source instanceof Certification) {
871                        return ((Certification) source).getAttributes();
872                } else if (source instanceof CertificationDefinition) {
873                        return ((CertificationDefinition) source).getAttributes();
874                } else if (source instanceof TaskDefinition) {
875                        return ((TaskDefinition) source).getArguments();
876                } else if (source instanceof TaskItem) {
877                        return ((TaskItem) source).getAttributes();
878                } else if (source instanceof ManagedAttribute) {
879                        return ((ManagedAttribute) source).getAttributes();
880                } else if (source instanceof Form) {
881                        return ((Form) source).getAttributes();
882                } else if (source instanceof IdentityRequest) {
883                        return ((IdentityRequest) source).getAttributes();
884                } else if (source instanceof IdentitySnapshot) {
885                        return ((IdentitySnapshot) source).getAttributes();
886                } else if (source instanceof ResourceObject) {
887                        Attributes<String, Object> attributes = ((ResourceObject) source).getAttributes();
888                        if (attributes == null) {
889                                attributes = new Attributes<>();
890                        }
891                        return attributes;
892                } else if (source instanceof Field) {
893                        return ((Field) source).getAttributes();
894                } else if (source instanceof ProvisioningPlan) {
895                        Attributes<String, Object> arguments = ((ProvisioningPlan) source).getArguments();
896                        if (arguments == null) {
897                                arguments = new Attributes<>();
898                        }
899                        return arguments;
900                } else if (source instanceof IntegrationConfig) {
901                        return ((IntegrationConfig) source).getAttributes();
902                } else if (source instanceof ProvisioningProject) {
903                        return ((ProvisioningProject) source).getAttributes();
904                } else if (source instanceof ProvisioningTransaction) {
905                        return ((ProvisioningTransaction) source).getAttributes();
906                } else if (source instanceof ProvisioningPlan.AbstractRequest) {
907                        return ((ProvisioningPlan.AbstractRequest) source).getArguments();
908                } else if (source instanceof Rule) {
909                        return ((Rule) source).getAttributes();
910                } else if (source instanceof WorkItem) {
911                        return ((WorkItem) source).getAttributes();
912                } else if (source instanceof Entitlements) {
913                        return ((Entitlements) source).getAttributes();
914                } else if (source instanceof RpcRequest) {
915                        return new Attributes<>(((RpcRequest) source).getArguments());
916                } else if (source instanceof ApprovalItem) {
917                        return ((ApprovalItem) source).getAttributes();
918                }
919                return null;
920        }
921
922        /**
923         * Gets the time zone associated with the logged in user's session, based on a
924         * UserContext or Identity. As a fallback, the {@link WebUtil} API is used to
925         * try to retrieve the time zone from the HTTP session.
926         *
927         * You can use {@link #tryCaptureLocationInfo(SailPointContext, UserContext)}
928         * in a plugin REST API or QuickLink textScript context to permanently store the
929         * Identity's local time zone as a UI preference.
930         *
931         * @param userContext The user context to check for a time zone, or null
932         * @param identity The identity to check for a time zone, or null
933         * @return The time zone for this user
934         */
935        public static TimeZone getClientTimeZone(Identity identity, UserContext userContext) {
936                if (userContext != null) {
937                        return userContext.getUserTimeZone();
938                } else if (identity != null && identity.getUIPreference(MOST_RECENT_TIMEZONE) != null) {
939                        return TimeZone.getTimeZone((String) identity.getUIPreference(MOST_RECENT_TIMEZONE));
940                } else {
941                        return WebUtil.getTimeZone(TimeZone.getDefault());
942                }
943        }
944
945        /**
946         * Extracts a list of all country names from the JDK's Locale class. This will
947         * be as up-to-date as your JDK itself is.
948         *
949         * @return A sorted list of country names
950         */
951        public static List<String> getCountryNames() {
952                String[] countryCodes = Locale.getISOCountries();
953                List<String> countries = new ArrayList<>();
954                for (String countryCode : countryCodes) {
955                        Locale obj = new Locale("", countryCode);
956                        countries.add(obj.getDisplayCountry());
957                }
958                Collections.sort(countries);
959                return countries;
960        }
961
962        /**
963         * Returns the first item in the input that is not nothing according to the {@link #isNothing(Object)} method.
964         *
965         * @param items The input items
966         * @param <T>   The superclass of all input items
967         * @return if the item is not null or empty
968         */
969        @SafeVarargs
970        public static <T> T getFirstNotNothing(T... items) {
971                if (items == null || items.length == 0) {
972                        return null;
973                }
974                for (T item : items) {
975                        if (!Utilities.isNothing(item)) {
976                                return item;
977                        }
978                }
979                return null;
980        }
981
982        /**
983         * Returns the first item in the input that is not null, an empty Optional,
984         * or a Supplier that returns null.
985         *
986         * @param items The input items
987         * @param <T>   The superclass of all input items
988         * @return if the item is not null or empty
989         */
990        public static <T> T getFirstNotNull(List<? extends T> items) {
991                if (items == null) {
992                        return null;
993                }
994                for (T item : items) {
995                        boolean nullish = (item == null);
996
997                        if (!nullish && item instanceof Optional) {
998                                nullish = !((Optional<?>) item).isPresent();
999                        }
1000
1001                        if (!nullish && item instanceof Supplier) {
1002                                Object output = ((Supplier<?>) item).get();
1003                                if (output instanceof Optional) {
1004                                        nullish = !((Optional<?>) output).isPresent();
1005                                } else {
1006                                        nullish = (output == null);
1007                                }
1008                        }
1009
1010                        if (!nullish) {
1011                                return item;
1012                        }
1013                }
1014                return null;
1015        }
1016
1017        /**
1018         * Returns the first item in the input that is not null, an empty Optional,
1019         * or a Supplier that returns null.
1020         *
1021         * @param items The input items
1022         * @param <T>   The superclass of all input items
1023         * @return if the item is not null or empty
1024         */
1025        @SafeVarargs
1026        public static <T> T getFirstNotNull(T... items) {
1027                return getFirstNotNull(Arrays.asList(items));
1028        }
1029
1030        /**
1031         * Gets the iiq.properties file contents (properly closing it, unlike IIQ...)
1032         *
1033         * @return The IIQ properties
1034         * @throws GeneralException if any load failures occur
1035         */
1036        public static Properties getIIQProperties() throws GeneralException {
1037                Properties props = new Properties();
1038
1039                try (InputStream is = AbstractSailPointContext.class.getResourceAsStream("/" + BrandingServiceFactory.getService().getPropertyFile())) {
1040                        props.load(is);
1041                } catch (IOException e) {
1042                        throw new GeneralException(e);
1043                }
1044                return props;
1045        }
1046
1047        /**
1048         * Returns a default Jackson object mapper, independent of IIQ versions.
1049         * The template ObjectMapper is generated on the
1050         *
1051         * @return A copy of our cached ObjectMapper
1052         */
1053        public static ObjectMapper getJacksonObjectMapper() {
1054                if (DEFAULT_OBJECT_MAPPER.get() == null) {
1055                        synchronized (CustomGlobal.class) {
1056                                if (DEFAULT_OBJECT_MAPPER.get() == null) {
1057                                        ObjectMapper objectMapper = new ObjectMapper();
1058                                        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
1059                                        objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
1060                                        objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
1061                                        DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter("    ", DefaultIndenter.SYS_LF);
1062                                        DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
1063                                        printer.indentObjectsWith(indenter);
1064                                        printer.indentArraysWith(indenter);
1065                                        objectMapper.setDefaultPrettyPrinter(printer);
1066
1067                                        DEFAULT_OBJECT_MAPPER.set(objectMapper);
1068                                }
1069                        }
1070                }
1071
1072                return DEFAULT_OBJECT_MAPPER.get().copy();
1073        }
1074
1075        /**
1076         * IIQ tries to display timestamps in a locale-specific way to the user. It does
1077         * this by storing the browser's time zone in the HTTP session and converting
1078         * dates to a local value before display. This includes things like scheduled
1079         * task execution times, etc.
1080         *
1081         * However, for date form fields, which should be timeless (i.e. just a date, no time
1082         * component), IIQ sends a value "of midnight at the browser's time zone". Depending
1083         * on the browser's offset from the server time, this can result (in the worst case)
1084         * in the actual selected instant being a full day ahead or behind the intended value.
1085         *
1086         * This method corrects the offset using Java 8's Time API, which allows for timeless
1087         * representations, to determine the date the user intended to select, then converting
1088         * that back to midnight in the server time zone. The result is stored back onto the Field.
1089         *
1090         * If the time is offset from midnight but the user time zone is the same as the server
1091         * time zone, it means we're in a weird context where IIQ does not know the user's time
1092         * zone, and we have to guess. We will guess up to +12 and -11 from the server timezone,
1093         * which should cover most cases. However, if you have users directly around the world
1094         * from your server timezone, you may see problems.
1095         *
1096         * If the input date is null, returns null.
1097         *
1098         * @param inputDate The input date for the user
1099         * @param identity  The identity who has timezone information stored
1100         * @return The calculated local date, based on the input date and the Identity's time zone
1101         */
1102        public static Date getLocalDate(Date inputDate, Identity identity) {
1103                if (inputDate == null) {
1104                        return null;
1105                }
1106                Instant instant = Instant.ofEpochMilli(inputDate.getTime());
1107                TimeZone userTimeZone = getClientTimeZone(identity, null);
1108                ZoneId userZoneId = userTimeZone.toZoneId();
1109                ZoneId serverTimeZone = TimeZone.getDefault().toZoneId();
1110                ZonedDateTime zonedDateTime = instant.atZone(userZoneId);
1111                if (zonedDateTime.getHour() != 0) {
1112                        // Need to shift
1113                        if (userZoneId.equals(serverTimeZone)) {
1114                                // IIQ doesn't know where the user is located, so we have to guess
1115                                // Note that this will fail for user-to-server shifts greater than +12 and -11
1116                                LocalDate timelessDate;
1117                                if (zonedDateTime.getHour() >= 12) {
1118                                        // Assume that the user is located in a time zone ahead of the server (e.g. server is in UTC and user is in Calcutta), submitted time will appear to be before midnight in server time
1119                                        timelessDate = LocalDate.of(zonedDateTime.getYear(), zonedDateTime.getMonth(), zonedDateTime.getDayOfMonth());
1120                                        timelessDate = timelessDate.plusDays(1);
1121                                } else {
1122                                        // The user is located in a time zone behind the server (e.g. server is in UTC and user is in New York), submitted time will appear to be after midnight in server time
1123                                        timelessDate = LocalDate.of(zonedDateTime.getYear(), zonedDateTime.getMonth(), zonedDateTime.getDayOfMonth());
1124                                }
1125                                return new Date(timelessDate.atStartOfDay(serverTimeZone).toInstant().toEpochMilli());
1126                        } else {
1127                                // IIQ knows where the user is located and we can just directly convert
1128                                LocalDate timelessDate = LocalDate.of(zonedDateTime.getYear(), zonedDateTime.getMonth(), zonedDateTime.getDayOfMonth());
1129                                return new Date(timelessDate.atStartOfDay(serverTimeZone).toInstant().toEpochMilli());
1130                        }
1131                }
1132                // If the zonedDateTime in user time is midnight, then the user and server are aligned
1133                return inputDate;
1134        }
1135
1136        /**
1137         * Localizes a message based on the locale information captured on the
1138         * target Identity. This information can be captured in any plugin web
1139         * service or other session-attached code using {@link #tryCaptureLocationInfo(SailPointContext, UserContext)}
1140         * <p>
1141         * If no locale information has been captured, the system default will
1142         * be used instead.
1143         *
1144         * @param target  The target user
1145         * @param message The non-null message to translate
1146         * @return The localized message
1147         */
1148        public static String getLocalizedMessage(Identity target, Message message) {
1149                if (message == null) {
1150                        throw new NullPointerException("message must not be null");
1151                }
1152                TimeZone tz = TimeZone.getDefault();
1153                if (target != null) {
1154                        String mrtz = Util.otoa(target.getUIPreference(MOST_RECENT_TIMEZONE));
1155                        if (Util.isNotNullOrEmpty(mrtz)) {
1156                                tz = TimeZone.getTimeZone(mrtz);
1157                        }
1158                }
1159
1160                Locale locale = Locale.getDefault();
1161                if (target != null) {
1162                        String mrl = Util.otoa(target.getUIPreference(MOST_RECENT_LOCALE));
1163                        if (Util.isNotNullOrEmpty(mrl)) {
1164                                locale = Locale.forLanguageTag(mrl);
1165                        }
1166                }
1167
1168                return message.getLocalizedMessage(locale, tz);
1169        }
1170
1171        /**
1172         * Gets the current plugin version from the environment, or NA if not defined
1173         * @return The current plugin cache version
1174         */
1175        public static String getPluginVersion() {
1176                String version = "NA";
1177                if (Environment.getEnvironment() != null) {
1178                        PluginsCache cache = Environment.getEnvironment().getPluginsCache();
1179                        if (cache != null) {
1180                                version = String.valueOf(cache.getVersion());
1181                        }
1182                }
1183                return version;
1184        }
1185
1186
1187        /**
1188         * Gets the given property by introspection
1189         *
1190         * @param source            The source object
1191         * @param paramPropertyPath The property path
1192         * @return The object at the given path
1193         * @throws GeneralException if a failure occurs
1194         */
1195        public static Object getProperty(Object source, String paramPropertyPath) throws GeneralException {
1196                return getProperty(source, paramPropertyPath, false);
1197        }
1198
1199        /**
1200         * Gets the given property by introspection and 'dot-walking' the given path. Paths have the
1201         * following semantics:
1202         *
1203         *  - Certain common 'quick paths' will be recognized and returned immediately, rather than
1204         *    using reflection to construct the output.
1205         *
1206         *  - A dot '.' is used to separate path elements. Quotes are supported.
1207         *
1208         *  - Paths are evaluated against the current context.
1209         *
1210         *  - Elements within Collections and Maps can be addressed using three different syntaxes,
1211         *    depending on your needs: list[1], list.1, or list._1. Similarly, map[key], map.key, or
1212         *    map._key. Three options are available to account for Sailpoint's various parsers.
1213         *
1214         *  - If an element is a Collection, the context object (and thus output) becomes a Collection. All
1215         *    further properties (other than indexes) are evalulated against each item in the collection.
1216         *    For example, on an Identity, the property 'links.application.name' resolves to a List of Strings.
1217         *
1218         *    This does not cascade. For example, links.someMultiValuedAttribute will result in a List of Lists,
1219         *    one for each item in 'links', rather than a single List containing all values of the attribute.
1220         *    TODO improve nested expansion, if it makes sense.
1221         *
1222         * If you pass true for 'gracefulNulls', a null value or an invalid index at any point in the path
1223         * will simply result in a null output. If it is not set, a NullPointerException or IndexOutOfBoundsException
1224         * will be thrown as appropriate.
1225         *
1226         * @param source            The context object against which to evaluate the path
1227         * @param paramPropertyPath The property path to evaluate
1228         * @param gracefulNulls     If true, encountering a null or bad index mid-path will result in an overall null return value, not an exception
1229         * @return The object at the given path
1230         * @throws GeneralException if a failure occurs
1231         */
1232        public static Object getProperty(Object source, String paramPropertyPath, boolean gracefulNulls) throws GeneralException {
1233                String propertyPath = paramPropertyPath.replaceAll("\\[(\\w+)]", ".$1");
1234
1235                Object tryQuick = getQuickProperty(source, propertyPath);
1236                // This returns Utilities.NONE if this isn't an available "quick property", because
1237                // a property can legitimately have the value null.
1238                if (!Util.nullSafeEq(tryQuick, NONE)) {
1239                        return tryQuick;
1240                }
1241                RFC4180LineParser parser = new RFC4180LineParser('.');
1242                List<String> tokens = parser.parseLine(propertyPath);
1243                Object current = source;
1244                StringBuilder filterString = new StringBuilder();
1245                for(String property : Util.safeIterable(tokens)) {
1246                        try {
1247                                if (current == null) {
1248                                        if (gracefulNulls) {
1249                                                return null;
1250                                        } else {
1251                                                throw new NullPointerException("Found a nested null object at " + filterString);
1252                                        }
1253                                }
1254                                boolean found = false;
1255                                // If this looks likely to be an index...
1256                                if (current instanceof List) {
1257                                        if (property.startsWith("_") && property.length() > 1) {
1258                                                property = property.substring(1);
1259                                        }
1260                                        if (Character.isDigit(property.charAt(0))) {
1261                                                int index = Integer.parseInt(property);
1262                                                if (gracefulNulls) {
1263                                                        current = Utilities.safeSubscript((List<?>) current, index);
1264                                                } else {
1265                                                        current = ((List<?>) current).get(index);
1266                                                }
1267                                                found = true;
1268                                        } else {
1269                                                List<Object> result = new ArrayList<>();
1270                                                for (Object input : (List<?>) current) {
1271                                                        result.add(getProperty(input, property, gracefulNulls));
1272                                                }
1273                                                current = result;
1274                                                found = true;
1275                                        }
1276                                } else if (current instanceof Map) {
1277                                        if (property.startsWith("_") && property.length() > 1) {
1278                                                property = property.substring(1);
1279                                        }
1280                                        current = Util.get((Map<?, ?>) current, property);
1281                                        found = true;
1282                                }
1283                                if (!found) {
1284                                        // This returns Utilities.NONE if this isn't an available "quick property", because
1285                                        // a property can legitimately have the value null.
1286                                        Object result = getQuickProperty(current, property);
1287                                        if (Util.nullSafeEq(result, NONE)) {
1288                                                Method getter = Reflection.getGetter(current.getClass(), property);
1289                                                if (getter != null) {
1290                                                        current = getter.invoke(current);
1291                                                } else {
1292                                                        if (current instanceof Identity) {
1293                                                                current = ((Identity) current).getAttribute(property);
1294                                                        } else {
1295                                                                Attributes<String, Object> attrs = Utilities.getAttributes(current);
1296                                                                if (attrs != null) {
1297                                                                        current = PropertyUtils.getProperty(attrs, property);
1298                                                                } else {
1299                                                                        current = PropertyUtils.getProperty(current, property);
1300                                                                }
1301                                                        }
1302                                                }
1303                                        } else {
1304                                                current = result;
1305                                        }
1306                                }
1307                        } catch (Exception e) {
1308                                throw new GeneralException("Error resolving path '" + filterString + "." + property + "'", e);
1309                        }
1310                        filterString.append('.').append(property);
1311                }
1312                return current;
1313        }
1314
1315        /**
1316         * Returns a "quick property" which does not involve introspection. If the
1317         * property is not in this list, the result will be {@link Utilities#NONE}.
1318         *
1319         * @param source       The source object
1320         * @param propertyPath The property path to check
1321         * @return the object, if this is a known "quick property" or Utilities.NONE if not
1322         * @throws GeneralException if a failure occurs
1323         */
1324        public static Object getQuickProperty(Object source, String propertyPath) throws GeneralException {
1325                // Giant ladders of if statements for common attributes will be much faster.
1326                if (source == null || propertyPath == null) {
1327                        return null;
1328                }
1329                if (source instanceof SailPointObject) {
1330                        if (propertyPath.equals("name")) {
1331                                return ((SailPointObject) source).getName();
1332                        } else if (propertyPath.equals("id")) {
1333                                return ((SailPointObject) source).getId();
1334                        } else if (propertyPath.equals("xml")) {
1335                                return ((SailPointObject) source).toXml();
1336                        } else if (propertyPath.equals("owner.id")) {
1337                                Identity other = ((SailPointObject) source).getOwner();
1338                                if (other != null) {
1339                                        return other.getId();
1340                                } else {
1341                                        return null;
1342                                }
1343                        } else if (propertyPath.equals("owner.name")) {
1344                                Identity other = ((SailPointObject) source).getOwner();
1345                                if (other != null) {
1346                                        return other.getName();
1347                                } else {
1348                                        return null;
1349                                }
1350                        } else if (propertyPath.equals("owner")) {
1351                                return ((SailPointObject) source).getOwner();
1352                        } else if (propertyPath.equals("created")) {
1353                                return ((SailPointObject) source).getCreated();
1354                        } else if (propertyPath.equals("modified")) {
1355                                return ((SailPointObject) source).getModified();
1356                        }
1357                }
1358                if (source instanceof Describable) {
1359                        if (propertyPath.equals("description")) {
1360                                return ((Describable) source).getDescription(Locale.getDefault());
1361                        }
1362                }
1363                if (source instanceof ManagedAttribute) {
1364                        if (propertyPath.equals("value")) {
1365                                return ((ManagedAttribute) source).getValue();
1366                        } else if (propertyPath.equals("attribute")) {
1367                                return ((ManagedAttribute) source).getAttribute();
1368                        } else if (propertyPath.equals("application")) {
1369                                return ((ManagedAttribute) source).getApplication();
1370                        } else if (propertyPath.equals("applicationId") || propertyPath.equals("application.id")) {
1371                                return ((ManagedAttribute) source).getApplicationId();
1372                        } else if (propertyPath.equals("application.name")) {
1373                                return ((ManagedAttribute) source).getApplication().getName();
1374                        } else if (propertyPath.equals("displayName")) {
1375                                return ((ManagedAttribute) source).getDisplayName();
1376                        } else if (propertyPath.equals("displayableName")) {
1377                                return ((ManagedAttribute) source).getDisplayableName();
1378                        }
1379                } else if (source instanceof Link) {
1380                        if (propertyPath.equals("nativeIdentity")) {
1381                                return ((Link) source).getNativeIdentity();
1382                        } else if (propertyPath.equals("displayName") || propertyPath.equals("displayableName")) {
1383                                return ((Link) source).getDisplayableName();
1384                        } else if (propertyPath.equals("description")) {
1385                                return ((Link) source).getDescription();
1386                        } else if (propertyPath.equals("applicationName") || propertyPath.equals("application.name")) {
1387                                return ((Link) source).getApplicationName();
1388                        } else if (propertyPath.equals("applicationId") || propertyPath.equals("application.id")) {
1389                                return ((Link) source).getApplicationId();
1390                        } else if (propertyPath.equals("application")) {
1391                                return ((Link) source).getApplication();
1392                        } else if (propertyPath.equals("identity")) {
1393                                return ((Link) source).getIdentity();
1394                        } else if (propertyPath.equals("permissions")) {
1395                                return ((Link) source).getPermissions();
1396                        }
1397                } else if (source instanceof Identity) {
1398                        if (propertyPath.equals("manager")) {
1399                                return ((Identity) source).getManager();
1400                        } else if (propertyPath.equals("manager.name")) {
1401                                Identity manager = ((Identity) source).getManager();
1402                                if (manager != null) {
1403                                        return manager.getName();
1404                                } else {
1405                                        return null;
1406                                }
1407                        } else if (propertyPath.equals("manager.id")) {
1408                                Identity manager = ((Identity) source).getManager();
1409                                if (manager != null) {
1410                                        return manager.getId();
1411                                } else {
1412                                        return null;
1413                                }
1414                        } else if (propertyPath.equals("lastRefresh")) {
1415                                return ((Identity) source).getLastRefresh();
1416                        } else if (propertyPath.equals("needsRefresh")) {
1417                                return ((Identity) source).isNeedsRefresh();
1418                        } else if (propertyPath.equals("lastname")) {
1419                                return ((Identity) source).getLastname();
1420                        } else if (propertyPath.equals("firstname")) {
1421                                return ((Identity) source).getFirstname();
1422                        } else if (propertyPath.equals("type")) {
1423                                return ((Identity) source).getType();
1424                        } else if (propertyPath.equals("displayName") || propertyPath.equals("displayableName")) {
1425                                return ((Identity) source).getDisplayableName();
1426                        } else if (propertyPath.equals("roleAssignments")) {
1427                                return nullToEmpty(((Identity) source).getRoleAssignments());
1428                        } else if (propertyPath.equals("roleDetections")) {
1429                                return nullToEmpty(((Identity) source).getRoleDetections());
1430                        } else if (propertyPath.equals("assignedRoles")) {
1431                                return nullToEmpty(((Identity) source).getAssignedRoles());
1432                        } else if (propertyPath.equals("assignedRoles.name")) {
1433                                return safeStream(((Identity) source).getAssignedRoles()).map(Bundle::getName).collect(Collectors.toList());
1434                        } else if (propertyPath.equals("detectedRoles")) {
1435                                return nullToEmpty(((Identity) source).getDetectedRoles());
1436                        } else if (propertyPath.equals("detectedRoles.name")) {
1437                                return safeStream(((Identity) source).getDetectedRoles()).map(Bundle::getName).collect(Collectors.toList());
1438                        } else if (propertyPath.equals("links.application.name")) {
1439                                return safeStream(((Identity) source).getLinks()).map(Link::getApplicationName).collect(Collectors.toList());
1440                        } else if (propertyPath.equals("links.application.id")) {
1441                                return safeStream(((Identity) source).getLinks()).map(Link::getApplicationId).collect(Collectors.toList());
1442                        } else if (propertyPath.equals("links.application")) {
1443                                return safeStream(((Identity) source).getLinks()).map(Link::getApplication).collect(Collectors.toList());
1444                        } else if (propertyPath.equals("links")) {
1445                                return nullToEmpty(((Identity) source).getLinks());
1446                        } else if (propertyPath.equals("administrator")) {
1447                                return ((Identity) source).getAdministrator();
1448                        } else if (propertyPath.equals("administrator.id")) {
1449                                Identity other = ((Identity) source).getAdministrator();
1450                                if (other != null) {
1451                                        return other.getId();
1452                                } else {
1453                                        return null;
1454                                }
1455                        } else if (propertyPath.equals("administrator.name")) {
1456                                Identity other = ((Identity) source).getAdministrator();
1457                                if (other != null) {
1458                                        return other.getName();
1459                                } else {
1460                                        return null;
1461                                }
1462                        } else if (propertyPath.equals("capabilities")) {
1463                                return nullToEmpty(((Identity) source).getCapabilityManager().getEffectiveCapabilities());
1464                        } else if (propertyPath.equals("email")) {
1465                                return ((Identity) source).getEmail();
1466                        }
1467                } else if (source instanceof Bundle) {
1468                        if (propertyPath.equals("type")) {
1469                                return ((Bundle) source).getType();
1470                        }
1471                }
1472                return NONE;
1473        }
1474
1475        /**
1476         * Gets the shared background pool, an instance of {@link ForkJoinPool}. This is stored
1477         * in the core CustomGlobal class so that it can be shared across all IIQ classloader
1478         * contexts and will not leak when a new plugin is deployed.
1479         * <p>
1480         * The parallelism count can be changed in SystemConfiguration under the key 'commonThreadPoolParallelism'.
1481         *
1482         * @return An instance of the shared background pool
1483         */
1484        public static ExecutorService getSharedBackgroundPool() {
1485                ExecutorService backgroundPool = (ExecutorService) CustomGlobal.get(IDW_WORKER_POOL);
1486                if (backgroundPool == null) {
1487                        synchronized (CustomGlobal.class) {
1488                                Configuration systemConfig = Configuration.getSystemConfig();
1489                                int parallelism = 8;
1490
1491                                if (systemConfig != null) {
1492                                        Integer configValue = systemConfig.getInteger("commonThreadPoolParallelism");
1493                                        if (configValue != null && configValue > 1) {
1494                                                parallelism = configValue;
1495                                        }
1496                                }
1497
1498                                backgroundPool = (ExecutorService) CustomGlobal.get(IDW_WORKER_POOL);
1499                                if (backgroundPool == null) {
1500                                        backgroundPool = new ForkJoinPool(parallelism);
1501                                        CustomGlobal.put(IDW_WORKER_POOL, backgroundPool);
1502                                }
1503                        }
1504                }
1505                return backgroundPool;
1506        }
1507
1508        /**
1509         * Returns true if parentClass is assignable from testClass, e.g. if the following code
1510         * would not fail to compile:
1511         *
1512         * TestClass ot = new TestClass();
1513         * ParentClass tt = ot;
1514         *
1515         * This is also equivalent to 'b instanceof A' or 'B extends A'.
1516         *
1517         * Primitive types and their boxed equivalents have special handling.
1518         *
1519         * @param parentClass The first (parent-ish) class
1520         * @param testClass  The second (child-ish) class
1521         * @param <A> The parent type
1522         * @param <B> The potential child type
1523         * @return True if parentClass is assignable from testClass
1524         */
1525        public static <A, B> boolean isAssignableFrom(Class<A> parentClass, Class<B> testClass) {
1526                Class<?> targetType = Objects.requireNonNull(parentClass);
1527                Class<?> otherType = Objects.requireNonNull(testClass);
1528                if (targetType.isPrimitive() != otherType.isPrimitive()) {
1529                        if (targetType.isPrimitive()) {
1530                                targetType = box(targetType);
1531                        } else {
1532                                otherType = box(otherType);
1533                        }
1534                } else if (targetType.isPrimitive()) {
1535                        // We know the 'primitive' flags are the same, so they must both be primitive.
1536                        if (targetType.equals(Long.TYPE)) {
1537                                return otherType.equals(Long.TYPE) || otherType.equals(Integer.TYPE) || otherType.equals(Short.TYPE) || otherType.equals(Character.TYPE) || otherType.equals(Byte.TYPE);
1538                        } else if (targetType.equals(Integer.TYPE)) {
1539                                return otherType.equals(Integer.TYPE) || otherType.equals(Short.TYPE) || otherType.equals(Character.TYPE) || otherType.equals(Byte.TYPE);
1540                        } else if (targetType.equals(Short.TYPE)) {
1541                                return otherType.equals(Short.TYPE) || otherType.equals(Character.TYPE) || otherType.equals(Byte.TYPE);
1542                        } else if (targetType.equals(Character.TYPE)) {
1543                                return otherType.equals(Character.TYPE) || otherType.equals(Byte.TYPE);
1544                        } else if (targetType.equals(Byte.TYPE)) {
1545                                return otherType.equals(Byte.TYPE);
1546                        } else if (targetType.equals(Boolean.TYPE)) {
1547                                return otherType.equals(Boolean.TYPE);
1548                        } else if (targetType.equals(Double.TYPE)) {
1549                                return otherType.equals(Double.TYPE) || otherType.equals(Float.TYPE) || otherType.equals(Long.TYPE) || otherType.equals(Integer.TYPE) || otherType.equals(Short.TYPE) || otherType.equals(Character.TYPE) || otherType.equals(Byte.TYPE);
1550                        } else if (targetType.equals(Float.TYPE)) {
1551                                return otherType.equals(Float.TYPE) || otherType.equals(Long.TYPE) || otherType.equals(Integer.TYPE) || otherType.equals(Short.TYPE) || otherType.equals(Character.TYPE) || otherType.equals(Byte.TYPE);
1552                        } else {
1553                                throw new IllegalArgumentException("Unrecognized primitive target class: " + targetType.getName());
1554                        }
1555                }
1556
1557                return targetType.isAssignableFrom(otherType);
1558        }
1559
1560        /**
1561         * Returns the inverse of {@link #isFlagSet(Object)}.
1562         *
1563         * @param flagValue The flag value to check
1564         * @return True if the flag is NOT a 'true' value
1565         */
1566        public static boolean isFlagNotSet(Object flagValue) {
1567                return !isFlagSet(flagValue);
1568        }
1569
1570        /**
1571         * Forwards to {@link Utilities#isFlagSet(Object)}
1572         *
1573         * @param stringFlag The string to check against the set of 'true' values
1574         * @return True if the string input flag is set
1575         */
1576        public static boolean isFlagSet(String stringFlag) {
1577                if (stringFlag == null) {
1578                        return false;
1579                }
1580                return isFlagSet((Object)stringFlag);
1581        }
1582
1583        /**
1584         * Check if a String, Boolean, or Number flag object should be considered equivalent to boolean true.
1585         *
1586         * For strings, true values are: (true, yes, 1, Y), all case-insensitive. All other strings are false.
1587         *
1588         * Boolean 'true' will also be interpreted as a true value.
1589         *
1590         * Numeric '1' will also be interpreted as a true value.
1591         *
1592         * All other values, including null, will always be false.
1593         *
1594         * @param flagValue String representation of a flag
1595         * @return if flag is true
1596         */
1597        public static boolean isFlagSet(Object flagValue) {
1598                if (flagValue instanceof String) {
1599                        String stringFlag = (String)flagValue;
1600                        if (stringFlag.equalsIgnoreCase("true")) {
1601                                return true;
1602                        } else if (stringFlag.equalsIgnoreCase("yes")) {
1603                                return true;
1604                        } else if (stringFlag.equals("1")) {
1605                                return true;
1606                        } else if (stringFlag.equalsIgnoreCase("Y")) {
1607                                return true;
1608                        }
1609                } else if (flagValue instanceof Boolean) {
1610                        return ((Boolean) flagValue);
1611                } else if (flagValue instanceof Number) {
1612                        int numValue = ((Number) flagValue).intValue();
1613                        return (numValue == 1);
1614                }
1615                return false;
1616        }
1617
1618        /**
1619         * Returns the inverse of {@link #isAssignableFrom(Class, Class)}. In
1620         * other words, returns true if the following code would fail to compile:
1621         *
1622         * TestClass ot = new TestClass();
1623         * ParentClass tt = ot;
1624         *
1625         * @param parentClass The first (parent-ish) class
1626         * @param testClass  The second (child-ish) class
1627         * @param <A> The parent type
1628         * @param <B> The potential child type
1629         * @return True if parentClass is NOT assignable from testClass
1630         */
1631        public static <A, B> boolean isNotAssignableFrom(Class<A> parentClass, Class<B> testClass) {
1632                return !isAssignableFrom(parentClass, testClass);
1633        }
1634
1635        /**
1636         * Returns true if the input is NOT an empty Map.
1637         *
1638         * @param map The map to check
1639         * @return True if the input is NOT an empty map
1640         */
1641        public static boolean isNotEmpty(Map<?, ?> map) {
1642                return !Util.isEmpty(map);
1643        }
1644
1645        /**
1646         * Returns true if the input is NOT an empty Collection.
1647         *
1648         * @param list The list to check
1649         * @return True if the input is NOT an empty collection
1650         */
1651        public static boolean isNotEmpty(Collection<?> list) {
1652                return !Util.isEmpty(list);
1653        }
1654
1655        /**
1656         * Returns true if the input string is not null and contains any non-whitespace
1657         * characters.
1658         *
1659         * @param input The input string to check
1660         * @return True if the input is not null, empty, or only whitespace
1661         */
1662        public static boolean isNotNullEmptyOrWhitespace(String input) {
1663                return !isNullEmptyOrWhitespace(input);
1664        }
1665
1666        /**
1667         * Returns true if the input is NOT only digits
1668         *
1669         * @param input The input to check
1670         * @return True if the input contains any non-digit characters
1671         */
1672        public static boolean isNotNumber(String input) {
1673                return !isNumber(input);
1674        }
1675
1676        /**
1677         * Returns true if the given object is 'nothing': a null, empty string, empty map, empty
1678         * Collection, empty Optional, or empty Iterable. If the given object is a Supplier,
1679         * returns true if {@link Supplier#get()} returns a 'nothing' result.
1680         *
1681         * Iterables will be flushed using {@link Util#flushIterator(Iterator)}. That means this may
1682         * be a slow process for Iterables.
1683         *
1684         * @param thing The object to test for nothingness
1685         * @return True if the object is nothing
1686         */
1687        public static boolean isNothing(Object thing) {
1688                if (thing == null) {
1689                        return true;
1690                } else if (thing instanceof String) {
1691                        return ((String) thing).trim().isEmpty();
1692                } else if (thing instanceof Object[]) {
1693                        return (((Object[]) thing).length == 0);
1694                } else if (thing instanceof Collection) {
1695                        return ((Collection<?>) thing).isEmpty();
1696                } else if (thing instanceof Map) {
1697                        return ((Map<?,?>) thing).isEmpty();
1698                } else if (thing instanceof Iterable) {
1699                        Iterator<?> i = ((Iterable<?>) thing).iterator();
1700                        boolean empty = !i.hasNext();
1701                        Util.flushIterator(i);
1702                        return empty;
1703                } else if (thing instanceof Optional) {
1704                        return !((Optional<?>) thing).isPresent();
1705                } else if (thing instanceof Supplier) {
1706                        Object output = ((Supplier<?>) thing).get();
1707                        return isNothing(output);
1708                }
1709                return false;
1710        }
1711
1712        /**
1713         * Returns true if the input string is null, empty, or contains only whitespace
1714         * characters according to {@link Character#isWhitespace(char)}.
1715         * @param input The input string
1716         * @return True if the input is null, empty, or only whitespace
1717         */
1718        public static boolean isNullEmptyOrWhitespace(String input) {
1719                if (input == null || input.isEmpty()) {
1720                        return true;
1721                }
1722                for(char c : input.toCharArray()) {
1723                        if (!Character.isWhitespace(c)) {
1724                                return false;
1725                        }
1726                }
1727                return true;
1728        }
1729
1730        /**
1731         * Returns true if this string contains only digits
1732         * @param input The input string
1733         * @return True if this string contains only digits
1734         */
1735        public static boolean isNumber(String input) {
1736                if (input == null || input.isEmpty()) {
1737                        return false;
1738                }
1739
1740                int nonDigits = 0;
1741
1742                for(int i = 0; i < input.length(); ++i) {
1743                        char ch = input.charAt(i);
1744                        if (!Character.isDigit(ch)) {
1745                                nonDigits++;
1746                                break;
1747                        }
1748                }
1749
1750                return (nonDigits == 0);
1751        }
1752
1753        /**
1754         * Returns true if {@link #isNothing(Object)} would return false.
1755         *
1756         * @param thing The thing to check
1757         * @return True if the object is NOT a 'nothing' value
1758         */
1759        public static boolean isSomething(Object thing) {
1760                return !isNothing(thing);
1761        }
1762
1763        /**
1764         * Returns the current time in the standard ISO offset (date T time+1:00) format
1765         * @return The formatted current time
1766         */
1767        public static String isoOffsetTimestamp() {
1768                LocalDateTime now = LocalDateTime.now();
1769                DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
1770                return now.format(formatter);
1771        }
1772
1773        /**
1774         * Creates a MessageAccumulator that just inserts the message into the target list.
1775         *
1776         * @param target The target list, to which messages will be added. Must be thread-safe for add.
1777         * @param dated If true, messages will be prepended with the output of {@link #timestamp()}
1778         * @return The resulting MessageAccumulator implementation
1779         */
1780        public static MessageAccumulator listMessageAccumulator(List<String> target, boolean dated) {
1781                return (msg) -> {
1782                        String finalMessage = msg.getLocalizedMessage();
1783                        if (dated) {
1784                                finalMessage = timestamp() + " " + finalMessage;
1785                        }
1786                        target.add(finalMessage);
1787                };
1788        }
1789
1790        /**
1791         * Returns a new *modifiable* list with the objects specified added to it.
1792         * A new list will be returned on each invocation. This method will never
1793         * return null.
1794         *
1795         * In Java 11+, the List.of() method constructs a list of arbitrary type,
1796         * which is not modifiable by default.
1797         *
1798         * @param objects The objects to add to the list
1799         * @param <T> The type of the list
1800         * @return A list containing each of the items
1801         */
1802        @SafeVarargs
1803        public static <T> List<T> listOf(T... objects) {
1804                List<T> list = new ArrayList<>();
1805                if (objects != null) {
1806                        Collections.addAll(list, objects);
1807                }
1808                return list;
1809        }
1810
1811        /**
1812         * Returns true if every key and value in the Map can be cast to the types given.
1813         * Passing a null for either Class parameter will ignore that check.
1814         *
1815         * A true result should guarantee no ClassCastExceptions will result from casting
1816         * the Map keys to any of the given values.
1817         *
1818         * If both expected types are null or equal to {@link Object}, this method will trivially
1819         * return true. If the map is null or empty, this method will trivially return true.
1820         *
1821         * NOTE that this method will iterate over all key-value pairs in the Map, so may
1822         * be quite expensive if the Map is large.
1823         *
1824         * @param inputMap The input map to check
1825         * @param expectedKeyType The type implemented or extended by all Map keys
1826         * @param expectedValueType The type implemented or exended by all Map values
1827         * @param <S> The map key type
1828         * @param <T> The map value type
1829         * @return True if all map keys and values will NOT throw an exception on being cast to either given type, false otherwise
1830         */
1831        public static <S, T> boolean mapConformsToType(Map<?, ?> inputMap, Class<S> expectedKeyType, Class<T> expectedValueType) {
1832                if (inputMap == null || inputMap.isEmpty()) {
1833                        // Trivially true, since nothing can throw a ClassCastException
1834                        return true;
1835                }
1836                if ((expectedKeyType == null || Object.class.equals(expectedKeyType)) && (expectedValueType == null || Object.class.equals(expectedValueType))) {
1837                        return true;
1838                }
1839                for(Map.Entry<?, ?> entry : inputMap.entrySet()) {
1840                        Object key = entry.getKey();
1841                        if (key != null && expectedKeyType != null && !Object.class.equals(expectedKeyType) && !expectedKeyType.isAssignableFrom(key.getClass())) {
1842                                return false;
1843                        }
1844                        Object value = entry.getValue();
1845                        if (value != null && expectedValueType != null && !Object.class.equals(expectedValueType) && !expectedValueType.isAssignableFrom(value.getClass())) {
1846                                return false;
1847                        }
1848                }
1849                return true;
1850        }
1851
1852        /**
1853         * Creates a map from a varargs list of items, where every other item is a key or value.
1854         *
1855         * If the arguments list does not have enough items for the final value, null will be used.
1856         *
1857         * @param input The input items, alternating key and value
1858         * @param <S> The key type
1859         * @param <T> The value type
1860         * @return on failures
1861         */
1862        @SuppressWarnings("unchecked")
1863        public static <S, T> Map<S, T> mapOf(Object... input) {
1864                Map<S, T> map = new HashMap<>();
1865
1866                if (input != null && input.length > 0) {
1867                        for(int i = 0; i < input.length; i += 2) {
1868                                S key = (S) input[i];
1869                                T value = null;
1870                                if (input.length > (i + 1)) {
1871                                        value = (T) input[i + 1];
1872                                }
1873                                map.put(key, value);
1874                        }
1875                }
1876
1877                return map;
1878        }
1879
1880        /**
1881         * Transforms a MatchExpression selector into a CompoundFilter
1882         * @param input The input expression
1883         * @return The newly created CompoundFilter corresponding to the MatchExpression
1884         */
1885        public static CompoundFilter matchExpressionToCompoundFilter(IdentitySelector.MatchExpression input) {
1886                CompoundFilter filter = new CompoundFilter();
1887                List<Application> applications = new ArrayList<>();
1888
1889                Application expressionApplication = input.getApplication();
1890                if (expressionApplication != null) {
1891                        applications.add(expressionApplication);
1892                }
1893
1894                List<Filter> filters = new ArrayList<>();
1895
1896                for(IdentitySelector.MatchTerm term : Util.safeIterable(input.getTerms())) {
1897                        filters.add(matchTermToFilter(term, expressionApplication, applications));
1898                }
1899
1900                Filter mainFilter;
1901
1902                if (filters.size() == 1) {
1903                        mainFilter = filters.get(0);
1904                } else if (input.isAnd()) {
1905                        mainFilter = Filter.and(filters);
1906                } else {
1907                        mainFilter = Filter.or(filters);
1908                }
1909
1910                filter.setFilter(mainFilter);
1911                filter.setApplications(applications);
1912                return filter;
1913        }
1914
1915        /**
1916         * Create a MatchTerm from scratch
1917         * @param property The property to test
1918         * @param value The value to test
1919         * @param application The application to associate the term with (or null)
1920         * @return The newly created MatchTerm
1921         */
1922        public static IdentitySelector.MatchTerm matchTerm(String property, Object value, Application application) {
1923                IdentitySelector.MatchTerm term = new IdentitySelector.MatchTerm();
1924                term.setApplication(application);
1925                term.setName(property);
1926                term.setValue(Util.otoa(value));
1927                term.setType(IdentitySelector.MatchTerm.Type.Entitlement);
1928                return term;
1929        }
1930
1931        /**
1932         * Transforms a MatchTerm into a Filter, which can be useful for then modifying it
1933         * @param term The matchterm to transform
1934         * @param defaultApplication The default application if none is specified (may be null)
1935         * @param applications The list of applications
1936         * @return The new Filter object
1937         */
1938        public static Filter matchTermToFilter(IdentitySelector.MatchTerm term, Application defaultApplication, List<Application> applications) {
1939                int applId = -1;
1940                Application appl = null;
1941                if (term.getApplication() != null) {
1942                        appl = term.getApplication();
1943                        if (!applications.contains(appl)) {
1944                                applications.add(appl);
1945                        }
1946                        applId = applications.indexOf(appl);
1947                }
1948                if (appl == null && defaultApplication != null) {
1949                        appl = defaultApplication;
1950                        applId = applications.indexOf(defaultApplication);
1951                }
1952                if (term.isContainer()) {
1953                        List<Filter> filters = new ArrayList<>();
1954                        for(IdentitySelector.MatchTerm child : Util.safeIterable(term.getChildren())) {
1955                                filters.add(matchTermToFilter(child, appl, applications));
1956                        }
1957                        if (term.isAnd()) {
1958                                return Filter.and(filters);
1959                        } else {
1960                                return Filter.or(filters);
1961                        }
1962                } else {
1963                        String filterProperty = term.getName();
1964                        if (applId >= 0) {
1965                                filterProperty = applId + ":" + filterProperty;
1966                        }
1967                        if (Util.isNullOrEmpty(term.getValue())) {
1968                                return Filter.isnull(filterProperty);
1969                        }
1970                        return Filter.eq(filterProperty, term.getValue());
1971                }
1972        }
1973
1974        /**
1975         * Returns the maximum of the given dates. If no values are passed, returns the
1976         * earliest possible date.
1977         *
1978         * @param dates The dates to find the max of
1979         * @return The maximum date in the array
1980         */
1981        public static Date max(Date... dates) {
1982                Date maxDate = new Date(Long.MIN_VALUE);
1983                for(Date d : Objects.requireNonNull(dates)) {
1984                        if (d.after(maxDate)) {
1985                                maxDate = d;
1986                        }
1987                }
1988                return new Date(maxDate.getTime());
1989        }
1990
1991        /**
1992         * Returns the maximum of the given dates. If no values are passed, returns the
1993         * latest possible date. The returned value is always a new object, not one
1994         * of the actual objects in the input.
1995         *
1996         * @param dates The dates to find the max of
1997         * @return The maximum date in the array
1998         */
1999        public static Date min(Date... dates) {
2000                Date minDate = new Date(Long.MAX_VALUE);
2001                for(Date d : Objects.requireNonNull(dates)) {
2002                        if (d.before(minDate)) {
2003                                minDate = d;
2004                        }
2005                }
2006                return new Date(minDate.getTime());
2007        }
2008
2009        /**
2010         * Returns true if {@link Util#nullSafeEq(Object, Object)} would return
2011         * false and vice versa. Two null values will be considered equal.
2012         *
2013         * @param a The first value
2014         * @param b The second value
2015         * @return True if the values are NOT equal
2016         */
2017        public static boolean nullSafeNotEq(Object a, Object b) {
2018                return !Util.nullSafeEq(a, b, true);
2019        }
2020
2021        /**
2022         * Returns the input string if it is not null. Otherwise, returns an empty string.
2023         *
2024         * @param maybeNull The input string, which is possibly null
2025         * @return The input string or an empty string
2026         */
2027        public static String nullToEmpty(String maybeNull) {
2028                if (maybeNull == null) {
2029                        return "";
2030                }
2031                return maybeNull;
2032        }
2033
2034        /**
2035         * Converts the given collection to an empty Attributes, if a null object is passed,
2036         * the input object (if an Attributes is passed), or a new Attributes containing all
2037         * elements from the input Map (if any other type of Map is passed).
2038         *
2039         * @param input The input map or attributes
2040         * @return The result as described above
2041         */
2042        @SuppressWarnings("unchecked")
2043        public static Attributes<String, Object> nullToEmpty(Map<String, ? extends Object> input) {
2044                if (input == null) {
2045                        return new Attributes<>();
2046                } else if (input instanceof Attributes) {
2047                        return (Attributes<String, Object>) input;
2048                } else {
2049                        return new Attributes<>(input);
2050                }
2051        }
2052
2053        /**
2054         * Converts the given collection to an empty list (if a null value is passed),
2055         * the input object (if a List is passed), or a copy of the input object in a
2056         * new ArrayList (if any other Collection is passed).
2057         *
2058         * @param input The input collection
2059         * @param <T> The type of the list
2060         * @return The result as described above
2061         */
2062        public static <T> List<T> nullToEmpty(Collection<T> input) {
2063                if (input == null) {
2064                        return new ArrayList<>();
2065                } else if (input instanceof List) {
2066                        return (List<T>)input;
2067                } else {
2068                        return new ArrayList<>(input);
2069                }
2070        }
2071
2072        /**
2073         * Returns the input string if it is not null, explicitly noting the input as a String to
2074         * make Beanshell happy at runtime.
2075         *
2076         * Identical to {@link Utilities#nullToEmpty(String)}, except Beanshell can't decide
2077         * which of the method variants to call when the input is null. This leads to scripts
2078         * sometimes calling {@link Utilities#nullToEmpty(Map)} instead. (Java would be able
2079         * to infer the overload to invoke at compile time by using the variable type.)
2080         *
2081         * @param maybeNull The input string, which is possibly null
2082         * @return The input string or an empty string
2083         */
2084        public static String nullToEmptyString(String maybeNull) {
2085                return Utilities.nullToEmpty(maybeNull);
2086        }
2087
2088        /**
2089         * Parses the input string into a LocalDateTime, returning an {@link Optional}
2090         * if the date parses properly. If the date does not parse properly, or if the
2091         * input is null, returns an {@link Optional#empty()}.
2092         *
2093         * @param inputString The input string
2094         * @param format The format used to parse the input string per {@link DateTimeFormatter} rules
2095         * @return The parsed string
2096         */
2097        public static Optional<LocalDateTime> parseDateString(String inputString, String format) {
2098                if (Util.isNullOrEmpty(inputString) || Util.isNullOrEmpty(format)) {
2099                        return Optional.empty();
2100                }
2101
2102                try {
2103                        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
2104                        return Optional.of(LocalDateTime.parse(inputString, formatter));
2105                } catch(DateTimeParseException e) {
2106                        if (logger.isDebugEnabled()) {
2107                                logger.debug("Failed to parse date [" + inputString + "] according to format [" + format + "]");
2108                        }
2109                        return Optional.empty();
2110                }
2111        }
2112
2113        /**
2114         * Constructs a new map with each key having the prefix added to it. This can be
2115         * used to merge two maps without overwriting keys from either one, for example.
2116         *
2117         * @param map The input map, which will not be modified
2118         * @param prefix The prefix to add to each key
2119         * @param <V> The value type of the map
2120         * @return A new {@link HashMap} with each key prefixed by the given prefix
2121         */
2122        public static <V> Map<String, V> prefixMap(Map<String, V> map, String prefix) {
2123                Map<String, V> newMap = new HashMap<>();
2124                for(String key : map.keySet()) {
2125                        newMap.put(prefix + key, map.get(key));
2126                }
2127                return newMap;
2128        }
2129
2130        /**
2131         * Attempts to dynamically evaluate whatever thing is passed in and return the
2132         * output of running the thing. If the thing is not of a known type, this method
2133         * silently returns null.
2134         *
2135         * The following types of things are accept3ed:
2136         *
2137         *  - sailpoint.object.Rule: Evaluates as a SailPoint rule
2138         *  - sailpoint.object.Script: Evaluates as a SailPoint script after being cloned
2139         *  - java.lang.String: Evaluates as a SailPoint script
2140         *  - java.util.function.Function: Accepts the Map of parameters, returns a value
2141         *  - java.util.function.Consumer: Accepts the Map of parameters
2142         *  - sailpoint.object.JavaRuleExecutor: Evaluates as a Java rule
2143         *
2144         * For Function and Consumer things, the context and a log will be added to the
2145         * params.
2146         *
2147         * @param context The sailpoint context
2148         * @param thing The thing to run
2149         * @param params The parameters to pass to the thing, if any
2150         * @param <T> the context-sensitive type of the return
2151         * @return The return value from the thing executed, or null
2152         * @throws GeneralException if any failure occurs
2153         */
2154        @SuppressWarnings("unchecked")
2155        public static <T> T quietRun(SailPointContext context, Object thing, Map<String, Object> params) throws GeneralException {
2156                if (thing instanceof Rule) {
2157                        Rule rule = (Rule)thing;
2158                        return (T)context.runRule(rule, params);
2159                } else if (thing instanceof String || thing instanceof Script) {
2160                        Script safeScript = getAsScript(thing);
2161                        return (T) context.runScript(safeScript, params);
2162                } else if (thing instanceof Reference) {
2163                        SailPointObject ref = ((Reference) thing).resolve(context);
2164                        return quietRun(context, ref, params);
2165                } else if (thing instanceof Callable) {
2166                        try {
2167                                Callable<T> callable = (Callable<T>)thing;
2168                                return callable.call();
2169                        } catch (Exception e) {
2170                                throw new GeneralException(e);
2171                        }
2172                } else if (thing instanceof Function) {
2173                        Function<Map<String, Object>, T> function = (Function<Map<String, Object>, T>) thing;
2174                        params.put("context", context);
2175                        params.put("log", LogFactory.getLog(thing.getClass()));
2176                        return function.apply(params);
2177                } else if (thing instanceof Consumer) {
2178                        Consumer<Map<String, Object>> consumer = (Consumer<Map<String, Object>>) thing;
2179                        params.put("context", context);
2180                        params.put("log", LogFactory.getLog(thing.getClass()));
2181                        consumer.accept(params);
2182                } else if (thing instanceof JavaRuleExecutor) {
2183                        JavaRuleExecutor executor = (JavaRuleExecutor)thing;
2184                        JavaRuleContext javaRuleContext = new JavaRuleContext(context, params);
2185                        try {
2186                                return (T)executor.execute(javaRuleContext);
2187                        } catch(GeneralException e) {
2188                                throw e;
2189                        } catch(Exception e) {
2190                                throw new GeneralException(e);
2191                        }
2192                }
2193                return null;
2194        }
2195
2196        /**
2197         * Safely casts the given input to the target type.
2198         *
2199         * If the object cannot be cast to the target type, this method returns null instead of throwing a ClassCastException.
2200         *
2201         * If the input object is null, this method returns null.
2202         *
2203         * If the targetClass is null, this method throws a {@link NullPointerException}.
2204         *
2205         * @param input The input object to cast
2206         * @param targetClass The target class to which it should be cast
2207         * @param <T> The expected return type
2208         * @return The object cast to the given type, or null if it cannot be cast
2209         */
2210        public static <T> T safeCast(Object input, Class<T> targetClass) {
2211                if (input == null) {
2212                        return null;
2213                }
2214                Objects.requireNonNull(targetClass, "targetClass must not be null");
2215                if (targetClass.isAssignableFrom(input.getClass())) {
2216                        return targetClass.cast(input);
2217                }
2218                return null;
2219        }
2220
2221        /**
2222         * Returns the class name of the object, or the string 'null', suitable for logging safely
2223         * @param any The object to get the type of
2224         * @return The class name, or the string 'null'
2225         */
2226        public static String safeClassName(Object any) {
2227                if (any == null) {
2228                        return "null";
2229                } else {
2230                        return any.getClass().getName();
2231                }
2232        }
2233
2234        /**
2235         * Returns true if the bigger collection contains all elements in the smaller
2236         * collection. If the inputs are null, the comparison will always be false.
2237         * If the inputs are not collections, they will be coerced to collections
2238         * before comparison.
2239         *
2240         * @param maybeBiggerCollection An object that may be the bigger collection
2241         * @param maybeSmallerCollection AN object that may be the smaller collection
2242         * @return True if the bigger collection contains all elements of the smaller collection
2243         */
2244        public static boolean safeContainsAll(Object maybeBiggerCollection, Object maybeSmallerCollection) {
2245                if (maybeBiggerCollection == null || maybeSmallerCollection == null) {
2246                        return false;
2247                }
2248                List<Object> biggerCollection = new ArrayList<>();
2249                if (maybeBiggerCollection instanceof Collection) {
2250                        biggerCollection.addAll((Collection<?>) maybeBiggerCollection);
2251                } else if (maybeBiggerCollection instanceof Object[]) {
2252                        biggerCollection.addAll(Arrays.asList((Object[])maybeBiggerCollection));
2253                } else {
2254                        biggerCollection.add(maybeBiggerCollection);
2255                }
2256
2257                List<Object> smallerCollection = new ArrayList<>();
2258                if (maybeSmallerCollection instanceof Collection) {
2259                        smallerCollection.addAll((Collection<?>) maybeSmallerCollection);
2260                } else if (maybeSmallerCollection instanceof Object[]) {
2261                        smallerCollection.addAll(Arrays.asList((Object[])maybeSmallerCollection));
2262                } else {
2263                        smallerCollection.add(maybeSmallerCollection);
2264                }
2265
2266                return new HashSet<>(biggerCollection).containsAll(smallerCollection);
2267        }
2268
2269        /**
2270         * Returns a long timestamp for the input Date, returning {@link Long#MIN_VALUE} if the
2271         * Date is null.
2272         *
2273         * @param input The input date
2274         * @return The timestamp of the date, or Long.MIN_VALUE if it is null
2275         */
2276        public static long safeDateTimestamp(Date input) {
2277                if (input == null) {
2278                        return Long.MIN_VALUE;
2279                }
2280                return input.getTime();
2281        }
2282
2283        /**
2284         * Invokes an action against each key-value pair in the Map. If the Map is null,
2285         * or if the function is null, no action is taken and no exception is thrown.
2286         *
2287         * @param map The map
2288         * @param function A BiConsumer that will be applied to each key-avlue pair in the Map
2289         * @param <A> The type of the map's keys
2290         * @param <B> The type of the map's values
2291         */
2292        public static <A, B> void safeForeach(Map<A, B> map, BiConsumer<A, B> function) {
2293                if (map == null || function == null) {
2294                        return;
2295                }
2296                for(Map.Entry<A, B> entry : map.entrySet()) {
2297                        function.accept(entry.getKey(), entry.getValue());
2298                }
2299        }
2300
2301        /**
2302         * Returns a stream for the given map's keys if it's not null, or an empty stream if it is
2303         * @param map The map
2304         * @param <T> The type of the map's keys
2305         * @return A stream from the map's keys, or an empty stream
2306         */
2307        public static <T> Stream<T> safeKeyStream(Map<T, ?> map) {
2308                if (map == null) {
2309                        return Stream.empty();
2310                }
2311                return map.keySet().stream();
2312        }
2313
2314        /**
2315         * Safely converts the given input to a List.
2316         *
2317         * If the input is a String, it will be added to a new List and returned.
2318         *
2319         * If the input is a Number, Boolean, or {@link Message}, it will be converted to String, added to a List, and returned.
2320         *
2321         * If the input is already a List, the input object will be returned as-is.
2322         *
2323         * If the input is an array of strings, they will be added to a new list and returned.
2324         *
2325         * If the input is an array of any other type of object, they will be converted to strings, added to a new list, and returned.
2326         *
2327         * If the input is any other kind of Collection, all elements will be added to a new List and returned.
2328         *
2329         * If the input is a {@link Stream}, all elements will be converted to Strings using {@link Utilities#safeString(Object)}, then added to a new List and returned.
2330         *
2331         * All other values result in an empty list.
2332         *
2333         * This method never returns null.
2334         *
2335         * Unlike {@link Util#otol(Object)}, this method does not split strings as CSVs.
2336         *
2337         * It's not my problem if your existing lists have something other than Strings in them.
2338         *
2339         * @param value The value to listify
2340         * @return The resulting List
2341         */
2342        @SuppressWarnings("unchecked")
2343        public static List<String> safeListify(Object value) {
2344                if (value instanceof String) {
2345                        List<String> single = new ArrayList<>();
2346                        single.add((String) value);
2347                        return single;
2348                } else if (value instanceof Number || value instanceof Boolean) {
2349                        List<String> single = new ArrayList<>();
2350                        single.add(String.valueOf(value));
2351                        return single;
2352                } else if (value instanceof Message) {
2353                        List<String> single = new ArrayList<>();
2354                        single.add(((Message) value).getLocalizedMessage());
2355                        return single;
2356                } else if (value instanceof String[]) {
2357                        String[] strings = (String[])value;
2358                        return new ArrayList<>(Arrays.asList(strings));
2359                } else if (value instanceof Object[]) {
2360                        Object[] objs = (Object[])value;
2361                        return Arrays.stream(objs).map(Utilities::safeString).collect(Collectors.toCollection(ArrayList::new));
2362                } else if (value instanceof List) {
2363                        return (List<String>)value;
2364                } else if (value instanceof Collection) {
2365                        return new ArrayList<>((Collection<String>)value);
2366                } else if (value instanceof Stream) {
2367                        return ((Stream<?>)value).map(Utilities::safeString).collect(Collectors.toList());
2368                }
2369                return new ArrayList<>();
2370        }
2371
2372        /**
2373         * Returns a Map cast to the given generic types if and only if it passes the check in
2374         * {@link #mapConformsToType(Map, Class, Class)} for the same type parameters.
2375         *
2376         * If the input is not a Map or if the key/value types do not conform to the expected
2377         * types, this method returns null.
2378         *
2379         * @param input The input map
2380         * @param keyType The key type
2381         * @param valueType The value type
2382         * @param <S> The resulting key type
2383         * @param <T> The resulting value type
2384         * @return The resulting Map
2385         */
2386        @SuppressWarnings("unchecked")
2387        public static <S, T> Map<S, T> safeMapCast(Object input, Class<S> keyType, Class<T> valueType) {
2388                if (!(input instanceof Map)) {
2389                        return null;
2390                }
2391                boolean conforms = mapConformsToType((Map<?, ?>) input, keyType, valueType);
2392                if (conforms) {
2393                        return (Map<S, T>)input;
2394                } else {
2395                        return null;
2396                }
2397        }
2398
2399        /**
2400         * Returns the size of the input array, returning 0 if the array is null
2401         * @param input The array to get the size of
2402         * @param <T> The type of the array (just for compiler friendliness)
2403         * @return The size of the array
2404         */
2405        public static <T> int safeSize(T[] input) {
2406                if (input == null) {
2407                        return 0;
2408                }
2409                return input.length;
2410        }
2411
2412        /**
2413         * Returns the size of the input collection, returning 0 if the collection is null
2414         * @param input The collection to get the size of
2415         * @return The size of the collection
2416         */
2417        public static int safeSize(Collection<?> input) {
2418                if (input == null) {
2419                        return 0;
2420                }
2421                return input.size();
2422        }
2423
2424        /**
2425         * Returns the length of the input string, returning 0 if the string is null
2426         * @param input The string to get the length of
2427         * @return The size of the string
2428         */
2429        public static int safeSize(String input) {
2430                if (input == null) {
2431                        return 0;
2432                }
2433                return input.length();
2434        }
2435
2436        /**
2437         * Returns a stream for the given array if it's not null, or an empty stream if it is
2438         * @param array The array
2439         * @param <T> The type of the array
2440         * @return A stream from the array, or an empty stream
2441         */
2442        public static <T> Stream<T> safeStream(T[] array) {
2443                if (array == null || array.length == 0) {
2444                        return Stream.empty();
2445                }
2446                return Arrays.stream(array);
2447        }
2448
2449        /**
2450         * Returns a stream for the given list if it's not null, or an empty stream if it is
2451         * @param list The list
2452         * @param <T> The type of the list
2453         * @return A stream from the list, or an empty stream
2454         */
2455        public static <T> Stream<T> safeStream(List<T> list) {
2456                if (list == null) {
2457                        return Stream.empty();
2458                }
2459                return list.stream();
2460        }
2461
2462        /**
2463         * Returns a stream for the given set if it's not null, or an empty stream if it is
2464         * @param set The list
2465         * @param <T> The type of the list
2466         * @return A stream from the list, or an empty stream
2467         */
2468        public static <T> Stream<T> safeStream(Set<T> set) {
2469                if (set == null) {
2470                        return Stream.empty();
2471                }
2472                return set.stream();
2473        }
2474
2475        /**
2476         * Returns the given value as a "safe string". If the value is null, it will be returned as an empty string. If the value is already a String, it will be returned as-is. If the value is anything else, it will be passed through {@link String#valueOf(Object)}.
2477         *
2478         * If the input is an array, it will be converted to a temporary list for String.valueOf() output.
2479         *
2480         * The output will never be null.
2481         *
2482         * @param whatever The thing to return
2483         * @return The string
2484         */
2485        public static String safeString(Object whatever) {
2486                if (whatever == null) {
2487                        return "";
2488                }
2489                if (whatever instanceof String) {
2490                        return (String)whatever;
2491                }
2492                // If we've got an array, make it a list for a nicer toString()
2493                if (whatever.getClass().isArray()) {
2494                        Object[] array = (Object[])whatever;
2495                        whatever = Arrays.stream(array).collect(Collectors.toCollection(ArrayList::new));
2496                }
2497                return String.valueOf(whatever);
2498        }
2499
2500        /**
2501         * Performs a safe subscript operation against the given array.
2502         *
2503         * If the array is null, or if the index is out of bounds, this method
2504         * returns null instead of throwing an exception.
2505         *
2506         * @param list The array to get the value from
2507         * @param index The index from which to get the value.
2508         * @param <T> The expected return type
2509         * @return The value at the given index in the array, or null
2510         */
2511        public static <T, S extends T> T safeSubscript(S[] list, int index) {
2512                return safeSubscript(list, index, null);
2513        }
2514
2515        /**
2516         * Performs a safe subscript operation against the given array.
2517         *
2518         * If the array is null, or if the index is out of bounds, this method returns
2519         * the default instead of throwing an exception.
2520         *
2521         * @param list The array to get the value from
2522         * @param index The index from which to get the value.
2523         * @param defaultValue The default value to return if the input is null
2524         * @param <T> The expected return type
2525         * @return The value at the given index in the array, or null
2526         */
2527        public static <T, S extends T> T safeSubscript(S[] list, int index, T defaultValue) {
2528                if (list == null) {
2529                        return defaultValue;
2530                }
2531                if (index >= list.length || index < 0) {
2532                        return defaultValue;
2533                }
2534                return list[index];
2535        }
2536
2537        /**
2538         * Performs a safe subscript operation against the given {@link List}.
2539         *
2540         * If the list is null, or if the index is out of bounds, this method returns null instead of throwing an exception.
2541         *
2542         * Equivalent to safeSubscript(list, index, null).
2543         *
2544         * @param list The List to get the value from
2545         * @param index The index from which to get the value.
2546         * @param <T> The expected return type
2547         * @return The value at the given index in the List, or null
2548         */
2549        public static <T, S extends T> T safeSubscript(List<S> list, int index) {
2550                return safeSubscript(list, index, null);
2551        }
2552
2553        /**
2554         * Performs a safe subscript operation against the given {@link List}.
2555         *
2556         * If the list is null, or if the index is out of bounds, this method returns the given default object instead of throwing an exception.
2557         *
2558         * @param list The List to get the value from
2559         * @param index The index from which to get the value.
2560         * @param defaultObject The default object to return in null or out-of-bounds cases
2561         * @param <S> The actual type of the list, which must be a subclass of T
2562         * @param <T> The expected return type
2563         * @return The value at the given index in the List, or null
2564         */
2565        public static <T, S extends T> T safeSubscript(List<S> list, int index, T defaultObject) {
2566                if (list == null) {
2567                        return defaultObject;
2568                }
2569                if (index >= list.size() || index < 0) {
2570                        return defaultObject;
2571                }
2572                return list.get(index);
2573        }
2574
2575        /**
2576         * Safely substring the given input String, accounting for odd index situations.
2577         * This method should never throw a StringIndexOutOfBounds exception.
2578         *
2579         * Negative values will be interpreted as distance from the end, like Python.
2580         *
2581         * If the start index is higher than the end index, or if the start index is
2582         * higher than the string length, the substring is not defined and an empty
2583         * string will be returned.
2584         *
2585         * If the end index is higher than the length of the string, the whole
2586         * remaining string after the start index will be returned.
2587         *
2588         * @param input The input string to substring
2589         * @param start The start index
2590         * @param end The end index
2591         * @return The substring
2592         */
2593        public static String safeSubstring(String input, int start, int end) {
2594                if (input == null) {
2595                        return null;
2596                }
2597
2598                if (end < 0) {
2599                        end = input.length() + end;
2600                }
2601
2602                if (start < 0) {
2603                        start = input.length() + start;
2604                }
2605
2606                if (end > input.length()) {
2607                        end = input.length();
2608                }
2609
2610                if (start > end) {
2611                        return "";
2612                }
2613
2614                if (start < 0) {
2615                        start = 0;
2616                }
2617                if (end < 0) {
2618                        end = 0;
2619                }
2620
2621                return input.substring(start, end);
2622        }
2623
2624        /**
2625         * Returns a trimmed version of the input string, returning an empty
2626         * string if it is null.
2627         * @param input The input string to trim
2628         * @return A non-null trimmed copy of the input string
2629         */
2630        public static String safeTrim(String input) {
2631                if (input == null) {
2632                        return "";
2633                }
2634                return input.trim();
2635        }
2636
2637        /**
2638         * Sets the given attribute on the given object, if it supports attributes. The
2639         * attributes on some object types may be called other things like arguments.
2640         *
2641         * @param source The source object, which may implement an Attributes container method
2642         * @param attributeName The name of the attribute to set
2643         * @param value The value to set in that attribute
2644         */
2645        public static void setAttribute(Object source, String attributeName, Object value) {
2646                /*
2647                 * In Java 8+, using instanceof is about as fast as using == and way faster than
2648                 * reflection and possibly throwing an exception, so this is the best way to get
2649                 * attributes if we aren't sure of the type of the object.
2650                 *
2651                 * Most classes have their attributes exposed via getAttributes, but some have
2652                 * them exposed via something like getArguments. This method will take care of
2653                 * the difference for you.
2654                 *
2655                 * Did I miss any? Probably. But this is a lot.
2656                 */
2657                Objects.requireNonNull(source, "You cannot set an attribute on a null object");
2658                Objects.requireNonNull(attributeName, "Attribute names must not be null");
2659                Attributes<String, Object> existing = null;
2660                if (!(source instanceof Custom || source instanceof Configuration || source instanceof Identity || source instanceof Link || source instanceof Bundle || source instanceof Application || source instanceof TaskDefinition || source instanceof TaskItem)) {
2661                        existing = getAttributes(source);
2662                        if (existing == null) {
2663                                existing = new Attributes<>();
2664                        }
2665                        if (value == null) {
2666                                existing.remove(attributeName);
2667                        } else {
2668                                existing.put(attributeName, value);
2669                        }
2670                }
2671                if (source instanceof Identity) {
2672                        // This does special stuff
2673                        ((Identity) source).setAttribute(attributeName, value);
2674                } else if (source instanceof Link) {
2675                        ((Link) source).setAttribute(attributeName, value);
2676                } else if (source instanceof Bundle) {
2677                        ((Bundle) source).setAttribute(attributeName, value);
2678                } else if (source instanceof Custom) {
2679                        // Custom objects are gross
2680                        ((Custom) source).put(attributeName, value);
2681                } else if (source instanceof Configuration) {
2682                        ((Configuration) source).put(attributeName, value);
2683                } else if (source instanceof Application) {
2684                        ((Application) source).setAttribute(attributeName, value);
2685                } else if (source instanceof CertificationItem) {
2686                        // This one returns a Map for some reason
2687                        ((CertificationItem) source).setAttributes(existing);
2688                } else if (source instanceof CertificationEntity) {
2689                        ((CertificationEntity) source).setAttributes(existing);
2690                } else if (source instanceof Certification) {
2691                        ((Certification) source).setAttributes(existing);
2692                } else if (source instanceof CertificationDefinition) {
2693                        ((CertificationDefinition) source).setAttributes(existing);
2694                } else if (source instanceof TaskDefinition) {
2695                        ((TaskDefinition) source).setArgument(attributeName, value);
2696                } else if (source instanceof TaskItem) {
2697                        ((TaskItem) source).setAttribute(attributeName, value);
2698                } else if (source instanceof ManagedAttribute) {
2699                        ((ManagedAttribute) source).setAttributes(existing);
2700                } else if (source instanceof Form) {
2701                        ((Form) source).setAttributes(existing);
2702                } else if (source instanceof IdentityRequest) {
2703                        ((IdentityRequest) source).setAttributes(existing);
2704                } else if (source instanceof IdentitySnapshot) {
2705                        ((IdentitySnapshot) source).setAttributes(existing);
2706                } else if (source instanceof ResourceObject) {
2707                        ((ResourceObject) source).setAttributes(existing);
2708                } else if (source instanceof Field) {
2709                        ((Field) source).setAttributes(existing);
2710                } else if (source instanceof ProvisioningPlan) {
2711                        ((ProvisioningPlan) source).setArguments(existing);
2712                } else if (source instanceof IntegrationConfig) {
2713                        ((IntegrationConfig) source).setAttributes(existing);
2714                } else if (source instanceof ProvisioningProject) {
2715                        ((ProvisioningProject) source).setAttributes(existing);
2716                } else if (source instanceof ProvisioningTransaction) {
2717                        ((ProvisioningTransaction) source).setAttributes(existing);
2718                } else if (source instanceof ProvisioningPlan.AbstractRequest) {
2719                        ((ProvisioningPlan.AbstractRequest) source).setArguments(existing);
2720                } else if (source instanceof Rule) {
2721                        ((Rule) source).setAttributes(existing);
2722                } else if (source instanceof WorkItem) {
2723                        ((WorkItem) source).setAttributes(existing);
2724                } else if (source instanceof RpcRequest) {
2725                        ((RpcRequest) source).setArguments(existing);
2726                } else if (source instanceof ApprovalItem) {
2727                        ((ApprovalItem) source).setAttributes(existing);
2728                } else {
2729                        throw new UnsupportedOperationException("This method does not support objects of type " + source.getClass().getName());
2730                }
2731        }
2732
2733        /**
2734         * Returns a new *modifiable* set with the objects specified added to it.
2735         * A new set will be returned on each invocation. This method will never
2736         * return null.
2737         *
2738         * @param objects The objects to add to the set
2739         * @param <T> The type of each item
2740         * @return A set containing each of the items
2741         */
2742        @SafeVarargs
2743        public static <T> Set<T> setOf(T... objects) {
2744                Set<T> set = new HashSet<>();
2745                if (objects != null) {
2746                        Collections.addAll(set, objects);
2747                }
2748                return set;
2749        }
2750
2751        /**
2752         * The same as {@link #sortMapListByKey(List, Object)}, except that the list will be
2753         * copied and returned, rather than sorted in place.
2754         *
2755         * @param listOfMaps The list of maps to sort
2756         * @param key        The key to extract from each map and sort by
2757         * @param <K>        The type of the key
2758         * @return A new list, copied from the input, sorted according to the given key
2759         */
2760        public static <K> List<Map<? super K, ?>> sortCopiedMapListByKey(final List<Map<? super K, ?>> listOfMaps, final K key) {
2761                List<Map<? super K, ?>> newList = new ArrayList<>(listOfMaps);
2762                sortMapListByKey(newList, key);
2763                return newList;
2764        }
2765
2766        /**
2767         * Sorts the given list of Maps *in place* by the value of the given key. If
2768         * the value is a {@link Number}, it will be passed to {@link String#valueOf(Object)}.
2769         * If the value is a {@link Date}, its epoch millisecond timestamp will be passed
2770         * to {@link String#valueOf(Object)}. Otherwise, the input will be passed to
2771         * {@link Util#otoa(Object)}.
2772         *
2773         * Nulls will always be sorted higher than non-nulls, both if the List contains
2774         * a null instead of a Map object, or if the value corresponding to the sort key
2775         * is null.
2776         *
2777         * @param listOfMaps The list of maps to sort
2778         * @param key        The key to extract from each map and sort by
2779     * @param <K>        The type of the key
2780         */
2781        public static <K> void sortMapListByKey(final List<Map<? super K, ?>> listOfMaps, final K key) {
2782                listOfMaps.sort(
2783                                Comparator.nullsLast(
2784                                                Comparator.comparing(
2785                                                                m -> {
2786                                                                        Object val = m.get(key);
2787                                                                        if (val instanceof Number) {
2788                                                                                return String.valueOf(((Number) val).longValue());
2789                                                                        } else if (val instanceof Date) {
2790                                                                                return String.valueOf(((Date) val).getTime());
2791                                                                        } else {
2792                                                                                return Util.otoa(val);
2793                                                                        }
2794                                                                },
2795                                                                Comparator.nullsLast(Comparator.naturalOrder())
2796                                                )
2797                                )
2798                );
2799        }
2800
2801        /**
2802         * Adds the given key and value to the Map if no existing value for the key is
2803         * present. The Map will be synchronized so that only one thread is guaranteed
2804         * to be able to insert the initial value.
2805         *
2806         * If possible, you should use a {@link java.util.concurrent.ConcurrentMap}, which
2807         * already handles this situation with greater finesse.
2808         *
2809         * @param target The target Map to which the value should be inserted if missing
2810         * @param key The key to insert
2811         * @param value A supplier for the value to insert
2812         * @param <S> The key type
2813         * @param <T> The value type
2814         */
2815        public static <S, T> void synchronizedPutIfAbsent(final Map<S, T> target, final S key, final Supplier<T> value) {
2816                Objects.requireNonNull(target, "The Map passed to synchronizedPutIfAbsent must not be null");
2817                if (!target.containsKey(key)) {
2818                        synchronized(target) {
2819                                if (!target.containsKey(key)) {
2820                                        T valueObj = value.get();
2821                                        target.put(key, valueObj);
2822                                }
2823                        }
2824                }
2825        }
2826
2827        /**
2828         * Converts two Date objects to {@link LocalDateTime} at the system default
2829         * time zone and returns the {@link Duration} between them.
2830         *
2831         * If you pass the dates in the wrong order (first parameter is the later
2832         * date), they will be silently swapped before returning the Duration.
2833         *
2834         * @param firstTime The first time to compare
2835         * @param secondTime The second time to compare
2836         * @return The {@link Period} between the two days
2837         */
2838        public static Duration timeDifference(Date firstTime, Date secondTime) {
2839                if (firstTime == null || secondTime == null) {
2840                        throw new IllegalArgumentException("Both arguments to dateDifference must be non-null");
2841                }
2842
2843                LocalDateTime ldt1 = firstTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
2844                LocalDateTime ldt2 = secondTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
2845
2846                // Swap the dates if they're backwards
2847                if (ldt1.isAfter(ldt2)) {
2848                        LocalDateTime tmp = ldt2;
2849                        ldt2 = ldt1;
2850                        ldt1 = tmp;
2851                }
2852
2853                return Duration.between(ldt1, ldt2);
2854        }
2855
2856        /**
2857         * Coerces the millisecond timestamps to Date objects, then invokes the API
2858         * {@link #timeDifference(Date, Date)}.
2859         *
2860         * @param firstTime The first millisecond timestamp
2861         * @param secondTime The second millisecond timestamp
2862         * @return The difference between the two times as a {@link Duration}.
2863         *
2864         * @see #timeDifference(Date, Date)
2865         */
2866        public static Duration timeDifference(long firstTime, long secondTime) {
2867                return timeDifference(new Date(firstTime), new Date(secondTime));
2868        }
2869
2870        /**
2871         * Returns the current time in a standard format
2872         * @return The current time
2873         */
2874        public static String timestamp() {
2875                SimpleDateFormat formatter = new SimpleDateFormat(CommonConstants.STANDARD_TIMESTAMP);
2876                formatter.setTimeZone(TimeZone.getDefault());
2877                return formatter.format(new Date());
2878        }
2879
2880        /**
2881         * Translates the input to XML, if a serializer is registered for it. In
2882         * general, this can be anything in 'sailpoint.object', a Map, a List, a
2883         * String, or other primitives.
2884         *
2885         * @param input The object to serialize
2886         * @return the output XML
2887         * @throws ConfigurationException if the object type cannot be serialized
2888         */
2889        public static String toXml(Object input) throws ConfigurationException {
2890                if (input == null) {
2891                        return null;
2892                }
2893
2894                XMLObjectFactory xmlObjectFactory = XMLObjectFactory.getInstance();
2895                return xmlObjectFactory.toXml(input);
2896        }
2897
2898        /**
2899         * Attempts to capture the user's time zone information from the current JSF
2900         * context / HTTP session, if one is available. The time zone and locale will
2901         * be captured to the user's UIPreferences as 'mostRecentTimezone' and
2902         * 'mostRecentLocale'.
2903         *
2904         * If a session is not available, this method does nothing.
2905         *
2906         * The JSF session is available in a subset of rule contexts, most notably the
2907         * QuickLink textScript context, which runs on each load of the user's home.jsf page.
2908         *
2909         * If you are trying to capture a time zone in a plugin REST API call, you should
2910         * use {@link #tryCaptureLocationInfo(SailPointContext, UserContext)}, passing the
2911         * plugin resource itself as a {@link UserContext}.
2912         *
2913         * @param context The current IIQ context
2914         * @param currentUser The user to modify with the detected information
2915         */
2916        public static void tryCaptureLocationInfo(SailPointContext context, Identity currentUser) {
2917                Objects.requireNonNull(currentUser, "A non-null Identity must be provided");
2918                TimeZone tz = null;
2919                Locale locale = null;
2920                FacesContext fc = FacesContext.getCurrentInstance();
2921                if (fc != null) {
2922                        if (fc.getViewRoot() != null) {
2923                                locale = fc.getViewRoot().getLocale();
2924                        }
2925                        HttpSession session = (HttpSession)fc.getExternalContext().getSession(true);
2926                        if (session != null) {
2927                                tz = (TimeZone)session.getAttribute("timeZone");
2928                        }
2929                }
2930                boolean save = false;
2931                if (tz != null) {
2932                        save = true;
2933                        currentUser.setUIPreference(MOST_RECENT_TIMEZONE, tz.getID());
2934                }
2935                if (locale != null) {
2936                        currentUser.setUIPreference(MOST_RECENT_LOCALE, locale.toLanguageTag());
2937                        save = true;
2938                }
2939                if (save) {
2940                        try {
2941                                context.saveObject(currentUser);
2942                                context.saveObject(currentUser.getUIPreferences());
2943                                context.commitTransaction();
2944                        } catch(Exception e) {
2945                                /* Ignore this */
2946                        }
2947                }
2948        }
2949
2950        /**
2951         * Attempts to capture the user's time zone information from the user context. This could be used via a web services call where the {@link BaseResource} class is a {@link UserContext}.
2952         *
2953         * @param context The current IIQ context
2954         * @param currentUser The current user context
2955         * @throws GeneralException if there is no currently logged in user
2956         */
2957        public static void tryCaptureLocationInfo(SailPointContext context, UserContext currentUser) throws GeneralException {
2958                TimeZone tz = currentUser.getUserTimeZone();
2959                Locale locale = currentUser.getLocale();
2960                boolean save = false;
2961                if (tz != null) {
2962                        save = true;
2963                        currentUser.getLoggedInUser().setUIPreference(MOST_RECENT_TIMEZONE, tz.getID());
2964                }
2965                if (locale != null) {
2966                        currentUser.getLoggedInUser().setUIPreference(MOST_RECENT_LOCALE, locale.toLanguageTag());
2967                        save = true;
2968                }
2969                if (save) {
2970                        try {
2971                                context.saveObject(currentUser.getLoggedInUser());
2972                                context.saveObject(currentUser.getLoggedInUser().getUIPreferences());
2973                                context.commitTransaction();
2974                        } catch(Exception e) {
2975                                /* Ignore this */
2976                        }
2977                }
2978        }
2979
2980        /**
2981         * Attempts to get the SPKeyStore, a class whose getInstance() is for some
2982         * reason protected.
2983         * @return The keystore, if we could get it
2984         * @throws GeneralException If the keystore could not be retrieved
2985         */
2986        public static SPKeyStore tryGetKeystore() throws GeneralException {
2987                SPKeyStore result;
2988                try {
2989                        Method getMethod = SPKeyStore.class.getDeclaredMethod("getInstance");
2990                        try {
2991                                getMethod.setAccessible(true);
2992                                result = (SPKeyStore) getMethod.invoke(null);
2993                        } finally {
2994                                getMethod.setAccessible(false);
2995                        }
2996                } catch(NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
2997                        throw new GeneralException(e);
2998                }
2999                return result;
3000        }
3001
3002        /**
3003         * Renders the given template using Velocity, passing the given arguments to the renderer.
3004         * Velocity will be initialized on the first invocation of this method.
3005         *
3006         * TODO invoke the Sailpoint utility in versions over 8.2
3007         *
3008         * @param template The VTL template string
3009         * @param args The arguments to pass to the template renderer
3010         * @return The rendered string
3011         * @throws IOException if any Velocity failures occur
3012         */
3013        public static String velocityRender(String template, Map<String, ?> args) throws IOException {
3014                if (!VELOCITY_INITIALIZED.get()) {
3015                        synchronized (VELOCITY_INITIALIZED) {
3016                                if (!VELOCITY_INITIALIZED.get()) {
3017                                        Velocity.setProperty("runtime.log.logsystem.class", "org.apache.velocity.runtime.log.AvalonLogChute,org.apache.velocity.runtime.log.Log4JLogChute,org.apache.velocity.runtime.log.JdkLogChute");
3018                                        Velocity.setProperty("ISO-8859-1", "UTF-8");
3019                                        Velocity.setProperty("output.encoding", "UTF-8");
3020                                        Velocity.setProperty("resource.loader", "classpath");
3021                                        Velocity.setProperty("classpath.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
3022                                        Velocity.init();
3023                                        VELOCITY_INITIALIZED.set(true);
3024                                }
3025                        }
3026                }
3027                Map<String, Object> params = new HashMap<>(args);
3028                params.put("escapeTools", new VelocityEscapeTools());
3029                params.put("spTools", new VelocityUtil.SailPointVelocityTools(Locale.getDefault(), TimeZone.getDefault()));
3030                VelocityContext velocityContext = new VelocityContext(params);
3031
3032                try (StringWriter writer = new StringWriter()) {
3033                        String tag = "anonymous";
3034                        boolean success = Velocity.evaluate(velocityContext, writer, tag, template);
3035
3036                        if (!success) {
3037                                throw new IOException("Velocity rendering did not succeed");
3038                        }
3039
3040                        writer.flush();
3041
3042                        return writer.toString();
3043                }
3044        }
3045
3046        /**
3047         * Transforms a wildcard like 'a*' to a Filter. This method cannot support mid-string
3048         * cases like 'a*a' at this time.
3049         *
3050         * @param property The property being filtered
3051         * @param input The input (e.g., 'a*'
3052         * @param caseInsensitive Whether the Filter should be case-insensitive
3053         * @return A filter matching the given wildcard
3054         */
3055        public static Filter wildcardToFilter(String property, String input, boolean caseInsensitive) {
3056                Filter output = null;
3057
3058                // Simple cases
3059                if (input.replace("*", "").isEmpty()) {
3060                        output = Filter.notnull(property);
3061                } else if (input.startsWith("*") && !input.substring(1).contains("*")) {
3062                        output = Filter.like(property, input.substring(1), Filter.MatchMode.END);
3063                } else if (input.endsWith("*") && !input.substring(0, input.length() - 1).contains("*")) {
3064                        output = Filter.like(property, input.substring(0, input.length() - 1), Filter.MatchMode.START);
3065                } else if (input.length() > 2 && input.startsWith("*") && input.endsWith("*") && !input.substring(1, input.length() - 1).contains("*")) {
3066                        output = Filter.like(property, input.substring(1, input.length() - 1), Filter.MatchMode.ANYWHERE);
3067                } else {
3068                        output = Filter.like(property, input, Filter.MatchMode.ANYWHERE);
3069                }
3070
3071                // TODO complex cases like `*a*b*`
3072
3073                if (output instanceof Filter.LeafFilter && caseInsensitive) {
3074                        output = Filter.ignoreCase(output);
3075                }
3076
3077                return output;
3078        }
3079
3080        /**
3081         * Uses the valueProducer to extract the value from the input object if it is not
3082         * null, otherwise returns the default value.
3083         *
3084         * @param maybeNull An object which may be null
3085         * @param defaultValue The value to return if the object is null
3086         * @param valueProducer The generator of the value to return if the value is not nothing
3087         * @param <T> The return type
3088         * @return The result of the value producer, or the default
3089         */
3090        public static <T> T withDefault(Object maybeNull, T defaultValue, Functions.FunctionWithError<Object, T> valueProducer) {
3091                if (maybeNull != null) {
3092                        try {
3093                                return valueProducer.applyWithError(maybeNull);
3094                        } catch(Error e) {
3095                                throw e;
3096                        } catch (Throwable throwable) {
3097                                logger.debug("Caught an error in withDefault", throwable);
3098                        }
3099                }
3100                return defaultValue;
3101        }
3102
3103        /**
3104         * Safely handles the given iterator by passing it to the Consumer and, regardless
3105         * of outcome, by flushing it when the Consumer returns.
3106         *
3107         * @param iterator The iterator to process
3108         * @param iteratorConsumer The iterator consumer, which will be invoked with the iterator
3109         * @param <T> The iterator type
3110         * @throws GeneralException if any failures occur
3111         */
3112        public static <T> void withIterator(Iterator<T> iterator, Functions.ConsumerWithError<Iterator<T>> iteratorConsumer) throws GeneralException {
3113                withIterator(() -> iterator, iteratorConsumer);
3114        }
3115
3116        /**
3117         * Safely handles the given iterator by passing it to the Consumer and, regardless
3118         * of outcome, by flushing it when the Consumer returns.
3119         *
3120         * @param iteratorSupplier The iterator supplier, which will be invoked once
3121         * @param iteratorConsumer The iterator consumer, which will be invoked with the iterator
3122         * @param <T> The iterator type
3123         * @throws GeneralException if any failures occur
3124         */
3125        public static <T> void withIterator(Functions.SupplierWithError<Iterator<T>> iteratorSupplier, Functions.ConsumerWithError<Iterator<T>> iteratorConsumer) throws GeneralException {
3126                try {
3127                        Iterator<T> iterator = iteratorSupplier.getWithError();
3128                        if (iterator != null) {
3129                                try {
3130                                        iteratorConsumer.acceptWithError(iterator);
3131                                } finally {
3132                                        Util.flushIterator(iterator);
3133                                }
3134                        }
3135                } catch(GeneralException | RuntimeException | Error e) {
3136                        throw e;
3137                } catch(Throwable t) {
3138                        throw new GeneralException(t);
3139                }
3140        }
3141
3142        /**
3143         * Obtains the lock, then executes the callback
3144         * @param lock The lock to lock before doing the execution
3145         * @param callback The callback to invoke after locking
3146         * @throws GeneralException if any failures occur or if the lock is interrupted
3147         */
3148        public static void withJavaLock(Lock lock, Callable<?> callback) throws GeneralException {
3149                try {
3150                        lock.lockInterruptibly();
3151                        try {
3152                                callback.call();
3153                        } catch(InterruptedException | GeneralException e) {
3154                                throw e;
3155                        } catch (Exception e) {
3156                                throw new GeneralException(e);
3157                        } finally {
3158                                lock.unlock();
3159                        }
3160                } catch(InterruptedException e) {
3161                        throw new GeneralException(e);
3162                }
3163        }
3164
3165        /**
3166         * Obtains the lock, then executes the callback
3167         * @param lock The lock to lock before doing the execution
3168         * @param timeoutMillis The timeout for the lock, in milliseconds
3169         * @param callback The callback to invoke after locking
3170         * @throws GeneralException if any failures occur or if the lock is interrupted
3171         */
3172        public static void withJavaTimeoutLock(Lock lock, long timeoutMillis, Callable<?> callback) throws GeneralException {
3173                try {
3174                        boolean locked = lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS);
3175                        if (!locked) {
3176                                throw new GeneralException("Unable to obtain the lock within timeout period " + timeoutMillis + " ms");
3177                        }
3178                        try {
3179                                callback.call();
3180                        } catch(InterruptedException | GeneralException e) {
3181                                throw e;
3182                        } catch (Exception e) {
3183                                throw new GeneralException(e);
3184                        } finally {
3185                                lock.unlock();
3186                        }
3187                } catch(InterruptedException e) {
3188                        throw new GeneralException(e);
3189                }
3190        }
3191
3192        /**
3193         * Creates a new database connection using the context provided, sets its auto-commit
3194         * flag to false, then passes it to the consumer provided. The consumer is responsible
3195         * for committing.
3196         *
3197         * @param context The context to produce the connection
3198         * @param consumer The consumer lambda or class to handle the connection
3199         * @throws GeneralException on failures
3200         */
3201        public static void withNoCommitConnection(SailPointContext context, Functions.ConnectionHandler consumer) throws GeneralException {
3202                try (Connection connection = ContextConnectionWrapper.getConnection(context)) {
3203                        try {
3204                                connection.setAutoCommit(false);
3205                                try {
3206                                        consumer.accept(connection);
3207                                } catch (GeneralException | SQLException | RuntimeException | Error e) {
3208                                        Quietly.rollback(connection);
3209                                        throw e;
3210                                } catch (Throwable t) {
3211                                        Quietly.rollback(connection);
3212                                        throw new GeneralException(t);
3213                                }
3214                        } finally{
3215                                connection.setAutoCommit(true);
3216                        }
3217                } catch(SQLException e) {
3218                        throw new GeneralException(e);
3219                }
3220        }
3221
3222        /**
3223         * Obtains a persistent lock on the object (sets lock = 1 in the DB), then executes the callback.
3224         * The lock will have a duration of 1 minute, so you should ensure that your operation is
3225         * relatively short.
3226         *
3227         * @param context The Sailpoint context
3228         * @param sailpointClass The Sailpoint class to lock
3229         * @param id The ID of the Sailpoint object
3230         * @param timeoutSeconds How long to wait, in seconds, before throwing {@link ObjectAlreadyLockedException}
3231         * @param callback The callback to invoke after locking which will be called with the locked object
3232         * @throws GeneralException if any failures occur or if the lock is interrupted
3233         */
3234        public static <V extends SailPointObject> void withPersistentLock(SailPointContext context, Class<V> sailpointClass, String id, int timeoutSeconds, Functions.ConsumerWithError<V> callback) throws GeneralException {
3235                PersistenceManager.LockParameters lockParameters = PersistenceManager.LockParameters.createById(id, PersistenceManager.LOCK_TYPE_PERSISTENT);
3236                lockParameters.setLockTimeout(timeoutSeconds);
3237                lockParameters.setLockDuration(1);
3238
3239                V object = ObjectUtil.lockObject(context, sailpointClass, lockParameters);
3240                try {
3241                        callback.acceptWithError(object);
3242                } catch(Error | GeneralException e) {
3243                        throw e;
3244                } catch(Throwable t) {
3245                        throw new GeneralException(t);
3246                } finally {
3247                        ObjectUtil.unlockObject(context, object, PersistenceManager.LOCK_TYPE_PERSISTENT);
3248                }
3249        }
3250
3251        /**
3252         * Begins a private, temporary SailpointContext session and then calls the given
3253         * Beanshell method within the previous Beanshell environment.
3254         *
3255         * @param bshThis The 'this' object from the current Beanshell script
3256         * @param methodName The callback method name to invoke after creating the private context
3257         * @throws GeneralException on any Beanshell failures
3258         */
3259        public static void withPrivateContext(bsh.This bshThis, String methodName) throws GeneralException {
3260                try {
3261                        SailPointContext previousContext = SailPointFactory.pushContext();
3262                        SailPointContext privateContext = SailPointFactory.getCurrentContext();
3263                        try {
3264                                Object[] args = new Object[] { privateContext };
3265                                bshThis.invokeMethod(methodName, args);
3266                        } finally {
3267                                if (privateContext != null) {
3268                                        SailPointFactory.releaseContext(privateContext);
3269                                }
3270                                if (previousContext != null) {
3271                                        SailPointFactory.setContext(previousContext);
3272                                }
3273                        }
3274                } catch(Throwable t) {
3275                        throw new GeneralException(t);
3276                }
3277        }
3278
3279        /**
3280         * Begins a private, temporary SailpointContext session and then calls the given
3281         * Beanshell method within the previous Beanshell environment. The 'params' will
3282         * be appended to the method call. The first argument to the Beanshell method will
3283         * be the SailPointContext, and the remaining arguments will be the parameters.
3284         *
3285         * @param bshThis The 'this' object from the current Beanshell script
3286         * @param methodName The callback method name to invoke after creating the private context
3287         * @param params Any other parameters to pass to the Beanshell method
3288         * @throws GeneralException on any Beanshell failures
3289         */
3290        public static void withPrivateContext(bsh.This bshThis, String methodName, List<Object> params) throws GeneralException {
3291                try {
3292                        SailPointContext previousContext = SailPointFactory.pushContext();
3293                        SailPointContext privateContext = SailPointFactory.getCurrentContext();
3294                        try {
3295                                Object[] args = new Object[1 + params.size()];
3296                                args[0] = privateContext;
3297                                for(int i = 0; i < params.size(); i++) {
3298                                        args[i + 1] = params.get(i);
3299                                }
3300
3301                                bshThis.invokeMethod(methodName, args);
3302                        } finally {
3303                                if (privateContext != null) {
3304                                        SailPointFactory.releaseContext(privateContext);
3305                                }
3306                                if (previousContext != null) {
3307                                        SailPointFactory.setContext(previousContext);
3308                                }
3309                        }
3310                } catch(Throwable t) {
3311                        throw new GeneralException(t);
3312                }
3313        }
3314
3315        /**
3316         * Begins a private, temporary SailpointContext session and then invokes the given
3317         * Consumer as a callback. The Consumer's input will be the temporary context.
3318         *
3319         * @param runner The runner
3320         * @throws GeneralException if anything fails at any point
3321         */
3322        public static void withPrivateContext(Functions.ConsumerWithError<SailPointContext> runner) throws GeneralException {
3323                try {
3324                        SailPointContext previousContext = SailPointFactory.pushContext();
3325                        SailPointContext privateContext = SailPointFactory.getCurrentContext();
3326                        try {
3327                                runner.acceptWithError(privateContext);
3328                        } finally {
3329                                if (privateContext != null) {
3330                                        SailPointFactory.releaseContext(privateContext);
3331                                }
3332                                if (previousContext != null) {
3333                                        SailPointFactory.setContext(previousContext);
3334                                }
3335                        }
3336                } catch(GeneralException | RuntimeException | Error e) {
3337                        throw e;
3338                } catch(Throwable t) {
3339                        throw new GeneralException(t);
3340                }
3341        }
3342
3343        /**
3344         * Begins a private, temporary SailpointContext session and then invokes the given
3345         * Function as a callback. The Function's input will be the temporary context.
3346         * The result of the Function will be returned.
3347         *
3348         * @param runner A function that performs some action and returns a value
3349         * @param <T> The output type of the function
3350         * @return The value returned from the runner
3351         * @throws GeneralException if anything fails at any point
3352         */
3353        public static <T> T withPrivateContext(Functions.FunctionWithError<SailPointContext, T> runner) throws GeneralException {
3354                try {
3355                        SailPointContext previousContext = SailPointFactory.pushContext();
3356                        SailPointContext privateContext = SailPointFactory.getCurrentContext();
3357                        try {
3358                                return runner.applyWithError(privateContext);
3359                        } finally {
3360                                if (privateContext != null) {
3361                                        SailPointFactory.releaseContext(privateContext);
3362                                }
3363                                if (previousContext != null) {
3364                                        SailPointFactory.setContext(previousContext);
3365                                }
3366                        }
3367                } catch(GeneralException e) {
3368                        throw e;
3369                } catch(Throwable t) {
3370                        throw new GeneralException(t);
3371                }
3372        }
3373
3374        /**
3375         * Begins a private, temporary SailpointContext session and then calls the given
3376         * Beanshell method within the previous Beanshell environment. Each item in the
3377         * 'params' list will be passed individually, so the method is expected to take
3378         * two arguments: the context and the Object.
3379         *
3380         * @param bshThis The 'this' object from the current Beanshell script
3381         * @param methodName The callback method name to invoke after creating the private context
3382         * @param params Any other parameters to pass to the Beanshell method
3383         * @throws GeneralException on any Beanshell failures
3384         */
3385        public static void withPrivateContextIterate(bsh.This bshThis, String methodName, Collection<Object> params) throws GeneralException {
3386                try {
3387                        SailPointContext previousContext = SailPointFactory.pushContext();
3388                        SailPointContext privateContext = SailPointFactory.getCurrentContext();
3389                        try {
3390                                for (Object param : params) {
3391                                        Object[] args = new Object[2];
3392                                        args[0] = privateContext;
3393                                        args[1] = param;
3394
3395                                        bshThis.invokeMethod(methodName, args);
3396                                }
3397                        } finally {
3398                                if (privateContext != null) {
3399                                        SailPointFactory.releaseContext(privateContext);
3400                                }
3401                                if (previousContext != null) {
3402                                        SailPointFactory.setContext(previousContext);
3403                                }
3404                        }
3405                } catch(Throwable t) {
3406                        throw new GeneralException(t);
3407                }
3408        }
3409
3410        /**
3411         * Obtains a transaction lock on the object (selects it 'for update'), then executes the callback.
3412         * @param context The Sailpoint context
3413         * @param sailpointClass The Sailpoint class to lock
3414         * @param id The ID of the Sailpoint object
3415         * @param timeoutSeconds How long to wait, in seconds, before throwing {@link ObjectAlreadyLockedException}
3416         * @param callback The callback to invoke after locking
3417         * @param <V> The type of the SailPointObject to lock
3418         * @throws GeneralException if any failures occur or if the lock is interrupted
3419         */
3420        public static <V extends SailPointObject> void withTransactionLock(SailPointContext context, Class<V> sailpointClass, String id, int timeoutSeconds, Functions.ConsumerWithError<V> callback) throws GeneralException {
3421                PersistenceManager.LockParameters lockParameters = PersistenceManager.LockParameters.createById(id, PersistenceManager.LOCK_TYPE_TRANSACTION);
3422                lockParameters.setLockTimeout(timeoutSeconds);
3423                V object = ObjectUtil.lockObject(context, sailpointClass, lockParameters);
3424                try {
3425                        callback.acceptWithError(object);
3426                } catch(Error | GeneralException e) {
3427                        throw e;
3428                } catch(Throwable t) {
3429                        throw new GeneralException(t);
3430                } finally {
3431                        ObjectUtil.unlockObject(context, object, PersistenceManager.LOCK_TYPE_TRANSACTION);
3432                }
3433        }
3434
3435}