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