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}