
src.org.grlea.log.rollover.RolloverManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of simple-log-commons-logging Show documentation
Show all versions of simple-log-commons-logging Show documentation
Simple Log Adapter for Jakarta commons-logging
The newest version!
package org.grlea.log.rollover;
// $Id: RolloverManager.java,v 1.7 2006/07/21 11:57:31 grlea Exp $
// Copyright (c) 2004-2006 Graham Lea. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.text.Format;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
/**
* The RolloverManager is a {@link Writer} implementation used by Simple Log to enable periodic
* log-rolling functionality. It is a writer which writes to an "active" log file and, when told by
* its {@link RolloverStrategy} that the file is due to be rolled over, moves the content of the
* active log file into a new rollover log file. Sufficient mechanics are employed to ensure that
* no log content is lost while the roll over is being conducted.
*
* @author Graham Lea
* @version $Revision: 1.7 $
*/
public class
RolloverManager
extends Writer
{
/** The prefix for all special properties keys. */
private static final String KEY_PREFIX = "simplelog.";
/** The property key for the rollover strategy. */
private static final String KEY_ROLLOVER_STRATEGY = KEY_PREFIX + "rollover";
/** The rollover strategy alias for FileSizeRolloverStrategy. */
private static final String ROLLOVER_STRATEGY_FILESIZE = "fileSize";
/** The rollover strategy alias for TimeOfDayRolloverStrategy. */
private static final String ROLLOVER_STRATEGY_TIMEOFDAY = "timeOfDay";
/** The property key for the active log file name. */
private static final String KEY_ACTIVE_LOG_FILE = KEY_PREFIX + "logFile";
/** The property key for rolled log file names. */
private static final String KEY_ROLLOVER_LOG_FILE = KEY_PREFIX + "rollover.filename";
/** The property key for rolled log file names. */
private static final String KEY_ROLLOVER_DIRECTORY = KEY_PREFIX + "rollover.directory";
/** The property key for the log rollover check period. */
private static final String KEY_ROLLOVER_PERIOD = KEY_PREFIX + "rollover.period";
/** The default rollover period. */
private static final String DEFAULT_ROLLOVER_PERIOD = "60";
/**
* A semaphore lock which, in concert which {@link #printerCount} is used to monitor the number
* of printing threads and to notify a thread waiting to change the writer.
*/
private final Object PRINTERS_SEMAPHORE = new Object();
/** The number of threads currently executing a printing operation. */
private int printerCount = 0;
/**
* Lock object used as a gate to prevent new printing threads from acquiring a printing semaphore
* when another thread is waiting to change the writer. Basically, it is used to ensure priority
* for the writer-changing thread.
*/
private final Object WRITER_CHANGE_GATE = new Object();
/** The current writer. */
private Writer writer;
/** The temporary writer, used while the rollover is occurring. */
private StringWriter tempWriter = new StringWriter(1024);
/** The rollover strategy in use. */
private RolloverStrategy strategy;
/** The name used to create the rollover strategy currently in use. */
private String currentRolloverStrategyName;
/** The active log file currently in use. */
private File currentActiveLogFile;
/** The directory of the current active log file. */
private File currentActiveLogFileDirectory;
/** The directory where rolled log files will be stored. */
private File rolloverDirectory;
/** The format used to construct rolled log file names. */
private MessageFormat rolloverLogFileFormat;
/** The read/write stream for the active log file. */
private RandomAccessFile fileOut;
/** Indicates whether the strategy currently in use was set programatically. */
private boolean strategySetProgramatically = false;
/** The time (in seconds) between checks for rollover. */
private long rolloverPeriod;
/** The timer used to prompt rollover to occur. */
private Timer timer;
/** The current unique file ID */
private int uniqueFileId = 0;
/** An object to which errors should be reported. */
private ErrorReporter errorReporter;
/**
* Creates a new RolloverManager
, configuring it with provided properties. The given
* {@link ErrorReporter} object is retained and used to report errors for the life of the new
* RolloverManager
instance.
*
* @param properties properties to be used to configure the new RolloverManager
*
* @param errorReporter and object to which errors that occur while the
* RolloverManager
is running will be reported. May be null
.
*
* @throws IOException if an error occurs while configuring the RolloverManager
or
* opening the files it will be logging to.
*
* @throws IllegalArgumentException if properties
is null
.
*/
public
RolloverManager(Properties properties, ErrorReporter errorReporter)
throws IOException
{
if (properties == null)
throw new IllegalArgumentException("properties cannot be null.");
this.errorReporter = errorReporter;
configure(properties);
}
/**
* Configures this RolloverManager
using the given properties.
*
* @param properties properties to be used to configure this RolloverManager
*
* @throws IOException if an error occurs while configuring the RolloverManager
or
* opening the files it will be logging to.
*/
public void
configure(Properties properties)
throws IOException
{
configureStrategy(properties);
configureWriter(properties);
}
/**
* Configures the strategy of this RolloverManager
.
*/
private void
configureStrategy(Properties properties)
throws IOException
{
if (strategySetProgramatically)
{
return;
}
// Get the name.
String rolloverStrategyString = properties.getProperty(KEY_ROLLOVER_STRATEGY);
if (rolloverStrategyString == null)
{
throw new IOException("RolloverManger created, but rollover property not specified.");
}
rolloverStrategyString = rolloverStrategyString.trim();
if (rolloverStrategyString.length() == 0)
{
throw new IOException("RolloverManger created, but rollover property not specified.");
}
boolean strategyChanged = !rolloverStrategyString.equals(currentRolloverStrategyName);
RolloverStrategy newStrategy;
if (strategyChanged)
{
try
{
if (ROLLOVER_STRATEGY_FILESIZE.equals(rolloverStrategyString))
{
newStrategy = new FileSizeRolloverStrategy();
}
else if (ROLLOVER_STRATEGY_TIMEOFDAY.equals(rolloverStrategyString))
{
newStrategy = new TimeOfDayRolloverStrategy();
}
else
{
try
{
Class strategyClass = Class.forName(rolloverStrategyString);
Object strategyObject = strategyClass.newInstance();
if (!(strategyObject instanceof RolloverStrategy))
{
throw new IOException(strategyClass.getName() + " is not a RolloverStrategy");
}
newStrategy = (RolloverStrategy) strategyObject;
}
catch (ClassNotFoundException e)
{
throw new IOException("Class '" + rolloverStrategyString + "' not found: " + e);
}
catch (InstantiationException e)
{
throw new IOException("Failed to create an instance of " +
rolloverStrategyString + ": " + e);
}
catch (IllegalAccessException e)
{
throw new IOException("Failed to create an instance of " +
rolloverStrategyString + ": " + e);
}
}
}
catch (IOException e)
{
throw new IOException("Error creating RolloverStrategy: " + e);
}
}
else
{
newStrategy = strategy;
}
try
{
newStrategy.configure(Collections.unmodifiableMap(properties));
}
catch (IOException e)
{
throw new IOException("Error configuring RolloverStrategy: " + e);
}
this.strategy = newStrategy;
currentRolloverStrategyName = rolloverStrategyString;
}
/**
* Configures the writer of this RolloverManager
.
*/
private void
configureWriter(Properties properties)
throws IOException
{
// Active log file
String newActiveLogFileName = properties.getProperty(KEY_ACTIVE_LOG_FILE);
if (newActiveLogFileName == null)
throw new IOException("RolloverManager created but no active log file name specified.");
// Create active log file's directory
File newActiveLogFile = new File(newActiveLogFileName.trim()).getAbsoluteFile();
if (newActiveLogFile.isDirectory())
throw new IOException("The specified active log file name already exists as a directory.");
File newActiveLogFileDirectory = newActiveLogFile.getParentFile();
if (newActiveLogFileDirectory != null)
newActiveLogFileDirectory.mkdirs();
else
throw new IOException("Caanot access the active log file's parent directory.");
// Rollover log file name pattern
String newRolloverLogFileNamePattern = properties.getProperty(KEY_ROLLOVER_LOG_FILE);
if (newRolloverLogFileNamePattern == null)
newRolloverLogFileNamePattern = "{1}-" + newActiveLogFile.getName();
newRolloverLogFileNamePattern = newRolloverLogFileNamePattern.trim();
// Ensure rollover log fille name pattern compiles
MessageFormat newRolloverLogFileNameFormat;
try
{
newRolloverLogFileNameFormat = new MessageFormat(newRolloverLogFileNamePattern);
}
catch (IllegalArgumentException e)
{
throw new IOException("Illegal pattern provided for rollover log file name: " +
e.getMessage());
}
// Rollover log file directory
String newRolloverDirectoryName = properties.getProperty(KEY_ROLLOVER_DIRECTORY);
File newRolloverDirectory;
if (newRolloverDirectoryName != null)
newRolloverDirectory = new File(newRolloverDirectoryName.trim()).getAbsoluteFile();
else
newRolloverDirectory = newActiveLogFileDirectory;
if (newRolloverDirectory.exists() && !newRolloverDirectory.isDirectory())
{
throw new IOException(
"The location specified for storing rolled log files is not a directory: " +
newRolloverDirectory);
}
// Rollover period
String rolloverPeriodString = (String) properties.get(KEY_ROLLOVER_PERIOD);
if (rolloverPeriodString == null)
rolloverPeriodString = DEFAULT_ROLLOVER_PERIOD;
rolloverPeriodString = rolloverPeriodString.trim();
if (rolloverPeriodString.length() == 0)
rolloverPeriodString = DEFAULT_ROLLOVER_PERIOD;
long newRolloverPeriod;
try
{
newRolloverPeriod = Long.parseLong(rolloverPeriodString);
if (newRolloverPeriod < 1)
throw new NumberFormatException("Must be greater than 0");
}
catch (NumberFormatException e)
{
throw new IOException("Invalid rollover period specified: " + rolloverPeriodString + " (" +
e.getMessage() + ")");
}
// Determine the current unique ID if one is required
Format[] formats = newRolloverLogFileNameFormat.getFormatsByArgumentIndex();
boolean uniqueIdUsedInPattern = formats.length > 1;
if (uniqueIdUsedInPattern)
{
String[] filenames = newRolloverDirectory.list();
int maximumFileId = 0;
for (int i = 0; filenames != null && i < filenames.length; i++)
{
String filename = filenames[i];
try
{
Object[] parameters = newRolloverLogFileNameFormat.parse(filename);
if (parameters.length > 1 && parameters[1] != null)
{
String uniqueFileIdString = (String) parameters[1];
int parsedFileId = Integer.parseInt(uniqueFileIdString, 16);
if (parsedFileId > maximumFileId)
maximumFileId = parsedFileId;
}
}
catch (NumberFormatException e)
{
// This will happen for any filenames that appear to match the pattern but don't
// e.g. 'old-application.log' matches {1}-application.log, but 'old' isn't a number
}
catch (ParseException e)
{
// This will happen for any filenames that don't match the pattern
}
}
uniqueFileId = maximumFileId + 1;
}
rolloverDirectory = newRolloverDirectory;
rolloverLogFileFormat = newRolloverLogFileNameFormat;
// Create/Open standard log file
if (!newActiveLogFile.equals(currentActiveLogFile))
{
openWriter(newActiveLogFile, newActiveLogFileDirectory);
currentActiveLogFile = newActiveLogFile;
currentActiveLogFileDirectory = newActiveLogFileDirectory;
}
// Setup the timer
if (timer == null || newRolloverPeriod != rolloverPeriod)
{
if (timer != null)
timer.cancel();
timer = new Timer(true);
timer.schedule(new RolloverTask(), 0, newRolloverPeriod * 1000L);
rolloverPeriod = newRolloverPeriod;
}
}
/**
* Opens the specified file as an active log file for this RolloverManager
. This
* will also result in the creation of a "creation date" file if necessary.
*
* @param newActiveLogFile the file that will be opened as the active log file
*
* @param activeLogFileDirectory the directory in which the active log file resides
*/
private void
openWriter(File newActiveLogFile, File activeLogFileDirectory)
throws IOException
{
synchronized (WRITER_CHANGE_GATE)
{
synchronized (PRINTERS_SEMAPHORE)
{
if (printerCount != 0)
{
try
{
PRINTERS_SEMAPHORE.wait();
}
catch (InterruptedException e)
{
throw new IllegalStateException("Interrupted while attempting to configure writer");
}
}
if (writer != tempWriter)
{
if (writer != null)
{
try
{
writer.close();
}
catch (IOException e)
{
throw new IOException("Failed to close open file: " + e);
}
}
RandomAccessFile newFileOut = new RandomAccessFile(newActiveLogFile, "rws");
FileChannel channel = newFileOut.getChannel();
long initialChannelSize = channel.size();
newFileOut.seek(initialChannelSize);
Writer newFileWriter = Channels.newWriter(newFileOut.getChannel(), "UTF-8");
// Record the time the file was created if it is new
storeFileCreationTimeIfNecessary(activeLogFileDirectory, newActiveLogFile);
fileOut = newFileOut;
writer = newFileWriter;
}
}
}
}
/**
* Writes the creation time (which is the current time) of the active log file to a new file if
* necessary (i.e. if the creation file doesn't already exist).
*
* @param newActiveLogFile the file that will be opened as the active log file
*
* @param activeLogFileDirectory the directory in which the active log file resides
*/
private void
storeFileCreationTimeIfNecessary(File activeLogFileDirectory, File newActiveLogFile)
throws IOException
{
long creationTime = System.currentTimeMillis();
try
{
File creationTimeFile = getCreationTimeFile(activeLogFileDirectory, newActiveLogFile);
// The active log file being new is determined by whether the creation time file exists
boolean recordCreationTime = !creationTimeFile.exists();
if (recordCreationTime)
{
FileWriter creationTimeFileOut = new FileWriter(creationTimeFile);
creationTimeFileOut.write(String.valueOf(creationTime));
creationTimeFileOut.close();
}
}
catch (IOException e)
{
throw new IOException("Failed to write file creation time: " + e);
}
}
/**
* Reads and returns the time (in milliseconds since the 1970 epoch) that the active log file was
* created, according to its associated creation date file.
*
* @return the time the active log file was created (in millis since the epoch) or -1 if the
* creation date file is not available.
*
* @throws IOException if any error occurs opening or reading and interpreting the contents of
* the file
*/
private long
readCreationTime()
throws IOException
{
try
{
File creationTimeFile =
getCreationTimeFile(currentActiveLogFileDirectory, currentActiveLogFile);
if (!creationTimeFile.exists())
storeFileCreationTimeIfNecessary(currentActiveLogFileDirectory, currentActiveLogFile);
FileReader fileIn = new FileReader(creationTimeFile);
BufferedReader in = new BufferedReader(fileIn);
String firstLine = in.readLine();
in.close();
return Long.parseLong(firstLine);
}
catch (IOException e)
{
throw new IOException("Error reading creation time file: " + e);
}
catch (NumberFormatException e)
{
throw new IOException("Error reading creation time: " + e);
}
}
/**
* Returns the name of the creation date file associated with the specified active log file.
*
* @param newActiveLogFile the file that will be opened as the active log file
*
* @param activeLogFileDirectory the directory in which the active log file resides
*
* @return the file used to store the creation date of the active log file. It doesn't
* necessarily exist when this method returns.
*/
private File
getCreationTimeFile(File activeLogFileDirectory, File newActiveLogFile)
{
return new File(activeLogFileDirectory, newActiveLogFile.getName() + "-CREATED");
}
/**
* Closes the file resources associated with this RolloverManager
and releases any
* other resources used by it. Any errors occurring during this operation are captured and
* reported to the error reporter.
*/
public void
close()
{
synchronized (WRITER_CHANGE_GATE)
{
synchronized (PRINTERS_SEMAPHORE)
{
if (printerCount != 0)
{
try
{
PRINTERS_SEMAPHORE.wait();
}
catch (InterruptedException e)
{
throw new IllegalStateException("Interrupted while attempting to configure writer");
}
}
writer = tempWriter;
}
timer.cancel();
try
{
writer.close();
}
catch (IOException e)
{
reportError("Error closing RolloverManager's writer", e, true);
}
try
{
fileOut.close();
}
catch (IOException e)
{
reportError("Error closing RolloverManager's file", e, true);
}
}
}
/**
* Reports the given error to the error reporter, if there is one.
*
* @param description a description of the error
*
* @param e the exception that indicates the error
*
* @param printExceptionType a flag indicating whether the type of the exception should be
* printed.
*/
private void
reportError(String description, IOException e, boolean printExceptionType)
{
if (errorReporter != null)
errorReporter.error(description, e, printExceptionType);
}
/**
* Does nothing, as the RolloverManager
auto-flushes after every write()
*/
public void
flush()
{
}
/**
* Writes the output to the current writer and flushes it.
*
* @throws IOException if an error occurs writing to the current writer.
*/
public void
write(char cbuf[], int off, int len)
throws IOException
{
// Writer priority:
// Waiting for this lock prevents new printers from starting once a Writer Change starts
synchronized (WRITER_CHANGE_GATE)
{}
// Increment the printers semaphore
synchronized (PRINTERS_SEMAPHORE)
{
printerCount++;
}
try
{
writer.write(cbuf, off, len);
writer.flush();
}
finally
{
// Decrement the printers semaphore and notify if we are last out
synchronized (PRINTERS_SEMAPHORE)
{
printerCount--;
if (printerCount == 0)
{
PRINTERS_SEMAPHORE.notifyAll();
}
}
}
}
/**
* Prompts the RolloverManager
to check whether the log file needs to be rolled and
* to perform that rolling if necessary.
*
* @throws IOException if an error occurs while rolling the log file.
*/
public void
rolloverIfNecessary()
throws IOException
{
long creationTime = readCreationTime();
FileChannel activeFileChannel = fileOut.getChannel();
long fileLength = activeFileChannel.size();
Date creationDate = new Date(creationTime);
boolean rolloverNow = strategy.rolloverNow(creationDate, fileLength);
if (rolloverNow)
{
// Switch to the temporary writer
synchronized (WRITER_CHANGE_GATE)
{
synchronized (PRINTERS_SEMAPHORE)
{
if (printerCount != 0)
{
try
{
PRINTERS_SEMAPHORE.wait();
}
catch (InterruptedException e)
{
throw new IllegalStateException("Interrupted while attempting to configure writer");
}
}
writer = tempWriter;
}
}
// Create the rollover file
int fileIdNumber = uniqueFileId++;
StringBuffer fileIdString = new StringBuffer();
fileIdString.append(Integer.toHexString(fileIdNumber).toUpperCase());
char[] zeroes = new char[8 - fileIdString.length()];
Arrays.fill(zeroes, '0');
fileIdString.insert(0, zeroes);
String rolloverFileName =
rolloverLogFileFormat.format(new Object[] {new Date(), fileIdString.toString()});
if (!rolloverDirectory.exists())
rolloverDirectory.mkdirs();
File rolloverFile = new File(rolloverDirectory, rolloverFileName);
FileOutputStream rolloverOut = new FileOutputStream(rolloverFile, false);
// Copy all the contents of the real file over to the rollover file
FileChannel rolloverChannel = rolloverOut.getChannel();
activeFileChannel.transferTo(0, activeFileChannel.size(), rolloverChannel);
// Close the rollover file
rolloverOut.close();
// Truncate the real file and create a new writer
activeFileChannel.truncate(0);
fileOut.seek(0);
Writer newFileWriter = Channels.newWriter(activeFileChannel, "UTF-8");
//Write the active file creation time
File creationTimeFile =
getCreationTimeFile(currentActiveLogFileDirectory, currentActiveLogFile);
creationTimeFile.delete();
storeFileCreationTimeIfNecessary(currentActiveLogFileDirectory, currentActiveLogFile);
// Switch back to the active file writer
synchronized (WRITER_CHANGE_GATE)
{
synchronized (PRINTERS_SEMAPHORE)
{
if (printerCount != 0)
{
try
{
PRINTERS_SEMAPHORE.wait();
}
catch (InterruptedException e)
{
throw new IllegalStateException("Interrupted while attempting to configure writer");
}
}
// Switch back to a real file writer.
writer = newFileWriter;
// Write the temporary contents to the file writer and clear it
StringBuffer tempBuffer = tempWriter.getBuffer();
newFileWriter.write(tempBuffer.toString());
tempBuffer.delete(0, tempBuffer.length());
}
}
}
}
/**
* Returns the {@link RolloverStrategy} object currently in use by this
* RolloverManager
.
*/
public RolloverStrategy
getStrategy()
{
return strategy;
}
/**
* Sets the {@link RolloverStrategy} to be used by this RolloverManager
. Once the
* strategy has been set using this method, it will never be changed by calls to
* {@link #configure}, which includes auto-reloading of the properties.
*
* @param strategy the new strategy implementation to be used
*
* @throws IllegalArgumentException if strategy
is null
.
*/
public void
setStrategy(RolloverStrategy strategy)
{
if (strategy == null)
{
throw new IllegalArgumentException("strategy cannot be null.");
}
this.strategy = strategy;
strategySetProgramatically = true;
}
/**
* A {@link TimerTask} used to initiate {@link RolloverManager#rolloverIfNecessary}.
*/
private final class
RolloverTask
extends TimerTask
{
public void
run()
{
try
{
rolloverIfNecessary();
}
catch (IOException e)
{
reportError("SimpleLog ERROR: Failed to check or attempt rollover", e, true);
}
}
}
public static Writer
createRolloverManager(Properties properties, ErrorReporter errorReporter)
throws IOException
{
return new RolloverManager(properties, errorReporter);
}
/**
* An interface for objects wishing to be notified of errors occurring in a
* {@link RolloverManager} while it is running.
*/
public static interface
ErrorReporter
{
public void
error(String description, Throwable t, boolean printExceptionType);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy