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