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