001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, 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 v1.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
012 * as published by 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        ZoneId gmtZone = ZoneId.of("UTC");
083        if (datePattern != null) {
084            for (PeriodicityType i : PeriodicityType.VALID_ORDERED_LIST) {
085                DateTimeFormatter dtf = DateTimeFormatter.ofPattern(datePattern).withZone(gmtZone);
086
087                //SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
088                //simpleDateFormat.setTimeZone(GMT_TIMEZONE); // all date formatting done in GMT
089
090                String r0 = dtf.format(epoch);
091
092                Instant next = innerGetEndOfThisPeriod(calendar, i, epoch);
093                String r1 = dtf.format(next);
094
095                // System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);
096                if ((r0 != null) && (r1 != null) && !r0.equals(r1)) {
097                    return i;
098                }
099            }
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}