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