Changed to use RollingFileAppender to allow for log file names to end with a log...
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / log / RollingFileAppender.java
1
2 package edu.internet2.middleware.shibboleth.log;
3
4 import java.io.File;
5 import java.io.IOException;
6 import java.text.SimpleDateFormat;
7 import java.util.Calendar;
8 import java.util.Date;
9 import java.util.GregorianCalendar;
10 import java.util.Locale;
11 import java.util.TimeZone;
12
13 import org.apache.log4j.FileAppender;
14 import org.apache.log4j.Layout;
15 import org.apache.log4j.helpers.LogLog;
16 import org.apache.log4j.spi.LoggingEvent;
17
18 /**
19  * A minor refactoring of Log4J's DailyRollingFileAppender. The log4j appender does not provide an easy mechanism for
20  * having a file name of name.date.extension, instead it wants to do name.extension.date which on some platforms can be
21  * a pain. This appender is meant to handle this case.
22  * 
23  * The file appender will create a file called filename.extension, then it will roll over the files to
24  * filename.date.extension. The default date pattern is "'.'yyyy-MM-dd" (i.e. daily roll over) and the default file
25  * extnsion is '.log'.
26  * 
27  * @author Chad La Joie
28  */
29 /*
30  * This class borrows extensively from the Log4J DailyRollingFileAppender written by Eirik Lygre and Ceki Gulcu.
31  */
32 public class RollingFileAppender
33         extends FileAppender {
34
35     // The code assumes that the following constants are in a increasing
36     // sequence.
37     static final int TOP_OF_TROUBLE = -1;
38
39     static final int TOP_OF_MINUTE = 0;
40
41     static final int TOP_OF_HOUR = 1;
42
43     static final int HALF_DAY = 2;
44
45     static final int TOP_OF_DAY = 3;
46
47     static final int TOP_OF_WEEK = 4;
48
49     static final int TOP_OF_MONTH = 5;
50
51     /**
52      * The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" meaning daily rollover.
53      */
54     private String datePattern = "'.'yyyy-MM-dd";
55
56     /**
57      * The log file will be renamed to the value of the scheduledFilename variable when the next interval is entered.
58      * For example, if the rollover period is one hour, the log file will be renamed to the value of "scheduledFilename"
59      * at the beginning of the next hour.
60      * 
61      * The precise time when a rollover occurs depends on logging activity.
62      */
63     private String scheduledFilename;
64
65     /**
66      * The next time we estimate a rollover should occur.
67      */
68     private long nextCheck = System.currentTimeMillis() - 1;
69
70     /**
71      * Current date
72      */
73     private Date now = new Date();
74
75     /**
76      * The date pattern
77      */
78     private SimpleDateFormat sdf;
79
80     /**
81      * Calendar for determing roll over information
82      */
83     private RollingCalendar rc = new RollingCalendar();
84
85     /**
86      * How often to check for roll over
87      */
88     int checkPeriod = TOP_OF_TROUBLE;
89
90     /**
91      * The gmtTimeZone is used only in computeCheckPeriod() method.
92      */
93     static final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
94
95     /**
96      * The file extension
97      */
98     private String fileExtension = ".log";
99
100     /**
101      * The file name without the date or extension components.
102      */
103     private String simpleFileName;
104
105     /**
106      * Default constructor
107      */
108     public RollingFileAppender() {
109
110     }
111
112     /**
113      * Constructor.
114      * 
115      * @param layout the log entry layout pattern
116      * @param filename the file name
117      * @param datePattern the date pattern used to determine rolling behavior
118      * @param fileExtension the file extension to post-pend to log file name
119      * 
120      * @throws IOException thrown if the file can not be created
121      */
122     public RollingFileAppender(Layout layout, String filename, String datePattern, String fileExtension)
123             throws IOException {
124         super(layout, filename + fileExtension, true);
125         simpleFileName = filename;
126         setDatePattern(datePattern);
127         setFileExtension(fileExtension);
128         activateOptions();
129     }
130
131     /**
132      * Gets the extension post-pended to the file name.
133      * 
134      * @return the log file's extension
135      */
136     public String getFileExtension() {
137         return fileExtension;
138     }
139
140     /**
141      * Sets the extension post-pended to the file name.
142      * 
143      * @param extension the log file's extension
144      */
145     public void setFileExtension(String extension) {
146         fileExtension = extension;
147     }
148
149     /**
150      * The <b>DatePattern</b> takes a string in the same format as expected by {@link SimpleDateFormat}. This options
151      * determines the rollover schedule.
152      * 
153      * @param pattern the rollover date pattern
154      */
155     public void setDatePattern(String pattern) {
156         datePattern = pattern;
157     }
158
159     /**
160      * Returns the value of the <b>DatePattern</b> option.
161      * 
162      * @return the rollover date pattern
163      */
164     public String getDatePattern() {
165         return datePattern;
166     }
167
168     public void activateOptions() {
169         super.activateOptions();
170         if (datePattern != null && fileName != null) {
171             now.setTime(System.currentTimeMillis());
172             sdf = new SimpleDateFormat(datePattern);
173             int type = computeCheckPeriod();
174             printPeriodicity(type);
175             rc.setType(type);
176             File file = new File(fileName);
177             scheduledFilename = simpleFileName + sdf.format(new Date(file.lastModified())) + fileExtension;
178
179         } else {
180             LogLog.error("Either File or DatePattern options are not set for appender [" + name + "].");
181         }
182     }
183
184     void printPeriodicity(int type) {
185         switch (type) {
186             case TOP_OF_MINUTE:
187                 LogLog.debug("Appender [" + name + "] to be rolled every minute.");
188                 break;
189             case TOP_OF_HOUR:
190                 LogLog.debug("Appender [" + name + "] to be rolled on top of every hour.");
191                 break;
192             case HALF_DAY:
193                 LogLog.debug("Appender [" + name + "] to be rolled at midday and midnight.");
194                 break;
195             case TOP_OF_DAY:
196                 LogLog.debug("Appender [" + name + "] to be rolled at midnight.");
197                 break;
198             case TOP_OF_WEEK:
199                 LogLog.debug("Appender [" + name + "] to be rolled at start of week.");
200                 break;
201             case TOP_OF_MONTH:
202                 LogLog.debug("Appender [" + name + "] to be rolled at start of every month.");
203                 break;
204             default:
205                 LogLog.warn("Unknown periodicity for appender [" + name + "].");
206         }
207     }
208
209     // This method computes the roll over period by looping over the
210     // periods, starting with the shortest, and stopping when the r0 is
211     // different from from r1, where r0 is the epoch formatted according
212     // the datePattern (supplied by the user) and r1 is the
213     // epoch+nextMillis(i) formatted according to datePattern. All date
214     // formatting is done in GMT and not local format because the test
215     // logic is based on comparisons relative to 1970-01-01 00:00:00
216     // GMT (the epoch).
217
218     int computeCheckPeriod() {
219         RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone, Locale.ENGLISH);
220         // set sate to 1970-01-01 00:00:00 GMT
221         Date epoch = new Date(0);
222         if (datePattern != null) {
223             for (int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
224                 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
225                 simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in GMT
226                 String r0 = simpleDateFormat.format(epoch);
227                 rollingCalendar.setType(i);
228                 Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
229                 String r1 = simpleDateFormat.format(next);
230                 // System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);
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     int type = RollingFileAppender.TOP_OF_TROUBLE;
311
312     RollingCalendar() {
313         super();
314     }
315
316     RollingCalendar(TimeZone tz, Locale locale) {
317         super(tz, locale);
318     }
319
320     void setType(int type) {
321         this.type = type;
322     }
323
324     public long getNextCheckMillis(Date now) {
325         return getNextCheckDate(now).getTime();
326     }
327
328     public Date getNextCheckDate(Date now) {
329         this.setTime(now);
330
331         switch (type) {
332             case RollingFileAppender.TOP_OF_MINUTE:
333                 this.set(Calendar.SECOND, 0);
334                 this.set(Calendar.MILLISECOND, 0);
335                 this.add(Calendar.MINUTE, 1);
336                 break;
337             case RollingFileAppender.TOP_OF_HOUR:
338                 this.set(Calendar.MINUTE, 0);
339                 this.set(Calendar.SECOND, 0);
340                 this.set(Calendar.MILLISECOND, 0);
341                 this.add(Calendar.HOUR_OF_DAY, 1);
342                 break;
343             case RollingFileAppender.HALF_DAY:
344                 this.set(Calendar.MINUTE, 0);
345                 this.set(Calendar.SECOND, 0);
346                 this.set(Calendar.MILLISECOND, 0);
347                 int hour = get(Calendar.HOUR_OF_DAY);
348                 if (hour < 12) {
349                     this.set(Calendar.HOUR_OF_DAY, 12);
350                 } else {
351                     this.set(Calendar.HOUR_OF_DAY, 0);
352                     this.add(Calendar.DAY_OF_MONTH, 1);
353                 }
354                 break;
355             case RollingFileAppender.TOP_OF_DAY:
356                 this.set(Calendar.HOUR_OF_DAY, 0);
357                 this.set(Calendar.MINUTE, 0);
358                 this.set(Calendar.SECOND, 0);
359                 this.set(Calendar.MILLISECOND, 0);
360                 this.add(Calendar.DATE, 1);
361                 break;
362             case RollingFileAppender.TOP_OF_WEEK:
363                 this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
364                 this.set(Calendar.HOUR_OF_DAY, 0);
365                 this.set(Calendar.SECOND, 0);
366                 this.set(Calendar.MILLISECOND, 0);
367                 this.add(Calendar.WEEK_OF_YEAR, 1);
368                 break;
369             case RollingFileAppender.TOP_OF_MONTH:
370                 this.set(Calendar.DATE, 1);
371                 this.set(Calendar.HOUR_OF_DAY, 0);
372                 this.set(Calendar.SECOND, 0);
373                 this.set(Calendar.MILLISECOND, 0);
374                 this.add(Calendar.MONTH, 1);
375                 break;
376             default:
377                 throw new IllegalStateException("Unknown periodicity type.");
378         }
379         return getTime();
380     }
381 }