From 4a90a53884880db9bc1f9b3a6ace39d3f6c176b0 Mon Sep 17 00:00:00 2001 From: Ben Fortuna Date: Tue, 6 Jul 2021 13:39:27 +1000 Subject: [PATCH] Use chronology to define allowable range of recurrence values --- build.gradle | 3 + .../net/fortuna/ical4j/model/NumberList.java | 67 +++++++++++++---- .../java/net/fortuna/ical4j/model/Recur.java | 72 +++++++++++++------ 3 files changed, 107 insertions(+), 35 deletions(-) diff --git a/build.gradle b/build.gradle index 907cff281..925faeb70 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,9 @@ dependencies { // optional timezone caching.. implementation 'javax.cache:cache-api:1.1.1', optional + // optional support for non-Gregorian chronologies.. + implementation "org.threeten:threeten-extra:$threetenExtraVersion", optional + // optional groovy DSL for calendar builder.. implementation "org.codehaus.groovy:groovy:$groovyVersion", optional implementation "org.codehaus.groovy:groovy-dateutil:$groovyVersion", optional diff --git a/src/main/java/net/fortuna/ical4j/model/NumberList.java b/src/main/java/net/fortuna/ical4j/model/NumberList.java index 5043a0d44..6fc17de36 100644 --- a/src/main/java/net/fortuna/ical4j/model/NumberList.java +++ b/src/main/java/net/fortuna/ical4j/model/NumberList.java @@ -34,8 +34,8 @@ import net.fortuna.ical4j.util.Numbers; import java.io.Serializable; -import java.util.ArrayList; -import java.util.StringTokenizer; +import java.time.temporal.ValueRange; +import java.util.*; import java.util.stream.Collectors; /** @@ -49,9 +49,7 @@ public class NumberList extends ArrayList implements Serializable { private static final long serialVersionUID = -1667481795613729889L; - private final int minValue; - - private final int maxValue; + private final ValueRange valueRange; private final boolean allowsNegativeValues; @@ -59,7 +57,17 @@ public class NumberList extends ArrayList implements Serializable { * Default constructor. */ public NumberList() { - this(Integer.MIN_VALUE, Integer.MAX_VALUE, true); + this(ValueRange.of(Integer.MIN_VALUE, Integer.MAX_VALUE), true); + } + + /** + * Construct a number list restricted by the specified {@link ValueRange}. + * @param valueRange a range defining the lower and upper bounds of allowed values + * @param allowsNegativeValues allow negative values, where abs(value) is within the specified range + */ + public NumberList(ValueRange valueRange, boolean allowsNegativeValues) { + this.valueRange = valueRange; + this.allowsNegativeValues = allowsNegativeValues; } /** @@ -67,11 +75,12 @@ public NumberList() { * @param minValue the minimum allowable value * @param maxValue the maximum allowable value * @param allowsNegativeValues indicates whether negative values are allowed + * + * @deprecated use {@link NumberList#NumberList(ValueRange, boolean)} */ + @Deprecated public NumberList(int minValue, int maxValue, boolean allowsNegativeValues) { - this.minValue = minValue; - this.maxValue = maxValue; - this.allowsNegativeValues = allowsNegativeValues; + this(ValueRange.of(minValue, maxValue), allowsNegativeValues); } /** @@ -79,15 +88,29 @@ public NumberList(int minValue, int maxValue, boolean allowsNegativeValues) { * @param aString a string representation of a number list */ public NumberList(final String aString) { - this(aString, Integer.MIN_VALUE, Integer.MAX_VALUE, true); + this(aString, ValueRange.of(Integer.MIN_VALUE, Integer.MAX_VALUE), true); } - + + /** + * Construct a number list restricted by the specified {@link ValueRange}. + * @param aString a string representation of a list of values + * @param valueRange a range defining the lower and upper bounds of allowed values + * @param allowsNegativeValues allow negative values, where abs(value) is within the specified range + */ + public NumberList(final String aString, ValueRange valueRange, boolean allowsNegativeValues) { + this(valueRange, allowsNegativeValues); + addAll(Arrays.stream(aString.split(",")).map(Numbers::parseInt).collect(Collectors.toList())); + } + /** * @param aString a string representation of a number list * @param minValue the minimum allowable value * @param maxValue the maximum allowable value * @param allowsNegativeValues indicates whether negative values are allowed + * + * @deprecated use {@link NumberList#NumberList(String, ValueRange, boolean)} */ + @Deprecated public NumberList(final String aString, int minValue, int maxValue, boolean allowsNegativeValues) { this(minValue, maxValue, allowsNegativeValues); final StringTokenizer t = new StringTokenizer(aString, ","); @@ -110,13 +133,29 @@ public final boolean add(final Integer aNumber) { } abs = Math.abs(abs); } - if (abs < minValue || abs > maxValue) { - throw new IllegalArgumentException( - "Value not in range [" + minValue + ".." + maxValue + "]: " + aNumber); + if (!valueRange.isValidIntValue(abs)) { + throw new IllegalArgumentException("Value not in range [" + valueRange + "]: " + aNumber); } return super.add(aNumber); } + @Override + public boolean addAll(Collection c) { + Optional negativeValue = c.stream().filter(v -> (v >> 31 | -v >>> 31) < 0) + .findFirst(); + if (!allowsNegativeValues && negativeValue.isPresent()) { + throw new IllegalArgumentException("Negative value not allowed: " + negativeValue.get()); + } + + Optional invalidValue = c.stream().filter(v -> !valueRange.isValidValue(Math.abs(v))) + .findFirst(); + if (invalidValue.isPresent()) { + throw new IllegalArgumentException( + "Value not in range [" + valueRange + "]: " + invalidValue); + } + return super.addAll(c); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/net/fortuna/ical4j/model/Recur.java b/src/main/java/net/fortuna/ical4j/model/Recur.java index 963778c2a..c579d3470 100644 --- a/src/main/java/net/fortuna/ical4j/model/Recur.java +++ b/src/main/java/net/fortuna/ical4j/model/Recur.java @@ -43,7 +43,8 @@ import java.io.IOException; import java.io.Serializable; import java.text.ParseException; -import java.time.temporal.ValueRange; +import java.time.chrono.Chronology; +import java.time.temporal.ChronoField; import java.util.Calendar; import java.util.*; @@ -95,6 +96,30 @@ public enum Frequency { SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY; } + public enum RScale { + + JAPANESE("Japanese"), + BUDDHIST("ThaiBuddhist"), + ROC("Minguo"), + ISLAMIC("islamic"), + ISO8601("ISO"), + + CHINESE("ISO"), + ETHIOPIC("Ethiopic"), + HEBREW("ISO"), + GREGORIAN("ISO"); + + private final String chronology; + + RScale(String chronology) { + this.chronology = chronology; + } + + public String getChronology() { + return chronology; + } + } + public enum Skip { OMIT, BACKWARD, FORWARD; } @@ -170,7 +195,7 @@ public enum Skip { private Date until; - private String rscale; + private RScale rscale; private Integer count; @@ -225,6 +250,7 @@ public Recur(final String aValue) throws ParseException { // default week start is Monday per RFC5545 calendarWeekStartDay = Calendar.MONDAY; + Chronology chronology = Chronology.ofLocale(Locale.getDefault()); Iterator tokens = Arrays.asList(aValue.split("[;=]")).iterator(); while (tokens.hasNext()) { final String token = tokens.next(); @@ -233,7 +259,8 @@ public Recur(final String aValue) throws ParseException { } else if (SKIP.equals(token)) { skip = Skip.valueOf(nextToken(tokens, token)); } else if (RSCALE.equals(token)) { - rscale = nextToken(tokens, token); + rscale = RScale.valueOf(nextToken(tokens, token)); + chronology = Chronology.of(rscale.getChronology()); } else if (UNTIL.equals(token)) { final String untilString = nextToken(tokens, token); if (untilString != null && untilString.contains("T")) { @@ -248,23 +275,23 @@ public Recur(final String aValue) throws ParseException { } else if (INTERVAL.equals(token)) { interval = Integer.parseInt(nextToken(tokens, token)); } else if (BYSECOND.equals(token)) { - secondList = new NumberList(nextToken(tokens, token), 0, 59, false); + secondList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.SECOND_OF_MINUTE), false); } else if (BYMINUTE.equals(token)) { - minuteList = new NumberList(nextToken(tokens, token), 0, 59, false); + minuteList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.MINUTE_OF_HOUR), false); } else if (BYHOUR.equals(token)) { - hourList = new NumberList(nextToken(tokens, token), 0, 23, false); + hourList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.HOUR_OF_DAY), false); } else if (BYDAY.equals(token)) { dayList = new WeekDayList(nextToken(tokens, token)); } else if (BYMONTHDAY.equals(token)) { - monthDayList = new NumberList(nextToken(tokens, token), 1, 31, true); + monthDayList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.DAY_OF_MONTH), true); } else if (BYYEARDAY.equals(token)) { - yearDayList = new NumberList(nextToken(tokens, token), 1, 366, true); + yearDayList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.DAY_OF_YEAR), true); } else if (BYWEEKNO.equals(token)) { - weekNoList = new NumberList(nextToken(tokens, token), 1, 53, true); + weekNoList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.ALIGNED_WEEK_OF_YEAR), true); } else if (BYMONTH.equals(token)) { - monthList = new MonthList(nextToken(tokens, token), ValueRange.of(1, 12, 13)); + monthList = new MonthList(nextToken(tokens, token), chronology.range(ChronoField.MONTH_OF_YEAR)); } else if (BYSETPOS.equals(token)) { - setPosList = new NumberList(nextToken(tokens, token), 1, 366, true); + setPosList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.DAY_OF_YEAR), true); } else if (WKST.equals(token)) { weekStartDay = WeekDay.Day.valueOf(nextToken(tokens, token)); calendarWeekStartDay = WeekDay.getCalendarDay(WeekDay.getWeekDay(weekStartDay)); @@ -336,41 +363,43 @@ public Recur(final Frequency frequency, final int count) { private void initTransformers() { transformers = new HashMap<>(); + Chronology chronology = rscale != null ? Chronology.of(rscale.getChronology()) + : Chronology.ofLocale(Locale.getDefault()); if (secondList != null) { transformers.put(BYSECOND, new BySecondRule(secondList, frequency, Optional.ofNullable(weekStartDay))); } else { - secondList = new NumberList(0, 59, false); + secondList = new NumberList(chronology.range(ChronoField.SECOND_OF_MINUTE), false); } if (minuteList != null) { transformers.put(BYMINUTE, new ByMinuteRule(minuteList, frequency, Optional.ofNullable(weekStartDay))); } else { - minuteList = new NumberList(0, 59, false); + minuteList = new NumberList(chronology.range(ChronoField.MINUTE_OF_HOUR), false); } if (hourList != null) { transformers.put(BYHOUR, new ByHourRule(hourList, frequency, Optional.ofNullable(weekStartDay))); } else { - hourList = new NumberList(0, 23, false); + hourList = new NumberList(chronology.range(ChronoField.HOUR_OF_DAY), false); } if (monthDayList != null) { transformers.put(BYMONTHDAY, new ByMonthDayRule(monthDayList, frequency, Optional.ofNullable(weekStartDay))); } else { - monthDayList = new NumberList(1, 31, true); + monthDayList = new NumberList(chronology.range(ChronoField.DAY_OF_MONTH), true); } if (yearDayList != null) { transformers.put(BYYEARDAY, new ByYearDayRule(yearDayList, frequency, Optional.ofNullable(weekStartDay))); } else { - yearDayList = new NumberList(1, 366, true); + yearDayList = new NumberList(chronology.range(ChronoField.DAY_OF_YEAR), true); } if (weekNoList != null) { transformers.put(BYWEEKNO, new ByWeekNoRule(weekNoList, frequency, Optional.ofNullable(weekStartDay))); } else { - weekNoList = new NumberList(1, 53, true); + weekNoList = new NumberList(chronology.range(ChronoField.ALIGNED_WEEK_OF_YEAR), true); } if (monthList != null) { transformers.put(BYMONTH, new ByMonthRule(monthList, frequency, Optional.ofNullable(weekStartDay))); } else { - monthList = new MonthList(ValueRange.of(1, 12, 13)); + monthList = new MonthList(chronology.range(ChronoField.MONTH_OF_YEAR)); } if (dayList != null) { transformers.put(BYDAY, new ByDayRule(dayList, deriveFilterType(), Optional.ofNullable(weekStartDay))); @@ -380,7 +409,7 @@ private void initTransformers() { if (setPosList != null) { transformers.put(BYSETPOS, new BySetPosRule(setPosList)); } else { - setPosList = new NumberList(1, 366, true); + setPosList = new NumberList(chronology.range(ChronoField.DAY_OF_YEAR), true); } } @@ -1106,7 +1135,7 @@ public static class Builder { private Date until; - private String rscale; + private RScale rscale; private Integer count; @@ -1147,7 +1176,7 @@ public Builder until(Date until) { return this; } - public Builder rscale(String rscale) { + public Builder rscale(RScale rscale) { this.rscale = rscale; return this; } @@ -1215,6 +1244,7 @@ public Builder weekStartDay(WeekDay.Day weekStartDay) { public Recur build() { Recur recur = new Recur(); recur.frequency = frequency; + recur.rscale = rscale; recur.skip = skip; recur.until = until; recur.count = count;