1 //========================================================================
2 //Copyright 2006 Mort Bay Consulting Pty. Ltd.
3 //------------------------------------------------------------------------
4 //Licensed under the Apache License, Version 2.0 (the "License");
5 //you may not use this file except in compliance with the License.
6 //You may obtain a copy of the License at
7 //http://www.apache.org/licenses/LICENSE-2.0
8 //Unless required by applicable law or agreed to in writing, software
9 //distributed under the License is distributed on an "AS IS" BASIS,
10 //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 //See the License for the specific language governing permissions and
12 //limitations under the License.
13 //========================================================================
14
15 package org.mortbay.util;
16
17 import java.io.File;
18 import java.io.FileOutputStream;
19 import java.io.FilterOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.text.SimpleDateFormat;
23 import java.util.Calendar;
24 import java.util.Date;
25 import java.util.GregorianCalendar;
26 import java.util.TimeZone;
27 import java.util.Timer;
28 import java.util.TimerTask;
29
30 /**
31 * RolloverFileOutputStream
32 *
33 * This output stream puts content in a file that is rolled over every 24 hours.
34 * The filename must include the string "yyyy_mm_dd", which is replaced with the
35 * actual date when creating and rolling over the file.
36 *
37 * Old files are retained for a number of days before being deleted.
38 *
39 * @author Greg Wilkins
40 */
41 public class RolloverFileOutputStream extends FilterOutputStream
42 {
43 private static Timer __rollover;
44
45 final static String YYYY_MM_DD="yyyy_mm_dd";
46
47 private RollTask _rollTask;
48 private SimpleDateFormat _fileBackupFormat;
49 private SimpleDateFormat _fileDateFormat;
50
51 private String _filename;
52 private File _file;
53 private boolean _append;
54 private int _retainDays;
55
56 /* ------------------------------------------------------------ */
57 /**
58 * @param filename The filename must include the string "yyyy_mm_dd",
59 * which is replaced with the actual date when creating and rolling over the file.
60 * @throws IOException
61 */
62 public RolloverFileOutputStream(String filename)
63 throws IOException
64 {
65 this(filename,true,Integer.getInteger("ROLLOVERFILE_RETAIN_DAYS",31).intValue());
66 }
67
68 /* ------------------------------------------------------------ */
69 /**
70 * @param filename The filename must include the string "yyyy_mm_dd",
71 * which is replaced with the actual date when creating and rolling over the file.
72 * @param append If true, existing files will be appended to.
73 * @throws IOException
74 */
75 public RolloverFileOutputStream(String filename, boolean append)
76 throws IOException
77 {
78 this(filename,append,Integer.getInteger("ROLLOVERFILE_RETAIN_DAYS",31).intValue());
79 }
80
81 /* ------------------------------------------------------------ */
82 /**
83 * @param filename The filename must include the string "yyyy_mm_dd",
84 * which is replaced with the actual date when creating and rolling over the file.
85 * @param append If true, existing files will be appended to.
86 * @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
87 * @throws IOException
88 */
89 public RolloverFileOutputStream(String filename,
90 boolean append,
91 int retainDays)
92 throws IOException
93 {
94 this(filename,append,retainDays,TimeZone.getDefault());
95 }
96
97 /* ------------------------------------------------------------ */
98 /**
99 * @param filename The filename must include the string "yyyy_mm_dd",
100 * which is replaced with the actual date when creating and rolling over the file.
101 * @param append If true, existing files will be appended to.
102 * @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
103 * @throws IOException
104 */
105 public RolloverFileOutputStream(String filename,
106 boolean append,
107 int retainDays,
108 TimeZone zone)
109 throws IOException
110 {
111
112 this(filename,append,retainDays,zone,null,null);
113 }
114
115 /* ------------------------------------------------------------ */
116 /**
117 * @param filename The filename must include the string "yyyy_mm_dd",
118 * which is replaced with the actual date when creating and rolling over the file.
119 * @param append If true, existing files will be appended to.
120 * @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
121 * @param dateFormat The format for the date file substitution. If null the system property ROLLOVERFILE_DATE_FORMAT is
122 * used and if that is null, then default is "yyyy_MM_dd".
123 * @param backupFormat The format for the file extension of backup files. If null the system property
124 * ROLLOVERFILE_BACKUP_FORMAT is used and if that is null, then 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=System.getProperty("ROLLOVERFILE_DATE_FORMAT","yyyy_MM_dd");
139 _fileDateFormat = new SimpleDateFormat(dateFormat);
140
141 if (backupFormat==null)
142 backupFormat=System.getProperty("ROLLOVERFILE_BACKUP_FORMAT","HHmmssSSS");
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(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 public void write (byte[] buf)
279 throws IOException
280 {
281 out.write (buf);
282 }
283
284 /* ------------------------------------------------------------ */
285 public void write (byte[] buf, int off, int len)
286 throws IOException
287 {
288 out.write (buf, off, len);
289 }
290
291 /* ------------------------------------------------------------ */
292 /**
293 */
294 public void close()
295 throws IOException
296 {
297 synchronized(RolloverFileOutputStream.class)
298 {
299 try{super.close();}
300 finally
301 {
302 out=null;
303 _file=null;
304 }
305
306 _rollTask.cancel();
307 }
308 }
309
310 /* ------------------------------------------------------------ */
311 /* ------------------------------------------------------------ */
312 /* ------------------------------------------------------------ */
313 private class RollTask extends TimerTask
314 {
315 public void run()
316 {
317 try
318 {
319 RolloverFileOutputStream.this.setFile();
320 RolloverFileOutputStream.this.removeOldFiles();
321
322 }
323 catch(IOException e)
324 {
325 e.printStackTrace();
326 }
327 }
328 }
329 }