View Javadoc
1   /*
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
4    *
5    * This program and the accompanying materials are dual-licensed under
6    * either the terms of the Eclipse Public License v2.0 as published by
7    * the Eclipse Foundation
8    *
9    *   or (per the licensee's choosing)
10   *
11   * under the terms of the GNU Lesser General Public License version 2.1 as published by
12   * the Free Software Foundation.
13   */
14  package ch.qos.logback.core.rolling.helper;
15  
16  import static ch.qos.logback.core.CoreConstants.MILLIS_IN_ONE_HOUR;
17  import static ch.qos.logback.core.CoreConstants.MILLIS_IN_ONE_MINUTE;
18  import static ch.qos.logback.core.CoreConstants.MILLIS_IN_ONE_SECOND;
19  import static ch.qos.logback.core.CoreConstants.MILLIS_IN_ONE_WEEK;
20  import static ch.qos.logback.core.CoreConstants.MILLIS_IN_ONE_DAY;
21  
22  import java.text.SimpleDateFormat;
23  import java.time.Instant;
24  import java.time.ZoneId;
25  import java.time.format.DateTimeFormatter;
26  import java.util.Calendar;
27  import java.util.Date;
28  import java.util.GregorianCalendar;
29  import java.util.Locale;
30  import java.util.TimeZone;
31  
32  import ch.qos.logback.core.spi.ContextAwareBase;
33  
34  /**
35   * RollingCalendar is a helper class to
36   * {@link ch.qos.logback.core.rolling.TimeBasedRollingPolicy } or similar
37   * timed-based rolling policies. Given a periodicity type and the current time,
38   * it computes the start of the next interval (i.e. the triggering date).
39   *
40   * @author Ceki Gülcü
41   */
42  public class RollingCalendar extends GregorianCalendar {
43  
44      private static final long serialVersionUID = -5937537740925066161L;
45  
46      // The gmtTimeZone is used only in computeCheckPeriod() method.
47      static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT");
48  
49      PeriodicityType periodicityType = PeriodicityType.ERRONEOUS;
50      String datePattern;
51  
52      public RollingCalendar(String datePattern) {
53          super();
54          this.datePattern = datePattern;
55          this.periodicityType = computePeriodicityType();
56      }
57  
58      public RollingCalendar(String datePattern, TimeZone tz, Locale locale) {
59          super(tz, locale);
60          this.datePattern = datePattern;
61          this.periodicityType = computePeriodicityType();
62      }
63  
64      public PeriodicityType getPeriodicityType() {
65          return periodicityType;
66      }
67  
68      // This method computes the roll-over period by looping over the
69      // periods, starting with the shortest, and stopping when the r0 is
70      // different from r1, where r0 is the epoch formatted according
71      // the datePattern (supplied by the user) and r1 is the
72      // epoch+nextMillis(i) formatted according to datePattern. All date
73      // formatting is done in GMT and not local format because the test
74      // logic is based on comparisons relative to 1970-01-01 00:00:00
75      // GMT (the epoch).
76      public PeriodicityType computePeriodicityType() {
77  
78          GregorianCalendar calendar = new GregorianCalendar(GMT_TIMEZONE, Locale.getDefault());
79  
80          // set sate to 1970-01-01 00:00:00 GMT
81          Instant epoch = Instant.ofEpochMilli(0);
82          if (datePattern != null) {
83              for (PeriodicityType i : PeriodicityType.VALID_ORDERED_LIST) {
84                  SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern, Locale.getDefault());
85                  simpleDateFormat.setTimeZone(GMT_TIMEZONE);
86  
87                  String r0 = simpleDateFormat.format(new java.util.Date(0));
88  
89                  Instant next = innerGetEndOfThisPeriod(calendar, i, epoch);
90                  String r1 = simpleDateFormat.format(new java.util.Date(next.toEpochMilli()));
91  
92                  // System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);
93                  if ((r0 != null) && (r1 != null) && !r0.equals(r1)) {
94                      return i;
95                  }
96              }
97          }
98          // we failed
99          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 }