Made attribution more specific.
[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         // set sate to 1970-01-01 00:00:00 GMT
223         Date epoch = new Date(0);
224         if (datePattern != null) {
225             for (int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
226                 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
227                 simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in GMT
228                 String r0 = simpleDateFormat.format(epoch);
229                 rollingCalendar.setType(i);
230                 Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
231                 String r1 = simpleDateFormat.format(next);
232                 // System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);
233                 if (r0 != null && r1 != null && !r0.equals(r1)) {
234                     return i;
235                 }
236             }
237         }
238         return TOP_OF_TROUBLE; // Deliberately head for trouble...
239     }
240
241     /**
242      * Rollover the current file to a new file.
243      */
244     void rollOver() {
245
246         /* Compute filename, but only if datePattern is specified */
247         if (datePattern == null) {
248             errorHandler.error("Missing DatePattern option in rollOver().");
249             return;
250         }
251
252         String datedFilename = simpleFileName + sdf.format(now) + fileExtension;
253         // It is too early to roll over because we are still within the
254         // bounds of the current interval. Rollover will occur once the
255         // next interval is reached.
256         if (scheduledFilename.equals(datedFilename)) {
257             return;
258         }
259
260         // close current file, and rename it to datedFilename
261         this.closeFile();
262
263         File target = new File(scheduledFilename);
264         if (target.exists()) {
265             target.delete();
266         }
267
268         File file = new File(fileName);
269         boolean result = file.renameTo(target);
270         if (result) {
271             LogLog.debug(fileName + " -> " + scheduledFilename);
272         } else {
273             LogLog.error("Failed to rename [" + fileName + "] to [" + scheduledFilename + "].");
274         }
275
276         try {
277             // This will also close the file. This is OK since multiple
278             // close operations are safe.
279             this.setFile(fileName, false, this.bufferedIO, this.bufferSize);
280         }
281         catch (IOException e) {
282             errorHandler.error("setFile(" + fileName + ", false) call failed.");
283         }
284         scheduledFilename = datedFilename;
285     }
286
287     /**
288      * This method differentiates DailyRollingFileAppender from its super class.
289      * 
290      * <p>
291      * Before actually logging, this method will check whether it is time to do a rollover. If it is, it will schedule
292      * the next rollover time and then rollover.
293      */
294     protected void subAppend(LoggingEvent event) {
295         long n = System.currentTimeMillis();
296         if (n >= nextCheck) {
297             now.setTime(n);
298             nextCheck = rc.getNextCheckMillis(now);
299             rollOver();
300         }
301         super.subAppend(event);
302     }
303 }
304
305 /**
306  * RollingCalendar is a helper class to DailyRollingFileAppender. Given a periodicity type and the current time, it
307  * computes the start of the next interval.
308  */
309 class RollingCalendar
310         extends GregorianCalendar {
311
312     /**
313      * Serial Number
314      */
315     private static final long serialVersionUID = -1818276930015758128L;
316     
317     int type = RollingFileAppender.TOP_OF_TROUBLE;
318
319     RollingCalendar() {
320         super();
321     }
322
323     RollingCalendar(TimeZone tz, Locale locale) {
324         super(tz, locale);
325     }
326
327     void setType(int type) {
328         this.type = type;
329     }
330
331     public long getNextCheckMillis(Date now) {
332         return getNextCheckDate(now).getTime();
333     }
334
335     public Date getNextCheckDate(Date now) {
336         this.setTime(now);
337
338         switch (type) {
339             case RollingFileAppender.TOP_OF_MINUTE:
340                 this.set(Calendar.SECOND, 0);
341                 this.set(Calendar.MILLISECOND, 0);
342                 this.add(Calendar.MINUTE, 1);
343                 break;
344             case RollingFileAppender.TOP_OF_HOUR:
345                 this.set(Calendar.MINUTE, 0);
346                 this.set(Calendar.SECOND, 0);
347                 this.set(Calendar.MILLISECOND, 0);
348                 this.add(Calendar.HOUR_OF_DAY, 1);
349                 break;
350             case RollingFileAppender.HALF_DAY:
351                 this.set(Calendar.MINUTE, 0);
352                 this.set(Calendar.SECOND, 0);
353                 this.set(Calendar.MILLISECOND, 0);
354                 int hour = get(Calendar.HOUR_OF_DAY);
355                 if (hour < 12) {
356                     this.set(Calendar.HOUR_OF_DAY, 12);
357                 } else {
358                     this.set(Calendar.HOUR_OF_DAY, 0);
359                     this.add(Calendar.DAY_OF_MONTH, 1);
360                 }
361                 break;
362             case RollingFileAppender.TOP_OF_DAY:
363                 this.set(Calendar.HOUR_OF_DAY, 0);
364                 this.set(Calendar.MINUTE, 0);
365                 this.set(Calendar.SECOND, 0);
366                 this.set(Calendar.MILLISECOND, 0);
367                 this.add(Calendar.DATE, 1);
368                 break;
369             case RollingFileAppender.TOP_OF_WEEK:
370                 this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
371                 this.set(Calendar.HOUR_OF_DAY, 0);
372                 this.set(Calendar.SECOND, 0);
373                 this.set(Calendar.MILLISECOND, 0);
374                 this.add(Calendar.WEEK_OF_YEAR, 1);
375                 break;
376             case RollingFileAppender.TOP_OF_MONTH:
377                 this.set(Calendar.DATE, 1);
378                 this.set(Calendar.HOUR_OF_DAY, 0);
379                 this.set(Calendar.SECOND, 0);
380                 this.set(Calendar.MILLISECOND, 0);
381                 this.add(Calendar.MONTH, 1);
382                 break;
383             default:
384                 throw new IllegalStateException("Unknown periodicity type.");
385         }
386         return getTime();
387     }
388 }