
net.sf.eBus.util.logging.CalendarFileHandler Maven / Gradle / Ivy
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later
// version.
//
// This library is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General
// Public License along with this library; if not, write to the
//
// Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330,
// Boston, MA
// 02111-1307 USA
//
// The Initial Developer of the Original Code is Charles W. Rapp.
// Portions created by Charles W. Rapp are
// Copyright (C) 2001 - 2005, 2013. Charles W. Rapp.
// All Rights Reserved.
//
package net.sf.eBus.util.logging;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.StreamHandler;
import net.sf.eBus.util.TimerEvent;
import net.sf.eBus.util.TimerTask;
import net.sf.eBus.util.TimerTaskListener;
/**
* Logs messages to a user-specified file, rolling over to a
* new file at midnight. Log files are kept for only so many
* days before {@code CalendarFileHandler} deletes them.
* This retention limit is configurable but defaults to 10 days.
*
* The {@code CalendarFileHandler} uses three parameters to
* generate the complete file name:
*
* <base name>.<date pattern>.<extension>
*
*
* -
* Base Name: Store the log files here using this base
* name.
*
* Example: {@code /var/log/app/app}
*
* -
* Date Pattern: Use this pattern to format the date
* portion of the file name.
*
* The data pattern is passed to a
* {@link java.text.SimpleDateFormat#SimpleDateFormat(String)}. See
* {@link java.text.SimpleDateFormat} for a detailed
* explanation of valid date formats.
*
* -
* Extension: This is first part of the log file's
* name.
*
* Example: log
*
*
*
* Given the base name {@code /var/log/eBus/eBus}, a date
* pattern "ddMMyyyy" and extension {@code log}, the
* July 15, 2001 log file name is
* {@code /var/log/eBus/eBus.15072001.log}
*
* Configuration: {@code CalendarFileHandler}
* default configuration uses the following LogManager
* properties. If the named properties are either not defined or
* have invalid values, then the default settings are used.
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.basename
* (defaults to "./Logger")
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.pattern
* (defaults to "yyyyMMdd")
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.extension
* (defaults to "log")
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.days_kept
* (defaults to 10 days)
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.formatter
* (defaults to "net.sf.eBus.util.logging.PatternFormatter")
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.level
* (defaults to system default)
*
*
*
* @author Charles Rapp
*/
public final class CalendarFileHandler
extends StreamHandler
implements TimerTaskListener
{
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new {@link CalendarFileHandler} and configures
* it according to {@code LogManager} configuration
* properties.
*/
public CalendarFileHandler()
{
super ();
_logRollTimer = new TimerTask(this);
// Get the handler property settings.
final LogManager manager = LogManager.getLogManager();
String value;
int daysKept = DEFAULT_DAYS_KEPT;
value =
getProperty(BASENAME_KEY, DEFAULT_BASENAME, manager);
_directory = dirname(value);
_basename = basename(value);
_extension =
getProperty(
EXTENSION_KEY, DEFAULT_EXTENSION, manager);
_dateFormat =
getProperty(
PATTERN_KEY, DEFAULT_DATE_FORMAT, manager);
// Get formatter class name and instantiate an instance.
value = manager.getProperty(FORMATTER_KEY);
if (value != null && value.length() > 0)
{
final Formatter formatter = createFormatter(value);
if (formatter != null)
{
setFormatter(formatter);
}
}
// Get the log level. Do nothing if not specified.
value = manager.getProperty(LEVEL_KEY);
if (value != null && value.length() > 0)
{
try
{
setLevel(Level.parse(value));
}
catch (IllegalArgumentException |
SecurityException jex)
{
// Ignore exceptions - use default level.
}
}
// Get the days kept. Use default value if invalid.
value = manager.getProperty(DAYS_KEPT_KEY);
if (value != null && value.length() > 0)
{
try
{
daysKept = Integer.parseInt(value);
}
catch (Exception jex)
{}
}
_daysKept = daysKept;
// Finish up by setting the handler's output stream as
// per the configuration.
final OutputStream logStream =
openLogStream(
_directory, _basename, _dateFormat, _extension);
if (logStream != null)
{
setOutputStream(logStream);
}
deleteLogFiles();
startMidnightTimer(_logRollTimer);
} // end of CalendarFileHandler()
/**
* Creates a new {@link CalendarFileHandler} instance for
* the specified base name, date format pattern, file name
* extension and how long to keep the files around.
* @param baseName where to put the log files.
* @param datePattern date format
* @param extension file name extension
* @param daysKept how long the log files are kept around
* (in days).
* @exception IllegalArgumentException
* if:
*
* -
* if {@code baseName}, {@code datePattern} or
* {@code extension} is {@code null}.
*
* -
* {@code baseName}, {@code datePattern} or
* {@code extension} is an empty string.
*
* -
* {@code daysKept} is < {@link #MIN_DAYS_KEPT}
* or > {@link #MAX_DAYS_KEPT}.
*
* -
* {@code datePattern} is an invalid date format
* pattern as per
* {@code java.text.SimpleDateFormat}.
*
* -
* {@code baseName} is in an unknown directory or
* directory cannot be accessed.
*
*
*/
public CalendarFileHandler(final String baseName,
final String datePattern,
final String extension,
final int daysKept)
throws IllegalArgumentException
{
super ();
if (baseName == null)
{
throw (
new IllegalArgumentException("null baseName"));
}
else if (datePattern == null)
{
throw (
new IllegalArgumentException(
"null datePattern"));
}
else if (extension == null)
{
throw (
new IllegalArgumentException("null extension"));
}
else if (baseName.length() == 0)
{
throw (
new IllegalArgumentException("empty baseName"));
}
else if (datePattern.length() == 0)
{
throw (
new IllegalArgumentException(
"empty datePattern"));
}
else if (extension.length() == 0)
{
throw (
new IllegalArgumentException("empty extension"));
}
_directory = dirname(baseName);
_basename = basename(baseName);
_dateFormat = datePattern;
_extension = extension;
_daysKept = daysKept;
_logRollTimer = new TimerTask(this);
final File dir = new File(_directory);
// Make sure the directory is valid. If not, then use
// the current working directory.
if (dir.exists() == false ||
dir.isDirectory() == false ||
dir.canWrite() == false)
{
throw (
new IllegalArgumentException(
_directory + " is an invalid directory"));
}
// Get the handler property settings.
final LogManager manager = LogManager.getLogManager();
String value;
// Get formatter class name and instantiate an instance.
value = manager.getProperty(FORMATTER_KEY);
if (value != null && value.length() > 0)
{
final Formatter formatter = createFormatter(value);
if (formatter != null)
{
setFormatter(formatter);
}
}
// Get the log level. Do nothing if not specified.
value = manager.getProperty(LEVEL_KEY);
if (value != null && value.length() > 0)
{
try
{
setLevel(Level.parse(value));
}
catch (IllegalArgumentException |
SecurityException jex)
{
// Ignore exceptions - use default level.
}
}
// Finish up by setting the handler's output stream as
// per the configuration.
final OutputStream logStream =
openLogStream(
_directory, _basename, _dateFormat, _extension);
if (logStream != null)
{
setOutputStream(logStream);
}
deleteLogFiles();
startMidnightTimer(_logRollTimer);
} // end of CalendarFileHandler(String, String, String, int)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// TimerTaskListener Interface Implementation.
//
/**
* Time to roll over to the next log file.
* @param task the roll file timer task.
*/
@Override
public void handleTimeout(final TimerEvent task)
{
final OutputStream logStream =
openLogStream(
_directory, _basename, _dateFormat, _extension);
LogRecord logRecord;
logRecord =
new LogRecord(Level.INFO, "Rolling log file.");
logRecord.setSourceClassName(
CalendarFileHandler.class.getName());
logRecord.setSourceMethodName("handleTimeout");
publish(logRecord);
deleteLogFiles();
// Close the current log file and open the new log
if (logStream != null)
{
setOutputStream(logStream);
}
logRecord =
new LogRecord(
Level.INFO, "Finished rolling log file.");
logRecord.setSourceClassName(
CalendarFileHandler.class.getName());
logRecord.setSourceMethodName("handleTimeout");
publish(logRecord);
// Schedule the next log roll.
// Why keep rescheduling? Why not schedule a repeating
// timer task?
// Because the local timezone might use
// daylight savings time.
_logRollTimer = new TimerTask(this);
startMidnightTimer(_logRollTimer);
return;
} // end of handleTimeout(TimerEvent)
//
// end of TimerTaskListener Interface Implementation.
//-----------------------------------------------------------
/**
* Flushes the output stream after {@link StreamHandler}
* publishes the log record. {@code StreamHandler} does not
* do this which means records are not seen in the log
* file as they are published.
* @param record Publish this log record to the log file.
*/
@Override
public void publish(final LogRecord record)
{
super.publish(record);
super.flush();
return;
} // end of publish(LogRecord)
// Performs the work of deleting out-of-date log files
private void deleteLogFiles()
{
final Calendar calendar = Calendar.getInstance();
final File directory = new File(_directory);
LogFileFilter logFilter;
File[] logFiles;
// Get today's date and subtract the days kept.
// Then set the time to midnight (00:00:00 AM).
// Delete any log files older than that date.
calendar.add(Calendar.DAY_OF_MONTH, (_daysKept * -1));
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// Create the log name filter.
logFilter =
new LogFileFilter(
directory,
_basename + ".", _dateFormat,
"." + _extension,
calendar.getTime());
// Now get the list of expired log files.
logFiles = directory.listFiles(logFilter);
// Are there any log files to be deleted?
if (logFiles != null && logFiles.length > 0)
{
final java.util.Formatter buffer =
new java.util.Formatter();
LogRecord logRecord;
int index;
buffer.format("Deleted the following log files:");
// Go through each of the log files.
for (index = 0; index < logFiles.length; ++index)
{
buffer.format(
"%n %s", logFiles[index].getName());
if (logFiles[index].delete() == false)
{
buffer.format(" failed");
}
}
logRecord =
new LogRecord(Level.INFO, buffer.toString());
logRecord.setSourceClassName(
CalendarFileHandler.class.getName());
logRecord.setSourceMethodName("handleTimeout");
publish(logRecord);
}
return;
} // end of deleteLogFiles()
// Opens an output stream for the calendar log file.
private static OutputStream openLogStream(
final String directory,
final String basename,
final String datePattern,
final String extension)
{
OutputStream retval = null;
try
{
final File logFile =
new File(
generateLogFilename(
directory,
basename,
datePattern,
extension));
retval = new FileOutputStream(logFile, true);
}
catch (IOException ioex)
{
// Return null.
}
return (retval);
} // end of openLogStream(String, String, String, String)
// Generates the log file name based on the given
// information.
private static String generateLogFilename(
final String directory,
final String basename,
final String datePattern,
final String extension)
throws IOException
{
final SimpleDateFormat formatter =
new SimpleDateFormat(datePattern);
return (
String.format(
"%s%c%s.%s.%s",
directory,
File.separatorChar,
basename,
formatter.format(new Date()),
extension));
} // end of generateLogFilename(String, String, String)
// Returns the named property or its default value.
private static String getProperty(final String key,
final String defaultValue,
final LogManager manager)
{
String retval = manager.getProperty(key);
if (retval == null || retval.length() == 0)
{
retval = defaultValue;
}
return (retval);
} // end of getProperty(String, String, LogManager)
// Creates this handler's formatter based on the specified
// class name.
@SuppressWarnings("unchecked")
private static Formatter createFormatter(
final String className)
{
Formatter retval = null;
try
{
final Class extends Formatter> formatterClass =
(Class extends Formatter>)
Class.forName(className);
final Constructor extends Formatter> ctor =
formatterClass.getDeclaredConstructor();
retval = ctor.newInstance();
}
catch (ClassNotFoundException |
NoSuchMethodException |
SecurityException |
InstantiationException |
IllegalAccessException |
IllegalArgumentException |
InvocationTargetException jex)
{
// Do nothing if any exception is caught.
}
return (retval);
} // end of createFormatter(String)
// Returns the directory portion of this baseName.
private static String dirname(final String baseName)
{
final int index = baseName.lastIndexOf((int) '/');
String retval;
if (index < 0)
{
retval = ".";
}
else
{
retval = baseName.substring(0, index);
}
return (retval);
} // end of dirname(String)
// Returns the file name portion.
private static String basename(final String baseName)
{
final int index = baseName.lastIndexOf((int) '/');
String retval;
if (index < 0)
{
retval = baseName;
}
else
{
retval = baseName.substring(index + 1);
}
return (retval);
} // end of basename(String)
// Starts the timer task to expire at midnight. When the
// task executes, it rolls the log file to the new day
// and deletes log files older than the configured days
// kept.
private void startMidnightTimer(final TimerTask task)
{
final Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
_Timer.schedule(task, calendar.getTime());
return;
} // end of startMidnightTimer(TimerTask)
//---------------------------------------------------------------
// Member data.
//
// The log file's info.
private final String _directory;
private final String _basename;
private final String _dateFormat;
private final String _extension;
private final int _daysKept;
// When this timer expires, roll the log files into the
// gutter.
private TimerTask _logRollTimer;
//-----------------------------------------------------------
// Statics.
//
private static Timer _Timer =
new Timer("LogRollTimer", true);
//-----------------------------------------------------------
// Constants.
//
/**
* Log files are placed in the application's current working
* directory by default.
*/
public static final String DEFAULT_DIRECTORY = ".";
/**
* The log file's default base name is "Logger".
*/
public static final String DEFAULT_BASENAME = "Logger";
/**
* The log file's default data format is "yyyyMMdd".
* For July 4, 1776 the formatted string is "17760704".
*/
public static final String DEFAULT_DATE_FORMAT = "yyyyMMdd";
/**
* The default file extension is "log".
*/
public static final String DEFAULT_EXTENSION = "log";
/**
* The minimum number of days a log file is kept is 0. Which
* means that the file is deleted as soon as the day ends.
*/
public static final int MIN_DAYS_KEPT = 0;
/**
* The maximum number of days a log file is kept is 96.
* That's three months. That is plenty of time to
* archive the file if necessary.
*/
public static final int MAX_DAYS_KEPT = 96;
/**
* Log files are kept for 10 days by default.
*/
public static final int DEFAULT_DAYS_KEPT = 10;
// Configuration property keys.
private static final String BASENAME_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.basename";
private static final String PATTERN_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.pattern";
private static final String EXTENSION_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.extension";
private static final String DAYS_KEPT_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.days_kept";
private static final String FORMATTER_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.formatter";
private static final String LEVEL_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.level";
//---------------------------------------------------------------
// Inner classes.
//
/**
* This file name filter looks for log files in a specified
* directory and whose names start and end with the specified
* strings.
*/
private static final class LogFileFilter
implements FilenameFilter
{
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* All log files are placed in the same directory, have
* the same base name and file name extension.
* @param directory the log file directory.
* @param basename the log file name's base name.
* @param dateFormat the file name date format.
* @param extension the log file name's extension.
* @param expiration the log file expiration date.
*/
private LogFileFilter(final File directory,
final String basename,
final String dateFormat,
final String extension,
final Date expiration)
{
_directory = directory;
_basename = basename;
_basenameSize = _basename.length();
_dateFormatter = new SimpleDateFormat(dateFormat);
_extension = extension;
_extensionSize = _extension.length();
_expiration = expiration;
_calendar = Calendar.getInstance();
return;
} // end of LogFileFilter(File,String,String,String,Date)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// FilenameFilter Interface Implemenation.
//
/**
* Returns {@code true} if {@code directory}
* is the same as the logging directory and the file
* {@code name} starts with the expected basename
* and ends with the proper extension.
* @param directory the file's directory.
* @param name the file's name.
* @return {@code true} if {@code directory}
* is the same as the logging directory and the file
* {@code name} starts with the expected basename
* and ends with the proper extension.
*/
@Override
public boolean accept(final File directory,
final String name)
{
boolean retval =
(directory.equals(_directory) == true &&
name.startsWith(_basename) &&
name.endsWith(_extension));
if (retval == true)
{
String dateSubstring;
ParsePosition pos;
Date logFileDate;
// Get the date portion of the file name.
dateSubstring =
name.substring(
_basenameSize,
(name.length() - _extensionSize));
// Parse the log's name to get its date.
pos = new ParsePosition(0);
logFileDate =
_dateFormatter.parse(dateSubstring, pos);
if (logFileDate != null)
{
_calendar.setTime(logFileDate);
_calendar.set(Calendar.HOUR_OF_DAY, 0);
_calendar.set(Calendar.MINUTE, 0);
_calendar.set(Calendar.SECOND, 0);
_calendar.set(Calendar.MILLISECOND, 0);
// Is the log file beyond keeping?
// Toss it if it is beyond expiration.
retval =
_calendar.getTime().before(_expiration);
}
}
return (retval);
} // end of accept(File, String)
//
// end of FilenameFilter Interface Implemenation.
//-------------------------------------------------------
//-----------------------------------------------------------
// Member data.
//
// Place the log files in this directory.
private final File _directory;
// All log file names start with the basename.
private final String _basename;
private final int _basenameSize;
// Parses the file names date format.
private final SimpleDateFormat _dateFormatter;
// All log file names end with this extension.
private final String _extension;
private final int _extensionSize;
// Accept only log files older than this date.
private final Date _expiration;
// Use this calendar to normalize the log file date.
private final Calendar _calendar;
} // end of class LogFileFilter
} // end of class CalendarFileHandler
//
// CHANGE LOG
// $Log: CalendarFileHandler.java,v $
// Revision 1.14 2006/10/01 17:29:16 charlesr
// Updated EventThread start and halt methods.
//
// Revision 1.13 2005/11/05 15:56:30 charlesr
// Gave _Timer a name: "LogRollTimer".
//
// Revision 1.12 2005/07/22 01:53:10 charlesr
// Moved to Java 5:
// + Added generics.
// + Using Java enums for report frequency.
// + Replaced StringBuffer with StringBuilder.
// + Removed net.sf.eBus.util.ThreadListener.
//
// Revision 1.11 2005/04/02 14:09:39 charlesr
// Check if the writer thread is halted before attempting to
// enqueue a log message.
//
// Revision 1.10 2005/03/28 14:53:08 charlesr
// + Replaced instantiating GregorianCalendar with
// Calendar.getInstance().
// + Added new lines back into system log messages.
//
// Revision 1.9 2005/01/09 20:03:55 charlesr
// Replace net.sf.eBus.util.TimerKeeper with java.util.Timer.
//
// Revision 1.8 2004/12/14 18:09:58 charlesr
// Updated for ThreadListener interface change.
// Replaced notifyAll() with notify().
//
// Revision 1.7 2004/12/13 16:32:55 charlesr
// Removed EOL from rollLogs().
//
// Revision 1.6 2004/12/13 15:42:05 charlesr
// Using java.nio to write to log file.
//
// Revision 1.5 2004/07/23 00:34:50 charlesr
// Added default constructor which gets configuration from
// java.util.logging.LogManager.
//
// Revision 1.4 2004/04/27 14:06:14 charlesr
// Using net.sf.eBus.util.TimerKeeper's singleton java.util.Timer.
//
// Revision 1.3 2004/04/27 00:05:31 charlesr
// Gave the writer thread a name.
//
// Revision 1.2 2004/02/28 14:50:20 charlesr
// Removed EOL.
//
// Revision 1.1 2004/02/20 12:11:55 charlesr
// Fixed log rolling which wasn't rolling. Also use handler's
// formatter to format log roll message.
//
// Revision 1.0 2003/11/20 01:48:50 charlesr
// Initial revision
//