1 //
2 //  ========================================================================
3 //  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4 //  ------------------------------------------------------------------------
5 //  All rights reserved. This program and the accompanying materials
6 //  are made available under the terms of the Eclipse Public License v1.0
7 //  and Apache License v2.0 which accompanies this distribution.
8 //
9 //      The Eclipse Public License is available at
10 //      http://www.eclipse.org/legal/epl-v10.html
11 //
12 //      The Apache License v2.0 is available at
13 //      http://www.opensource.org/licenses/apache2.0.php
14 //
15 //  You may elect to redistribute this code under either of these licenses.
16 //  ========================================================================
17 //
18 
19 package org.eclipse.jetty.util;
20 
21 import java.io.File;
22 import java.io.FileOutputStream;
23 import java.io.FilterOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStream;
26 import java.text.SimpleDateFormat;
27 import java.util.Calendar;
28 import java.util.Date;
29 import java.util.GregorianCalendar;
30 import java.util.Locale;
31 import java.util.TimeZone;
32 import java.util.Timer;
33 import java.util.TimerTask;
34 
35 /**
36  * RolloverFileOutputStream
37  *
38  * This output stream puts content in a file that is rolled over every 24 hours.
39  * The filename must include the string "yyyy_mm_dd", which is replaced with the
40  * actual date when creating and rolling over the file.
41  *
42  * Old files are retained for a number of days before being deleted.
43  *
44  *
45  */
46 public class RolloverFileOutputStream extends FilterOutputStream
47 {
48     private static Timer __rollover;
49 
50     final static String YYYY_MM_DD="yyyy_mm_dd";
51     final static String ROLLOVER_FILE_DATE_FORMAT = "yyyy_MM_dd";
52     final static String ROLLOVER_FILE_BACKUP_FORMAT = "HHmmssSSS";
53     final static int ROLLOVER_FILE_RETAIN_DAYS = 31;
54 
55     private RollTask _rollTask;
56     private SimpleDateFormat _fileBackupFormat;
57     private SimpleDateFormat _fileDateFormat;
58 
59     private String _filename;
60     private File _file;
61     private boolean _append;
62     private int _retainDays;
63 
64     /* ------------------------------------------------------------ */
65     /**
66      * @param filename The filename must include the string "yyyy_mm_dd",
67      * which is replaced with the actual date when creating and rolling over the file.
68      * @throws IOException
69      */
RolloverFileOutputStream(String filename)70     public RolloverFileOutputStream(String filename)
71         throws IOException
72     {
73         this(filename,true,ROLLOVER_FILE_RETAIN_DAYS);
74     }
75 
76     /* ------------------------------------------------------------ */
77     /**
78      * @param filename The filename must include the string "yyyy_mm_dd",
79      * which is replaced with the actual date when creating and rolling over the file.
80      * @param append If true, existing files will be appended to.
81      * @throws IOException
82      */
RolloverFileOutputStream(String filename, boolean append)83     public RolloverFileOutputStream(String filename, boolean append)
84         throws IOException
85     {
86         this(filename,append,ROLLOVER_FILE_RETAIN_DAYS);
87     }
88 
89     /* ------------------------------------------------------------ */
90     /**
91      * @param filename The filename must include the string "yyyy_mm_dd",
92      * which is replaced with the actual date when creating and rolling over the file.
93      * @param append If true, existing files will be appended to.
94      * @param retainDays The number of days to retain files before deleting them.  0 to retain forever.
95      * @throws IOException
96      */
RolloverFileOutputStream(String filename, boolean append, int retainDays)97     public RolloverFileOutputStream(String filename,
98                                     boolean append,
99                                     int retainDays)
100         throws IOException
101     {
102         this(filename,append,retainDays,TimeZone.getDefault());
103     }
104 
105     /* ------------------------------------------------------------ */
106     /**
107      * @param filename The filename must include the string "yyyy_mm_dd",
108      * which is replaced with the actual date when creating and rolling over the file.
109      * @param append If true, existing files will be appended to.
110      * @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
111      * @throws IOException
112      */
RolloverFileOutputStream(String filename, boolean append, int retainDays, TimeZone zone)113     public RolloverFileOutputStream(String filename,
114                                     boolean append,
115                                     int retainDays,
116                                     TimeZone zone)
117         throws IOException
118     {
119 
120          this(filename,append,retainDays,zone,null,null);
121     }
122 
123     /* ------------------------------------------------------------ */
124     /**
125      * @param filename The filename must include the string "yyyy_mm_dd",
126      * which is replaced with the actual date when creating and rolling over the file.
127      * @param append If true, existing files will be appended to.
128      * @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
129      * @param dateFormat The format for the date file substitution. The default is "yyyy_MM_dd".
130      * @param backupFormat The format for the file extension of backup files. The default is "HHmmssSSS".
131      * @throws IOException
132      */
RolloverFileOutputStream(String filename, boolean append, int retainDays, TimeZone zone, String dateFormat, String backupFormat)133     public RolloverFileOutputStream(String filename,
134                                     boolean append,
135                                     int retainDays,
136                                     TimeZone zone,
137                                     String dateFormat,
138                                     String backupFormat)
139         throws IOException
140     {
141         super(null);
142 
143         if (dateFormat==null)
144             dateFormat=ROLLOVER_FILE_DATE_FORMAT;
145         _fileDateFormat = new SimpleDateFormat(dateFormat);
146 
147         if (backupFormat==null)
148             backupFormat=ROLLOVER_FILE_BACKUP_FORMAT;
149         _fileBackupFormat = new SimpleDateFormat(backupFormat);
150 
151         _fileBackupFormat.setTimeZone(zone);
152         _fileDateFormat.setTimeZone(zone);
153 
154         if (filename!=null)
155         {
156             filename=filename.trim();
157             if (filename.length()==0)
158                 filename=null;
159         }
160         if (filename==null)
161             throw new IllegalArgumentException("Invalid filename");
162 
163         _filename=filename;
164         _append=append;
165         _retainDays=retainDays;
166         setFile();
167 
168         synchronized(RolloverFileOutputStream.class)
169         {
170             if (__rollover==null)
171                 __rollover=new Timer(RolloverFileOutputStream.class.getName(),true);
172 
173             _rollTask=new RollTask();
174 
175              Calendar now = Calendar.getInstance();
176              now.setTimeZone(zone);
177 
178              GregorianCalendar midnight =
179                  new GregorianCalendar(now.get(Calendar.YEAR),
180                          now.get(Calendar.MONTH),
181                          now.get(Calendar.DAY_OF_MONTH),
182                          23,0);
183              midnight.setTimeZone(zone);
184              midnight.add(Calendar.HOUR,1);
185              __rollover.scheduleAtFixedRate(_rollTask,midnight.getTime(),1000L*60*60*24);
186         }
187     }
188 
189     /* ------------------------------------------------------------ */
getFilename()190     public String getFilename()
191     {
192         return _filename;
193     }
194 
195     /* ------------------------------------------------------------ */
getDatedFilename()196     public String getDatedFilename()
197     {
198         if (_file==null)
199             return null;
200         return _file.toString();
201     }
202 
203     /* ------------------------------------------------------------ */
getRetainDays()204     public int getRetainDays()
205     {
206         return _retainDays;
207     }
208 
209     /* ------------------------------------------------------------ */
setFile()210     private synchronized void setFile()
211         throws IOException
212     {
213         // Check directory
214         File file = new File(_filename);
215         _filename=file.getCanonicalPath();
216         file=new File(_filename);
217         File dir= new File(file.getParent());
218         if (!dir.isDirectory() || !dir.canWrite())
219             throw new IOException("Cannot write log directory "+dir);
220 
221         Date now=new Date();
222 
223         // Is this a rollover file?
224         String filename=file.getName();
225         int i=filename.toLowerCase(Locale.ENGLISH).indexOf(YYYY_MM_DD);
226         if (i>=0)
227         {
228             file=new File(dir,
229                           filename.substring(0,i)+
230                           _fileDateFormat.format(now)+
231                           filename.substring(i+YYYY_MM_DD.length()));
232         }
233 
234         if (file.exists()&&!file.canWrite())
235             throw new IOException("Cannot write log file "+file);
236 
237         // Do we need to change the output stream?
238         if (out==null || !file.equals(_file))
239         {
240             // Yep
241             _file=file;
242             if (!_append && file.exists())
243                 file.renameTo(new File(file.toString()+"."+_fileBackupFormat.format(now)));
244             OutputStream oldOut=out;
245             out=new FileOutputStream(file.toString(),_append);
246             if (oldOut!=null)
247                 oldOut.close();
248             //if(log.isDebugEnabled())log.debug("Opened "+_file);
249         }
250     }
251 
252     /* ------------------------------------------------------------ */
removeOldFiles()253     private void removeOldFiles()
254     {
255         if (_retainDays>0)
256         {
257             long now = System.currentTimeMillis();
258 
259             File file= new File(_filename);
260             File dir = new File(file.getParent());
261             String fn=file.getName();
262             int s=fn.toLowerCase(Locale.ENGLISH).indexOf(YYYY_MM_DD);
263             if (s<0)
264                 return;
265             String prefix=fn.substring(0,s);
266             String suffix=fn.substring(s+YYYY_MM_DD.length());
267 
268             String[] logList=dir.list();
269             for (int i=0;i<logList.length;i++)
270             {
271                 fn = logList[i];
272                 if(fn.startsWith(prefix)&&fn.indexOf(suffix,prefix.length())>=0)
273                 {
274                     File f = new File(dir,fn);
275                     long date = f.lastModified();
276                     if ( ((now-date)/(1000*60*60*24))>_retainDays)
277                         f.delete();
278                 }
279             }
280         }
281     }
282 
283     /* ------------------------------------------------------------ */
284     @Override
write(byte[] buf)285     public void write (byte[] buf)
286             throws IOException
287      {
288             out.write (buf);
289      }
290 
291     /* ------------------------------------------------------------ */
292     @Override
write(byte[] buf, int off, int len)293     public void write (byte[] buf, int off, int len)
294             throws IOException
295      {
296             out.write (buf, off, len);
297      }
298 
299     /* ------------------------------------------------------------ */
300     /**
301      */
302     @Override
close()303     public void close()
304         throws IOException
305     {
306         synchronized(RolloverFileOutputStream.class)
307         {
308             try{super.close();}
309             finally
310             {
311                 out=null;
312                 _file=null;
313             }
314 
315             _rollTask.cancel();
316         }
317     }
318 
319     /* ------------------------------------------------------------ */
320     /* ------------------------------------------------------------ */
321     /* ------------------------------------------------------------ */
322     private class RollTask extends TimerTask
323     {
324         @Override
run()325         public void run()
326         {
327             try
328             {
329                 RolloverFileOutputStream.this.setFile();
330                 RolloverFileOutputStream.this.removeOldFiles();
331 
332             }
333             catch(IOException e)
334             {
335                 // Cannot log this exception to a LOG, as RolloverFOS can be used by logging
336                 e.printStackTrace();
337             }
338         }
339     }
340 }
341