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 19package org.eclipse.jetty.util; 20 21import java.io.File; 22import java.io.FileOutputStream; 23import java.io.FilterOutputStream; 24import java.io.IOException; 25import java.io.OutputStream; 26import java.text.SimpleDateFormat; 27import java.util.Calendar; 28import java.util.Date; 29import java.util.GregorianCalendar; 30import java.util.Locale; 31import java.util.TimeZone; 32import java.util.Timer; 33import 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 */ 46public 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 */ 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 */ 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 */ 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 */ 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 */ 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 /* ------------------------------------------------------------ */ 190 public String getFilename() 191 { 192 return _filename; 193 } 194 195 /* ------------------------------------------------------------ */ 196 public String getDatedFilename() 197 { 198 if (_file==null) 199 return null; 200 return _file.toString(); 201 } 202 203 /* ------------------------------------------------------------ */ 204 public int getRetainDays() 205 { 206 return _retainDays; 207 } 208 209 /* ------------------------------------------------------------ */ 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 /* ------------------------------------------------------------ */ 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 285 public void write (byte[] buf) 286 throws IOException 287 { 288 out.write (buf); 289 } 290 291 /* ------------------------------------------------------------ */ 292 @Override 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 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 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