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        prevFileTime = nextFileTime;
217    }
218
219    private long debugGetNextTime(final long nextTime) {
220        if (LOGGER.isTraceEnabled()) {
221            LOGGER.trace("PatternProcessor.getNextTime returning {}, nextFileTime={}, prevFileTime={}, current={}, freq={}", //
222                    format(nextTime), format(nextFileTime), format(prevFileTime), format(System.currentTimeMillis()), frequency);
223        }
224        return nextTime;
225    }
226
227    private String format(final long time) {
228        return new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss.SSS").format(new Date(time));
229    }
230
231    private void increment(final Calendar cal, final int type, final int increment, final boolean modulate) {
232        final int interval =  modulate ? increment - (cal.get(type) % increment) : increment;
233        cal.add(type, interval);
234    }
235
236    /**
237     * Format file name.
238     * @param buf string buffer to which formatted file name is appended, may not be null.
239     * @param obj object to be evaluated in formatting, may not be null.
240     */
241    public final void formatFileName(final StringBuilder buf, final boolean useCurrentTime, final Object obj) {
242        long time = useCurrentTime ? currentFileTime : prevFileTime;
243        if (time == 0) {
244            time = System.currentTimeMillis();
245        }
246        formatFileName(buf, new Date(time), obj);
247    }
248
249    /**
250     * Formats file name.
251     * @param subst The StrSubstitutor.
252     * @param buf string buffer to which formatted file name is appended, may not be null.
253     * @param obj object to be evaluated in formatting, may not be null.
254     */
255    public final void formatFileName(final StrSubstitutor subst, final StringBuilder buf, final Object obj) {
256        formatFileName(subst, buf, false, obj);
257    }
258
259    /**
260     * Formats file name.
261     * @param subst The StrSubstitutor.
262     * @param buf string buffer to which formatted file name is appended, may not be null.
263     * @param obj object to be evaluated in formatting, may not be null.
264     */
265    public final void formatFileName(final StrSubstitutor subst, final StringBuilder buf, final boolean useCurrentTime,
266                                     final Object obj) {
267        // LOG4J2-628: we deliberately use System time, not the log4j.Clock time
268        // for creating the file name of rolled-over files.
269        final long time = useCurrentTime && currentFileTime != 0 ? currentFileTime :
270                prevFileTime != 0 ? prevFileTime : System.currentTimeMillis();
271        formatFileName(buf, new Date(time), obj);
272        final LogEvent event = new Log4jLogEvent.Builder().setTimeMillis(time).build();
273        final String fileName = subst.replace(event, buf);
274        buf.setLength(0);
275        buf.append(fileName);
276    }
277
278    /**
279     * Formats file name.
280     * @param buf string buffer to which formatted file name is appended, may not be null.
281     * @param objects objects to be evaluated in formatting, may not be null.
282     */
283    protected final void formatFileName(final StringBuilder buf, final Object... objects) {
284        for (int i = 0; i < patternConverters.length; i++) {
285            final int fieldStart = buf.length();
286            patternConverters[i].format(buf, objects);
287
288            if (patternFields[i] != null) {
289                patternFields[i].format(fieldStart, buf);
290            }
291        }
292    }
293
294    private RolloverFrequency calculateFrequency(final String pattern) {
295        if (patternContains(pattern, MILLIS_CHAR)) {
296            return RolloverFrequency.EVERY_MILLISECOND;
297        }
298        if (patternContains(pattern, SECOND_CHAR)) {
299            return RolloverFrequency.EVERY_SECOND;
300        }
301        if (patternContains(pattern, MINUTE_CHAR)) {
302            return RolloverFrequency.EVERY_MINUTE;
303        }
304        if (patternContains(pattern, HOUR_CHARS)) {
305            return RolloverFrequency.HOURLY;
306        }
307        if (patternContains(pattern, DAY_CHARS)) {
308            return RolloverFrequency.DAILY;
309        }
310        if (patternContains(pattern, WEEK_CHARS)) {
311            return RolloverFrequency.WEEKLY;
312        }
313        if (patternContains(pattern, MONTH_CHAR)) {
314            return RolloverFrequency.MONTHLY;
315        }
316        if (patternContains(pattern, YEAR_CHAR)) {
317            return RolloverFrequency.ANNUALLY;
318        }
319        return null;
320    }
321
322    private PatternParser createPatternParser() {
323
324        return new PatternParser(null, KEY, null);
325    }
326
327    private boolean patternContains(final String pattern, final char... chars) {
328        for (final char character : chars) {
329            if (patternContains(pattern, character)) {
330                return true;
331            }
332        }
333        return false;
334    }
335
336    private boolean patternContains(final String pattern, final char character) {
337        return pattern.indexOf(character) >= 0;
338    }
339
340    public RolloverFrequency getFrequency() {
341        return frequency;
342    }
343
344    public long getNextFileTime() {
345        return nextFileTime;
346    }
347
348}