a0f35b43a473f59cb783559c951e4b855395d8fd
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / log / RollingFileAppender.java
1 /*
2  * This class borrows extensively from the Log4J DailyRollingFileAppender 
3  * written by Eirik Lygre and Ceki Gulcu and copyrighted to the Apache Foundation
4  * under the Apache 2 License (http://apache.org/licenses/LICENSE-2.0).
5  */
6
7 package edu.internet2.middleware.shibboleth.log;
8
9 import java.io.File;
10 import java.io.IOException;
11 import java.text.SimpleDateFormat;
12 import java.util.Calendar;
13 import java.util.Date;
14 import java.util.GregorianCalendar;
15 import java.util.Locale;
16 import java.util.TimeZone;
17
18 import org.apache.log4j.FileAppender;
19 import org.apache.log4j.Layout;
20 import org.apache.log4j.helpers.LogLog;
21 import org.apache.log4j.spi.LoggingEvent;
22
23 /**
24  * A minor refactoring of Log4J's DailyRollingFileAppender. The log4j appender does not provide an easy mechanism for
25  * having a file name of name.date.extension, instead it wants to do name.extension.date which on some platforms can be
26  * a pain. This appender is meant to handle this case.
27  * 
28  * The file appender will create a file called filename.extension, then it will roll over the files to
29  * filename.date.extension. The default date pattern is "'.'yyyy-MM-dd" (i.e. daily roll over) and the default file
30  * extnsion is '.log'.
31  * 
32  * @author Chad La Joie
33  */
34 public class RollingFileAppender
35         extends FileAppender {
36
37     // The code assumes that the following constants are in a increasing
38     // sequence.
39     static final int TOP_OF_TROUBLE = -1;
40
41     static final int TOP_OF_MINUTE = 0;
42
43     static final int TOP_OF_HOUR = 1;
44
45     static final int HALF_DAY = 2;
46
47     static final int TOP_OF_DAY = 3;
48
49     static final int TOP_OF_WEEK = 4;
50
51     static final int TOP_OF_MONTH = 5;
52
53     /**
54      * The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" meaning daily rollover.
55      */
56     private String datePattern = "'-'yyyy-MM-dd";
57
58     /**
59      * The log file will be renamed to the value of the scheduledFilename variable when the next interval is entered.
60      * For example, if the rollover period is one hour, the log file will be renamed to the value of "scheduledFilename"
61      * at the beginning of the next hour.
62      * 
63      * The precise time when a rollover occurs depends on logging activity.
64      */
65     private String scheduledFilename;
66
67     /**
68      * The next time we estimate a rollover should occur.
69      */
70     private long nextCheck = System.currentTimeMillis() - 1;
71
72     /**
73      * Current date
74      */
75     private Date now = new Date();
76
77     /**
78      * The date pattern
79      */
80     private SimpleDateFormat sdf;
81
82     /**
83      * Calendar for determing roll over information
84      */
85     private RollingCalendar rc = new RollingCalendar();
86
87     /**
88      * How often to check for roll over
89      */
90     int checkPeriod = TOP_OF_TROUBLE;
91
92     /**
93      * The gmtTimeZone is used only in computeCheckPeriod() method.
94      */
95     static final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
96
97     /**
98      * The file extension
99      */
100     private String fileExtension = ".log";
101
102     /**
103      * The file name without the date or extension components.
104      */
105     private String simpleFileName;
106
107     /**
108      * Default constructor
109      */
110     public RollingFileAppender() {
111
112     }
113     
114     /**
115      * Constructor.
116      * 
117      * @param layout the log entry layout pattern
118      * @param filename the file name
119      * @param datePattern the date pattern used to determine rolling behavior
120      * @param fileExtension the file extension to post-pend to log file name
121      * 
122      * @throws IOException thrown if the file can not be created
123      */
124     public RollingFileAppender(Layout layout, String filename, String datePattern, String fileExtension)
125             throws IOException {
126         super(layout, filename + fileExtension, true);
127         simpleFileName = filename;
128         setDatePattern(datePattern);
129         setFileExtension(fileExtension);
130         activateOptions();
131     }
132
133     /**
134      * Gets the extension post-pended to the file name.
135      * 
136      * @return the log file's extension
137      */
138     public String getFileExtension() {
139         return fileExtension;
140     }
141
142     /**
143      * Sets the extension post-pended to the file name.
144      * 
145      * @param extension the log file's extension
146      */
147     public void setFileExtension(String extension) {
148         fileExtension = extension;
149     }
150
151     /**
152      * The <b>DatePattern</b> takes a string in the same format as expected by {@link SimpleDateFormat}. This options
153      * determines the rollover schedule.
154      * 
155      * @param pattern the rollover date pattern
156      */
157     public void setDatePattern(String pattern) {
158         datePattern = pattern;
159     }
160
161     /**
162      * Returns the value of the <b>DatePattern</b> option.
163      * 
164      * @return the rollover date pattern
165      */
166     public String getDatePattern() {
167         return datePattern;
168     }
169
170     public void activateOptions() {
171         super.activateOptions();
172         if (datePattern != null && fileName != null) {
173             now.setTime(System.currentTimeMillis());
174             sdf = new SimpleDateFormat(datePattern);
175             int type = computeCheckPeriod();
176             printPeriodicity(type);
177             rc.setType(type);
178             File file = new File(fileName);
179             scheduledFilename = simpleFileName + sdf.format(new Date(file.lastModified())) + fileExtension;
180
181         } else {
182             LogLog.error("Either File or DatePattern options are not set for appender [" + name + "].");
183         }
184     }
185
186     void printPeriodicity(int type) {
187         switch (type) {
188             case TOP_OF_MINUTE:
189                 LogLog.debug("Appender [" + name + "] to be rolled every minute.");
190                 break;
191             case TOP_OF_HOUR:
192                 LogLog.debug("Appender [" + name + "] to be rolled on top of every hour.");
193                 break;
194             case HALF_DAY:
195                 LogLog.debug("Appender [" + name + "] to be rolled at midday and midnight.");
196                 break;
197             case TOP_OF_DAY:
198                 LogLog.debug("Appender [" + name + "] to be rolled at midnight.");
199                 break;
200             case TOP_OF_WEEK:
201                 LogLog.debug("Appender [" + name + "] to be rolled at start of week.");
202                 break;
203             case TOP_OF_MONTH:
204                 LogLog.debug("Appender [" + name + "] to be rolled at start of every month.");
205                 break;
206             default:
207                 LogLog.warn("Unknown periodicity for appender [" + name + "].");
208         }
209     }
210
211     // This method computes the roll over period by looping over the
212     // periods, starting with the shortest, and stopping when the r0 is
213     // different from from r1, where r0 is the epoch formatted according
214     // the datePattern (supplied by the user) and r1 is the
215     // epoch+nextMillis(i) formatted according to datePattern. All date
216     // formatting is done in GMT and not local format because the test
217     // logic is based on comparisons relative to 1970-01-01 00:00:00
218     // GMT (the epoch).
219
220     int computeCheckPeriod() {
221         RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone, Locale.ENGLISH);
222         Date epoch = new Date(0);
223         if (datePattern != null) {
224             for (int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
225                 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
226                 simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in GMT
227                 String r0 = simpleDateFormat.format(epoch);
228                 rollingCalendar.setType(i);
229                 Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
230                 String r1 = simpleDateFormat.format(next);
231                 if (r0 != null && r1 != null && !r0.equals(r1)) {
232                     return i;
233                 }
234             }
235         }
236         return TOP_OF_TROUBLE; // Deliberately head for trouble...
237     }
238
239     /**
240      * Rollover the current file to a new file.
241      */
242     void rollOver() {
243
244         /* Compute filename, but only if datePattern is specified */
245         if (datePattern == null) {
246             errorHandler.error("Missing DatePattern option in rollOver().");
247             return;
248         }
249
250         String datedFilename = simpleFileName + sdf.format(now) + fileExtension;
251         // It is too early to roll over because we are still within the
252         // bounds of the current interval. Rollover will occur once the
253         // next interval is reached.
254         if (scheduledFilename.equals(datedFilename)) {
255             return;
256         }
257
258         // close current file, and rename it to datedFilename
259         this.closeFile();
260
261         File target = new File(scheduledFilename);
262         if (target.exists()) {
263             target.delete();
264         }
265
266         File file = new File(fileName);
267         boolean result = file.renameTo(target);
268         if (result) {
269             LogLog.debug(fileName + " -> " + scheduledFilename);
270         } else {
271             LogLog.error("Failed to rename [" + fileName + "] to [" + scheduledFilename + "].");
272         }
273
274         try {
275             // This will also close the file. This is OK since multiple
276             // close operations are safe.
277             this.setFile(fileName, false, this.bufferedIO, this.bufferSize);
278         }
279         catch (IOException e) {
280             errorHandler.error("setFile(" + fileName + ", false) call failed.");
281         }
282         scheduledFilename = datedFilename;
283     }
284
285     /**
286      * This method differentiates DailyRollingFileAppender from its super class.
287      * 
288      * <p>
289      * Before actually logging, this method will check whether it is time to do a rollover. If it is, it will schedule
290      * the next rollover time and then rollover.
291      */
292     protected void subAppend(LoggingEvent event) {
293         long n = System.currentTimeMillis();
294         if (n >= nextCheck) {
295             now.setTime(n);
296             nextCheck = rc.getNextCheckMillis(now);
297             rollOver();
298         }
299         super.subAppend(event);
300     }
301 }
302
303 /**
304  * RollingCalendar is a helper class to DailyRollingFileAppender. Given a periodicity type and the current time, it
305  * computes the start of the next interval.
306  */
307 class RollingCalendar
308         extends GregorianCalendar {
309
310     /**
311      * Serial Number
312      */
313     private static final long serialVersionUID = -1818276930015758128L;
314     
315     int type = RollingFileAppender.TOP_OF_TROUBLE;
316
317     RollingCalendar() {
318         super();
319     }
320
321     RollingCalendar(TimeZone tz, Locale locale) {
322         super(tz, locale);
323     }
324
325     void setType(int type) {
326         this.type = type;
327     }
328
329     public long getNextCheckMillis(Date now) {
330         return getNextCheckDate(now).getTime();
331     }
332
333     public Date getNextCheckDate(Date now) {
334         this.setTime(now);
335
336         switch (type) {
337             case RollingFileAppender.TOP_OF_MINUTE:
338                 this.set(Calendar.SECOND, 0);
339                 this.set(Calendar.MILLISECOND, 0);
340                 this.add(Calendar.MINUTE, 1);
341                 break;
342             case RollingFileAppender.TOP_OF_HOUR:
343                 this.set(Calendar.MINUTE, 0);
344                 this.set(Calendar.SECOND, 0);
345                 this.set(Calendar.MILLISECOND, 0);
346                 this.add(Calendar.HOUR_OF_DAY, 1);
347                 break;
348             case RollingFileAppender.HALF_DAY:
349                 this.set(Calendar.MINUTE, 0);
350                 this.set(Calendar.SECOND, 0);
351                 this.set(Calendar.MILLISECOND, 0);
352                 int hour = get(Calendar.HOUR_OF_DAY);
353                 if (hour < 12) {
354                     this.set(Calendar.HOUR_OF_DAY, 12);
355                 } else {
356                     this.set(Calendar.HOUR_OF_DAY, 0);
357                     this.add(Calendar.DAY_OF_MONTH, 1);
358                 }
359                 break;
360             case RollingFileAppender.TOP_OF_DAY:
361                 this.set(Calendar.HOUR_OF_DAY, 0);
362                 this.set(Calendar.MINUTE, 0);
363                 this.set(Calendar.SECOND, 0);
364                 this.set(Calendar.MILLISECOND, 0);
365                 this.add(Calendar.DATE, 1);
366                 break;
367             case RollingFileAppender.TOP_OF_WEEK:
368                 this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
369                 this.set(Calendar.HOUR_OF_DAY, 0);
370                 this.set(Calendar.SECOND, 0);
371                 this.set(Calendar.MILLISECOND, 0);
372                 this.add(Calendar.WEEK_OF_YEAR, 1);
373                 break;
374             case RollingFileAppender.TOP_OF_MONTH:
375                 this.set(Calendar.DATE, 1);
376                 this.set(Calendar.HOUR_OF_DAY, 0);
377                 this.set(Calendar.SECOND, 0);
378                 this.set(Calendar.MILLISECOND, 0);
379                 this.add(Calendar.MONTH, 1);
380                 break;
381             default:
382                 throw new IllegalStateException("Unknown periodicity type.");
383         }
384         return getTime();
385     }
386 }