001package com.identityworksllc.iiq.common.task; 002 003import org.apache.commons.logging.Log; 004import org.apache.commons.logging.LogFactory; 005import sailpoint.object.TaskResult; 006import sailpoint.object.TaskSchedule; 007import sailpoint.tools.GeneralException; 008import sailpoint.tools.Util; 009 010import java.text.SimpleDateFormat; 011import java.time.*; 012import java.time.temporal.ChronoUnit; 013import java.util.Date; 014import java.util.HashMap; 015import java.util.Locale; 016import java.util.Map; 017import java.util.regex.Matcher; 018import java.util.regex.Pattern; 019 020/** 021 * Substitutes time tokens in a particular string, typically for task scheduling. 022 * 023 * Time tokens can be any of: 024 * 025 * - $NOW The current timestamp 026 * - $MIDNIGHT The most recent midnight in the time zone given 027 * - $YESTERDAY The second most recent midnight in the time zone given 028 * - $LASTSTART The last time the task was started, or 0 if no data 029 * - $LASTSTOP The last time the task was stopped, or 0 if no data 030 * - $(anyVariable as format) 031 * Formats the input timestamp using the given format (for querying string dates) 032 * 033 * - $(today at 14:00) 034 * - $(yesterday at 10:00) 035 * The described timestamp 036 * 037 * - $(+/-N UNITS) 038 * An offset of N UNITS (e.g. "-3 hours"), where the unit is one of {@link ChronoUnit} 039 * 040 * - $(+/-N UNITS at midnight) 041 * - $(LASTSTOP -N UNITS) 042 */ 043public class TimeTokenizer { 044 045 private static final Log log = LogFactory.getLog(TimeTokenizer.class); 046 047 private static final Pattern tokenFormatPattern = Pattern.compile("\\$\\(([^:]*?) as (.*?)\\)"); 048 049 /** 050 * Parses the input string, which may or may not contain time tokens, by 051 * replacing the token with a string suitable for a Sailpoint filter (i.e., 052 * the DATE$ format). 053 * 054 * @param taskSchedule The task schedule, optionally. It will be used to populate the LASTSTART and LASTSTOP tokens if present. 055 * @param inputString the input string to replace tokens within 056 * @param zoneIdString The timezone to use for time tokens (optional) 057 * @return The parsed and reformatted input string 058 * @throws GeneralException if any failures occur during parsing 059 */ 060 public static String parseTimeComponents(TaskSchedule taskSchedule, String inputString, String zoneIdString) throws GeneralException { 061 log.debug("String before token replacement: " + inputString); 062 063 ZoneId zoneId = ZoneId.systemDefault(); 064 if (Util.isNotNullOrEmpty(zoneIdString)) { 065 zoneId = ZoneId.of(zoneIdString); 066 } 067 Instant now = Instant.now(); 068 Instant midnight = LocalDate.now().atTime(LocalTime.MIDNIGHT).atZone(zoneId).toInstant(); 069 Instant yesterday = LocalDate.now().atTime(LocalTime.MIDNIGHT).minus(1, ChronoUnit.DAYS).atZone(zoneId).toInstant(); 070 071 Map<String, Long> tokenMap = new HashMap<>(); 072 tokenMap.put("NOW", now.toEpochMilli()); 073 tokenMap.put("MIDNIGHT", midnight.toEpochMilli()); 074 tokenMap.put("YESTERDAY", yesterday.toEpochMilli()); 075 076 if (taskSchedule != null) { 077 tokenMap.put("LASTSTART", 0L); 078 tokenMap.put("LASTSTOP", 0L); 079 080 if (taskSchedule.getLastExecution() != null) { 081 tokenMap.put("LASTSTART", taskSchedule.getLastExecution().getTime()); 082 } 083 084 if (taskSchedule.getLatestResult() != null) { 085 TaskResult latestResult = taskSchedule.getLatestResult(); 086 if (latestResult.getCompleted() != null) { 087 tokenMap.put("LASTSTOP", latestResult.getCompleted().getTime()); 088 } 089 } 090 } 091 092 for(Map.Entry<String, Long> token : tokenMap.entrySet()) { 093 inputString = inputString.replace("$" + token.getKey(), "DATE$" + token.getValue()); 094 inputString = inputString.replace("$" + token.getKey().toLowerCase(Locale.ROOT), "DATE$" + token.getValue()); 095 } 096 097 StringBuffer replacement = new StringBuffer(); 098 Matcher matcher = tokenFormatPattern.matcher(inputString); 099 while(matcher.find()) { 100 String token = matcher.group(1); 101 String formatString = matcher.group(2); 102 Long timestamp = tokenMap.get(token.toUpperCase(Locale.ROOT)); 103 if (timestamp == null) { 104 matcher.appendReplacement(replacement, Matcher.quoteReplacement(matcher.group())); 105 } else { 106 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(formatString); 107 String formattedDate = simpleDateFormat.format(new Date(timestamp)); 108 matcher.appendReplacement(replacement, Matcher.quoteReplacement(formattedDate)); 109 } 110 } 111 matcher.appendTail(replacement); 112 113 inputString = replacement.toString(); 114 115 String outputString = replaceOffsetTokens(inputString, tokenMap, now, midnight); 116 outputString = replaceTimeTokens(outputString, zoneId); 117 118 log.debug("String after token replacement: " + outputString); 119 return outputString; 120 } 121 122 /** 123 * Replaces any offset-type tokens, like $(-2 hours), within the string. The 124 * unit should be a ChronoUnit. 125 */ 126 public static String replaceOffsetTokens(String inputString, Map<String, Long> tokenMap, Instant now, Instant midnight) { 127 Pattern subtractPattern = Pattern.compile("\\$\\(" + 128 "(LASTSTART |LASTSTOP )?([\\-+]?\\d+) (\\w+?)( at midnight)?( as (.*?))?" + 129 "\\)"); 130 StringBuffer finalFilter = new StringBuffer(); 131 Matcher matcher = subtractPattern.matcher(inputString); 132 while(matcher.find()) { 133 String offset = matcher.group(1); 134 String amount = matcher.group(2); 135 String type = matcher.group(3); 136 String atMidnight = matcher.group(4); 137 String formatString = matcher.group(6); 138 ChronoUnit unit = ChronoUnit.valueOf(type.toUpperCase()); 139 int amountNum = Util.otoi(amount); 140 141 Instant start; 142 if (Util.isNotNullOrEmpty(offset)) { 143 String which = offset.trim().toUpperCase(); 144 Long startingPointOffset = tokenMap.get(which); 145 if (startingPointOffset == null) { 146 startingPointOffset = 0L; 147 } 148 if (startingPointOffset == 0) { 149 log.warn("Starting point for date offset " + which + " is zero"); 150 } 151 start = Instant.ofEpochMilli(startingPointOffset); 152 } else { 153 start = (Util.isNullOrEmpty(atMidnight) ? now : midnight); 154 } 155 156 Instant when = start.plus(amountNum, unit); 157 if (Util.isNotNullOrEmpty(formatString)) { 158 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(formatString); 159 String replacement = simpleDateFormat.format(new Date(when.toEpochMilli())); 160 matcher.appendReplacement(finalFilter, Matcher.quoteReplacement(replacement)); 161 } else { 162 String replacement = "DATE$" + when.toEpochMilli(); 163 matcher.appendReplacement(finalFilter, Matcher.quoteReplacement(replacement)); 164 } 165 } 166 matcher.appendTail(finalFilter); 167 return finalFilter.toString(); 168 } 169 170 /** 171 * Replaces any time-type tokens $(today at 14:00) in the input string 172 */ 173 public static String replaceTimeTokens(String inputString, ZoneId timeZone) { 174 Pattern subtractPattern = Pattern.compile("\\$\\(" + 175 "(today|tomorrow|yesterday) at (\\d+:\\d+(:\\d+)?)( as (.*?))?" + 176 "\\)"); 177 StringBuffer finalFilter = new StringBuffer(); 178 Matcher matcher = subtractPattern.matcher(inputString); 179 while(matcher.find()) { 180 String type = matcher.group(1); 181 String timeString = matcher.group(2); 182 String seconds = matcher.group(3); 183 String format = matcher.group(5); 184 185 // Append seconds if needed or it won't parse 186 if (Util.isNullOrEmpty(seconds)) { 187 timeString += ":00"; 188 } 189 // Prepend a leading zero to the hour if needed or it won't parse 190 if (timeString.indexOf(":") == 1) { 191 timeString = "0" + timeString; 192 } 193 194 LocalTime timeOffset = LocalTime.parse(timeString); 195 196 LocalDateTime dateTimeOffset = LocalDate.now().atTime(timeOffset); 197 198 if (Util.nullSafeEq(type, "yesterday")) { 199 dateTimeOffset = dateTimeOffset.minus(1, ChronoUnit.DAYS); 200 } else if (Util.nullSafeEq(type, "tomorrow")) { 201 dateTimeOffset = dateTimeOffset.plus(1, ChronoUnit.DAYS); 202 } 203 204 ZonedDateTime instant = dateTimeOffset.atZone(timeZone); 205 206 if (Util.isNotNullOrEmpty(format)) { 207 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); 208 String replacement = simpleDateFormat.format(new Date(instant.toInstant().toEpochMilli())); 209 matcher.appendReplacement(finalFilter, Matcher.quoteReplacement(replacement)); 210 } else { 211 String replacement = "DATE$" + instant.toInstant().toEpochMilli(); 212 matcher.appendReplacement(finalFilter, Matcher.quoteReplacement(replacement)); 213 } 214 } 215 matcher.appendTail(finalFilter); 216 return finalFilter.toString(); 217 } 218}