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