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