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 boolean isTimeBased = false;
061
062    private RolloverFrequency frequency = null;
063
064    private final String pattern;
065
066    public String getPattern() {
067        return pattern;
068    }
069
070    @Override
071    public String toString() {
072        return pattern;
073    }
074
075    /**
076     * Constructor.
077     * @param pattern The file pattern.
078     */
079    public PatternProcessor(final String pattern) {
080        this.pattern = pattern;
081        final PatternParser parser = createPatternParser();
082        final List<PatternConverter> converters = new ArrayList<>();
083        final List<FormattingInfo> fields = new ArrayList<>();
084        parser.parse(pattern, converters, fields, false, false, false);
085        final FormattingInfo[] infoArray = new FormattingInfo[fields.size()];
086        patternFields = fields.toArray(infoArray);
087        final ArrayPatternConverter[] converterArray = new ArrayPatternConverter[converters.size()];
088        patternConverters = converters.toArray(converterArray);
089
090        for (final ArrayPatternConverter converter : patternConverters) {
091            if (converter instanceof DatePatternConverter) {
092                final DatePatternConverter dateConverter = (DatePatternConverter) converter;
093                frequency = calculateFrequency(dateConverter.getPattern());
094            }
095        }
096    }
097
098    /**
099     * Copy constructor with another pattern as source.
100     *
101     * @param pattern  The file pattern.
102     * @param copy Source pattern processor
103     */
104    public PatternProcessor(final String pattern, final PatternProcessor copy) {
105        this(pattern);
106        this.prevFileTime = copy.prevFileTime;
107        this.nextFileTime = copy.nextFileTime;
108        this.currentFileTime = copy.currentFileTime;
109    }
110
111    public void setTimeBased(boolean isTimeBased) {
112        this.isTimeBased = isTimeBased;
113    }
114
115    public long getCurrentFileTime() {
116        return currentFileTime;
117    }
118
119    public void setCurrentFileTime(final long currentFileTime) {
120        this.currentFileTime = currentFileTime;
121    }
122
123    public long getPrevFileTime() {
124        return prevFileTime;
125    }
126
127    public void setPrevFileTime(final long prevFileTime) {
128        LOGGER.debug("Setting prev file time to {}", new Date(prevFileTime));
129        this.prevFileTime = prevFileTime;
130    }
131
132    /**
133     * Returns the next potential rollover time.
134     * @param currentMillis The current time.
135     * @param increment The increment to the next time.
136     * @param modulus If true the time will be rounded to occur on a boundary aligned with the increment.
137     * @return the next potential rollover time and the timestamp for the target file.
138     */
139    public long getNextTime(final long currentMillis, final int increment, final boolean modulus) {
140        //
141        // https://issues.apache.org/jira/browse/LOG4J2-1232
142        // Call setMinimalDaysInFirstWeek(7);
143        //
144        prevFileTime = nextFileTime;
145        long nextTime;
146
147        if (frequency == null) {
148            throw new IllegalStateException("Pattern does not contain a date");
149        }
150        final Calendar currentCal = Calendar.getInstance();
151        currentCal.setTimeInMillis(currentMillis);
152        final Calendar cal = Calendar.getInstance();
153        currentCal.setMinimalDaysInFirstWeek(7);
154        cal.setMinimalDaysInFirstWeek(7);
155        cal.set(currentCal.get(Calendar.YEAR), 0, 1, 0, 0, 0);
156        cal.set(Calendar.MILLISECOND, 0);
157        if (frequency == RolloverFrequency.ANNUALLY) {
158            increment(cal, Calendar.YEAR, increment, modulus);
159            nextTime = cal.getTimeInMillis();
160            cal.add(Calendar.YEAR, -1);
161            nextFileTime = cal.getTimeInMillis();
162            return debugGetNextTime(nextTime);
163        }
164        cal.set(Calendar.MONTH, currentCal.get(Calendar.MONTH));
165        if (frequency == RolloverFrequency.MONTHLY) {
166            increment(cal, Calendar.MONTH, increment, modulus);
167            nextTime = cal.getTimeInMillis();
168            cal.add(Calendar.MONTH, -1);
169            nextFileTime = cal.getTimeInMillis();
170            return debugGetNextTime(nextTime);
171        }
172        if (frequency == RolloverFrequency.WEEKLY) {
173            cal.set(Calendar.WEEK_OF_YEAR, currentCal.get(Calendar.WEEK_OF_YEAR));
174            increment(cal, Calendar.WEEK_OF_YEAR, increment, modulus);
175            cal.set(Calendar.DAY_OF_WEEK, currentCal.getFirstDayOfWeek());
176            nextTime = cal.getTimeInMillis();
177            cal.add(Calendar.WEEK_OF_YEAR, -1);
178            nextFileTime = cal.getTimeInMillis();
179            return debugGetNextTime(nextTime);
180        }
181        cal.set(Calendar.DAY_OF_YEAR, currentCal.get(Calendar.DAY_OF_YEAR));
182        if (frequency == RolloverFrequency.DAILY) {
183            increment(cal, Calendar.DAY_OF_YEAR, increment, modulus);
184            nextTime = cal.getTimeInMillis();
185            cal.add(Calendar.DAY_OF_YEAR, -1);
186            nextFileTime = cal.getTimeInMillis();
187            return debugGetNextTime(nextTime);
188        }
189        cal.set(Calendar.HOUR_OF_DAY, currentCal.get(Calendar.HOUR_OF_DAY));
190        if (frequency == RolloverFrequency.HOURLY) {
191            increment(cal, Calendar.HOUR_OF_DAY, increment, modulus);
192            nextTime = cal.getTimeInMillis();
193            cal.add(Calendar.HOUR_OF_DAY, -1);
194            nextFileTime = cal.getTimeInMillis();
195            return debugGetNextTime(nextTime);
196        }
197        cal.set(Calendar.MINUTE, currentCal.get(Calendar.MINUTE));
198        if (frequency == RolloverFrequency.EVERY_MINUTE) {
199            increment(cal, Calendar.MINUTE, increment, modulus);
200            nextTime = cal.getTimeInMillis();
201            cal.add(Calendar.MINUTE, -1);
202            nextFileTime = cal.getTimeInMillis();
203            return debugGetNextTime(nextTime);
204        }
205        cal.set(Calendar.SECOND, currentCal.get(Calendar.SECOND));
206        if (frequency == RolloverFrequency.EVERY_SECOND) {
207            increment(cal, Calendar.SECOND, increment, modulus);
208            nextTime = cal.getTimeInMillis();
209            cal.add(Calendar.SECOND, -1);
210            nextFileTime = cal.getTimeInMillis();
211            return debugGetNextTime(nextTime);
212        }
213        cal.set(Calendar.MILLISECOND, currentCal.get(Calendar.MILLISECOND));
214        increment(cal, Calendar.MILLISECOND, increment, modulus);
215        nextTime = cal.getTimeInMillis();
216        cal.add(Calendar.MILLISECOND, -1);
217        nextFileTime = cal.getTimeInMillis();
218        return debugGetNextTime(nextTime);
219    }
220
221    public void updateTime() {
222        if (nextFileTime != 0 || !isTimeBased) {
223                        prevFileTime = nextFileTime;
224                }
225    }
226
227    private long debugGetNextTime(final long nextTime) {
228        if (LOGGER.isTraceEnabled()) {
229            LOGGER.trace("PatternProcessor.getNextTime returning {}, nextFileTime={}, prevFileTime={}, current={}, freq={}", //
230                    format(nextTime), format(nextFileTime), format(prevFileTime), format(System.currentTimeMillis()), frequency);
231        }
232        return nextTime;
233    }
234
235    private String format(final long time) {
236        return new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss.SSS").format(new Date(time));
237    }
238
239    private void increment(final Calendar cal, final int type, final int increment, final boolean modulate) {
240        final int interval =  modulate ? increment - (cal.get(type) % increment) : increment;
241        cal.add(type, interval);
242    }
243
244    /**
245     * Format file name.
246     * @param buf string buffer to which formatted file name is appended, may not be null.
247     * @param obj object to be evaluated in formatting, may not be null.
248     */
249    public final void formatFileName(final StringBuilder buf, final boolean useCurrentTime, final Object obj) {
250        long time = useCurrentTime ? currentFileTime : prevFileTime;
251        if (time == 0) {
252            time = System.currentTimeMillis();
253        }
254        formatFileName(buf, new Date(time), obj);
255    }
256
257    /**
258     * Formats file name.
259     * @param subst The StrSubstitutor.
260     * @param buf string buffer to which formatted file name is appended, may not be null.
261     * @param obj object to be evaluated in formatting, may not be null.
262     */
263    public final void formatFileName(final StrSubstitutor subst, final StringBuilder buf, final Object obj) {
264        formatFileName(subst, buf, false, obj);
265    }
266
267    /**
268     * Formats file name.
269     * @param subst The StrSubstitutor.
270     * @param buf string buffer to which formatted file name is appended, may not be null.
271     * @param obj object to be evaluated in formatting, may not be null.
272     */
273    public final void formatFileName(final StrSubstitutor subst, final StringBuilder buf, final boolean useCurrentTime,
274                                     final Object obj) {
275        // LOG4J2-628: we deliberately use System time, not the log4j.Clock time
276        // for creating the file name of rolled-over files.
277        LOGGER.debug("Formatting file name. useCurrentTime={}. currentFileTime={}, prevFileTime={}",
278            useCurrentTime, currentFileTime, prevFileTime);
279        final long time = useCurrentTime ? currentFileTime != 0 ? currentFileTime : System.currentTimeMillis() :
280                prevFileTime != 0 ? prevFileTime : System.currentTimeMillis();
281        formatFileName(buf, new Date(time), obj);
282        final LogEvent event = new Log4jLogEvent.Builder().setTimeMillis(time).build();
283        final String fileName = subst.replace(event, buf);
284        buf.setLength(0);
285        buf.append(fileName);
286    }
287
288    /**
289     * Formats file name.
290     * @param buf string buffer to which formatted file name is appended, may not be null.
291     * @param objects objects to be evaluated in formatting, may not be null.
292     */
293    protected final void formatFileName(final StringBuilder buf, final Object... objects) {
294        for (int i = 0; i < patternConverters.length; i++) {
295            final int fieldStart = buf.length();
296            patternConverters[i].format(buf, objects);
297
298            if (patternFields[i] != null) {
299                patternFields[i].format(fieldStart, buf);
300            }
301        }
302    }
303
304    private RolloverFrequency calculateFrequency(final String pattern) {
305        if (patternContains(pattern, MILLIS_CHAR)) {
306            return RolloverFrequency.EVERY_MILLISECOND;
307        }
308        if (patternContains(pattern, SECOND_CHAR)) {
309            return RolloverFrequency.EVERY_SECOND;
310        }
311        if (patternContains(pattern, MINUTE_CHAR)) {
312            return RolloverFrequency.EVERY_MINUTE;
313        }
314        if (patternContains(pattern, HOUR_CHARS)) {
315            return RolloverFrequency.HOURLY;
316        }
317        if (patternContains(pattern, DAY_CHARS)) {
318            return RolloverFrequency.DAILY;
319        }
320        if (patternContains(pattern, WEEK_CHARS)) {
321            return RolloverFrequency.WEEKLY;
322        }
323        if (patternContains(pattern, MONTH_CHAR)) {
324            return RolloverFrequency.MONTHLY;
325        }
326        if (patternContains(pattern, YEAR_CHAR)) {
327            return RolloverFrequency.ANNUALLY;
328        }
329        return null;
330    }
331
332    private PatternParser createPatternParser() {
333
334        return new PatternParser(null, KEY, null);
335    }
336
337    private boolean patternContains(final String pattern, final char... chars) {
338        for (final char character : chars) {
339            if (patternContains(pattern, character)) {
340                return true;
341            }
342        }
343        return false;
344    }
345
346    private boolean patternContains(final String pattern, final char character) {
347        return pattern.indexOf(character) >= 0;
348    }
349
350    public RolloverFrequency getFrequency() {
351        return frequency;
352    }
353
354    public long getNextFileTime() {
355        return nextFileTime;
356    }
357
358}