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}