//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.Timer;
import java.util.TimerTask;
RolloverFileOutputStream.
This output stream puts content in a file that is rolled over every 24 hours.
The filename must include the string "yyyy_mm_dd", which is replaced with the
actual date when creating and rolling over the file.
Old files are retained for a number of days before being deleted.
/**
* RolloverFileOutputStream.
*
* <p>
* This output stream puts content in a file that is rolled over every 24 hours.
* The filename must include the string "yyyy_mm_dd", which is replaced with the
* actual date when creating and rolling over the file.
* </p>
* <p>
* Old files are retained for a number of days before being deleted.
* </p>
*/
public class RolloverFileOutputStream extends OutputStream
{
private static Timer __rollover;
static final String YYYY_MM_DD = "yyyy_mm_dd";
static final String ROLLOVER_FILE_DATE_FORMAT = "yyyy_MM_dd";
static final String ROLLOVER_FILE_BACKUP_FORMAT = "HHmmssSSS";
static final int ROLLOVER_FILE_RETAIN_DAYS = 31;
private OutputStream _out;
private RollTask _rollTask;
private SimpleDateFormat _fileBackupFormat;
private SimpleDateFormat _fileDateFormat;
private String _filename;
private File _file;
private boolean _append;
private int _retainDays;
Params: - filename – The filename must include the string "yyyy_mm_dd",
which is replaced with the actual date when creating and rolling over the file.
Throws: - IOException – if unable to create output
/**
* @param filename The filename must include the string "yyyy_mm_dd",
* which is replaced with the actual date when creating and rolling over the file.
* @throws IOException if unable to create output
*/
public RolloverFileOutputStream(String filename)
throws IOException
{
this(filename, true, ROLLOVER_FILE_RETAIN_DAYS);
}
Params: - filename – The filename must include the string "yyyy_mm_dd",
which is replaced with the actual date when creating and rolling over the file.
- append – If true, existing files will be appended to.
Throws: - IOException – if unable to create output
/**
* @param filename The filename must include the string "yyyy_mm_dd",
* which is replaced with the actual date when creating and rolling over the file.
* @param append If true, existing files will be appended to.
* @throws IOException if unable to create output
*/
public RolloverFileOutputStream(String filename, boolean append)
throws IOException
{
this(filename, append, ROLLOVER_FILE_RETAIN_DAYS);
}
Params: - filename – The filename must include the string "yyyy_mm_dd",
which is replaced with the actual date when creating and rolling over the file.
- append – If true, existing files will be appended to.
- retainDays – The number of days to retain files before deleting them. 0 to retain forever.
Throws: - IOException – if unable to create output
/**
* @param filename The filename must include the string "yyyy_mm_dd",
* which is replaced with the actual date when creating and rolling over the file.
* @param append If true, existing files will be appended to.
* @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
* @throws IOException if unable to create output
*/
public RolloverFileOutputStream(String filename,
boolean append,
int retainDays)
throws IOException
{
this(filename, append, retainDays, TimeZone.getDefault());
}
Params: - filename – The filename must include the string "yyyy_mm_dd",
which is replaced with the actual date when creating and rolling over the file.
- append – If true, existing files will be appended to.
- retainDays – The number of days to retain files before deleting them. 0 to retain forever.
- zone – the timezone for the output
Throws: - IOException – if unable to create output
/**
* @param filename The filename must include the string "yyyy_mm_dd",
* which is replaced with the actual date when creating and rolling over the file.
* @param append If true, existing files will be appended to.
* @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
* @param zone the timezone for the output
* @throws IOException if unable to create output
*/
public RolloverFileOutputStream(String filename,
boolean append,
int retainDays,
TimeZone zone)
throws IOException
{
this(filename, append, retainDays, zone, null, null, ZonedDateTime.now(zone.toZoneId()));
}
Params: - filename – The filename must include the string "yyyy_mm_dd",
which is replaced with the actual date when creating and rolling over the file.
- append – If true, existing files will be appended to.
- retainDays – The number of days to retain files before deleting them. 0 to retain forever.
- zone – the timezone for the output
- dateFormat – The format for the date file substitution. The default is "yyyy_MM_dd". If set to the
empty string, the file is rolledover to the same filename, with the current file being renamed to the backup filename.
- backupFormat – The format for the file extension of backup files. The default is "HHmmssSSS".
Throws: - IOException – if unable to create output
/**
* @param filename The filename must include the string "yyyy_mm_dd",
* which is replaced with the actual date when creating and rolling over the file.
* @param append If true, existing files will be appended to.
* @param retainDays The number of days to retain files before deleting them. 0 to retain forever.
* @param zone the timezone for the output
* @param dateFormat The format for the date file substitution. The default is "yyyy_MM_dd". If set to the
* empty string, the file is rolledover to the same filename, with the current file being renamed to the backup filename.
* @param backupFormat The format for the file extension of backup files. The default is "HHmmssSSS".
* @throws IOException if unable to create output
*/
public RolloverFileOutputStream(String filename,
boolean append,
int retainDays,
TimeZone zone,
String dateFormat,
String backupFormat)
throws IOException
{
this(filename, append, retainDays, zone, dateFormat, backupFormat, ZonedDateTime.now(zone.toZoneId()));
}
RolloverFileOutputStream(String filename,
boolean append,
int retainDays,
TimeZone zone,
String dateFormat,
String backupFormat,
ZonedDateTime now)
throws IOException
{
if (dateFormat == null)
dateFormat = ROLLOVER_FILE_DATE_FORMAT;
_fileDateFormat = new SimpleDateFormat(dateFormat);
if (backupFormat == null)
backupFormat = ROLLOVER_FILE_BACKUP_FORMAT;
_fileBackupFormat = new SimpleDateFormat(backupFormat);
_fileBackupFormat.setTimeZone(zone);
_fileDateFormat.setTimeZone(zone);
if (filename != null)
{
filename = filename.trim();
if (filename.length() == 0)
filename = null;
}
if (filename == null)
throw new IllegalArgumentException("Invalid filename");
_filename = filename;
_append = append;
_retainDays = retainDays;
// Calculate Today's Midnight, based on Configured TimeZone (will be in past, even if by a few milliseconds)
setFile(now);
synchronized (RolloverFileOutputStream.class)
{
if (__rollover == null)
__rollover = new Timer(RolloverFileOutputStream.class.getName(), true);
}
// This will schedule the rollover event to the next midnight
scheduleNextRollover(now);
}
Get the "start of day" for the provided DateTime at the zone specified.
Params: - now – the date time to calculate from
Returns: start of the day of the date provided
/**
* Get the "start of day" for the provided DateTime at the zone specified.
*
* @param now the date time to calculate from
* @return start of the day of the date provided
*/
public static ZonedDateTime toMidnight(ZonedDateTime now)
{
return now.toLocalDate().atStartOfDay(now.getZone()).plus(1, ChronoUnit.DAYS);
}
private void scheduleNextRollover(ZonedDateTime now)
{
_rollTask = new RollTask();
// Get tomorrow's midnight based on Configured TimeZone
ZonedDateTime midnight = toMidnight(now);
// Schedule next rollover event to occur, based on local machine's Unix Epoch milliseconds
long delay = midnight.toInstant().toEpochMilli() - now.toInstant().toEpochMilli();
synchronized (RolloverFileOutputStream.class)
{
__rollover.schedule(_rollTask, delay);
}
}
public String getFilename()
{
return _filename;
}
public String getDatedFilename()
{
if (_file == null)
return null;
return _file.toString();
}
public int getRetainDays()
{
return _retainDays;
}
void setFile(ZonedDateTime now)
throws IOException
{
File oldFile = null;
File newFile = null;
File backupFile = null;
synchronized (this)
{
// Check directory
File file = new File(_filename);
_filename = file.getCanonicalPath();
file = new File(_filename);
File dir = new File(file.getParent());
if (!dir.isDirectory() || !dir.canWrite())
throw new IOException("Cannot write log directory " + dir);
// Is this a rollover file?
String filename = file.getName();
int datePattern = filename.toLowerCase(Locale.ENGLISH).indexOf(YYYY_MM_DD);
if (datePattern >= 0)
{
file = new File(dir,
filename.substring(0, datePattern) +
_fileDateFormat.format(new Date(now.toInstant().toEpochMilli())) +
filename.substring(datePattern + YYYY_MM_DD.length()));
}
if (file.exists() && !file.canWrite())
throw new IOException("Cannot write log file " + file);
// Do we need to change the output stream?
if (_out == null || datePattern >= 0)
{
// Yep
oldFile = _file;
_file = file;
newFile = _file;
OutputStream oldOut = _out;
if (oldOut != null)
oldOut.close();
if (!_append && file.exists())
{
backupFile = new File(file.toString() + "." + _fileBackupFormat.format(new Date(now.toInstant().toEpochMilli())));
renameFile(file, backupFile);
}
_out = new FileOutputStream(file.toString(), _append);
//if(log.isDebugEnabled())log.debug("Opened "+_file);
}
}
if (newFile != null)
rollover(oldFile, backupFile, newFile);
}
private void renameFile(File src, File dest) throws IOException
{
// Try old school rename
if (!src.renameTo(dest))
{
try
{
// Try new move
Files.move(src.toPath(), dest.toPath());
}
catch (IOException e)
{
// Copy
Files.copy(src.toPath(), dest.toPath());
// Delete
Files.deleteIfExists(src.toPath());
}
}
}
This method is called whenever a log file is rolled over
Params: - oldFile – The original filename or null if this is the first creation
- backupFile – The backup filename or null if the filename is dated.
- newFile – The new filename that is now being used for logging
/**
* This method is called whenever a log file is rolled over
*
* @param oldFile The original filename or null if this is the first creation
* @param backupFile The backup filename or null if the filename is dated.
* @param newFile The new filename that is now being used for logging
*/
protected void rollover(File oldFile, File backupFile, File newFile)
{
}
void removeOldFiles(ZonedDateTime now)
{
if (_retainDays > 0)
{
// Establish expiration time, based on configured TimeZone
long expired = now.minus(_retainDays, ChronoUnit.DAYS).toInstant().toEpochMilli();
File file = new File(_filename);
File dir = new File(file.getParent());
String fn = file.getName();
int s = fn.toLowerCase(Locale.ENGLISH).indexOf(YYYY_MM_DD);
if (s < 0)
return;
String prefix = fn.substring(0, s);
String suffix = fn.substring(s + YYYY_MM_DD.length());
String[] logList = dir.list();
for (int i = 0; i < logList.length; i++)
{
fn = logList[i];
if (fn.startsWith(prefix) && fn.indexOf(suffix, prefix.length()) >= 0)
{
File f = new File(dir, fn);
if (f.lastModified() < expired)
{
f.delete();
}
}
}
}
}
@Override
public void write(int b) throws IOException
{
synchronized (this)
{
_out.write(b);
}
}
@Override
public void write(byte[] buf)
throws IOException
{
synchronized (this)
{
_out.write(buf);
}
}
@Override
public void write(byte[] buf, int off, int len)
throws IOException
{
synchronized (this)
{
_out.write(buf, off, len);
}
}
@Override
public void flush() throws IOException
{
synchronized (this)
{
_out.flush();
}
}
@Override
public void close()
throws IOException
{
synchronized (this)
{
try
{
_out.close();
}
finally
{
_out = null;
_file = null;
}
}
synchronized (RolloverFileOutputStream.class)
{
if (_rollTask != null)
{
_rollTask.cancel();
}
}
}
private class RollTask extends TimerTask
{
@Override
public void run()
{
try
{
ZonedDateTime now = ZonedDateTime.now(_fileDateFormat.getTimeZone().toZoneId());
RolloverFileOutputStream.this.setFile(now);
RolloverFileOutputStream.this.removeOldFiles(now);
RolloverFileOutputStream.this.scheduleNextRollover(now);
}
catch (Throwable t)
{
// Cannot log this exception to a LOG, as RolloverFOS can be used by logging
t.printStackTrace(System.err);
}
}
}
}