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