From 2e874b085a207d3977b7abdaea88020bf3932f47 Mon Sep 17 00:00:00 2001 From: Nikita Koksharov <nkoksharov@redisson.pro> Date: Fri, 14 Oct 2022 08:46:35 +0300 Subject: [PATCH] Fixed - RScheduledExecutorService cron triggers fire continuously for hours for some time zones (regression since 3.16.5) --- .../org/redisson/RedissonExecutorService.java | 11 +- .../java/org/redisson/api/CronSchedule.java | 8 +- .../org/redisson/executor/CronExpression.java | 1877 ++++++++++++----- .../redisson/executor/CronExpressionEx.java | 71 - .../redisson/executor/TasksRunnerService.java | 13 +- 5 files changed, 1379 insertions(+), 601 deletions(-) delete mode 100644 redisson/src/main/java/org/redisson/executor/CronExpressionEx.java diff --git a/redisson/src/main/java/org/redisson/RedissonExecutorService.java b/redisson/src/main/java/org/redisson/RedissonExecutorService.java index b3f440e22..cc9a05433 100644 --- a/redisson/src/main/java/org/redisson/RedissonExecutorService.java +++ b/redisson/src/main/java/org/redisson/RedissonExecutorService.java @@ -40,8 +40,6 @@ import java.lang.ref.ReferenceQueue; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.*; @@ -1218,12 +1216,11 @@ public class RedissonExecutorService implements RScheduledExecutorService { check(task); ClassBody classBody = getClassBody(task); byte[] state = encode(task); - ZonedDateTime currentDate = ZonedDateTime.of(LocalDateTime.now(), cronSchedule.getZoneId()); - ZonedDateTime startDate = cronSchedule.getExpression().nextTimeAfter(currentDate); + Date startDate = cronSchedule.getExpression().getNextValidTimeAfter(new Date()); if (startDate == null) { throw new IllegalArgumentException("Wrong cron expression! Unable to calculate start date"); } - long startTime = startDate.toInstant().toEpochMilli(); + long startTime = startDate.getTime(); String taskId = id; ScheduledCronExpressionParameters params = new ScheduledCronExpressionParameters(taskId); @@ -1232,14 +1229,14 @@ public class RedissonExecutorService implements RScheduledExecutorService { params.setLambdaBody(classBody.getLambda()); params.setState(state); params.setStartTime(startTime); - params.setCronExpression(cronSchedule.getExpression().getExpr()); + params.setCronExpression(cronSchedule.getExpression().getCronExpression()); params.setTimezone(cronSchedule.getZoneId().toString()); params.setExecutorId(executorId); RemotePromise<Void> result = (RemotePromise<Void>) asyncScheduledServiceAtFixed.schedule(params).toCompletableFuture(); addListener(result); RedissonScheduledFuture<Void> f = new RedissonScheduledFuture<Void>(result, startTime) { public long getDelay(TimeUnit unit) { - return unit.convert(startDate.toInstant().toEpochMilli() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + return unit.convert(startDate.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); }; }; storeReference(f, result.getRequestId()); diff --git a/redisson/src/main/java/org/redisson/api/CronSchedule.java b/redisson/src/main/java/org/redisson/api/CronSchedule.java index 4c05b0b48..f117ae7d4 100644 --- a/redisson/src/main/java/org/redisson/api/CronSchedule.java +++ b/redisson/src/main/java/org/redisson/api/CronSchedule.java @@ -16,9 +16,9 @@ package org.redisson.api; import org.redisson.executor.CronExpression; -import org.redisson.executor.CronExpressionEx; import java.time.ZoneId; +import java.util.TimeZone; /** * Cron expression object used in {@link RScheduledExecutorService}. @@ -49,7 +49,7 @@ public final class CronSchedule { * wrapping a ParseException if the expression is invalid */ public static CronSchedule of(String expression) { - return new CronSchedule(new CronExpressionEx(expression), ZoneId.systemDefault()); + return of(expression, ZoneId.systemDefault()); } /** @@ -62,7 +62,9 @@ public final class CronSchedule { * wrapping a ParseException if the expression is invalid */ public static CronSchedule of(String expression, ZoneId zoneId) { - return new CronSchedule(new CronExpressionEx(expression), zoneId); + CronExpression ce = new CronExpression(expression); + ce.setTimeZone(TimeZone.getTimeZone(zoneId)); + return new CronSchedule(ce, zoneId); } /** diff --git a/redisson/src/main/java/org/redisson/executor/CronExpression.java b/redisson/src/main/java/org/redisson/executor/CronExpression.java index 14ccfd37f..ef2de39cc 100644 --- a/redisson/src/main/java/org/redisson/executor/CronExpression.java +++ b/redisson/src/main/java/org/redisson/executor/CronExpression.java @@ -13,648 +13,1499 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** - * Copyright (C) 2012- Frode Carlsen. +/* + * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. * - * Note: rewritten to standard Java 8 DateTime by zemiak (c) 2016 */ + package org.redisson.executor; -import java.time.*; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.io.Serializable; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.SortedSet; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.TreeSet; /** - * This provides cron support for java8 using java-time. - * <P> - * - * Parser for unix-like cron expressions: Cron expressions allow specifying combinations of criteria for time - * such as: "Each Monday-Friday at 08:00" or "Every last friday of the month at 01:30" - * <p> - * A cron expressions consists of 5 or 6 mandatory fields (seconds may be omitted) separated by space. <br> - * These are: - * - * <table cellspacing="8"> - * <tr> - * <th align="left">Field</th> - * <th align="left"> </th> - * <th align="left">Allowable values</th> - * <th align="left"> </th> - * <th align="left">Special Characters</th> - * </tr> - * <tr> - * <td align="left"><code>Seconds (may be omitted)</code></td> - * <td align="left"> </th> - * <td align="left"><code>0-59</code></td> - * <td align="left"> </th> - * <td align="left"><code>, - * /</code></td> - * </tr> - * <tr> - * <td align="left"><code>Minutes</code></td> - * <td align="left"> </th> - * <td align="left"><code>0-59</code></td> - * <td align="left"> </th> - * <td align="left"><code>, - * /</code></td> - * </tr> - * <tr> - * <td align="left"><code>Hours</code></td> - * <td align="left"> </th> - * <td align="left"><code>0-23</code></td> - * <td align="left"> </th> - * <td align="left"><code>, - * /</code></td> - * </tr> - * <tr> - * <td align="left"><code>Day of month</code></td> - * <td align="left"> </th> - * <td align="left"><code>1-31</code></td> - * <td align="left"> </th> - * <td align="left"><code>, - * ? / L W</code></td> - * </tr> - * <tr> - * <td align="left"><code>Month</code></td> - * <td align="left"> </th> - * <td align="left"><code>1-12 or JAN-DEC (note: english abbreviations)</code></td> - * <td align="left"> </th> - * <td align="left"><code>, - * /</code></td> - * </tr> - * <tr> - * <td align="left"><code>Day of week</code></td> - * <td align="left"> </th> - * <td align="left"><code>1-7 or MON-SUN (note: english abbreviations)</code></td> - * <td align="left"> </th> - * <td align="left"><code>, - * ? / L #</code></td> - * </tr> - * </table> - * - * <P> - * '*' Can be used in all fields and means 'for all values'. E.g. "*" in minutes, means 'for all minutes' - * <P> - * '?' Can be used in Day-of-month and Day-of-week fields. Used to signify 'no special value'. It is used when one want - * to specify something for one of those two fields, but not the other. - * <P> - * '-' Used to specify a time interval. E.g. "10-12" in Hours field means 'for hours 10, 11 and 12' - * <P> - * ',' Used to specify multiple values for a field. E.g. "MON,WED,FRI" in Day-of-week field means "for - * monday, wednesday and friday" - * <P> - * '/' Used to specify increments. E.g. "0/15" in Seconds field means "for seconds 0, 15, 30, ad - * 45". And "5/15" in seconds field means "for seconds 5, 20, 35, and 50". If '*' s specified - * before '/' it is the same as saying it starts at 0. For every field there's a list of values that can be turned on or - * off. For Seconds and Minutes these range from 0-59. For Hours from 0 to 23, For Day-of-month it's 1 to 31, For Months - * 1 to 12. "/" character helsp turn some of these values back on. Thus "7/6" in Months field - * specify just Month 7. It doesn't turn on every 6 month following, since cron fields never roll over - * <P> - * 'L' Can be used on Day-of-month and Day-of-week fields. It signifies last day of the set of allowed values. In - * Day-of-month field it's the last day of the month (e.g.. 31 jan, 28 feb (29 in leap years), 31 march, etc.). In - * Day-of-week field it's Sunday. If there's a prefix, this will be subtracted (5L in Day-of-month means 5 days before - * last day of Month: 26 jan, 23 feb, etc.) - * <P> - * 'W' Can be specified in Day-of-Month field. It specifies closest weekday (monday-friday). Holidays are not accounted - * for. "15W" in Day-of-Month field means 'closest weekday to 15 i in given month'. If the 15th is a Saturday, - * it gives Friday. If 15th is a Sunday, the it gives following Monday. - * <P> - * '#' Can be used in Day-of-Week field. For example: "5#3" means 'third friday in month' (day 5 = friday, #3 - * - the third). If the day does not exist (e.g. "5#5" - 5th friday of month) and there aren't 5 fridays in - * the month, then it won't match until the next month with 5 fridays. - * <P> - * <b>Case-sensitive</b> No fields are case-sensitive - * <P> - * <b>Dependencies between fields</b> Fields are always evaluated independently, but the expression doesn't match until - * the constraints of each field are met. Overlap of intervals are not allowed. That is: for - * Day-of-week field "FRI-MON" is invalid,but "FRI-SUN,MON" is valid - * + * @author Sharada Jambula, James House + * @author Contributions from Mads Henderson + * @author Refactoring from CronTrigger to CronExpression by Aaron Craven */ -@SuppressWarnings({"AvoidInlineConditionals", "MultipleVariableDeclarations", "InnerAssignment", "UnnecessaryParentheses"}) -public class CronExpression { +@SuppressWarnings({"EmptyStatement", "InnerAssignment", "ConstantName", + "BooleanExpressionComplexity", "NestedIfDepth", "ParenPad", + "MethodLength", "WhitespaceAfter", "NoClone", "UnnecessaryParentheses", "AvoidInlineConditionals", + "EqualsAvoidNull", "OneStatementPerLine"}) +public final class CronExpression implements Serializable, Cloneable { + + private static final long serialVersionUID = 12423409423L; + + protected static final int SECOND = 0; + protected static final int MINUTE = 1; + protected static final int HOUR = 2; + protected static final int DAY_OF_MONTH = 3; + protected static final int MONTH = 4; + protected static final int DAY_OF_WEEK = 5; + protected static final int YEAR = 6; + protected static final int ALL_SPEC_INT = 99; // '*' + protected static final int NO_SPEC_INT = 98; // '?' + protected static final Integer ALL_SPEC = ALL_SPEC_INT; + protected static final Integer NO_SPEC = NO_SPEC_INT; + + protected static final Map<String, Integer> monthMap = new HashMap<String, Integer>(20); + protected static final Map<String, Integer> dayMap = new HashMap<String, Integer>(60); + static { + monthMap.put("JAN", 0); + monthMap.put("FEB", 1); + monthMap.put("MAR", 2); + monthMap.put("APR", 3); + monthMap.put("MAY", 4); + monthMap.put("JUN", 5); + monthMap.put("JUL", 6); + monthMap.put("AUG", 7); + monthMap.put("SEP", 8); + monthMap.put("OCT", 9); + monthMap.put("NOV", 10); + monthMap.put("DEC", 11); + + dayMap.put("SUN", 1); + dayMap.put("MON", 2); + dayMap.put("TUE", 3); + dayMap.put("WED", 4); + dayMap.put("THU", 5); + dayMap.put("FRI", 6); + dayMap.put("SAT", 7); + } - enum CronFieldType { - SECOND(0, 59, null) { - @Override - int getValue(ZonedDateTime dateTime) { - return dateTime.getSecond(); - } + private final String cronExpression; + private TimeZone timeZone = null; + protected transient TreeSet<Integer> seconds; + protected transient TreeSet<Integer> minutes; + protected transient TreeSet<Integer> hours; + protected transient TreeSet<Integer> daysOfMonth; + protected transient TreeSet<Integer> months; + protected transient TreeSet<Integer> daysOfWeek; + protected transient TreeSet<Integer> years; + + protected transient boolean lastdayOfWeek = false; + protected transient int nthdayOfWeek = 0; + protected transient boolean lastdayOfMonth = false; + protected transient boolean nearestWeekday = false; + protected transient int lastdayOffset = 0; + protected transient boolean expressionParsed = false; + + public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; - @Override - ZonedDateTime setValue(ZonedDateTime dateTime, int value) { - return dateTime.withSecond(value).withNano(0); - } + /** + * Constructs a new <CODE>CronExpression</CODE> based on the specified + * parameter. + * + * @param cronExpression String representation of the cron expression the + * new object should represent + */ + public CronExpression(String cronExpression) { + if (cronExpression == null) { + throw new IllegalArgumentException("cronExpression cannot be null"); + } - @Override - ZonedDateTime overflow(ZonedDateTime dateTime) { - return dateTime.plusMinutes(1).withSecond(0).withNano(0); - } - }, - MINUTE(0, 59, null) { - @Override - int getValue(ZonedDateTime dateTime) { - return dateTime.getMinute(); - } + this.cronExpression = cronExpression.toUpperCase(Locale.US); - @Override - ZonedDateTime setValue(ZonedDateTime dateTime, int value) { - return dateTime.withMinute(value).withSecond(0).withNano(0); - } + try { + buildExpression(this.cronExpression); + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + } - @Override - ZonedDateTime overflow(ZonedDateTime dateTime) { - return dateTime.plusHours(1).withMinute(0).withSecond(0).withNano(0); - } - }, - HOUR(0, 23, null) { - @Override - int getValue(ZonedDateTime dateTime) { - return dateTime.getHour(); - } + /** + * Constructs a new {@code CronExpression} as a copy of an existing + * instance. + * + * @param expression + * The existing cron expression to be copied + */ + public CronExpression(CronExpression expression) { + /* + * We don't call the other constructor here since we need to swallow the + * ParseException. We also elide some of the sanity checking as it is + * not logically trippable. + */ + this.cronExpression = expression.getCronExpression(); + try { + buildExpression(cronExpression); + } catch (ParseException ex) { + throw new AssertionError(); + } + if (expression.getTimeZone() != null) { + setTimeZone((TimeZone) expression.getTimeZone().clone()); + } + } - @Override - ZonedDateTime setValue(ZonedDateTime dateTime, int value) { - return dateTime.withHour(value).withMinute(0).withSecond(0).withNano(0); - } + /** + * Indicates whether the given date satisfies the cron expression. Note that + * milliseconds are ignored, so two Dates falling on different milliseconds + * of the same second will always have the same result here. + * + * @param date the date to evaluate + * @return a boolean indicating whether the given date satisfies the cron + * expression + */ + public boolean isSatisfiedBy(Date date) { + Calendar testDateCal = Calendar.getInstance(getTimeZone()); + testDateCal.setTime(date); + testDateCal.set(Calendar.MILLISECOND, 0); + Date originalDate = testDateCal.getTime(); - @Override - ZonedDateTime overflow(ZonedDateTime dateTime) { - return dateTime.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); - } - }, - DAY_OF_MONTH(1, 31, null) { - @Override - int getValue(ZonedDateTime dateTime) { - return dateTime.getDayOfMonth(); - } + testDateCal.add(Calendar.SECOND, -1); - @Override - ZonedDateTime setValue(ZonedDateTime dateTime, int value) { - return dateTime.withDayOfMonth(value).withHour(0).withMinute(0).withSecond(0).withNano(0); - } + Date timeAfter = getTimeAfter(testDateCal.getTime()); - @Override - ZonedDateTime overflow(ZonedDateTime dateTime) { - return dateTime.plusMonths(1).withDayOfMonth(0).withHour(0).withMinute(0).withSecond(0).withNano(0); - } - }, - MONTH(1, 12, - Arrays.asList("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC")) { - @Override - int getValue(ZonedDateTime dateTime) { - return dateTime.getMonthValue(); - } + return ((timeAfter != null) && (timeAfter.equals(originalDate))); + } - @Override - ZonedDateTime setValue(ZonedDateTime dateTime, int value) { - return dateTime.withMonth(value).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0); - } + /** + * Returns the next date/time <I>after</I> the given date/time which + * satisfies the cron expression. + * + * @param date the date/time at which to begin the search for the next valid + * date/time + * @return the next valid date/time + */ + public Date getNextValidTimeAfter(Date date) { + return getTimeAfter(date); + } - @Override - ZonedDateTime overflow(ZonedDateTime dateTime) { - return dateTime.plusYears(1).withMonth(1).withHour(0).withDayOfMonth(1).withMinute(0).withSecond(0).withNano(0); - } - }, - DAY_OF_WEEK(1, 7, Arrays.asList("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN")) { - @Override - int getValue(ZonedDateTime dateTime) { - return dateTime.getDayOfWeek().getValue(); - } + /** + * Returns the next date/time <I>after</I> the given date/time which does + * <I>not</I> satisfy the expression + * + * @param date the date/time at which to begin the search for the next + * invalid date/time + * @return the next valid date/time + */ + public Date getNextInvalidTimeAfter(Date date) { + long difference = 1000; - @Override - ZonedDateTime setValue(ZonedDateTime dateTime, int value) { - throw new UnsupportedOperationException(); - } + //move back to the nearest second so differences will be accurate + Calendar adjustCal = Calendar.getInstance(getTimeZone()); + adjustCal.setTime(date); + adjustCal.set(Calendar.MILLISECOND, 0); + Date lastDate = adjustCal.getTime(); - @Override - ZonedDateTime overflow(ZonedDateTime dateTime) { - throw new UnsupportedOperationException(); - } - }; + Date newDate; - final int from, to; - final List<String> names; + //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution. - CronFieldType(int from, int to, List<String> names) { - this.from = from; - this.to = to; - this.names = names; - } + //keep getting the next included time until it's farther than one second + // apart. At that point, lastDate is the last valid fire time. We return + // the second immediately following it. + while (difference == 1000) { + newDate = getTimeAfter(lastDate); + if(newDate == null) + break; - /** - * @param dateTime {@link ZonedDateTime} instance - * @return The field time or date value from {@code dateTime} - */ - abstract int getValue(ZonedDateTime dateTime); + difference = newDate.getTime() - lastDate.getTime(); - /** - * @param dateTime Initial {@link ZonedDateTime} instance to use - * @param value to set for this field in {@code dateTime} - * @return {@link ZonedDateTime} with {@code value} set for this field and all smaller fields cleared - */ - abstract ZonedDateTime setValue(ZonedDateTime dateTime, int value); + if (difference == 1000) { + lastDate = newDate; + } + } - /** - * Handle when this field overflows and the next higher field should be incremented - * - * @param dateTime Initial {@link ZonedDateTime} instance to use - * @return {@link ZonedDateTime} with the next greater field incremented and all smaller fields cleared - */ - abstract ZonedDateTime overflow(ZonedDateTime dateTime); + return new Date(lastDate.getTime() + 1000); } - private final String expr; - private final SimpleField secondField; - private final SimpleField minuteField; - private final SimpleField hourField; - private final DayOfWeekField dayOfWeekField; - private final SimpleField monthField; - private final DayOfMonthField dayOfMonthField; + /** + * Returns the time zone for which this <code>CronExpression</code> + * will be resolved. + * + * @return time zone + */ + public TimeZone getTimeZone() { + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } - public CronExpression(final String expr) { - this(expr, true); + return timeZone; } - public CronExpression(final String expr, final boolean withSeconds) { - if (expr == null) { - throw new IllegalArgumentException("expr is null"); //$NON-NLS-1$ - } + /** + * Sets the time zone for which this <code>CronExpression</code> + * will be resolved. + * + * @param timeZone object + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Returns the string representation of the <CODE>CronExpression</CODE> + * + * @return a string representation of the <CODE>CronExpression</CODE> + */ + @Override + public String toString() { + return cronExpression; + } - this.expr = expr; + /** + * Indicates whether the specified cron expression can be parsed into a + * valid cron expression + * + * @param cronExpression the expression to evaluate + * @return a boolean indicating whether the given expression is a valid cron + * expression + */ + public static boolean isValidExpression(String cronExpression) { - final int expectedParts = withSeconds ? 6 : 5; - final String[] parts = expr.split("\\s+"); //$NON-NLS-1$ - if (parts.length != expectedParts) { - throw new IllegalArgumentException(String.format("Invalid cron expression [%s], expected %s field, got %s", expr, expectedParts, parts.length)); + try { + new CronExpression(cronExpression); + } catch (IllegalArgumentException pe) { + return false; } - int ix = withSeconds ? 1 : 0; - this.secondField = new SimpleField(CronFieldType.SECOND, withSeconds ? parts[0] : "0"); - this.minuteField = new SimpleField(CronFieldType.MINUTE, parts[ix++]); - this.hourField = new SimpleField(CronFieldType.HOUR, parts[ix++]); - this.dayOfMonthField = new DayOfMonthField(parts[ix++]); - this.monthField = new SimpleField(CronFieldType.MONTH, parts[ix++]); - this.dayOfWeekField = new DayOfWeekField(parts[ix++]); + return true; } - public static CronExpression create(final String expr) { - return new CronExpression(expr, true); - } + public static void validateExpression(String cronExpression) throws ParseException { - public static CronExpression createWithoutSeconds(final String expr) { - return new CronExpression(expr, false); + new CronExpression(cronExpression); } - public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime) { - // will search for the next time within the next 4 years. If there is no - // time matching, an InvalidArgumentException will be thrown (it is very - // likely that the cron expression is invalid, like the February 30th). - return nextTimeAfter(afterTime, afterTime.plusYears(4)); - } - public LocalDateTime nextLocalDateTimeAfter(LocalDateTime dateTime) { - return nextTimeAfter(ZonedDateTime.of(dateTime, ZoneId.systemDefault())).toLocalDateTime(); - } + //////////////////////////////////////////////////////////////////////////// + // + // Expression Parsing Functions + // + //////////////////////////////////////////////////////////////////////////// - public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime, long durationInMillis) { - // will search for the next time within the next durationInMillis - // millisecond. Be aware that the duration is specified in millis, - // but in fact the limit is checked on a day-to-day basis. - return nextTimeAfter(afterTime, afterTime.plus(Duration.ofMillis(durationInMillis))); - } + protected void buildExpression(String expression) throws ParseException { + expressionParsed = true; - public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime, ZonedDateTime dateTimeBarrier) { - ZonedDateTime[] nextDateTime = { afterTime.plusSeconds(1).withNano(0) }; + try { - while (true) { - checkIfDateTimeBarrierIsReached(nextDateTime[0], dateTimeBarrier); - if (!monthField.nextMatch(nextDateTime)) { - continue; + if (seconds == null) { + seconds = new TreeSet<Integer>(); } - if (!findDay(nextDateTime, dateTimeBarrier)) { - continue; + if (minutes == null) { + minutes = new TreeSet<Integer>(); } - if (!hourField.nextMatch(nextDateTime)) { - continue; + if (hours == null) { + hours = new TreeSet<Integer>(); } - if (!minuteField.nextMatch(nextDateTime)) { - continue; + if (daysOfMonth == null) { + daysOfMonth = new TreeSet<Integer>(); } - if (!secondField.nextMatch(nextDateTime)) { - continue; + if (months == null) { + months = new TreeSet<Integer>(); + } + if (daysOfWeek == null) { + daysOfWeek = new TreeSet<Integer>(); + } + if (years == null) { + years = new TreeSet<Integer>(); } - checkIfDateTimeBarrierIsReached(nextDateTime[0], dateTimeBarrier); - return nextDateTime[0]; - } - } + int exprOn = SECOND; - /** - * Find the next match for the day field. - * <p> - * This is handled different than all other fields because there are two ways to describe the day and it is easier - * to handle them together in the same method. - * - * @param dateTime Initial {@link ZonedDateTime} instance to start from - * @param dateTimeBarrier At which point stop searching for next execution time - * @return {@code true} if a match was found for this field or {@code false} if the field overflowed - * @see {@link SimpleField#nextMatch(ZonedDateTime[])} - */ - private boolean findDay(ZonedDateTime[] dateTime, ZonedDateTime dateTimeBarrier) { - int month = dateTime[0].getMonthValue(); + StringTokenizer exprsTok = new StringTokenizer(expression, " \t", + false); + + while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { + String expr = exprsTok.nextToken().trim(); - while (!(dayOfMonthField.matches(dateTime[0].toLocalDate()) - && dayOfWeekField.matches(dateTime[0].toLocalDate()))) { - dateTime[0] = dateTime[0].plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); - if (dateTime[0].getMonthValue() != month) { - return false; + // throw an exception if L is used with other days of the month + if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); + } + // throw an exception if L is used with other days of the week + if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); + } + if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) { + throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); + } + + StringTokenizer vTok = new StringTokenizer(expr, ","); + while (vTok.hasMoreTokens()) { + String v = vTok.nextToken(); + storeExpressionVals(0, v, exprOn); + } + + exprOn++; } - } - return true; - } - private static void checkIfDateTimeBarrierIsReached(ZonedDateTime nextTime, ZonedDateTime dateTimeBarrier) { - if (nextTime.isAfter(dateTimeBarrier)) { - throw new IllegalArgumentException("No next execution time could be determined that is before the limit of " + dateTimeBarrier); - } - } + if (exprOn <= DAY_OF_WEEK) { + throw new ParseException("Unexpected end of expression.", + expression.length()); + } - public String getExpr() { - return expr; - } + if (exprOn <= YEAR) { + storeExpressionVals(0, "*", YEAR); + } - @Override - public String toString() { - return getClass().getSimpleName() + "<" + expr + ">"; - } + TreeSet<Integer> dow = getSet(DAY_OF_WEEK); + TreeSet<Integer> dom = getSet(DAY_OF_MONTH); - static class FieldPart implements Comparable<FieldPart> { - private int from = -1, to = -1, increment = -1; - private String modifier, incrementModifier; + // Copying the logic from the UnsupportedOperationException below + boolean dayOfMSpec = !dom.contains(NO_SPEC); + boolean dayOfWSpec = !dow.contains(NO_SPEC); - @Override - public int compareTo(FieldPart o) { - return Integer.compare(from, o.from); + if (!dayOfMSpec || dayOfWSpec) { + if (!dayOfWSpec || dayOfMSpec) { + throw new ParseException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); + } + } + } catch (ParseException pe) { + throw pe; + } catch (Exception e) { + throw new ParseException("Illegal cron expression format (" + + e.toString() + ")", 0); } } - abstract static class BasicField { - private static final Pattern CRON_FIELD_REGEXP = Pattern - .compile("(?: # start of group 1\n" - + " (?:(?<all>\\*)|(?<ignore>\\?)|(?<last>L)) # global flag (L, ?, *)\n" - + " | (?<start>[0-9]{1,2}|[a-z]{3,3}) # or start number or symbol\n" - + " (?: # start of group 2\n" - + " (?<mod>L|W) # modifier (L,W)\n" - + " | -(?<end>[0-9]{1,2}|[a-z]{3,3}) # or end nummer or symbol (in range)\n" - + " )? # end of group 2\n" - + ") # end of group 1\n" - + "(?:(?<incmod>/|\\#)(?<inc>[0-9]{1,7}))? # increment and increment modifier (/ or \\#)\n", - Pattern.CASE_INSENSITIVE | Pattern.COMMENTS); + protected int storeExpressionVals(int pos, String s, int type) + throws ParseException { - final CronFieldType fieldType; - final List<FieldPart> parts = new ArrayList<>(); + int incr = 0; + int i = skipWhiteSpace(pos, s); + if (i >= s.length()) { + return i; + } + char c = s.charAt(i); + if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { + String sub = s.substring(i, i + 3); + int sval = -1; + int eval = -1; + if (type == MONTH) { + sval = getMonthNumber(sub) + 1; + if (sval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getMonthNumber(sub) + 1; + if (eval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + } + } + } else if (type == DAY_OF_WEEK) { + sval = getDayOfWeekNumber(sub); + if (sval < 0) { + throw new ParseException("Invalid Day-of-Week value: '" + + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getDayOfWeekNumber(sub); + if (eval < 0) { + throw new ParseException( + "Invalid Day-of-Week value: '" + sub + + "'", i); + } + } else if (c == '#') { + try { + i += 4; + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + } else if (c == 'L') { + lastdayOfWeek = true; + i++; + } + } - private BasicField(CronFieldType fieldType, String fieldExpr) { - this.fieldType = fieldType; - parse(fieldExpr); + } else { + throw new ParseException( + "Illegal characters for this position: '" + sub + "'", + i); + } + if (eval != -1) { + incr = 1; + } + addToSet(sval, eval, incr, type); + return (i + 3); } - private void parse(String fieldExpr) { // NOSONAR - String[] rangeParts = fieldExpr.split(","); - for (String rangePart : rangeParts) { - Matcher m = CRON_FIELD_REGEXP.matcher(rangePart); - if (!m.matches()) { - throw new IllegalArgumentException("Invalid cron field '" + rangePart + "' for field [" + fieldType + "]"); + if (c == '?') { + i++; + if ((i + 1) < s.length() + && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { + throw new ParseException("Illegal character after '?': " + + s.charAt(i), i); + } + if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { + throw new ParseException( + "'?' can only be specfied for Day-of-Month or Day-of-Week.", + i); + } + if (type == DAY_OF_WEEK && !lastdayOfMonth) { + int val = daysOfMonth.last(); + if (val == NO_SPEC_INT) { + throw new ParseException( + "'?' can only be specfied for Day-of-Month -OR- Day-of-Week.", + i); } - String startNummer = m.group("start"); - String modifier = m.group("mod"); - String sluttNummer = m.group("end"); - String incrementModifier = m.group("incmod"); - String increment = m.group("inc"); - - FieldPart part = new FieldPart(); - part.increment = 999; - if (startNummer != null) { - part.from = mapValue(startNummer); - part.modifier = modifier; - if (sluttNummer != null) { - part.to = mapValue(sluttNummer); - part.increment = 1; - } else if (increment != null) { - part.to = fieldType.to; - } else { - part.to = part.from; - } - } else if (m.group("all") != null) { - part.from = fieldType.from; - part.to = fieldType.to; - part.increment = 1; - } else if (m.group("ignore") != null) { - part.modifier = m.group("ignore"); - } else if (m.group("last") != null) { - part.modifier = m.group("last"); - } else { - throw new IllegalArgumentException("Invalid cron part: " + rangePart); + } + + addToSet(NO_SPEC_INT, -1, 0, type); + return i; + } + + if (c == '*' || c == '/') { + if (c == '*' && (i + 1) >= s.length()) { + addToSet(ALL_SPEC_INT, -1, incr, type); + return i + 1; + } else if (c == '/' + && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s + .charAt(i + 1) == '\t')) { + throw new ParseException("'/' must be followed by an integer.", i); + } else if (c == '*') { + i++; + } + c = s.charAt(i); + if (c == '/') { // is an increment specified? + i++; + if (i >= s.length()) { + throw new ParseException("Unexpected end of string.", i); } - if (increment != null) { - part.incrementModifier = incrementModifier; - part.increment = Integer.parseInt(increment); + incr = getNumericValue(s, i); + + i++; + if (incr > 10) { + i++; + } + if (incr > 59 && (type == SECOND || type == MINUTE)) { + throw new ParseException("Increment > 60 : " + incr, i); + } else if (incr > 23 && (type == HOUR)) { + throw new ParseException("Increment > 24 : " + incr, i); + } else if (incr > 31 && (type == DAY_OF_MONTH)) { + throw new ParseException("Increment > 31 : " + incr, i); + } else if (incr > 7 && (type == DAY_OF_WEEK)) { + throw new ParseException("Increment > 7 : " + incr, i); + } else if (incr > 12 && (type == MONTH)) { + throw new ParseException("Increment > 12 : " + incr, i); } + } else { + incr = 1; + } - validateRange(part); - validatePart(part); - parts.add(part); + addToSet(ALL_SPEC_INT, -1, incr, type); + return i; + } else if (c == 'L') { + i++; + if (type == DAY_OF_MONTH) { + lastdayOfMonth = true; + } + if (type == DAY_OF_WEEK) { + addToSet(7, 7, 0, type); + } + if(type == DAY_OF_MONTH && s.length() > i) { + c = s.charAt(i); + if(c == '-') { + ValueSet vs = getValue(0, s, i+1); + lastdayOffset = vs.value; + if(lastdayOffset > 30) + throw new ParseException("Offset from last day must be <= 30", i+1); + i = vs.pos; + } + if(s.length() > i) { + c = s.charAt(i); + if(c == 'W') { + nearestWeekday = true; + i++; + } + } + } + return i; + } else if (c >= '0' && c <= '9') { + int val = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, -1, -1, type); + } else { + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(val, s, i); + val = vs.value; + i = vs.pos; + } + i = checkNext(i, s, val, type); + return i; } + } else { + throw new ParseException("Unexpected character: " + c, i); + } + + return i; + } + + protected int checkNext(int pos, String s, int val, int type) + throws ParseException { + + int end = -1; + int i = pos; - Collections.sort(parts); + if (i >= s.length()) { + addToSet(val, end, -1, type); + return i; } - protected void validatePart(FieldPart part) { - if (part.modifier != null) { - throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier)); - } else if (part.incrementModifier != null && !"/".equals(part.incrementModifier)) { - throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier)); + char c = s.charAt(pos); + + if (c == 'L') { + if (type == DAY_OF_WEEK) { + if(val < 1 || val > 7) + throw new ParseException("Day-of-Week values must be between 1 and 7", -1); + lastdayOfWeek = true; + } else { + throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); } + TreeSet<Integer> set = getSet(type); + set.add(val); + i++; + return i; } - private void validateRange(FieldPart part) { - if ((part.from != -1 && part.from < fieldType.from) || (part.to != -1 && part.to > fieldType.to)) { - throw new IllegalArgumentException(String.format("Invalid interval [%s-%s], must be %s<=_<=%s", part.from, part.to, fieldType.from, - fieldType.to)); - } else if (part.from != -1 && part.to != -1 && part.from > part.to) { - throw new IllegalArgumentException( - String.format( - "Invalid interval [%s-%s]. Rolling periods are not supported (ex. 5-1, only 1-5) since this won't give a deterministic result. Must be %s<=_<=%s", - part.from, part.to, fieldType.from, fieldType.to)); + if (c == 'W') { + if (type == DAY_OF_MONTH) { + nearestWeekday = true; + } else { + throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); } + if(val > 31) + throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); + TreeSet<Integer> set = getSet(type); + set.add(val); + i++; + return i; } - protected int mapValue(String value) { - int idx; - if (fieldType.names != null && (idx = fieldType.names.indexOf(value.toUpperCase(Locale.getDefault()))) >= 0) { - return idx + fieldType.from; + if (c == '#') { + if (type != DAY_OF_WEEK) { + throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); + } + i++; + try { + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); } - return Integer.parseInt(value); + + TreeSet<Integer> set = getSet(type); + set.add(val); + i++; + return i; } - protected boolean matches(int val, FieldPart part) { - if (val >= part.from && val <= part.to && (val - part.from) % part.increment == 0) { - return true; + if (c == '-') { + i++; + c = s.charAt(i); + int v = Integer.parseInt(String.valueOf(c)); + end = v; + i++; + if (i >= s.length()) { + addToSet(val, end, 1, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v, s, i); + end = vs.value; + i = vs.pos; + } + if (i < s.length() && ((c = s.charAt(i)) == '/')) { + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + addToSet(val, end, v2, type); + return i; + } + } else { + addToSet(val, end, 1, type); + return i; } - return false; } - protected int nextMatch(int val, FieldPart part) { - if (val > part.to) { - return -1; + if (c == '/') { + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; } - int nextPotential = Math.max(val, part.from); - if (part.increment == 1 || nextPotential == part.from) { - return nextPotential; + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + throw new ParseException("Unexpected character '" + c + "' after '/'", i); } + } + + addToSet(val, end, 0, type); + i++; + return i; + } + + public String getCronExpression() { + return cronExpression; + } + + public String getExpressionSummary() { + StringBuilder buf = new StringBuilder(); + + buf.append("seconds: "); + buf.append(getExpressionSetSummary(seconds)); + buf.append("\n"); + buf.append("minutes: "); + buf.append(getExpressionSetSummary(minutes)); + buf.append("\n"); + buf.append("hours: "); + buf.append(getExpressionSetSummary(hours)); + buf.append("\n"); + buf.append("daysOfMonth: "); + buf.append(getExpressionSetSummary(daysOfMonth)); + buf.append("\n"); + buf.append("months: "); + buf.append(getExpressionSetSummary(months)); + buf.append("\n"); + buf.append("daysOfWeek: "); + buf.append(getExpressionSetSummary(daysOfWeek)); + buf.append("\n"); + buf.append("lastdayOfWeek: "); + buf.append(lastdayOfWeek); + buf.append("\n"); + buf.append("nearestWeekday: "); + buf.append(nearestWeekday); + buf.append("\n"); + buf.append("NthDayOfWeek: "); + buf.append(nthdayOfWeek); + buf.append("\n"); + buf.append("lastdayOfMonth: "); + buf.append(lastdayOfMonth); + buf.append("\n"); + buf.append("years: "); + buf.append(getExpressionSetSummary(years)); + buf.append("\n"); + + return buf.toString(); + } + + protected String getExpressionSetSummary(java.util.Set<Integer> set) { - int remainder = ((nextPotential - part.from) % part.increment); - if (remainder != 0) { - nextPotential += part.increment - remainder; + if (set.contains(NO_SPEC)) { + return "?"; + } + if (set.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator<Integer> itr = set.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); } + buf.append(val); + first = false; + } - return nextPotential <= part.to ? nextPotential : -1; + return buf.toString(); + } + + protected String getExpressionSetSummary(java.util.ArrayList<Integer> list) { + + if (list.contains(NO_SPEC)) { + return "?"; } + if (list.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator<Integer> itr = list.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); } - static class SimpleField extends BasicField { - SimpleField(CronFieldType fieldType, String fieldExpr) { - super(fieldType, fieldExpr); + protected int skipWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) { + ; } - public boolean matches(int val) { - if (val >= fieldType.from && val <= fieldType.to) { - for (FieldPart part : parts) { - if (matches(val, part)) { - return true; - } - } + return i; + } + + protected int findNextWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) { + ; + } + + return i; + } + + protected void addToSet(int val, int end, int incr, int type) + throws ParseException { + + TreeSet<Integer> set = getSet(type); + + if (type == SECOND || type == MINUTE) { + if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Minute and Second values must be between 0 and 59", + -1); + } + } else if (type == HOUR) { + if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Hour values must be between 0 and 23", -1); + } + } else if (type == DAY_OF_MONTH) { + if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day of month values must be between 1 and 31", -1); + } + } else if (type == MONTH) { + if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Month values must be between 1 and 12", -1); + } + } else if (type == DAY_OF_WEEK) { + if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day-of-Week values must be between 1 and 7", -1); } - return false; } - /** - * Find the next match for this field. If a match cannot be found force an overflow and increase the next - * greatest field. - * - * @param dateTime {@link ZonedDateTime} array so the reference can be modified - * @return {@code true} if a match was found for this field or {@code false} if the field overflowed - */ - protected boolean nextMatch(ZonedDateTime[] dateTime) { - int value = fieldType.getValue(dateTime[0]); - - for (FieldPart part : parts) { - int nextMatch = nextMatch(value, part); - if (nextMatch > -1) { - if (nextMatch != value) { - dateTime[0] = fieldType.setValue(dateTime[0], nextMatch); - } - return true; - } + if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { + if (val != -1) { + set.add(val); + } else { + set.add(NO_SPEC); } - dateTime[0] = fieldType.overflow(dateTime[0]); - return false; + return; } - } - static class DayOfWeekField extends BasicField { + int startAt = val; + int stopAt = end; - DayOfWeekField(String fieldExpr) { - super(CronFieldType.DAY_OF_WEEK, fieldExpr); + if (val == ALL_SPEC_INT && incr <= 0) { + incr = 1; + set.add(ALL_SPEC); // put in a marker, but also fill values } - boolean matches(LocalDate dato) { - for (FieldPart part : parts) { - if ("L".equals(part.modifier)) { - YearMonth ym = YearMonth.of(dato.getYear(), dato.getMonth().getValue()); - return dato.getDayOfWeek() == DayOfWeek.of(part.from) && dato.getDayOfMonth() > (ym.lengthOfMonth() - 7); - } else if ("#".equals(part.incrementModifier)) { - if (dato.getDayOfWeek() == DayOfWeek.of(part.from)) { - int num = dato.getDayOfMonth() / 7; - return part.increment == (dato.getDayOfMonth() % 7 == 0 ? num : num + 1); - } - return false; - } else if (matches(dato.getDayOfWeek().getValue(), part)) { - return true; - } + if (type == SECOND || type == MINUTE) { + if (stopAt == -1) { + stopAt = 59; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == HOUR) { + if (stopAt == -1) { + stopAt = 23; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == DAY_OF_MONTH) { + if (stopAt == -1) { + stopAt = 31; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == MONTH) { + if (stopAt == -1) { + stopAt = 12; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == DAY_OF_WEEK) { + if (stopAt == -1) { + stopAt = 7; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == YEAR) { + if (stopAt == -1) { + stopAt = MAX_YEAR; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1970; } - return false; } - @Override - protected int mapValue(String value) { - // Use 1-7 for weedays, but 0 will also represent sunday (linux practice) - return "0".equals(value) ? 7 : super.mapValue(value); + // if the end of the range is before the start, then we need to overflow into + // the next day, month etc. This is done by adding the maximum amount for that + // type, and using modulus max to determine the value being added. + int max = -1; + if (stopAt < startAt) { + switch (type) { + case SECOND : max = 60; break; + case MINUTE : max = 60; break; + case HOUR : max = 24; break; + case MONTH : max = 12; break; + case DAY_OF_WEEK : max = 7; break; + case DAY_OF_MONTH : max = 31; break; + case YEAR : throw new IllegalArgumentException("Start year must be less than stop year"); + default : throw new IllegalArgumentException("Unexpected type encountered"); + } + stopAt += max; } - @Override - protected boolean matches(int val, FieldPart part) { - return "?".equals(part.modifier) || super.matches(val, part); + for (int i = startAt; i <= stopAt; i += incr) { + if (max == -1) { + // ie: there's no max to overflow over + set.add(i); + } else { + // take the modulus to get the real value + int i2 = i % max; + + // 1-indexed ranges should not include 0, and should include their max + if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) { + i2 = max; + } + + set.add(i2); + } } + } + + TreeSet<Integer> getSet(int type) { + switch (type) { + case SECOND: + return seconds; + case MINUTE: + return minutes; + case HOUR: + return hours; + case DAY_OF_MONTH: + return daysOfMonth; + case MONTH: + return months; + case DAY_OF_WEEK: + return daysOfWeek; + case YEAR: + return years; + default: + return null; + } + } - @Override - protected void validatePart(FieldPart part) { - if (part.modifier != null && Arrays.asList("L", "?").indexOf(part.modifier) == -1) { - throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier)); - } else if (part.incrementModifier != null && Arrays.asList("/", "#").indexOf(part.incrementModifier) == -1) { - throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier)); + protected ValueSet getValue(int v, String s, int i) { + char c = s.charAt(i); + StringBuilder s1 = new StringBuilder(String.valueOf(v)); + while (c >= '0' && c <= '9') { + s1.append(c); + i++; + if (i >= s.length()) { + break; } + c = s.charAt(i); + } + ValueSet val = new ValueSet(); + + val.pos = (i < s.length()) ? i : i + 1; + val.value = Integer.parseInt(s1.toString()); + return val; + } + + protected int getNumericValue(String s, int i) { + int endOfVal = findNextWhiteSpace(i, s); + String val = s.substring(i, endOfVal); + return Integer.parseInt(val); + } + + protected int getMonthNumber(String s) { + Integer integer = monthMap.get(s); + + if (integer == null) { + return -1; } + + return integer; } - static class DayOfMonthField extends BasicField { - DayOfMonthField(String fieldExpr) { - super(CronFieldType.DAY_OF_MONTH, fieldExpr); + protected int getDayOfWeekNumber(String s) { + Integer integer = dayMap.get(s); + + if (integer == null) { + return -1; } - boolean matches(LocalDate dato) { - for (FieldPart part : parts) { - if ("L".equals(part.modifier)) { - YearMonth ym = YearMonth.of(dato.getYear(), dato.getMonth().getValue()); - return dato.getDayOfMonth() == (ym.lengthOfMonth() - (part.from == -1 ? 0 : part.from)); - } else if ("W".equals(part.modifier)) { - if (dato.getDayOfWeek().getValue() <= 5) { - if (dato.getDayOfMonth() == part.from) { - return true; - } else if (dato.getDayOfWeek().getValue() == 5) { - return dato.plusDays(1).getDayOfMonth() == part.from; - } else if (dato.getDayOfWeek().getValue() == 1) { - return dato.minusDays(1).getDayOfMonth() == part.from; + return integer; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Computation Functions + // + //////////////////////////////////////////////////////////////////////////// + + public Date getTimeAfter(Date afterTime) { + + // Computation is based on Gregorian year only. + Calendar cl = new java.util.GregorianCalendar(getTimeZone()); + + // move ahead one second, since we're computing the time *after* the + // given time + afterTime = new Date(afterTime.getTime() + 1000); + // CronTrigger does not deal with milliseconds + cl.setTime(afterTime); + cl.set(Calendar.MILLISECOND, 0); + + boolean gotOne = false; + // loop until we've computed the next time, or we've past the endTime + while (!gotOne) { + + //if (endTime != null && cl.getTime().after(endTime)) return null; + if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop... + return null; + } + + SortedSet<Integer> st = null; + int t = 0; + + int sec = cl.get(Calendar.SECOND); + int min = cl.get(Calendar.MINUTE); + + // get second................................................. + st = seconds.tailSet(sec); + if (st != null && st.size() != 0) { + sec = st.first(); + } else { + sec = seconds.first(); + min++; + cl.set(Calendar.MINUTE, min); + } + cl.set(Calendar.SECOND, sec); + + min = cl.get(Calendar.MINUTE); + int hr = cl.get(Calendar.HOUR_OF_DAY); + t = -1; + + // get minute................................................. + st = minutes.tailSet(min); + if (st != null && st.size() != 0) { + t = min; + min = st.first(); + } else { + min = minutes.first(); + hr++; + } + if (min != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, min); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.MINUTE, min); + + hr = cl.get(Calendar.HOUR_OF_DAY); + int day = cl.get(Calendar.DAY_OF_MONTH); + t = -1; + + // get hour................................................... + st = hours.tailSet(hr); + if (st != null && st.size() != 0) { + t = hr; + hr = st.first(); + } else { + hr = hours.first(); + day++; + } + if (hr != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.HOUR_OF_DAY, hr); + + day = cl.get(Calendar.DAY_OF_MONTH); + int mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + t = -1; + int tmon = mon; + + // get day................................................... + boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); + boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); + if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule + st = daysOfMonth.tailSet(day); + if (lastdayOfMonth) { + if(!nearestWeekday) { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + if(t > day) { + mon++; + if(mon > 12) { + mon = 1; + tmon = 3333; // ensure test of mon != tmon further below fails + cl.add(Calendar.YEAR, 1); + } + day = 1; + } + } else { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + + java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if(dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if(dow == Calendar.SATURDAY) { + day -= 1; + } else if(dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if(dow == Calendar.SUNDAY) { + day += 1; + } + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if(nTime.before(afterTime)) { + day = 1; + mon++; } } - } else if (matches(dato.getDayOfMonth(), part)) { - return true; + } else if(nearestWeekday) { + t = day; + day = daysOfMonth.first(); + + java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if(dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if(dow == Calendar.SATURDAY) { + day -= 1; + } else if(dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if(dow == Calendar.SUNDAY) { + day += 1; + } + + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if(nTime.before(afterTime)) { + day = daysOfMonth.first(); + mon++; + } + } else if (st != null && st.size() != 0) { + t = day; + day = st.first(); + // make sure we don't over-run a short month, such as february + int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + if (day > lastDay) { + day = daysOfMonth.first(); + mon++; + } + } else { + day = daysOfMonth.first(); + mon++; + } + + if (day != t || mon != tmon) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we + // are 1-based + continue; } + } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule + if (lastdayOfWeek) { // are we looking for the last XXX day of + // the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // did we already miss the + // last one? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + + // find date of last occurrence of this day in this month... + while ((day + daysToAdd + 7) <= lDay) { + daysToAdd += 7; + } + + day += daysToAdd; + + if (daysToAdd > 0) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are not promoting the month + continue; + } + + } else if (nthdayOfWeek != 0) { + // are we looking for the Nth XXX day in the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } else if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + boolean dayShifted = false; + if (daysToAdd > 0) { + dayShifted = true; + } + + day += daysToAdd; + int weekOfMonth = day / 7; + if (day % 7 > 0) { + weekOfMonth++; + } + + daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; + day += daysToAdd; + if (daysToAdd < 0 + || day > getLastDayOfMonth(mon, cl + .get(Calendar.YEAR))) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0 || dayShifted) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are NOT promoting the month + continue; + } + } else { + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int dow = daysOfWeek.first(); // desired + // d-o-w + st = daysOfWeek.tailSet(cDow); + if (st != null && st.size() > 0) { + dow = st.first(); + } + + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // will we pass the end of + // the month? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0) { // are we swithing days? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, + // and we are 1-based + continue; + } + } + } else { // dayOfWSpec && !dayOfMSpec + throw new UnsupportedOperationException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); + } + cl.set(Calendar.DAY_OF_MONTH, day); + + mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + int year = cl.get(Calendar.YEAR); + t = -1; + + // test for expressions that never generate a valid fire date, + // but keep looping... + if (year > MAX_YEAR) { + return null; + } + + // get month................................................... + st = months.tailSet(mon); + if (st != null && st.size() != 0) { + t = mon; + mon = st.first(); + } else { + mon = months.first(); + year++; + } + if (mon != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + + year = cl.get(Calendar.YEAR); + t = -1; + + // get year................................................... + st = years.tailSet(year); + if (st != null && st.size() != 0) { + t = year; + year = st.first(); + } else { + return null; // ran out of years... } - return false; - } - @Override - protected void validatePart(FieldPart part) { - if (part.modifier != null && Arrays.asList("L", "W", "?").indexOf(part.modifier) == -1) { - throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier)); - } else if (part.incrementModifier != null && !"/".equals(part.incrementModifier)) { - throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier)); + if (year != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, 0); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; } + cl.set(Calendar.YEAR, year); + + gotOne = true; + } // while( !done ) + + return cl.getTime(); + } + + /** + * Advance the calendar to the particular hour paying particular attention + * to daylight saving problems. + * + * @param cal the calendar to operate on + * @param hour the hour to set + */ + protected void setCalendarHour(Calendar cal, int hour) { + cal.set(java.util.Calendar.HOUR_OF_DAY, hour); + if (cal.get(java.util.Calendar.HOUR_OF_DAY) != hour && hour != 24) { + cal.set(java.util.Calendar.HOUR_OF_DAY, hour + 1); } + } + + public Date getTimeBefore(Date endTime) { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + public Date getFinalFireTime() { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + protected boolean isLeapYear(int year) { + return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); + } - @Override - protected boolean matches(int val, FieldPart part) { - return "?".equals(part.modifier) || super.matches(val, part); + protected int getLastDayOfMonth(int monthNum, int year) { + + switch (monthNum) { + case 1: + return 31; + case 2: + return (isLeapYear(year)) ? 29 : 28; + case 3: + return 31; + case 4: + return 30; + case 5: + return 31; + case 6: + return 30; + case 7: + return 31; + case 8: + return 31; + case 9: + return 30; + case 10: + return 31; + case 11: + return 30; + case 12: + return 31; + default: + throw new IllegalArgumentException("Illegal month number: " + + monthNum); } } -} \ No newline at end of file + + + private void readObject(java.io.ObjectInputStream stream) + throws java.io.IOException, ClassNotFoundException { + + stream.defaultReadObject(); + try { + buildExpression(cronExpression); + } catch (Exception ignore) { + } // never happens + } + + @Override + @Deprecated + public Object clone() { + return new CronExpression(this); + } +} + +@SuppressWarnings("VisibilityModifier") +class ValueSet { + public int value; + + public int pos; +} diff --git a/redisson/src/main/java/org/redisson/executor/CronExpressionEx.java b/redisson/src/main/java/org/redisson/executor/CronExpressionEx.java deleted file mode 100644 index b04db7bf5..000000000 --- a/redisson/src/main/java/org/redisson/executor/CronExpressionEx.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2013-2022 Nikita Koksharov - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.redisson.executor; - -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.NavigableSet; -import java.util.TreeSet; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -public class CronExpressionEx extends CronExpression { - - private static final ThreadLocal<NavigableSet<Integer>> YEARS_FIELD = new ThreadLocal<>(); - - private final NavigableSet<Integer> years; - - public CronExpressionEx(String expr) { - super(parseYear(expr)); - years = YEARS_FIELD.get(); - YEARS_FIELD.remove(); - } - - public CronExpressionEx(String expr, boolean withSeconds) { - super(parseYear(expr), withSeconds); - years = YEARS_FIELD.get(); - YEARS_FIELD.remove(); - } - - @Override - public ZonedDateTime nextTimeAfter(ZonedDateTime afterTime) { - if (years != null) { - afterTime = afterTime.withYear(years.ceiling(afterTime.getYear())); - } - return super.nextTimeAfter(afterTime); - } - - private static String parseYear(String expr) { - String[] parts = expr.split("\\s+"); - if (parts.length == 7) { - String year = parts[6]; - String[] years = year.split(","); - if (years.length > 1) { - NavigableSet<Integer> yy = new TreeSet<>(Arrays.stream(years).map(y -> Integer.valueOf(y)) - .collect(Collectors.toList())); - YEARS_FIELD.set(yy); - } - String[] yearsRange = year.split("-"); - if (yearsRange.length > 1) { - NavigableSet<Integer> yy = new TreeSet<>(IntStream.rangeClosed(Integer.valueOf(yearsRange[0]), Integer.valueOf(yearsRange[1])) - .boxed().collect(Collectors.toList())); - YEARS_FIELD.set(yy); - } - return expr.replace(year, "").trim(); - } - return expr; - } -} diff --git a/redisson/src/main/java/org/redisson/executor/TasksRunnerService.java b/redisson/src/main/java/org/redisson/executor/TasksRunnerService.java index 5d241b348..d454ebbad 100644 --- a/redisson/src/main/java/org/redisson/executor/TasksRunnerService.java +++ b/redisson/src/main/java/org/redisson/executor/TasksRunnerService.java @@ -38,11 +38,10 @@ import org.redisson.remote.ResponseEntry; import java.io.ByteArrayInputStream; import java.io.ObjectInput; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; import java.util.Arrays; +import java.util.Date; import java.util.Map; +import java.util.TimeZone; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; @@ -137,13 +136,13 @@ public class TasksRunnerService implements RemoteExecutorService { @Override public void schedule(ScheduledCronExpressionParameters params) { - CronExpression expression = new CronExpressionEx(params.getCronExpression()); - ZonedDateTime currentDate = ZonedDateTime.of(LocalDateTime.now(), ZoneId.of(params.getTimezone())); - ZonedDateTime nextStartDate = expression.nextTimeAfter(currentDate); + CronExpression expression = new CronExpression(params.getCronExpression()); + expression.setTimeZone(TimeZone.getTimeZone(params.getTimezone())); + Date nextStartDate = expression.getNextValidTimeAfter(new Date()); RFuture<Void> future = null; if (nextStartDate != null) { RemoteExecutorServiceAsync service = asyncScheduledServiceAtFixed(params.getExecutorId(), params.getRequestId()); - params.setStartTime(nextStartDate.toInstant().toEpochMilli()); + params.setStartTime(nextStartDate.getTime()); future = service.schedule(params); } try {