src.org.grlea.log.SimpleLog Maven / Gradle / Ivy
Show all versions of simple-log-rollover Show documentation
package org.grlea.log;
// $Id: SimpleLog.java,v 1.21 2006/07/13 12:40:07 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 org.grlea.log.rollover.RolloverManager;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.io.UnsupportedEncodingException;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.MessageFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Set;
import java.util.Map;
/**
* Controls the configuration and formatting of a group of SimpleLogger
s.
*
* SimpleLog
has a default instance, which is probably the only one that anyone will
* ever use. If you use the default instance, you don't even really need to know anything about
* SimpleLog
- just use the {@link SimpleLogger#SimpleLogger(Class) basic SimpleLogger
* constructor} and you'll never even know nor care.
*
* @version $Revision: 1.21 $
* @author $Author: grlea $
*/
public final class
SimpleLog
{
// TODO (grahaml) Need a way to turn logging OFF for a package/class?
// Constants
//...............................................................................................
/** The name of the system property defining the location of the properties. */
private static final String CONFIG_PROPERTY = "simplelog.configuration";
/** The name of the system property for debugging Simple Log itself. */
private static final String DEV_DEBUG_PROPERTY = "simplelog.dev.debug";
/** The name of the system property printing Simple Log stack traces. */
private static final String DEV_STACKTRACES_PROPERTY = "simplelog.dev.printStackTraces";
/** The prefix used when the configuration is coming from a file. */
private static final String FILE_CONFIG_PREFIX = "file:";
/** The prefix used when the configuration is coming from the classpath. */
private static final String CLASSPATH_CONFIG_PREFIX = "classpath:";
/** The default name of the properties file used to configure a SimpleLog
. */
private static final String DEFAULT_PROPERTIES_FILE_NAME = "simplelog.properties";
/** The prefix for all special properties keys. */
private static final String KEY_PREFIX = "simplelog.";
/** The property key for a list of files to import. */
private static final String KEY_IMPORT = KEY_PREFIX + "import";
/** The prefix for all format-related properties keys. */
private static final String KEY_FORMAT_PREFIX = KEY_PREFIX + "format.";
/** The suffix for format property keys relating to instance formats. */
private static final String KEY_FORMAT_INSTANCE_SUFFIX = ".instance";
/** The property key for the debug format. */
private static final String KEY_FORMAT_DB = KEY_FORMAT_PREFIX + "debug";
/** The property key for the debug object format. */
private static final String KEY_FORMAT_DBO = KEY_FORMAT_PREFIX + "debugObject";
/** The property key for the debug exception format. */
private static final String KEY_FORMAT_DBE = KEY_FORMAT_PREFIX + "debugException";
/** The property key for the entry format. */
private static final String KEY_FORMAT_ENTRY = KEY_FORMAT_PREFIX + "entry";
/** The property key for the exit format. */
private static final String KEY_FORMAT_EXIT = KEY_FORMAT_PREFIX + "exit";
/** The property key for properties reloading. */
private static final String KEY_RELOADING = KEY_PREFIX + "reloading";
/** The default value for the reloading properties property. */
private static final String RELOADING_DEFAULT = "false";
/** The property key for the log file name. */
private static final String KEY_LOG_FILE = KEY_PREFIX + "logFile";
/** The property key for whether the log file name should be interpreted. */
private static final String KEY_INTERPRET_NAME = KEY_LOG_FILE + ".interpretName";
/** The default value for the parse log file name property. */
private static final boolean INTREPRET_NAME_DEFAULT = true;
/** The property key for whether the log file should be appended. */
private static final String KEY_APPEND = KEY_LOG_FILE + ".append";
/** The default value for the append property. */
private static final boolean APPEND_DEFAULT = true;
/** The property key for whether output to a log file should also be printed to the console. */
private static final String KEY_PIPE_TO_CONSOLE = KEY_LOG_FILE + ".andConsole";
/** The default value for the andConsole property. */
private static final boolean PIPE_TO_CONSOLE_DEFAULT = false;
/** The property key for the default debug level. */
private static final String KEY_DEFAULT_LEVEL = KEY_PREFIX + "defaultLevel";
/** The property key for the default tracing flag. */
private static final String KEY_DEFAULT_TRACE = KEY_PREFIX + "defaultTrace";
/** The property key for the date format. */
private static final String KEY_DATE_FORMAT = KEY_PREFIX + "dateFormat";
/** The default date format. */
private static final String DATE_FORMAT_DEFAULT = "EEE yyyy/MM/dd HH:mm:ss.SSS";
/** The property key for the print stack traces property. */
private static final String KEY_PRINT_STACK_TRACES = KEY_PREFIX + "printStackTraces";
/** The default value for the print stack traces property. */
private static final String PRINT_STACK_TRACES_DEFAULT = "true";
/** The property key for the rollover strategy. */
private static final String KEY_ROLLOVER_STRATEGY = "simplelog.rollover";
/**
* The period (in milliseconds) between checks of the properties file (must be a FILE, not inside
* a JAR).
*/
private static final int RELOAD_FILE_CHECK_PERIOD = 20 * 1000;
/**
* The period (in milliseconds) between checks of the properties when they're not in a file.
*/
private static final int RELOAD_URL_CHECK_PERIOD = 60 * 1000;
/**
* The suffix used in the properties file to identify a trace flag.
*/
private static final String TRACE_SUFFIX = "#trace";
/**
* The string to use to print a new line between the debug exception line and the stack trace.
*/
private static final String LINE_SEP = System.getProperty("line.separator");
/** The class name of the RolloverManager. */
private static final String ROLLOVER_WRITER_CLASS = "org.grlea.log.rollover.RolloverManager";
// Default Message Formats
//..........................................................................................
// The following arguments are always the same:
// {0} - The current date and time (formatted as specified by DATE_FORMAT)
// {1} - The name of the current Thread.
// {2} - The name of the debugging class.
// {3} - The instance ID, if in use.
// {4} - The DebugLevel name
private static final String DEFAULT_FORMAT_STRING_DB = "{0}| |{1}|{2}|{5}";
private static final String DEFAULT_FORMAT_STRING_DBO = "{0}|---|{1}|{2}|{5}|{6}";
private static final String DEFAULT_FORMAT_STRING_DBE = "{0}|***|{1}|{2}|{5}";
private static final String DEFAULT_FORMAT_STRING_ENTRY = "{0}|>>>|{1}|{2}|{5}";
private static final String DEFAULT_FORMAT_STRING_EXIT = "{0}|<<<|{1}|{2}|{5}";
private static final String DEFAULT_FORMAT_STRING_DB_INSTANCE = "{0}| |{1}|{2}[{3}]|{5}";
private static final String DEFAULT_FORMAT_STRING_DBO_INSTANCE = "{0}|---|{1}|{2}[{3}]|{5}|{6}";
private static final String DEFAULT_FORMAT_STRING_DBE_INSTANCE = "{0}|***|{1}|{2}[{3}]|{5}";
private static final String DEFAULT_FORMAT_STRING_ENTRY_INSTANCE = "{0}|>>>|{1}|{2}[{3}]|{5}";
private static final String DEFAULT_FORMAT_STRING_EXIT_INSTANCE = "{0}|<<<|{1}|{2}[{3}]|{5}";
private static final int DEFAULT_FORMAT_EXCEPTION_INDEX = 5;
private static final int DEFAULT_FORMAT_EXCEPTION_INDEX_INSTANCE = 5;
// Static variables
//..........................................................................................
/**
* The default instance of SimpleLog
.
*
* @see #defaultInstance()
*/
private static SimpleLog defaultInstance = null;
/**
* An object to synchronize on when checking or changing the {@link #defaultInstance}.
*/
private static final Object defaultInstanceLock = new Object();
/**
* Specifies whether Simple Log will output debug information about itself.
*/
private static boolean devDebug = false;
static
{
try
{
String devDebugString = System.getProperty(DEV_DEBUG_PROPERTY);
if (devDebugString != null && "true".equalsIgnoreCase(devDebugString.trim()))
{
devDebug = true;
printDebugIfEnabled("Simple Log DEV Debugging enabled (-Dsimplelog.dev.debug)");
}
}
catch (Exception e)
{
printError("Exception while reading system property '" + DEV_DEBUG_PROPERTY + "'", e, true);
}
}
// Constants
//...............................................................................................
/**
* The URL from where this SimpleLog
loaded (and may reload) its configuration.
*/
private final URL configurationSource;
/** The properties governing this SimpleLog
. */
private final Properties properties;
/** The destination of this SimpleLog
's output. */
private PrintWriter out;
/** The writer that the print writer is printing to. */
private Writer currentWriter;
/** Indicates whether the output writer has been set programatically. */
private boolean outputSetProgramatically = false;
/**
* The path and name of the log file being written to, or null
if output is going
* somewhere else.
*/
private String logFile;
/** Whether the PrintWriter is printing straight to the console. */
private boolean printWriterGoesToConsole = true;
/** Whether output is currently going to the console as well as a log file. */
private boolean pipingOutputToConsole = false;
/** The default level of this SimpleLog
. */
private DebugLevel defaultLevel = DebugLevel.L4_INFO;
/** The default trace flag of this SimpleLog
. */
private boolean defaultTracing = false;
/**
* The non-instance {@link SimpleLogger}s attached to this SimpleLog
.
*/
private final List loggers = new ArrayList(32);
/**
* A list of {@link WeakReference}s to instance {@link SimpleLogger}s attached to this
* SimpleLog
.
*/
private final List instanceLoggerReferences = new ArrayList(16);
/** Queue of references to SimpleLoggers now unreachable. */
private ReferenceQueue instanceLoggersReferenceQueue = null;
/** An object to synchronize on when accessing or modifying the {@link #loggers} list. */
private final Object LOGGERS_LOCK = new Object();
/** The date format used in all formats in this SimpleLog
. */
private DateFormat dateFormat;
/**
* Message format for a simple debug message.
* 4 = message (String)
*/
private MessageFormat dbFormat;
/**
* Message format for a debug object message.
* 4 = object name (String)
* 5 = object value (Object)
*/
private MessageFormat dboFormat;
/**
* Message format for a debug exception message.
* 4 = exception (Throwable)
*/
private MessageFormat dbeFormat;
/**
* Message format for a trace entry message.
* 4 = method name (String)
*/
private MessageFormat entryFormat;
/**
* Message format for a trace exit message.
* 4 = method name (String)
*/
private MessageFormat exitFormat;
/**
* Message format for a simple debug message (instance version).
* 4 = message (String)
*/
private MessageFormat dbFormat4Instance;
/**
* Message format for a debug object message (instance version).
* 4 = object name (String)
* 5 = object value (Object)
*/
private MessageFormat dboFormat4Instance;
/**
* Message format for a debug exception message (instance version).
* 4 = exception (Throwable)
*/
private MessageFormat dbeFormat4Instance;
/**
* Message format for a trace entry message (instance version).
* 4 = method name (String)
*/
private MessageFormat entryFormat4Instance;
/**
* Message format for a trace exit message (instance version).
* 4 = method name (String)
*/
private MessageFormat exitFormat4Instance;
/**
* Creates a new SimpleLog
configured by the given properties object.
* All SimpleLog
s log to System.err by default.
*
* @param properties a properties object containing properties that will be used to configure
* the SimpleLog
.
*
* @throws IllegalArgumentException if properties
is null
.
*/
public
SimpleLog(Properties properties)
{
if (properties == null)
{
throw new IllegalArgumentException("properties cannot be null.");
}
this.configurationSource = null;
this.properties = properties;
this.out = new PrintWriter(System.err, true);
readSettingsFromProperties();
}
/**
* Creates a new SimpleLog
configured by a properties file read from the given URL.
*
* All SimpleLog
s log to System.err by default.
*
* @param configurationSource the properties file to use to configure the SimpleLog
* instance.
*
* @throws IOException if the properties file cannot be read from the given URL.
*/
public
SimpleLog(URL configurationSource)
throws IOException
{
this.configurationSource = configurationSource;
this.properties = new Properties();
this.out = new PrintWriter(System.err, true);
loadProperties();
readSettingsFromProperties();
// Note: Reloading is only checked on creation
String reloadingString = properties.getProperty(KEY_RELOADING, RELOADING_DEFAULT);
boolean reloading = Boolean.valueOf(reloadingString).booleanValue();
if (reloading)
{
printDebugIfEnabled("Configuration reloading enabled");
Timer timer = new Timer(true);
TimerTask reloadTask;
int reloadPeriod;
if (configurationSource.getProtocol() != null &&
"file".equals(configurationSource.getProtocol().toLowerCase()))
{
reloadTask = new FileConfigurationReloader();
reloadPeriod = RELOAD_FILE_CHECK_PERIOD;
}
else
{
reloadTask = new UrlConfigurationReloader();
reloadPeriod = RELOAD_URL_CHECK_PERIOD;
}
timer.schedule(reloadTask, reloadPeriod, reloadPeriod);
}
else
{
printDebugIfEnabled("Configuration reloading is disabled");
}
}
/**
* Loads the properties file from this SimpleLog
's configuration source and reads
* all the special properties used to configure the SimpleLog
(as opposed to those
* that configure the attached SimpleLogger
s).
*
* @throws IOException if the properties file cannot be read from the URL.
*/
private void
loadProperties()
throws IOException
{
if (properties == null)
{
return;
}
printDebugIfEnabled("Loading properties");
// Load the properties into a new object, then replace the current ones if the read suceeds.
InputStream inputStream = configurationSource.openStream();
Properties newProperties = new Properties();
try
{
newProperties.load(inputStream);
}
finally
{
inputStream.close();
}
// Import any properties files as specified
String importList = newProperties.getProperty(KEY_IMPORT);
if (importList != null)
{
importList = importList.trim();
if (importList.length() > 0)
{
String[] filesToImport = importList.split(",");
if (filesToImport != null && filesToImport.length != 0)
{
String configurationContext = configurationSource.toExternalForm();
int lastSlash = configurationContext.lastIndexOf('/');
lastSlash += 1;
configurationContext = configurationContext.substring(0, lastSlash);
for (int i = 0; i < filesToImport.length; i++)
{
String filenameToImport = filesToImport[i];
URL urlToImport = new URL(configurationContext + filenameToImport);
InputStream importStream = null;
try
{
printDebugIfEnabled("Importing file", urlToImport);
importStream = urlToImport.openStream();
newProperties.load(importStream);
}
catch (IOException e)
{
printError("Error importing properties file: " + filenameToImport +
"(" + urlToImport + ")", e, true);
}
finally
{
if (importStream != null)
importStream.close();
}
}
}
}
}
// List all loaded properties if debug is on
if (devDebug)
{
Set properties = newProperties.entrySet();
printDebugIfEnabled("_____ Properties List START _____");
for (Iterator iterator = properties.iterator(); iterator.hasNext();)
{
Map.Entry entry = (Map.Entry) iterator.next();
printDebugIfEnabled((String) entry.getKey(), entry.getValue());
}
printDebugIfEnabled("______ Properties List END ______");
}
// Replace the current properties with the loaded ones
properties.clear();
properties.putAll(newProperties);
}
/**
* Reads settings from this SimpleLog
's properties object and changes the object's
* state accordingly.
*/
private void
readSettingsFromProperties()
{
if (!outputSetProgramatically)
{
try
{
String rolloverStrategyString = properties.getProperty(KEY_ROLLOVER_STRATEGY);
boolean useRollover =
rolloverStrategyString != null && rolloverStrategyString.trim().length() != 0;
Writer newWriter;
if (useRollover)
{
newWriter = configureRolloverWriter();
}
else
{
newWriter = configureFileWriter();
}
if (newWriter != currentWriter)
{
out = new PrintWriter(newWriter, true);
if (currentWriter != null)
{
try
{
currentWriter.close();
}
catch (IOException e)
{
printError("Error while closing log file", e, true);
}
}
currentWriter = newWriter;
}
}
catch (IOException e)
{
printError("Error opening log file for writing", e, true);
}
}
// Read the "andConsole" property
String pipeOutputToConsoleString = properties.getProperty(KEY_PIPE_TO_CONSOLE);
// The strategy here is to only turn andConsole on if the property definitely says true
pipingOutputToConsole = PIPE_TO_CONSOLE_DEFAULT;
if (pipeOutputToConsoleString != null)
{
pipingOutputToConsole = pipeOutputToConsoleString.trim().equalsIgnoreCase("true");
}
// Read the Default level
String defaultLevelStr = properties.getProperty(KEY_DEFAULT_LEVEL);
if (defaultLevelStr != null)
{
defaultLevelStr = defaultLevelStr.trim();
try
{
// Try to read it as an int first...
int level = Integer.parseInt(defaultLevelStr);
defaultLevel = DebugLevel.fromInt(level);
}
catch (NumberFormatException e1)
{
// ... then try it as a name.
try
{
defaultLevel = DebugLevel.fromName(defaultLevelStr);
}
catch (IllegalArgumentException e2)
{
printError("Error parsing debug level for '" + KEY_DEFAULT_LEVEL + "'", e1, true);
printError("Error parsing debug level for '" + KEY_DEFAULT_LEVEL + "'", e2, false);
}
}
}
// Read the Default trace
String defaultTraceStr = properties.getProperty(KEY_DEFAULT_TRACE);
if (defaultTraceStr != null)
{
defaultTracing = Boolean.valueOf(defaultTraceStr).booleanValue();
}
// Read the Date format
String dateFormatString = properties.getProperty(KEY_DATE_FORMAT, DATE_FORMAT_DEFAULT);
try
{
dateFormat = new SimpleDateFormat(dateFormatString);
}
catch (IllegalArgumentException e)
{
printError("Error parsing date format", e, false);
}
// Read formats
dbFormat = readFormat(KEY_FORMAT_DB, DEFAULT_FORMAT_STRING_DB);
dboFormat = readFormat(KEY_FORMAT_DBO, DEFAULT_FORMAT_STRING_DBO);
dbeFormat = readFormat(KEY_FORMAT_DBE, DEFAULT_FORMAT_STRING_DBE);
entryFormat = readFormat(KEY_FORMAT_ENTRY, DEFAULT_FORMAT_STRING_ENTRY);
exitFormat = readFormat(KEY_FORMAT_EXIT, DEFAULT_FORMAT_STRING_EXIT);
dbFormat4Instance =
readFormat(KEY_FORMAT_DB + KEY_FORMAT_INSTANCE_SUFFIX, DEFAULT_FORMAT_STRING_DB_INSTANCE);
dboFormat4Instance =
readFormat(KEY_FORMAT_DBO + KEY_FORMAT_INSTANCE_SUFFIX, DEFAULT_FORMAT_STRING_DBO_INSTANCE);
dbeFormat4Instance =
readFormat(KEY_FORMAT_DBE + KEY_FORMAT_INSTANCE_SUFFIX, DEFAULT_FORMAT_STRING_DBE_INSTANCE);
entryFormat4Instance =
readFormat(KEY_FORMAT_ENTRY + KEY_FORMAT_INSTANCE_SUFFIX,
DEFAULT_FORMAT_STRING_ENTRY_INSTANCE);
exitFormat4Instance =
readFormat(KEY_FORMAT_EXIT + KEY_FORMAT_INSTANCE_SUFFIX,
DEFAULT_FORMAT_STRING_EXIT_INSTANCE);
updateDateFormats();
// Read the Print stack traces property
String printStackTracesStr =
properties.getProperty(KEY_PRINT_STACK_TRACES, PRINT_STACK_TRACES_DEFAULT);
boolean printStackTraces = Boolean.valueOf(printStackTracesStr).booleanValue();
if (printStackTraces)
{
ExceptionFormat exceptionFormat = new ExceptionFormat();
dbeFormat.setFormatByArgumentIndex(DEFAULT_FORMAT_EXCEPTION_INDEX, exceptionFormat);
dbeFormat4Instance.setFormatByArgumentIndex(
DEFAULT_FORMAT_EXCEPTION_INDEX_INSTANCE, exceptionFormat);
}
// Copy any properties that have '$' in their name
Enumeration propertyNames = properties.propertyNames();
Properties newProperties = new Properties();
while (propertyNames.hasMoreElements())
{
String key = (String) propertyNames.nextElement();
if (key.indexOf('$') != -1)
{
newProperties.put(key.replace('$', '.'), properties.getProperty(key));
}
}
properties.putAll(newProperties);
}
/**
* Configures this SimpleLog
to use a plain FileWriter, according to the properties
* in the current properties object.
*
* @return the writer to be used to write log output.
*/
private Writer
configureFileWriter()
throws IOException
{
Writer writer;
// Read the log file name
String newLogFile = properties.getProperty(KEY_LOG_FILE);
boolean interpretName = INTREPRET_NAME_DEFAULT;
String interpretNameStr = properties.getProperty(KEY_INTERPRET_NAME);
// The strategy here is to only turn interpret off if the property definitely says false
if (interpretNameStr != null)
{
interpretName = !(interpretNameStr.trim().equalsIgnoreCase("false"));
}
// Substitue the date into the name if necessary
boolean newLogFileNotNull = newLogFile != null;
boolean nameContainsBraces = newLogFileNotNull && newLogFile.indexOf('{') != -1;
if (newLogFileNotNull && nameContainsBraces && interpretName)
{
try
{
MessageFormat logFileNameFormat = new MessageFormat(newLogFile);
newLogFile = logFileNameFormat.format(new Object[] {new Date()});
}
catch (Exception e)
{
printError("Error generating log file name", e, true);
newLogFile = null;
}
}
// The log file has changed if it used to be null and now isn't, or vice versa...
boolean logFileChanged = (logFile == null) != (newLogFile == null);
// or it's changed if it wasn't null and still isn't null but the name has changed.
logFileChanged |= logFile != null && newLogFileNotNull && !newLogFile.equals(logFile);
// or it's changed if rollover was on before (but it's now off if we're in here)
boolean rolloverWasInUse =
currentWriter != null &&
currentWriter.getClass().getName().equals(ROLLOVER_WRITER_CLASS);
logFileChanged |= rolloverWasInUse;
if (logFileChanged)
{
if (newLogFile == null)
{
writer = new OutputStreamWriter(System.err);
printWriterGoesToConsole = true;
}
else
{
File file = new File(newLogFile).getAbsoluteFile();
if (file.isDirectory())
{
throw new IOException(
"The specified log file name already exists as a directory.");
}
File parentFile = file.getParentFile();
if (parentFile != null)
{
parentFile.mkdirs();
}
boolean append = APPEND_DEFAULT;
String appendStr = properties.getProperty(KEY_APPEND);
// The strategy here is to only turn append off if the property definitely says
// false
if (appendStr != null)
{
append = !(appendStr.trim().equalsIgnoreCase("false"));
}
writer = new FileWriter(file, append);
printWriterGoesToConsole = false;
}
logFile = newLogFile;
}
else
{
writer = currentWriter;
}
return writer;
}
/**
* Configures this SimpleLog
to use a {@link RolloverManager}, according to the
* properties in the current properties object.
*
* @return the writer to be used to write log output.
*/
private Writer
configureRolloverWriter()
throws IOException
{
// Test we've got the RolloverManager class, so we can throw something really meaningful.
try
{
Class.forName(ROLLOVER_WRITER_CLASS);
}
catch (ClassNotFoundException e)
{
throw new IOException("The RolloverManager class is not available: " + e);
}
catch (Throwable t)
{
// Ignore anything else that might be thrown (e.g. SecurityException).
}
Writer writer;
if (currentWriter != null && currentWriter.getClass().getName().equals(ROLLOVER_WRITER_CLASS))
{
((RolloverManager) currentWriter).configure(properties);
writer = currentWriter;
}
else
{
writer = RolloverManager.createRolloverManager(properties, ErrorReporter.create());
}
printWriterGoesToConsole = false;
return writer;
}
/**
* Reloads this SimpleLog
's configuration.
*/
public void
reloadProperties()
{
try
{
if (configurationSource != null)
{
loadProperties();
}
readSettingsFromProperties();
reconfigureAllLoggers();
}
catch (Exception e)
{
printError("Falied to reload properties", e, true);
}
}
/**
* Reads a format from a specified key.
*
* @param key the key to read from the properties to obtain a new message format string.
*
* @param defaultPattern the default pattern to use if the property doesn't exist.
*
* @return a new MessageFormat, created from the resulting message format string.
*/
private MessageFormat
readFormat(String key, String defaultPattern)
{
String formatString = properties.getProperty(key, defaultPattern);
MessageFormat format;
try
{
format = new MessageFormat(formatString);
}
catch (Exception e)
{
printError("Error reading format string from " + key, e, false);
format = new MessageFormat(defaultPattern);
}
return format;
}
/**
* Returns the default instance of SimpleLog
.
*
* The default instance is either configured from a properties file found on the classpath
* with the name 'simplelog.properties', or not configured at all (if the file cannot be found).
*
If the instance is not configured at all, it will not produce any output unless a writer
* is programmatically assigned to it (see {@link #setWriter}).
*
* @return the default instance of SimpleLog
.
*/
public static SimpleLog
defaultInstance()
{
synchronized (defaultInstanceLock)
{
if (defaultInstance == null)
{
printDebugIfEnabled("Creating default SimpleLog instance");
URL propertiesUrl = getPropertiesUrl();
if (propertiesUrl != null)
{
printDebugIfEnabled("Attempting to configure using properties at", propertiesUrl);
try
{
defaultInstance = new SimpleLog(propertiesUrl);
}
catch (Exception e)
{
printError("Error while attempting to load default properties", e, true);
}
}
if (defaultInstance == null)
{
printDebugIfEnabled("");
printDebugIfEnabled("FAILED to load any SimpleLog configuration.");
printDebugIfEnabled("");
printDebugIfEnabled("NO LOG OUTPUT WILL BE GENERATED.");
defaultInstance = new SimpleLog(new Properties());
defaultInstance.setWriter(null);
}
}
}
return defaultInstance;
}
private static URL
getPropertiesUrl()
{
// Read the system property
String propertiesDefinition = null;
try
{
propertiesDefinition = System.getProperty(CONFIG_PROPERTY);
}
catch (SecurityException e)
{
printError("SecurityException while trying to read system property", e, true);
}
printDebugIfEnabled("System property '" + CONFIG_PROPERTY + "'", propertiesDefinition);
URL propertiesUrl = null;
if (propertiesDefinition != null)
{
// File
if (propertiesDefinition.startsWith(FILE_CONFIG_PREFIX))
{
String propertiesLocation =
propertiesDefinition.substring(FILE_CONFIG_PREFIX.length());
File propertiesFile = new File(propertiesLocation);
if (propertiesFile.exists())
{
try
{
propertiesUrl = propertiesFile.toURL();
}
catch (MalformedURLException e)
{
printError("Error creating URL from filename '" + propertiesLocation + "'", e,
false);
}
}
else
{
printError("Properties file not found at '" + propertiesLocation + "'");
}
}
// Classpath
else if (propertiesDefinition.startsWith(CLASSPATH_CONFIG_PREFIX))
{
String propertiesLocation =
propertiesDefinition.substring(CLASSPATH_CONFIG_PREFIX.length());
propertiesUrl = SimpleLog.class.getClassLoader().getResource(propertiesLocation);
if (propertiesUrl == null)
printError("Properties not found in classpath at '" + propertiesLocation + "'");
}
// Junk
else
{
printError(CONFIG_PROPERTY + " property must begin with '" + FILE_CONFIG_PREFIX +
"' or '" + CLASSPATH_CONFIG_PREFIX + "' ('" + propertiesDefinition + "')");
}
}
// Default: simplelog.properties in the root of the classpath
if (propertiesUrl == null)
{
printDebugIfEnabled(
"Attempting to load default properties (simplelog.properties) from classpath");
propertiesUrl = SimpleLog.class.getClassLoader().getResource(DEFAULT_PROPERTIES_FILE_NAME);
}
// Encode any spaces in the URL
if (propertiesUrl != null && propertiesUrl.toExternalForm().indexOf(' ') != -1)
{
try
{
propertiesUrl = new URL(propertiesUrl.toString().replaceAll(" ", "%20"));
}
catch (MalformedURLException e)
{
printError("Failed to encode spaces in properties URL", e, true);
}
}
return propertiesUrl;
}
/**
* Prints the given string to this SimpleLog
's output destination, followed by a
* newline sequence.
*
* @param s the string to print
*
* @see PrintWriter#println(String)
*/
void
println(String s)
{
if (out != null)
{
out.println(s);
if (!printWriterGoesToConsole && pipingOutputToConsole)
{
System.err.println(s);
}
}
}
/**
* Configures the given logger by setting its debug level and trace flag according to the current
* properties.
*
* @param logger the logger to configure.
*/
void
configure(SimpleLogger logger)
{
logger.setDebugLevel(getDebugLevel(logger));
logger.setTracing(getTracingFlag(logger));
}
/**
* Retrieves the debug level for the given class from the properties.
*
* @param logger the logger for which to retrieve the debug level
*
* @return the debug level to be used for the class.
*/
private DebugLevel
getDebugLevel(SimpleLogger logger)
{
if (properties == null)
{
return defaultLevel;
}
String loggerConfigName = logger.getConfigName();
int dotIndex = loggerConfigName.length();
DebugLevel debugLevel = null;
do
{
// On first iteration, this substring() call returns the whole string.
// On subsequent iterations, it removes everything after and including the last period.
loggerConfigName = loggerConfigName.substring(0, dotIndex);
String value = properties.getProperty(loggerConfigName);
if (value != null)
{
value = value.trim();
try
{
// Try to read it as an int first...
int level = Integer.parseInt(value);
debugLevel = DebugLevel.fromInt(level);
}
catch (NumberFormatException e1)
{
// ... then try it as a loggerConfigName.
try
{
debugLevel = DebugLevel.fromName(value);
}
catch (IllegalArgumentException e2)
{
printError("Error parsing debug level for '" + loggerConfigName + "'", e1, true);
printError("Error parsing debug level for '" + loggerConfigName + "'", e2, false);
}
}
}
dotIndex = loggerConfigName.lastIndexOf('.');
}
while (debugLevel == null && dotIndex != -1);
// If we found no level, use the default.
if (debugLevel == null)
{
debugLevel = defaultLevel;
}
return debugLevel;
}
/**
* Retrieves the tracing flag for the given class from the properties.
*
* @param logger the logger for which to retrieve the debug level
*
* @return the tracing flag to be used for the class.
*/
private boolean
getTracingFlag(SimpleLogger logger)
{
if (properties == null)
{
return defaultTracing;
}
String loggerConfigName = logger.getConfigName();
int dotIndex = loggerConfigName.length();
boolean trace = defaultTracing;
do
{
loggerConfigName = loggerConfigName.substring(0, dotIndex);
String value = properties.getProperty(loggerConfigName + TRACE_SUFFIX);
if (value != null)
{
trace = Boolean.valueOf(value).booleanValue();
break;
}
dotIndex = loggerConfigName.lastIndexOf('.');
}
while (dotIndex != -1);
return trace;
}
/**
* Registers the give logger with this SimpleLog
, so that the logger can be updaed
* if the properties change.
*
* @param logger the logger to register
*/
void
register(SimpleLogger logger)
{
synchronized (LOGGERS_LOCK)
{
if (!logger.isInstanceDebugger())
{
loggers.add(logger);
}
else
{
// Instance loggers get weak referenced, because they're much more likely to be GC'd.
if (instanceLoggersReferenceQueue == null)
{
createInstanceLoggersReferenceQueue();
}
instanceLoggerReferences.add(new WeakReference(logger, instanceLoggersReferenceQueue));
}
}
configure(logger);
}
/**
* Creates the {@link ReferenceQueue} for instance loggers and starts a daemon thread to clear
* queue.
*/
private void
createInstanceLoggersReferenceQueue()
{
instanceLoggersReferenceQueue = new ReferenceQueue();
Thread thread = new Thread(new Runnable()
{
public void
run()
{
while (true)
{
try
{
Thread.yield();
Reference reference = instanceLoggersReferenceQueue.remove();
synchronized (LOGGERS_LOCK)
{
instanceLoggerReferences.remove(reference);
}
}
catch (Throwable t)
{
// Ignore - who cares?
}
}
}
}, "SimpleLog Instance Logger Cleaner");
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
}
/**
* Re-configures all {@link SimpleLogger}s registered with this SimpleLog
.
*/
private void
reconfigureAllLoggers()
{
synchronized (LOGGERS_LOCK)
{
printDebugIfEnabled("Re-configuring all loggers");
for (Iterator iter = loggers.iterator(); iter.hasNext();)
{
configure((SimpleLogger) iter.next());
}
for (Iterator iter = instanceLoggerReferences.iterator(); iter.hasNext();)
{
Reference loggerReference = (Reference) iter.next();
SimpleLogger logger = (SimpleLogger) loggerReference.get();
if (logger != null)
configure(logger);
}
}
}
/**
* Prints the given debug message to System.err if Simple Log dev debugging is enabled.
*
* @param message a message to print
*/
private static void
printDebugIfEnabled(String message)
{
if (devDebug)
System.err.println("SimpleLog [dev.debug]: " + message);
}
/**
* Prints the given debug message and object value to System.err if Simple Log dev debugging is
* enabled.
*
* @param message a message to print
*
* @param value an object to print
*/
private static void
printDebugIfEnabled(String message, Object value)
{
if (devDebug)
System.err.println("SimpleLog [dev.debug]: " + message + ": " + value);
}
/**
* Prints an error message from this SimpleLog
.
*
* @param description a description the error
*/
private static void
printError(String description)
{
printError(description, null, false);
}
/**
* Prints an error message from this SimpleLog
.
*
* @param description a description the error
* @param error the exception that occurred to cause the error (may be null
)
* @param printExceptionType whether the whole toString of the exception should be printed (true)
* of just the exception's 'message' (false).
*/
private static void
printError(String description, Throwable error, boolean printExceptionType)
{
boolean printStackTraces = false;
try
{
String printStackTracesStr = System.getProperty(DEV_STACKTRACES_PROPERTY);
printStackTraces = printStackTracesStr != null &&
printStackTracesStr.trim().equalsIgnoreCase("true");
}
catch (SecurityException e)
{
// Ignore SecurityExceptions from trying to read system properties
}
synchronized (System.err)
{
System.err.println();
System.err.print(" SimpleLog ERROR: ");
System.err.print(description);
if (error != null)
{
System.err.print(": ");
if (printExceptionType)
{
System.err.println(error);
}
else
{
System.err.println(error.getMessage());
}
if (printStackTraces)
{
try
{
System.err.println();
error.printStackTrace(System.err);
}
catch (SecurityException e)
{
// Ignore SecurityExceptions from trying to print stack traces
}
}
System.err.println();
}
else
{
System.err.println();
System.err.println();
}
}
}
/**
* Returns this SimpleLog
's default level.
*/
public DebugLevel
getDefaultLevel()
{
return defaultLevel;
}
/**
* Sets this SimpleLog
's default level.
*
* @param defaultLevel the new default debug level
*
* @throws IllegalArgumentException if defaultLevel is null.
*/
public void
setDefaultLevel(DebugLevel defaultLevel)
{
if (defaultLevel == null)
{
throw new IllegalArgumentException("defaultLevel cannot be null.");
}
this.defaultLevel = defaultLevel;
reconfigureAllLoggers();
}
/**
* Returns this SimpleLog
's default tracing flag.
*/
public boolean
isDefaultTracing()
{
return defaultTracing;
}
/**
* Sets this SimpleLog
's default tracing flag.
*
* @param defaultTracing the new default trace value (true
for on, false
* for off).
*/
public void
setDefaultTracing(boolean defaultTracing)
{
this.defaultTracing = defaultTracing;
reconfigureAllLoggers();
}
/**
* Returns this SimpleLog
's default date format.
*/
public DateFormat
getDateFormat()
{
return dateFormat;
}
/**
* Sets this SimpleLog
's default date format. If the given value is
* null
, the default date format will be used.
*
* @param newDateFormat the new date format. May be null
.
*/
public void
setDateFormat(DateFormat newDateFormat)
{
if (newDateFormat == null)
{
dateFormat = new SimpleDateFormat(DATE_FORMAT_DEFAULT);
}
else
{
dateFormat = newDateFormat;
}
updateDateFormats();
}
/**
* Returns the print writer that is the destination for all of this SimpleLog
's
* output.
*
* @return the print writer being used by this SimpleLog
, or null
if
* it doesn't have one.
*/
public PrintWriter
getWriter()
{
return out;
}
/**
* Sets the print writer to be used as the destination for all of this SimpleLog
's
* output.
*
* @param out the print to be used by this SimpleLog
, or null
if
* it should not create any output.
*/
public void
setWriter(PrintWriter out)
{
this.out = out;
this.outputSetProgramatically = true;
}
/**
* Returns whether this SimpleLog
is currently printing output at all.
*/
boolean
isOutputting()
{
return out != null;
}
/**
* Updates the date format in all of this SimpleLog
's message formats.
*/
private void
updateDateFormats()
{
dbFormat.setFormatByArgumentIndex(0, dateFormat);
dboFormat.setFormatByArgumentIndex(0, dateFormat);
dbeFormat.setFormatByArgumentIndex(0, dateFormat);
entryFormat.setFormatByArgumentIndex(0, dateFormat);
exitFormat.setFormatByArgumentIndex(0, dateFormat);
dbFormat4Instance.setFormatByArgumentIndex(0, dateFormat);
dboFormat4Instance.setFormatByArgumentIndex(0, dateFormat);
dbeFormat4Instance.setFormatByArgumentIndex(0, dateFormat);
entryFormat4Instance.setFormatByArgumentIndex(0, dateFormat);
exitFormat4Instance.setFormatByArgumentIndex(0, dateFormat);
}
MessageFormat
getDebugFormat()
{
return dbFormat;
}
MessageFormat
getDebugInstanceFormat()
{
return dbFormat4Instance;
}
MessageFormat
getDebugObjectFormat()
{
return dboFormat;
}
MessageFormat
getDebugObjectInstanceFormat()
{
return dboFormat4Instance;
}
MessageFormat
getDebugExceptionFormat()
{
return dbeFormat;
}
MessageFormat
getDebugExceptionInstanceFormat()
{
return dbeFormat4Instance;
}
MessageFormat
getEntryFormat()
{
return entryFormat;
}
MessageFormat
getEntryInstanceFormat()
{
return entryFormat4Instance;
}
MessageFormat
getExitFormat()
{
return exitFormat;
}
MessageFormat
getExitInstanceFormat()
{
return exitFormat4Instance;
}
/**
* Returns whether this SimpleLog
, when printing output to a file, will also print
* the output to the console. This attribute has no effect when output is not going to a file.
*
* @return true
if output will be piped to the console, false
if it
* won't.
*/
public boolean
isPipingOutputToConsole()
{
return pipingOutputToConsole;
}
/**
* Sets whether this SimpleLog
, when printing output to a file, should also print
* the output to the console. This attribute has no effect when output is not going to a file.
*
* @param pipeOutputToConsole true
if output should be piped to the console as well
* as the output file, false
if it should not.
*/
public void
setPipingOutputToConsole(boolean pipeOutputToConsole)
{
this.pipingOutputToConsole = pipeOutputToConsole;
}
/**
* Format implementation that prints the stack trace of an exception after the exception object's
* toString.
*/
private static final class
ExceptionFormat
extends Format
{
public StringBuffer
format(Object obj, StringBuffer buf, FieldPosition pos)
{
if (!(obj instanceof Throwable))
{
throw new IllegalArgumentException(getClass().getName() + " only formats Throwables.");
}
Throwable t = (Throwable) obj;
buf.append(t);
// Append the stack trace to the buffer.
StringWriter sw;
t.printStackTrace(new PrintWriter(sw = new StringWriter()));
buf.append(LINE_SEP).append(sw.toString());
return buf;
}
public Object
parseObject (String source, ParsePosition pos)
{
// Parsing not supported.
throw new UnsupportedOperationException();
}
}
/**
* A {@link TimerTask} that checks to see if the last modified date of the configuration source
* (which must be a "file" URL) has changed and, if it has, reloads the SimpleLog
's
* configuration.
*/
private final class
FileConfigurationReloader
extends TimerTask
{
private final File configurationFile;
private long previousLastModified;
public
FileConfigurationReloader()
{
try
{
URI uri = new URI(configurationSource.toExternalForm());
this.configurationFile = new File(uri);
this.previousLastModified = configurationFile.lastModified();
}
catch (URISyntaxException e)
{
throw new IllegalArgumentException("Failed to create URI from URL due to " + e);
}
}
public void
run()
{
long lastModified = configurationFile.lastModified();
if (previousLastModified != lastModified)
{
reloadProperties();
previousLastModified = lastModified;
}
}
}
/**
* A {@link TimerTask} that checks to see if the last modified date of the configuration source
* has changed and, if it has, reloads the SimpleLog
's configuration. For
* configuration sources that cannot provide a last modified date, the properties in the
* configuration source are loaded on every iteration and compared to the current properties.
*/
private final class
UrlConfigurationReloader
extends TimerTask
{
private long previousLastModified;
public
UrlConfigurationReloader()
{
try
{
URLConnection connection = configurationSource.openConnection();
previousLastModified = connection.getLastModified();
}
catch (IOException e)
{
previousLastModified = 0;
}
}
public void
run()
{
long lastModified;
try
{
URLConnection connection = configurationSource.openConnection();
lastModified = connection.getLastModified();
}
catch (IOException e)
{
lastModified = 0;
}
if (lastModified == 0)
{
// Can't get a date, so read the properties and compare
Properties currentProperties = new Properties();
try
{
InputStream inputStream = configurationSource.openStream();
currentProperties.load(inputStream);
if (!currentProperties.equals(properties))
{
reloadProperties();
}
}
catch (IOException e)
{
// Fail silently! (My pet hate)
}
}
else if (previousLastModified != lastModified)
{
reloadProperties();
previousLastModified = lastModified;
}
}
}
private static class
ErrorReporter
implements RolloverManager.ErrorReporter
{
public void
error(String description, Throwable t, boolean printExceptionType)
{
printError(description, t, printExceptionType);
}
private static RolloverManager.ErrorReporter
create()
{
return new ErrorReporter();
}
}
}