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 TOP_OF_DAY: 109 // EE or uu 110 if (collision(7 * MILLIS_IN_ONE_DAY)) 111 return false; 112 // isolated dd 113 if (collision(31 * MILLIS_IN_ONE_DAY)) 114 return false; 115 // DD 116 if (collision(365 * MILLIS_IN_ONE_DAY)) 117 return false; 118 return true; 119 case TOP_OF_WEEK: 120 // WW 121 if (collision(34 * MILLIS_IN_ONE_DAY)) 122 return false; 123 // isolated ww 124 if (collision(366 * MILLIS_IN_ONE_DAY)) 125 return false; 126 return true; 127 default: 128 return true; 129 } 130 } 131 132 private boolean collision(long delta) { 133 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern); 134 simpleDateFormat.setTimeZone(GMT_TIMEZONE); // all date formatting done in GMT 135 Date epoch0 = new Date(0); 136 String r0 = simpleDateFormat.format(epoch0); 137 Date epoch12 = new Date(delta); 138 String r12 = simpleDateFormat.format(epoch12); 139 140 return r0.equals(r12); 141 } 142 143 public void printPeriodicity(ContextAwareBase cab) { 144 switch (periodicityType) { 145 case TOP_OF_MILLISECOND: 146 cab.addInfo("Roll-over every millisecond."); 147 break; 148 149 case TOP_OF_SECOND: 150 cab.addInfo("Roll-over every second."); 151 break; 152 153 case TOP_OF_MINUTE: 154 cab.addInfo("Roll-over every minute."); 155 break; 156 157 case TOP_OF_HOUR: 158 cab.addInfo("Roll-over at the top of every hour."); 159 break; 160 161 case HALF_DAY: 162 cab.addInfo("Roll-over at midday and midnight."); 163 break; 164 165 case TOP_OF_DAY: 166 cab.addInfo("Roll-over at midnight."); 167 break; 168 169 case TOP_OF_WEEK: 170 cab.addInfo("Rollover at the start of week."); 171 break; 172 173 case TOP_OF_MONTH: 174 cab.addInfo("Rollover at start of every month."); 175 break; 176 177 default: 178 cab.addInfo("Unknown periodicity."); 179 } 180 } 181 182 public long periodBarriersCrossed(long start, long end) { 183 if (start > end) 184 throw new IllegalArgumentException("Start cannot come before end"); 185 186 long startFloored = getStartOfCurrentPeriodWithGMTOffsetCorrection(start, getTimeZone()); 187 long endFloored = getStartOfCurrentPeriodWithGMTOffsetCorrection(end, getTimeZone()); 188 189 long diff = endFloored - startFloored; 190 191 switch (periodicityType) { 192 193 case TOP_OF_MILLISECOND: 194 return diff; 195 case TOP_OF_SECOND: 196 return diff / MILLIS_IN_ONE_SECOND; 197 case TOP_OF_MINUTE: 198 return diff / MILLIS_IN_ONE_MINUTE; 199 case TOP_OF_HOUR: 200 return diff / MILLIS_IN_ONE_HOUR; 201 case TOP_OF_DAY: 202 return diff / MILLIS_IN_ONE_DAY; 203 case TOP_OF_WEEK: 204 return diff / MILLIS_IN_ONE_WEEK; 205 case TOP_OF_MONTH: 206 return diffInMonths(start, end); 207 default: 208 throw new IllegalStateException("Unknown periodicity type."); 209 } 210 } 211 212 public static int diffInMonths(long startTime, long endTime) { 213 if (startTime > endTime) 214 throw new IllegalArgumentException("startTime cannot be larger than endTime"); 215 Calendar startCal = Calendar.getInstance(); 216 startCal.setTimeInMillis(startTime); 217 Calendar endCal = Calendar.getInstance(); 218 endCal.setTimeInMillis(endTime); 219 int yearDiff = endCal.get(Calendar.YEAR) - startCal.get(Calendar.YEAR); 220 int monthDiff = endCal.get(Calendar.MONTH) - startCal.get(Calendar.MONTH); 221 return yearDiff * 12 + monthDiff; 222 } 223 224 static private Instant innerGetEndOfThisPeriod(Calendar cal, PeriodicityType periodicityType, Instant instant) { 225 return innerGetEndOfNextNthPeriod(cal, periodicityType, instant, 1); 226 } 227 228 static private Instant innerGetEndOfNextNthPeriod(Calendar cal, PeriodicityType periodicityType, Instant instant, 229 int numPeriods) { 230 cal.setTimeInMillis(instant.toEpochMilli()); 231 switch (periodicityType) { 232 case TOP_OF_MILLISECOND: 233 cal.add(Calendar.MILLISECOND, numPeriods); 234 break; 235 236 case TOP_OF_SECOND: 237 cal.set(Calendar.MILLISECOND, 0); 238 cal.add(Calendar.SECOND, numPeriods); 239 break; 240 241 case TOP_OF_MINUTE: 242 cal.set(Calendar.SECOND, 0); 243 cal.set(Calendar.MILLISECOND, 0); 244 cal.add(Calendar.MINUTE, numPeriods); 245 break; 246 247 case TOP_OF_HOUR: 248 cal.set(Calendar.MINUTE, 0); 249 cal.set(Calendar.SECOND, 0); 250 cal.set(Calendar.MILLISECOND, 0); 251 cal.add(Calendar.HOUR_OF_DAY, numPeriods); 252 break; 253 254 case TOP_OF_DAY: 255 cal.set(Calendar.HOUR_OF_DAY, 0); 256 cal.set(Calendar.MINUTE, 0); 257 cal.set(Calendar.SECOND, 0); 258 cal.set(Calendar.MILLISECOND, 0); 259 cal.add(Calendar.DATE, numPeriods); 260 break; 261 262 case TOP_OF_WEEK: 263 cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek()); 264 cal.set(Calendar.HOUR_OF_DAY, 0); 265 cal.set(Calendar.MINUTE, 0); 266 cal.set(Calendar.SECOND, 0); 267 cal.set(Calendar.MILLISECOND, 0); 268 cal.add(Calendar.WEEK_OF_YEAR, numPeriods); 269 break; 270 271 case TOP_OF_MONTH: 272 cal.set(Calendar.DATE, 1); 273 cal.set(Calendar.HOUR_OF_DAY, 0); 274 cal.set(Calendar.MINUTE, 0); 275 cal.set(Calendar.SECOND, 0); 276 cal.set(Calendar.MILLISECOND, 0); 277 cal.add(Calendar.MONTH, numPeriods); 278 break; 279 280 default: 281 throw new IllegalStateException("Unknown periodicity type."); 282 } 283 284 return Instant.ofEpochMilli(cal.getTimeInMillis()); 285 } 286 287 public Instant getEndOfNextNthPeriod(Instant instant, int periods) { 288 return innerGetEndOfNextNthPeriod(this, this.periodicityType, instant, periods); 289 } 290 291 public Instant getNextTriggeringDate(Instant instant) { 292 return getEndOfNextNthPeriod(instant, 1); 293 } 294 295 public long getStartOfCurrentPeriodWithGMTOffsetCorrection(long now, TimeZone timezone) { 296 Instant toppedInstant; 297 298 // there is a bug in Calendar which prevents it from 299 // computing the correct DST_OFFSET when the time changes 300 { 301 Calendar aCal = Calendar.getInstance(timezone); 302 aCal.setTimeInMillis(now); 303 Instant instant = Instant.ofEpochMilli(aCal.getTimeInMillis()); 304 toppedInstant = getEndOfNextNthPeriod(instant, 0); 305 } 306 Calendar secondCalendar = Calendar.getInstance(timezone); 307 secondCalendar.setTimeInMillis(toppedInstant.toEpochMilli()); 308 long gmtOffset = secondCalendar.get(Calendar.ZONE_OFFSET) + secondCalendar.get(Calendar.DST_OFFSET); 309 return toppedInstant.toEpochMilli() + gmtOffset; 310 } 311}