| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache license, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the license for the specific language governing permissions and |
| * limitations under the license. |
| */ |
| package org.apache.logging.log4j.core.appender.rolling; |
| |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.List; |
| |
| import org.apache.logging.log4j.Logger; |
| import org.apache.logging.log4j.core.LogEvent; |
| import org.apache.logging.log4j.core.impl.Log4jLogEvent; |
| import org.apache.logging.log4j.core.lookup.StrSubstitutor; |
| import org.apache.logging.log4j.core.pattern.ArrayPatternConverter; |
| import org.apache.logging.log4j.core.pattern.DatePatternConverter; |
| import org.apache.logging.log4j.core.pattern.FileDatePatternConverter; |
| import org.apache.logging.log4j.core.pattern.FormattingInfo; |
| import org.apache.logging.log4j.core.pattern.PatternConverter; |
| import org.apache.logging.log4j.core.pattern.PatternParser; |
| import org.apache.logging.log4j.status.StatusLogger; |
| |
| /** |
| * Parses the rollover pattern. |
| */ |
| public class PatternProcessor { |
| |
| protected static final Logger LOGGER = StatusLogger.getLogger(); |
| private static final String KEY = "FileConverter"; |
| |
| private static final char YEAR_CHAR = 'y'; |
| private static final char MONTH_CHAR = 'M'; |
| private static final char[] WEEK_CHARS = {'w', 'W'}; |
| private static final char[] DAY_CHARS = {'D', 'd', 'F', 'E'}; |
| private static final char[] HOUR_CHARS = {'H', 'K', 'h', 'k'}; |
| private static final char MINUTE_CHAR = 'm'; |
| private static final char SECOND_CHAR = 's'; |
| private static final char MILLIS_CHAR = 'S'; |
| |
| private final ArrayPatternConverter[] patternConverters; |
| private final FormattingInfo[] patternFields; |
| private final FileExtension fileExtension; |
| |
| private long prevFileTime = 0; |
| private long nextFileTime = 0; |
| private long currentFileTime = 0; |
| |
| private boolean isTimeBased = false; |
| |
| private RolloverFrequency frequency = null; |
| |
| private final String pattern; |
| |
| public String getPattern() { |
| return pattern; |
| } |
| |
| @Override |
| public String toString() { |
| return pattern; |
| } |
| |
| /** |
| * Constructor. |
| * @param pattern The file pattern. |
| */ |
| public PatternProcessor(final String pattern) { |
| this.pattern = pattern; |
| final PatternParser parser = createPatternParser(); |
| // FIXME: this seems to expect List<ArrayPatternConverter> in practice; types need to be fixed around this |
| final List<PatternConverter> converters = new ArrayList<>(); |
| final List<FormattingInfo> fields = new ArrayList<>(); |
| parser.parse(pattern, converters, fields, false, false, false); |
| final FormattingInfo[] infoArray = new FormattingInfo[fields.size()]; |
| patternFields = fields.toArray(infoArray); |
| final ArrayPatternConverter[] converterArray = new ArrayPatternConverter[converters.size()]; |
| patternConverters = converters.toArray(converterArray); |
| this.fileExtension = FileExtension.lookupForFile(pattern); |
| |
| for (final ArrayPatternConverter converter : patternConverters) { |
| // TODO: extract common interface |
| if (converter instanceof DatePatternConverter) { |
| final DatePatternConverter dateConverter = (DatePatternConverter) converter; |
| frequency = calculateFrequency(dateConverter.getPattern()); |
| } else if (converter instanceof FileDatePatternConverter) { |
| frequency = calculateFrequency(((FileDatePatternConverter) converter).getPattern()); |
| } |
| } |
| } |
| |
| /** |
| * Copy constructor with another pattern as source. |
| * |
| * @param pattern The file pattern. |
| * @param copy Source pattern processor |
| */ |
| public PatternProcessor(final String pattern, final PatternProcessor copy) { |
| this(pattern); |
| this.prevFileTime = copy.prevFileTime; |
| this.nextFileTime = copy.nextFileTime; |
| this.currentFileTime = copy.currentFileTime; |
| } |
| |
| public void setTimeBased(boolean isTimeBased) { |
| this.isTimeBased = isTimeBased; |
| } |
| |
| public long getCurrentFileTime() { |
| return currentFileTime; |
| } |
| |
| public void setCurrentFileTime(final long currentFileTime) { |
| this.currentFileTime = currentFileTime; |
| } |
| |
| public long getPrevFileTime() { |
| return prevFileTime; |
| } |
| |
| public void setPrevFileTime(final long prevFileTime) { |
| LOGGER.debug("Setting prev file time to {}", new Date(prevFileTime)); |
| this.prevFileTime = prevFileTime; |
| } |
| |
| public FileExtension getFileExtension() { |
| return fileExtension; |
| } |
| |
| /** |
| * Returns the next potential rollover time. |
| * @param currentMillis The current time. |
| * @param increment The increment to the next time. |
| * @param modulus If true the time will be rounded to occur on a boundary aligned with the increment. |
| * @return the next potential rollover time and the timestamp for the target file. |
| */ |
| public long getNextTime(final long currentMillis, final int increment, final boolean modulus) { |
| // |
| // https://issues.apache.org/jira/browse/LOG4J2-1232 |
| // Call setMinimalDaysInFirstWeek(7); |
| // |
| prevFileTime = nextFileTime; |
| long nextTime; |
| |
| if (frequency == null) { |
| throw new IllegalStateException("Pattern does not contain a date"); |
| } |
| final Calendar currentCal = Calendar.getInstance(); |
| currentCal.setTimeInMillis(currentMillis); |
| final Calendar cal = Calendar.getInstance(); |
| currentCal.setMinimalDaysInFirstWeek(7); |
| cal.setMinimalDaysInFirstWeek(7); |
| cal.set(currentCal.get(Calendar.YEAR), 0, 1, 0, 0, 0); |
| cal.set(Calendar.MILLISECOND, 0); |
| if (frequency == RolloverFrequency.ANNUALLY) { |
| increment(cal, Calendar.YEAR, increment, modulus); |
| nextTime = cal.getTimeInMillis(); |
| cal.add(Calendar.YEAR, -1); |
| nextFileTime = cal.getTimeInMillis(); |
| return debugGetNextTime(nextTime); |
| } |
| cal.set(Calendar.MONTH, currentCal.get(Calendar.MONTH)); |
| if (frequency == RolloverFrequency.MONTHLY) { |
| increment(cal, Calendar.MONTH, increment, modulus); |
| nextTime = cal.getTimeInMillis(); |
| cal.add(Calendar.MONTH, -1); |
| nextFileTime = cal.getTimeInMillis(); |
| return debugGetNextTime(nextTime); |
| } |
| if (frequency == RolloverFrequency.WEEKLY) { |
| cal.set(Calendar.WEEK_OF_YEAR, currentCal.get(Calendar.WEEK_OF_YEAR)); |
| increment(cal, Calendar.WEEK_OF_YEAR, increment, modulus); |
| cal.set(Calendar.DAY_OF_WEEK, currentCal.getFirstDayOfWeek()); |
| nextTime = cal.getTimeInMillis(); |
| cal.add(Calendar.WEEK_OF_YEAR, -1); |
| nextFileTime = cal.getTimeInMillis(); |
| return debugGetNextTime(nextTime); |
| } |
| cal.set(Calendar.DAY_OF_YEAR, currentCal.get(Calendar.DAY_OF_YEAR)); |
| if (frequency == RolloverFrequency.DAILY) { |
| increment(cal, Calendar.DAY_OF_YEAR, increment, modulus); |
| nextTime = cal.getTimeInMillis(); |
| cal.add(Calendar.DAY_OF_YEAR, -1); |
| nextFileTime = cal.getTimeInMillis(); |
| return debugGetNextTime(nextTime); |
| } |
| cal.set(Calendar.HOUR_OF_DAY, currentCal.get(Calendar.HOUR_OF_DAY)); |
| if (frequency == RolloverFrequency.HOURLY) { |
| increment(cal, Calendar.HOUR_OF_DAY, increment, modulus); |
| nextTime = cal.getTimeInMillis(); |
| cal.add(Calendar.HOUR_OF_DAY, -1); |
| nextFileTime = cal.getTimeInMillis(); |
| return debugGetNextTime(nextTime); |
| } |
| cal.set(Calendar.MINUTE, currentCal.get(Calendar.MINUTE)); |
| if (frequency == RolloverFrequency.EVERY_MINUTE) { |
| increment(cal, Calendar.MINUTE, increment, modulus); |
| nextTime = cal.getTimeInMillis(); |
| cal.add(Calendar.MINUTE, -1); |
| nextFileTime = cal.getTimeInMillis(); |
| return debugGetNextTime(nextTime); |
| } |
| cal.set(Calendar.SECOND, currentCal.get(Calendar.SECOND)); |
| if (frequency == RolloverFrequency.EVERY_SECOND) { |
| increment(cal, Calendar.SECOND, increment, modulus); |
| nextTime = cal.getTimeInMillis(); |
| cal.add(Calendar.SECOND, -1); |
| nextFileTime = cal.getTimeInMillis(); |
| return debugGetNextTime(nextTime); |
| } |
| cal.set(Calendar.MILLISECOND, currentCal.get(Calendar.MILLISECOND)); |
| increment(cal, Calendar.MILLISECOND, increment, modulus); |
| nextTime = cal.getTimeInMillis(); |
| cal.add(Calendar.MILLISECOND, -1); |
| nextFileTime = cal.getTimeInMillis(); |
| return debugGetNextTime(nextTime); |
| } |
| |
| public void updateTime() { |
| if (nextFileTime != 0 || !isTimeBased) { |
| prevFileTime = nextFileTime; |
| } |
| } |
| |
| private long debugGetNextTime(final long nextTime) { |
| if (LOGGER.isTraceEnabled()) { |
| LOGGER.trace("PatternProcessor.getNextTime returning {}, nextFileTime={}, prevFileTime={}, current={}, freq={}", // |
| format(nextTime), format(nextFileTime), format(prevFileTime), format(System.currentTimeMillis()), frequency); |
| } |
| return nextTime; |
| } |
| |
| private String format(final long time) { |
| return new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss.SSS").format(new Date(time)); |
| } |
| |
| private void increment(final Calendar cal, final int type, final int increment, final boolean modulate) { |
| final int interval = modulate ? increment - (cal.get(type) % increment) : increment; |
| cal.add(type, interval); |
| } |
| |
| /** |
| * Format file name. |
| * @param buf string buffer to which formatted file name is appended, may not be null. |
| * @param obj object to be evaluated in formatting, may not be null. |
| */ |
| public final void formatFileName(final StringBuilder buf, final boolean useCurrentTime, final Object obj) { |
| long time = useCurrentTime ? currentFileTime : prevFileTime; |
| if (time == 0) { |
| time = System.currentTimeMillis(); |
| } |
| formatFileName(buf, new Date(time), obj); |
| } |
| |
| /** |
| * Formats file name. |
| * @param subst The StrSubstitutor. |
| * @param buf string buffer to which formatted file name is appended, may not be null. |
| * @param obj object to be evaluated in formatting, may not be null. |
| */ |
| public final void formatFileName(final StrSubstitutor subst, final StringBuilder buf, final Object obj) { |
| formatFileName(subst, buf, false, obj); |
| } |
| |
| /** |
| * Formats file name. |
| * @param subst The StrSubstitutor. |
| * @param buf string buffer to which formatted file name is appended, may not be null. |
| * @param obj object to be evaluated in formatting, may not be null. |
| */ |
| public final void formatFileName(final StrSubstitutor subst, final StringBuilder buf, final boolean useCurrentTime, |
| final Object obj) { |
| // LOG4J2-628: we deliberately use System time, not the log4j.Clock time |
| // for creating the file name of rolled-over files. |
| LOGGER.debug("Formatting file name. useCurrentTime={}, currentFileTime={}, prevFileTime={}", |
| useCurrentTime, currentFileTime, prevFileTime); |
| final long time = useCurrentTime ? currentFileTime != 0 ? currentFileTime : System.currentTimeMillis() : |
| prevFileTime != 0 ? prevFileTime : System.currentTimeMillis(); |
| formatFileName(buf, new Date(time), obj); |
| final LogEvent event = new Log4jLogEvent.Builder().setTimeMillis(time).build(); |
| final String fileName = subst.replace(event, buf); |
| buf.setLength(0); |
| buf.append(fileName); |
| } |
| |
| /** |
| * Formats file name. |
| * @param buf string buffer to which formatted file name is appended, may not be null. |
| * @param objects objects to be evaluated in formatting, may not be null. |
| */ |
| protected final void formatFileName(final StringBuilder buf, final Object... objects) { |
| for (int i = 0; i < patternConverters.length; i++) { |
| final int fieldStart = buf.length(); |
| patternConverters[i].format(buf, objects); |
| |
| if (patternFields[i] != null) { |
| patternFields[i].format(fieldStart, buf); |
| } |
| } |
| } |
| |
| private RolloverFrequency calculateFrequency(final String pattern) { |
| if (patternContains(pattern, MILLIS_CHAR)) { |
| return RolloverFrequency.EVERY_MILLISECOND; |
| } |
| if (patternContains(pattern, SECOND_CHAR)) { |
| return RolloverFrequency.EVERY_SECOND; |
| } |
| if (patternContains(pattern, MINUTE_CHAR)) { |
| return RolloverFrequency.EVERY_MINUTE; |
| } |
| if (patternContains(pattern, HOUR_CHARS)) { |
| return RolloverFrequency.HOURLY; |
| } |
| if (patternContains(pattern, DAY_CHARS)) { |
| return RolloverFrequency.DAILY; |
| } |
| if (patternContains(pattern, WEEK_CHARS)) { |
| return RolloverFrequency.WEEKLY; |
| } |
| if (patternContains(pattern, MONTH_CHAR)) { |
| return RolloverFrequency.MONTHLY; |
| } |
| if (patternContains(pattern, YEAR_CHAR)) { |
| return RolloverFrequency.ANNUALLY; |
| } |
| return null; |
| } |
| |
| private PatternParser createPatternParser() { |
| |
| return new PatternParser(null, KEY, null); |
| } |
| |
| private boolean patternContains(final String pattern, final char... chars) { |
| for (final char character : chars) { |
| if (patternContains(pattern, character)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean patternContains(final String pattern, final char character) { |
| return pattern.indexOf(character) >= 0; |
| } |
| |
| public RolloverFrequency getFrequency() { |
| return frequency; |
| } |
| |
| public long getNextFileTime() { |
| return nextFileTime; |
| } |
| |
| } |