001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache license, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the license for the specific language governing permissions and 015 * limitations under the license. 016 */ 017package org.apache.logging.log4j.core.appender.rolling; 018 019import java.text.SimpleDateFormat; 020import java.util.ArrayList; 021import java.util.Calendar; 022import java.util.Date; 023import java.util.List; 024 025import org.apache.logging.log4j.Logger; 026import org.apache.logging.log4j.core.LogEvent; 027import org.apache.logging.log4j.core.impl.Log4jLogEvent; 028import org.apache.logging.log4j.core.lookup.StrSubstitutor; 029import org.apache.logging.log4j.core.pattern.ArrayPatternConverter; 030import org.apache.logging.log4j.core.pattern.DatePatternConverter; 031import org.apache.logging.log4j.core.pattern.FormattingInfo; 032import org.apache.logging.log4j.core.pattern.PatternConverter; 033import org.apache.logging.log4j.core.pattern.PatternParser; 034import org.apache.logging.log4j.status.StatusLogger; 035 036/** 037 * Parses the rollover pattern. 038 */ 039public class PatternProcessor { 040 041 protected static final Logger LOGGER = StatusLogger.getLogger(); 042 private static final String KEY = "FileConverter"; 043 044 private static final char YEAR_CHAR = 'y'; 045 private static final char MONTH_CHAR = 'M'; 046 private static final char[] WEEK_CHARS = {'w', 'W'}; 047 private static final char[] DAY_CHARS = {'D', 'd', 'F', 'E'}; 048 private static final char[] HOUR_CHARS = {'H', 'K', 'h', 'k'}; 049 private static final char MINUTE_CHAR = 'm'; 050 private static final char SECOND_CHAR = 's'; 051 private static final char MILLIS_CHAR = 'S'; 052 053 private final ArrayPatternConverter[] patternConverters; 054 private final FormattingInfo[] patternFields; 055 056 private long prevFileTime = 0; 057 private long nextFileTime = 0; 058 private long currentFileTime = 0; 059 060 private RolloverFrequency frequency = null; 061 062 private final String pattern; 063 064 public String getPattern() { 065 return pattern; 066 } 067 068 @Override 069 public String toString() { 070 return pattern; 071 } 072 073 /** 074 * Constructor. 075 * @param pattern The file pattern. 076 */ 077 public PatternProcessor(final String pattern) { 078 this.pattern = pattern; 079 final PatternParser parser = createPatternParser(); 080 final List<PatternConverter> converters = new ArrayList<>(); 081 final List<FormattingInfo> fields = new ArrayList<>(); 082 parser.parse(pattern, converters, fields, false, false, false); 083 final FormattingInfo[] infoArray = new FormattingInfo[fields.size()]; 084 patternFields = fields.toArray(infoArray); 085 final ArrayPatternConverter[] converterArray = new ArrayPatternConverter[converters.size()]; 086 patternConverters = converters.toArray(converterArray); 087 088 for (final ArrayPatternConverter converter : patternConverters) { 089 if (converter instanceof DatePatternConverter) { 090 final DatePatternConverter dateConverter = (DatePatternConverter) converter; 091 frequency = calculateFrequency(dateConverter.getPattern()); 092 } 093 } 094 } 095 096 /** 097 * Copy constructor with another pattern as source. 098 * 099 * @param pattern The file pattern. 100 * @param copy Source pattern processor 101 */ 102 public PatternProcessor(final String pattern, final PatternProcessor copy) { 103 this(pattern); 104 this.prevFileTime = copy.prevFileTime; 105 this.nextFileTime = copy.nextFileTime; 106 this.currentFileTime = copy.currentFileTime; 107 } 108 109 public long getCurrentFileTime() { 110 return currentFileTime; 111 } 112 113 public void setCurrentFileTime(final long currentFileTime) { 114 this.currentFileTime = currentFileTime; 115 } 116 117 public long getPrevFileTime() { 118 return prevFileTime; 119 } 120 121 public void setPrevFileTime(final long prevFileTime) { 122 LOGGER.debug("Setting prev file time to {}", new Date(prevFileTime)); 123 this.prevFileTime = prevFileTime; 124 } 125 126 /** 127 * Returns the next potential rollover time. 128 * @param currentMillis The current time. 129 * @param increment The increment to the next time. 130 * @param modulus If true the time will be rounded to occur on a boundary aligned with the increment. 131 * @return the next potential rollover time and the timestamp for the target file. 132 */ 133 public long getNextTime(final long currentMillis, final int increment, final boolean modulus) { 134 // 135 // https://issues.apache.org/jira/browse/LOG4J2-1232 136 // Call setMinimalDaysInFirstWeek(7); 137 // 138 prevFileTime = nextFileTime; 139 long nextTime; 140 141 if (frequency == null) { 142 throw new IllegalStateException("Pattern does not contain a date"); 143 } 144 final Calendar currentCal = Calendar.getInstance(); 145 currentCal.setTimeInMillis(currentMillis); 146 final Calendar cal = Calendar.getInstance(); 147 currentCal.setMinimalDaysInFirstWeek(7); 148 cal.setMinimalDaysInFirstWeek(7); 149 cal.set(currentCal.get(Calendar.YEAR), 0, 1, 0, 0, 0); 150 cal.set(Calendar.MILLISECOND, 0); 151 if (frequency == RolloverFrequency.ANNUALLY) { 152 increment(cal, Calendar.YEAR, increment, modulus); 153 nextTime = cal.getTimeInMillis(); 154 cal.add(Calendar.YEAR, -1); 155 nextFileTime = cal.getTimeInMillis(); 156 return debugGetNextTime(nextTime); 157 } 158 cal.set(Calendar.MONTH, currentCal.get(Calendar.MONTH)); 159 if (frequency == RolloverFrequency.MONTHLY) { 160 increment(cal, Calendar.MONTH, increment, modulus); 161 nextTime = cal.getTimeInMillis(); 162 cal.add(Calendar.MONTH, -1); 163 nextFileTime = cal.getTimeInMillis(); 164 return debugGetNextTime(nextTime); 165 } 166 if (frequency == RolloverFrequency.WEEKLY) { 167 cal.set(Calendar.WEEK_OF_YEAR, currentCal.get(Calendar.WEEK_OF_YEAR)); 168 increment(cal, Calendar.WEEK_OF_YEAR, increment, modulus); 169 cal.set(Calendar.DAY_OF_WEEK, currentCal.getFirstDayOfWeek()); 170 nextTime = cal.getTimeInMillis(); 171 cal.add(Calendar.WEEK_OF_YEAR, -1); 172 nextFileTime = cal.getTimeInMillis(); 173 return debugGetNextTime(nextTime); 174 } 175 cal.set(Calendar.DAY_OF_YEAR, currentCal.get(Calendar.DAY_OF_YEAR)); 176 if (frequency == RolloverFrequency.DAILY) { 177 increment(cal, Calendar.DAY_OF_YEAR, increment, modulus); 178 nextTime = cal.getTimeInMillis(); 179 cal.add(Calendar.DAY_OF_YEAR, -1); 180 nextFileTime = cal.getTimeInMillis(); 181 return debugGetNextTime(nextTime); 182 } 183 cal.set(Calendar.HOUR_OF_DAY, currentCal.get(Calendar.HOUR_OF_DAY)); 184 if (frequency == RolloverFrequency.HOURLY) { 185 increment(cal, Calendar.HOUR_OF_DAY, increment, modulus); 186 nextTime = cal.getTimeInMillis(); 187 cal.add(Calendar.HOUR_OF_DAY, -1); 188 nextFileTime = cal.getTimeInMillis(); 189 return debugGetNextTime(nextTime); 190 } 191 cal.set(Calendar.MINUTE, currentCal.get(Calendar.MINUTE)); 192 if (frequency == RolloverFrequency.EVERY_MINUTE) { 193 increment(cal, Calendar.MINUTE, increment, modulus); 194 nextTime = cal.getTimeInMillis(); 195 cal.add(Calendar.MINUTE, -1); 196 nextFileTime = cal.getTimeInMillis(); 197 return debugGetNextTime(nextTime); 198 } 199 cal.set(Calendar.SECOND, currentCal.get(Calendar.SECOND)); 200 if (frequency == RolloverFrequency.EVERY_SECOND) { 201 increment(cal, Calendar.SECOND, increment, modulus); 202 nextTime = cal.getTimeInMillis(); 203 cal.add(Calendar.SECOND, -1); 204 nextFileTime = cal.getTimeInMillis(); 205 return debugGetNextTime(nextTime); 206 } 207 cal.set(Calendar.MILLISECOND, currentCal.get(Calendar.MILLISECOND)); 208 increment(cal, Calendar.MILLISECOND, increment, modulus); 209 nextTime = cal.getTimeInMillis(); 210 cal.add(Calendar.MILLISECOND, -1); 211 nextFileTime = cal.getTimeInMillis(); 212 return debugGetNextTime(nextTime); 213 } 214 215 public void updateTime() { 216 if (nextFileTime != 0) { 217 prevFileTime = nextFileTime; 218 } 219 } 220 221 private long debugGetNextTime(final long nextTime) { 222 if (LOGGER.isTraceEnabled()) { 223 LOGGER.trace("PatternProcessor.getNextTime returning {}, nextFileTime={}, prevFileTime={}, current={}, freq={}", // 224 format(nextTime), format(nextFileTime), format(prevFileTime), format(System.currentTimeMillis()), frequency); 225 } 226 return nextTime; 227 } 228 229 private String format(final long time) { 230 return new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss.SSS").format(new Date(time)); 231 } 232 233 private void increment(final Calendar cal, final int type, final int increment, final boolean modulate) { 234 final int interval = modulate ? increment - (cal.get(type) % increment) : increment; 235 cal.add(type, interval); 236 } 237 238 /** 239 * Format file name. 240 * @param buf string buffer to which formatted file name is appended, may not be null. 241 * @param obj object to be evaluated in formatting, may not be null. 242 */ 243 public final void formatFileName(final StringBuilder buf, final boolean useCurrentTime, final Object obj) { 244 long time = useCurrentTime ? currentFileTime : prevFileTime; 245 if (time == 0) { 246 time = System.currentTimeMillis(); 247 } 248 formatFileName(buf, new Date(time), obj); 249 } 250 251 /** 252 * Formats file name. 253 * @param subst The StrSubstitutor. 254 * @param buf string buffer to which formatted file name is appended, may not be null. 255 * @param obj object to be evaluated in formatting, may not be null. 256 */ 257 public final void formatFileName(final StrSubstitutor subst, final StringBuilder buf, final Object obj) { 258 formatFileName(subst, buf, false, obj); 259 } 260 261 /** 262 * Formats file name. 263 * @param subst The StrSubstitutor. 264 * @param buf string buffer to which formatted file name is appended, may not be null. 265 * @param obj object to be evaluated in formatting, may not be null. 266 */ 267 public final void formatFileName(final StrSubstitutor subst, final StringBuilder buf, final boolean useCurrentTime, 268 final Object obj) { 269 // LOG4J2-628: we deliberately use System time, not the log4j.Clock time 270 // for creating the file name of rolled-over files. 271 LOGGER.debug("Formatting file name. useCurrentTime={}. currentFileTime={}, prevFileTime={}", 272 useCurrentTime, currentFileTime, prevFileTime); 273 final long time = useCurrentTime ? currentFileTime != 0 ? currentFileTime : System.currentTimeMillis() : 274 prevFileTime != 0 ? prevFileTime : System.currentTimeMillis(); 275 formatFileName(buf, new Date(time), obj); 276 final LogEvent event = new Log4jLogEvent.Builder().setTimeMillis(time).build(); 277 final String fileName = subst.replace(event, buf); 278 buf.setLength(0); 279 buf.append(fileName); 280 } 281 282 /** 283 * Formats file name. 284 * @param buf string buffer to which formatted file name is appended, may not be null. 285 * @param objects objects to be evaluated in formatting, may not be null. 286 */ 287 protected final void formatFileName(final StringBuilder buf, final Object... objects) { 288 for (int i = 0; i < patternConverters.length; i++) { 289 final int fieldStart = buf.length(); 290 patternConverters[i].format(buf, objects); 291 292 if (patternFields[i] != null) { 293 patternFields[i].format(fieldStart, buf); 294 } 295 } 296 } 297 298 private RolloverFrequency calculateFrequency(final String pattern) { 299 if (patternContains(pattern, MILLIS_CHAR)) { 300 return RolloverFrequency.EVERY_MILLISECOND; 301 } 302 if (patternContains(pattern, SECOND_CHAR)) { 303 return RolloverFrequency.EVERY_SECOND; 304 } 305 if (patternContains(pattern, MINUTE_CHAR)) { 306 return RolloverFrequency.EVERY_MINUTE; 307 } 308 if (patternContains(pattern, HOUR_CHARS)) { 309 return RolloverFrequency.HOURLY; 310 } 311 if (patternContains(pattern, DAY_CHARS)) { 312 return RolloverFrequency.DAILY; 313 } 314 if (patternContains(pattern, WEEK_CHARS)) { 315 return RolloverFrequency.WEEKLY; 316 } 317 if (patternContains(pattern, MONTH_CHAR)) { 318 return RolloverFrequency.MONTHLY; 319 } 320 if (patternContains(pattern, YEAR_CHAR)) { 321 return RolloverFrequency.ANNUALLY; 322 } 323 return null; 324 } 325 326 private PatternParser createPatternParser() { 327 328 return new PatternParser(null, KEY, null); 329 } 330 331 private boolean patternContains(final String pattern, final char... chars) { 332 for (final char character : chars) { 333 if (patternContains(pattern, character)) { 334 return true; 335 } 336 } 337 return false; 338 } 339 340 private boolean patternContains(final String pattern, final char character) { 341 return pattern.indexOf(character) >= 0; 342 } 343 344 public RolloverFrequency getFrequency() { 345 return frequency; 346 } 347 348 public long getNextFileTime() { 349 return nextFileTime; 350 } 351 352}