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