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}