Index: D:/home/wsargent/javawork/layered-configurator-extra/src/test/java/org/apache/log4j/spi/MockAppender.java =================================================================== --- D:/home/wsargent/javawork/layered-configurator-extra/src/test/java/org/apache/log4j/spi/MockAppender.java (revision 0) +++ D:/home/wsargent/javawork/layered-configurator-extra/src/test/java/org/apache/log4j/spi/MockAppender.java (revision 0) @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.log4j.spi; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.log4j.AppenderSkeleton; +import org.apache.log4j.Level; + +/** + * A mock appender that provides a nice and easy way to verify that logging + * events passed through a logger end up where they are supposed to go. + *

+ * The basic structure of this class is taken from EasyMock. You tell it to + * expect some number of events, then set it to "replay", run the test and then + * call "verify" to see that the expected result occured. + *

+ * This class is written solely for testing the LayeredConfigurator, and may or + * may not be suitable for other tests. + * + * @author Will Sargent + * + */ +public class MockAppender extends AppenderSkeleton { + + private List expectedEvents = new ArrayList(); + + private List actualEvents = new ArrayList(); + + private boolean replayMode = false; + + private static MockAppender sSingleton; + + /** + * Gets the singleton mock appender. This is inelegant, but we need it so that + * we can get at the appender instance defined in the base-appenders.xml file. + * + * @return the mock appender defined. + */ + public static MockAppender getMockAppender() { + return sSingleton; + } + + /** + * A no-args constructor that initializes the singleton. Probably not + * thread-safe. + */ + public MockAppender() { + sSingleton = this; + } + + public void expectEvent(LoggingEvent expectedEvent) { + if (replayMode) { + throw new IllegalStateException("Cannot expect events in replay mode"); + } + + expectedEvents.add(expectedEvent); + } + + /** + * Sets replayMode to false. + */ + public void replay() { + replayMode = true; + } + + /** + * Resets the lists and sets replayMode to false. + */ + public void reset() { + expectedEvents.clear(); + actualEvents.clear(); + replayMode = false; + } + + public void verify() { + // Run through the expected events and see if they match the actual events. + if (!replayMode) { + throw new IllegalStateException("verify() called before replay()"); + } + + // Check we've got the same number... + if (actualEvents.size() != expectedEvents.size()) { + String msg = "Expected " + expectedEvents.size() + + " events, but received " + actualEvents.size() + "."; + throw new AssertionError(msg); + } + + // Only check that the level and message are the same... + for (int i = 0; i < expectedEvents.size(); i++) { + LoggingEvent expectedEvent = (LoggingEvent) expectedEvents.get(i); + LoggingEvent actualEvent = (LoggingEvent) actualEvents.get(i); + + final Level expectedLevel = expectedEvent.getLevel(); + final Level actualLevel = actualEvent.getLevel(); + if (!expectedLevel.equals(actualLevel)) { + String msg = "Expected " + expectedLevel + ", but was " + actualLevel; + throw new AssertionError(msg); + } + + final Object expectedMessage = expectedEvent.getMessage(); + final Object actualMessage = actualEvent.getMessage(); + if (!expectedMessage.equals(actualMessage)) { + String msg = "Expected " + expectedMessage + ", but was " + + actualMessage; + throw new AssertionError(msg); + } + } + } + + protected void append(LoggingEvent event) { + // Check that we're okay to get events... + if (!replayMode) { + throw new IllegalStateException( + "Event appended before replay() was called"); + } + + // Save this for later. + actualEvents.add(event); + } + + public void close() { + ; // do nothing. + } + + public boolean requiresLayout() { + return false; + } + +} Index: D:/home/wsargent/javawork/layered-configurator-extra/src/test/java/org/apache/log4j/spi/LayeredConfiguratorTest.java =================================================================== --- D:/home/wsargent/javawork/layered-configurator-extra/src/test/java/org/apache/log4j/spi/LayeredConfiguratorTest.java (revision 0) +++ D:/home/wsargent/javawork/layered-configurator-extra/src/test/java/org/apache/log4j/spi/LayeredConfiguratorTest.java (revision 0) @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.log4j.spi; + +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; + +import junit.framework.TestCase; + +import org.apache.log4j.Hierarchy; +import org.apache.log4j.Level; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; +import org.apache.log4j.helpers.Loader; + +/** + * Unit tests for the layered configurator. Most of these have to do with + * parsing the ${system.property} syntax. + * + * @author Will Sargent + */ +public class LayeredConfiguratorTest extends TestCase { + private static final Object GUARD = new Object(); + + LayeredConfigurator configurator; + + LoggerRepository repository = new Hierarchy(new RootLogger(Level.DEBUG)); + + protected void setUp() throws Exception { + super.setUp(); + configurator = new LayeredConfigurator(); + } + + /** + * Tests that a normal line makes it through parsing. + */ + public final void testParseLineWithNoParameter() { + String expected = "http://example.org"; + try { + String actual = configurator.parseLine(expected); + assertEquals("Line should have been seen.", expected, actual); + } catch (ParseException e) { + fail(e.toString()); + } + } + + /** + * Tests that a line with substitution makes it through parsing. + */ + public final void testParseLineWithParameter() { + String expected = "http://${my.hostname}.org"; + try { + String actual = configurator.parseLine(expected); + assertEquals("Line should have been seen.", expected, actual); + } catch (ParseException e) { + fail(e.toString()); + } + } + + /** + * Tests that a line with no terminator does NOT make it through parsing. + */ + public final void testParseLineWithNoParameterTermination() { + try { + String url = "http://${my.hostname.org"; + configurator.parseLine(url); + fail("Should have thrown parse exception"); + } catch (ParseException pe) { + } + } + + /** + * Comments should show up as null through the parser. + */ + public final void testParseLineWithComment() { + try { + String url = "# http://${my.hostname.org"; + String actual = configurator.parseLine(url); + assertNull("Line should have been null.", actual); + } catch (ParseException pe) { + fail(pe.toString()); + } + } + + /** + * Tests that a line containing a raw dollar makes it through parsing. + */ + public final void testParseLineWithDollar() { + try { + String url = "http://$my.hostname.org"; + configurator.parseLine(url); + } catch (ParseException pe) { + fail("Should not have thrown parse exception"); + } + } + + /** + * Tests that parameters are substituted appropriately. + */ + public final void testSubstituteParameters() { + // Currently only system properties are substituted. + System.setProperty("my.hostname", "example"); + + String url = "http://${my.hostname}.org"; + String expected = "http://example.org"; + String actual = configurator.substituteParameters(url); + + assertEquals("Parameter should have been substituted", expected, actual); + } + + /** + * Tests the layered configurator in the development configuration. This should + * run all the way through debug to fatal. + * + * @throws MalformedURLException + */ + public final void testDevelopmentConfiguration() throws MalformedURLException { + + // Run through the log4j configuration using the development settings. + // The LogManager has all of the initialization logic tied up in a + // static method, so we have to set things up so that we can switch + // it out on the fly. + + // Set the property so it will resolve to "dev-loggers.properties" + System.setProperty("my.environment", "dev"); + + URL url = Loader + .getResource("org/apache/log4j/spi/log4j-config.properties"); + configurator.doConfigure(url, repository); + + // Set up the log manager to have the appropriate repository selector... + RepositorySelector selector = new DefaultRepositorySelector(repository); + + LogManager.setRepositorySelector(selector, GUARD); + + // Call up a logger, and run it through everything. + String categoryClass = "com.tersesystems.log4j"; + Logger logger = LogManager.getLogger(categoryClass); + + // Look at the appender and see if we got all the messages we expect. + MockAppender appender = MockAppender.getMockAppender(); + appender.reset(); + + final String debugMessage = "Debug Message"; + final String infoMessage = "Info message"; + final String warnMessage = "Warning message"; + final String errorMessage = "Error Message"; + final String fatalMessage = "Fatal message"; + + appender.expectEvent(new LoggingEvent(categoryClass, logger, Level.DEBUG, debugMessage, null)); + appender.expectEvent(new LoggingEvent(categoryClass, logger, Level.INFO, infoMessage, null)); + appender.expectEvent(new LoggingEvent(categoryClass, logger, Level.WARN, warnMessage, null)); + appender.expectEvent(new LoggingEvent(categoryClass, logger, Level.ERROR, errorMessage, null)); + appender.expectEvent(new LoggingEvent(categoryClass, logger, Level.FATAL, fatalMessage, null)); + + appender.replay(); + logger.debug(debugMessage); + logger.info(infoMessage); + logger.warn(warnMessage); + logger.error(errorMessage); + logger.fatal(fatalMessage); + appender.verify(); + } + + /** + * Tests the layered configurator in the development configuration. This should + * run all the way through debug to fatal. + * + * @throws MalformedURLException + */ + public final void testProductionConfiguration() throws MalformedURLException { + + // Run through the log4j configuration using the development settings. + // The LogManager has all of the initialization logic tied up in a + // static method, so we have to set things up so that we can switch + // it out on the fly. + + // Set the property so it will resolve to "prod-loggers.properties" + System.setProperty("my.environment", "prod"); + + URL url = Loader + .getResource("org/apache/log4j/spi/log4j-config.properties"); + configurator.doConfigure(url, repository); + + // Set up the log manager to have the appropriate repository selector... + RepositorySelector selector = new DefaultRepositorySelector(repository); + + LogManager.setRepositorySelector(selector, GUARD); + + // Call up a logger, and run it through everything. + String categoryClass = "com.tersesystems.log4j"; + Logger logger = LogManager.getLogger(categoryClass); + + // Look at the appender and see if we got all the messages we expect. + MockAppender appender = MockAppender.getMockAppender(); + appender.reset(); + + final String debugMessage = "Debug Message"; + final String infoMessage = "Info message"; + final String warnMessage = "Warning message"; + final String errorMessage = "Error Message"; + final String fatalMessage = "Fatal message"; + + // Only expect WARN and above to actually be appended. + appender.expectEvent(new LoggingEvent(categoryClass, logger, Level.WARN, warnMessage, null)); + appender.expectEvent(new LoggingEvent(categoryClass, logger, Level.ERROR, errorMessage, null)); + appender.expectEvent(new LoggingEvent(categoryClass, logger, Level.FATAL, fatalMessage, null)); + + // Play through everything, including the (disabled) debug and info messages. + appender.replay(); + logger.debug(debugMessage); + logger.info(infoMessage); + logger.warn(warnMessage); + logger.error(errorMessage); + logger.fatal(fatalMessage); + appender.verify(); + } +} Index: D:/home/wsargent/javawork/layered-configurator-extra/src/test/java/org/apache/log4j/spi/ConfiguratorFactoryTest.java =================================================================== --- D:/home/wsargent/javawork/layered-configurator-extra/src/test/java/org/apache/log4j/spi/ConfiguratorFactoryTest.java (revision 0) +++ D:/home/wsargent/javawork/layered-configurator-extra/src/test/java/org/apache/log4j/spi/ConfiguratorFactoryTest.java (revision 0) @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.log4j.spi; + +import java.net.URL; + +import org.apache.log4j.PropertyConfigurator; +import org.apache.log4j.xml.DOMConfigurator; + +import junit.framework.TestCase; + +/** + * Tests the configurator factory. + * + * @author Will Sargent + * @see org.apache.log4j.spi.ConfiguratorFactory + */ +public class ConfiguratorFactoryTest extends TestCase { + + private ConfiguratorFactory configuratorFactory; + + protected void setUp() throws Exception { + super.setUp(); + configuratorFactory = new ConfiguratorFactory(); + } + + /** + * Tests adding a fake configurator. + * @throws IllegalAccessException + * @throws InstantiationException + */ + public final void testAddConfigurator() throws InstantiationException, IllegalAccessException { + configuratorFactory.addConfigurator("yaml", YamlConfigurator.class); + Configurator actual = configuratorFactory.findConfiguratorByType("yaml"); + + assertTrue("Expected yaml configurator here", actual instanceof YamlConfigurator); + } + + /** + * Checks that we can get a properties configurator. + */ + public final void testGetPropertiesConfigurator() { + Configurator actual = configuratorFactory + .getConfigurator("log4j.properties"); + assertTrue("Expected a property configurator", + actual instanceof PropertyConfigurator); + } + + /** + * Tests getting the XML configurator. + */ + public final void testGetDOMConfigurator() { + Configurator actual = configuratorFactory.getConfigurator("log4j.xml"); + assertTrue("Expected a property configurator", + actual instanceof DOMConfigurator); + } + + /** + * Tests that an exception is thrown if we don't find a configurator. + */ + public final void testGetMissingConfigurator() { + try { + configuratorFactory.getConfigurator("log4j.yaml"); + fail("An illegal state exception should have been thrown"); + } catch (IllegalStateException e) { + } + } + + /** + * A fake YAML configurator to test things out. + */ + static class YamlConfigurator implements Configurator { + + public void doConfigure(URL url, LoggerRepository repository) { + ; // do nothing + } + } +} Index: D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/prod-loggers.properties =================================================================== --- D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/prod-loggers.properties (revision 0) +++ D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/prod-loggers.properties (revision 0) @@ -0,0 +1,2 @@ +# Define loggers with production properties +log4j.logger.com.tersesystems.log4j=WARN \ No newline at end of file Index: D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/base-appenders.xml =================================================================== --- D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/base-appenders.xml (revision 0) +++ D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/base-appenders.xml (revision 0) @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + Index: D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/dev-loggers.properties =================================================================== --- D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/dev-loggers.properties (revision 0) +++ D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/dev-loggers.properties (revision 0) @@ -0,0 +1,2 @@ +# Define loggers with development settings. +log4j.logger.com.tersesystems.log4j=DEBUG \ No newline at end of file Index: D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/log4j-config.properties =================================================================== --- D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/log4j-config.properties (revision 0) +++ D:/home/wsargent/javawork/layered-configurator-extra/src/test/resources/org/apache/log4j/spi/log4j-config.properties (revision 0) @@ -0,0 +1,9 @@ +# This file contains the XML and properties files to load, in order. +# It is used by the layered configurator. + +# The complicated appenders are rendered in XML. +org/apache/log4j/spi/base-appenders.xml + +# The logging levels can be defined in properties, and can use system properties +# as parameters. +org/apache/log4j/spi/${my.environment}-loggers.properties Index: D:/home/wsargent/javawork/layered-configurator-extra/src/main/java/org/apache/log4j/spi/LayeredConfigurator.java =================================================================== --- D:/home/wsargent/javawork/layered-configurator-extra/src/main/java/org/apache/log4j/spi/LayeredConfigurator.java (revision 0) +++ D:/home/wsargent/javawork/layered-configurator-extra/src/main/java/org/apache/log4j/spi/LayeredConfigurator.java (revision 0) @@ -0,0 +1,387 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.log4j.spi; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.log4j.LogManager; +import org.apache.log4j.helpers.Loader; +import org.apache.log4j.helpers.LogLog; +import org.apache.log4j.helpers.OptionConverter; + +/** + * This class will go through a properties files with references to DOM or + * Properties configurator file, and call the appropriate configurator in the + * order they are defined. + *

+ * This doesn't mean much in theory, so let's explain why this class exists, and + * give an example. + *

+ * Log4J lacks a way to spread configuration over several files. You have one + * configuration file, either log4j.properties or log4j.xml, and that's it. + *

+ * In projects where you have several environments to keep track of, you may + * want to have some loggers set to DEBUG in the development environment, but + * set to WARN on the production environment. However, you usually want to keep + * all the appenders and logging infrastructure the same. + *

+ * Because Log4J defines appenders and loggers in the same file, you either have + * to define several almost identical files with the appropriate changes for the + * environment, or you have to have an Ant script that goes through and replaces + * tokens for the appropriate environment. + *

+ * The file syntax for the configurators is simple. Here's an example + * log4j-config.properties file: + * + *

+ * # The complicated appenders are rendered in XML.
+ * base-appenders.xml
+ *
+ * # The logging levels can be defined in properties, and can use system properties
+ * # as parameters.
+ * ${my.environment}-loggers.properties
+ * 
+ * + *

+ * The class is called LayeredConfigurator because it's meant to work in layers. + * The configurators are not reset, so all the settings from the previous + * configurator will still apply unless you override them. + *

+ * When you define -Dmy.environment=dev, then your development + * settings will be loaded. When you define -Dmy.environment=prod, + * then your production settings will be loaded. Either way, your binary + * distribution is exactly the same, with only the environment specific + * properties + *

+ * To enable this class, you must start the JVM with the following system + * properties: + * + *

+ * -Dlog4j.configuratorClass=com.tersesystems.log4j.LayeredConfigurator
+ * -Dlog4j.configuration=log4j-config.properties
+ * 
+ * + *

+ * This class has been written using Java 1.3 syntax, lots of internal methods, + * and with the internal methods protected, so that it can be subclassed and + * modified as needed. + * + * @author Will Sargent + * + * @see org.apache.log4j.spi.Configurator + * @see org.apache.log4j.spi.ConfiguratorFactory + */ +public class LayeredConfigurator implements Configurator { + + protected static final String PARAMETER_START = "${"; + + protected static final String PARAMETER_END = "}"; + + protected static final String COMMENT = "#"; + + /** + * A static version of {@link #doConfigure(URL, LoggerRepository)}. + */ + public static void configure(URL url) { + new LayeredConfigurator() + .doConfigure(url, LogManager.getLoggerRepository()); + } + + /* + * (non-Javadoc) + * + * @see org.apache.log4j.spi.Configurator#doConfigure(java.net.URL, + * org.apache.log4j.spi.LoggerRepository) + */ + public void doConfigure(URL configURL, LoggerRepository repository) { + LogLog.debug("Reading configuration from URL " + configURL); + + if (configURL == null) { + throw new IllegalArgumentException("null configURL"); + } + + if (repository == null) { + throw new IllegalArgumentException("null repository"); + } + + InputStream istream = null; + try { + istream = configURL.openStream(); + doConfigure(istream, repository); + } catch (Exception e) { + LogLog.error("Could not read configuration file from URL [" + configURL + + "].", e); + LogLog.error("Ignoring configuration file [" + configURL + "]."); + return; + } + } + + /** + * Converts the input stream into a reader and then passes to the reader + * method. + * + * @param istream + * @param repository + * @throws IOException + * @throws ParseException + */ + protected void doConfigure(InputStream istream, LoggerRepository repository) + throws IOException, ParseException { + if (istream == null) { + return; + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(istream)); + List configurationReferences = parseConfigurationList(reader); + + LogLog.debug("list = " + configurationReferences); + + doConfigure(configurationReferences, repository); + } + + /** + * Parses the lines of text from the reader. This method deals with the + * opening and closing of IO handles. + * + * @param reader + * the reader to pull in the file. + * @return a list containing the parsed strings. + */ + protected List parseConfigurationList(BufferedReader reader) + throws IOException, ParseException { + if (reader == null) { + throw new IllegalArgumentException("null reader"); + } + + List configurationList = new ArrayList(); + String line = null; + try { + while ((line = reader.readLine()) != null) { + String parsedLine = parseLine(line); + if (parsedLine != null) { + configurationList.add(parsedLine); + } + } + } finally { + reader.close(); + } + return configurationList; + } + + /** + * See if the line starts with #. If it does, then it's a comment. + * + * We don't assume that anything after # can be a comment, because there's + * always the possibility that someone has factored in a URL with a bookmark. + * + * This method does not do any substitution of variables; we want to make sure + * we can parse everything before we move any further. + * + * @throws ParseException + * if the line cannot be parsed. + */ + protected String parseLine(String rawLine) throws ParseException { + // If nothing, then nothing. + if (rawLine == null) { + return null; + } + + String line = rawLine; + String trimmedLine = line.trim(); + + // Ignore a completely empty line. + if (trimmedLine.length() == 0) // NOPMD + { + return null; + } + + // If the line contains nothing but whitespace and then "#" then we + // consider it a comment. + if (trimmedLine.startsWith(COMMENT)) { + return null; + } + + // If the line contains "${" but not "}" then throw an exception. + int fromIndex = line.indexOf(PARAMETER_START); + if (fromIndex != -1) { + int endIndex = line.indexOf(PARAMETER_END, fromIndex + 1); + if (endIndex == -1) { + throw new ParseException("Cannot parse line: " + line, fromIndex); + } + + // For simplicity's sake, let's not nest. + int newStartIndex = line.indexOf(PARAMETER_START, fromIndex + 1); + if (newStartIndex != -1 && newStartIndex < endIndex) { + throw new ParseException("Cannot parse line: " + line, newStartIndex); + } + } + + return rawLine; + } + + /** + * Returns a configurator factory. + * + * @return a new configurator factory. + */ + protected ConfiguratorFactory getConfiguratorFactory() { + return new ConfiguratorFactory(); + } + + /** + * Configures a list of configuration references, substituting parameters as + * needed. + * + * @param configurationReferences + * @param repository + */ + protected void doConfigure(List configurationReferences, + LoggerRepository repository) { + // Run through any substitutions of parameters. Parameters are defined + // in the syntax ${parameter}. They must resolve to strings. + List parsedReferences = substituteListParameters(configurationReferences); + + ConfiguratorFactory configuratorFactory = getConfiguratorFactory(); + + // Go through the list of references, using a configurator factory to + // pull out the appropriate configurator. + for (Iterator iter = parsedReferences.iterator(); iter.hasNext();) { + String reference = (String) iter.next(); + Configurator c = configuratorFactory.getConfigurator(reference); + + URL url; + try { + url = new URL(reference); + } catch (MalformedURLException ex) { + // so, resource is not a URL: + // attempt to get the resource from the class path + url = Loader.getResource(reference); + } + + // The internal reference doesn't resolve to anything either... + if (url == null) { + String msg = "The internal reference " + reference + " does not resolve to a resource"; + throw new IllegalArgumentException(msg); + } + + // Tell the configurator to go do its stuff. + c.doConfigure(url, repository); + } + } + + /** + * Goes through a list of configuration references that may contain + * ${parameter} in the string, and resolves the parameters. + * + * @param configurationReferences + * a list of configuration references with parameters. + * @return a list of configuration files with the parameters replaced by text. + */ + protected List substituteListParameters(List configurationReferences) { + List resolvedList = new ArrayList(); + for (Iterator iter = configurationReferences.iterator(); iter.hasNext();) { + String configRef = (String) iter.next(); + String resolvedRef = substituteParameters(configRef); + resolvedList.add(resolvedRef); + } + + return resolvedList; + } + + /** + * Substitute all the parameters in a reference. + * + * @param configRef + * @return a reference with all the parameters replaced by text. + */ + protected String substituteParameters(String configRef) { + // If I assume Java 1.4 or 1.5, this could be much easier. But let's do + // it the old fashioned way. + + // state can be 1 {dollar), 2 (parsing), or 0 (raw). + // We assume that the string cannot be malformed here. + int state = 0; + StringBuffer line = new StringBuffer(); + StringBuffer parameterName = new StringBuffer(); + byte[] characters = configRef.getBytes(); + for (int i = 0; i < characters.length; i++) { + char ch = (char) characters[i]; + switch (ch) { + case '$': + state = 1; + break; + + case '{': + if (state == 1) { + state = 2; + parameterName.setLength(0); // clear the param name. + } + break; + + case '}': + state = 0; + // look up the parameter name, and append to the line. + String parameterValue = substituteParameter(parameterName.toString()); + line.append(parameterValue); + break; + + default: + if (state == 0) // a raw char + { + line.append(ch); + } else if (state == 1) // just a raw dollar + { + state = 0; + line.append(ch); + } else if (state == 2) // this is a parameter name + { + parameterName.append(ch); + } + } + } + + return line.toString(); + } + + /** + * Substitutes a single parameter. A null value will be replaced by the empty + * string. + * + * @param parameterName + * the parameter's name. + * @return the parameter value. + */ + protected String substituteParameter(String parameterName) { + LogLog.debug("substituteParameter: found " + parameterName); + + String parameterValue = OptionConverter + .getSystemProperty(parameterName, ""); + LogLog.debug("substituteParameter: replacing " + parameterName + " with " + + parameterValue); + return parameterValue; + } +} Index: D:/home/wsargent/javawork/layered-configurator-extra/src/main/java/org/apache/log4j/spi/ConfiguratorFactory.java =================================================================== --- D:/home/wsargent/javawork/layered-configurator-extra/src/main/java/org/apache/log4j/spi/ConfiguratorFactory.java (revision 0) +++ D:/home/wsargent/javawork/layered-configurator-extra/src/main/java/org/apache/log4j/spi/ConfiguratorFactory.java (revision 0) @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +package org.apache.log4j.spi; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.log4j.PropertyConfigurator; +import org.apache.log4j.spi.Configurator; +import org.apache.log4j.xml.DOMConfigurator; + +/** + * This is a simple class to abstract the instantation of a configurator. + *

+ * The factory contains a map of types, along with the configurator objects that + * will be used for those types. The factory is configured for references ending + * in ".properties" or ".xml", but other configurators can be added as need be. + *

+ * This class may or may not be thread-safe. No attempt has been made to ensure + * thread-safety. + * + * @author Will Sargent + * @see org.apache.log4j.PropertyConfigurator + * @see org.apache.log4j.xml.DOMConfigurator + */ +public class ConfiguratorFactory { + + /** The string that will return a Properties Configurator. */ + public static final String PROPERTIES_TYPE = "properties"; + + /** The string that will return a DOM configurator. */ + public static final String XML_TYPE = "xml"; + + /** + * A map of types to configurator instances. + */ + private Map configuratorMap = new HashMap(); + + /** + * A default constructor that will call the initialize() method. + */ + public ConfiguratorFactory() { + initialize(); + } + + /** + * Initializes the configurator map. + */ + protected void initialize() { + addConfigurator(PROPERTIES_TYPE, PropertyConfigurator.class); + addConfigurator(XML_TYPE, DOMConfigurator.class); + } + + /** + * Adds a configurator class with the given type. + * + * @param type + * the type of configurator to use. + * @param configuratorClass + * the configurator class. Must take a no-args constructor. + */ + public void addConfigurator(String type, Class configuratorClass) { + if (type == null) { + throw new IllegalArgumentException("Null type"); + } + + if (configuratorClass == null) { + throw new IllegalArgumentException("Null configurator"); + } + + configuratorMap.put(type, configuratorClass); + } + + /** + * Given a reference to a configurator, returns the appropriate configurator + * instance. For example, if the reference is "log4j.properties", then it has + * the "properties" type and a PropertyConfigurator is used. + * + * @param reference + * a reference to a name (typically a file or classpath reference) + * @return the appropriate Configurator object for the reference. + * @throw IllegalStateException if no configurator is found, or the + * configurator could not be instantiated. + */ + public Configurator getConfigurator(String reference) { + if (reference == null) { + throw new IllegalArgumentException("Null reference found."); + } + + // Find the last index of dot. We'll use this as a lookup table. + int lastIndex = reference.lastIndexOf('.'); + + // No dot found. + if (lastIndex == -1) { + throw new IllegalArgumentException("No dot suffix found"); + } + + String suffix = reference.substring(lastIndex + 1); + if (suffix.length() == 0) { + throw new IllegalArgumentException("Null or empty suffix found."); + } + + // Get the configurator. + try { + Configurator c = findConfiguratorByType(suffix); + return c; + } catch (InstantiationException ie) { + String msg = "Could not instantiate configurator from reference " + reference; + throw new IllegalStateException(msg, ie); + } catch (IllegalAccessException iae) { + String msg = "Could not instantiate configurator from reference " + reference; + throw new IllegalStateException(msg, iae); + } + } + + /** + * Instantiates a new configurator given the appropriate type. + * + * @param type + * the type of configurator to use. + * @return the configurator object for that type. + * @throws IllegalAccessException + * @throws InstantiationException + * @throw IllegalStateException if no configurator is found for the reference. + */ + protected Configurator findConfiguratorByType(String type) + throws InstantiationException, IllegalAccessException { + Class configuratorClass = (Class) configuratorMap.get(type); + if (configuratorClass == null) { + throw new IllegalStateException("No configurator class found for type " + + type); + } + return (Configurator) configuratorClass.newInstance(); + } +}