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 TOP_OF_DAY:
109            // EE or uu
110            if (collision(7 * MILLIS_IN_ONE_DAY))
111                return false;
112            // isolated dd
113            if (collision(31 * MILLIS_IN_ONE_DAY))
114                return false;
115            // DD
116            if (collision(365 * MILLIS_IN_ONE_DAY))
117                return false;
118            return true;
119        case TOP_OF_WEEK:
120            // WW
121            if (collision(34 * MILLIS_IN_ONE_DAY))
122                return false;
123            // isolated ww
124            if (collision(366 * MILLIS_IN_ONE_DAY))
125                return false;
126            return true;
127        default:
128            return true;
129        }
130    }
131
132    private boolean collision(long delta) {
133        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
134        simpleDateFormat.setTimeZone(GMT_TIMEZONE); // all date formatting done in GMT
135        Date epoch0 = new Date(0);
136        String r0 = simpleDateFormat.format(epoch0);
137        Date epoch12 = new Date(delta);
138        String r12 = simpleDateFormat.format(epoch12);
139
140        return r0.equals(r12);
141    }
142
143    public void printPeriodicity(ContextAwareBase cab) {
144        switch (periodicityType) {
145        case TOP_OF_MILLISECOND:
146            cab.addInfo("Roll-over every millisecond.");
147            break;
148
149        case TOP_OF_SECOND:
150            cab.addInfo("Roll-over every second.");
151            break;
152
153        case TOP_OF_MINUTE:
154            cab.addInfo("Roll-over every minute.");
155            break;
156
157        case TOP_OF_HOUR:
158            cab.addInfo("Roll-over at the top of every hour.");
159            break;
160
161        case HALF_DAY:
162            cab.addInfo("Roll-over at midday and midnight.");
163            break;
164
165        case TOP_OF_DAY:
166            cab.addInfo("Roll-over at midnight.");
167            break;
168
169        case TOP_OF_WEEK:
170            cab.addInfo("Rollover at the start of week.");
171            break;
172
173        case TOP_OF_MONTH:
174            cab.addInfo("Rollover at start of every month.");
175            break;
176
177        default:
178            cab.addInfo("Unknown periodicity.");
179        }
180    }
181
182    public long periodBarriersCrossed(long start, long end) {
183        if (start > end)
184            throw new IllegalArgumentException("Start cannot come before end");
185
186        long startFloored = getStartOfCurrentPeriodWithGMTOffsetCorrection(start, getTimeZone());
187        long endFloored = getStartOfCurrentPeriodWithGMTOffsetCorrection(end, getTimeZone());
188
189        long diff = endFloored - startFloored;
190
191        switch (periodicityType) {
192
193        case TOP_OF_MILLISECOND:
194            return diff;
195        case TOP_OF_SECOND:
196            return diff / MILLIS_IN_ONE_SECOND;
197        case TOP_OF_MINUTE:
198            return diff / MILLIS_IN_ONE_MINUTE;
199        case TOP_OF_HOUR:
200            return diff / MILLIS_IN_ONE_HOUR;
201        case TOP_OF_DAY:
202            return diff / MILLIS_IN_ONE_DAY;
203        case TOP_OF_WEEK:
204            return diff / MILLIS_IN_ONE_WEEK;
205        case TOP_OF_MONTH:
206            return diffInMonths(start, end);
207        default:
208            throw new IllegalStateException("Unknown periodicity type.");
209        }
210    }
211
212    public static int diffInMonths(long startTime, long endTime) {
213        if (startTime > endTime)
214            throw new IllegalArgumentException("startTime cannot be larger than endTime");
215        Calendar startCal = Calendar.getInstance();
216        startCal.setTimeInMillis(startTime);
217        Calendar endCal = Calendar.getInstance();
218        endCal.setTimeInMillis(endTime);
219        int yearDiff = endCal.get(Calendar.YEAR) - startCal.get(Calendar.YEAR);
220        int monthDiff = endCal.get(Calendar.MONTH) - startCal.get(Calendar.MONTH);
221        return yearDiff * 12 + monthDiff;
222    }
223
224    static private Instant innerGetEndOfThisPeriod(Calendar cal, PeriodicityType periodicityType, Instant instant) {
225        return innerGetEndOfNextNthPeriod(cal, periodicityType, instant, 1);
226    }
227
228    static private Instant innerGetEndOfNextNthPeriod(Calendar cal, PeriodicityType periodicityType, Instant instant,
229            int numPeriods) {
230        cal.setTimeInMillis(instant.toEpochMilli());
231        switch (periodicityType) {
232        case TOP_OF_MILLISECOND:
233            cal.add(Calendar.MILLISECOND, numPeriods);
234            break;
235
236        case TOP_OF_SECOND:
237            cal.set(Calendar.MILLISECOND, 0);
238            cal.add(Calendar.SECOND, numPeriods);
239            break;
240
241        case TOP_OF_MINUTE:
242            cal.set(Calendar.SECOND, 0);
243            cal.set(Calendar.MILLISECOND, 0);
244            cal.add(Calendar.MINUTE, numPeriods);
245            break;
246
247        case TOP_OF_HOUR:
248            cal.set(Calendar.MINUTE, 0);
249            cal.set(Calendar.SECOND, 0);
250            cal.set(Calendar.MILLISECOND, 0);
251            cal.add(Calendar.HOUR_OF_DAY, numPeriods);
252            break;
253
254        case TOP_OF_DAY:
255            cal.set(Calendar.HOUR_OF_DAY, 0);
256            cal.set(Calendar.MINUTE, 0);
257            cal.set(Calendar.SECOND, 0);
258            cal.set(Calendar.MILLISECOND, 0);
259            cal.add(Calendar.DATE, numPeriods);
260            break;
261
262        case TOP_OF_WEEK:
263            cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
264            cal.set(Calendar.HOUR_OF_DAY, 0);
265            cal.set(Calendar.MINUTE, 0);
266            cal.set(Calendar.SECOND, 0);
267            cal.set(Calendar.MILLISECOND, 0);
268            cal.add(Calendar.WEEK_OF_YEAR, numPeriods);
269            break;
270
271        case TOP_OF_MONTH:
272            cal.set(Calendar.DATE, 1);
273            cal.set(Calendar.HOUR_OF_DAY, 0);
274            cal.set(Calendar.MINUTE, 0);
275            cal.set(Calendar.SECOND, 0);
276            cal.set(Calendar.MILLISECOND, 0);
277            cal.add(Calendar.MONTH, numPeriods);
278            break;
279
280        default:
281            throw new IllegalStateException("Unknown periodicity type.");
282        }
283
284        return Instant.ofEpochMilli(cal.getTimeInMillis());
285    }
286
287    public Instant getEndOfNextNthPeriod(Instant instant, int periods) {
288        return innerGetEndOfNextNthPeriod(this, this.periodicityType, instant, periods);
289    }
290
291    public Instant getNextTriggeringDate(Instant instant) {
292        return getEndOfNextNthPeriod(instant, 1);
293    }
294
295    public long getStartOfCurrentPeriodWithGMTOffsetCorrection(long now, TimeZone timezone) {
296        Instant toppedInstant;
297
298        // there is a bug in Calendar which prevents it from
299        // computing the correct DST_OFFSET when the time changes
300        {
301            Calendar aCal = Calendar.getInstance(timezone);
302            aCal.setTimeInMillis(now);
303            Instant instant = Instant.ofEpochMilli(aCal.getTimeInMillis());
304            toppedInstant = getEndOfNextNthPeriod(instant, 0);
305        }
306        Calendar secondCalendar = Calendar.getInstance(timezone);
307        secondCalendar.setTimeInMillis(toppedInstant.toEpochMilli());
308        long gmtOffset = secondCalendar.get(Calendar.ZONE_OFFSET) + secondCalendar.get(Calendar.DST_OFFSET);
309        return toppedInstant.toEpochMilli() + gmtOffset;
310    }
311}