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