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