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 * Parse 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
059    private RolloverFrequency frequency = null;
060
061    /**
062     * Constructor.
063     * @param pattern The file pattern.
064     */
065    public PatternProcessor(final String pattern) {
066        final PatternParser parser = createPatternParser();
067        final List<PatternConverter> converters = new ArrayList<PatternConverter>();
068        final List<FormattingInfo> fields = new ArrayList<FormattingInfo>();
069        parser.parse(pattern, converters, fields, false, false);
070        final FormattingInfo[] infoArray = new FormattingInfo[fields.size()];
071        patternFields = fields.toArray(infoArray);
072        final ArrayPatternConverter[] converterArray = new ArrayPatternConverter[converters.size()];
073        patternConverters = converters.toArray(converterArray);
074
075        for (final ArrayPatternConverter converter : patternConverters) {
076            if (converter instanceof DatePatternConverter) {
077                final DatePatternConverter dateConverter = (DatePatternConverter) converter;
078                frequency = calculateFrequency(dateConverter.getPattern());
079            }
080        }
081    }
082
083    /**
084     * Returns the next potential rollover time.
085     * @param current The current time.
086     * @param increment The increment to the next time.
087     * @param modulus If true the time will be rounded to occur on a boundary aligned with the increment.
088     * @return the next potential rollover time and the timestamp for the target file.
089     */
090    public long getNextTime(final long current, final int increment, final boolean modulus) {
091        prevFileTime = nextFileTime;
092        long nextTime;
093
094        if (frequency == null) {
095            throw new IllegalStateException("Pattern does not contain a date");
096        }
097        final Calendar currentCal = Calendar.getInstance();
098        currentCal.setTimeInMillis(current);
099        final Calendar cal = Calendar.getInstance();
100        cal.set(currentCal.get(Calendar.YEAR), 0, 1, 0, 0, 0);
101        cal.set(Calendar.MILLISECOND, 0);
102        if (frequency == RolloverFrequency.ANNUALLY) {
103            increment(cal, Calendar.YEAR, increment, modulus);
104            nextTime = cal.getTimeInMillis();
105            cal.add(Calendar.YEAR, -1);
106            nextFileTime = cal.getTimeInMillis();
107            return debugGetNextTime(nextTime);
108        }
109        cal.set(Calendar.MONTH, currentCal.get(Calendar.MONTH));
110        if (frequency == RolloverFrequency.MONTHLY) {
111            increment(cal, Calendar.MONTH, increment, modulus);
112            nextTime = cal.getTimeInMillis();
113            cal.add(Calendar.MONTH, -1);
114            nextFileTime = cal.getTimeInMillis();
115            return debugGetNextTime(nextTime);
116        }
117        if (frequency == RolloverFrequency.WEEKLY) {
118            cal.set(Calendar.WEEK_OF_YEAR, currentCal.get(Calendar.WEEK_OF_YEAR));
119            increment(cal, Calendar.WEEK_OF_YEAR, increment, modulus);
120            cal.set(Calendar.DAY_OF_WEEK, currentCal.getFirstDayOfWeek());
121            nextTime = cal.getTimeInMillis();
122            cal.add(Calendar.WEEK_OF_YEAR, -1);
123            nextFileTime = cal.getTimeInMillis();
124            return debugGetNextTime(nextTime);
125        }
126        cal.set(Calendar.DAY_OF_YEAR, currentCal.get(Calendar.DAY_OF_YEAR));
127        if (frequency == RolloverFrequency.DAILY) {
128            increment(cal, Calendar.DAY_OF_YEAR, increment, modulus);
129            nextTime = cal.getTimeInMillis();
130            cal.add(Calendar.DAY_OF_YEAR, -1);
131            nextFileTime = cal.getTimeInMillis();
132            return debugGetNextTime(nextTime);
133        }
134        cal.set(Calendar.HOUR_OF_DAY, currentCal.get(Calendar.HOUR_OF_DAY));
135        if (frequency == RolloverFrequency.HOURLY) {
136            increment(cal, Calendar.HOUR_OF_DAY, increment, modulus);
137            nextTime = cal.getTimeInMillis();
138            cal.add(Calendar.HOUR_OF_DAY, -1);
139            nextFileTime = cal.getTimeInMillis();
140            return debugGetNextTime(nextTime);
141        }
142        cal.set(Calendar.MINUTE, currentCal.get(Calendar.MINUTE));
143        if (frequency == RolloverFrequency.EVERY_MINUTE) {
144            increment(cal, Calendar.MINUTE, increment, modulus);
145            nextTime = cal.getTimeInMillis();
146            cal.add(Calendar.MINUTE, -1);
147            nextFileTime = cal.getTimeInMillis();
148            return debugGetNextTime(nextTime);
149        }
150        cal.set(Calendar.SECOND, currentCal.get(Calendar.SECOND));
151        if (frequency == RolloverFrequency.EVERY_SECOND) {
152            increment(cal, Calendar.SECOND, increment, modulus);
153            nextTime = cal.getTimeInMillis();
154            cal.add(Calendar.SECOND, -1);
155            nextFileTime = cal.getTimeInMillis();
156            return debugGetNextTime(nextTime);
157        }
158        cal.set(Calendar.MILLISECOND, currentCal.get(Calendar.MILLISECOND));
159        increment(cal, Calendar.MILLISECOND, increment, modulus);
160        nextTime = cal.getTimeInMillis();
161        cal.add(Calendar.MILLISECOND, -1);
162        nextFileTime = cal.getTimeInMillis();
163        return debugGetNextTime(nextTime);
164    }
165
166    public void updateTime() {
167        prevFileTime = nextFileTime;
168    }
169
170    private long debugGetNextTime(final long nextTime) {
171        if (LOGGER.isTraceEnabled()) {
172            LOGGER.trace("PatternProcessor.getNextTime returning {}, nextFileTime={}, prevFileTime={}, current={}, freq={}", //
173                    format(nextTime), format(nextFileTime), format(prevFileTime), format(System.currentTimeMillis()), frequency);
174        }
175        return nextTime;
176    }
177
178    private String format(final long time) {
179        return new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss.SSS").format(new Date(time));
180    }
181
182    private void increment(final Calendar cal, final int type, final int increment, final boolean modulate) {
183        final int interval =  modulate ? increment - (cal.get(type) % increment) : increment;
184        cal.add(type, interval);
185    }
186
187    /**
188     * Format file name.
189     * @param buf string buffer to which formatted file name is appended, may not be null.
190     * @param obj object to be evaluated in formatting, may not be null.
191     */
192    public final void formatFileName(final StringBuilder buf, final Object obj) {
193        final long time = prevFileTime == 0 ? System.currentTimeMillis() : prevFileTime;
194        formatFileName(buf, new Date(time), obj);
195    }
196
197    /**
198     * Format file name.
199     * @param subst The StrSubstitutor.
200     * @param buf string buffer to which formatted file name is appended, may not be null.
201     * @param obj object to be evaluated in formatting, may not be null.
202     */
203    public final void formatFileName(final StrSubstitutor subst, final StringBuilder buf, final Object obj) {
204        // LOG4J2-628: we deliberately use System time, not the log4j.Clock time
205        // for creating the file name of rolled-over files. 
206        final long time = prevFileTime == 0 ? System.currentTimeMillis() : prevFileTime;
207        formatFileName(buf, new Date(time), obj);
208        final LogEvent event = new Log4jLogEvent(time);
209        final String fileName = subst.replace(event, buf);
210        buf.setLength(0);
211        buf.append(fileName);
212    }
213
214    /**
215     * Format file name.
216     * @param buf string buffer to which formatted file name is appended, may not be null.
217     * @param objects objects to be evaluated in formatting, may not be null.
218     */
219    protected final void formatFileName(final StringBuilder buf, final Object... objects) {
220        for (int i = 0; i < patternConverters.length; i++) {
221            final int fieldStart = buf.length();
222            patternConverters[i].format(buf, objects);
223
224            if (patternFields[i] != null) {
225                patternFields[i].format(fieldStart, buf);
226            }
227        }
228    }
229
230    private RolloverFrequency calculateFrequency(final String pattern) {
231        if (patternContains(pattern, MILLIS_CHAR)) {
232            return RolloverFrequency.EVERY_MILLISECOND;
233        }
234        if (patternContains(pattern, SECOND_CHAR)) {
235            return RolloverFrequency.EVERY_SECOND;
236        }
237        if (patternContains(pattern, MINUTE_CHAR)) {
238            return RolloverFrequency.EVERY_MINUTE;
239        }
240        if (patternContains(pattern, HOUR_CHARS)) {
241            return RolloverFrequency.HOURLY;
242        }
243        if (patternContains(pattern, DAY_CHARS)) {
244            return RolloverFrequency.DAILY;
245        }
246        if (patternContains(pattern, WEEK_CHARS)) {
247            return RolloverFrequency.WEEKLY;
248        }
249        if (patternContains(pattern, MONTH_CHAR)) {
250            return RolloverFrequency.MONTHLY;
251        }
252        if (patternContains(pattern, YEAR_CHAR)) {
253            return RolloverFrequency.ANNUALLY;
254        }
255        return null;
256    }
257
258    private PatternParser createPatternParser() {
259
260        return new PatternParser(null, KEY, null);
261    }
262
263    private boolean patternContains(final String pattern, final char... chars) {
264        for (final char character : chars) {
265            if (patternContains(pattern, character)) {
266                return true;
267            }
268        }
269        return false;
270    }
271
272    private boolean patternContains(final String pattern, final char character) {
273        return pattern.indexOf(character) >= 0;
274    }
275}