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