001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v2.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1 as published by
012 * the Free Software Foundation.
013 */
014package ch.qos.logback.core.rolling.helper;
015
016import static ch.qos.logback.core.CoreConstants.MILLIS_IN_ONE_HOUR;
017import static ch.qos.logback.core.CoreConstants.MILLIS_IN_ONE_MINUTE;
018import static ch.qos.logback.core.CoreConstants.MILLIS_IN_ONE_SECOND;
019import static ch.qos.logback.core.CoreConstants.MILLIS_IN_ONE_WEEK;
020import static ch.qos.logback.core.CoreConstants.MILLIS_IN_ONE_DAY;
021
022import java.text.SimpleDateFormat;
023import java.time.Instant;
024import java.time.ZoneId;
025import java.time.format.DateTimeFormatter;
026import java.util.Calendar;
027import java.util.Date;
028import java.util.GregorianCalendar;
029import java.util.Locale;
030import java.util.TimeZone;
031
032import ch.qos.logback.core.spi.ContextAwareBase;
033
034/**
035 * RollingCalendar is a helper class to
036 * {@link ch.qos.logback.core.rolling.TimeBasedRollingPolicy } or similar
037 * timed-based rolling policies. Given a periodicity type and the current time,
038 * it computes the start of the next interval (i.e. the triggering date).
039 *
040 * @author Ceki Gülcü
041 */
042public class RollingCalendar extends GregorianCalendar {
043
044    private static final long serialVersionUID = -5937537740925066161L;
045
046    // The gmtTimeZone is used only in computeCheckPeriod() method.
047    static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT");
048
049    PeriodicityType periodicityType = PeriodicityType.ERRONEOUS;
050    String datePattern;
051
052    public RollingCalendar(String datePattern) {
053        super();
054        this.datePattern = datePattern;
055        this.periodicityType = computePeriodicityType();
056    }
057
058    public RollingCalendar(String datePattern, TimeZone tz, Locale locale) {
059        super(tz, locale);
060        this.datePattern = datePattern;
061        this.periodicityType = computePeriodicityType();
062    }
063
064    public PeriodicityType getPeriodicityType() {
065        return periodicityType;
066    }
067
068    // This method computes the roll-over period by looping over the
069    // periods, starting with the shortest, and stopping when the r0 is
070    // different from r1, where r0 is the epoch formatted according
071    // the datePattern (supplied by the user) and r1 is the
072    // epoch+nextMillis(i) formatted according to datePattern. All date
073    // formatting is done in GMT and not local format because the test
074    // logic is based on comparisons relative to 1970-01-01 00:00:00
075    // GMT (the epoch).
076    public PeriodicityType computePeriodicityType() {
077
078        GregorianCalendar calendar = new GregorianCalendar(GMT_TIMEZONE, Locale.getDefault());
079
080        // set sate to 1970-01-01 00:00:00 GMT
081        Instant epoch = Instant.ofEpochMilli(0);
082        if (datePattern != null) {
083            for (PeriodicityType i : PeriodicityType.VALID_ORDERED_LIST) {
084                SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern, Locale.getDefault());
085                simpleDateFormat.setTimeZone(GMT_TIMEZONE);
086
087                String r0 = simpleDateFormat.format(new java.util.Date(0));
088
089                Instant next = innerGetEndOfThisPeriod(calendar, i, epoch);
090                String r1 = simpleDateFormat.format(new java.util.Date(next.toEpochMilli()));
091
092                // System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);
093                if ((r0 != null) && (r1 != null) && !r0.equals(r1)) {
094                    return i;
095                }
096            }
097        }
098        // we failed
099        return PeriodicityType.ERRONEOUS;
100    }
101
102    public boolean isCollisionFree() {
103        switch (periodicityType) {
104        case TOP_OF_HOUR:
105            // isolated hh or KK
106            return !collision(12 * MILLIS_IN_ONE_HOUR);
107
108        case HALF_DAY:
109            // isolated 'a' (AM/PM) without a date component repeats every day
110            return !collision(MILLIS_IN_ONE_DAY);
111
112        case TOP_OF_DAY:
113            // EE or uu
114            if (collision(7 * MILLIS_IN_ONE_DAY))
115                return false;
116            // isolated dd
117            if (collision(31 * MILLIS_IN_ONE_DAY))
118                return false;
119            // DD
120            if (collision(365 * MILLIS_IN_ONE_DAY))
121                return false;
122            return true;
123        case TOP_OF_WEEK:
124            // WW
125            if (collision(34 * MILLIS_IN_ONE_DAY))
126                return false;
127            // isolated ww
128            if (collision(366 * MILLIS_IN_ONE_DAY))
129                return false;
130            return true;
131        default:
132            return true;
133        }
134    }
135
136    private boolean collision(long delta) {
137        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
138        simpleDateFormat.setTimeZone(GMT_TIMEZONE); // all date formatting done in GMT
139        Date epoch0 = new Date(0);
140        String r0 = simpleDateFormat.format(epoch0);
141        Date epoch12 = new Date(delta);
142        String r12 = simpleDateFormat.format(epoch12);
143
144        return r0.equals(r12);
145    }
146
147    public void printPeriodicity(ContextAwareBase cab) {
148        switch (periodicityType) {
149        case TOP_OF_MILLISECOND:
150            cab.addInfo("Roll-over every millisecond.");
151            break;
152
153        case TOP_OF_SECOND:
154            cab.addInfo("Roll-over every second.");
155            break;
156
157        case TOP_OF_MINUTE:
158            cab.addInfo("Roll-over every minute.");
159            break;
160
161        case TOP_OF_HOUR:
162            cab.addInfo("Roll-over at the top of every hour.");
163            break;
164
165        case HALF_DAY:
166            cab.addInfo("Roll-over at midday and midnight.");
167            break;
168
169        case TOP_OF_DAY:
170            cab.addInfo("Roll-over at midnight.");
171            break;
172
173        case TOP_OF_WEEK:
174            cab.addInfo("Rollover at the start of week.");
175            break;
176
177        case TOP_OF_MONTH:
178            cab.addInfo("Rollover at start of every month.");
179            break;
180
181        default:
182            cab.addInfo("Unknown periodicity.");
183        }
184    }
185
186    public long periodBarriersCrossed(long start, long end) {
187        if (start > end)
188            throw new IllegalArgumentException("Start cannot come before end");
189
190        long startFloored = getStartOfCurrentPeriodWithGMTOffsetCorrection(start, getTimeZone());
191        long endFloored = getStartOfCurrentPeriodWithGMTOffsetCorrection(end, getTimeZone());
192
193        long diff = endFloored - startFloored;
194
195        switch (periodicityType) {
196
197        case TOP_OF_MILLISECOND:
198            return diff;
199        case TOP_OF_SECOND:
200            return diff / MILLIS_IN_ONE_SECOND;
201        case TOP_OF_MINUTE:
202            return diff / MILLIS_IN_ONE_MINUTE;
203        case TOP_OF_HOUR:
204            return diff / MILLIS_IN_ONE_HOUR;
205        case HALF_DAY:
206            return diff / (12 * MILLIS_IN_ONE_HOUR);
207        case TOP_OF_DAY:
208            return diff / MILLIS_IN_ONE_DAY;
209        case TOP_OF_WEEK:
210            return diff / MILLIS_IN_ONE_WEEK;
211        case TOP_OF_MONTH:
212            return diffInMonths(start, end);
213        default:
214            throw new IllegalStateException("Unknown periodicity type.");
215        }
216    }
217
218    public static int diffInMonths(long startTime, long endTime) {
219        if (startTime > endTime)
220            throw new IllegalArgumentException("startTime cannot be larger than endTime");
221        Calendar startCal = Calendar.getInstance();
222        startCal.setTimeInMillis(startTime);
223        Calendar endCal = Calendar.getInstance();
224        endCal.setTimeInMillis(endTime);
225        int yearDiff = endCal.get(Calendar.YEAR) - startCal.get(Calendar.YEAR);
226        int monthDiff = endCal.get(Calendar.MONTH) - startCal.get(Calendar.MONTH);
227        return yearDiff * 12 + monthDiff;
228    }
229
230    static private Instant innerGetEndOfThisPeriod(Calendar cal, PeriodicityType periodicityType, Instant instant) {
231        return innerGetEndOfNextNthPeriod(cal, periodicityType, instant, 1);
232    }
233
234    static private Instant innerGetEndOfNextNthPeriod(Calendar cal, PeriodicityType periodicityType, Instant instant,
235            int numPeriods) {
236        cal.setTimeInMillis(instant.toEpochMilli());
237        switch (periodicityType) {
238        case TOP_OF_MILLISECOND:
239            cal.add(Calendar.MILLISECOND, numPeriods);
240            break;
241
242        case TOP_OF_SECOND:
243            cal.set(Calendar.MILLISECOND, 0);
244            cal.add(Calendar.SECOND, numPeriods);
245            break;
246
247        case TOP_OF_MINUTE:
248            cal.set(Calendar.SECOND, 0);
249            cal.set(Calendar.MILLISECOND, 0);
250            cal.add(Calendar.MINUTE, numPeriods);
251            break;
252
253        case TOP_OF_HOUR:
254            cal.set(Calendar.MINUTE, 0);
255            cal.set(Calendar.SECOND, 0);
256            cal.set(Calendar.MILLISECOND, 0);
257            cal.add(Calendar.HOUR_OF_DAY, numPeriods);
258            break;
259
260        case HALF_DAY:
261            // floor to the start of the current half-day (00:00 or 12:00), then advance
262            cal.set(Calendar.HOUR_OF_DAY, cal.get(Calendar.HOUR_OF_DAY) < 12 ? 0 : 12);
263            cal.set(Calendar.MINUTE, 0);
264            cal.set(Calendar.SECOND, 0);
265            cal.set(Calendar.MILLISECOND, 0);
266            cal.add(Calendar.HOUR_OF_DAY, numPeriods * 12);
267            break;
268
269        case TOP_OF_DAY:
270            cal.set(Calendar.HOUR_OF_DAY, 0);
271            cal.set(Calendar.MINUTE, 0);
272            cal.set(Calendar.SECOND, 0);
273            cal.set(Calendar.MILLISECOND, 0);
274            cal.add(Calendar.DATE, numPeriods);
275            break;
276
277        case TOP_OF_WEEK:
278            cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
279            cal.set(Calendar.HOUR_OF_DAY, 0);
280            cal.set(Calendar.MINUTE, 0);
281            cal.set(Calendar.SECOND, 0);
282            cal.set(Calendar.MILLISECOND, 0);
283            cal.add(Calendar.WEEK_OF_YEAR, numPeriods);
284            break;
285
286        case TOP_OF_MONTH:
287            cal.set(Calendar.DATE, 1);
288            cal.set(Calendar.HOUR_OF_DAY, 0);
289            cal.set(Calendar.MINUTE, 0);
290            cal.set(Calendar.SECOND, 0);
291            cal.set(Calendar.MILLISECOND, 0);
292            cal.add(Calendar.MONTH, numPeriods);
293            break;
294
295        default:
296            throw new IllegalStateException("Unknown periodicity type.");
297        }
298
299        return Instant.ofEpochMilli(cal.getTimeInMillis());
300    }
301
302    public Instant getEndOfNextNthPeriod(Instant instant, int periods) {
303        return innerGetEndOfNextNthPeriod(this, this.periodicityType, instant, periods);
304    }
305
306    public Instant getNextTriggeringDate(Instant instant) {
307        return getEndOfNextNthPeriod(instant, 1);
308    }
309
310    public long getStartOfCurrentPeriodWithGMTOffsetCorrection(long now, TimeZone timezone) {
311        Instant toppedInstant;
312
313        // there is a bug in Calendar which prevents it from
314        // computing the correct DST_OFFSET when the time changes
315        {
316            Calendar aCal = Calendar.getInstance(timezone);
317            aCal.setTimeInMillis(now);
318            Instant instant = Instant.ofEpochMilli(aCal.getTimeInMillis());
319            toppedInstant = getEndOfNextNthPeriod(instant, 0);
320        }
321        Calendar secondCalendar = Calendar.getInstance(timezone);
322        secondCalendar.setTimeInMillis(toppedInstant.toEpochMilli());
323        long gmtOffset = secondCalendar.get(Calendar.ZONE_OFFSET) + secondCalendar.get(Calendar.DST_OFFSET);
324        return toppedInstant.toEpochMilli() + gmtOffset;
325    }
326}