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     */
017    package org.apache.logging.log4j.core.appender.rolling;
018    
019    import java.text.SimpleDateFormat;
020    import java.util.ArrayList;
021    import java.util.Calendar;
022    import java.util.Date;
023    import java.util.List;
024    
025    import org.apache.logging.log4j.Logger;
026    import org.apache.logging.log4j.core.LogEvent;
027    import org.apache.logging.log4j.core.impl.Log4jLogEvent;
028    import org.apache.logging.log4j.core.lookup.StrSubstitutor;
029    import org.apache.logging.log4j.core.pattern.ArrayPatternConverter;
030    import org.apache.logging.log4j.core.pattern.DatePatternConverter;
031    import org.apache.logging.log4j.core.pattern.FormattingInfo;
032    import org.apache.logging.log4j.core.pattern.PatternConverter;
033    import org.apache.logging.log4j.core.pattern.PatternParser;
034    import org.apache.logging.log4j.status.StatusLogger;
035    
036    /**
037     * Parse the rollover pattern.
038     */
039    public 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    }