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