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