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