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