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