Moved component and receivers companion sources to the Chainsaw source tree - those companions won't be released.


git-svn-id: https://svn.apache.org/repos/asf/logging/chainsaw/trunk@1178304 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/HOWTOBUILD.txt b/HOWTOBUILD.txt
index 7375489..ebbd4bb 100644
--- a/HOWTOBUILD.txt
+++ b/HOWTOBUILD.txt
@@ -23,19 +23,6 @@
 http://maven.apache.org
 
 
-Next, an interim step is required until the 'companions' are voted on and released:
-
-* SVN check out the following projects:
-	log4j-component - http://svn.apache.org/repos/asf/logging/log4j/companions/component/trunk/
-	log4j-extras - http://svn.apache.org/repos/asf/logging/log4j/companions/extras/trunk/
-	log4j-receivers - http://svn.apache.org/repos/asf/logging/log4j/companions/receivers/trunk/
-
-  In each of these checkout directories:
-  	mvn install
-
-  This will install into your local maven repository the correct releases of the required dependencies. 
-  These dependencies are not yet available in the standard maven repositories, and so failure to complete this step will prevent you from building Chainsaw.
-
 * cd to Chainsaw project checkout directory
 
 	mvn install
diff --git a/pom.xml b/pom.xml
index f434fcd..18c0f43 100644
--- a/pom.xml
+++ b/pom.xml
@@ -118,8 +118,7 @@
       <plugin>

         <artifactId>maven-compiler-plugin</artifactId>

         <configuration>

-          <source>1.2</source>

-          <target>1.1</target>

+          <source>1.4</source>

         </configuration>

       </plugin>

       <plugin>

@@ -220,7 +219,7 @@
             <artifactId>ant-contrib</artifactId>

             <version>1.0b2</version>

           </dependency>

-        </dependencies>

+	</dependencies>

       </plugin>

         <plugin>

           <groupId>org.codehaus.mojo</groupId>

@@ -324,6 +323,14 @@
           <iconFile>${basedir}/src/main/resources/logo.icns</iconFile>

           <bundleName>Chainsaw</bundleName>

         </configuration>

+        <executions>

+            <execution>

+                <phase>package</phase>

+                <goals>

+                    <goal>bundle</goal>

+                </goals>

+            </execution>

+       </executions>

       </plugin>

       <plugin>

         <groupId>org.codehaus.mojo</groupId>

@@ -386,17 +393,6 @@
     </dependency>

     <dependency>

       <groupId>log4j</groupId>

-      <artifactId>apache-log4j-receivers</artifactId>

-      <version>1.0-SNAPSHOT</version>

-    </dependency>

-    <dependency>

-      <groupId>log4j</groupId>

-      <artifactId>apache-log4j-receivers</artifactId>

-      <version>1.0-SNAPSHOT</version>

-      <classifier>javadoc</classifier>

-    </dependency>

-    <dependency>

-      <groupId>log4j</groupId>

       <artifactId>log4j</artifactId>

       <version>1.2.16</version>

     </dependency>

@@ -437,6 +433,18 @@
       <artifactId>jsch</artifactId>

       <version>0.1.42</version>

     </dependency>

+    <dependency>

+		<groupId>hsqldb</groupId>

+		<artifactId>hsqldb</artifactId>

+		<version>1.8.0.7</version>

+		<scope>test</scope>

+	</dependency>

+    <dependency>

+        <groupId>org.apache.geronimo.specs</groupId>

+        <artifactId>geronimo-jms_1.1_spec</artifactId>

+        <version>1.0</version>

+        <optional>true</optional>

+    </dependency>

   </dependencies>

   <reporting>

     <excludeDefaults>true</excludeDefaults>

@@ -480,7 +488,7 @@
       </plugin>

     </plugins>

   </reporting>

-  <distributionManagement>

+    <distributionManagement>

     <repository>

       <id>logging.repo</id>

       <url>scp://people.apache.org/www/people.apache.org/builds/logging/repo/</url>

diff --git a/src/main/java/org/apache/log4j/LoggerRepositoryExImpl.java b/src/main/java/org/apache/log4j/LoggerRepositoryExImpl.java
new file mode 100644
index 0000000..c2f5aaf
--- /dev/null
+++ b/src/main/java/org/apache/log4j/LoggerRepositoryExImpl.java
@@ -0,0 +1,697 @@
+/*
+ * 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;
+
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.or.ObjectRenderer;
+import org.apache.log4j.or.RendererMap;
+import org.apache.log4j.plugins.Plugin;
+import org.apache.log4j.plugins.PluginRegistry;
+import org.apache.log4j.scheduler.Scheduler;
+import org.apache.log4j.spi.ErrorItem;
+import org.apache.log4j.spi.HierarchyEventListener;
+import org.apache.log4j.spi.LoggerEventListener;
+import org.apache.log4j.spi.LoggerFactory;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggerRepositoryEventListener;
+import org.apache.log4j.spi.LoggerRepositoryEx;
+import org.apache.log4j.spi.RendererSupport;
+import org.apache.log4j.xml.UnrecognizedElementHandler;
+import org.apache.log4j.xml.DOMConfigurator;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Vector;
+
+
+/**
+ * This class implements LoggerRepositoryEx by
+ *   wrapping an existing LoggerRepository implementation
+ *   and implementing the newly added capabilities.
+*/
+public final class LoggerRepositoryExImpl
+        implements LoggerRepositoryEx,
+        RendererSupport,
+        UnrecognizedElementHandler {
+
+    /**
+     * Wrapped logger repository.
+     */
+  private final LoggerRepository repo;
+
+    /**
+     * Logger factory.  Does not affect class of logger
+     * created by underlying repository.
+     */
+  private LoggerFactory loggerFactory;
+
+    /**
+     *  Renderer support.
+     */
+  private final RendererSupport rendererSupport;
+
+    /**
+     * List of repository event listeners.
+     */
+  private final ArrayList repositoryEventListeners = new ArrayList();
+    /**
+     * Map of HierarchyEventListener keyed by LoggingEventListener.
+     */
+  private final Map loggerEventListeners = new HashMap();
+    /**
+     * Name of hierarchy.
+     */
+  private String name;
+    /**
+     * Plug in registry.
+     */
+  private PluginRegistry pluginRegistry;
+    /**
+     * Properties.
+     */
+  private final Map properties = new Hashtable();
+    /**
+     * Scheduler.
+     */
+  private Scheduler scheduler;
+
+  /** The repository can also be used as an object store
+   * for various objects used by log4j components.
+   */
+  private Map objectMap = new HashMap();
+
+
+    /**
+     * Error list.
+     */
+  private List errorList = new Vector();
+
+    /**
+     * True if hierarchy has not been modified.
+     */
+  private boolean pristine = true;
+
+  /**
+     Constructs a new logger hierarchy.
+
+     @param repository Base implementation of repository.
+
+   */
+  public LoggerRepositoryExImpl(final LoggerRepository repository) {
+    super();
+    if (repository == null) {
+        throw new NullPointerException("repository");
+    }
+    repo = repository;
+    if (repository instanceof RendererSupport) {
+        rendererSupport = (RendererSupport) repository;
+    } else {
+        rendererSupport = new RendererSupportImpl();
+    }
+  }
+
+
+  /**
+    Add a {@link LoggerRepositoryEventListener} to the repository. The
+    listener will be called when repository events occur.
+    @param listener listener
+    */
+  public void addLoggerRepositoryEventListener(
+    final LoggerRepositoryEventListener listener) {
+    synchronized (repositoryEventListeners) {
+      if (repositoryEventListeners.contains(listener)) {
+        LogLog.warn(
+          "Ignoring attempt to add a previously "
+                  + "registered LoggerRepositoryEventListener.");
+      } else {
+        repositoryEventListeners.add(listener);
+      }
+    }
+  }
+
+
+  /**
+    Remove a {@link LoggerRepositoryEventListener} from the repository.
+    @param listener listener
+    */
+  public void removeLoggerRepositoryEventListener(
+    final LoggerRepositoryEventListener listener) {
+    synchronized (repositoryEventListeners) {
+      if (!repositoryEventListeners.contains(listener)) {
+        LogLog.warn(
+          "Ignoring attempt to remove a "
+                  + "non-registered LoggerRepositoryEventListener.");
+      } else {
+        repositoryEventListeners.remove(listener);
+      }
+    }
+  }
+
+  /**
+    Add a {@link LoggerEventListener} to the repository. The  listener
+    will be called when repository events occur.
+    @param listener listener
+   */
+  public void addLoggerEventListener(final LoggerEventListener listener) {
+    synchronized (loggerEventListeners) {
+      if (loggerEventListeners.get(listener) != null) {
+        LogLog.warn(
+         "Ignoring attempt to add a previously registerd LoggerEventListener.");
+      } else {
+        HierarchyEventListenerProxy proxy =
+                new HierarchyEventListenerProxy(listener);
+        loggerEventListeners.put(listener, proxy);
+        repo.addHierarchyEventListener(proxy);
+      }
+    }
+  }
+
+    /**
+       Add a {@link org.apache.log4j.spi.HierarchyEventListener}
+     event to the repository.
+     @param listener listener
+       @deprecated Superceded by addLoggerEventListener
+    */
+    public
+    void addHierarchyEventListener(final HierarchyEventListener listener) {
+        repo.addHierarchyEventListener(listener);
+    }
+
+
+  /**
+    Remove a {@link LoggerEventListener} from the repository.
+    @param listener listener to be removed
+    */
+  public void removeLoggerEventListener(final LoggerEventListener listener) {
+    synchronized (loggerEventListeners) {
+      HierarchyEventListenerProxy proxy =
+              (HierarchyEventListenerProxy) loggerEventListeners.get(listener);
+      if (proxy == null) {
+        LogLog.warn(
+          "Ignoring attempt to remove a non-registered LoggerEventListener.");
+      } else {
+        loggerEventListeners.remove(listener);
+        proxy.disable();
+      }
+    }
+  }
+
+    /**
+     * Issue warning that there are no appenders in hierarchy.
+     * @param cat logger, not currently used.
+     */
+  public void emitNoAppenderWarning(final Category cat) {
+    repo.emitNoAppenderWarning(cat);
+  }
+
+  /**
+     Check if the named logger exists in the hierarchy. If so return
+     its reference, otherwise returns <code>null</code>.
+
+     @param loggerName The name of the logger to search for.
+     @return true if logger exists.
+  */
+  public Logger exists(final String loggerName) {
+    return repo.exists(loggerName);
+  }
+
+  /**
+   * Return the name of this hierarchy.
+   * @return name of hierarchy
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Set the name of this repository.
+   *
+   * Note that once named, a repository cannot be rerenamed.
+   * @param repoName name of hierarchy
+   */
+  public void setName(final String repoName) {
+    if (name == null) {
+      name = repoName;
+    } else if (!name.equals(repoName)) {
+      throw new IllegalStateException(
+        "Repository [" + name + "] cannot be renamed as [" + repoName + "].");
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public Map getProperties() {
+    return properties;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public String getProperty(final String key) {
+     return (String) properties.get(key);
+  }
+
+  /**
+   * Set a property by key and value. The property will be shared by all
+   * events in this repository.
+   * @param key property name
+   * @param value property value
+   */
+  public void setProperty(final String key,
+                          final String value) {
+   properties.put(key, value);
+  }
+
+  /**
+     The string form of {@link #setThreshold(Level)}.
+   @param levelStr symbolic name for level
+  */
+  public void setThreshold(final String levelStr) {
+    repo.setThreshold(levelStr);
+  }
+
+  /**
+     Enable logging for logging requests with level <code>l</code> or
+     higher. By default all levels are enabled.
+
+     @param l The minimum level for which logging requests are sent to
+     their appenders.  */
+  public void setThreshold(final Level l) {
+    repo.setThreshold(l);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public PluginRegistry getPluginRegistry() {
+   if (pluginRegistry == null) {
+     pluginRegistry = new PluginRegistry(this);
+   }
+   return pluginRegistry;
+  }
+
+
+    /**
+      Requests that a appender added event be sent to any registered
+      {@link LoggerEventListener}.
+      @param logger The logger to which the appender was added.
+      @param appender The appender added to the logger.
+     */
+    public void fireAddAppenderEvent(final Category logger,
+                                     final Appender appender) {
+        repo.fireAddAppenderEvent(logger, appender);
+    }
+
+
+    /**
+      Requests that a appender removed event be sent to any registered
+      {@link LoggerEventListener}.
+      @param logger The logger from which the appender was removed.
+      @param appender The appender removed from the logger.
+      */
+    public void fireRemoveAppenderEvent(final Category logger,
+                                        final Appender appender) {
+       if (repo instanceof Hierarchy) {
+           ((Hierarchy) repo).fireRemoveAppenderEvent(logger, appender);
+       }
+    }
+
+
+  /**
+    Requests that a level changed event be sent to any registered
+    {@link LoggerEventListener}.
+    @param logger The logger which changed levels.
+    */
+  public void fireLevelChangedEvent(final Logger logger) {
+  }
+
+  /**
+   *
+   * Requests that a configuration changed event be sent to any registered
+   * {@link LoggerRepositoryEventListener}.
+   * 
+   */
+  public void fireConfigurationChangedEvent() {
+  }
+
+
+  /**
+     Returns the current threshold.
+     @return current threshold level
+
+     @since 1.2 */
+  public Level getThreshold() {
+    return repo.getThreshold();
+  }
+
+
+  /**
+     Return a new logger instance named as the first parameter using
+     the default factory.
+
+     <p>If a logger of that name already exists, then it will be
+     returned.  Otherwise, a new logger will be instantiated and
+     then linked with its existing ancestors as well as children.
+
+     @param loggerName The name of the logger to retrieve.
+     @return logger
+
+  */
+  public Logger getLogger(final String loggerName) {
+    return repo.getLogger(loggerName);
+  }
+
+  /**
+      Return a new logger instance named as the first parameter using
+      <code>factory</code>.
+
+      <p>If a logger of that name already exists, then it will be
+      returned.  Otherwise, a new logger will be instantiated by the
+      <code>factory</code> parameter and linked with its existing
+      ancestors as well as children.
+
+      @param loggerName The name of the logger to retrieve.
+      @param factory The factory that will make the new logger instance.
+      @return logger
+
+  */
+  public Logger getLogger(final String loggerName,
+                          final LoggerFactory factory) {
+    return repo.getLogger(loggerName, factory);
+  }
+
+  /**
+     Returns all the currently defined categories in this hierarchy as
+     an {@link java.util.Enumeration Enumeration}.
+
+     <p>The root logger is <em>not</em> included in the returned
+     {@link Enumeration}.
+     @return enumerator of current loggers
+   */
+  public Enumeration getCurrentLoggers() {
+    return repo.getCurrentLoggers();
+  }
+
+  /**
+   * Return the the list of previously encoutered {@link ErrorItem error items}.
+   * @return list of errors
+   */
+  public List getErrorList() {
+    return errorList;
+  }
+
+  /**
+   * Add an error item to the list of previously encountered errors.
+   * @param errorItem error to add to list of errors.
+   */
+  public void addErrorItem(final ErrorItem errorItem) {
+    getErrorList().add(errorItem);
+  }
+
+  /**
+   * Get enumerator over current loggers.
+   * @return enumerator over current loggers
+     @deprecated Please use {@link #getCurrentLoggers} instead.
+   */
+  public Enumeration getCurrentCategories() {
+    return repo.getCurrentCategories();
+  }
+
+  /**
+     Get the renderer map for this hierarchy.
+   @return renderer map
+  */
+  public RendererMap getRendererMap() {
+    return rendererSupport.getRendererMap();
+  }
+
+  /**
+     Get the root of this hierarchy.
+
+     @since 0.9.0
+   @return root of hierarchy
+   */
+  public Logger getRootLogger() {
+    return repo.getRootLogger();
+  }
+
+  /**
+     This method will return <code>true</code> if this repository is
+     disabled for <code>level</code> value passed as parameter and
+     <code>false</code> otherwise. See also the {@link
+     #setThreshold(Level) threshold} method.
+   @param level numeric value for level.
+   @return true if disabled for specified level
+   */
+  public boolean isDisabled(final int level) {
+    return repo.isDisabled(level);
+  }
+
+  /**
+     Reset all values contained in this hierarchy instance to their
+     default.  This removes all appenders from all categories, sets
+     the level of all non-root categories to <code>null</code>,
+     sets their additivity flag to <code>true</code> and sets the level
+     of the root logger to DEBUG.  Moreover,
+     message disabling is set its default "off" value.
+
+     <p>Existing categories are not removed. They are just reset.
+
+     <p>This method should be used sparingly and with care as it will
+     block all logging until it is completed.</p>
+
+     @since 0.8.5 */
+  public void resetConfiguration() {
+    repo.resetConfiguration();
+  }
+
+  /**
+     Used by subclasses to add a renderer to the hierarchy passed as parameter.
+   @param renderedClass class
+   @param renderer object used to render class.
+   */
+  public void setRenderer(final Class renderedClass,
+                          final ObjectRenderer renderer) {
+    rendererSupport.setRenderer(renderedClass, renderer);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public boolean isPristine() {
+    return pristine;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public void setPristine(final boolean state) {
+    pristine = state;
+  }
+
+  /**
+     Shutting down a hierarchy will <em>safely</em> close and remove
+     all appenders in all categories including the root logger.
+
+     <p>Some appenders such as org.apache.log4j.net.SocketAppender
+     and AsyncAppender need to be closed before the
+     application exists. Otherwise, pending logging events might be
+     lost.
+
+     <p>The <code>shutdown</code> method is careful to close nested
+     appenders before closing regular appenders. This is allows
+     configurations where a regular appender is attached to a logger
+     and again to a nested appender.
+
+     @since 1.0 */
+  public void shutdown() {
+    repo.shutdown();
+  }
+
+
+  /**
+   * Return this repository's own scheduler.
+   * The scheduler is lazily instantiated.
+   * @return this repository's own scheduler.
+   */
+  public Scheduler getScheduler() {
+    if (scheduler == null) {
+      scheduler = new Scheduler();
+      scheduler.setDaemon(true);
+      scheduler.start();
+    }
+    return scheduler;
+  }
+
+    /**
+     * Puts object by key.
+     * @param key key, may not be null.
+     * @param value object to associate with key.
+     */
+  public void putObject(final String key,
+                        final Object value) {
+    objectMap.put(key, value);
+  }
+
+    /**
+     * Get object by key.
+     * @param key key, may not be null.
+     * @return object associated with key or null.
+     */
+  public Object getObject(final String key) {
+    return objectMap.get(key);
+  }
+
+    /**
+     * Set logger factory.
+     * @param factory logger factory.
+     */
+  public void setLoggerFactory(final LoggerFactory factory) {
+    if (factory == null) {
+      throw new NullPointerException();
+    }
+    this.loggerFactory = factory;
+  }
+
+    /**
+     * Get logger factory.
+     * @return logger factory.
+     */
+  public LoggerFactory getLoggerFactory() {
+    return loggerFactory;
+  }
+
+    /** {@inheritDoc} */
+    public boolean parseUnrecognizedElement(
+            final Element element,
+            final Properties props) throws Exception {
+        if ("plugin".equals(element.getNodeName())) {
+            Object instance =
+                    DOMConfigurator.parseElement(element, props, Plugin.class);
+            if (instance instanceof Plugin) {
+                Plugin plugin = (Plugin) instance;
+                String pluginName = DOMConfigurator.subst(element.getAttribute("name"), props);
+                if (pluginName.length() > 0) {
+                    plugin.setName(pluginName);
+                }
+                getPluginRegistry().addPlugin(plugin);
+                plugin.setLoggerRepository(this);
+
+                LogLog.debug("Pushing plugin on to the object stack.");
+                plugin.activateOptions();
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+
+    /**
+     * Implementation of RendererSupportImpl if not
+     * provided by LoggerRepository.
+     */
+   private static final class RendererSupportImpl implements RendererSupport {
+        /**
+         * Renderer map.
+         */
+       private final RendererMap renderers = new RendererMap();
+
+        /**
+         * Create new instance.
+         */
+       public RendererSupportImpl() {
+           super();
+       }
+
+        /** {@inheritDoc} */
+       public RendererMap getRendererMap() {
+           return renderers;
+       }
+
+        /** {@inheritDoc} */
+       public void setRenderer(final Class renderedClass,
+                               final ObjectRenderer renderer) {
+           renderers.put(renderedClass, renderer);
+       }
+   }
+
+    /**
+     * Proxy that implements HierarchyEventListener
+     * and delegates to LoggerEventListener.
+     */
+   private static final class HierarchyEventListenerProxy
+        implements HierarchyEventListener {
+        /**
+         * Wrapper listener.
+         */
+       private LoggerEventListener listener;
+
+        /**
+         * Creates new instance.
+         * @param l listener
+         */
+       public HierarchyEventListenerProxy(final LoggerEventListener l) {
+           super();
+           if (l == null) {
+               throw new NullPointerException("l");
+           }
+           listener = l;
+       }
+
+        /** {@inheritDoc} */
+       public void addAppenderEvent(final Category cat,
+                                    final Appender appender) {
+           if (isEnabled() && cat instanceof Logger) {
+                listener.appenderAddedEvent((Logger) cat, appender);
+           }
+       }
+
+        /** {@inheritDoc} */
+       public void removeAppenderEvent(final Category cat,
+                                    final Appender appender) {
+           if (isEnabled() && cat instanceof Logger) {
+                listener.appenderRemovedEvent((Logger) cat, appender);
+           }
+       }
+
+        /**
+         * Disable forwarding of notifications to
+         * simulate removal of listener.
+         */
+       public synchronized void disable() {
+           listener = null;
+       }
+
+        /**
+         * Gets whether proxy is enabled.
+         * @return true if proxy is enabled.
+         */
+       private synchronized boolean isEnabled() {
+           return listener != null;
+       }
+   }
+
+}
diff --git a/src/main/java/org/apache/log4j/ULogger.java b/src/main/java/org/apache/log4j/ULogger.java
new file mode 100644
index 0000000..83a6bb8
--- /dev/null
+++ b/src/main/java/org/apache/log4j/ULogger.java
@@ -0,0 +1,203 @@
+/*
+ * 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;
+
+
+/**
+ * A proxy for org.slf4j.ULogger.  In slf4j implementing builds, this
+ *     interface will extend org.slf4j.ULogger and add no additional methods.
+ *
+ * @author Ceki G&uuml;lc&uuml;
+ * @author Curt Arnold
+ */
+ public interface ULogger {
+
+
+  /**
+   * Is the logger instance enabled for the DEBUG level?
+   * @return true if debug is enabled.
+   */
+  boolean isDebugEnabled();
+
+  /**
+   * Log a message object with the DEBUG level.
+   * @param msg - the message object to be logged
+   */
+  void debug(Object msg);
+
+
+  /**
+   * Log a parameterized message object at the DEBUG level.
+   *
+   * <p>This form is useful in avoiding the superflous object creation
+   * problem when invoking this method while it is disabled.
+   * </p>
+   * @param parameterizedMsg - the parameterized message object
+   * @param param1 - the parameter
+   */
+  void debug(Object parameterizedMsg, Object param1);
+
+  /**
+   * Log a parameterized message object at the DEBUG level.
+   *
+   * <p>This form is useful in avoiding the superflous object creation
+   * problem when invoking this method while it is disabled.
+   * </p>
+   * @param parameterizedMsg - the parameterized message object
+   * @param param1 - the first parameter
+   * @param param2 - the second parameter
+   */
+  void debug(String parameterizedMsg, Object param1, Object param2);
+    /**
+     * Log a message object with the <code>DEBUG</code> level including the
+     * stack trace of the {@link Throwable}<code>t</code> passed as parameter.
+     *
+     *
+     * @param msg the message object to log.
+     * @param t the exception to log, including its stack trace.
+     */
+  void debug(Object msg, Throwable t);
+
+
+    /**
+     * Is the logger instance enabled for the INFO level?
+     * @return true if debug is enabled.
+     */
+  boolean isInfoEnabled();
+    /**
+     * Log a message object with the INFO level.
+     * @param msg - the message object to be logged
+     */
+  void info(Object msg);
+    /**
+     * Log a parameterized message object at the INFO level.
+     *
+     * <p>This form is useful in avoiding the superflous object creation
+     * problem when invoking this method while it is disabled.
+     * </p>
+     * @param parameterizedMsg - the parameterized message object
+     * @param param1 - the parameter
+     */
+  void info(Object parameterizedMsg, Object param1);
+    /**
+     * Log a parameterized message object at the INFO level.
+     *
+     * <p>This form is useful in avoiding the superflous object creation
+     * problem when invoking this method while it is disabled.
+     * </p>
+     * @param parameterizedMsg - the parameterized message object
+     * @param param1 - the first parameter
+     * @param param2 - the second parameter
+     */
+  void info(String parameterizedMsg, Object param1, Object param2);
+    /**
+     * Log a message object with the <code>INFO</code> level including the
+     * stack trace of the {@link Throwable}<code>t</code> passed as parameter.
+     *
+     *
+     * @param msg the message object to log.
+     * @param t the exception to log, including its stack trace.
+     */
+  void info(Object msg, Throwable t);
+
+
+    /**
+     * Is the logger instance enabled for the WARN level?
+     * @return true if debug is enabled.
+     */
+  boolean isWarnEnabled();
+    /**
+     * Log a message object with the WARN level.
+     * @param msg - the message object to be logged
+     */
+  void warn(Object msg);
+    /**
+     * Log a parameterized message object at the WARN level.
+     *
+     * <p>This form is useful in avoiding the superflous object creation
+     * problem when invoking this method while it is disabled.
+     * </p>
+     * @param parameterizedMsg - the parameterized message object
+     * @param param1 - the parameter
+     */
+  void warn(Object parameterizedMsg, Object param1);
+    /**
+     * Log a parameterized message object at the WARN level.
+     *
+     * <p>This form is useful in avoiding the superflous object creation
+     * problem when invoking this method while it is disabled.
+     * </p>
+     * @param parameterizedMsg - the parameterized message object
+     * @param param1 - the first parameter
+     * @param param2 - the second parameter
+     */
+  void warn(String parameterizedMsg, Object param1, Object param2);
+    /**
+     * Log a message object with the <code>WARN</code> level including the
+     * stack trace of the {@link Throwable}<code>t</code> passed as parameter.
+     *
+     *
+     * @param msg the message object to log.
+     * @param t the exception to log, including its stack trace.
+     */
+  void warn(Object msg, Throwable t);
+
+
+    /**
+     * Is the logger instance enabled for the ERROR level?
+     * @return true if debug is enabled.
+     */
+  boolean isErrorEnabled();
+    /**
+     * Log a message object with the ERROR level.
+     * @param msg - the message object to be logged
+     */
+  void error(Object msg);
+    /**
+     * Log a parameterized message object at the ERROR level.
+     *
+     * <p>This form is useful in avoiding the superflous object creation
+     * problem when invoking this method while it is disabled.
+     * </p>
+     * @param parameterizedMsg - the parameterized message object
+     * @param param1 - the parameter
+     */
+  void error(Object parameterizedMsg, Object param1);
+    /**
+     * Log a parameterized message object at the ERROR level.
+     *
+     * <p>This form is useful in avoiding the superflous object creation
+     * problem when invoking this method while it is disabled.
+     * </p>
+     * @param parameterizedMsg - the parameterized message object
+     * @param param1 - the first parameter
+     * @param param2 - the second parameter
+     */
+  void error(String parameterizedMsg, Object param1, Object param2);
+
+    /**
+     * Log a message object with the <code>ERROR</code> level including the
+     * stack trace of the {@link Throwable}<code>t</code> passed as parameter.
+     *
+     *
+     * @param msg the message object to log.
+     * @param t the exception to log, including its stack trace.
+     */
+    void error(Object msg, Throwable t);
+
+}
diff --git a/src/main/java/org/apache/log4j/db/ConnectionSource.java b/src/main/java/org/apache/log4j/db/ConnectionSource.java
new file mode 100644
index 0000000..0578734
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/ConnectionSource.java
@@ -0,0 +1,69 @@
+/*
+ * 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.db;
+
+import org.apache.log4j.spi.Component;
+import org.apache.log4j.spi.OptionHandler;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+
+/**
+ *  The <id>ConnectionSource</id> interface provides a pluggable means of
+ *  transparently obtaining JDBC {@link java.sql.Connection}s for log4j classes
+ *  that require the use of a {@link java.sql.Connection}.
+ *
+ *  @author <a href="mailto:rdecampo@twcny.rr.com">Ray DeCampo</a>
+ */
+public interface ConnectionSource extends Component, OptionHandler {
+
+  final int UNKNOWN_DIALECT = 0;
+  final int POSTGRES_DIALECT = 1;
+  final int MYSQL_DIALECT = 2;
+  final int ORACLE_DIALECT = 3;
+  final int MSSQL_DIALECT = 4;
+  final int HSQL_DIALECT = 5;  
+  /**
+   *  Obtain a {@link java.sql.Connection} for use.  The client is
+   *  responsible for closing the {@link java.sql.Connection} when it is no
+   *  longer required.
+   *
+   *  @throws SQLException  if a {@link java.sql.Connection} could not be
+   *                        obtained
+   */
+  Connection getConnection() throws SQLException;
+
+  /**
+   * Get the SQL dialect that should be used for this connection. Note that the
+   * dialect is not needed if the JDBC driver supports the getGeneratedKeys 
+   * method.
+   */
+  int getSQLDialectCode();
+  
+  /**
+   * If the connection supports the JDBC 3.0 getGeneratedKeys method, then
+   * we do not need any specific dialect support.
+   */
+  boolean supportsGetGeneratedKeys();
+  
+  /**
+   * If the connection does not support batch updates, we will avoid using them.
+   */  
+  public boolean supportsBatchUpdates();
+}
diff --git a/src/main/java/org/apache/log4j/db/ConnectionSourceSkeleton.java b/src/main/java/org/apache/log4j/db/ConnectionSourceSkeleton.java
new file mode 100644
index 0000000..a5abecb
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/ConnectionSourceSkeleton.java
@@ -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.db;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+
+import org.apache.log4j.db.dialect.Util;
+import org.apache.log4j.spi.ComponentBase;
+
+
+/**
+ * @author Ceki G&uuml;lc&uuml;
+ */
+public abstract class ConnectionSourceSkeleton extends ComponentBase implements ConnectionSource {
+  
+  private Boolean overriddenSupportsGetGeneratedKeys = null;
+  
+  private String user = null;
+  private String password = null;
+
+  // initially we have an unkonw dialect
+  private int dialectCode = UNKNOWN_DIALECT;
+  private boolean supportsGetGeneratedKeys = false;
+  private boolean supportsBatchUpdates = false;
+
+
+  /**
+   * Learn relevant information about this connection source.
+   *
+   */
+  public void discoverConnnectionProperties() {
+    Connection connection = null;
+    try {
+      connection = getConnection();
+      if (connection == null) {
+        getLogger().warn("Could not get a conneciton");
+        return;
+      }
+      DatabaseMetaData meta = connection.getMetaData();
+      Util util = new Util();
+      util.setLoggerRepository(repository);
+      if (overriddenSupportsGetGeneratedKeys != null) {
+        supportsGetGeneratedKeys = overriddenSupportsGetGeneratedKeys
+            .booleanValue();
+      } else {
+        supportsGetGeneratedKeys = util.supportsGetGeneratedKeys(meta);
+      }
+      supportsBatchUpdates = util.supportsBatchUpdates(meta);
+      dialectCode = Util.discoverSQLDialect(meta);
+    } catch (SQLException se) {
+      getLogger().warn("Could not discover the dialect to use.", se);
+    } finally {
+      DBHelper.closeConnection(connection);
+    }
+  }
+
+  /**
+   * Does this connection support the JDBC Connection.getGeneratedKeys method?
+   */
+  public final boolean supportsGetGeneratedKeys() {
+    return supportsGetGeneratedKeys;
+  }
+
+  public final int getSQLDialectCode() {
+    return dialectCode;
+  }
+
+  /**
+   * Get the password for this connection source.
+   */
+  public final String getPassword() {
+    return password;
+  }
+
+  /**
+   * Sets the password.
+   * @param password The password to set
+   */
+  public final void setPassword(final String password) {
+    this.password = password;
+  }
+
+  /**
+   * Get the user for this connection source.
+   */
+  public final String getUser() {
+    return user;
+  }
+
+  /**
+   * Sets the username.
+   * @param username The username to set
+   */
+  public final void setUser(final String username) {
+    this.user = username;
+  }
+
+  /**
+   * Returns the "overridden" value of "supportsGetGeneratedKeys" property of
+   * the JDBC driver. In certain cases, getting (e.g. Oracle 10g) generated keys
+   * does not work because it returns the ROWID, not the value of the sequence.
+   * 
+   * @return A non null string, with "true" or "false" value, if overridden,
+   *         <code>null</code> if not overridden.
+   */
+  public String getOverriddenSupportsGetGeneratedKeys() {
+    return overriddenSupportsGetGeneratedKeys != null ? overriddenSupportsGetGeneratedKeys
+        .toString()
+        : null;
+  }
+
+  /**
+   * Sets the "overridden" value of "supportsGetGeneratedKeys" property of the
+   * JDBC driver. In certain cases, getting (e.g. Oracle 10g) generated keys
+   * does not work because it returns the ROWID, not the value of the sequence.
+   * 
+   * @param overriddenSupportsGetGeneratedKeys
+   *          A non null string, with "true" or "false" value, if overridden,
+   *          <code>null</code> if not overridden.
+   */
+  public void setOverriddenSupportsGetGeneratedKeys(
+      String overriddenSupportsGetGeneratedKeys) {
+    this.overriddenSupportsGetGeneratedKeys = Boolean
+        .valueOf(overriddenSupportsGetGeneratedKeys);
+  }
+  
+  /**
+   * Does this connection support batch updates?
+   */
+  public final boolean supportsBatchUpdates() {
+    return supportsBatchUpdates;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/CustomSQLDBReceiver.java b/src/main/java/org/apache/log4j/db/CustomSQLDBReceiver.java
new file mode 100644
index 0000000..6c2d78b
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/CustomSQLDBReceiver.java
@@ -0,0 +1,468 @@
+/*
+ * 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.db;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Hashtable;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.scheduler.Job;
+import org.apache.log4j.scheduler.Scheduler;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggerRepositoryEx;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
+import org.apache.log4j.xml.DOMConfigurator;
+import org.apache.log4j.xml.UnrecognizedElementHandler;
+import org.w3c.dom.Element;
+
+/**
+ * Converts log data stored in a database into LoggingEvents.
+ * <p>
+ * <b>NOTE:</b> This receiver cannot yet be created through Chainsaw's receiver panel.  
+ * It must be created through an XML configuration file.
+ * <p>
+ * This receiver supports database configuration via ConnectionSource, in the
+ * org.apache.log4j.db package: DriverManagerConnectionSource,
+ * DataSourceConnectionSource, JNDIConnectionSource
+ * <p>
+ * This database receiver differs from DBReceiver in that this receiver relies
+ * on custom SQL to retrieve logging event data, where DBReceiver requires the
+ * use of a log4j-defined schema.
+ * <p>
+ * A 'refreshMillis' int parameter controls SQL execution. If 'refreshMillis' is
+ * zero (the default), the receiver will run only one time. If it is set to any
+ * other numeric value, the SQL will be executed on a recurring basis every
+ * 'refreshMillis' milliseconds.
+ * <p>
+ * The receiver closes the connection and acquires a new connection on each 
+ * execution of the SQL (use pooled connections if possible).
+ * <p>
+ * If the SQL will be executing on a recurring basis, specify the IDField param -
+ * the column name holding the unique identifier (int) representing the logging
+ * event.
+ * <p>
+ * As events are retrieved, the column represented by IDField is examined and
+ * the largest value is held and used by the next execution of the SQL statement
+ * to avoid retrieving previously processed events.
+ * <p>
+ * As an example, the IDField references a 'COUNTER' (int, auto-increment,
+ * unique) column. The first execution of the SQL statement returns 500 rows,
+ * with a final value in the COUNTER field of 500.
+ * <p>
+ * The SQL statement is manipulated prior to the next execution, adding ' WHERE
+ * COUNTER > 500' to the statement to avoid retrieval of previously processed
+ * events.
+ * <p>
+ * The select statement must provide ALL fields which define a LoggingEvent.
+ * <p>
+ * The SQL statement MUST include the columns: LOGGER, TIMESTAMP, LEVEL, THREAD,
+ * MESSAGE, NDC, MDC, CLASS, METHOD, FILE, LINE, PROPERTIES, THROWABLE
+ * <p>
+ * Use ' AS ' in the SQL statement to alias the SQL's column names to match your
+ * database schema. (see example below).
+ * <p>
+ * Include all fields in the SQL statement, even if you don't have data for the
+ * field (specify an empty string as the value for columns which you don't have
+ * data).
+ * <p>
+ * The TIMESTAMP column must be a datetime.
+ * <p>
+ * Both a PROPERTIES column and an MDC column are supported. These fields
+ * represent Maps on the logging event, but require the use of string
+ * concatenation database functions to hold the (possibly multiple) name/value
+ * pairs in the column.
+ * <p>
+ * For example, to include both 'userid' and 'lastname' properties in the
+ * logging event (from either the PROPERTIES or MDC columns), the name/value
+ * pairs must be concatenated together by your database.
+ * <p>
+ * The resulting PROPERTIES or MDC column must have data in this format: {{name,
+ * value, name2, value2}}
+ * <p>
+ * The resulting PROPERTIES column would contain this text: {{userid, someone,
+ * lastname, mylastname}}
+ * <p>
+ * Here is an example of concatenating a PROPERTIES or MDC column using MySQL's
+ * concat function, where the 'application' and 'hostname' parameters were fixed
+ * text, but the 'log4jid' key's value is the value of the COUNTER column:
+ * <p>
+ * concat("{{application,databaselogs,hostname,mymachine,log4jid,", COUNTER,
+ * "}}") as PROPERTIES
+ * <p>
+ * log4jid is a special property that is used by Chainsaw to represent an 'ID'
+ * field. Specify this property to ensure you can map events in Chainsaw to
+ * events in the database if you need to go back and view events at a later time
+ * or save the events to XML for later analysis.
+ * <p>
+ * Here is a complete MySQL SQL statement which can be used to provide events to
+ * Chainsaw (note how in the example below, there is no column in logtable representing the throwable, so an
+ * empty string is passed in and an ALIAS is still defined):
+ * <p>
+ * select myloggercolumn as LOGGER, mytimestampcolumn as TIMESTAMP, mylevelcolumn as LEVEL, mythreadcolumn as
+ * THREAD, mymessagecolumn as MESSAGE, myndccolumn as NDC, mymdccolumn as MDC, myclasscolumn as CLASS, mymethodcolumn as
+ * METHOD, myfilecolumn as FILE, mylinecolumn as LINE,
+ * concat("{{application,databaselogs,hostname,mymachine, log4jid,",
+ * COUNTER,"}}") as PROPERTIES, "" as THROWABLE from logtable
+ * <p>
+ * @author Scott Deboy <sdeboy@apache.org>
+ * <p>
+ */
+public class CustomSQLDBReceiver extends Receiver implements Pauseable, UnrecognizedElementHandler {
+
+    protected volatile Connection connection = null;
+
+    protected String sqlStatement = "";
+
+    /**
+     * By default we refresh data every 1000 milliseconds.
+     * 
+     * @see #setRefreshMillis
+     */
+    static int DEFAULT_REFRESH_MILLIS = 1000;
+
+    int refreshMillis = DEFAULT_REFRESH_MILLIS;
+
+    protected String idField = null;
+
+    int lastID = -1;
+
+    private static final String WHERE_CLAUSE = " WHERE ";
+
+    private static final String AND_CLAUSE = " AND ";
+
+    private boolean whereExists = false;
+
+    private boolean paused = false;
+
+    private ConnectionSource connectionSource;
+
+    public static final String LOG4J_ID_KEY = "log4jid";
+
+    private Job customReceiverJob;
+
+    public void activateOptions() {
+      
+      if(connectionSource == null)  {
+        throw new IllegalStateException(
+          "CustomSQLDBReceiver cannot function without a connection source");
+      }
+      whereExists = (sqlStatement.toUpperCase().indexOf(WHERE_CLAUSE) > -1);
+    
+      customReceiverJob = new CustomReceiverJob();
+        
+      if(this.repository == null) {
+        throw new IllegalStateException(
+        "CustomSQLDBReceiver cannot function without a reference to its owning repository");
+      }
+     
+    
+
+      if (repository instanceof LoggerRepositoryEx) {
+        Scheduler scheduler = ((LoggerRepositoryEx) repository).getScheduler();
+      
+        scheduler.schedule(
+          customReceiverJob, System.currentTimeMillis() + 500, refreshMillis);
+      }
+
+    }
+
+    void closeConnection() {
+        if (connection != null) {
+            try {
+                // LogLog.warn("closing the connection. ", new Exception("x"));
+                connection.close();
+            } catch (SQLException sqle) {
+                // nothing we can do here
+            }
+        }
+    }
+
+    public void setRefreshMillis(int refreshMillis) {
+        this.refreshMillis = refreshMillis;
+    }
+
+    public int getRefreshMillis() {
+        return refreshMillis;
+    }
+
+    /**
+     * @return Returns the connectionSource.
+     */
+    public ConnectionSource getConnectionSource() {
+        return connectionSource;
+    }
+
+    /**
+     * @param connectionSource
+     *            The connectionSource to set.
+     */
+    public void setConnectionSource(ConnectionSource connectionSource) {
+        this.connectionSource = connectionSource;
+    }
+
+    public void close() {
+        try {
+            if ((connection != null) && !connection.isClosed()) {
+                connection.close();
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        } finally {
+            connection = null;
+        }
+    }
+
+    public void finalize() throws Throwable {
+        super.finalize();
+        close();
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.apache.log4j.plugins.Plugin#shutdown()
+     */
+    public void shutdown() {
+        getLogger().info("removing receiverJob from the Scheduler.");
+
+        if(this.repository instanceof LoggerRepositoryEx) {
+          Scheduler scheduler = ((LoggerRepositoryEx) repository).getScheduler();
+          scheduler.delete(customReceiverJob);
+        }
+
+        lastID = -1;
+    }
+
+    public void setSql(String s) {
+        sqlStatement = s;
+    }
+
+    public String getSql() {
+        return sqlStatement;
+    }
+
+    public void setIDField(String id) {
+        idField = id;
+    }
+
+    public String getIDField() {
+        return idField;
+    }
+
+    public synchronized void setPaused(boolean p) {
+        paused = p;
+    }
+
+    public synchronized boolean isPaused() {
+        return paused;
+    }
+
+    class CustomReceiverJob implements Job {
+        public void execute() {
+            int oldLastID = lastID;
+            try {
+                connection = connectionSource.getConnection();
+                Statement statement = connection.createStatement();
+
+                Logger eventLogger = null;
+                long timeStamp = 0L;
+                String level = null;
+                String threadName = null;
+                Object message = null;
+                String ndc = null;
+                Hashtable mdc = null;
+                String[] throwable = null;
+                String className = null;
+                String methodName = null;
+                String fileName = null;
+                String lineNumber = null;
+                Hashtable properties = null;
+
+                String currentSQLStatement = sqlStatement;
+                if (whereExists) {
+                    currentSQLStatement = sqlStatement + AND_CLAUSE + idField
+                            + " > " + lastID;
+                } else {
+                    currentSQLStatement = sqlStatement + WHERE_CLAUSE + idField
+                            + " > " + lastID;
+                }
+
+                ResultSet rs = statement.executeQuery(currentSQLStatement);
+
+                int i = 0;
+                while (rs.next()) {
+                    // add a small break every 1000 received events
+                    if (++i == 1000) {
+                        synchronized (this) {
+                            try {
+                                // add a delay
+                                wait(300);
+                            } catch (InterruptedException ie) {
+                            }
+                            i = 0;
+                        }
+                    }
+                    eventLogger = Logger.getLogger(rs.getString("LOGGER"));
+                    timeStamp = rs.getTimestamp("TIMESTAMP").getTime();
+
+                    level = rs.getString("LEVEL");
+                    threadName = rs.getString("THREAD");
+                    message = rs.getString("MESSAGE");
+                    ndc = rs.getString("NDC");
+
+                    String mdcString = rs.getString("MDC");
+                    mdc = new Hashtable();
+
+                    if (mdcString != null) {
+                        // support MDC being wrapped in {{name, value}}
+                        // or
+                        // just name, value
+                        if ((mdcString.indexOf("{{") > -1)
+                                && (mdcString.indexOf("}}") > -1)) {
+                            mdcString = mdcString
+                                    .substring(mdcString.indexOf("{{") + 2,
+                                            mdcString.indexOf("}}"));
+                        }
+
+                        StringTokenizer tok = new StringTokenizer(mdcString,
+                                ",");
+
+                        while (tok.countTokens() > 1) {
+                            mdc.put(tok.nextToken(), tok.nextToken());
+                        }
+                    }
+
+                    throwable = new String[] { rs.getString("THROWABLE") };
+                    className = rs.getString("CLASS");
+                    methodName = rs.getString("METHOD");
+                    fileName = rs.getString("FILE");
+                    lineNumber = rs.getString("LINE");
+
+                    // if properties are provided in the
+                    // SQL they can be used here (for example, to route
+                    // events to a unique tab in
+                    // Chainsaw if the machinename and/or appname
+                    // property
+                    // are set)
+                    String propertiesString = rs.getString("PROPERTIES");
+                    properties = new Hashtable();
+
+                    if (propertiesString != null) {
+                        // support properties being wrapped in {{name,
+                        // value}} or just name, value
+                        if ((propertiesString.indexOf("{{") > -1)
+                                && (propertiesString.indexOf("}}") > -1)) {
+                            propertiesString = propertiesString.substring(
+                                    propertiesString.indexOf("{{") + 2,
+                                    propertiesString.indexOf("}}"));
+                        }
+
+                        StringTokenizer tok2 = new StringTokenizer(
+                                propertiesString, ",");
+                        while (tok2.countTokens() > 1) {
+                            String tokenName = tok2.nextToken();
+                            String value = tok2.nextToken();
+                            if (tokenName.equals(LOG4J_ID_KEY)) {
+                                try {
+                                    int thisInt = Integer.parseInt(value);
+                                    value = String.valueOf(thisInt);
+                                    if (thisInt > lastID) {
+                                        lastID = thisInt;
+                                    }
+                                } catch (Exception e) {
+                                }
+                            }
+                            properties.put(tokenName, value);
+                        }
+                    }
+
+                    Level levelImpl = Level.toLevel(level);
+
+
+					LocationInfo locationInfo = new LocationInfo(fileName,
+	                            className, methodName, lineNumber);
+	
+					ThrowableInformation throwableInfo =  new ThrowableInformation(
+		                            throwable);
+	
+					properties.putAll(mdc);
+		
+				    LoggingEvent event = new LoggingEvent(eventLogger.getName(),
+				            eventLogger, timeStamp, levelImpl, message,
+				            threadName,
+				            throwableInfo,
+				            ndc,
+				            locationInfo,
+				            properties);
+
+                    doPost(event);
+                }
+                //log when rows are retrieved
+                if (lastID != oldLastID) {
+                    getLogger().debug("lastID: " + lastID);
+                    oldLastID = lastID;
+                }
+
+                statement.close();
+                statement = null;
+            } catch (SQLException sqle) {
+                getLogger()
+                        .error("*************Problem receiving events", sqle);
+            } finally {
+                closeConnection();
+            }
+
+            // if paused, loop prior to executing sql query
+            synchronized (this) {
+                while (isPaused()) {
+                    try {
+                        wait(1000);
+                    } catch (InterruptedException ie) {
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * @{inheritDoc}
+     */
+  public boolean parseUnrecognizedElement(Element element, Properties props) throws Exception {
+        if ("connectionSource".equals(element.getNodeName())) {
+            Object instance =
+                    DOMConfigurator.parseElement(element, props, ConnectionSource.class);
+            if (instance instanceof ConnectionSource) {
+               ConnectionSource source = (ConnectionSource) instance;
+               source.activateOptions();
+               setConnectionSource(source);
+            }
+            return true;
+        }
+        return false;
+  }
+    
+}
diff --git a/src/main/java/org/apache/log4j/db/DBAppender.java b/src/main/java/org/apache/log4j/db/DBAppender.java
new file mode 100644
index 0000000..15deac3
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DBAppender.java
@@ -0,0 +1,403 @@
+/*
+ * 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.db;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.db.dialect.SQLDialect;
+import org.apache.log4j.db.dialect.Util;
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.xml.DOMConfigurator;
+import org.apache.log4j.xml.UnrecognizedElementHandler;
+import org.w3c.dom.Element;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.Set;
+
+
+/**
+ * The DBAppender inserts loggin events into three database tables in a format
+ * independent of the Java programming language. The three tables that
+ * DBAppender inserts to must exists before DBAppender can be used. These tables
+ * may be created with the help of SQL scripts found in the
+ * <em>src/java/org/apache/log4j/db/dialect</em> directory. There is a
+ * specific script for each of the most popular database systems. If the script
+ * for your particular type of database system is missing, it should be quite
+ * easy to write one, taking example on the already existing scripts. If you
+ * send them to us, we will gladly include missing scripts in future releases.
+ *
+ * <p>
+ * If the JDBC driver you are using supports the
+ * {@link java.sql.Statement#getGeneratedKeys}method introduced in JDBC 3.0
+ * specification, then you are all set. Otherwise, there must be an
+ * {@link SQLDialect}appropriate for your database system. Currently, we have
+ * dialects for PostgreSQL, MySQL, Oracle and MsSQL. As mentioed previously, an
+ * SQLDialect is required only if the JDBC driver for your database system does
+ * not support the {@link java.sql.Statement#getGeneratedKeys getGeneratedKeys}
+ * method.
+ * </p>
+ *
+ * <table border="1" cellpadding="4">
+ * <tr>
+ * <th>RDBMS</th>
+ * <th>supports <br/><code>getGeneratedKeys()</code> method</th>
+ * <th>specific <br/>SQLDialect support</th>
+ * <tr>
+ * <tr>
+ * <td>PostgreSQL</td>
+ * <td align="center">NO</td>
+ * <td>present and used</td>
+ * <tr>
+ * <tr>
+ * <td>MySQL</td>
+ * <td align="center">YES</td>
+ * <td>present, but not actually needed or used</td>
+ * <tr>
+ * <tr>
+ * <td>Oracle</td>
+ * <td align="center">YES</td>
+ * <td>present, but not actually needed or used</td>
+ * <tr>
+ * <tr>
+ * <td>DB2</td>
+ * <td align="center">YES</td>
+ * <td>not present, and not needed or used</td>
+ * <tr>
+ * <tr>
+ * <td>MsSQL</td>
+ * <td align="center">YES</td>
+ * <td>not present, and not needed or used</td>
+ * <tr>
+ * <tr>
+ *   <td>HSQL</td>
+ *    <td align="center">NO</td>
+ *    <td>present and used</td>
+ * <tr>
+ *
+ * </table>
+ * <p>
+ * <b>Performance: </b> Experiments show that writing a single event into the
+ * database takes approximately 50 milliseconds, on a "standard" PC. If pooled
+ * connections are used, this figure drops to under 10 milliseconds. Note that
+ * most JDBC drivers already ship with connection pooling support.
+ * </p>
+ *
+ *
+ *
+ * <p>
+ * <b>Configuration </b> DBAppender can be configured programmatically, or using
+ * {@link org.apache.log4j.xml.DOMConfigurator JoranConfigurator}. Example
+ * scripts can be found in the <em>tests/input/db</em> directory.
+ *
+ * @author Ceki G&uuml;lc&uuml;
+ * @author Ray DeCampo
+ */
+public class DBAppender extends AppenderSkeleton implements UnrecognizedElementHandler {
+  static final String insertPropertiesSQL =
+    "INSERT INTO  logging_event_property (event_id, mapped_key, mapped_value) VALUES (?, ?, ?)";
+  static final String insertExceptionSQL =
+    "INSERT INTO  logging_event_exception (event_id, i, trace_line) VALUES (?, ?, ?)";
+  static final String insertSQL;
+  private static final Method GET_GENERATED_KEYS_METHOD;
+
+
+  static {
+    StringBuffer sql = new StringBuffer();
+    sql.append("INSERT INTO logging_event (");
+    sql.append("sequence_number, ");
+    sql.append("timestamp, ");
+    sql.append("rendered_message, ");
+    sql.append("logger_name, ");
+    sql.append("level_string, ");
+    sql.append("ndc, ");
+    sql.append("thread_name, ");
+    sql.append("reference_flag, ");
+    sql.append("caller_filename, ");
+    sql.append("caller_class, ");
+    sql.append("caller_method, ");
+    sql.append("caller_line) ");
+    sql.append(" VALUES (?, ?, ? ,?, ?, ?, ?, ?, ?, ?, ?, ?)");
+    insertSQL = sql.toString();
+    //
+    //   PreparedStatement.getGeneratedKeys added in JDK 1.4
+    //
+    Method getGeneratedKeysMethod;
+    try {
+        getGeneratedKeysMethod = PreparedStatement.class.getMethod("getGeneratedKeys", null);
+    } catch(Exception ex) {
+        getGeneratedKeysMethod = null;
+    }
+    GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod;
+  }
+
+  ConnectionSource connectionSource;
+  boolean cnxSupportsGetGeneratedKeys = false;
+  boolean cnxSupportsBatchUpdates = false;
+  SQLDialect sqlDialect;
+  boolean locationInfo = false;
+  
+
+  public DBAppender() {
+      super(false);
+  }
+
+  public void activateOptions() {
+    LogLog.debug("DBAppender.activateOptions called");
+
+    if (connectionSource == null) {
+      throw new IllegalStateException(
+        "DBAppender cannot function without a connection source");
+    }
+
+    sqlDialect = Util.getDialectFromCode(connectionSource.getSQLDialectCode());
+    if (GET_GENERATED_KEYS_METHOD != null) {
+        cnxSupportsGetGeneratedKeys = connectionSource.supportsGetGeneratedKeys();
+    } else {
+        cnxSupportsGetGeneratedKeys = false;
+    }
+    cnxSupportsBatchUpdates = connectionSource.supportsBatchUpdates();
+    if (!cnxSupportsGetGeneratedKeys && (sqlDialect == null)) {
+      throw new IllegalStateException(
+        "DBAppender cannot function if the JDBC driver does not support getGeneratedKeys method *and* without a specific SQL dialect");
+    }
+    
+    // all nice and dandy on the eastern front
+    super.activateOptions();
+  }
+
+  /**
+   * @return Returns the connectionSource.
+   */
+  public ConnectionSource getConnectionSource() {
+    return connectionSource;
+  }
+
+  /**
+   * @param connectionSource
+   *          The connectionSource to set.
+   */
+  public void setConnectionSource(ConnectionSource connectionSource) {
+    LogLog.debug("setConnectionSource called for DBAppender");
+    this.connectionSource = connectionSource;
+  }
+
+  protected void append(LoggingEvent event) {
+      Connection connection = null;
+      try {
+          connection = connectionSource.getConnection();
+          connection.setAutoCommit(false);
+          
+          PreparedStatement insertStatement;
+          if (cnxSupportsGetGeneratedKeys) {
+        	  insertStatement = connection.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS);
+          } else {
+              insertStatement = connection.prepareStatement(insertSQL);
+          }
+
+/*          insertStatement.setLong(1, event.getSequenceNumber());*/
+		  insertStatement.setLong(1, 0);
+		
+          insertStatement.setLong(2, event.getTimeStamp());
+          insertStatement.setString(3, event.getRenderedMessage());
+          insertStatement.setString(4, event.getLoggerName());
+          insertStatement.setString(5, event.getLevel().toString());
+          insertStatement.setString(6, event.getNDC());
+          insertStatement.setString(7, event.getThreadName());
+          insertStatement.setShort(8, DBHelper.computeReferenceMask(event));
+          
+          LocationInfo li;
+          
+          if (event.locationInformationExists() || locationInfo) {
+              li = event.getLocationInformation();
+          } else {
+              li = LocationInfo.NA_LOCATION_INFO;
+          }
+          
+          insertStatement.setString(9, li.getFileName());
+          insertStatement.setString(10, li.getClassName());
+          insertStatement.setString(11, li.getMethodName());
+          insertStatement.setString(12, li.getLineNumber());
+          
+          int updateCount = insertStatement.executeUpdate();
+          if (updateCount != 1) {
+              LogLog.warn("Failed to insert loggingEvent");
+          }
+          
+          ResultSet rs = null;
+          Statement idStatement = null;
+          boolean gotGeneratedKeys = false;
+          if (cnxSupportsGetGeneratedKeys) {
+              try {
+                  rs = (ResultSet) GET_GENERATED_KEYS_METHOD.invoke(insertStatement, null);
+                  gotGeneratedKeys = true;
+              } catch(InvocationTargetException ex) {
+                  Throwable target = ex.getTargetException();
+                  if (target instanceof SQLException) {
+                      throw (SQLException) target;
+                  }
+                  throw ex; 
+              } catch(IllegalAccessException ex) {
+                  LogLog.warn("IllegalAccessException invoking PreparedStatement.getGeneratedKeys", ex);
+              }
+          }
+          
+          if (!gotGeneratedKeys) {
+              insertStatement.close();
+              insertStatement = null;
+              
+              idStatement = connection.createStatement();
+              idStatement.setMaxRows(1);
+              rs = idStatement.executeQuery(sqlDialect.getSelectInsertId());
+          }
+          
+          // A ResultSet cursor is initially positioned before the first row; the 
+          // first call to the method next makes the first row the current row
+          rs.next();
+          int eventId = rs.getInt(1);
+          
+          rs.close();
+
+          // we no longer need the insertStatement
+          if(insertStatement != null) {
+              insertStatement.close();
+              insertStatement = null;
+          }
+
+          if(idStatement != null) {
+              idStatement.close();
+              idStatement = null;
+          }
+
+          Set propertiesKeys = event.getPropertyKeySet();
+          
+          if (propertiesKeys.size() > 0) {
+              PreparedStatement insertPropertiesStatement =
+                  connection.prepareStatement(insertPropertiesSQL);
+              
+              for (Iterator i = propertiesKeys.iterator(); i.hasNext();) {
+                  String key = (String) i.next();
+                  String value = (String) event.getProperty(key);
+                  
+                  //LogLog.info("id " + eventId + ", key " + key + ", value " + value);
+                  insertPropertiesStatement.setInt(1, eventId);
+                  insertPropertiesStatement.setString(2, key);
+                  insertPropertiesStatement.setString(3, value);
+                  
+                  if (cnxSupportsBatchUpdates) {
+                      insertPropertiesStatement.addBatch();
+                  } else {
+                      insertPropertiesStatement.execute();
+                  }
+              }
+              
+              if (cnxSupportsBatchUpdates) {
+                  insertPropertiesStatement.executeBatch();
+              }
+              
+              insertPropertiesStatement.close();
+              insertPropertiesStatement = null;
+          }
+          
+          String[] strRep = event.getThrowableStrRep();
+          
+          if (strRep != null) {
+              LogLog.debug("Logging an exception");
+              
+              PreparedStatement insertExceptionStatement =
+                  connection.prepareStatement(insertExceptionSQL);
+              
+              for (short i = 0; i < strRep.length; i++) {
+                  insertExceptionStatement.setInt(1, eventId);
+                  insertExceptionStatement.setShort(2, i);
+                  insertExceptionStatement.setString(3, strRep[i]);
+                  if (cnxSupportsBatchUpdates) {
+                      insertExceptionStatement.addBatch();
+                  } else {
+                      insertExceptionStatement.execute();
+                  }
+              }
+              if (cnxSupportsBatchUpdates) {
+                  insertExceptionStatement.executeBatch();
+              }
+              insertExceptionStatement.close();
+              insertExceptionStatement = null;
+          }
+          
+          connection.commit();
+      } catch (Throwable sqle) {
+          LogLog.error("problem appending event", sqle);
+      } finally {
+          DBHelper.closeConnection(connection);
+      }
+  }
+
+  public void close() {
+    closed = true;
+  }
+
+  /**
+   * Returns value of the <b>LocationInfo </b> property which determines whether
+   * caller's location info is written to the database.
+   */
+  public boolean getLocationInfo() {
+    return locationInfo;
+  }
+
+  /**
+   * If true, the information written to the database will include caller's
+   * location information. Due to performance concerns, by default no location
+   * information is written to the database.
+   */
+  public void setLocationInfo(boolean locationInfo) {
+    this.locationInfo = locationInfo;
+  }
+
+    /**
+     * Gets whether appender requires a layout.
+     * @return false
+     */
+  public boolean requiresLayout() {
+      return false;
+  }
+
+    /**
+     * @{inheritDoc}
+     */
+  public boolean parseUnrecognizedElement(Element element, Properties props) throws Exception {
+        if ("connectionSource".equals(element.getNodeName())) {
+            Object instance =
+                    DOMConfigurator.parseElement(element, props, ConnectionSource.class);
+            if (instance instanceof ConnectionSource) {
+               ConnectionSource source = (ConnectionSource) instance;
+               source.activateOptions();
+               setConnectionSource(source);
+            }
+            return true;
+        }
+        return false;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/DBHelper.java b/src/main/java/org/apache/log4j/db/DBHelper.java
new file mode 100644
index 0000000..b400506
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DBHelper.java
@@ -0,0 +1,68 @@
+/*
+ * 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.db;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Set;
+
+import org.apache.log4j.spi.LoggingEvent;
+
+/**
+ * @author Ceki G&uuml;lc&uuml;
+ *
+ */
+public class DBHelper {
+  
+  public final static short PROPERTIES_EXIST = 0x01;
+  public final static short EXCEPTION_EXISTS = 0x02;
+  
+  public  static short computeReferenceMask(LoggingEvent event) {
+    short mask = 0;
+    Set propertiesKeys = event.getPropertyKeySet();
+    if(propertiesKeys.size() > 0) {
+      mask = PROPERTIES_EXIST;
+    }
+    String[] strRep = event.getThrowableStrRep();
+    if(strRep != null) {
+      mask |= EXCEPTION_EXISTS;
+    }
+    return mask;
+  }
+  
+  static public void closeConnection(Connection connection) {
+    if(connection != null) {
+      try { 
+        connection.close();
+      } catch(SQLException sqle) {
+        // static utility classes should not log without an explicit repository
+        // reference
+      }
+    }
+  }
+  
+  public static void closeStatement(Statement statement) {
+    if(statement != null) {
+      try {
+        statement.close();
+      } catch(SQLException sqle) {
+      }
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/DBReceiver.java b/src/main/java/org/apache/log4j/db/DBReceiver.java
new file mode 100644
index 0000000..eee1068
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DBReceiver.java
@@ -0,0 +1,140 @@
+/*
+ * 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.db;
+
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.scheduler.Scheduler;
+import org.apache.log4j.spi.LoggerRepositoryEx;
+import org.apache.log4j.xml.DOMConfigurator;
+import org.apache.log4j.xml.UnrecognizedElementHandler;
+import org.w3c.dom.Element;
+
+import java.util.Properties;
+
+/**
+ *
+ * @author Scott Deboy <sdeboy@apache.org>
+ * @author Ceki G&uuml;lc&uuml;
+ *
+ */
+public class DBReceiver extends Receiver implements Pauseable, UnrecognizedElementHandler {
+  /**
+   * By default we refresh data every 1000 milliseconds.
+   * @see #setRefreshMillis
+   */
+  static int DEFAULT_REFRESH_MILLIS = 1000;
+  ConnectionSource connectionSource;
+  int refreshMillis = DEFAULT_REFRESH_MILLIS;
+  DBReceiverJob receiverJob;
+  boolean paused = false;
+
+  public void activateOptions() {
+    
+    if(connectionSource == null)  {
+      throw new IllegalStateException(
+        "DBAppender cannot function without a connection source");
+    }
+  
+    receiverJob = new DBReceiverJob(this);
+    receiverJob.setLoggerRepository(repository);
+      
+    if(this.repository == null) {
+      throw new IllegalStateException(
+      "DBAppender cannot function without a reference to its owning repository");
+    }
+
+    if (repository instanceof LoggerRepositoryEx) {
+        Scheduler scheduler = ((LoggerRepositoryEx) repository).getScheduler();
+    
+        scheduler.schedule(
+            receiverJob, System.currentTimeMillis() + 500, refreshMillis);
+    }
+   
+  }
+
+  public void setRefreshMillis(int refreshMillis) {
+    this.refreshMillis = refreshMillis;
+  }
+
+  public int getRefreshMillis() {
+    return refreshMillis;
+  }
+
+
+  /**
+   * @return Returns the connectionSource.
+   */
+  public ConnectionSource getConnectionSource() {
+    return connectionSource;
+  }
+
+
+  /**
+   * @param connectionSource The connectionSource to set.
+   */
+  public void setConnectionSource(ConnectionSource connectionSource) {
+    this.connectionSource = connectionSource;
+  }
+
+
+  /* (non-Javadoc)
+   * @see org.apache.log4j.plugins.Plugin#shutdown()
+   */
+  public void shutdown() {
+    getLogger().info("removing receiverJob from the Scheduler.");
+
+    if(this.repository instanceof LoggerRepositoryEx) {
+      Scheduler scheduler = ((LoggerRepositoryEx) repository).getScheduler();
+      scheduler.delete(receiverJob);
+    }
+  }
+
+
+  /* (non-Javadoc)
+   * @see org.apache.log4j.plugins.Pauseable#setPaused(boolean)
+   */
+  public void setPaused(boolean paused) {
+    this.paused = paused;
+  }
+
+  /* (non-Javadoc)
+   * @see org.apache.log4j.plugins.Pauseable#isPaused()
+   */
+  public boolean isPaused() {
+    return paused;
+  }
+
+    /**
+     * @{inheritDoc}
+     */
+  public boolean parseUnrecognizedElement(Element element, Properties props) throws Exception {
+        if ("connectionSource".equals(element.getNodeName())) {
+            Object instance =
+                    DOMConfigurator.parseElement(element, props, ConnectionSource.class);
+            if (instance instanceof ConnectionSource) {
+                ConnectionSource source = (ConnectionSource) instance;
+                source.activateOptions();
+                setConnectionSource(source);
+            }
+            return true;
+        }
+        return false;
+  }
+
+}
diff --git a/src/main/java/org/apache/log4j/db/DBReceiverJob.java b/src/main/java/org/apache/log4j/db/DBReceiverJob.java
new file mode 100644
index 0000000..1e29663
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DBReceiverJob.java
@@ -0,0 +1,230 @@
+/*
+ * 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.db;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.scheduler.Job;
+import org.apache.log4j.spi.ComponentBase;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Hashtable;
+import java.util.Vector;
+
+/**
+ * Actual retrieval of data is made by the instance of DBReceiverJob associated
+ * with DBReceiver.
+ * 
+ * @author Ceki G&uuml;lc&uuml;
+ */
+class DBReceiverJob extends ComponentBase implements Job {
+
+  String sqlException = "SELECT trace_line FROM logging_event_exception where event_id=? ORDER by i ASC";
+  String sqlProperties = "SELECT mapped_key, mapped_value FROM logging_event_property WHERE event_id=?";
+  String sqlSelect = 
+    "SELECT " +
+    "sequence_number, timestamp, rendered_message, logger_name, " +
+    "level_string, ndc, thread_name, reference_flag, " +
+    "caller_filename, caller_class, caller_method, caller_line, " +
+    "event_id " +
+    "FROM logging_event " +
+    "WHERE event_id > ?  ORDER BY event_id ASC";
+
+
+  long lastId = Short.MIN_VALUE;
+
+  DBReceiver parentDBReceiver;
+
+  DBReceiverJob(DBReceiver parent) {
+    parentDBReceiver = parent;
+  }
+
+  public void execute() {
+    getLogger().debug("DBReceiverJob.execute() called");
+
+    Connection connection = null;
+
+    try {
+      connection = parentDBReceiver.connectionSource.getConnection();
+      PreparedStatement statement = connection.prepareStatement(sqlSelect);
+      statement.setLong(1, lastId);
+      ResultSet rs = statement.executeQuery();
+      //rs.beforeFirst();
+
+      while (rs.next()) {
+	    Logger logger = null;
+	    long timeStamp = 0L;
+	    String level = null;
+	    String threadName = null;
+	    Object message = null;
+	    String ndc = null;
+	    String className = null;
+	    String methodName = null;
+	    String fileName = null;
+	    String lineNumber = null;
+	    Hashtable properties = new Hashtable();
+	
+
+        //event.setSequenceNumber(rs.getLong(1));
+        timeStamp = rs.getLong(2);
+        message = rs.getString(3);
+		logger = Logger.getLogger(rs.getString(4));
+        level = rs.getString(5);
+		Level levelImpl = Level.toLevel(level.trim());
+
+        ndc = rs.getString(6);
+        threadName = rs.getString(7);
+
+        short mask = rs.getShort(8);
+
+        fileName = rs.getString(9);
+        className = rs.getString(10);
+        methodName = rs.getString(11);
+        lineNumber = rs.getString(12).trim();
+
+		LocationInfo locationInfo = null;
+        if (fileName.equals(LocationInfo.NA)) {
+          locationInfo = LocationInfo.NA_LOCATION_INFO;
+        } else {
+          locationInfo = new LocationInfo(fileName, className,
+              methodName, lineNumber);
+        }
+
+        long id = rs.getLong(13);
+        //LogLog.info("Received event with id=" + id);
+        lastId = id;
+
+		ThrowableInformation throwableInfo = null;
+        if ((mask & DBHelper.EXCEPTION_EXISTS) != 0) {
+          throwableInfo = getException(connection, id);
+        }
+
+	    LoggingEvent event = new LoggingEvent(logger.getName(),
+	            logger, timeStamp, levelImpl, message,
+	            threadName,
+	            throwableInfo,
+	            ndc,
+	            locationInfo,
+	            properties);
+
+
+        // Scott asked for this info to be
+        event.setProperty("log4jid", Long.toString(id));
+
+        if ((mask & DBHelper.PROPERTIES_EXIST) != 0) {
+          getProperties(connection, id, event);
+        }
+
+
+
+
+        if (!parentDBReceiver.isPaused()) {
+          parentDBReceiver.doPost(event);
+        }
+      } // while
+      statement.close();
+      statement = null;
+    } catch (SQLException sqle) {
+      getLogger().error("Problem receiving events", sqle);
+    } finally {
+      closeConnection(connection);
+    }
+  }
+
+  void closeConnection(Connection connection) {
+    if (connection != null) {
+      try {
+        //LogLog.warn("closing the connection. ", new Exception("x"));
+        connection.close();
+      } catch (SQLException sqle) {
+        // nothing we can do here
+      }
+    }
+  }
+
+  /**
+   * Retrieve the event properties from the logging_event_property table.
+   * 
+   * @param connection
+   * @param id
+   * @param event
+   * @throws SQLException
+   */
+  void getProperties(Connection connection, long id, LoggingEvent event)
+      throws SQLException {
+
+    PreparedStatement statement = connection.prepareStatement(sqlProperties);
+    try {
+      statement.setLong(1, id);
+      ResultSet rs = statement.executeQuery();
+
+      while (rs.next()) {
+        String key = rs.getString(1);
+        String value = rs.getString(2);
+        event.setProperty(key, value);
+      }
+    } finally {
+      statement.close();
+    }
+  }
+
+  /**
+   * Retrieve the exception string representation from the
+   * logging_event_exception table.
+   * 
+   * @param connection
+   * @param id
+   * @throws SQLException
+   */
+  ThrowableInformation getException(Connection connection, long id)
+      throws SQLException {
+
+    PreparedStatement statement = null;
+
+    try {
+      statement = connection.prepareStatement(sqlException);
+      statement.setLong(1, id);
+      ResultSet rs = statement.executeQuery();
+
+      Vector v = new Vector();
+
+      while (rs.next()) {
+        //int i = rs.getShort(1);
+        v.add(rs.getString(1));
+      }
+
+      int len = v.size();
+      String[] strRep = new String[len];
+      for (int i = 0; i < len; i++) {
+        strRep[i] = (String) v.get(i);
+      }
+      // we've filled strRep, we now attach it to the event
+      return new ThrowableInformation(strRep);
+    } finally {
+      if (statement != null) {
+        statement.close();
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/log4j/db/DataSourceConnectionSource.java b/src/main/java/org/apache/log4j/db/DataSourceConnectionSource.java
new file mode 100644
index 0000000..d5975ee
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DataSourceConnectionSource.java
@@ -0,0 +1,105 @@
+/*
+ * 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.db;
+
+
+import org.apache.log4j.xml.DOMConfigurator;
+import org.apache.log4j.xml.UnrecognizedElementHandler;
+import org.w3c.dom.Element;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Properties;
+
+
+/**
+ *  The DataSourceConnectionSource is an implementation of {@link ConnectionSource}
+ *  that obtains the Connection in the recommended JDBC manner based on
+ *  a {@link javax.sql.DataSource DataSource}.
+ *  <p>
+ *
+ *  @author Ray DeCampo
+ *  @author Ceki G&uuml;lc&uuml;
+ */
+public class DataSourceConnectionSource extends ConnectionSourceSkeleton
+        implements UnrecognizedElementHandler {
+
+  private DataSource dataSource;
+
+  
+  public void activateOptions() {
+    //LogLog.debug("**********DataSourceConnectionSource.activateOptions called");
+    if (dataSource == null) {
+      getLogger().warn("WARNING: No data source specified");
+    } else {
+      Connection connection = null;
+      try {
+        connection = getConnection();
+      } catch(SQLException se) {
+        getLogger().warn("Could not get a connection to discover the dialect to use.", se);
+      }
+      if(connection != null) {
+        discoverConnnectionProperties();
+      } 
+      if(!supportsGetGeneratedKeys() && getSQLDialectCode() == ConnectionSource.UNKNOWN_DIALECT) {
+        getLogger().warn("Connection does not support GetGeneratedKey method and could not discover the dialect.");
+      }
+    }
+  }
+
+  /**
+   * @see org.apache.log4j.db.ConnectionSource#getConnection()
+   */
+  public Connection getConnection() throws SQLException {
+    if (dataSource == null) {
+      getLogger().error("WARNING: No data source specified");
+      return null;
+    }
+
+    if (getUser() == null) {
+      return dataSource.getConnection();
+    } else {
+      return dataSource.getConnection(getUser(), getPassword());
+    }
+  }
+
+  public DataSource getDataSource() {
+    return dataSource;
+  }
+
+  public void setDataSource(DataSource dataSource) {
+    this.dataSource = dataSource;
+  }
+
+    /**
+     * @{inheritDoc}
+     */
+  public boolean parseUnrecognizedElement(Element element, Properties props) throws Exception {
+        if ("dataSource".equals(element.getNodeName())) {
+            Object instance =
+                    DOMConfigurator.parseElement(element, props, DataSource.class);
+            if (instance instanceof DataSource) {
+               setDataSource((DataSource) instance);
+            }
+            return true;
+        }
+        return false;
+  }
+
+}
diff --git a/src/main/java/org/apache/log4j/db/DriverManagerConnectionSource.java b/src/main/java/org/apache/log4j/db/DriverManagerConnectionSource.java
new file mode 100644
index 0000000..9bfdf65
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DriverManagerConnectionSource.java
@@ -0,0 +1,133 @@
+/*
+ * 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.db;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+
+
+/**
+ *  The DriverManagerConnectionSource is an implementation of {@link ConnectionSource}
+ *  that obtains the Connection in the traditional JDBC manner based on the
+ *  connection URL.
+ *  <p>
+ *  Note that this class will establish a new Connection for each call to
+ *  {@link #getConnection()}.  It is recommended that you either use a JDBC
+ *  driver that natively supported Connection pooling or that you create
+ *  your own implementation of {@link ConnectionSource} that taps into whatever
+ *  pooling mechanism you are already using.  (If you have access to a JNDI
+ *  implementation that supports {@link javax.sql.DataSource}s, e.g. within
+ *  a J2EE application server, see {@link JNDIConnectionSource}).  See
+ *  <a href="#dbcp">below</a> for a configuration example that uses the
+ *  <a href="http://jakarta.apache.org/commons/dbcp/index.html">commons-dbcp</a>
+ *  package from Apache.
+ *  <p>
+ *  Sample configuration:<br>
+ *  <pre>
+ *     &lt;connectionSource class="org.apache.log4j.jdbc.DriverManagerConnectionSource"&gt;
+ *        &lt;param name="driver" value="com.mysql.jdbc.Driver" /&gt;
+ *        &lt;param name="url" value="jdbc:mysql://localhost:3306/mydb" /&gt;
+ *        &lt;param name="username" value="myUser" /&gt;
+ *        &lt;param name="password" value="myPassword" /&gt;
+ *     &lt;/connectionSource&gt;
+ *  </pre>
+ *  <p>
+ *  <a name="dbcp">If</a> you do not have another connection pooling mechanism
+ *  built into your application, you can use  the
+ *  <a href="http://jakarta.apache.org/commons/dbcp/index.html">commons-dbcp</a>
+ *  package from Apache:<br>
+ *  <pre>
+ *     &lt;connectionSource class="org.apache.log4j.jdbc.DriverManagerConnectionSource"&gt;
+ *        &lt;param name="driver" value="org.apache.commons.dbcp.PoolingDriver" /&gt;
+ *        &lt;param name="url" value="jdbc:apache:commons:dbcp:/myPoolingDriver" /&gt;
+ *     &lt;/connectionSource&gt;
+ *  </pre>
+ *  Then the configuration information for the commons-dbcp package goes into
+ *  the file myPoolingDriver.jocl and is placed in the classpath.  See the
+ *  <a href="http://jakarta.apache.org/commons/dbcp/index.html">commons-dbcp</a>
+ *  documentation for details.
+ *
+ *  @author <a href="mailto:rdecampo@twcny.rr.com">Ray DeCampo</a>
+ */
+public class DriverManagerConnectionSource extends ConnectionSourceSkeleton {
+  private String driverClass = null;
+  private String url = null;
+
+  public void activateOptions() {
+    try {
+      if (driverClass != null) {
+        Class.forName(driverClass);
+        discoverConnnectionProperties();
+      } else {
+        getLogger().error(
+          "WARNING: No JDBC driver specified for log4j DriverManagerConnectionSource.");
+      }
+    } catch (final ClassNotFoundException cnfe) {
+     getLogger().error("Could not load JDBC driver class: " + driverClass, cnfe);
+    }
+  }
+
+
+  /**
+   * @see org.apache.log4j.db.ConnectionSource#getConnection()
+   */
+  public Connection getConnection() throws SQLException {
+    if (getUser() == null) {
+      return DriverManager.getConnection(url);
+    } else {
+      return DriverManager.getConnection(url, getUser(), getPassword());
+    }
+  }
+
+
+  /**
+   * Returns the url.
+   * @return String
+   */
+  public String getUrl() {
+    return url;
+  }
+
+
+  /**
+   * Sets the url.
+   * @param url The url to set
+   */
+  public void setUrl(String url) {
+    this.url = url;
+  }
+
+
+  /**
+   * Returns the name of the driver class.
+   * @return String
+   */
+  public String getDriverClass() {
+    return driverClass;
+  }
+
+
+  /**
+   * Sets the driver class.
+   * @param driverClass The driver class to set
+   */
+  public void setDriverClass(String driverClass) {
+    this.driverClass = driverClass;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/JNDIConnectionSource.java b/src/main/java/org/apache/log4j/db/JNDIConnectionSource.java
new file mode 100644
index 0000000..7073738
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/JNDIConnectionSource.java
@@ -0,0 +1,143 @@
+/*
+ * 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.db;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+
+// PortableRemoteObject was introduced in JDK 1.3. We won't use it.
+// import javax.rmi.PortableRemoteObject;
+import javax.sql.DataSource;
+
+
+/**
+ *  The <id>JNDIConnectionSource</id> is an implementation of
+ *  {@link ConnectionSource} that obtains a {@link javax.sql.DataSource} from a
+ *  JNDI provider and uses it to obtain a {@link java.sql.Connection}.  It is
+ *  primarily designed to be used inside of J2EE application servers or
+ *  application server clients, assuming the application server supports remote
+ *  access of {@link javax.sql.DataSource}s.  In this way one can take
+ *  advantage of  connection pooling and whatever other goodies the application
+ *  server provides.
+ *  <p>
+ *  Sample configuration:<br>
+ *  <pre>
+ *    &lt;connectionSource class="org.apache.log4j.jdbc.JNDIConnectionSource"&gt;
+ *        &lt;param name="jndiLocation" value="jdbc/MySQLDS" /&gt;
+ *    &lt;/connectionSource&gt;
+ *  </pre>
+ *  <p>
+ *  Sample configuration (with username and password):<br>
+ *  <pre>
+ *    &lt;connectionSource class="org.apache.log4j.jdbc.JNDIConnectionSource"&gt;
+ *        &lt;param name="jndiLocation" value="jdbc/MySQLDS" /&gt;
+ *        &lt;param name="username" value="myUser" /&gt;
+ *        &lt;param name="password" value="myPassword" /&gt;
+ *    &lt;/connectionSource&gt;
+ *  </pre>
+ *  <p>
+ *  Note that this class will obtain an {@link javax.naming.InitialContext}
+ *  using the no-argument constructor.  This will usually work when executing
+ *  within a J2EE environment.  When outside the J2EE environment, make sure
+ *  that you provide a jndi.properties file as described by your JNDI
+ *  provider's documentation.
+ *
+ *  @author <a href="mailto:rdecampo@twcny.rr.com">Ray DeCampo</a>
+ */
+public class JNDIConnectionSource
+       extends ConnectionSourceSkeleton {
+  private String jndiLocation = null;
+  private DataSource dataSource = null;
+
+  /**
+   * @see org.apache.log4j.spi.OptionHandler#activateOptions()
+   */
+  public void activateOptions() {
+    if (jndiLocation == null) {
+      getLogger().error("No JNDI location specified for JNDIConnectionSource.");
+    }
+    
+    discoverConnnectionProperties();
+
+  }
+  
+  /**
+   * @see org.apache.log4j.db.ConnectionSource#getConnection()
+   */
+  public Connection getConnection()
+         throws SQLException {
+    Connection conn = null;
+    try {
+
+      if(dataSource == null) {
+        dataSource = lookupDataSource();
+      }
+      if (getUser() == null) {
+        conn = dataSource.getConnection();
+      } else {
+        conn = dataSource.getConnection(getUser(), getPassword());
+      }
+    } catch (final NamingException ne) {
+         getLogger().error("Error while getting data source", ne);
+      throw new SQLException("NamingException while looking up DataSource: " + ne.getMessage());
+    } catch (final ClassCastException cce) {
+      getLogger().error("ClassCastException while looking up DataSource.", cce);
+      throw new SQLException("ClassCastException while looking up DataSource: " + cce.getMessage());
+    }
+
+    return conn;
+  }
+
+  /**
+   * Returns the jndiLocation.
+   * @return String
+   */
+  public String getJndiLocation() {
+    return jndiLocation;
+  }
+
+
+  /**
+   * Sets the jndiLocation.
+   * @param jndiLocation The jndiLocation to set
+   */
+  public void setJndiLocation(String jndiLocation) {
+    this.jndiLocation = jndiLocation;
+  }
+
+
+  private DataSource lookupDataSource()
+         throws NamingException, SQLException {
+    DataSource ds;
+    Context ctx = new InitialContext();
+    Object obj = ctx.lookup(jndiLocation);
+
+    // PortableRemoteObject was introduced in JDK 1.3. We won't use it.
+    //ds = (DataSource)PortableRemoteObject.narrow(obj, DataSource.class);
+    ds = (DataSource) obj;
+
+    if (ds == null) {
+      throw new SQLException("Failed to obtain data source from JNDI location " + jndiLocation);
+    } else {
+      return ds;
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/HSQLDBDialect.java b/src/main/java/org/apache/log4j/db/dialect/HSQLDBDialect.java
new file mode 100644
index 0000000..164a125
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/HSQLDBDialect.java
@@ -0,0 +1,31 @@
+/*
+ * 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.db.dialect; 
+
+/** 
+ * The HSQLDB dialect. 
+ * 
+ * @author <a href="http://www.qos.ch/log4j/">Ceki G&uuml;lc&uuml;</a>
+*/ 
+public class HSQLDBDialect implements SQLDialect { 
+ public static final String SELECT_CURRVAL = "CALL IDENTITY()"; 
+
+ public String getSelectInsertId() { 
+   return SELECT_CURRVAL; 
+ } 
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/MsSQLDialect.java b/src/main/java/org/apache/log4j/db/dialect/MsSQLDialect.java
new file mode 100644
index 0000000..08f4fc3
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/MsSQLDialect.java
@@ -0,0 +1,34 @@
+/*
+ * 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.db.dialect; 
+
+/** 
+* The MS SQL Server dialect is untested. 
+* 
+* Note that the dialect is not needed if your JDBC driver supports 
+* the getGeneratedKeys method introduced in JDBC 3.0 specification.
+* 
+* @author James Stauffer 
+*/ 
+public class MsSQLDialect implements SQLDialect { 
+ public static final String SELECT_CURRVAL = "SELECT @@identity id"; 
+
+ public String getSelectInsertId() { 
+   return SELECT_CURRVAL; 
+ } 
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/MySQLDialect.java b/src/main/java/org/apache/log4j/db/dialect/MySQLDialect.java
new file mode 100644
index 0000000..c1a63cd
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/MySQLDialect.java
@@ -0,0 +1,32 @@
+/*
+ * 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.db.dialect;
+
+/**
+ * 
+ * 
+ * @author Ceki
+ *
+ */
+public class MySQLDialect implements SQLDialect {
+  public static final String SELECT_LAST_INSERT_ID = "SELECT LAST_INSERT_ID()";
+  
+  public String getSelectInsertId() {
+    return SELECT_LAST_INSERT_ID;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/OracleDialect.java b/src/main/java/org/apache/log4j/db/dialect/OracleDialect.java
new file mode 100644
index 0000000..1714f1f
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/OracleDialect.java
@@ -0,0 +1,33 @@
+/*
+ * 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.db.dialect;
+
+/**
+ * The Oracle dialect. Tested successfully on Oracle9i Release 9.2.0.3.0 by 
+ * James Stauffer.
+ * 
+ * @author Ceki G&uuml;lc&uuml;
+ */
+public class OracleDialect implements SQLDialect {
+  public static final String SELECT_CURRVAL = "SELECT logging_event_id_seq.currval from dual";
+
+  public String getSelectInsertId() {
+    return SELECT_CURRVAL;
+  }
+
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/PostgreSQLDialect.java b/src/main/java/org/apache/log4j/db/dialect/PostgreSQLDialect.java
new file mode 100644
index 0000000..dde4ff9
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/PostgreSQLDialect.java
@@ -0,0 +1,35 @@
+/*
+ * 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.db.dialect;
+
+
+/**
+ * 
+ * @author ceki
+ *
+ * To change the template for this generated type comment go to
+ * Window>Preferences>Java>Code Generation>Code and Comments
+ */
+public class PostgreSQLDialect
+       implements SQLDialect {
+  public static final String SELECT_CURRVAL = "SELECT currval('logging_event_id_seq')";
+
+  public String getSelectInsertId() {
+    return SELECT_CURRVAL;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/SQLDialect.java b/src/main/java/org/apache/log4j/db/dialect/SQLDialect.java
new file mode 100644
index 0000000..291283f
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/SQLDialect.java
@@ -0,0 +1,27 @@
+/*
+ * 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.db.dialect;
+
+/**
+ * @author ceki
+ *
+ */
+public interface SQLDialect {
+  
+  public String getSelectInsertId();
+  
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/SybaseDialect.java b/src/main/java/org/apache/log4j/db/dialect/SybaseDialect.java
new file mode 100644
index 0000000..44ba75e
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/SybaseDialect.java
@@ -0,0 +1,32 @@
+/*
+ * 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.db.dialect; 
+
+/** 
+ * The Sybase dialect. 
+ * 
+*/ 
+public class SybaseDialect implements SQLDialect { 
+ public static final String SELECT_CURRVAL = "select @@identity"; 
+
+ public String getSelectInsertId() { 
+   return SELECT_CURRVAL; 
+ } 
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/Util.java b/src/main/java/org/apache/log4j/db/dialect/Util.java
new file mode 100644
index 0000000..5d60b12
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/Util.java
@@ -0,0 +1,125 @@
+/*
+ * 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.db.dialect;
+
+import org.apache.log4j.db.ConnectionSource;
+import org.apache.log4j.spi.ComponentBase;
+
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+
+
+/**
+ * 
+ * @author Ceki Gulcu
+ *
+ */
+public class Util extends ComponentBase {
+  private static final String POSTGRES_PART = "postgresql";
+  private static final String MYSQL_PART = "mysql";
+  private static final String ORACLE_PART = "oracle";
+  //private static final String MSSQL_PART = "mssqlserver4";
+  private static final String MSSQL_PART = "microsoft";
+  private static final String HSQL_PART = "hsql";
+  
+  public static int discoverSQLDialect(DatabaseMetaData meta) {
+    int dialectCode = 0;
+
+    try {
+
+      String dbName = meta.getDatabaseProductName().toLowerCase();
+
+      if (dbName.indexOf(POSTGRES_PART) != -1) {
+        return ConnectionSource.POSTGRES_DIALECT;
+      } else if (dbName.indexOf(MYSQL_PART) != -1) {
+        return ConnectionSource.MYSQL_DIALECT;
+      } else if (dbName.indexOf(ORACLE_PART) != -1) {
+        return ConnectionSource.ORACLE_DIALECT;
+      } else if (dbName.indexOf(MSSQL_PART) != -1) {
+        return ConnectionSource.MSSQL_DIALECT;
+      } else if (dbName.indexOf(HSQL_PART) != -1) {
+        return ConnectionSource.HSQL_DIALECT;
+      } else {
+        return ConnectionSource.UNKNOWN_DIALECT;
+      }
+    } catch (SQLException sqle) {
+      // we can't do much here
+    }
+
+    return dialectCode;
+  }
+
+  public static SQLDialect getDialectFromCode(int dialectCode) {
+    SQLDialect sqlDialect = null;
+
+    switch (dialectCode) {
+    case ConnectionSource.POSTGRES_DIALECT:
+      sqlDialect = new PostgreSQLDialect();
+
+      break;
+    case ConnectionSource.MYSQL_DIALECT:
+      sqlDialect = new MySQLDialect();
+
+      break;
+    case ConnectionSource.ORACLE_DIALECT:
+      sqlDialect = new OracleDialect();
+
+      break;
+    case ConnectionSource.MSSQL_DIALECT:
+      sqlDialect = new MsSQLDialect();
+
+      break;
+    case ConnectionSource.HSQL_DIALECT:
+      sqlDialect = new HSQLDBDialect();
+
+      break;
+    }
+    return sqlDialect;
+  }
+  
+  /**
+   * This method handles cases where the 
+   * {@link DatabaseMetaData#supportsGetGeneratedKeys} method is missing in the
+   * JDBC driver implementation.
+   */
+  public boolean supportsGetGeneratedKeys(DatabaseMetaData meta) {
+    try {
+      //
+      //   invoking JDK 1.4 method by reflection
+      //
+      return ((Boolean) DatabaseMetaData.class.getMethod("supportsGetGeneratedKeys", null).invoke(meta, null)).booleanValue();
+    } catch(Throwable e) {
+      getLogger().info("Could not call supportsGetGeneratedKeys method. This may be recoverable");
+      return false;
+    }
+  }
+  
+/** 
+  * This method handles cases where the 
+  * {@link DatabaseMetaData#supportsBatchUpdates} method is missing in the
+  * JDBC driver implementation.
+  */
+  public boolean supportsBatchUpdates(DatabaseMetaData meta) {
+    try {
+      return meta.supportsBatchUpdates();
+    } catch(Throwable e) {
+      getLogger().info("Missing DatabaseMetaData.supportsBatchUpdates method.");
+      return false;
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/db2.sql b/src/main/java/org/apache/log4j/db/dialect/db2.sql
new file mode 100644
index 0000000..47d2164
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/db2.sql
@@ -0,0 +1,64 @@
+#  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.
+#
+# This SQL script creates the required tables by org.apache.log4j.db.DBAppender and 
+# org.apache.log4j.db.DBReceiver.
+#
+# It is intended for IBM DB2 databases.
+#
+# WARNING  WARNING WARNING  WARNING 
+# =================================
+# This SQL script has not been tested on an actual DB2
+# instance. It may contain errors or even invalid SQL
+# statements.
+
+DROP TABLE  logging_event_property;
+DROP TABLE  logging_event_exception;
+DROP TABLE  logging_event;
+
+CREATE TABLE logging_event 
+  (
+    sequence_number   BIGINT NOT NULL,
+    timestamp         BIGINT NOT NULL,
+    rendered_message  VARCHAR(4000) NOT NULL,
+    logger_name       VARCHAR(254) NOT NULL,
+    level_string      VARCHAR(254) NOT NULL,
+    ndc               VARCHAR(4000),
+    thread_name       VARCHAR(254),
+    reference_flag    SMALLINT,
+    caller_filename   VARCHAR(254) NOT NULL,
+    caller_class      VARCHAR(254) NOT NULL,
+    caller_method     VARCHAR(254) NOT NULL,
+    caller_line       CHAR(4) NOT NULL,
+    event_id          INTEGER GENERATED ALWAYS AS IDENTITY (START WITH 1)
+  );
+
+CREATE TABLE logging_event_property
+  (
+    event_id	      INTEGER NOT NULL,
+    mapped_key        VARCHAR(254) NOT NULL,
+    mapped_value      VARCHAR(1024),
+    PRIMARY KEY(event_id, mapped_key),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+
+CREATE TABLE logging_event_exception
+  (
+    event_id         INTEGER NOT NULL,
+    i                SMALLINT NOT NULL,
+    trace_line       VARCHAR(254) NOT NULL,
+    PRIMARY KEY(event_id, i),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
diff --git a/src/main/java/org/apache/log4j/db/dialect/db2l.sql b/src/main/java/org/apache/log4j/db/dialect/db2l.sql
new file mode 100644
index 0000000..0f91315
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/db2l.sql
@@ -0,0 +1,61 @@
+#  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.
+# This SQL script creates the required tables by org.apache.log4j.db.DBAppender and 
+# org.apache.log4j.db.DBReceiver.
+#
+# It is intended for PostgreSQL databases.
+
+DROP TABLE    logging_event_property;
+DROP TABLE    logging_event_exception;
+DROP TABLE    logging_event;
+
+
+CREATE SEQUENCE logging_event_id_seq MINVALUE 1 START 1;
+
+
+CREATE TABLE logging_event 
+  (
+    sequence_number   BIGINT NOT NULL,
+    timestamp         BIGINT NOT NULL,
+    rendered_message  TEXT NOT NULL,
+    logger_name       VARCHAR(254) NOT NULL,
+    level_string      VARCHAR(254) NOT NULL,
+    ndc               TEXT,
+    thread_name       VARCHAR(254),
+    reference_flag    SMALLINT,
+    caller_filename   VARCHAR(254) NOT NULL,
+    caller_class      VARCHAR(254) NOT NULL,
+    caller_method     VARCHAR(254) NOT NULL,
+    caller_line       CHAR(4) NOT NULL,
+    event_id          INT IDENTITY GENERATED ALWAYS PRIMARY KEY
+  );
+
+CREATE TABLE logging_event_property
+  (
+    event_id	      INT NOT NULL,
+    mapped_key        VARCHAR(254) NOT NULL,
+    mapped_value      VARCHAR(1024),
+    PRIMARY KEY(event_id, mapped_key),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+
+CREATE TABLE logging_event_exception
+  (
+    event_id         INT NOT NULL,
+    i                SMALLINT NOT NULL,
+    trace_line       VARCHAR(254) NOT NULL,
+    PRIMARY KEY(event_id, i),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
diff --git a/src/main/java/org/apache/log4j/db/dialect/hsqldb.sql b/src/main/java/org/apache/log4j/db/dialect/hsqldb.sql
new file mode 100644
index 0000000..50f8f78
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/hsqldb.sql
@@ -0,0 +1,60 @@
+//  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.
+// This SQL script creates the required tables by
+// org.apache.log4j.db.DBAppender and org.apache.log4j.db.DBReceiver.
+//
+// It is intended for HSQLDB. 
+//
+
+DROP TABLE logging_event_exception IF EXISTS;
+DROP TABLE logging_event_property IF EXISTS;
+DROP TABLE logging_event IF EXISTS;
+
+
+CREATE TABLE logging_event 
+  (
+    sequence_number   BIGINT NOT NULL,
+    timestamp         BIGINT NOT NULL,
+    rendered_message  LONGVARCHAR NOT NULL,
+    logger_name       VARCHAR NOT NULL,
+    level_string      VARCHAR NOT NULL,
+    ndc               LONGVARCHAR,
+    thread_name       VARCHAR,
+    reference_flag    SMALLINT,
+    caller_filename   VARCHAR, 
+    caller_class      VARCHAR, 
+    caller_method     VARCHAR, 
+    caller_line       CHAR(4), 
+    event_id          INT NOT NULL IDENTITY
+  );
+
+
+CREATE TABLE logging_event_property
+  (
+    event_id	      INT NOT NULL,
+    mapped_key        VARCHAR(254) NOT NULL,
+    mapped_value      LONGVARCHAR,
+    PRIMARY KEY(event_id, mapped_key),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+
+CREATE TABLE logging_event_exception
+  (
+    event_id         INT NOT NULL,
+    i                SMALLINT NOT NULL,
+    trace_line       VARCHAR NOT NULL,
+    PRIMARY KEY(event_id, i),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
diff --git a/src/main/java/org/apache/log4j/db/dialect/mssql.sql b/src/main/java/org/apache/log4j/db/dialect/mssql.sql
new file mode 100644
index 0000000..d87e0a0
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/mssql.sql
@@ -0,0 +1,61 @@
+--  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.
+--
+--
+-- This SQL script creates the required tables by org.apache.log4j.db.DBAppender and 
+-- org.apache.log4j.db.DBReceiver. 
+-- 
+-- It is intended for MS SQL Server databases.  This has been tested with version 7.0. 
+
+DROP TABLE logging_event_property 
+DROP TABLE logging_event_exception 
+DROP TABLE logging_event 
+
+CREATE TABLE logging_event 
+  ( 
+    sequence_number   DECIMAL(20) NOT NULL, 
+    timestamp         DECIMAL(20) NOT NULL, 
+    rendered_message  VARCHAR(4000) NOT NULL, 
+    logger_name       VARCHAR(254) NOT NULL, 
+    level_string      VARCHAR(254) NOT NULL, 
+    ndc               VARCHAR(4000), 
+    thread_name       VARCHAR(254), 
+    reference_flag    SMALLINT, 
+    caller_filename   VARCHAR(254) NOT NULL,
+    caller_class      VARCHAR(254) NOT NULL,
+    caller_method     VARCHAR(254) NOT NULL,
+    caller_line       CHAR(4) NOT NULL,
+    event_id          INT NOT NULL identity, 
+    PRIMARY KEY(event_id) 
+  ) 
+
+CREATE TABLE logging_event_property 
+  ( 
+    event_id          INT NOT NULL, 
+    mapped_key        VARCHAR(254) NOT NULL, 
+    mapped_value      VARCHAR(1024), 
+    PRIMARY KEY(event_id, mapped_key), 
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id) 
+  ) 
+
+CREATE TABLE logging_event_exception 
+  ( 
+    event_id         INT NOT NULL, 
+    i                SMALLINT NOT NULL, 
+    trace_line       VARCHAR(254) NOT NULL, 
+    PRIMARY KEY(event_id, i), 
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id) 
+  ) 
+
diff --git a/src/main/java/org/apache/log4j/db/dialect/mysql.sql b/src/main/java/org/apache/log4j/db/dialect/mysql.sql
new file mode 100644
index 0000000..e3a2be1
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/mysql.sql
@@ -0,0 +1,71 @@
+#  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.
+#
+#
+#
+# This SQL script creates the required tables by org.apache.log4j.db.DBAppender and 
+# org.apache.log4j.db.DBReceiver.
+#
+# It is intended for MySQL databases. It has been tested on MySQL 4.1.1 with 
+# INNODB tables.
+
+
+BEGIN;
+DROP TABLE IF EXISTS logging_event_property;
+DROP TABLE IF EXISTS logging_event_exception;
+DROP TABLE IF EXISTS logging_event;
+COMMIT;
+
+
+BEGIN;
+CREATE TABLE logging_event 
+  (
+    sequence_number BIGINT NOT NULL,
+    timestamp         BIGINT NOT NULL,
+    rendered_message  TEXT NOT NULL,
+    logger_name       VARCHAR(254) NOT NULL,
+    level_string      VARCHAR(254) NOT NULL,
+    ndc               TEXT,
+    thread_name       VARCHAR(254),
+    reference_flag    SMALLINT,
+    caller_filename   VARCHAR(254) NOT NULL,
+    caller_class      VARCHAR(254) NOT NULL,
+    caller_method     VARCHAR(254) NOT NULL,
+    caller_line       CHAR(4) NOT NULL,
+    event_id          INT NOT NULL AUTO_INCREMENT PRIMARY KEY
+  );
+COMMIT;
+
+BEGIN;
+CREATE TABLE logging_event_property
+  (
+    event_id	      INT NOT NULL,
+    mapped_key        VARCHAR(254) NOT NULL,
+    mapped_value      TEXT,
+    PRIMARY KEY(event_id, mapped_key),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+COMMIT;
+
+BEGIN;
+CREATE TABLE logging_event_exception
+  (
+    event_id         INT NOT NULL,
+    i                SMALLINT NOT NULL,
+    trace_line       VARCHAR(254) NOT NULL,
+    PRIMARY KEY(event_id, i),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+COMMIT;
diff --git a/src/main/java/org/apache/log4j/db/dialect/oracle.sql b/src/main/java/org/apache/log4j/db/dialect/oracle.sql
new file mode 100644
index 0000000..84bf9e5
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/oracle.sql
@@ -0,0 +1,77 @@
+--  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.
+--
+--
+-- This SQL script creates the required tables by org.apache.log4j.db.DBAppender and 
+-- org.apache.log4j.db.DBReceiver.
+--
+-- It is intended for Oracle databases.
+
+-- Tested successfully on Oracle9i Release 9.2.0.3.0 by James Stauffer
+-- Tested successfully on Oracle9i Release by Elias Ross
+
+-- The following lines are useful in cleaning any previous tables 
+
+--drop TRIGGER logging_event_id_seq_trig; 
+--drop SEQUENCE logging_event_id_seq; 
+--drop table logging_event_property; 
+--drop table logging_event_exception; 
+--drop table logging_event; 
+
+CREATE SEQUENCE logging_event_id_seq MINVALUE 1 START WITH 1;
+
+CREATE TABLE logging_event 
+  (
+    sequence_number   NUMBER(20) NOT NULL,
+    timestamp         NUMBER(20) NOT NULL,
+    rendered_message  VARCHAR2(4000) NOT NULL,
+    logger_name       VARCHAR2(254) NOT NULL,
+    level_string      VARCHAR2(254) NOT NULL,
+    ndc               VARCHAR2(4000),
+    thread_name       VARCHAR2(254),
+    reference_flag    NUMBER(5),
+    caller_filename   VARCHAR2(254) NOT NULL,
+    caller_class      VARCHAR2(254) NOT NULL,
+    caller_method     VARCHAR2(254) NOT NULL,
+    caller_line       CHAR(4) NOT NULL,
+    event_id          NUMBER(10) PRIMARY KEY
+  );
+
+CREATE OR REPLACE TRIGGER logging_event_id_seq_trig
+BEFORE INSERT ON logging_event
+FOR EACH ROW
+BEGIN
+   SELECT logging_event_id_seq.nextval
+   INTO :new.sequence_number FROM dual;
+END;
+
+CREATE TABLE logging_event_property
+  (
+    event_id	      NUMBER(10) NOT NULL,
+    mapped_key        VARCHAR2(254) NOT NULL,
+    mapped_value      VARCHAR2(1024),
+    PRIMARY KEY(event_id, mapped_key),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+  
+CREATE TABLE logging_event_exception
+  (
+    event_id         NUMBER(10) NOT NULL,
+    i                NUMBER(5)  NOT NULL,
+    trace_line       VARCHAR2(254) NOT NULL,
+    PRIMARY KEY(event_id, i),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+  
diff --git a/src/main/java/org/apache/log4j/db/dialect/postgresql.sql b/src/main/java/org/apache/log4j/db/dialect/postgresql.sql
new file mode 100644
index 0000000..c38757b
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/postgresql.sql
@@ -0,0 +1,63 @@
+#  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.
+#
+## This SQL script creates the required tables by org.apache.log4j.db.DBAppender and 
+# org.apache.log4j.db.DBReceiver.
+#
+# It is intended for PostgreSQL databases.
+
+DROP TABLE    logging_event_property;
+DROP TABLE    logging_event_exception;
+DROP SEQUENCE logging_event_id_seq;
+DROP TABLE    logging_event;
+
+
+CREATE SEQUENCE logging_event_id_seq MINVALUE 1 START 1;
+
+
+CREATE TABLE logging_event 
+  (
+    sequence_number   BIGINT NOT NULL,
+    timestamp         BIGINT NOT NULL,
+    rendered_message  TEXT NOT NULL,
+    logger_name       VARCHAR(254) NOT NULL,
+    level_string      VARCHAR(254) NOT NULL,
+    ndc               TEXT,
+    thread_name       VARCHAR(254),
+    reference_flag    SMALLINT,
+    caller_filename   VARCHAR(254) NOT NULL,
+    caller_class      VARCHAR(254) NOT NULL,
+    caller_method     VARCHAR(254) NOT NULL,
+    caller_line       CHAR(4) NOT NULL,
+    event_id          INT DEFAULT nextval('logging_event_id_seq') PRIMARY KEY
+  );
+
+CREATE TABLE logging_event_property
+  (
+    event_id	      INT NOT NULL,
+    mapped_key        VARCHAR(254) NOT NULL,
+    mapped_value      VARCHAR(1024),
+    PRIMARY KEY(event_id, mapped_key),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+
+CREATE TABLE logging_event_exception
+  (
+    event_id         INT NOT NULL,
+    i                SMALLINT NOT NULL,
+    trace_line       VARCHAR(254) NOT NULL,
+    PRIMARY KEY(event_id, i),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
diff --git a/src/main/java/org/apache/log4j/db/package.html b/src/main/java/org/apache/log4j/db/package.html
new file mode 100644
index 0000000..55652fb
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/package.html
@@ -0,0 +1,36 @@
+<!--
+  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.
+-->
+
+<html>
+<body>
+
+<p>The org.apache.log4j.db package provides means to append logging events 
+into various databases. The persisted data can be later read back using
+{@link org.apache.log4j.db.DBReceiver}.
+</p>
+
+<p>Most popular database systems, such as PostgreSQL, MySQL, Oracle, DB2 and MsSQL
+are supported.
+</p>
+
+<p>Just as importantly, the way for obtaining JDBC connections is pluggable. Connections can
+be obtained through the tradinal way of DriverManager, or alternatively as a DataSource. 
+A DataSource can be instantiated directly or it can obtained through JNDI.
+</p>
+
+</body>
+</html>
diff --git a/src/main/java/org/apache/log4j/helpers/Constants.java b/src/main/java/org/apache/log4j/helpers/Constants.java
new file mode 100644
index 0000000..dbbbe59
--- /dev/null
+++ b/src/main/java/org/apache/log4j/helpers/Constants.java
@@ -0,0 +1,126 @@
+/*

+ * 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.helpers;

+

+

+/**

+ * Constants used internally throughout log4j.

+ *

+ */

+public interface Constants {

+

+    /**

+     * log4j package name string literal.

+     */

+  String LOG4J_PACKAGE_NAME = "org.apache.log4j";

+

+  /**

+   *  The name of the default repository is "default" (without the quotes).

+   */

+  String DEFAULT_REPOSITORY_NAME  = "default";

+

+    /**

+     * application string literal.

+     */

+  String APPLICATION_KEY = "application";

+    /**

+     * hostname string literal.

+     */

+  String HOSTNAME_KEY = "hostname";

+    /**

+     * receiver string literal.

+     */

+  String RECEIVER_NAME_KEY = "receiver";

+    /**

+     * log4jid string literal.

+     */

+  String LOG4J_ID_KEY = "log4jid";

+    /**

+     * time stamp pattern string literal.

+     */

+  String TIMESTAMP_RULE_FORMAT = "yyyy/MM/dd HH:mm:ss";

+

+  /**

+   * The default property file name for automatic configuration.

+   */

+  String DEFAULT_CONFIGURATION_FILE = "log4j.properties";

+  /**

+   * The default XML configuration file name for automatic configuration.

+   */

+  String DEFAULT_XML_CONFIGURATION_FILE = "log4j.xml";

+    /**

+     * log4j.configuration string literal.

+     */

+  String DEFAULT_CONFIGURATION_KEY = "log4j.configuration";

+    /**

+     * log4j.configuratorClass string literal.

+     */

+  String CONFIGURATOR_CLASS_KEY = "log4j.configuratorClass";

+

+    /**

+     * JNDI context name string literal.

+     */

+  String JNDI_CONTEXT_NAME = "java:comp/env/log4j/context-name";

+

+    /**

+     * TEMP_LIST_APPENDER string literal.

+     */

+  String TEMP_LIST_APPENDER_NAME = "TEMP_LIST_APPENDER";

+    /**

+     * TEMP_CONSOLE_APPENDER string literal.

+     */

+  String TEMP_CONSOLE_APPENDER_NAME = "TEMP_CONSOLE_APPENDER";

+    /**

+     * Codes URL string literal.

+     */

+  String CODES_HREF =

+          "http://logging.apache.org/log4j/docs/codes.html";

+

+

+    /**

+     * ABSOLUTE string literal.

+     */

+  String ABSOLUTE_FORMAT = "ABSOLUTE";

+    /**

+     * SimpleTimePattern for ABSOLUTE.

+     */

+  String ABSOLUTE_TIME_PATTERN = "HH:mm:ss,SSS";

+

+    /**

+     * SimpleTimePattern for ABSOLUTE.

+     */

+  String SIMPLE_TIME_PATTERN = "HH:mm:ss";

+

+    /**

+     * DATE string literal.

+     */

+  String DATE_AND_TIME_FORMAT = "DATE";

+    /**

+     * SimpleTimePattern for DATE.

+     */

+  String DATE_AND_TIME_PATTERN = "dd MMM yyyy HH:mm:ss,SSS";

+

+    /**

+     * ISO8601 string literal.

+     */

+  String ISO8601_FORMAT = "ISO8601";

+    /**

+     * SimpleTimePattern for ISO8601.

+     */

+  String ISO8601_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS";

+}

diff --git a/src/main/java/org/apache/log4j/helpers/MessageFormatter.java b/src/main/java/org/apache/log4j/helpers/MessageFormatter.java
new file mode 100644
index 0000000..8c77da1
--- /dev/null
+++ b/src/main/java/org/apache/log4j/helpers/MessageFormatter.java
@@ -0,0 +1,154 @@
+/*
+ * 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.helpers;
+
+
+/**
+ * Formats messages according to very simple rules.
+ * See {@link #format(String,Object)} and
+ * {@link #format(String,Object,Object)} for more details.
+ *
+ * @author Ceki G&uuml;lc&uuml;
+ */
+public final class MessageFormatter {
+    /**
+     * Private formatter since all methods and members are static.
+     */
+    private MessageFormatter() {
+        super();
+    }
+
+    /**
+     * Start of replacement block.
+     */
+    private static final char DELIM_START = '{';
+    /**
+     * End of replacement block.
+     */
+    private static final char DELIM_STOP = '}';
+
+    /**
+     * Performs single argument substitution for the 'messagePattern' passed as
+     * parameter.
+     * <p/>
+     * For example, <code>MessageFormatter.format("Hi {}.", "there");</code>
+     * will return the string "Hi there.".
+     * <p/>
+     * The {} pair is called the formatting element. It serves to designate the
+     * location where the argument needs to be inserted within the pattern.
+     *
+     * @param messagePattern
+     *     The message pattern which will be parsed and formatted
+     * @param argument
+     *     The argument to be inserted instead of the formatting element
+     * @return The formatted message
+     */
+    public static String format(final String messagePattern,
+                                final Object argument) {
+        int j = messagePattern.indexOf(DELIM_START);
+        int len = messagePattern.length();
+        char escape = 'x';
+
+        // if there are no { characters or { is the last character
+        // then we just return messagePattern
+        if (j == -1 || (j + 1 == len)) {
+            return messagePattern;
+        } else {
+            char delimStop = messagePattern.charAt(j + 1);
+            if (j > 0) {
+                escape = messagePattern.charAt(j - 1);
+            }
+            if ((delimStop != DELIM_STOP) || (escape == '\\')) {
+                // invalid DELIM_START/DELIM_STOP pair or espace character is
+                // present
+                return messagePattern;
+            } else {
+                StringBuffer sbuf = new StringBuffer(len + 20);
+                sbuf.append(messagePattern.substring(0, j));
+                sbuf.append(argument);
+                sbuf.append(messagePattern.substring(j + 2));
+                return sbuf.toString();
+            }
+        }
+    }
+
+    /**
+     * /**
+     * Performs a two argument substitution for the 'messagePattern' passed as
+     * parameter.
+     * <p/>
+     * For example, <code>MessageFormatter.format("Hi {}. My name is {}.",
+     * "there", "David");</code> will return the string
+     * "Hi there. My name is David.".
+     * <p/>
+     * The '{}' pair is called a formatting element. It serves to designate the
+     * location where the arguments need to be inserted within
+     * the message pattern.
+     *
+     * @param messagePattern
+     *     The message pattern which will be parsed and formatted
+     * @param arg1
+     *     The first argument to replace the first formatting element
+     * @param arg2
+     *     The second argument to replace the second formatting element
+     * @return The formatted message
+     */
+    public static String format(final String messagePattern,
+                                final Object arg1,
+                                final Object arg2) {
+        int i = 0;
+        int len = messagePattern.length();
+
+        StringBuffer sbuf = new StringBuffer(messagePattern.length() + 50);
+
+        for (int l = 0; l < 2; l++) {
+            int j = messagePattern.indexOf(DELIM_START, i);
+
+            if (j == -1 || (j + 1 == len)) {
+                // no more variables
+                if (i == 0) { // this is a simple string
+                    return messagePattern;
+                } else {
+                    // add the tail string which contains no variables
+                    // and return the result.
+                    sbuf.append(messagePattern.substring(i,
+                                    messagePattern.length()));
+                    return sbuf.toString();
+                }
+            } else {
+                char delimStop = messagePattern.charAt(j + 1);
+                if ((delimStop != DELIM_STOP)) {
+                    // invalid DELIM_START/DELIM_STOP pair
+                    sbuf.append(messagePattern.substring(i,
+                            messagePattern.length()));
+                    return sbuf.toString();
+                }
+                sbuf.append(messagePattern.substring(i, j));
+                if (l == 0) {
+                    sbuf.append(arg1);
+                } else {
+                    sbuf.append(arg2);
+                }
+                i = j + 2;
+            }
+        }
+        // append the characters following the second {} pair.
+        sbuf.append(messagePattern.substring(i, messagePattern.length()));
+        return sbuf.toString();
+    }
+}
diff --git a/src/main/java/org/apache/log4j/helpers/UtilLoggingLevel.java b/src/main/java/org/apache/log4j/helpers/UtilLoggingLevel.java
new file mode 100644
index 0000000..b15fbca
--- /dev/null
+++ b/src/main/java/org/apache/log4j/helpers/UtilLoggingLevel.java
@@ -0,0 +1,238 @@
+/*
+ * 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.helpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.log4j.Level;
+
+/**
+ * An extension of the Level class that provides support for java.util.logging
+ * Levels.
+ *
+ *
+ * @author Scott Deboy (sdeboy@apache.org)
+ */
+
+public class UtilLoggingLevel extends Level {
+
+    /**
+     * Serialization version id.
+     */
+    private static final long serialVersionUID = 909301162611820211L;
+
+    /**
+     * Numerical value for SEVERE.
+     */
+    public static final int SEVERE_INT = 22000;
+    /**
+     * Numerical value for WARNING.
+     */
+    public static final int WARNING_INT = 21000;
+
+    //INFO level defined in parent as 20000..no need to redefine here
+    
+    /**
+     * Numerical value for CONFIG.
+     */
+    public static final int CONFIG_INT = 14000;
+    /**
+     * Numerical value for FINE.
+     */
+    public static final int FINE_INT = 13000;
+    /**
+     * Numerical value for FINER.
+     */
+    public static final int FINER_INT = 12000;
+    /**
+     * Numerical value for FINEST.
+     */
+    public static final int FINEST_INT = 11000;
+    /**
+     * Numerical value for UNKNOWN.
+     */
+    public static final int UNKNOWN_INT = 10000;
+
+    /**
+     * SEVERE.
+     */
+    public static final UtilLoggingLevel SEVERE =
+            new UtilLoggingLevel(SEVERE_INT, "SEVERE", 0);
+    /**
+     * WARNING.
+     */
+    public static final UtilLoggingLevel WARNING =
+            new UtilLoggingLevel(WARNING_INT, "WARNING", 4);
+    /**
+     * INFO.
+     */
+    //note: we've aligned the int values of the java.util.logging INFO level with log4j's level
+    public static final UtilLoggingLevel INFO =
+            new UtilLoggingLevel(INFO_INT, "INFO", 5);
+    /**
+     * CONFIG.
+     */
+    public static final UtilLoggingLevel CONFIG =
+            new UtilLoggingLevel(CONFIG_INT, "CONFIG", 6);
+    /**
+     * FINE.
+     */
+    public static final UtilLoggingLevel FINE =
+            new UtilLoggingLevel(FINE_INT, "FINE", 7);
+    /**
+     * FINER.
+     */
+    public static final UtilLoggingLevel FINER =
+            new UtilLoggingLevel(FINER_INT, "FINER", 8);
+    /**
+     * FINEST.
+     */
+    public static final UtilLoggingLevel FINEST =
+            new UtilLoggingLevel(FINEST_INT, "FINEST", 9);
+
+    /**
+     * Create new instance.
+     * @param level numeric value for level.
+     * @param levelStr symbolic name for level.
+     * @param syslogEquivalent Equivalent syslog severity.
+     */
+    protected UtilLoggingLevel(final int level,
+                               final String levelStr,
+                               final int syslogEquivalent) {
+        super(level, levelStr, syslogEquivalent);
+    }
+
+    /**
+     * Convert an integer passed as argument to a level. If the
+     * conversion fails, then this method returns the specified default.
+     * @param val numeric value.
+     * @param defaultLevel level to be returned if no level matches
+     * numeric value.
+     * @return matching level or default level.
+     */
+    public static UtilLoggingLevel toLevel(final int val,
+                               final UtilLoggingLevel defaultLevel) {
+        switch (val) {
+            case SEVERE_INT:
+                return SEVERE;
+
+            case WARNING_INT:
+                return WARNING;
+
+            case INFO_INT:
+                return INFO;
+
+            case CONFIG_INT:
+                return CONFIG;
+
+            case FINE_INT:
+                return FINE;
+
+            case FINER_INT:
+                return FINER;
+
+            case FINEST_INT:
+                return FINEST;
+
+            default:
+                return defaultLevel;
+        }
+    }
+
+    /**
+     * Gets level matching numeric value.
+     * @param val numeric value.
+     * @return  matching level or UtilLoggerLevel.FINEST if no match.
+     */
+    public static Level toLevel(final int val) {
+        return toLevel(val, FINEST);
+    }
+
+    /**
+     * Gets list of supported levels.
+     * @return  list of supported levels.
+     */
+    public static List getAllPossibleLevels() {
+        ArrayList list = new ArrayList();
+        list.add(FINE);
+        list.add(FINER);
+        list.add(FINEST);
+        list.add(INFO);
+        list.add(CONFIG);
+        list.add(WARNING);
+        list.add(SEVERE);
+        return list;
+    }
+
+    /**
+     * Get level with specified symbolic name.
+     * @param s symbolic name.
+     * @return matching level or Level.DEBUG if no match.
+     */
+    public static Level toLevel(final String s) {
+        return toLevel(s, Level.DEBUG);
+    }
+
+
+    /**
+     * Get level with specified symbolic name.
+     * @param sArg symbolic name.
+     * @param defaultLevel level to return if no match.
+     * @return matching level or defaultLevel if no match.
+     */
+    public static Level toLevel(final String sArg,
+                                final Level defaultLevel) {
+        if (sArg == null) {
+            return defaultLevel;
+        }
+
+        String s = sArg.toUpperCase();
+
+        if (s.equals("SEVERE")) {
+            return SEVERE;
+        }
+
+        //if(s.equals("FINE")) return Level.FINE;
+        if (s.equals("WARNING")) {
+            return WARNING;
+        }
+
+        if (s.equals("INFO")) {
+            return INFO;
+        }
+
+        if (s.equals("CONFI")) {
+            return CONFIG;
+        }
+
+        if (s.equals("FINE")) {
+            return FINE;
+        }
+
+        if (s.equals("FINER")) {
+            return FINER;
+        }
+
+        if (s.equals("FINEST")) {
+            return FINEST;
+        }
+        return defaultLevel;
+    }
+
+}
diff --git a/src/main/java/org/apache/log4j/net/AddressBased.java b/src/main/java/org/apache/log4j/net/AddressBased.java
new file mode 100644
index 0000000..ccd2e6a
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/AddressBased.java
@@ -0,0 +1,36 @@
+/*
+ * 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.net;
+
+
+/**
+ * Net based entities that 'work with' an Address
+ * should consider implementing this
+ * interface so that they can be treated generically.
+ *
+ * @author Paul Smith (psmith@apache.org)
+ *
+ */
+public interface AddressBased extends NetworkBased {
+  /**
+   * Returns a String representation of the Address this instance
+   * encompasses.
+   * @return String representation of the Address
+   */
+  String getAddress();
+}
diff --git a/src/main/java/org/apache/log4j/net/JMSReceiver.java b/src/main/java/org/apache/log4j/net/JMSReceiver.java
new file mode 100644
index 0000000..d22e25c
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/JMSReceiver.java
@@ -0,0 +1,301 @@
+/*
+ * 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.net;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.jms.Message;
+import javax.jms.MessageListener;
+import javax.jms.TopicConnection;
+import javax.jms.Topic;
+import javax.jms.TopicConnectionFactory;
+import javax.jms.TopicSubscriber;
+import javax.jms.Session;
+import javax.jms.TopicSession;
+import javax.jms.ObjectMessage;
+
+import javax.naming.InitialContext;
+import javax.naming.Context;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingException;
+
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.plugins.Plugin;
+import org.apache.log4j.plugins.Receiver;
+
+/**
+  JMSReceiver receives a remote logging event on a configured
+  JSM topic and "posts" it to a LoggerRepository as if the event was 
+  generated locally. This class is designed to receive events from 
+  the JMSAppender class (or classes that send compatible events).
+  
+  <p>Once the event has been "posted", it will be handled by the 
+  appenders currently configured in the LoggerRespository.
+  
+  <p>This implementation borrows heavily from the JMSSink
+  implementation.
+  
+  @author Mark Womack
+  @author Paul Smith
+  @author Stephen Pain
+*/
+public class JMSReceiver extends Receiver implements MessageListener {
+
+  private boolean active = false;
+
+  protected String topicFactoryName;
+  protected String topicName;
+  protected String userId;
+  protected String password;
+  protected TopicConnection topicConnection;
+  protected String jndiPath;
+  
+  private String remoteInfo;
+  private String providerUrl;
+
+  public JMSReceiver() { }
+
+  public JMSReceiver(String _topicFactoryName, String _topicName,
+          String _userId, String _password, String _jndiPath) {      
+      topicFactoryName = _topicFactoryName;
+      topicName = _topicName;
+      userId = _userId;
+      password = _password;
+      jndiPath = _jndiPath;
+  }
+
+  /**
+         * Sets the path to a properties file containing
+         * the initial context and jndi provider url
+         */
+    public void setJndiPath(String _jndiPath) {
+          jndiPath = _jndiPath;
+    }
+  
+     /**
+         * Gets the path to a properties file containing
+         * the initial context and jndi provider url
+         */
+     public String getJndiPath() {
+          return jndiPath;
+     }
+  
+  /**
+    Sets the JMS topic factory name to use when creating the 
+    JMS connection. */
+  public void setTopicFactoryName(String _topicFactoryName) {
+    topicFactoryName = _topicFactoryName;
+  }
+  
+  /**
+    Gets the curernt JMS topic factory name property. */
+  public String getTopicFactoryName() {
+    return topicFactoryName;
+  }
+  
+  /**
+   * Sets the JMS topic name to use when creating the
+   * JMS connection.
+   */
+  public void setTopicName(String _topicName) {
+    topicName = _topicName;
+  }
+  
+  /**
+   * Gets the curernt JMS topic name property.
+   */
+  public String getTopicName() {
+    return topicName;
+  }
+
+  /**
+    Sets the user id to use when creating the 
+    JMS connection. */
+  public void setUserId(String _userId) {
+    userId = _userId;
+  }
+  
+  /**
+   * Gets the current user id property.
+   */
+  public String getUserId() {
+    return userId;
+  }
+
+  /**
+   * Sets the password to use when creating the
+   * JMS connection.
+   */
+  public void setPassword(String _password) {
+    password = _password;
+  }
+  
+  /**
+   * Gets the curernt password property.
+   */
+  public String getPassword() {
+    return password;
+  }
+ 
+  /**
+   * Returns true if the receiver is the same class and they are
+   * configured for the same properties, and super class also considers
+   * them to be equivalent. This is used by PluginRegistry when determining
+   * if the a similarly configured receiver is being started.
+   * 
+   * @param testPlugin The plugin to test equivalency against.
+   * @return boolean True if the testPlugin is equivalent to this plugin.
+   */
+  public boolean isEquivalent(Plugin testPlugin) {
+    // only do full check if an instance of this class
+    if (testPlugin instanceof JMSReceiver) {
+ 
+      JMSReceiver receiver = (JMSReceiver)testPlugin;
+      
+      // check for same topic name and super class equivalency
+      return (
+            topicFactoryName.equals(receiver.getTopicFactoryName()) && 
+            (jndiPath == null || jndiPath.equals(receiver.getJndiPath())) && 
+            super.isEquivalent(testPlugin)
+            );
+    }
+    
+    return false;
+  }
+  
+  /**
+    Returns true if this receiver is active. */
+  public synchronized boolean isActive() {
+    return active;
+  }
+  
+  /**
+    Sets the flag to indicate if receiver is active or not. */
+  protected synchronized void setActive(boolean _active) {
+    active = _active;
+  }
+  
+  /**
+    Starts the JMSReceiver with the current options. */
+  public void activateOptions() {
+    if (!isActive()) {
+      try {
+        remoteInfo = topicFactoryName + ":" + topicName;
+
+        Context ctx = null;
+        if (jndiPath == null || jndiPath.equals("")) {
+                ctx = new InitialContext();
+        } else {
+                FileInputStream is = new FileInputStream(jndiPath);
+                Properties p = new Properties();
+                p.load(is);
+                is.close();
+                ctx = new InitialContext(p);
+        }
+
+        // give some more flexibility about the choice of a tab name
+        providerUrl = (String)ctx.getEnvironment().get(Context.PROVIDER_URL);
+        TopicConnectionFactory topicConnectionFactory;
+        topicConnectionFactory = 
+          (TopicConnectionFactory) lookup(ctx, topicFactoryName);
+        
+        if (userId != null && password != null) {
+          topicConnection =
+    	       topicConnectionFactory.createTopicConnection(userId, password);
+        } else {
+          topicConnection =
+    	       topicConnectionFactory.createTopicConnection();
+        }
+  	       
+        TopicSession topicSession =
+          topicConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
+  
+        Topic topic = (Topic)ctx.lookup(topicName);
+  
+        TopicSubscriber topicSubscriber = topicSession.createSubscriber(topic);
+      
+        topicSubscriber.setMessageListener(this);
+ 
+        topicConnection.start();
+ 
+        setActive(true);
+      } catch(Exception e) {
+        setActive(false);
+        if (topicConnection != null) {
+          try {
+            topicConnection.close();
+          } catch (Exception e2) {
+            // do nothing
+          }
+          topicConnection = null;
+        }
+        getLogger().error("Could not start JMSReceiver.", e);
+      }
+    }
+  }
+  
+  /**
+    Called when the receiver should be stopped. */
+  public synchronized void shutdown() {
+    if (isActive()) {
+      // mark this as no longer running
+      setActive(false);
+    
+      if (topicConnection != null) {
+        try {
+          topicConnection.close();
+        } catch (Exception e) {
+          // do nothing
+        }
+        topicConnection = null;
+      }
+    }
+  }
+
+  public void onMessage(Message message) {
+    try {
+      if(message instanceof  ObjectMessage) {
+        // get the logging event and post it to the repository
+      	ObjectMessage objectMessage = (ObjectMessage) message;
+      	LoggingEvent event = (LoggingEvent) objectMessage.getObject();
+      	
+      	// store the known remote info in an event property
+      	event.setProperty("log4j.remoteSourceInfo", remoteInfo);
+        event.setProperty("log4j.jmsProviderUrl", providerUrl);
+        
+      	doPost(event);
+      } else {
+      	getLogger().warn("Received message is of type "+message.getJMSType()
+		    +", was expecting ObjectMessage.");
+      }      
+    } catch(Exception e) {
+      getLogger().error("Exception thrown while processing incoming message.", e);
+    }
+  }
+
+  protected Object lookup(Context ctx, String name) throws NamingException {
+    try {
+      return ctx.lookup(name);
+    } catch(NameNotFoundException e) {
+      getLogger().error("Could not find name ["+name+"].");
+      throw e;
+    }
+  }
+
+}
diff --git a/src/main/java/org/apache/log4j/net/JMSReceiverBeanInfo.java b/src/main/java/org/apache/log4j/net/JMSReceiverBeanInfo.java
new file mode 100644
index 0000000..eec19d3
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/JMSReceiverBeanInfo.java
@@ -0,0 +1,52 @@
+/*
+ * 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.net;
+
+import java.beans.PropertyDescriptor;
+import java.beans.SimpleBeanInfo;
+
+
+/**
+ * BeanInfo class for the JMSReceiver.
+ *
+ * @author Paul Smith <psmith@apache.org>
+ *
+ */
+public class JMSReceiverBeanInfo extends SimpleBeanInfo {
+
+    /* (non-Javadoc)
+     * @see java.beans.BeanInfo#getPropertyDescriptors()
+     */
+    public PropertyDescriptor[] getPropertyDescriptors() {
+
+        try {
+
+            return new PropertyDescriptor[] {
+                new PropertyDescriptor("name", JMSReceiver.class),
+                new PropertyDescriptor("topicFactoryName", JMSReceiver.class),
+                new PropertyDescriptor("topicName", JMSReceiver.class),
+                new PropertyDescriptor("threshold", JMSReceiver.class),
+                new PropertyDescriptor("jndiPath", JMSReceiver.class),
+                new PropertyDescriptor("userId",
+                        JMSReceiver.class),
+            };
+        } catch (Exception e) {
+        }
+
+        return null;
+    }
+}
diff --git a/src/main/java/org/apache/log4j/net/MulticastAppender.java b/src/main/java/org/apache/log4j/net/MulticastAppender.java
new file mode 100644
index 0000000..de002c5
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/MulticastAppender.java
@@ -0,0 +1,345 @@
+/*
+ * 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.net;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.helpers.Constants;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.xml.XMLLayout;
+
+
+/**
+ *  Multicast-based Appender.  Works in conjunction with the MulticastReceiver, which expects
+ *  a LoggingEvent encoded using XMLLayout. 
+ * 
+ *  Sends log information as a multicast datagrams.
+ *
+ *  <p>Messages are not sent as LoggingEvent objects but as text after
+ *  applying XMLLayout.
+ *
+ *  <p>The port and remoteHost properties can be set in configuration properties.
+ *  By setting the remoteHost to a broadcast address any number of clients can
+ *  listen for log messages.
+ *
+ *  <p>This was inspired and really extended/copied from {@link SocketAppender}.  Please
+ *  see the docs for the proper credit to the authors of that class.
+ *
+ *  @author  <a href="mailto:kbrown@versatilesolutions.com">Kevin Brown</a>
+ *  @author Scott Deboy <sdeboy@apache.org>
+ * 
+ */
+public class MulticastAppender extends AppenderSkeleton implements PortBased {
+  /**
+     The default port number for the multicast packets. (9991).
+  */
+  static final int DEFAULT_PORT = 9991;
+
+  /**
+   * The MulticastDNS zone advertised by a MulticastAppender
+   * the MulticastAppender also adds a 'multicastAddress' property with the multicast address value as a string
+   */
+  public static final String ZONE = "_log4j_xml_mcast_appender.local.";
+
+  /**
+     We remember host name as String in addition to the resolved
+     InetAddress so that it can be returned via getOption().
+  */
+  String hostname;
+  String remoteHost;
+  String application;
+  int timeToLive;
+  InetAddress address;
+  int port = DEFAULT_PORT;
+  MulticastSocket outSocket;
+  private String encoding;
+
+  private boolean locationInfo = false;
+  private boolean advertiseViaMulticastDNS;
+  private ZeroConfSupport zeroConf;
+
+  public MulticastAppender() {
+     super(false);
+  }
+
+  /**
+     Open the multicast sender for the <b>RemoteHost</b> and <b>Port</b>.
+  */
+  public void activateOptions() {
+    try {
+      hostname = InetAddress.getLocalHost().getHostName();
+    } catch (UnknownHostException uhe) {
+      try {
+        hostname = InetAddress.getLocalHost().getHostAddress();
+      } catch (UnknownHostException uhe2) {
+        hostname = "unknown";
+      }
+    }
+
+    //allow system property of application to be primary
+    if (application == null) {
+      application = System.getProperty(Constants.APPLICATION_KEY);
+    } else {
+      if (System.getProperty(Constants.APPLICATION_KEY) != null) {
+        application = application + "-" + System.getProperty(Constants.APPLICATION_KEY);
+      }
+    }
+
+    if(remoteHost != null) {
+      address = getAddressByName(remoteHost);
+    } else {
+      String err = "The RemoteHost property is required for MulticastAppender named "+ name;
+      LogLog.error(err);
+      throw new IllegalStateException(err);
+    }
+
+    if (layout == null) {
+        layout = new XMLLayout();
+    }
+      
+    if (advertiseViaMulticastDNS) {
+        Map properties = new HashMap();
+        properties.put("multicastAddress", remoteHost);
+        zeroConf = new ZeroConfSupport(ZONE, port, getName(), properties);
+        zeroConf.advertise();
+    }
+    connect();
+    super.activateOptions();
+  }
+
+  /**
+     Close this appender.
+     <p>This will mark the appender as closed and
+     call then {@link #cleanUp} method.
+  */
+  public synchronized void close() {
+    if (closed) {
+      return;
+    }
+
+    this.closed = true;
+    if (advertiseViaMulticastDNS) {
+        zeroConf.unadvertise();
+    }
+    cleanUp();
+  }
+
+  /**
+     Close the Socket and release the underlying
+     connector thread if it has been created
+   */
+  public void cleanUp() {
+    if (outSocket != null) {
+      try {
+        outSocket.close();
+      } catch (Exception e) {
+        LogLog.error("Could not close outSocket.", e);
+      }
+
+      outSocket = null;
+    }
+  }
+
+  void connect() {
+    if (this.address == null) {
+      return;
+    }
+
+    try {
+      // First, close the previous connection if any.
+      cleanUp();
+      outSocket = new MulticastSocket();
+      outSocket.setTimeToLive(timeToLive);
+    } catch (IOException e) {
+      LogLog.error("Error in connect method of MulticastAppender named "+name, e);
+    }
+  }
+
+  public void append(LoggingEvent event) {
+    if (event == null) {
+      return;
+    }
+
+    if(locationInfo) {
+	   event.getLocationInformation();
+	}
+
+    if (outSocket != null) {
+        event.setProperty(Constants.HOSTNAME_KEY, hostname);
+
+        if (application != null) {
+          event.setProperty(Constants.APPLICATION_KEY, application);
+        }
+      
+		if(locationInfo) {
+		   event.getLocationInformation();
+		}
+
+
+      try {
+        StringBuffer buf = new StringBuffer(layout.format(event));
+
+        byte[] payload;
+        if(encoding == null) {
+          payload = buf.toString().getBytes();
+        } else {
+          payload = buf.toString().getBytes(encoding);
+        }
+
+        DatagramPacket dp =
+           new DatagramPacket(payload, payload.length, address, port);
+        outSocket.send(dp);
+      } catch (IOException e) {
+        outSocket = null;
+        LogLog.warn("Detected problem with Multicast connection: " + e);
+      }
+    }
+  }
+
+  InetAddress getAddressByName(String host) {
+    try {
+      return InetAddress.getByName(host);
+    } catch (Exception e) {
+      LogLog.error("Could not find address of [" + host + "].", e);
+      return null;
+    }
+  }
+
+  /**
+     The <b>RemoteHost</b> option takes a string value which should be
+     the host name or ipaddress to send the multicast packets.
+   */
+  public void setRemoteHost(String host) {
+    remoteHost = host;
+  }
+
+  /**
+     Returns value of the <b>RemoteHost</b> option.
+   */
+  public String getRemoteHost() {
+    return remoteHost;
+  }
+
+  /**
+  The <b>LocationInfo</b> option takes a boolean value. If true,
+  the information sent to the remote host will include location
+  information. By default no location information is sent to the server.
+   */
+  public void setLocationInfo(boolean locationInfo) {
+	  this.locationInfo = locationInfo;
+  }
+
+  /**
+   * Returns value of the <b>LocationInfo</b> option.
+   */
+  public boolean getLocationInfo() {
+	  return locationInfo;
+  }
+
+  /**
+      The <b>Encoding</b> option specifies how the bytes are encoded.  If this option is not specified, 
+      the System encoding is used.
+    */
+   public void setEncoding(String encoding) {
+     this.encoding = encoding;
+   }
+
+   /**
+      Returns value of the <b>Encoding</b> option.
+    */
+   public String getEncoding() {
+     return encoding;
+   }
+  /**
+     The <b>App</b> option takes a string value which should be the name of the application getting logged.
+     If property was already set (via system property), don't set here.
+   */
+  public void setApplication(String app) {
+    this.application = app;
+  }
+
+  /**
+     Returns value of the <b>App</b> option.
+   */
+  public String getApplication() {
+    return application;
+  }
+
+  /**
+     The <b>Time to live</b> option takes a positive integer representing
+     the time to live value.
+   */
+  public void setTimeToLive(int timeToLive) {
+    this.timeToLive = timeToLive;
+  }
+
+  /**
+     Returns value of the <b>Time to Live</b> option.
+   */
+  public int getTimeToLive() {
+    return timeToLive;
+  }
+
+  /**
+     The <b>Port</b> option takes a positive integer representing
+     the port where multicast packets will be sent.
+   */
+  public void setPort(int port) {
+    this.port = port;
+  }
+
+  /**
+     Returns value of the <b>Port</b> option.
+   */
+  public int getPort() {
+    return port;
+  }
+
+  /* (non-Javadoc)
+   * @see org.apache.log4j.net.NetworkBased#isActive()
+   */
+  public boolean isActive() {
+    // TODO handle active/inactive
+    return true;
+  }
+
+    /**
+     * Gets whether appender requires a layout.
+     * @return false
+     */
+  public boolean requiresLayout() {
+      return true;
+  }
+
+  public boolean isAdvertiseViaMulticastDNS() {
+      return advertiseViaMulticastDNS;
+  }
+
+  public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
+      this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/net/MulticastReceiver.java b/src/main/java/org/apache/log4j/net/MulticastReceiver.java
new file mode 100644
index 0000000..2dfcec2
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/MulticastReceiver.java
@@ -0,0 +1,276 @@
+/*
+ * 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.net;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.Decoder;
+import org.apache.log4j.spi.LoggingEvent;
+
+
+/**
+ *  Multicast-based receiver.  Accepts LoggingEvents encoded using
+ *  MulticastAppender and XMLLayout. The the XML data is converted
+ *  back to a LoggingEvent and is posted.
+ *
+ *  @author Scott Deboy <sdeboy@apache.org>
+ *
+ */
+public class MulticastReceiver extends Receiver implements PortBased,
+  AddressBased, Pauseable {
+  private static final int PACKET_LENGTH = 16384;
+  private int port;
+  private String address;
+  private String encoding;
+  private MulticastSocket socket = null;
+
+  //default to log4j xml decoder
+  private String decoder = "org.apache.log4j.xml.XMLDecoder";
+  private Decoder decoderImpl;
+  private MulticastHandlerThread handlerThread;
+  private MulticastReceiverThread receiverThread;
+  private boolean paused;
+  private boolean advertiseViaMulticastDNS;
+  private ZeroConfSupport zeroConf;
+
+  /**
+   * The MulticastDNS zone advertised by a MulticastReceiver
+   */
+  public static final String ZONE = "_log4j_xml_mcast_receiver.local.";
+
+  public String getDecoder() {
+    return decoder;
+  }
+
+  public void setDecoder(String decoder) {
+    this.decoder = decoder;
+  }
+
+  public int getPort() {
+    return port;
+  }
+
+  public void setPort(int port) {
+    this.port = port;
+  }
+
+  public String getAddress() {
+    return address;
+  }
+
+  /**
+      The <b>Encoding</b> option specifies how the bytes are encoded.  If this option is not specified,
+      the system encoding will be used.
+    */
+  public void setEncoding(String encoding) {
+    this.encoding = encoding;
+  }
+
+  /**
+     Returns value of the <b>Encoding</b> option.
+   */
+  public String getEncoding() {
+    return encoding;
+  }
+
+  public synchronized void shutdown() {
+    active = false;
+    if (advertiseViaMulticastDNS) {
+        zeroConf.unadvertise();
+    }
+    if (handlerThread != null) {
+        handlerThread.interrupt();
+    }
+    if (receiverThread != null) {
+        receiverThread.interrupt();
+    }
+    if (socket != null) {
+        socket.close();
+    }
+  }
+
+  public void setAddress(String address) {
+    this.address = address;
+  }
+
+  public boolean isPaused() {
+    return paused;
+  }
+
+  public void setPaused(boolean b) {
+    paused = b;
+  }
+
+  public void activateOptions() {
+    InetAddress addr = null;
+
+    try {
+      Class c = Class.forName(decoder);
+      Object o = c.newInstance();
+
+      if (o instanceof Decoder) {
+        this.decoderImpl = (Decoder) o;
+      }
+    } catch (ClassNotFoundException cnfe) {
+      getLogger().warn("Unable to find decoder", cnfe);
+    } catch (IllegalAccessException iae) {
+      getLogger().warn("Could not construct decoder", iae);
+    } catch (InstantiationException ie) {
+      getLogger().warn("Could not construct decoder", ie);
+    }
+
+    try {
+      addr = InetAddress.getByName(address);
+    } catch (UnknownHostException uhe) {
+      uhe.printStackTrace();
+    }
+
+    try {
+      active = true;
+      socket = new MulticastSocket(port);
+      socket.joinGroup(addr);
+      receiverThread = new MulticastReceiverThread();
+      receiverThread.start();
+      handlerThread = new MulticastHandlerThread();
+      handlerThread.start();
+      if (advertiseViaMulticastDNS) {
+        zeroConf = new ZeroConfSupport(ZONE, port, getName());
+        zeroConf.advertise();
+      }
+
+    } catch (IOException ioe) {
+      ioe.printStackTrace();
+    }
+  }
+
+    public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
+        this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
+    }
+
+    public boolean isAdvertiseViaMulticastDNS() {
+        return advertiseViaMulticastDNS;
+    }
+
+    class MulticastHandlerThread extends Thread {
+    private List list = new ArrayList();
+
+    public MulticastHandlerThread() {
+      setDaemon(true);
+    }
+
+    public void append(String data) {
+      synchronized (list) {
+        list.add(data);
+        list.notify();
+      }
+    }
+
+    public void run() {
+      ArrayList list2 = new ArrayList();
+
+      while (isAlive()) {
+        synchronized (list) {
+          try {
+            while (list.size() == 0) {
+              list.wait();
+            }
+
+            if (list.size() > 0) {
+              list2.addAll(list);
+              list.clear();
+            }
+          } catch (InterruptedException ie) {
+          }
+        }
+
+        if (list2.size() > 0) {
+          Iterator iter = list2.iterator();
+
+          while (iter.hasNext()) {
+            String data = (String) iter.next();
+            List v = decoderImpl.decodeEvents(data.trim());
+
+            if (v != null) {
+              Iterator eventIter = v.iterator();
+
+              while (eventIter.hasNext()) {
+                if (!isPaused()) {
+                  doPost((LoggingEvent) eventIter.next());
+                }
+              }
+            }
+          }
+
+          list2.clear();
+        } else {
+          try {
+            synchronized (this) {
+              wait(1000);
+            }
+          } catch (InterruptedException ie) {
+          }
+        }
+      }
+    }
+  }
+
+  class MulticastReceiverThread extends Thread {
+    public MulticastReceiverThread() {
+      setDaemon(true);
+    }
+
+    public void run() {
+      active = true;
+
+      byte[] b = new byte[PACKET_LENGTH];
+      DatagramPacket p = new DatagramPacket(b, b.length);
+
+      while (active) {
+        try {
+          socket.receive(p);
+
+          //this string constructor which accepts a charset throws an exception if it is 
+          //null
+            if (encoding == null) {
+            handlerThread.append(
+              new String(p.getData(), 0, p.getLength()));
+          } else {
+            handlerThread.append(
+              new String(p.getData(), 0, p.getLength(), encoding));
+          }
+        } catch (SocketException se) {
+          //disconnected
+        } catch (IOException ioe) {
+          ioe.printStackTrace();
+        }
+      }
+
+      getLogger().debug("{}'s thread is ending.", MulticastReceiver.this.getName());
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/net/MulticastReceiverBeanInfo.java b/src/main/java/org/apache/log4j/net/MulticastReceiverBeanInfo.java
new file mode 100644
index 0000000..4dec14c
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/MulticastReceiverBeanInfo.java
@@ -0,0 +1,51 @@
+/*
+ * 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.net;
+
+import java.beans.PropertyDescriptor;
+import java.beans.SimpleBeanInfo;
+
+
+/**
+ * BeanInfo class for the meta-data of the MulticastReceiver.
+ *
+ * @author Paul Smith <psmith@apache.org>
+ *
+ */
+public class MulticastReceiverBeanInfo extends SimpleBeanInfo {
+
+    /* (non-Javadoc)
+     * @see java.beans.BeanInfo#getPropertyDescriptors()
+     */
+    public PropertyDescriptor[] getPropertyDescriptors() {
+
+        try {
+
+            return new PropertyDescriptor[] {
+                new PropertyDescriptor("name", MulticastReceiver.class),
+                new PropertyDescriptor("address", MulticastReceiver.class),
+                new PropertyDescriptor("port", MulticastReceiver.class),
+                new PropertyDescriptor("threshold", MulticastReceiver.class),
+                new PropertyDescriptor("decoder", MulticastReceiver.class),
+                new PropertyDescriptor("advertiseViaMulticastDNS", MulticastReceiver.class),
+            };
+        } catch (Exception e) {
+        }
+
+        return null;
+    }
+}
diff --git a/src/main/java/org/apache/log4j/net/NetworkBased.java b/src/main/java/org/apache/log4j/net/NetworkBased.java
new file mode 100644
index 0000000..9c5153f
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/NetworkBased.java
@@ -0,0 +1,39 @@
+/*
+ * 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.net;
+
+/**
+ * The parent of all the Network based interfaces.
+ *
+ * @author Paul Smith (psmith@apache.org)
+ *
+ */
+public interface NetworkBased {
+
+    /**
+     * Get name.
+     * @return name.
+     */
+  String getName();
+
+    /**
+     * Get if item is active.
+     * @return if true, item is active.
+     */
+  boolean isActive();
+}
diff --git a/src/main/java/org/apache/log4j/net/PortBased.java b/src/main/java/org/apache/log4j/net/PortBased.java
new file mode 100644
index 0000000..c7c1f97
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/PortBased.java
@@ -0,0 +1,34 @@
+/*
+ * 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.net;
+
+
+/**
+ * Net based entities that 'work with' a Port should consider implementing this
+ * interface so that they can be treated generically.
+ *
+ * @author Paul Smith (psmith@apache.org)
+ *
+ */
+public interface PortBased extends NetworkBased {
+  /**
+   * Returns the Port # that this net based thing is using.
+   * @return int port number
+   */
+  int getPort();
+}
diff --git a/src/main/java/org/apache/log4j/net/SocketHubReceiver.java b/src/main/java/org/apache/log4j/net/SocketHubReceiver.java
new file mode 100644
index 0000000..85058e8
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/SocketHubReceiver.java
@@ -0,0 +1,409 @@
+/*
+ * 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.net;
+
+import org.apache.log4j.plugins.Plugin;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.LoggerRepository;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+  SocketHubReceiver receives a remote logging event on a configured
+  socket and "posts" it to a LoggerRepository as if the event was
+  generated locally. This class is designed to receive events from
+  the SocketHubAppender class (or classes that send compatible events).
+
+  <p>Once the event has been "posted", it will be handled by the
+  appenders currently configured in the LoggerRespository.
+
+  @author Mark Womack
+  @author Ceki G&uuml;lc&uuml;
+  @author Paul Smith (psmith@apache.org)
+*/
+public class SocketHubReceiver
+extends Receiver implements SocketNodeEventListener, PortBased {
+
+    /**
+     * Default reconnection delay.
+     */
+  static final int DEFAULT_RECONNECTION_DELAY   = 30000;
+
+    /**
+     * Host.
+     */
+  protected String host;
+
+    /**
+     * Port.
+     */
+  protected int port;
+    /**
+     * Reconnection delay.
+     */
+  protected int reconnectionDelay = DEFAULT_RECONNECTION_DELAY;
+
+  /**
+   * The MulticastDNS zone advertised by a SocketHubReceiver
+   */
+  public static final String ZONE = "_log4j_obj_tcpconnect_receiver.local.";
+
+    /**
+     * Active.
+     */
+  protected boolean active = false;
+
+    /**
+     * Connector.
+     */
+  protected Connector connector;
+
+    /**
+     * Socket.
+     */
+  protected SocketNode13 socketNode;
+
+    /**
+     * Listener list.
+     */
+  private List listenerList = Collections.synchronizedList(new ArrayList());
+
+  private boolean advertiseViaMulticastDNS;
+  private ZeroConfSupport zeroConf;
+
+    /**
+     * Create new instance.
+     */
+  public SocketHubReceiver() {
+     super();
+  }
+
+    /**
+     * Create new instance.
+     * @param h host
+     * @param p port
+     */
+  public SocketHubReceiver(final String h,
+                           final int p) {
+    super();
+    host = h;
+    port = p;
+  }
+
+    /**
+     * Create new instance.
+     * @param h host
+     * @param p port
+     * @param repo logger repository
+     */
+  public SocketHubReceiver(final String h,
+                           final int p,
+                           final LoggerRepository repo) {
+    super();
+    host = h;
+    port = p;
+    repository = repo;
+  }
+
+  /**
+   * Adds a SocketNodeEventListener to this receiver to be notified
+   * of SocketNode events.
+   * @param l listener
+   */
+  public void addSocketNodeEventListener(final SocketNodeEventListener l) {
+    listenerList.add(l);
+  }
+
+  /**
+   * Removes a specific SocketNodeEventListener from this instance
+   * so that it will no  longer be notified of SocketNode events.
+   * @param l listener
+   */
+  public void removeSocketNodeEventListener(
+          final SocketNodeEventListener l) {
+    listenerList.remove(l);
+  }
+
+  /**
+    Get the remote host to connect to for logging events.
+    @return host
+   */
+  public String getHost() {
+    return host;
+  }
+
+  /**
+   * Configures the Host property, this will require activateOptions
+   * to be called for this to take effect.
+   * @param remoteHost address of remote host.
+   */
+  public void setHost(final String remoteHost) {
+    this.host = remoteHost;
+  }
+  /**
+    Set the remote host to connect to for logging events.
+   Equivalent to setHost.
+   @param remoteHost address of remote host.
+   */
+  public void setPort(final String remoteHost) {
+    host = remoteHost;
+  }
+
+  /**
+    Get the remote port to connect to for logging events.
+   @return port
+   */
+  public int getPort() {
+    return port;
+  }
+
+  /**
+    Set the remote port to connect to for logging events.
+    @param p port
+   */
+  public void setPort(final int p) {
+    this.port = p;
+  }
+
+  /**
+     The <b>ReconnectionDelay</b> option takes a positive integer
+     representing the number of milliseconds to wait between each
+     failed connection attempt to the server. The default value of
+     this option is 30000 which corresponds to 30 seconds.
+
+     <p>Setting this option to zero turns off reconnection
+     capability.
+   @param delay milliseconds to wait or zero to not reconnect.
+   */
+  public void setReconnectionDelay(final int delay) {
+    int oldValue = this.reconnectionDelay;
+    this.reconnectionDelay = delay;
+    firePropertyChange("reconnectionDelay", oldValue, this.reconnectionDelay);
+  }
+
+  /**
+     Returns value of the <b>ReconnectionDelay</b> option.
+   @return value of reconnection delay option.
+   */
+  public int getReconnectionDelay() {
+    return reconnectionDelay;
+  }
+
+  /**
+   * Returns true if the receiver is the same class and they are
+   * configured for the same properties, and super class also considers
+   * them to be equivalent. This is used by PluginRegistry when determining
+   * if the a similarly configured receiver is being started.
+   *
+   * @param testPlugin The plugin to test equivalency against.
+   * @return boolean True if the testPlugin is equivalent to this plugin.
+   */
+  public boolean isEquivalent(final Plugin testPlugin) {
+    if (testPlugin != null && testPlugin instanceof SocketHubReceiver) {
+      SocketHubReceiver sReceiver = (SocketHubReceiver) testPlugin;
+
+      return (port == sReceiver.getPort()
+              && host.equals(sReceiver.getHost())
+              && reconnectionDelay == sReceiver.getReconnectionDelay()
+              && super.isEquivalent(testPlugin));
+    }
+    return false;
+  }
+
+  /**
+    Sets the flag to indicate if receiver is active or not.
+   @param b new value
+   */
+  protected synchronized void setActive(final boolean b) {
+    active = b;
+  }
+
+  /**
+    Starts the SocketReceiver with the current options. */
+  public void activateOptions() {
+    if (!isActive()) {
+      setActive(true);
+      if (advertiseViaMulticastDNS) {
+        zeroConf = new ZeroConfSupport(ZONE, port, getName());
+        zeroConf.advertise();
+      }
+
+      fireConnector(false);
+    }
+  }
+
+  /**
+    Called when the receiver should be stopped. Closes the socket */
+  public synchronized void shutdown() {
+    // mark this as no longer running
+    active = false;
+
+    // close the socket
+    try {
+      if (socketNode != null) {
+        socketNode.close();
+        socketNode = null;
+      }
+    } catch (Exception e) {
+      getLogger().info("Excpetion closing socket", e);
+      // ignore for now
+    }
+
+    // stop the connector
+    if (connector != null) {
+      connector.interrupted = true;
+      connector = null;  // allow gc
+    }
+    if (advertiseViaMulticastDNS) {
+        zeroConf.unadvertise();
+    }
+  }
+
+  /**
+    Listen for a socketClosedEvent from the SocketNode. Reopen the
+    socket if this receiver is still active.
+   @param e exception not used.
+   */
+  public void socketClosedEvent(final Exception e) {
+    // if it is a non-normal closed event
+    // we clear the connector object here
+    // so that it actually does reconnect if the
+    // remote socket dies.
+    if (e != null) {
+      connector = null;
+      fireConnector(true);
+    }
+  }
+
+    /**
+     * Fire connectors.
+     * @param isReconnect true if reconnect.
+     */
+  private synchronized void fireConnector(final boolean isReconnect) {
+    if (active && connector == null) {
+      getLogger().debug("Starting a new connector thread.");
+      connector = new Connector(isReconnect);
+      connector.setDaemon(true);
+      connector.setPriority(Thread.MIN_PRIORITY);
+      connector.start();
+    }
+  }
+
+    /**
+     * Set socket.
+     * @param newSocket new value for socket.
+     */
+  private synchronized void setSocket(final Socket newSocket) {
+    connector = null;
+    socketNode = new SocketNode13(newSocket, this);
+    socketNode.addSocketNodeEventListener(this);
+
+    synchronized (listenerList) {
+        for (Iterator iter = listenerList.iterator(); iter.hasNext();) {
+            SocketNodeEventListener listener =
+                    (SocketNodeEventListener) iter.next();
+            socketNode.addSocketNodeEventListener(listener);
+        }
+    }
+    new Thread(socketNode).start();
+  }
+
+  public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
+      this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
+  }
+
+  public boolean isAdvertiseViaMulticastDNS() {
+      return advertiseViaMulticastDNS;
+  }
+
+  /**
+   The Connector will reconnect when the server becomes available
+   again.  It does this by attempting to open a new connection every
+   <code>reconnectionDelay</code> milliseconds.
+
+   <p>It stops trying whenever a connection is established. It will
+   restart to try reconnect to the server when previpously open
+   connection is droppped.
+
+   @author  Ceki G&uuml;lc&uuml;
+   */
+  private final class Connector extends Thread {
+
+      /**
+       * Interruption status.
+       */
+    boolean interrupted = false;
+      /**
+       * If true, then delay on next iteration.
+       */
+    boolean doDelay;
+
+      /**
+       * Create new instance.
+       * @param isReconnect true if reconnecting.
+       */
+    public Connector(final boolean isReconnect) {
+      super();
+      doDelay = isReconnect;
+    }
+
+      /**
+       * Attempt to connect until interrupted.
+       */
+    public void run() {
+      while (!interrupted) {
+        try {
+          if (doDelay) {
+            getLogger().debug("waiting for " + reconnectionDelay
+              + " milliseconds before reconnecting.");
+            sleep(reconnectionDelay);
+          }
+          doDelay = true;
+          getLogger().debug("Attempting connection to " + host);
+          Socket s = new Socket(host, port);
+          setSocket(s);
+          getLogger().debug(
+                  "Connection established. Exiting connector thread.");
+          break;
+        } catch (InterruptedException e) {
+          getLogger().debug("Connector interrupted. Leaving loop.");
+          return;
+        } catch (java.net.ConnectException e) {
+          getLogger().debug("Remote host {} refused connection.", host);
+        } catch (IOException e) {
+          getLogger().debug("Could not connect to {}. Exception is {}.",
+                  host, e);
+        }
+      }
+    }
+  }
+
+    /**
+     * This method does nothing.
+     * @param remoteInfo remote info.
+     */
+  public void socketOpened(final String remoteInfo) {
+
+    // This method does nothing.
+  }
+}
diff --git a/src/main/java/org/apache/log4j/net/SocketNode13.java b/src/main/java/org/apache/log4j/net/SocketNode13.java
new file mode 100644
index 0000000..e27c68e
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/SocketNode13.java
@@ -0,0 +1,299 @@
+/*
+ * 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.net;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.helpers.Constants;
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.ComponentBase;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggingEvent;
+
+
+// Contributors:  Moses Hohman <mmhohman@rainbow.uchicago.edu>
+
+/**
+   Read {@link LoggingEvent} objects sent from a remote client using
+   Sockets (TCP). These logging events are logged according to local
+   policy, as if they were generated locally.
+
+   <p>For example, the socket node might decide to log events to a
+   local file and also resent them to a second socket node.
+
+    Implementation lifted from org.apache.log4j.net.SocketNode
+    in log4j 1.3 and renamed to prevent collision with
+    log4j 1.2 implementation.
+
+    @author  Ceki G&uuml;lc&uuml;
+    @author  Paul Smith (psmith@apache.org)
+
+
+*/
+public class SocketNode13 extends ComponentBase implements Runnable, Pauseable {
+
+    /**
+     * Paused state.
+     */
+  private boolean paused;
+    /**
+     * Closed state.
+     */
+  private boolean closed;
+    /**
+     * Socket.
+     */
+  private Socket socket;
+    /**
+     * Receiver.
+     */
+  private Receiver receiver;
+    /**
+     * List of listeners.
+     */
+  private List listenerList = Collections.synchronizedList(new ArrayList());
+
+
+
+  /**
+    Constructor for socket and logger repository.
+   @param s socket
+   @param hierarchy logger repository
+   */
+  public SocketNode13(final Socket s,
+                    final LoggerRepository hierarchy) {
+    super();
+    this.socket = s;
+    this.repository = hierarchy;
+  }
+
+  /**
+    Constructor for socket and receiver.
+   @param s socket
+   @param r receiver
+   */
+  public SocketNode13(final Socket s, final Receiver r) {
+    super();
+    this.socket = s;
+    this.receiver = r;
+  }
+
+  /**
+   * Set the event listener on this node.
+   *
+   * @deprecated Now supports mutliple listeners, this method
+   * simply invokes the removeSocketNodeEventListener() to remove
+   * the listener, and then readds it.
+   * @param l listener
+   */
+  public void setListener(final SocketNodeEventListener l) {
+    removeSocketNodeEventListener(l);
+    addSocketNodeEventListener(l);
+  }
+
+  /**
+   * Adds the listener to the list of listeners to be notified of the
+   * respective event.
+   * @param listener the listener to add to the list
+   */
+  public void addSocketNodeEventListener(
+          final SocketNodeEventListener listener) {
+    listenerList.add(listener);
+  }
+
+  /**
+   * Removes the registered Listener from this instances list of
+   * listeners.  If the listener has not been registered, then invoking
+   * this method has no effect.
+   *
+   * @param listener the SocketNodeEventListener to remove
+   */
+  public void removeSocketNodeEventListener(
+          final SocketNodeEventListener listener) {
+    listenerList.remove(listener);
+  }
+
+
+    /**
+     * Deserialize events from socket until interrupted.
+     */
+  public void run() {
+    LoggingEvent event;
+    Logger remoteLogger;
+    Exception listenerException = null;
+    ObjectInputStream ois = null;
+
+    try {
+      ois =
+        new ObjectInputStream(
+          new BufferedInputStream(socket.getInputStream()));
+    } catch (Exception e) {
+      ois = null;
+      listenerException = e;
+      getLogger().error("Exception opening ObjectInputStream to " + socket, e);
+    }
+
+    if (ois != null) {
+
+      String hostName = socket.getInetAddress().getHostName();
+      String remoteInfo = hostName + ":" + socket.getPort();
+
+      /**
+       * notify the listener that the socket has been
+       * opened and this SocketNode is ready and waiting
+       */
+      fireSocketOpened(remoteInfo);
+
+      try {
+        while (!isClosed()) {
+          // read an event from the wire
+          event = (LoggingEvent) ois.readObject();
+          event.setProperty(Constants.HOSTNAME_KEY, hostName);
+          // store the known remote info in an event property
+          event.setProperty("log4j.remoteSourceInfo", remoteInfo);
+
+          // if configured with a receiver, tell it to post the event
+          if (!isPaused() && !isClosed()) {
+            if ((receiver != null)) {
+              receiver.doPost(event);
+
+              // else post it via the hierarchy
+            } else {
+              // get a logger from the hierarchy. The name of the logger
+              // is taken to be the name contained in the event.
+              remoteLogger = repository.getLogger(event.getLoggerName());
+
+              //event.logger = remoteLogger;
+              // apply the logger-level filter
+              if (event
+                .getLevel()
+                .isGreaterOrEqual(remoteLogger.getEffectiveLevel())) {
+                // finally log the event as if was generated locally
+                remoteLogger.callAppenders(event);
+              }
+            }
+          } else {
+            //we simply discard this event.
+          }
+        }
+      } catch (java.io.EOFException e) {
+        getLogger().info("Caught java.io.EOFException closing connection.");
+        listenerException = e;
+      } catch (java.net.SocketException e) {
+        getLogger().info("Caught java.net.SocketException closing connection.");
+        listenerException = e;
+      } catch (IOException e) {
+        getLogger().info("Caught java.io.IOException: " + e);
+        getLogger().info("Closing connection.");
+        listenerException = e;
+      } catch (Exception e) {
+        getLogger().error("Unexpected exception. Closing connection.", e);
+        listenerException = e;
+      }
+    }
+
+    // close the socket
+    try {
+      if (ois != null) {
+        ois.close();
+      }
+    } catch (Exception e) {
+      //getLogger().info("Could not close connection.", e);
+    }
+
+    // send event to listener, if configured
+    if (listenerList.size() > 0 && !isClosed()) {
+      fireSocketClosedEvent(listenerException);
+    }
+  }
+
+  /**
+   * Notifies all registered listeners regarding the closing of the Socket.
+   * @param listenerException listener exception
+   */
+  private void fireSocketClosedEvent(final Exception listenerException) {
+    synchronized (listenerList) {
+        for (Iterator iter = listenerList.iterator(); iter.hasNext();) {
+            SocketNodeEventListener snel =
+                    (SocketNodeEventListener) iter.next();
+            if (snel != null) {
+                snel.socketClosedEvent(listenerException);
+            }
+        }
+    }
+  }
+
+  /**
+   * Notifies all registered listeners regarding the opening of a Socket.
+   * @param remoteInfo remote info
+   */
+  private void fireSocketOpened(final String remoteInfo) {
+    synchronized (listenerList) {
+        for (Iterator iter = listenerList.iterator(); iter.hasNext();) {
+            SocketNodeEventListener snel =
+                    (SocketNodeEventListener) iter.next();
+            if (snel != null) {
+                snel.socketOpened(remoteInfo);
+            }
+        }
+    }
+  }
+
+    /**
+     * Sets if node is paused.
+     * @param b new value
+     */
+  public void setPaused(final boolean b) {
+    this.paused = b;
+  }
+
+    /**
+     * Get if node is paused.
+     * @return true if pause.
+     */
+  public boolean isPaused() {
+    return this.paused;
+  }
+
+    /**
+     * Close the node and underlying socket
+     */
+  public void close() throws IOException {
+    getLogger().debug("closing socket");
+    this.closed = true;
+    socket.close();
+    fireSocketClosedEvent(null);
+  }
+  
+    /**
+     * Get if node is closed.
+     * @return true if closed.
+     */
+  public boolean isClosed() {
+    return this.closed;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/net/SocketNodeEventListener.java b/src/main/java/org/apache/log4j/net/SocketNodeEventListener.java
new file mode 100644
index 0000000..6d17602
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/SocketNodeEventListener.java
@@ -0,0 +1,43 @@
+/*
+ * 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.net;
+
+import java.util.EventListener;
+
+/**
+  Interface used to listen for {@link SocketNode} related
+  events. Clients register an instance of the interface and the
+  instance is called back when the various events occur.
+
+  @author Mark Womack
+  @author Paul Smith (psmith@apache.org)
+*/
+public interface SocketNodeEventListener extends EventListener {
+
+  /**
+   * Called when the SocketNode is created and begins awaiting data.
+   *  @param remoteInfo remote info
+   */
+  void socketOpened(String remoteInfo);
+
+  /**
+    Called when the socket the node was given has been closed.
+    @param e exception
+   */
+  void socketClosedEvent(Exception e);
+}
diff --git a/src/main/java/org/apache/log4j/net/SocketReceiver.java b/src/main/java/org/apache/log4j/net/SocketReceiver.java
new file mode 100644
index 0000000..9d4aac9
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/SocketReceiver.java
@@ -0,0 +1,479 @@
+/*
+ * 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.net;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Plugin;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggingEvent;
+
+
+/**
+  SocketReceiver receives a remote logging event on a configured
+  socket and "posts" it to a LoggerRepository as if the event was
+  generated locally. This class is designed to receive events from
+  the SocketAppender class (or classes that send compatible events).
+
+  <p>Once the event has been "posted", it will be handled by the
+  appenders currently configured in the LoggerRespository.
+
+  @author Mark Womack
+  @author Scott Deboy (sdeboy@apache.org)
+  @author Paul Smith (psmith@apache.org)
+*/
+public class SocketReceiver extends Receiver implements Runnable, PortBased,
+  Pauseable {
+    /**
+     * socket map.
+     */
+  private Map socketMap = new HashMap();
+    /**
+     * Paused.
+     */
+  private boolean paused;
+    /**
+     * Thread.
+     */
+  private Thread rThread;
+    /**
+     * Port.
+     */
+  protected int port;
+    /**
+     * Server socket.
+     */
+  private ServerSocket serverSocket;
+    /**
+     * Socket list.
+     */
+  private Vector socketList = new Vector();
+
+  /**
+   * The MulticastDNS zone advertised by a SocketReceiver
+   */
+  public static final String ZONE = "_log4j_obj_tcpaccept_receiver.local.";
+
+    /**
+     * Listener.
+     */
+  private SocketNodeEventListener listener = null;
+    /**
+     * Listeners.
+     */
+  private List listenerList = Collections.synchronizedList(new ArrayList());
+  private boolean advertiseViaMulticastDNS;
+  private ZeroConfSupport zeroConf;
+
+    /**
+     * Create new instance.
+     */
+  public SocketReceiver() {
+        super();
+  }
+
+    /**
+     * Create new instance.
+     * @param p port
+     */
+  public SocketReceiver(final int p) {
+    super();
+    port = p;
+  }
+
+    /**
+     * Create new instance.
+     * @param p port
+     * @param repo logger repository
+     */
+  public SocketReceiver(final int p, final LoggerRepository repo) {
+    super();
+    this.port = p;
+    repository = repo;
+  }
+
+    /** {@inheritDoc} */
+  public int getPort() {
+    return port;
+  }
+
+  /** {@inheritDoc} */
+  public void setPort(final int p) {
+    port = p;
+  }
+
+  /**
+   * Returns true if the receiver is the same class and they are
+   * configured for the same properties, and super class also considers
+   * them to be equivalent. This is used by PluginRegistry when determining
+   * if the a similarly configured receiver is being started.
+   *
+   * @param testPlugin The plugin to test equivalency against.
+   * @return boolean True if the testPlugin is equivalent to this plugin.
+   */
+  public boolean isEquivalent(final Plugin testPlugin) {
+    if ((testPlugin != null) && testPlugin instanceof SocketReceiver) {
+      SocketReceiver sReceiver = (SocketReceiver) testPlugin;
+
+      return (port == sReceiver.getPort() && super.isEquivalent(testPlugin));
+    }
+
+    return false;
+  }
+
+  /**
+    Starts the SocketReceiver with the current options. */
+  public void activateOptions() {
+    if (!isActive()) {
+      //      shutdown();
+      rThread = new Thread(this);
+      rThread.setDaemon(true);
+      rThread.start();
+      if (advertiseViaMulticastDNS) {
+        zeroConf = new ZeroConfSupport(ZONE, port, getName());
+        zeroConf.advertise();
+      }
+
+      active = true;
+    }
+  }
+
+  /**
+   * Called when the receiver should be stopped. Closes the
+   * server socket and all of the open sockets.
+   */
+  public synchronized void shutdown() {
+    getLogger().debug(getName() + " received shutdown request");
+
+    // mark this as no longer running
+    active = false;
+
+    if (rThread != null) {
+      rThread.interrupt();
+      rThread = null;
+    }
+    if (advertiseViaMulticastDNS) {
+        zeroConf.unadvertise();
+    }
+
+    doShutdown();
+  }
+
+  /**
+   * Does the actual shutting down by closing the server socket
+   * and any connected sockets that have been created.
+   */
+  private synchronized void doShutdown() {
+    active = false;
+
+    getLogger().debug(getName() + " doShutdown called");
+
+    // close the server socket
+    closeServerSocket();
+
+    // close all of the accepted sockets
+    closeAllAcceptedSockets();
+  }
+
+  /**
+   * Closes the server socket, if created.
+   */
+  private void closeServerSocket() {
+    getLogger().debug("{} closing server socket", getName());
+
+    try {
+      if (serverSocket != null) {
+        serverSocket.close();
+      }
+    } catch (Exception e) {
+      // ignore for now
+    }
+
+    serverSocket = null;
+  }
+
+  /**
+   * Closes all the connected sockets in the List.
+   */
+  private synchronized void closeAllAcceptedSockets() {
+    for (int x = 0; x < socketList.size(); x++) {
+      try {
+        ((Socket) socketList.get(x)).close();
+      } catch (Exception e) {
+        // ignore for now
+      }
+    }
+
+    // clear member variables
+    socketMap.clear();
+    socketList.clear();
+  }
+
+  /**
+    Sets the flag to indicate if receiver is active or not.
+   @param b new value
+   */
+  protected synchronized void setActive(final boolean b) {
+    active = b;
+  }
+
+  public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
+      this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
+  }
+
+  public boolean isAdvertiseViaMulticastDNS() {
+      return advertiseViaMulticastDNS;
+  }
+
+  /**
+    Loop, accepting new socket connections. */
+  public void run() {
+    /**
+     * Ensure we start fresh.
+     */
+    closeServerSocket();
+    closeAllAcceptedSockets();
+
+    // start the server socket
+    try {
+      serverSocket = new ServerSocket(port);
+    } catch (Exception e) {
+      getLogger().error(
+        "error starting SocketReceiver (" + this.getName()
+        + "), receiver did not start", e);
+      active = false;
+
+      return;
+    }
+
+    Socket socket = null;
+
+    try {
+      getLogger().debug("in run-about to enter while not interrupted loop");
+
+      active = true;
+
+      while (!rThread.isInterrupted()) {
+        // if we have a socket, start watching it
+        if (socket != null) {
+          getLogger().debug(
+                  "socket not null - creating and starting socketnode");
+          socketList.add(socket);
+
+          SocketNode13 node = new SocketNode13(socket, this);
+          synchronized (listenerList) {
+            for (Iterator iter = listenerList.iterator();
+                 iter.hasNext();) {
+                SocketNodeEventListener l =
+                        (SocketNodeEventListener) iter.next();
+                node.addSocketNodeEventListener(l);
+            }
+          }
+          socketMap.put(socket, node);
+          new Thread(node).start();
+          socket = null;
+        }
+
+        getLogger().debug("waiting to accept socket");
+
+        // wait for a socket to open, then loop to start it
+        socket = serverSocket.accept();
+        getLogger().debug("accepted socket");
+      }
+    } catch (Exception e) {
+      getLogger().warn(
+        "exception while watching socket server in SocketReceiver ("
+        + this.getName() + "), stopping");
+    }
+
+    getLogger().debug("{} has exited the not interrupted loop", getName());
+
+    // socket not watched because we a no longer running
+    // so close it now.
+    if (socket != null) {
+      try {
+        socket.close();
+      } catch (IOException e1) {
+        getLogger().warn("socket exception caught - socket closed");
+      }
+    }
+
+    getLogger().debug("{} is exiting main run loop", getName());
+  }
+
+  /**
+   * Returns a Vector of SocketDetail representing the IP/Domain name
+   * of the currently connected sockets that this receiver has
+   * been responsible for creating.
+   * @return Vector of SocketDetails
+   */
+  public Vector getConnectedSocketDetails() {
+    Vector details = new Vector(socketList.size());
+
+    for (Enumeration enumeration = socketList.elements();
+         enumeration.hasMoreElements();
+            ) {
+      Socket socket = (Socket) enumeration.nextElement();
+      details.add(
+        new SocketDetail(socket, (SocketNode13) socketMap.get(socket)));
+    }
+
+    return details;
+  }
+
+  /**
+   * Returns the currently configured SocketNodeEventListener that
+   * will be automatically set for each SocketNode created.
+   * @return SocketNodeEventListener currently configured
+   *
+   * @deprecated This receiver now supports multiple listeners
+   */
+  public SocketNodeEventListener getListener() {
+    return listener;
+  }
+
+  /**
+   * Adds the listener to the list of listeners to be notified of the
+   * respective event.
+   * @param l the listener to add to the list
+   */
+  public void addSocketNodeEventListener(
+          final SocketNodeEventListener l) {
+    listenerList.add(l);
+  }
+
+  /**
+   * Removes the registered Listener from this instances list of
+   * listeners.  If the listener has not been registered, then invoking
+   * this method has no effect.
+   *
+   * @param l the SocketNodeEventListener to remove
+   */
+  public void removeSocketNodeEventListener(
+          final SocketNodeEventListener l) {
+    listenerList.remove(l);
+  }
+
+  /**
+   * Sets the SocketNodeEventListener that will be used for each
+   * created SocketNode.
+   * @param l the listener to set on each creation of a SocketNode
+   * @deprecated This receiver now supports multiple listeners and
+   * so this method simply removes the listener (if there already)
+   * and readds it to the list.
+   *
+   * The passed listener will also be returned via the getListener()
+   * method still, but this is also deprecated
+   */
+  public void setListener(final SocketNodeEventListener l) {
+    removeSocketNodeEventListener(l);
+    addSocketNodeEventListener(l);
+    this.listener = l;
+  }
+
+    /** {@inheritDoc} */
+  public boolean isPaused() {
+    return paused;
+  }
+
+    /** {@inheritDoc} */
+  public void setPaused(final boolean b) {
+    paused = b;
+  }
+
+    /**
+     * Socket detail.
+     */
+  private static final class SocketDetail implements AddressBased, PortBased,
+    Pauseable {
+      /**
+       * Address.
+       */
+    private String address;
+      /**
+       * Port.
+       */
+    private int port;
+      /**
+       * Socket node.
+       */
+    private SocketNode13 socketNode;
+
+      /**
+       * Create new instance.
+       * @param socket socket
+       * @param node socket node
+       */
+    private SocketDetail(final Socket socket,
+                         final SocketNode13 node) {
+      super();
+      this.address = socket.getInetAddress().getHostName();
+      this.port = socket.getPort();
+      this.socketNode = node;
+    }
+
+      /** {@inheritDoc} */
+    public String getAddress() {
+      return address;
+    }
+
+      /** {@inheritDoc} */
+    public int getPort() {
+      return port;
+    }
+
+      /** {@inheritDoc} */
+    public String getName() {
+      return "Socket";
+    }
+
+      /** {@inheritDoc} */
+    public boolean isActive() {
+      return true;
+    }
+
+      /** {@inheritDoc} */
+    public boolean isPaused() {
+      return socketNode.isPaused();
+    }
+
+      /** {@inheritDoc} */
+    public void setPaused(final boolean b) {
+      socketNode.setPaused(b);
+    }
+  }
+    /** {@inheritDoc} */
+  public void doPost(final LoggingEvent event) {
+    if (!isPaused()) {
+      super.doPost(event);
+    }
+  }
+
+}
diff --git a/src/main/java/org/apache/log4j/net/UDPAppender.java b/src/main/java/org/apache/log4j/net/UDPAppender.java
new file mode 100644
index 0000000..55bc6f1
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/UDPAppender.java
@@ -0,0 +1,330 @@
+/*
+ * 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.net;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.helpers.Constants;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.xml.XMLLayout;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+
+
+/**
+ *  Sends log information as a UDP datagrams.
+ *
+ *  <p>The UDPAppender is meant to be used as a diagnostic logging tool
+ *  so that logging can be monitored by a simple UDP client.
+ *
+ *  <p>Messages are not sent as LoggingEvent objects but as text after
+ *  applying the designated Layout.
+ *
+ *  <p>The port and remoteHost properties can be set in configuration properties.
+ *  By setting the remoteHost to a broadcast address any number of clients can
+ *  listen for log messages.
+ *
+ *  <p>This was inspired and really extended/copied from {@link SocketAppender}.
+ *  Please see the docs for the proper credit to the authors of that class.
+ *
+ *  @author  <a href="mailto:kbrown@versatilesolutions.com">Kevin Brown</a>
+ *  @author Scott Deboy <sdeboy@apache.org>
+ */
+public class UDPAppender extends AppenderSkeleton implements PortBased{
+  /**
+    * The default port number for the UDP packets, 9991.
+  */
+  public static final int DEFAULT_PORT = 9991;
+
+  /**
+     We remember host name as String in addition to the resolved
+     InetAddress so that it can be returned via getOption().
+  */
+  String hostname;
+  String remoteHost;
+  String application;
+  String encoding;
+  InetAddress address;
+  int port = DEFAULT_PORT;
+  DatagramSocket outSocket;
+
+  /**
+   * The MulticastDNS zone advertised by a UDPAppender
+   */
+  public static final String ZONE = "_log4j_xml_udp_appender.local.";
+
+  // if there is something irrecoverably wrong with the settings, there is no
+  // point in sending out packeets.
+  boolean inError = false;
+  private boolean advertiseViaMulticastDNS;
+  private ZeroConfSupport zeroConf;
+
+    public UDPAppender() {
+      super(false);
+  }
+
+  /**
+     Sends UDP packets to the <code>address</code> and <code>port</code>.
+  */
+  public UDPAppender(final InetAddress address, final int port) {
+    super(false);
+    this.address = address;
+    this.remoteHost = address.getHostName();
+    this.port = port;
+    activateOptions();
+  }
+
+  /**
+     Sends UDP packets to the <code>address</code> and <code>port</code>.
+  */
+  public UDPAppender(final String host, final int port) {
+    super(false);
+    this.port = port;
+    this.address = getAddressByName(host);
+    this.remoteHost = host;
+    activateOptions();
+  }
+
+  /**
+     Open the UDP sender for the <b>RemoteHost</b> and <b>Port</b>.
+  */
+  public void activateOptions() {
+    try {
+      hostname = InetAddress.getLocalHost().getHostName();
+    } catch (UnknownHostException uhe) {
+      try {
+        hostname = InetAddress.getLocalHost().getHostAddress();
+      } catch (UnknownHostException uhe2) {
+        hostname = "unknown";
+      }
+    }
+
+    //allow system property of application to be primary
+    if (application == null) {
+      application = System.getProperty(Constants.APPLICATION_KEY);
+    } else {
+      if (System.getProperty(Constants.APPLICATION_KEY) != null) {
+        application = application + "-" + System.getProperty(Constants.APPLICATION_KEY);
+      }
+    }
+
+    if(remoteHost != null) {
+      address = getAddressByName(remoteHost);
+      connect(address, port);
+    } else {
+      String err = "The RemoteHost property is required for SocketAppender named "+ name;
+      LogLog.error(err);
+      throw new IllegalStateException(err);
+    }
+
+    if (layout == null) {
+        layout = new XMLLayout();
+    }
+
+    if (advertiseViaMulticastDNS) {
+      zeroConf = new ZeroConfSupport(ZONE, port, getName());
+      zeroConf.advertise();
+    }
+
+    super.activateOptions();
+  }
+
+  /**
+     Close this appender.
+     <p>This will mark the appender as closed and
+     call then {@link #cleanUp} method.
+  */
+  public synchronized void close() {
+    if (closed) {
+      return;
+    }
+
+    if (advertiseViaMulticastDNS) {
+      zeroConf.unadvertise();
+    }
+      
+    this.closed = true;
+    cleanUp();
+  }
+
+  /**
+     Close the UDP Socket and release the underlying
+     connector thread if it has been created
+   */
+  public void cleanUp() {
+    if (outSocket != null) {
+      try {
+        outSocket.close();
+      } catch (Exception e) {
+        LogLog.error("Could not close outSocket.", e);
+      }
+
+      outSocket = null;
+    }
+  }
+
+  void connect(InetAddress address, int port) {
+    if (this.address == null) {
+      return;
+    }
+
+    try {
+      // First, close the previous connection if any.
+      cleanUp();
+      outSocket = new DatagramSocket();
+      outSocket.connect(address, port);
+    } catch (IOException e) {
+      LogLog.error(
+        "Could not open UDP Socket for sending.", e);
+      inError = true;
+    }
+  }
+
+  public void append(LoggingEvent event) {
+    if(inError) {
+      return;
+    }
+    
+    if (event == null) {
+      return;
+    }
+
+    if (address == null) {
+      return;
+    }
+
+    if (outSocket != null) {
+      event.setProperty(Constants.HOSTNAME_KEY, hostname);
+      if (application != null) {
+        event.setProperty(Constants.APPLICATION_KEY, application);
+      }
+
+      try {
+        StringBuffer buf = new StringBuffer(layout.format(event));
+
+        byte[] payload;
+        if(encoding == null) {
+          payload = buf.toString().getBytes();
+        } else {
+          payload = buf.toString().getBytes(encoding);
+        }
+
+        DatagramPacket dp =
+           new DatagramPacket(payload, payload.length, address, port);
+        outSocket.send(dp);
+      } catch (IOException e) {
+        outSocket = null;
+        LogLog.warn("Detected problem with UDP connection: " + e);
+      }
+    }
+  }
+
+  public boolean isActive() {
+    return !inError;
+  }
+  
+  InetAddress getAddressByName(String host) {
+    try {
+      return InetAddress.getByName(host);
+    } catch (Exception e) {
+      LogLog.error("Could not find address of [" + host + "].", e);
+      return null;
+    }
+  }
+
+  /**
+     The UDPAppender uses layouts. Hence, this method returns
+     <code>true</code>.
+  */
+  public boolean requiresLayout() {
+    return true;
+  }
+
+  /**
+     The <b>RemoteHost</b> option takes a string value which should be
+     the host name or ipaddress to send the UDP packets.
+   */
+  public void setRemoteHost(String host) {
+    remoteHost = host;
+  }
+
+  /**
+     Returns value of the <b>RemoteHost</b> option.
+   */
+  public String getRemoteHost() {
+    return remoteHost;
+  }
+
+  /**
+     The <b>App</b> option takes a string value which should be the name of the application getting logged.
+     If property was already set (via system property), don't set here.
+   */
+  public void setApplication(String app) {
+    this.application = app;
+  }
+
+  /**
+     Returns value of the <b>App</b> option.
+   */
+  public String getApplication() {
+    return application;
+  }
+
+  /**
+     The <b>Encoding</b> option specifies how the bytes are encoded.  If this option is not specified, 
+     the System encoding is used.
+   */
+  public void setEncoding(String encoding) {
+    this.encoding = encoding;
+  }
+
+  /**
+     Returns value of the <b>Encoding</b> option.
+   */
+  public String getEncoding() {
+    return encoding;
+  }
+
+    /**
+     The <b>Port</b> option takes a positive integer representing
+     the port where UDP packets will be sent.
+   */
+  public void setPort(int port) {
+    this.port = port;
+  }
+
+  /**
+     Returns value of the <b>Port</b> option.
+   */
+  public int getPort() {
+    return port;
+  }
+
+  public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
+    this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
+  }
+
+  public boolean isAdvertiseViaMulticastDNS() {
+    return advertiseViaMulticastDNS;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/net/UDPReceiver.java b/src/main/java/org/apache/log4j/net/UDPReceiver.java
new file mode 100644
index 0000000..a8c7375
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/UDPReceiver.java
@@ -0,0 +1,281 @@
+/*
+ * 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.net;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.Decoder;
+import org.apache.log4j.spi.LoggingEvent;
+
+
+/**
+ *  Receive LoggingEvents encoded with an XMLLayout, convert the XML data to a
+ *  LoggingEvent and post the LoggingEvent.
+ *
+ *  @author Scott Deboy <sdeboy@apache.org>
+ *
+ */
+public class UDPReceiver extends Receiver implements PortBased, Pauseable {
+  private static final int PACKET_LENGTH = 16384;
+  private UDPReceiverThread receiverThread;
+  private String encoding;
+
+  //default to log4j xml decoder
+  private String decoder = "org.apache.log4j.xml.XMLDecoder";
+  private Decoder decoderImpl;
+  protected boolean paused;
+  private transient boolean closed = false;
+  private int port;
+  private DatagramSocket socket;
+  UDPHandlerThread handlerThread;
+  private boolean advertiseViaMulticastDNS;
+  private ZeroConfSupport zeroConf;
+
+  /**
+   * The MulticastDNS zone advertised by a UDPReceiver
+   */
+  public static final String ZONE = "_log4j_xml_udp_receiver.local.";
+
+
+    public int getPort() {
+    return port;
+  }
+
+  public void setPort(int port) {
+    this.port = port;
+  }
+
+  /**
+   * The <b>Encoding</b> option specifies how the bytes are encoded.  If this 
+   * option is not specified, the system encoding will be used.
+   * */
+  public void setEncoding(String encoding) {
+    this.encoding = encoding;
+  }
+
+  /**
+   * Returns value of the <b>Encoding</b> option.
+   */
+  public String getEncoding() {
+    return encoding;
+  }
+
+  public String getDecoder() {
+    return decoder;
+  }
+
+  public void setDecoder(String decoder) {
+    this.decoder = decoder;
+  }
+
+  public boolean isPaused() {
+    return paused;
+  }
+
+  public void setPaused(boolean b) {
+    paused = b;
+  }
+
+  public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
+    this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
+  }
+
+  public boolean isAdvertiseViaMulticastDNS() {
+    return advertiseViaMulticastDNS;
+  }
+
+  public synchronized void shutdown() {
+    if(closed == true) {
+      return;
+    }
+    closed = true;
+    active = false;
+    // Closing the datagram socket will unblock the UDPReceiverThread if it is
+    // was waiting to receive data from the socket.
+    if (socket != null) {
+    	socket.close();
+    }
+
+    if (advertiseViaMulticastDNS) {
+      zeroConf.unadvertise();
+    }
+      
+    try {
+      if(handlerThread != null) {
+      	handlerThread.close();
+        handlerThread.join();
+      }
+      if(receiverThread != null) {
+        receiverThread.join();
+      }
+    } catch(InterruptedException ie) {
+    }
+  }
+
+  /**
+    Returns true if this receiver is active. */
+//  public synchronized boolean isActive() {
+//    return isActive;
+//}
+
+  public void activateOptions() {
+    try {
+      Class c = Class.forName(decoder);
+      Object o = c.newInstance();
+
+      if (o instanceof Decoder) {
+        this.decoderImpl = (Decoder) o;
+      }
+    } catch (ClassNotFoundException cnfe) {
+      getLogger().warn("Unable to find decoder", cnfe);
+    } catch (IllegalAccessException iae) {
+      getLogger().warn("Could not construct decoder", iae);
+    } catch (InstantiationException ie) {
+      getLogger().warn("Could not construct decoder", ie);
+    }
+
+    try {
+      socket = new DatagramSocket(port);
+      receiverThread = new UDPReceiverThread();
+      receiverThread.start();
+      handlerThread = new UDPHandlerThread();
+      handlerThread.start();
+      if (advertiseViaMulticastDNS) {
+        zeroConf = new ZeroConfSupport(ZONE, port, getName());
+        zeroConf.advertise();
+      }
+      active = true;
+    } catch (IOException ioe) {
+      ioe.printStackTrace();
+    }
+  }
+
+  class UDPHandlerThread extends Thread {
+    private List list = new ArrayList();
+
+    public UDPHandlerThread() {
+      setDaemon(true);
+    }
+
+    public void append(String data) {
+      synchronized (list) {
+        list.add(data);
+        list.notify();
+      }
+    }
+
+    /**
+     * Allow the UDPHandlerThread to wakeup and exit gracefully.
+     */
+    void close() {
+      synchronized(list) {
+      	list.notify();
+      }
+    }
+
+    public void run() {
+      ArrayList list2 = new ArrayList();
+
+      while (!UDPReceiver.this.closed) {
+        synchronized (list) {
+          try {
+            while (!UDPReceiver.this.closed && list.size() == 0) {
+              list.wait(300);
+            }
+
+            if (list.size() > 0) {
+              list2.addAll(list);
+              list.clear();
+            }
+          } catch (InterruptedException ie) {
+          }
+        }
+
+        if (list2.size() > 0) {
+          Iterator iter = list2.iterator();
+
+          while (iter.hasNext()) {
+            String data = (String) iter.next();
+            List v = decoderImpl.decodeEvents(data);
+
+            if (v != null) {
+              Iterator eventIter = v.iterator();
+
+              while (eventIter.hasNext()) {
+                if (!isPaused()) {
+                  doPost((LoggingEvent) eventIter.next());
+                }
+              }
+            }
+          }
+
+          list2.clear();
+        } else {
+          try {
+            synchronized (this) {
+              wait(1000);
+            }
+          } catch (InterruptedException ie) {
+          }
+        }
+      } // while
+      getLogger().debug(UDPReceiver.this.getName()+ "'s handler thread is exiting");
+    } // run
+  } // UDPHandlerThread
+
+  class UDPReceiverThread extends Thread {
+    public UDPReceiverThread() {
+      setDaemon(true);
+    }
+    
+    public void run() {
+      byte[] b = new byte[PACKET_LENGTH];
+      DatagramPacket p = new DatagramPacket(b, b.length);
+
+      while (!UDPReceiver.this.closed) {
+        try {
+          socket.receive(p);
+          
+          //this string constructor which accepts a charset throws an exception if it is 
+          //null
+          if (encoding == null) {
+            handlerThread.append(
+              new String(p.getData(), 0, p.getLength()));
+          } else {
+            handlerThread.append(
+              new String(p.getData(), 0, p.getLength(), encoding));
+          }
+        } catch (SocketException se) {
+          //disconnected
+        } catch (IOException ioe) {
+          ioe.printStackTrace();
+        }
+      }
+
+      //LogLog.debug(UDPReceiver.this.getName() + "'s thread is ending.");
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/net/XMLSocketNode.java b/src/main/java/org/apache/log4j/net/XMLSocketNode.java
new file mode 100644
index 0000000..95ab638
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/XMLSocketNode.java
@@ -0,0 +1,205 @@
+/*
+ * 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.net;
+
+import org.apache.log4j.*;
+import org.apache.log4j.helpers.Constants;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.net.Socket;
+
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+   Read {@link LoggingEvent} objects sent from a remote client using XML over
+   Sockets (TCP). These logging events are logged according to local
+   policy, as if they were generated locally.
+
+   <p>For example, the socket node might decide to log events to a
+   local file and also resent them to a second socket node.
+
+    @author  Scott Deboy <sdeboy@apache.org>;
+
+    @since 0.8.4
+*/
+public class XMLSocketNode extends ComponentBase implements Runnable {
+  Socket socket;
+  Receiver receiver;
+  Decoder decoder;
+  SocketNodeEventListener listener;
+
+  /**
+    Constructor for socket and logger repository. */
+  public XMLSocketNode(
+    String decoder, Socket socket, LoggerRepository hierarchy) {
+    this.repository = hierarchy;
+    try {
+      Class c = Class.forName(decoder);
+      Object o = c.newInstance();
+
+      if (o instanceof Decoder) {
+        this.decoder = (Decoder) o;
+      }
+    } catch (ClassNotFoundException cnfe) {
+      getLogger().warn("Unable to find decoder", cnfe);
+    } catch (IllegalAccessException iae) {
+      getLogger().warn("Unable to construct decoder", iae);
+    } catch (InstantiationException ie) {
+      getLogger().warn("Unable to construct decoder", ie);
+    }
+
+    this.socket = socket;
+  }
+
+  /**
+    Constructor for socket and reciever. */
+  public XMLSocketNode(String decoder, Socket socket, Receiver receiver) {
+    try {
+      Class c = Class.forName(decoder);
+      Object o = c.newInstance();
+
+      if (o instanceof Decoder) {
+        this.decoder = (Decoder) o;
+      }
+    } catch (ClassNotFoundException cnfe) {
+      getLogger().warn("Unable to find decoder", cnfe);
+    } catch (IllegalAccessException iae) {
+      getLogger().warn("Unable to construct decoder", iae);
+    } catch (InstantiationException ie) {
+      getLogger().warn("Unable to construct decoder", ie);
+    }
+
+    this.socket = socket;
+    this.receiver = receiver;
+  }
+
+  /**
+    Set the event listener on this node. */
+  public void setListener(SocketNodeEventListener _listener) {
+    listener = _listener;
+  }
+
+  public void run() {
+    Logger remoteLogger;
+    Exception listenerException = null;
+    InputStream is = null;
+
+    if ((this.receiver == null) || (this.decoder == null)) {
+      is = null;
+      listenerException =
+        new Exception(
+          "No receiver or decoder provided.  Cannot process xml socket events");
+      getLogger().error(
+        "Exception constructing XML Socket Receiver", listenerException);
+    }
+
+    try {
+      is = socket.getInputStream();
+    } catch (Exception e) {
+      is = null;
+      listenerException = e;
+      getLogger().error("Exception opening ObjectInputStream to " + socket, e);
+    }
+
+    if (is != null) {
+        String hostName = socket.getInetAddress().getHostName();
+        String remoteInfo = hostName + ":" + socket.getPort();
+    	
+      try {
+        //read data from the socket
+        //it's up to the individual decoder to handle incomplete event data
+        while (true) {
+          byte[] b = new byte[1024];
+          int length = is.read(b);
+          if (length == -1) {
+            getLogger().info(
+              "no bytes read from stream - closing connection.");
+            break;
+          }
+          List v = decoder.decodeEvents(new String(b, 0, length));
+
+          if (v != null) {
+            Iterator iter = v.iterator();
+
+            while (iter.hasNext()) {
+              LoggingEvent e = (LoggingEvent) iter.next();
+              e.setProperty(Constants.HOSTNAME_KEY, hostName);
+
+              // store the known remote info in an event property
+              e.setProperty("log4j.remoteSourceInfo", remoteInfo);
+
+              // if configured with a receiver, tell it to post the event
+              if (receiver != null) {
+                receiver.doPost(e);
+
+                // else post it via the hierarchy
+              } else {
+                // get a logger from the hierarchy. The name of the logger
+                // is taken to be the name contained in the event.
+                remoteLogger = repository.getLogger(e.getLoggerName());
+
+                //event.logger = remoteLogger;
+                // apply the logger-level filter
+                if (
+                  e.getLevel().isGreaterOrEqual(
+                      remoteLogger.getEffectiveLevel())) {
+                  // finally log the event as if was generated locally
+                  remoteLogger.callAppenders(e);
+                }
+              }
+            }
+          }
+        }
+      } catch (java.io.EOFException e) {
+        getLogger().info("Caught java.io.EOFException closing connection.");
+        listenerException = e;
+      } catch (java.net.SocketException e) {
+        getLogger().info(
+          "Caught java.net.SocketException closing connection.");
+        listenerException = e;
+      } catch (IOException e) {
+        getLogger().info("Caught java.io.IOException: " + e);
+        getLogger().info("Closing connection.");
+        listenerException = e;
+      } catch (Exception e) {
+        getLogger().error("Unexpected exception. Closing connection.", e);
+        listenerException = e;
+      }
+    }
+
+    // close the socket
+    try {
+      if (is != null) {
+        is.close();
+      }
+    } catch (Exception e) {
+      //logger.info("Could not close connection.", e);
+    }
+
+    // send event to listener, if configured
+    if (listener != null) {
+      listener.socketClosedEvent(listenerException);
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/net/XMLSocketReceiver.java b/src/main/java/org/apache/log4j/net/XMLSocketReceiver.java
new file mode 100644
index 0000000..cd37dc4
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/XMLSocketReceiver.java
@@ -0,0 +1,316 @@
+/*
+ * 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.net;
+
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.List;
+import java.util.Vector;
+
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Plugin;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggingEvent;
+
+
+/**
+  XMLSocketReceiver receives a remote logging event via XML on a configured
+  socket and "posts" it to a LoggerRepository as if the event were
+  generated locally. This class is designed to receive events from
+  the XMLSocketAppender class (or classes that send compatible events).
+  <p>
+  This receiver supports log files created using log4j's XMLLayout, as well as java.util.logging
+  XMLFormatter (via the org.apache.log4j.spi.Decoder interface).
+  <p>
+  By default, log4j's XMLLayout is supported (no need to specify a decoder in that case).
+  <p>
+  To configure this receiver to support java.util.logging's XMLFormatter, specify a 'decoder' param
+  of org.apache.log4j.xml.UtilLoggingXMLDecoder.
+  <p>
+  Once the event has been "posted", it will be handled by the
+  appenders currently configured in the LoggerRespository.
+
+  @author Mark Womack
+  @author Scott Deboy <sdeboy@apache.org>
+
+*/
+public class XMLSocketReceiver extends Receiver implements Runnable, PortBased, Pauseable {
+  private boolean paused;
+  //default to log4j xml decoder
+  protected String decoder = "org.apache.log4j.xml.XMLDecoder";
+  private ServerSocket serverSocket;
+  private List socketList = new Vector();
+  private Thread rThread;
+  public static final int DEFAULT_PORT = 4448;
+  protected int port = DEFAULT_PORT;
+  private boolean advertiseViaMulticastDNS;
+  private ZeroConfSupport zeroConf;
+
+  /**
+   * The MulticastDNS zone advertised by an XMLSocketReceiver
+   */
+  public static final String ZONE = "_log4j_xml_tcpaccept_receiver.local.";
+
+  /*
+   * Log4j doesn't provide an XMLSocketAppender, but the MulticastDNS zone that should be advertised by one is:
+   * _log4j_xml_tcpconnect_appender.local.
+   */
+
+  public XMLSocketReceiver() {
+  }
+
+  public XMLSocketReceiver(int _port) {
+    port = _port;
+  }
+
+  public XMLSocketReceiver(int _port, LoggerRepository _repository) {
+    port = _port;
+    repository = _repository;
+  }
+
+  /**
+    Get the port to receive logging events on. */
+  public int getPort() {
+    return port;
+  }
+
+  /**
+    Set the port to receive logging events on. */
+  public void setPort(int _port) {
+    port = _port;
+  }
+
+  public String getDecoder() {
+    return decoder;
+  }
+
+  /**
+   *Specify the class name implementing org.apache.log4j.spi.Decoder that can process the file.
+   */
+  public void setDecoder(String _decoder) {
+    decoder = _decoder;
+  }
+
+  public boolean isPaused() {
+    return paused;
+  }
+
+  public void setPaused(boolean b) {
+    paused = b;
+  }
+
+  /**
+   * Returns true if the receiver is the same class and they are
+   * configured for the same properties, and super class also considers
+   * them to be equivalent. This is used by PluginRegistry when determining
+   * if the a similarly configured receiver is being started.
+   * 
+   * @param testPlugin The plugin to test equivalency against.
+   * @return boolean True if the testPlugin is equivalent to this plugin.
+   */
+  public boolean isEquivalent(Plugin testPlugin) {
+    if ((testPlugin != null) && testPlugin instanceof XMLSocketReceiver) {
+      XMLSocketReceiver sReceiver = (XMLSocketReceiver) testPlugin;
+
+      return (port == sReceiver.getPort() && super.isEquivalent(testPlugin));
+    }
+
+    return false;
+  }
+
+  public int hashCode() {
+  	
+  	int result = 37 * (repository != null? repository.hashCode():0);
+  	result = result * 37 + port;
+  	return (result * 37 + (getName() != null? getName().hashCode():0));
+  }
+
+  /**
+    Sets the flag to indicate if receiver is active or not.
+   @param b new value
+   */
+  protected synchronized void setActive(final boolean b) {
+    active = b;
+  }
+
+  /**
+    Starts the SocketReceiver with the current options. */
+  public void activateOptions() {
+    if (!isActive()) {
+      rThread = new Thread(this);
+      rThread.setDaemon(true);
+      rThread.start();
+
+      if (advertiseViaMulticastDNS) {
+        zeroConf = new ZeroConfSupport(ZONE, port, getName());
+        zeroConf.advertise();
+      }
+
+      active = true;
+    }
+  }
+
+  public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
+    this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
+  }
+
+  public boolean isAdvertiseViaMulticastDNS() {
+    return advertiseViaMulticastDNS;
+  }
+
+  /**
+    Called when the receiver should be stopped. Closes the
+    server socket and all of the open sockets. */
+  public synchronized void shutdown() {
+    // mark this as no longer running
+    active = false;
+
+    if (rThread != null) {
+      rThread.interrupt();
+      rThread = null;
+    }
+    doShutdown();
+  }
+
+    /**
+     * Does the actual shutting down by closing the server socket
+     * and any connected sockets that have been created.
+     */
+    private synchronized void doShutdown() {
+      active = false;
+
+      getLogger().debug("{} doShutdown called", getName());
+
+      // close the server socket
+      closeServerSocket();
+
+      // close all of the accepted sockets
+      closeAllAcceptedSockets();
+
+      if (advertiseViaMulticastDNS) {
+          zeroConf.unadvertise();
+      }
+    }
+
+    /**
+      * Closes the server socket, if created.
+      */
+     private void closeServerSocket() {
+       getLogger().debug("{} closing server socket", getName());
+
+       try {
+         if (serverSocket != null) {
+           serverSocket.close();
+         }
+       } catch (Exception e) {
+         // ignore for now
+       }
+
+       serverSocket = null;
+     }
+
+    /**
+      * Closes all the connected sockets in the List.
+      */
+     private synchronized void closeAllAcceptedSockets() {
+       for (int x = 0; x < socketList.size(); x++) {
+         try {
+           ((Socket) socketList.get(x)).close();
+         } catch (Exception e) {
+           // ignore for now
+         }
+       }
+
+       // clear member variables
+       socketList.clear();
+     }
+
+  /**
+    Loop, accepting new socket connections. */
+  public void run() {
+      /**
+        * Ensure we start fresh.
+        */
+    getLogger().debug("performing socket cleanup prior to entering loop for {}",  name);
+    closeServerSocket();
+    closeAllAcceptedSockets();
+    getLogger().debug("socket cleanup complete for {}", name);       
+    active = true;
+
+    // start the server socket
+    try {
+      serverSocket = new ServerSocket(port);
+    } catch (Exception e) {
+      getLogger().error(
+        "error starting SocketReceiver (" + this.getName()
+        + "), receiver did not start", e);
+      active = false;
+      doShutdown();
+
+      return;
+    }
+
+    Socket socket = null;
+
+    try {
+      getLogger().debug("in run-about to enter while isactiveloop");
+
+      active = true;
+
+      while (!rThread.isInterrupted()) {
+        // if we have a socket, start watching it
+        if (socket != null) {
+          getLogger().debug("socket not null - creating and starting socketnode");
+          socketList.add(socket);
+
+          XMLSocketNode node = new XMLSocketNode(decoder, socket, this);
+          node.setLoggerRepository(this.repository);
+          new Thread(node).start();
+          socket = null;
+        }
+
+        getLogger().debug("waiting to accept socket");
+
+        // wait for a socket to open, then loop to start it
+        socket = serverSocket.accept();
+        getLogger().debug("accepted socket");
+      }
+
+      // socket not watched because we a no longer running
+      // so close it now.
+      if (socket != null) {
+        socket.close();
+      }
+    } catch (Exception e) {
+      getLogger().warn(
+        "socket server disconnected, stopping");
+    }
+  }
+
+  /* (non-Javadoc)
+   * @see org.apache.log4j.plugins.Receiver#doPost(org.apache.log4j.spi.LoggingEvent)
+   */
+  public void doPost(LoggingEvent event) {
+    if(!isPaused()){
+      super.doPost(event);
+    }
+  }
+
+
+}
diff --git a/src/main/java/org/apache/log4j/plugins/Pauseable.java b/src/main/java/org/apache/log4j/plugins/Pauseable.java
new file mode 100644
index 0000000..6268ba3
--- /dev/null
+++ b/src/main/java/org/apache/log4j/plugins/Pauseable.java
@@ -0,0 +1,39 @@
+/*
+ * 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.plugins;
+
+
+/**
+ * Instances of this interface can be paused, and resumed.
+ *
+ * @author Paul Smith (psmith@apache.org)
+ *
+ */
+public interface Pauseable {
+    /**
+     * Set paused state.
+     * @param paused new value
+     */
+  void setPaused(boolean paused);
+
+    /**
+     * Get paused state.
+     * @return paused state.
+     */
+  boolean isPaused();
+}
diff --git a/src/main/java/org/apache/log4j/plugins/Plugin.java b/src/main/java/org/apache/log4j/plugins/Plugin.java
new file mode 100644
index 0000000..e1b6a6f
--- /dev/null
+++ b/src/main/java/org/apache/log4j/plugins/Plugin.java
@@ -0,0 +1,144 @@
+/*
+ * 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.plugins;
+
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.OptionHandler;
+
+import java.beans.PropertyChangeListener;
+
+
+/**
+ * Defines the required interface for all Plugin objects.
+ * <p/>
+ * <p>A plugin implements some specific functionality to extend
+ * the log4j framework.  Each plugin is associated with a specific
+ * LoggerRepository, which it then uses/acts upon.  The functionality
+ * of the plugin is up to the developer.
+ * <p/>
+ * <p>Examples of plugins are Receiver and Watchdog. Receiver plugins
+ * allow for remote logging events to be received and processed by
+ * a repository as if the event was sent locally. Watchdog plugins
+ * allow for a repository to be reconfigured when some "watched"
+ * configuration data changes.
+ *
+ * @author Mark Womack (mwomack@apache.org)
+ * @author Nicko Cadell
+ * @author Paul Smith (psmith@apache.org)
+ */
+public interface Plugin extends OptionHandler {
+    /**
+     * Gets the name of the plugin.
+     *
+     * @return String the name of the plugin.
+     */
+    String getName();
+
+    /**
+     * Sets the name of the plugin.
+     *
+     * @param name the name of the plugin.
+     */
+    void setName(String name);
+
+    /**
+     * Gets the logger repository for this plugin.
+     *
+     * @return the logger repository to which this plugin is attached.
+     */
+    LoggerRepository getLoggerRepository();
+
+    /**
+     * Sets the logger repository used by this plugin. This
+     * repository will be used by the plugin functionality.
+     *
+     * @param repository the logger repository to attach this plugin to.
+     */
+    void setLoggerRepository(LoggerRepository repository);
+
+    /**
+     * Adds a PropertyChangeListener to this instance which is
+     * notified only by changes of the property with name propertyName.
+     *
+     * @param propertyName
+     *    the name of the property in standard JavaBean syntax
+     *    (e.g. for setName(), property="name")
+     * @param l listener
+     */
+    void addPropertyChangeListener(
+            String propertyName, PropertyChangeListener l);
+
+    /**
+     * Adds a PropertyChangeListener that will be notified of all property
+     * changes.
+     *
+     * @param l The listener to add.
+     */
+    void addPropertyChangeListener(PropertyChangeListener l);
+
+    /**
+     * Removes a specific PropertyChangeListener from this instances
+     * registry that has been mapped to be notified of all property
+     * changes.
+     *
+     * @param l The listener to remove.
+     */
+    void removePropertyChangeListener(PropertyChangeListener l);
+
+    /**
+     * Removes a specific PropertyChangeListener from this instance's
+     * registry which has been previously registered to be notified
+     * of only a specific property change.
+     *
+     * @param propertyName property name, may not be null.
+     * @param l listener to be removed.
+     */
+    void removePropertyChangeListener(
+            String propertyName, PropertyChangeListener l);
+
+    /**
+     * True if the plugin is active and running.
+     *
+     * @return boolean true if the plugin is currently active.
+     */
+    boolean isActive();
+
+    /**
+     * Returns true if the testPlugin is considered to be "equivalent" to the
+     * this plugin.  The equivalency test is at the discretion of the plugin
+     * implementation.  The PluginRegistry will use this method when starting
+     * new plugins to see if a given plugin is considered equivalent to an
+     * already running plugin with the same name.  If they are considered to
+     * be equivalent, the currently running plugin will be left in place, and
+     * the new plugin will not be started.
+     * <p/>
+     * It is possible to override the equals() method, however this has
+     * more meaning than is required for this simple test and would also
+     * require the overriding of the hashCode() method as well.  All of this
+     * is more work than is needed, so this simple method is used instead.
+     *
+     * @param testPlugin The plugin to test equivalency against.
+     * @return Returns true if testPlugin is considered to be equivelent.
+     */
+    boolean isEquivalent(Plugin testPlugin);
+
+    /**
+     * Call when the plugin should be stopped.
+     */
+    void shutdown();
+}
diff --git a/src/main/java/org/apache/log4j/plugins/PluginEvent.java b/src/main/java/org/apache/log4j/plugins/PluginEvent.java
new file mode 100644
index 0000000..1843034
--- /dev/null
+++ b/src/main/java/org/apache/log4j/plugins/PluginEvent.java
@@ -0,0 +1,47 @@
+/*
+ * 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.plugins;
+
+import java.util.EventObject;
+
+
+/**
+ * All Plugin events are encapsulated in this class, which
+ * simply contains the source Plugin, but may in future include more
+ * information.
+ *
+ * @author Paul Smith
+ */
+public class PluginEvent extends EventObject {
+    /**
+     * @param source The source plugin of the event
+     */
+    PluginEvent(final Plugin source) {
+        super(source);
+    }
+
+    /**
+     * Returns the source Plugin of this event, which is simple
+     * the getSource() method casted to Plugin for convenience.
+     *
+     * @return Plugin source of this event
+     */
+    public Plugin getPlugin() {
+        return (Plugin) getSource();
+  }
+}
diff --git a/src/main/java/org/apache/log4j/plugins/PluginListener.java b/src/main/java/org/apache/log4j/plugins/PluginListener.java
new file mode 100644
index 0000000..11f628e
--- /dev/null
+++ b/src/main/java/org/apache/log4j/plugins/PluginListener.java
@@ -0,0 +1,43 @@
+/*
+ * 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.plugins;
+
+import java.util.EventListener;
+
+
+/**
+ * PluginListeners are notified when plugins are started or stopped
+ * by the PluginRegistry.
+ *
+ * @author Paul Smith (psmith@apache.org)
+ */
+public interface PluginListener extends EventListener {
+    /**
+     * Notification that plugin has started.
+     * @param e event
+     */
+  void pluginStarted(PluginEvent e);
+
+    /**
+     * Notification that plugin has stopped.
+     * @param e event
+     */
+  void pluginStopped(PluginEvent e);
+}
diff --git a/src/main/java/org/apache/log4j/plugins/PluginRegistry.java b/src/main/java/org/apache/log4j/plugins/PluginRegistry.java
new file mode 100644
index 0000000..d34f885
--- /dev/null
+++ b/src/main/java/org/apache/log4j/plugins/PluginRegistry.java
@@ -0,0 +1,303 @@
+/*
+ * 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.plugins;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggerRepositoryEx;
+import org.apache.log4j.spi.LoggerRepositoryEventListener;
+
+
+/**
+ * This is a registry for Plugin instances. It provides methods to
+ * start and stop plugin objects individually and to stop all
+ * plugins for a repository.
+ *
+ * @author Mark Womack
+ * @author Paul Smith
+ */
+public final class PluginRegistry {
+    /**
+     * The pluginMap is keyed by plugin name and contains plugins as values.
+     * key=plugin.getName, value=plugin
+     */
+    private final Map pluginMap;
+    /**
+     * Logger repository.
+     */
+    private final LoggerRepositoryEx loggerRepository;
+
+    /**
+     * the listener used to listen for repository events.
+     */
+    private final RepositoryListener listener = new RepositoryListener();
+    /**
+     * List of listeners.
+     */
+    private final List listenerList =
+            Collections.synchronizedList(new ArrayList());
+
+    /**
+     * Creates a new instance.
+     * @param repository logger repository.
+     */
+    public PluginRegistry(final LoggerRepositoryEx repository) {
+        super();
+        pluginMap = new HashMap();
+        this.loggerRepository = repository;
+        this.loggerRepository.addLoggerRepositoryEventListener(listener);
+    }
+
+    /**
+     * Get logger repository.
+     * @return logger repository.
+     */
+    public LoggerRepositoryEx getLoggerRepository() {
+        return loggerRepository;
+    }
+
+
+    /**
+     * Returns true if the specified name is already taken by
+     * an existing Plugin registered within the scope of the specified
+     * LoggerRepository.
+     *
+     * @param name The name to check the repository for
+     * @return true if the name is already in use, otherwise false
+     */
+    public boolean pluginNameExists(final String name) {
+        synchronized (pluginMap) {
+            return pluginMap.containsKey(name);
+        }
+    }
+
+
+    /**
+     * Adds a plugin to the plugin registry.
+     * If a plugin with the same name exists
+     * already, it is shutdown and removed.
+     *
+     * @param plugin the plugin to add.
+     */
+    public void addPlugin(final Plugin plugin) {
+        // put plugin into the repository's reciever map
+        synchronized (pluginMap) {
+            String name = plugin.getName();
+
+            // make sure the plugin has reference to repository
+            plugin.setLoggerRepository(getLoggerRepository());
+
+            Plugin existingPlugin = (Plugin) pluginMap.get(name);
+            if (existingPlugin != null) {
+                existingPlugin.shutdown();
+            }
+
+            // put the new plugin into the map
+            pluginMap.put(name, plugin);
+            firePluginStarted(plugin);
+        }
+    }
+
+
+    /**
+     * Calls the pluginStarted method on every registered PluginListener.
+     *
+     * @param plugin The plugin that has been started.
+     */
+    private void firePluginStarted(final Plugin plugin) {
+        PluginEvent e = null;
+        synchronized (listenerList) {
+            for (Iterator iter = listenerList.iterator(); iter.hasNext();) {
+                PluginListener l = (PluginListener) iter.next();
+                if (e == null) {
+                    e = new PluginEvent(plugin);
+                }
+                l.pluginStarted(e);
+            }
+        }
+    }
+
+
+    /**
+     * Calls the pluginStopped method for every registered PluginListner.
+     *
+     * @param plugin The plugin that has been stopped.
+     */
+    private void firePluginStopped(final Plugin plugin) {
+        PluginEvent e = null;
+        synchronized (listenerList) {
+            for (Iterator iter = listenerList.iterator(); iter.hasNext();) {
+                PluginListener l = (PluginListener) iter.next();
+                if (e == null) {
+                    e = new PluginEvent(plugin);
+                }
+                l.pluginStopped(e);
+            }
+        }
+    }
+
+
+    /**
+     * Returns all the plugins for a given repository.
+     *
+     * @return List list of plugins from the repository.
+     */
+    public List getPlugins() {
+        synchronized (pluginMap) {
+            List pluginList = new ArrayList(pluginMap.size());
+            Iterator iter = pluginMap.values().iterator();
+
+            while (iter.hasNext()) {
+                pluginList.add(iter.next());
+            }
+            return pluginList;
+        }
+    }
+
+
+    /**
+     * Returns all the plugins for a given repository that are instances
+     * of a certain class.
+     *
+     * @param pluginClass the class the plugin must implement to be selected.
+     * @return List list of plugins from the repository.
+     */
+    public List getPlugins(final Class pluginClass) {
+        synchronized (pluginMap) {
+            List pluginList = new ArrayList(pluginMap.size());
+            Iterator iter = pluginMap.values().iterator();
+
+            while (iter.hasNext()) {
+                Object plugin = iter.next();
+
+                if (pluginClass.isInstance(plugin)) {
+                    pluginList.add(plugin);
+                }
+            }
+            return pluginList;
+        }
+    }
+
+
+    /**
+     * Stops a plugin by plugin name and repository.
+     *
+     * @param pluginName the name of the plugin to stop.
+     * @return Plugin the plugin, if stopped, or null if the
+     *         the plugin was not found in the registry.
+     */
+    public Plugin stopPlugin(final String pluginName) {
+        synchronized (pluginMap) {
+            Plugin plugin = (Plugin) pluginMap.get(pluginName);
+
+            if (plugin == null) {
+                return null;
+            }
+
+            // shutdown the plugin
+            plugin.shutdown();
+
+            // remove it from the plugin map
+            pluginMap.remove(pluginName);
+            firePluginStopped(plugin);
+
+            // return it for future use
+            return plugin;
+        }
+    }
+
+    /**
+     * Stops all plugins in the given logger repository.
+     */
+    public void stopAllPlugins() {
+        synchronized (pluginMap) {
+            // remove the listener for this repository
+            loggerRepository.removeLoggerRepositoryEventListener(listener);
+
+            Iterator iter = pluginMap.values().iterator();
+
+            while (iter.hasNext()) {
+                Plugin plugin = (Plugin) iter.next();
+                plugin.shutdown();
+                firePluginStopped(plugin);
+            }
+        }
+    }
+
+
+    /**
+     * Adds a PluginListener to this registry to be notified
+     * of PluginEvents.
+     *
+     * @param l PluginListener to add to this registry
+     */
+    public void addPluginListener(final PluginListener l) {
+        listenerList.add(l);
+    }
+
+
+    /**
+     * Removes a particular PluginListener from this registry
+     * such that it will no longer be notified of PluginEvents.
+     *
+     * @param l PluginListener to remove
+     */
+    public void removePluginListener(final PluginListener l) {
+        listenerList.remove(l);
+    }
+
+    /**
+     * Internal class used to handle listener events from repositories.
+     */
+    private class RepositoryListener implements LoggerRepositoryEventListener {
+        /**
+         * Stops all plugins associated with the repository being reset.
+         *
+         * @param repository the repository that was reset.
+         */
+        public void configurationResetEvent(final LoggerRepository repository) {
+            PluginRegistry.this.stopAllPlugins();
+        }
+
+
+        /**
+         * Called when the repository configuration is changed.
+         *
+         * @param repository the repository that was changed.
+         */
+        public void configurationChangedEvent(
+                final LoggerRepository repository) {
+            // do nothing with this event
+        }
+
+
+        /**
+         * Stops all plugins associated with the repository being shutdown.
+         *
+         * @param repository the repository being shutdown.
+         */
+        public void shutdownEvent(final LoggerRepository repository) {
+            PluginRegistry.this.stopAllPlugins();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/log4j/plugins/PluginSkeleton.java b/src/main/java/org/apache/log4j/plugins/PluginSkeleton.java
new file mode 100644
index 0000000..2660473
--- /dev/null
+++ b/src/main/java/org/apache/log4j/plugins/PluginSkeleton.java
@@ -0,0 +1,222 @@
+/*
+ * 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.plugins;
+
+import org.apache.log4j.spi.ComponentBase;
+import org.apache.log4j.spi.LoggerRepository;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+
+
+/**
+ * A convienent abstract class for plugin subclasses that implements
+ * the basic methods of the Plugin interface. Subclasses are required
+ * to implement the isActive(), activateOptions(), and shutdown()
+ * methods.
+ * <p/>
+ * <p>Developers are not required to subclass PluginSkeleton to
+ * develop their own plugins (they are only required to implement the
+ * Plugin interface), but it provides a convenient base class to start
+ * from.
+ * <p/>
+ * Contributors: Nicko Cadell
+ *
+ * @author Mark Womack (mwomack@apache.org)
+ * @author Paul Smith (psmith@apache.org)
+ */
+public abstract class PluginSkeleton extends ComponentBase implements Plugin {
+    /**
+     * Name of this plugin.
+     */
+    protected String name = "plugin";
+
+    /**
+     * Active state of plugin.
+     */
+    protected boolean active;
+
+    /**
+     * This is a delegate that does all the PropertyChangeListener
+     * support.
+     */
+    private PropertyChangeSupport propertySupport =
+            new PropertyChangeSupport(this);
+
+    /**
+     * Construct new instance.
+     */
+    protected PluginSkeleton() {
+        super();
+    }
+
+    /**
+     * Gets the name of the plugin.
+     *
+     * @return String the name of the plugin.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the name of the plugin and notifies
+     * PropertyChangeListeners of the change.
+     *
+     * @param newName the name of the plugin to set.
+     */
+    public void setName(final String newName) {
+        String oldName = this.name;
+        this.name = newName;
+        propertySupport.firePropertyChange("name", oldName, this.name);
+    }
+
+    /**
+     * Gets the logger repository for this plugin.
+     *
+     * @return LoggerRepository the logger repository this plugin will affect.
+     */
+    public LoggerRepository getLoggerRepository() {
+        return repository;
+    }
+
+    /**
+     * Sets the logger repository used by this plugin and notifies a
+     * relevant PropertyChangeListeners registered. This
+     * repository will be used by the plugin functionality.
+     *
+     * @param repository the logger repository that this plugin should affect.
+     */
+    public void setLoggerRepository(final LoggerRepository repository) {
+        Object oldValue = this.repository;
+        this.repository = repository;
+        firePropertyChange("loggerRepository", oldValue, this.repository);
+    }
+
+    /**
+     * Returns whether this plugin is Active or not.
+     *
+     * @return true/false
+     */
+    public synchronized boolean isActive() {
+        return active;
+    }
+
+    /**
+     * Returns true if the plugin has the same name and logger repository as the
+     * testPlugin passed in.
+     *
+     * @param testPlugin The plugin to test equivalency against.
+     * @return Returns true if testPlugin is considered to be equivalent.
+     */
+    public boolean isEquivalent(final Plugin testPlugin) {
+        return (repository == testPlugin.getLoggerRepository())
+                && ((this.name == null && testPlugin.getName() == null)
+                || (this.name != null
+                           && name.equals(testPlugin.getName())))
+                && this.getClass().equals(testPlugin.getClass());
+    }
+
+    /**
+     * Add property change listener.
+     * @param listener listener.
+     */
+    public final void addPropertyChangeListener(
+            final PropertyChangeListener listener) {
+        propertySupport.addPropertyChangeListener(listener);
+    }
+
+    /**
+     * Add property change listener for one property only.
+     * @param propertyName property name.
+     * @param listener listener.
+     */
+    public final void addPropertyChangeListener(
+            final String propertyName,
+            final PropertyChangeListener listener) {
+        propertySupport.addPropertyChangeListener(propertyName, listener);
+    }
+
+    /**
+     * Remove property change listener.
+     * @param listener listener.
+     */
+    public final void removePropertyChangeListener(
+            final PropertyChangeListener listener) {
+        propertySupport.removePropertyChangeListener(listener);
+    }
+
+    /**
+     * Remove property change listener on a specific property.
+     * @param propertyName property name.
+     * @param listener listener.
+     */
+    public final void removePropertyChangeListener(
+            final String propertyName,
+            final PropertyChangeListener listener) {
+        propertySupport.removePropertyChangeListener(propertyName, listener);
+    }
+
+    /**
+     * Fire a property change event to appropriate listeners.
+     * @param evt change event.
+     */
+    protected final void firePropertyChange(
+            final PropertyChangeEvent evt) {
+        propertySupport.firePropertyChange(evt);
+    }
+
+    /**
+     * Fire property change event to appropriate listeners.
+     * @param propertyName property name.
+     * @param oldValue old value.
+     * @param newValue new value.
+     */
+    protected final void firePropertyChange(
+            final String propertyName,
+            final boolean oldValue,
+            final boolean newValue) {
+        propertySupport.firePropertyChange(propertyName, oldValue, newValue);
+    }
+
+    /**
+     * Fire property change event to appropriate listeners.
+     * @param propertyName property name.
+     * @param oldValue old value.
+     * @param newValue new value.
+     */
+    protected final void firePropertyChange(
+            final String propertyName,
+            final int oldValue, final int newValue) {
+        propertySupport.firePropertyChange(propertyName, oldValue, newValue);
+    }
+
+    /**
+     * Fire property change event to appropriate listeners.
+     * @param propertyName property name.
+     * @param oldValue old value.
+     * @param newValue new value.
+     */
+    protected final void firePropertyChange(
+            final String propertyName,
+            final Object oldValue,
+            final Object newValue) {
+        propertySupport.firePropertyChange(propertyName, oldValue, newValue);
+    }
+}
diff --git a/src/main/java/org/apache/log4j/plugins/Receiver.java b/src/main/java/org/apache/log4j/plugins/Receiver.java
new file mode 100644
index 0000000..d78ae59
--- /dev/null
+++ b/src/main/java/org/apache/log4j/plugins/Receiver.java
@@ -0,0 +1,131 @@
+/*
+ * 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.plugins;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.Thresholdable;
+
+
+/**
+ * Defines the base class for Receiver plugins.
+ * <p/>
+ * <p>Just as Appenders send logging events outside of the log4j
+ * environment (to files, to smtp, to sockets, etc), Receivers bring
+ * logging events inside the log4j environment.
+ * <p/>
+ * <p>Receivers are meant to support the receiving of
+ * remote logging events from another process. For example,
+ * SocketAppender "appends" a logging event to a socket, configured
+ * for a specific host and port number.  On the receiving side of
+ * the socket can be a SocketReceiver object.  The SocketReceiver
+ * object receives the logging event, and then "posts" it to the
+ * log4j environment (LoggerRepository) on the receiving machine, to
+ * be handled by the configured appenders, etc.  The various
+ * settings in this environment (Logger levels, Appender filters &
+ * thresholds) are applied to the received logging event.
+ * <p/>
+ * <p>Receivers can also be used to "import" log messages from other
+ * logging packages into the log4j environment.
+ * <p/>
+ * <p>Receivers can be configured to post events to a given
+ * LoggerRepository.
+ * <p/>
+ * <p>Subclasses of Receiver must implement the isActive(),
+ * activateOptions(), and shutdown() methods. The doPost() method
+ * is provided to standardize the "import" of remote events into
+ * the repository.
+ *
+ * @author Mark Womack
+ * @author Ceki G&uuml;lc&uuml;
+ * @author Paul Smith (psmith@apache.org)
+ */
+public abstract class Receiver extends PluginSkeleton implements Thresholdable {
+    /**
+     * Threshold level.
+     */
+    protected Level thresholdLevel;
+
+    /**
+     * Create new instance.
+     */
+    protected Receiver() {
+        super();
+    }
+
+    /**
+     * Sets the receiver theshold to the given level.
+     *
+     * @param level The threshold level events must equal or be greater
+     *              than before further processing can be done.
+     */
+    public void setThreshold(final Level level) {
+        Level oldValue = this.thresholdLevel;
+        thresholdLevel = level;
+        firePropertyChange("threshold", oldValue, this.thresholdLevel);
+    }
+
+    /**
+     * Gets the current threshold setting of the receiver.
+     *
+     * @return Level The current threshold level of the receiver.
+     */
+    public Level getThreshold() {
+        return thresholdLevel;
+    }
+
+    /**
+     * Returns true if the given level is equals or greater than the current
+     * threshold value of the receiver.
+     *
+     * @param level The level to test against the receiver threshold.
+     * @return boolean True if level is equal or greater than the
+     *         receiver threshold.
+     */
+    public boolean isAsSevereAsThreshold(final Level level) {
+        return ((thresholdLevel == null)
+                || level.isGreaterOrEqual(thresholdLevel));
+    }
+
+    /**
+     * Posts the logging event to a logger in the configured logger
+     * repository.
+     *
+     * @param event the log event to post to the local log4j environment.
+     */
+    public void doPost(final LoggingEvent event) {
+        // if event does not meet threshold, exit now
+        if (!isAsSevereAsThreshold(event.getLevel())) {
+            return;
+        }
+
+        // get the "local" logger for this event from the
+        // configured repository.
+        Logger localLogger =
+                getLoggerRepository().getLogger(event.getLoggerName());
+
+        // if the logger level is greater or equal to the level
+        // of the event, use the logger to append the event.
+        if (event.getLevel()
+                .isGreaterOrEqual(localLogger.getEffectiveLevel())) {
+            // call the loggers appenders to process the event
+            localLogger.callAppenders(event);
+        }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/rewrite/MapRewritePolicy.java b/src/main/java/org/apache/log4j/rewrite/MapRewritePolicy.java
new file mode 100644
index 0000000..4fca465
--- /dev/null
+++ b/src/main/java/org/apache/log4j/rewrite/MapRewritePolicy.java
@@ -0,0 +1,85 @@
+/*
+ * 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.rewrite;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+
+/**
+ * This policy rewrites events where the message of the
+ * original event implementes java.util.Map.
+ * All other events are passed through unmodified.
+ * If the map contains a "message" entry, the value will be
+ * used as the message for the rewritten event.  The rewritten
+ * event will have a property set that is the combination of the
+ * original property set and the other members of the message map.
+ * If both the original property set and the message map
+ * contain the same entry, the value from the message map
+ * will overwrite the original property set.
+ *
+ * The combination of the RewriteAppender and this policy
+ * performs the same actions as the MapFilter from log4j 1.3. 
+ */
+public class MapRewritePolicy implements RewritePolicy {
+    /**
+     * {@inheritDoc}
+     */
+    public LoggingEvent rewrite(final LoggingEvent source) {
+        Object msg = source.getMessage();
+        if (msg instanceof Map) {
+            Map props = new HashMap(source.getProperties());
+            Map eventProps = (Map) msg;
+            //
+            //   if the map sent in the logging request
+            //      has "message" entry, use that as the message body
+            //      otherwise, use the entire map.
+            //
+            Object newMsg = eventProps.get("message");
+            if (newMsg == null) {
+                newMsg = msg;
+            }
+
+            for(Iterator iter = eventProps.entrySet().iterator();
+                    iter.hasNext();
+                  ) {
+                Map.Entry entry = (Map.Entry) iter.next();
+                if (!("message".equals(entry.getKey()))) {
+                    props.put(entry.getKey(), entry.getValue());
+                }
+            }
+
+            return new LoggingEvent(
+                    source.getFQNOfLoggerClass(),
+                    source.getLogger() != null ? source.getLogger(): Logger.getLogger(source.getLoggerName()), 
+                    source.getTimeStamp(),
+                    source.getLevel(),
+                    newMsg,
+                    source.getThreadName(),
+                    source.getThrowableInformation(),
+                    source.getNDC(),
+                    source.getLocationInformation(),
+                    props);
+        } else {
+            return source;
+        }
+
+    }
+}
diff --git a/src/main/java/org/apache/log4j/rewrite/PropertyRewritePolicy.java b/src/main/java/org/apache/log4j/rewrite/PropertyRewritePolicy.java
new file mode 100644
index 0000000..535736c
--- /dev/null
+++ b/src/main/java/org/apache/log4j/rewrite/PropertyRewritePolicy.java
@@ -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.rewrite;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+
+/**
+ * This policy rewrites events by adding
+ * a user-specified list of properties to the event.
+ * Existing properties are not modified.
+ *
+ * The combination of the RewriteAppender and this policy
+ * performs the same actions as the PropertyFilter from log4j 1.3.
+ */
+
+public class PropertyRewritePolicy implements RewritePolicy {
+    private Map properties = Collections.EMPTY_MAP;
+    public PropertyRewritePolicy() {
+    }
+
+    /**
+     * Set a string representing the property name/value pairs.
+     * 
+     * Form: propname1=propvalue1,propname2=propvalue2
+     * 
+     * @param props
+     */
+    public void setProperties(String props) {
+        Map hashTable = new HashMap();
+        StringTokenizer pairs = new StringTokenizer(props, ",");
+        while (pairs.hasMoreTokens()) {
+            StringTokenizer entry = new StringTokenizer(pairs.nextToken(), "=");
+            hashTable.put(entry.nextElement().toString().trim(), entry.nextElement().toString().trim());
+        }
+        synchronized(this) {
+            properties = hashTable;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public LoggingEvent rewrite(final LoggingEvent source) {
+        if (!properties.isEmpty()) {
+            Map rewriteProps = new HashMap(source.getProperties());
+            for(Iterator iter = properties.entrySet().iterator();
+                    iter.hasNext();
+                    ) {
+                Map.Entry entry = (Map.Entry) iter.next();
+                if (!rewriteProps.containsKey(entry.getKey())) {
+                    rewriteProps.put(entry.getKey(), entry.getValue());
+                }
+            }
+
+            return new LoggingEvent(
+                    source.getFQNOfLoggerClass(),
+                    source.getLogger() != null ? source.getLogger(): Logger.getLogger(source.getLoggerName()), 
+                    source.getTimeStamp(),
+                    source.getLevel(),
+                    source.getMessage(),
+                    source.getThreadName(),
+                    source.getThrowableInformation(),
+                    source.getNDC(),
+                    source.getLocationInformation(),
+                    rewriteProps);
+        }
+        return source;
+    }
+
+
+
+}
diff --git a/src/main/java/org/apache/log4j/rewrite/ReflectionRewritePolicy.java b/src/main/java/org/apache/log4j/rewrite/ReflectionRewritePolicy.java
new file mode 100644
index 0000000..f1a4cc5
--- /dev/null
+++ b/src/main/java/org/apache/log4j/rewrite/ReflectionRewritePolicy.java
@@ -0,0 +1,89 @@
+/*
+ * 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.rewrite;
+
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.spi.LoggingEvent;
+
+/**
+ * This policy rewrites events by evaluating any
+ * JavaBean properties on the message object and adding them
+ * to the event properties.  If the message object has a
+ * message property, the value of that property will be
+ * used as the message for the rewritten event and will
+ * not be added to the event properties.  Values from the
+ * JavaBean properties will replace any existing property
+ * with the same name.
+ *
+ * The combination of the RewriteAppender and this policy
+ * performs the same actions as the ReflectionFilter from log4j 1.3. 
+ */
+public class ReflectionRewritePolicy implements RewritePolicy {
+    /**
+     * {@inheritDoc}
+     */
+    public LoggingEvent rewrite(final LoggingEvent source) {
+        Object msg = source.getMessage();
+        if (!(msg instanceof String)) {
+            Object newMsg = msg;
+            Map rewriteProps = new HashMap(source.getProperties());
+
+            try {
+                PropertyDescriptor[] props = Introspector.getBeanInfo(
+                        msg.getClass(), Object.class).getPropertyDescriptors();
+                if (props.length > 0) {
+                    for (int i=0;i<props.length;i++) {
+                        try {
+                            Object propertyValue =
+                                props[i].getReadMethod().invoke(msg,
+                                        (Object[]) null);
+                            if ("message".equalsIgnoreCase(props[i].getName())) {
+                                newMsg = propertyValue;
+                            } else {
+                                rewriteProps.put(props[i].getName(), propertyValue);
+                            }
+                        } catch (Exception e) {
+                            LogLog.warn("Unable to evaluate property " +
+                                    props[i].getName(), e);
+                        }
+                    }
+                    return new LoggingEvent(
+                            source.getFQNOfLoggerClass(),
+                            source.getLogger() != null ? source.getLogger(): Logger.getLogger(source.getLoggerName()),
+                            source.getTimeStamp(),
+                            source.getLevel(),
+                            newMsg,
+                            source.getThreadName(),
+                            source.getThrowableInformation(),
+                            source.getNDC(),
+                            source.getLocationInformation(),
+                            rewriteProps);
+                }
+            } catch (Exception e) {
+                LogLog.warn("Unable to get property descriptors", e);
+            }
+
+        }
+        return source;
+    }
+}
diff --git a/src/main/java/org/apache/log4j/rewrite/RewriteAppender.java b/src/main/java/org/apache/log4j/rewrite/RewriteAppender.java
new file mode 100644
index 0000000..368ecf9
--- /dev/null
+++ b/src/main/java/org/apache/log4j/rewrite/RewriteAppender.java
@@ -0,0 +1,199 @@
+/*
+ * 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.rewrite;
+
+import org.apache.log4j.Appender;
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.helpers.AppenderAttachableImpl;
+import org.apache.log4j.spi.AppenderAttachable;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.OptionHandler;
+import org.apache.log4j.xml.UnrecognizedElementHandler;
+import org.w3c.dom.Element;
+
+import java.util.Enumeration;
+import java.util.Properties;
+
+/**
+ * This appender forwards a logging request to another
+ * appender after possibly rewriting the logging event.
+ *
+ * This appender (with the appropriate policy)
+ * replaces the MapFilter, PropertyFilter and ReflectionFilter
+ * from log4j 1.3.
+ */
+public class RewriteAppender extends AppenderSkeleton
+     implements AppenderAttachable, UnrecognizedElementHandler {
+    /**
+     * Rewrite policy.
+     */
+    private RewritePolicy policy;
+    /**
+     * Nested appenders.
+     */
+    private final AppenderAttachableImpl appenders;
+
+    public RewriteAppender() {
+        appenders = new AppenderAttachableImpl();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void append(final LoggingEvent event) {
+        LoggingEvent rewritten = event;
+        if (policy != null) {
+            rewritten = policy.rewrite(event);
+        }
+        if (rewritten != null) {
+            synchronized (appenders) {
+              appenders.appendLoopOnAppenders(rewritten);
+            }
+        }
+    }
+
+    /**
+     * Add appender.
+     *
+     * @param newAppender appender to add, may not be null.
+     */
+    public void addAppender(final Appender newAppender) {
+      synchronized (appenders) {
+        appenders.addAppender(newAppender);
+      }
+    }
+
+    /**
+     * Get iterator over attached appenders.
+     * @return iterator or null if no attached appenders.
+     */
+    public Enumeration getAllAppenders() {
+      synchronized (appenders) {
+        return appenders.getAllAppenders();
+      }
+    }
+
+    /**
+     * Get appender by name.
+     *
+     * @param name name, may not be null.
+     * @return matching appender or null.
+     */
+    public Appender getAppender(final String name) {
+      synchronized (appenders) {
+        return appenders.getAppender(name);
+      }
+    }
+
+
+    /**
+     * Close this <code>AsyncAppender</code> by interrupting the dispatcher
+     * thread which will process all pending events before exiting.
+     */
+    public void close() {
+      closed = true;
+      //
+      //    close all attached appenders.
+      //
+      synchronized (appenders) {
+        Enumeration iter = appenders.getAllAppenders();
+
+        if (iter != null) {
+          while (iter.hasMoreElements()) {
+            Object next = iter.nextElement();
+
+            if (next instanceof Appender) {
+              ((Appender) next).close();
+            }
+          }
+        }
+      }
+    }
+
+    /**
+     * Determines if specified appender is attached.
+     * @param appender appender.
+     * @return true if attached.
+     */
+    public boolean isAttached(final Appender appender) {
+      synchronized (appenders) {
+        return appenders.isAttached(appender);
+      }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean requiresLayout() {
+      return false;
+    }
+
+    /**
+     * Removes and closes all attached appenders.
+     */
+    public void removeAllAppenders() {
+      synchronized (appenders) {
+        appenders.removeAllAppenders();
+      }
+    }
+
+    /**
+     * Removes an appender.
+     * @param appender appender to remove.
+     */
+    public void removeAppender(final Appender appender) {
+      synchronized (appenders) {
+        appenders.removeAppender(appender);
+      }
+    }
+
+    /**
+     * Remove appender by name.
+     * @param name name.
+     */
+    public void removeAppender(final String name) {
+      synchronized (appenders) {
+        appenders.removeAppender(name);
+      }
+    }
+
+
+    public void setRewritePolicy(final RewritePolicy rewritePolicy) {
+        policy = rewritePolicy;
+    }
+    /**
+     * {@inheritDoc}
+     */
+    public boolean parseUnrecognizedElement(final Element element,
+                                            final Properties props) throws Exception {
+        final String nodeName = element.getNodeName();
+        if ("rewritePolicy".equals(nodeName)) {
+            Object rewritePolicy =
+                    org.apache.log4j.xml.DOMConfigurator.parseElement(
+                            element, props, RewritePolicy.class);
+            if (rewritePolicy != null) {
+                if (rewritePolicy instanceof OptionHandler) {
+                    ((OptionHandler) rewritePolicy).activateOptions();
+                }
+                this.setRewritePolicy((RewritePolicy) rewritePolicy);
+            }
+            return true;
+        }
+        return false;
+    }
+
+}
diff --git a/src/main/java/org/apache/log4j/rewrite/RewritePolicy.java b/src/main/java/org/apache/log4j/rewrite/RewritePolicy.java
new file mode 100644
index 0000000..bb40507
--- /dev/null
+++ b/src/main/java/org/apache/log4j/rewrite/RewritePolicy.java
@@ -0,0 +1,37 @@
+package org.apache.log4j.rewrite;
+
+import org.apache.log4j.spi.LoggingEvent;
+
+/*
+* 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.
+*/
+
+/**
+ * This interface is implemented to provide a rewrite
+ * strategy for RewriteAppender.  RewriteAppender will
+ * call the rewrite method with a source logging event.
+ * The strategy may return that event, create a new event
+ * or return null to suppress the logging request.
+ */
+public interface RewritePolicy {
+    /**
+     * Rewrite a logging event.
+     * @param source a logging event that may be returned or
+     * used to create a new logging event.
+     * @return a logging event or null to suppress processing.
+     */
+    LoggingEvent rewrite(final LoggingEvent source);
+}
diff --git a/src/main/java/org/apache/log4j/scheduler/Job.java b/src/main/java/org/apache/log4j/scheduler/Job.java
new file mode 100644
index 0000000..a0e9be4
--- /dev/null
+++ b/src/main/java/org/apache/log4j/scheduler/Job.java
@@ -0,0 +1,36 @@
+/*
+ * 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.scheduler;
+
+
+/**
+ * Job is a very simple interface. It only has a single method {@link #execute}
+ * which is called by the {@link Scheduler} when a task is ready for execution.
+ * <p/>
+ * It is assumed that the execution context
+ * are contained within the implementing
+ * {@link Job} itself.
+ *
+ * @author Ceki G&uuml;lc&uuml;
+ */
+public interface Job {
+    /**
+     * Execute job.
+     */
+    void execute();
+}
diff --git a/src/main/java/org/apache/log4j/scheduler/Scheduler.java b/src/main/java/org/apache/log4j/scheduler/Scheduler.java
new file mode 100644
index 0000000..72809f7
--- /dev/null
+++ b/src/main/java/org/apache/log4j/scheduler/Scheduler.java
@@ -0,0 +1,307 @@
+/*
+ * 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.scheduler;
+
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * A simple but still useful implementation of a Scheduler (in memory only).
+ * <p/>
+ * This implementation will work very well when the number of scheduled job is
+ * small, say less than 100 jobs. If a larger number of events need to be
+ * scheduled, than a better adapted data structure for the jobList can give
+ * improved performance.
+ *
+ * @author Ceki
+ */
+public class Scheduler extends Thread {
+
+    /**
+     * Job list.
+     */
+    List jobList;
+    /**
+     * If set true, scheduler has or should shut down.
+     */
+    boolean shutdown = false;
+
+    /**
+     * Create new instance.
+     */
+    public Scheduler() {
+        super();
+        jobList = new Vector();
+    }
+
+    /**
+     * Find the index of a given job.
+     * @param job job
+     * @return -1 if the job could not be found.
+     */
+    int findIndex(final Job job) {
+        int size = jobList.size();
+        boolean found = false;
+
+        int i = 0;
+        for (; i < size; i++) {
+            ScheduledJobEntry se = (ScheduledJobEntry) jobList.get(i);
+            if (se.job == job) {
+                found = true;
+                break;
+            }
+        }
+        if (found) {
+            return i;
+        } else {
+            return -1;
+        }
+    }
+
+    /**
+     * Delete the given job.
+     * @param job job.
+     * @return true if the job could be deleted, and
+     * false if the job could not be found or if the Scheduler is about to
+     * shutdown in which case deletions are not permitted.
+     */
+    public synchronized boolean delete(final Job job) {
+        // if already shutdown in the process of shutdown, there is no
+        // need to remove Jobs as they will never be executed.
+        if (shutdown) {
+            return false;
+        }
+        int i = findIndex(job);
+        if (i != -1) {
+            ScheduledJobEntry se = (ScheduledJobEntry) jobList.remove(i);
+            if (se.job != job) { // this should never happen
+                new IllegalStateException("Internal programming error");
+            }
+            // if the job is the first on the list,
+            // then notify the scheduler thread to schedule a new job
+            if (i == 0) {
+                this.notifyAll();
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+
+    /**
+     * Schedule a {@link Job} for execution at system time given by
+     * the <code>desiredTime</code> parameter.
+     * @param job job to schedule.
+     * @param desiredTime desired time of execution.
+     */
+    public synchronized void schedule(final Job job,
+                                      final long desiredTime) {
+        schedule(new ScheduledJobEntry(job, desiredTime));
+    }
+
+    /**
+     * Schedule a {@link Job} for execution at system time given by
+     * the <code>desiredTime</code> parameter.
+     * <p/>
+     * The job will be rescheduled. It will execute with a frequency determined
+     * by the period parameter.
+     * @param job job to schedule.
+     * @param desiredTime desired time of execution.
+     * @param period repeat period.
+     */
+    public synchronized void schedule(final Job job,
+                                      final long desiredTime,
+                                      final long period) {
+        schedule(new ScheduledJobEntry(job, desiredTime, period));
+    }
+
+    /**
+     * Change the period of a job. The original job must exist for its period
+     * to be changed.
+     * <p/>
+     * The method returns true if the period could be changed, and false
+     * otherwise.
+     * @param job job.
+     * @param newPeriod new repeat period.
+     * @return true if period could be changed.
+     */
+    public synchronized boolean changePeriod(final Job job,
+                                             final long newPeriod) {
+        if (newPeriod <= 0) {
+            throw new IllegalArgumentException(
+                    "Period must be an integer langer than zero");
+        }
+
+        int i = findIndex(job);
+        if (i == -1) {
+            return false;
+        } else {
+            ScheduledJobEntry se = (ScheduledJobEntry) jobList.get(i);
+            se.period = newPeriod;
+            return true;
+        }
+    }
+
+    /**
+     * Schedule a job.
+     * @param newSJE new job entry.
+     */
+    private synchronized void schedule(final ScheduledJobEntry newSJE) {
+        // disallow new jobs after shutdown
+        if (shutdown) {
+            return;
+        }
+        int max = jobList.size();
+        long desiredExecutionTime = newSJE.desiredExecutionTime;
+
+        // find the index i such that timeInMillis < jobList[i]
+        int i = 0;
+        for (; i < max; i++) {
+
+            ScheduledJobEntry sje = (ScheduledJobEntry) jobList.get(i);
+
+            if (desiredExecutionTime < sje.desiredExecutionTime) {
+                break;
+            }
+        }
+        jobList.add(i, newSJE);
+        // if the jobList was empty, then notify the scheduler thread
+        if (i == 0) {
+            this.notifyAll();
+        }
+    }
+
+    /**
+     * Shut down scheduler.
+     */
+    public synchronized void shutdown() {
+        shutdown = true;
+    }
+
+    /**
+     * Run scheduler.
+     */
+    public synchronized void run() {
+        while (!shutdown) {
+            if (jobList.isEmpty()) {
+                linger();
+            } else {
+                ScheduledJobEntry sje = (ScheduledJobEntry) jobList.get(0);
+                long now = System.currentTimeMillis();
+                if (now >= sje.desiredExecutionTime) {
+                    executeInABox(sje.job);
+                    jobList.remove(0);
+                    if (sje.period > 0) {
+                        sje.desiredExecutionTime = now + sje.period;
+                        schedule(sje);
+                    }
+                } else {
+                    linger(sje.desiredExecutionTime - now);
+                }
+            }
+        }
+        // clear out the job list to facilitate garbage collection
+        jobList.clear();
+        jobList = null;
+        System.out.println("Leaving scheduler run method");
+    }
+
+    /**
+     * We do not want a single failure to affect the whole scheduler.
+     * @param job job to execute.
+     */
+    void executeInABox(final Job job) {
+        try {
+            job.execute();
+        } catch (Exception e) {
+            System.err.println("The execution of the job threw an exception");
+            e.printStackTrace(System.err);
+        }
+    }
+
+    /**
+     * Wait for notification.
+     */
+    void linger() {
+        try {
+            while (jobList.isEmpty() && !shutdown) {
+                this.wait();
+            }
+        } catch (InterruptedException ie) {
+            shutdown = true;
+        }
+    }
+
+    /**
+     * Wait for notification or time to elapse.
+     * @param timeToLinger time to linger.
+     */
+    void linger(final long timeToLinger) {
+        try {
+            this.wait(timeToLinger);
+        } catch (InterruptedException ie) {
+            shutdown = true;
+        }
+    }
+
+    /**
+     * Represents an entry in job scheduler.
+     */
+    static final class ScheduledJobEntry {
+        /**
+         * Desired execution time.
+         */
+        long desiredExecutionTime;
+        /**
+         * Job to run.
+         */
+        Job job;
+        /**
+         * Repeat period.
+         */
+        long period = 0;
+
+        /**
+         * Create new instance.
+         * @param job job
+         * @param desiredTime desired time.
+         */
+        ScheduledJobEntry(final Job job, final long desiredTime) {
+            this(job, desiredTime, 0);
+        }
+
+        /**
+         * Create new instance.
+         * @param job job
+         * @param desiredTime desired time
+         * @param period repeat period
+         */
+        ScheduledJobEntry(final Job job,
+                          final long desiredTime,
+                          final long period) {
+            super();
+            this.desiredExecutionTime = desiredTime;
+            this.job = job;
+            this.period = period;
+        }
+    }
+
+}
+
+
diff --git a/src/main/java/org/apache/log4j/spi/Component.java b/src/main/java/org/apache/log4j/spi/Component.java
new file mode 100644
index 0000000..42ef29a
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/Component.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+
+/**
+ * A common interface shared by log4j components.
+ *
+ * @author Ceki Gulcu
+ */
+public interface Component {
+
+
+    /**
+     * Set owning logger repository for this component. This operation can
+     * only be performed once.
+     * Once set, a subsequent attempt will throw an IllegalStateException.
+     *
+     * @param repository The repository where this appender is attached.
+     */
+    void setLoggerRepository(LoggerRepository repository);
+
+}
diff --git a/src/main/java/org/apache/log4j/spi/ComponentBase.java b/src/main/java/org/apache/log4j/spi/ComponentBase.java
new file mode 100644
index 0000000..78932f7
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/ComponentBase.java
@@ -0,0 +1,126 @@
+/*
+ * 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 org.apache.log4j.ULogger;
+import org.apache.log4j.Logger;
+
+
+/**
+ * Most log4j components derive from this class.
+ *
+ * @author Ceki Gulcu
+ */
+public class ComponentBase implements Component {
+
+    /**
+     * Error count limit.
+     */
+    private static final int ERROR_COUNT_LIMIT = 3;
+
+    /**
+     * Logger repository.
+     */
+    protected LoggerRepository repository;
+    /**
+     * Logger.
+     */
+    private ULogger logger;
+    /**
+     * Error count.
+     */
+    private int errorCount = 0;
+
+    /**
+     * Construct a new instance.
+     */
+    protected ComponentBase() {
+        super();
+    }
+
+
+    /**
+     * Called by derived classes when they deem that the component has recovered
+     * from an erroneous state.
+     */
+    protected void resetErrorCount() {
+        errorCount = 0;
+    }
+
+    /**
+     * Set the owning repository. The owning repository cannot be set more than
+     * once.
+     *
+     * @param repository repository
+     */
+    public void setLoggerRepository(final LoggerRepository repository) {
+        if (this.repository == null) {
+            this.repository = repository;
+        } else if (this.repository != repository) {
+            throw new IllegalStateException("Repository has been already set");
+        }
+    }
+
+    /**
+     * Return the LoggerRepository to which this component is attached.
+     *
+     * @return Owning LoggerRepository
+     */
+    protected LoggerRepository getLoggerRepository() {
+        return repository;
+    }
+
+    /**
+     * Return an instance specific logger to be used by the component itself.
+     * This logger is not intended to be accessed by the end-user, hence the
+     * protected keyword.
+     * <p/>
+     * <p>In case the repository for this component is not set,
+     * this implementations returns a {@link SimpleULogger} instance.
+     *
+     * @return A ULogger instance.
+     */
+    protected ULogger getLogger() {
+        if (logger == null) {
+            if (repository != null) {
+                Logger l = repository.getLogger(this.getClass().getName());
+                if (l instanceof ULogger) {
+                    logger = (ULogger) l;
+                } else {
+                    logger = new Log4JULogger(l);
+                }
+            } else {
+                logger = SimpleULogger.getLogger(this.getClass().getName());
+            }
+        }
+        return logger;
+    }
+
+    /**
+     * Frequently called methods in log4j components can invoke this method in
+     * order to avoid flooding the output when logging lasting error conditions.
+     *
+     * @return a regular logger, or a NOPLogger if called too frequently.
+     */
+    protected ULogger getNonFloodingLogger() {
+        if (errorCount++ >= ERROR_COUNT_LIMIT) {
+            return NOPULogger.NOP_LOGGER;
+        } else {
+            return getLogger();
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/spi/Decoder.java b/src/main/java/org/apache/log4j/spi/Decoder.java
new file mode 100644
index 0000000..d4686ad
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/Decoder.java
@@ -0,0 +1,63 @@
+/*
+ * 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.IOException;
+
+import java.net.URL;
+
+import java.util.Map;
+import java.util.Vector;
+
+
+/**
+ *  Allow LoggingEvents to be reconstructed from a different format
+ * (usually XML).
+ *
+ *  @author Scott Deboy (sdeboy@apache.org)
+ */
+public interface Decoder {
+    /**
+     * Decode events from document.
+     * @param document document to decode.
+     * @return list of LoggingEvent instances.
+     */
+  Vector decodeEvents(String document);
+
+    /**
+     * Decode event from string.
+     * @param event string representation of event
+     * @return event
+     */
+  LoggingEvent decode(String event);
+
+    /**
+     * Decode event from document retreived from URL.
+     * @param url url of document
+     * @return list of LoggingEvent instances.
+     * @throws IOException if IO error resolving document.
+     */
+  Vector decode(URL url) throws IOException;
+
+    /**
+     * Sets additional properties.
+     * @param additionalProperties map of additional properties.
+     */
+  void setAdditionalProperties(Map additionalProperties);
+}
diff --git a/src/main/java/org/apache/log4j/spi/ErrorItem.java b/src/main/java/org/apache/log4j/spi/ErrorItem.java
new file mode 100644
index 0000000..f6f3686
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/ErrorItem.java
@@ -0,0 +1,172 @@
+/*
+ * 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.PrintStream;
+
+/**
+ * Used to store special log4j errors which cannot be logged using internal
+ * logging. Such errors include those occurring during the initial phases
+ * of log4j configuration or errors emanating from core components such as
+ * Logger or Hierarchy.
+ *
+ * @author Ceki Gulcu
+ */
+public class ErrorItem {
+    /**
+     * Message.
+     */
+  String message;
+    /**
+     * Column.
+     */
+  int colNumber = -1;
+    /**
+     * Line number.
+     */
+  int lineNumber = -1;
+    /**
+     * Exception.
+     */
+  Throwable exception;
+
+    /**
+     * Create new instance.
+     * @param message message
+     * @param e exception
+     */
+  public ErrorItem(final String message, final Exception e) {
+    super();
+    this.message = message;
+    exception = e;
+  }
+
+    /**
+     * Creaet new instance.
+     * @param message message.
+     */
+  public ErrorItem(final String message) {
+    this(message, null);
+  }
+
+    /**
+     * Get column number.
+     * @return column number.
+     */
+  public int getColNumber() {
+    return colNumber;
+  }
+
+    /**
+     * Set column number.
+     * @param colNumber new column number.
+     */
+  public void setColNumber(int colNumber) {
+    this.colNumber = colNumber;
+  }
+
+    /**
+     * Get exception.
+     * @return exception.
+     */
+  public Throwable getException() {
+    return exception;
+  }
+
+    /**
+     * Set exception.
+     * @param exception exception
+     */
+  public void setException(final Throwable exception) {
+    this.exception = exception;
+  }
+
+    /**
+     * Get line number.
+     * @return line number.
+     */
+  public int getLineNumber() {
+    return lineNumber;
+  }
+
+    /**
+     * Set line number.
+     * @param lineNumber line number.
+     */
+  public void setLineNumber(final int lineNumber) {
+    this.lineNumber = lineNumber;
+  }
+
+    /**
+     * Get message.
+     * @return message.
+     */
+  public String getMessage() {
+    return message;
+  }
+
+    /**
+     * Set message.
+     * @param message message.
+     */
+  public void setMessage(final String message) {
+    this.message = message;
+  }
+
+    /**
+     * String representation of ErrorItem.
+     * @return string.
+     */
+  public String toString() {
+    String str =
+      "Reported error: \"" + message + "\"";
+
+    if (lineNumber != -1) {
+      str += " at line " + lineNumber + " column " + colNumber;
+    }
+    if (exception != null) {
+      str += (" with exception " + exception);
+    }
+    return str;
+  }
+
+  /**
+   * Dump the details of this ErrorItem to System.out.
+   */
+  public void dump() {
+    dump(System.out);
+  }
+  
+  /**
+   * Dump the details of this ErrorItem on the specified {@link PrintStream}.
+   * @param ps print stream.
+   */
+  public void dump(final PrintStream ps) {
+    String str =
+      "Reported error: \"" + message + "\"";
+
+    if (lineNumber != -1) {
+      str += " at line " + lineNumber + " column " + colNumber;
+    }
+    ps.println(str);
+
+    if (exception != null) {
+      exception.printStackTrace(ps);
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/spi/Log4JULogger.java b/src/main/java/org/apache/log4j/spi/Log4JULogger.java
new file mode 100644
index 0000000..2476810
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/Log4JULogger.java
@@ -0,0 +1,229 @@
+/*
+ * 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 org.apache.log4j.Logger;
+import org.apache.log4j.ULogger;
+import org.apache.log4j.helpers.MessageFormatter;
+import org.apache.log4j.Level;
+
+
+/**
+ * An implementation of ULogger on org.apache.log4j.Logger.
+ */
+public final class Log4JULogger implements ULogger {
+
+    /**
+     * Wrapped log4j logger.
+     */
+    private final Logger logger;
+
+    /**
+     * Create a new instance.
+     *
+     * @param l logger, may not be null.
+     */
+    public Log4JULogger(final Logger l) {
+        super();
+        if (l == null) {
+            throw new NullPointerException("l");
+        }
+        logger = l;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isDebugEnabled() {
+        return logger.isDebugEnabled();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void debug(final Object msg) {
+        logger.debug(msg);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void debug(final Object parameterizedMsg,
+                      final Object param1) {
+        if (logger.isDebugEnabled()) {
+            logger.debug(MessageFormatter.format(
+                    parameterizedMsg.toString(), param1));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void debug(final String parameterizedMsg,
+                      final Object param1,
+                      final Object param2) {
+        if (logger.isDebugEnabled()) {
+            logger.debug(MessageFormatter.format(
+                    parameterizedMsg.toString(), param1, param2));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void debug(final Object msg,
+                      final Throwable t) {
+        logger.debug(msg, t);
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isInfoEnabled() {
+        return logger.isInfoEnabled();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void info(final Object msg) {
+        logger.info(msg);
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public void info(final Object parameterizedMsg,
+                     final Object param1) {
+        if (logger.isInfoEnabled()) {
+            logger.info(MessageFormatter.format(
+                    parameterizedMsg.toString(), param1));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void info(final String parameterizedMsg,
+                     final Object param1,
+                     final Object param2) {
+        if (logger.isInfoEnabled()) {
+            logger.info(MessageFormatter.format(
+                    parameterizedMsg.toString(),
+                    param1,
+                    param2));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void info(final Object msg, final Throwable t) {
+        logger.info(msg, t);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isWarnEnabled() {
+        return logger.isEnabledFor(Level.WARN);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void warn(final Object msg) {
+        logger.warn(msg);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void warn(final Object parameterizedMsg,
+                     final Object param1) {
+        if (logger.isEnabledFor(Level.WARN)) {
+            logger.warn(MessageFormatter.format(
+                    parameterizedMsg.toString(), param1));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void warn(final String parameterizedMsg,
+                     final Object param1,
+                     final Object param2) {
+        if (logger.isEnabledFor(Level.WARN)) {
+            logger.warn(MessageFormatter.format(
+                    parameterizedMsg.toString(), param1, param2));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void warn(final Object msg, final Throwable t) {
+        logger.warn(msg, t);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isErrorEnabled() {
+        return logger.isEnabledFor(Level.ERROR);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void error(final Object msg) {
+        logger.error(msg);
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public void error(final Object parameterizedMsg, final Object param1) {
+        if (logger.isEnabledFor(Level.ERROR)) {
+            logger.error(MessageFormatter.format(
+                    parameterizedMsg.toString(), param1));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void error(final String parameterizedMsg,
+                      final Object param1,
+                      final Object param2) {
+        if (logger.isEnabledFor(Level.ERROR)) {
+            logger.error(MessageFormatter.format(
+                    parameterizedMsg.toString(), param1, param2));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void error(final Object msg, final Throwable t) {
+        logger.error(msg, t);
+    }
+
+}
diff --git a/src/main/java/org/apache/log4j/spi/LoggerEventListener.java b/src/main/java/org/apache/log4j/spi/LoggerEventListener.java
new file mode 100644
index 0000000..b39f728
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/LoggerEventListener.java
@@ -0,0 +1,59 @@
+/*
+ * 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 org.apache.log4j.Appender;
+import org.apache.log4j.Logger;
+
+
+/**
+  Interface used to listen for Logger related events such as
+  add/remove appender or changing levels.  Clients register an instance of
+  the interface and the instance is called back when the various events occur.
+
+  LoggerRepository provides methods for adding and removing
+  LoggerEventListener instances.
+
+  When implementing the methods of this interface, it is useful to remember
+  that the Logger can access the repository using its getRepository()
+  method.
+
+  @author Ceki G&uuml;lc&uuml;
+  @author Mark Womack
+*/
+public interface LoggerEventListener {
+  /**
+    Called when an appender is added to the logger.
+
+    @param logger The logger to which the appender was added.
+    @param appender The appender added to the logger. */
+  void appenderAddedEvent(Logger logger, Appender appender);
+
+  /**
+    Called when an appender is removed from the logger.
+
+    @param logger The logger from which the appender was removed.
+    @param appender The appender removed from the logger. */
+  void appenderRemovedEvent(Logger logger, Appender appender);
+
+  /**
+    Called when level changed on the logger.
+
+    @param logger The logger that changed levels. */
+  void levelChangedEvent(Logger logger);
+}
diff --git a/src/main/java/org/apache/log4j/spi/LoggerRepositoryEventListener.java b/src/main/java/org/apache/log4j/spi/LoggerRepositoryEventListener.java
new file mode 100644
index 0000000..773a497
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/LoggerRepositoryEventListener.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+
+/**
+  Interface used to listen for LoggerRepository related
+  events such as startup, reset, and shutdown.  Clients register
+  an instance of the interface and the instance is called back
+  when the various events occur.
+
+  LoggerRepository provides methods for adding and removing
+  LoggerRepositoryEventListener instances.
+
+  @author Ceki G&uuml;lc&uuml;
+  @author Mark Womack
+*/
+public interface LoggerRepositoryEventListener {
+  /**
+    Called when the repository configuration is reset.
+   @param repository repository
+   */
+  void configurationResetEvent(LoggerRepository repository);
+
+  /**
+    Called when the repository configuration is changed.
+   @param repository repository
+   */
+  void configurationChangedEvent(LoggerRepository repository);
+
+  /**
+    Called when the repository is shutdown. When this method is
+    invoked, the repository is still valid (ie it has not been
+    shutdown, but will be after this method returns).
+    @param repository repository.
+   */
+  void shutdownEvent(LoggerRepository repository);
+}
diff --git a/src/main/java/org/apache/log4j/spi/LoggerRepositoryEx.java b/src/main/java/org/apache/log4j/spi/LoggerRepositoryEx.java
new file mode 100644
index 0000000..c079a2c
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/LoggerRepositoryEx.java
@@ -0,0 +1,198 @@
+/*
+ * 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 org.apache.log4j.Appender;
+import org.apache.log4j.Category;
+import org.apache.log4j.Logger;
+import org.apache.log4j.plugins.PluginRegistry;
+import org.apache.log4j.scheduler.Scheduler;
+
+import java.util.List;
+import java.util.Map;
+
+
+/**
+   A <code>LoggerRepository</code> is used to create and retrieve
+   <code>Loggers</code>. The relation between loggers in a repository
+   depends on the repository but typically loggers are arranged in a
+   named hierarchy.
+
+   <p>In addition to the creational methods, a
+   <code>LoggerRepository</code> can be queried for existing loggers,
+   can act as a point of registry for events related to loggers.
+
+   @author Ceki G&uuml;lc&uuml;
+   @author Mark Womack
+   @author Curt Arnold
+   */
+public interface LoggerRepositoryEx extends LoggerRepository {
+  /**
+    Add a {@link LoggerRepositoryEventListener} to the repository. The
+    listener will be called when repository events occur.
+     @param listener event listener, may not be null.
+    */
+  void addLoggerRepositoryEventListener(
+    LoggerRepositoryEventListener listener);
+
+  /**
+    Remove a {@link LoggerRepositoryEventListener} from the repository.
+   @param listener listener.
+    */
+  void removeLoggerRepositoryEventListener(
+    LoggerRepositoryEventListener listener);
+
+  /**
+    Add a {@link LoggerEventListener} to the repository. The  listener
+    will be called when repository events occur.
+   @param listener listener, may not be null.
+    */
+  void addLoggerEventListener(LoggerEventListener listener);
+
+  /**
+    Remove a {@link LoggerEventListener} from the repository.
+   @param listener listener, may not be null.
+    */
+  void removeLoggerEventListener(LoggerEventListener listener);
+
+  /**
+   * Get the name of this logger repository.
+   * @return name, may not be null.
+   */
+  String getName();
+
+  /**
+   * A logger repository is a named entity.
+   * @param repoName new name, may not be null.
+   */
+  void setName(String repoName);
+
+  /**
+   * Is the current configuration of the repository in its original (pristine)
+   * state?
+   * @return true if repository is in original state.
+   *
+   */
+  boolean isPristine();
+
+  /**
+   *  Set the pristine flag.
+   * @param state state
+   *  @see #isPristine
+   */
+  void setPristine(boolean state);
+
+  /**
+    Requests that a appender removed event be sent to any registered
+    {@link LoggerEventListener}.
+    @param logger The logger from which the appender was removed.
+    @param appender The appender removed from the logger.
+    */
+  void fireRemoveAppenderEvent(Category logger, Appender appender);
+
+  /**
+    Requests that a level changed event be sent to any registered
+    {@link LoggerEventListener}.
+    @param logger The logger which changed levels.
+    */
+  void fireLevelChangedEvent(Logger logger);
+
+  /**
+    Requests that a configuration changed event be sent to any registered
+    {@link LoggerRepositoryEventListener}.
+    */
+  void fireConfigurationChangedEvent();
+
+  /**
+   * Return the PluginRegisty for this LoggerRepository.
+   * @return plug in registry.
+   */
+  PluginRegistry getPluginRegistry();
+
+  /**
+   * Return the {@link Scheduler} for this LoggerRepository.
+   * @return scheduler.
+   */
+  Scheduler getScheduler();
+
+  /**
+   * Get the properties specific for this repository.
+   * @return property map.
+   */
+  Map getProperties();
+
+  /**
+   * Get the property of this repository.
+   * @param key property key.
+   * @return key value or null if not set.
+   */
+  String getProperty(String key);
+
+  /**
+   * Set a property of this repository.
+   * @param key key, may not be null.
+   * @param value new value, if null, property will be removed.
+   */
+  void setProperty(String key, String value);
+
+  /**
+   * Errors which cannot be logged, go to the error list.
+   *
+   * @return List
+   */
+  List getErrorList();
+
+  /**
+   * Errors which cannot be logged, go to the error list.
+   *
+   * @param errorItem an ErrorItem to add to the error list
+   */
+  void addErrorItem(ErrorItem errorItem);
+
+  /**
+   * A LoggerRepository can also act as a store for various objects used
+   * by log4j components.
+   *
+   * @param key key, may not be null.
+   * @return The object stored under 'key'.
+   */
+  Object getObject(String key);
+
+  /**
+   * Store an object under 'key'. If no object can be found, null is returned.
+   *
+   * @param key key, may not be null.
+   * @param value value, may be null.
+   */
+  void putObject(String key, Object value);
+
+  /**
+   * Sets the logger factory used by LoggerRepository.getLogger(String).
+   * @param loggerFactory factory to use, may not be null
+   */
+  void setLoggerFactory(LoggerFactory loggerFactory);
+
+  /**
+   * Returns the logger factory used by
+   * LoggerRepository.getLogger(String).
+   *
+   * @return non-null factory
+   */
+  LoggerFactory getLoggerFactory();
+
+}
diff --git a/src/main/java/org/apache/log4j/spi/NOPULogger.java b/src/main/java/org/apache/log4j/spi/NOPULogger.java
new file mode 100644
index 0000000..98fe537
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/NOPULogger.java
@@ -0,0 +1,200 @@
+/*
+ * 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 org.apache.log4j.ULogger;
+
+
+/**
+ * A no operation (NOP) implementation of {@link ULogger}.
+ *
+ * @author Ceki G&uuml;lc&uuml;
+ */
+public final class NOPULogger implements ULogger {
+
+    /**
+     * The unique instance of NOPLogger.
+     */
+    public static final NOPULogger NOP_LOGGER = new NOPULogger();
+
+    /**
+     * There is no point in people creating multiple instances of NullLogger.
+     * Hence, the private access modifier.
+     */
+    private NOPULogger() {
+        super();
+    }
+
+    /**
+     * Get instance.
+     * @param name logger name.
+     * @return logger.
+     */
+    public static NOPULogger getLogger(final String name) {
+        return NOP_LOGGER;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isDebugEnabled() {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void debug(final Object msg) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void debug(final Object parameterizedMsg, final Object param1) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void debug(final String parameterizedMsg,
+                      final Object param1,
+                      final Object param2) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void debug(final Object msg, final Throwable t) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isInfoEnabled() {
+        // NOP
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void info(final Object msg) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void info(final Object parameterizedMsg, final Object param1) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void info(final String parameterizedMsg,
+                     final Object param1, final Object param2) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void info(final Object msg, final Throwable t) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isWarnEnabled() {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void warn(final Object msg) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void warn(final Object parameterizedMsg,
+                     final Object param1) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void warn(final String parameterizedMsg,
+                     final Object param1,
+                     final Object param2) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void warn(final Object msg, final Throwable t) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isErrorEnabled() {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void error(final Object msg) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void error(final Object parameterizedMsg, final Object param1) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void error(final String parameterizedMsg,
+                      final Object param1,
+                      final Object param2) {
+        // NOP
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void error(final Object msg, final Throwable t) {
+        // NOP
+    }
+
+}
diff --git a/src/main/java/org/apache/log4j/spi/SimpleULogger.java b/src/main/java/org/apache/log4j/spi/SimpleULogger.java
new file mode 100644
index 0000000..714a57a
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/SimpleULogger.java
@@ -0,0 +1,305 @@
+/*
+ * 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 org.apache.log4j.ULogger;
+import org.apache.log4j.helpers.MessageFormatter;
+
+
+/**
+ * A simple implementation that logs messages of level INFO or higher on
+ * the console (<code>System.out</code>).
+ * <p>
+ * The output includes the relative time in milliseconds, thread name, level,
+ * logger name, and the message followed by the line separator for the host.
+ * In log4j terms it amounts to the "%r  [%t] %level %logger - %m%n" pattern.
+ * <pre>
+176 [main] INFO examples.Sort - Populating an array of 2 elements in reverse.
+225 [main] INFO examples.SortAlgo - Entered the sort method.
+304 [main] INFO SortAlgo.DUMP - Dump of interger array:
+317 [main] INFO SortAlgo.DUMP - Element [0] = 0
+331 [main] INFO SortAlgo.DUMP - Element [1] = 1
+343 [main] INFO examples.Sort - The next log statement should be an error msg.
+346 [main] ERROR SortAlgo.DUMP - Tried to dump an uninitialized array.
+        at org.log4j.examples.SortAlgo.dump(SortAlgo.java:58)
+        at org.log4j.examples.Sort.main(Sort.java:64)
+467 [main] INFO  examples.Sort - Exiting main method.
+</pre>
+ *
+ * @author Ceki G&uuml;lc&uuml;
+ */
+public final class SimpleULogger implements ULogger {
+
+    /**
+     * Logger name.
+     */
+  private final String loggerName;
+
+
+  /**
+   * Mark the time when this class gets loaded into memory.
+   */
+  private static long startTime = System.currentTimeMillis();
+
+    /**
+     * Line separator.
+     */
+  public static final String LINE_SEPARATOR
+            = System.getProperty("line.separator");
+
+    /**
+     * INFO string literal.
+     */
+  private static final String INFO_STR = "INFO";
+    /**
+     * WARN string literal.
+     */
+  private static final String WARN_STR = "WARN";
+    /**
+     * ERROR string literal.
+     */
+  private static final String ERROR_STR = "ERROR";
+
+  /**
+   * Constructor is private to force construction through getLogger.
+   * @param name logger name
+   */
+  private SimpleULogger(final String name) {
+    super();
+    this.loggerName = name;
+  }
+
+  /**
+   * Creates a new instance.
+   *
+   * @param name logger name
+   * @return  logger.
+   */
+  public static SimpleULogger getLogger(final String name) {
+      return new SimpleULogger(name);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public boolean isDebugEnabled() {
+    return false;
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void debug(final Object msg) {
+    // NOP
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void debug(final Object parameterizedMsg, final Object param1) {
+    // NOP
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void debug(final String parameterizedMsg,
+                    final Object param1,
+                    final Object param2) {
+    // NOP
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void debug(final Object msg, final Throwable t) {
+    // NOP
+  }
+
+  /**
+   * This is our internal implementation for logging regular (non-parameterized)
+   * log messages.
+   *
+   * @param level level
+   * @param message message
+   * @param t throwable
+   */
+  private void log(final String level,
+                   final String message,
+                   final Throwable t) {
+    StringBuffer buf = new StringBuffer();
+
+    long millis  = System.currentTimeMillis();
+    buf.append(millis - startTime);
+
+    buf.append(" [");
+    buf.append(Thread.currentThread().getName());
+    buf.append("] ");
+
+    buf.append(level);
+    buf.append(" ");
+
+    buf.append(loggerName);
+    buf.append(" - ");
+
+    buf.append(message);
+
+    buf.append(LINE_SEPARATOR);
+
+    System.out.print(buf.toString());
+    if (t != null) {
+      t.printStackTrace(System.out);
+    }
+    System.out.flush();
+  }
+  /**
+   * For parameterized messages, first substitute parameters and then log.
+   *
+   * @param level level
+   * @param parameterizedMsg message pattern
+   * @param param1 param1
+   * @param param2 param2
+   */
+  private void parameterizedLog(final String level,
+                                final Object parameterizedMsg,
+                                final Object param1,
+                                final Object param2) {
+    if (parameterizedMsg instanceof String) {
+      String msgStr = (String) parameterizedMsg;
+      msgStr = MessageFormatter.format(msgStr, param1, param2);
+      log(level, msgStr, null);
+    } else {
+      // To be failsafe, we handle the case where 'messagePattern' is not
+      // a String. Unless the user makes a mistake, this should not happen.
+      log(level, parameterizedMsg.toString(), null);
+    }
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public boolean isInfoEnabled() {
+    return true;
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void info(final Object msg) {
+    log(INFO_STR, msg.toString(), null);
+  }
+
+
+    /**
+     * {@inheritDoc}
+     */
+  public void info(final Object parameterizedMsg, final Object param1) {
+    parameterizedLog(INFO_STR, parameterizedMsg, param1, null);
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void info(final String parameterizedMsg,
+                   final Object param1,
+                   final Object param2) {
+    parameterizedLog(INFO_STR, parameterizedMsg, param1, param2);
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void info(final Object msg, final Throwable t) {
+    log(INFO_STR, msg.toString(), t);
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public boolean isWarnEnabled() {
+    return true;
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void warn(final Object msg) {
+    log(WARN_STR, msg.toString(), null);
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void warn(final Object parameterizedMsg, final Object param1) {
+    parameterizedLog(WARN_STR, parameterizedMsg, param1, null);
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void warn(final String parameterizedMsg,
+                   final Object param1,
+                   final Object param2) {
+    parameterizedLog(WARN_STR, parameterizedMsg, param1, param2);
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void warn(final Object msg, final Throwable t) {
+    log(WARN_STR, msg.toString(), t);
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public boolean isErrorEnabled() {
+    return true;
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void error(final Object msg) {
+    log(ERROR_STR, msg.toString(), null);
+  }
+
+
+    /**
+     * {@inheritDoc}
+     */
+  public void error(final Object parameterizedMsg, final Object param1) {
+    parameterizedLog(ERROR_STR, parameterizedMsg, param1, null);
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void error(final String parameterizedMsg,
+                    final Object param1,
+                    final Object param2) {
+    parameterizedLog(ERROR_STR, parameterizedMsg, param1, param2);
+  }
+
+    /**
+     * {@inheritDoc}
+     */
+  public void error(final Object msg, final Throwable t) {
+    log(ERROR_STR, msg.toString(), t);
+  }
+
+}
diff --git a/src/main/java/org/apache/log4j/spi/Thresholdable.java b/src/main/java/org/apache/log4j/spi/Thresholdable.java
new file mode 100644
index 0000000..222345f
--- /dev/null
+++ b/src/main/java/org/apache/log4j/spi/Thresholdable.java
@@ -0,0 +1,58 @@
+/*
+ * 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 org.apache.log4j.Level;
+
+/**
+ * An interface that defines the required methods for supporting the
+ * setting and getting of a level threshold.  Components should implement
+ * this interface if logging events they process should meet a certain
+ * threshold before being processed further.  Examples of this are
+ * Appenders and Receivers which will not process logging events unless
+ * the event level is at or greater than a set threshold level.
+ *
+ * @author Paul Smith (psmith@apache.org)
+ * @author Mark Womack
+ */
+public interface Thresholdable {
+    /**
+     * Sets the component theshold to the given level.
+     *
+     * @param level The threshold level events must equal or be greater
+     *              than before further processing can be done.
+     */
+    void setThreshold(Level level);
+
+    /**
+     * Gets the current threshold setting of the component.
+     *
+     * @return Level The current threshold level of the component.
+     */
+    Level getThreshold();
+
+    /**
+     * Returns true if the given level is equals or greater than the current
+     * threshold value of the component.
+     *
+     * @param level The level to test against the component threshold.
+     * @return boolean True if level is equal or greater than the
+     *         component threshold.
+     */
+    boolean isAsSevereAsThreshold(Level level);
+}
diff --git a/src/main/java/org/apache/log4j/varia/ListModelAppender.java b/src/main/java/org/apache/log4j/varia/ListModelAppender.java
new file mode 100644
index 0000000..ccbc9be
--- /dev/null
+++ b/src/main/java/org/apache/log4j/varia/ListModelAppender.java
@@ -0,0 +1,79 @@
+/*
+ * 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.varia;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.spi.LoggingEvent;
+
+import javax.swing.DefaultListModel;
+import javax.swing.ListModel;
+
+
+/**
+ * A very basic appender that takes the events and stores them in to a
+ * ListModel for late retrieval.
+ *
+ *
+ * @author Paul Smith (psmith@apache.org)
+ *
+ */
+public final class ListModelAppender extends AppenderSkeleton {
+    /**
+     * Default list model.
+     */
+  private final DefaultListModel model = new DefaultListModel();
+
+  /**
+   * Constructs a ListModelAppender.
+   */
+  public ListModelAppender() {
+      super(true);
+  }
+  /**
+   * Returns a reference to the ListModel that contains all the LoggingEvents
+   * that have been appended to this class.
+   *
+   * @return the list model
+   */
+  public ListModel getModel() {
+    return model;
+  }
+
+    /** {@inheritDoc} */
+  protected void append(final LoggingEvent event) {
+    model.addElement(event);
+  }
+
+    /** {@inheritDoc} */
+  public void close() {
+    clearModel();
+  }
+
+  /**
+   * Removes all the Events from the model.
+   */
+  public void clearModel() {
+    model.clear();
+  }
+
+    /** {@inheritDoc} */
+  public boolean requiresLayout() {
+      return false;
+  }
+
+}
diff --git a/src/main/java/org/apache/log4j/varia/LogFilePatternReceiver.java b/src/main/java/org/apache/log4j/varia/LogFilePatternReceiver.java
new file mode 100644
index 0000000..4430205
--- /dev/null
+++ b/src/main/java/org/apache/log4j/varia/LogFilePatternReceiver.java
@@ -0,0 +1,1043 @@
+/*
+ * 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.varia;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.regex.MatchResult;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.helpers.Constants;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.rule.ExpressionRule;
+import org.apache.log4j.rule.Rule;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
+
+/**
+ * LogFilePatternReceiver can parse and tail log files, converting entries into
+ * LoggingEvents.  If the file doesn't exist when the receiver is initialized, the
+ * receiver will look for the file once every 10 seconds.
+ * <p>
+ * This receiver relies on java.util.regex features to perform the parsing of text in the
+ * log file, however the only regular expression field explicitly supported is 
+ * a glob-style wildcard used to ignore fields in the log file if needed.  All other
+ * fields are parsed by using the supplied keywords.
+ * <p>
+ * <b>Features:</b><br>
+ * - specify the URL of the log file to be processed<br>
+ * - specify the timestamp format in the file (if one exists, using patterns from {@link java.text.SimpleDateFormat})<br>
+ * - specify the pattern (logFormat) used in the log file using keywords, a wildcard character (*) and fixed text<br>
+ * - 'tail' the file (allows the contents of the file to be continually read and new events processed)<br>
+ * - supports the parsing of multi-line messages and exceptions
+ * - 'hostname' property set to URL host (or 'file' if not available)
+ * - 'application' property set to URL path (or value of fileURL if not available) 
+ *<p>
+ * <b>Keywords:</b><br>
+ * TIMESTAMP<br>
+ * LOGGER<br>
+ * LEVEL<br>
+ * THREAD<br>
+ * CLASS<br>
+ * FILE<br>
+ * LINE<br>
+ * METHOD<br>
+ * RELATIVETIME<br>
+ * MESSAGE<br>
+ * NDC<br>
+ * PROP(key)<br>
+ * <p>
+ * Use a * to ignore portions of the log format that should be ignored
+ * <p>
+ * Example:<br>
+ * If your file's patternlayout is this:<br>
+ * <b>%d %-5p [%t] %C{2} (%F:%L) - %m%n</b>
+ *<p>
+ * specify this as the log format:<br>
+ * <b>TIMESTAMP LEVEL [THREAD] CLASS (FILE:LINE) - MESSAGE</b>
+ *<p>
+ * To define a PROPERTY field, use PROP(key)
+ * <p>
+ * Example:<br> 
+ * If you used the RELATIVETIME pattern layout character in the file, 
+ * you can use PROP(RELATIVETIME) in the logFormat definition to assign 
+ * the RELATIVETIME field as a property on the event.
+ * <p>
+ * If your file's patternlayout is this:<br>
+ * <b>%r [%t] %-5p %c %x - %m%n</b>
+ *<p>
+ * specify this as the log format:<br>
+ * <b>PROP(RELATIVETIME) [THREAD] LEVEL LOGGER * - MESSAGE</b>
+ * <p>
+ * Note the * - it can be used to ignore a single word or sequence of words in the log file
+ * (in order for the wildcard to ignore a sequence of words, the text being ignored must be
+ *  followed by some delimiter, like '-' or '[') - ndc is being ignored in the following example.
+ * <p>
+ * Assign a filterExpression in order to only process events which match a filter.
+ * If a filterExpression is not assigned, all events are processed.
+ *<p>
+ * <b>Limitations:</b><br>
+ * - no support for the single-line version of throwable supported by patternlayout<br>
+ *   (this version of throwable will be included as the last line of the message)<br>
+ * - the relativetime patternLayout character must be set as a property: PROP(RELATIVETIME)<br>
+ * - messages should appear as the last field of the logFormat because the variability in message content<br>
+ * - exceptions are converted if the exception stack trace (other than the first line of the exception)<br>
+ *   is stored in the log file with a tab followed by the word 'at' as the first characters in the line<br>
+ * - tailing may fail if the file rolls over. 
+ *<p>
+ * <b>Example receiver configuration settings</b> (add these as params, specifying a LogFilePatternReceiver 'plugin'):<br>
+ * param: "timestampFormat" value="yyyy-MM-d HH:mm:ss,SSS"<br>
+ * param: "logFormat" value="PROP(RELATIVETIME) [THREAD] LEVEL LOGGER * - MESSAGE"<br>
+ * param: "fileURL" value="file:///c:/events.log"<br>
+ * param: "tailing" value="true"
+ *<p>
+ * This configuration will be able to process these sample events:<br>
+ * 710    [       Thread-0] DEBUG                   first.logger first - <test>   <test2>something here</test2>   <test3 blah=something/>   <test4>       <test5>something else</test5>   </test4></test><br>
+ * 880    [       Thread-2] DEBUG                   first.logger third - <test>   <test2>something here</test2>   <test3 blah=something/>   <test4>       <test5>something else</test5>   </test4></test><br>
+ * 880    [       Thread-0] INFO                    first.logger first - infomsg-0<br>
+ * java.lang.Exception: someexception-first<br>
+ *     at Generator2.run(Generator2.java:102)<br>
+ *
+ *@author Scott Deboy
+ */
+public class LogFilePatternReceiver extends Receiver {
+  private final List keywords = new ArrayList();
+
+  private static final String PROP_START = "PROP(";
+  private static final String PROP_END = ")";
+
+  private static final String LOGGER = "LOGGER";
+  private static final String MESSAGE = "MESSAGE";
+  private static final String TIMESTAMP = "TIMESTAMP";
+  private static final String NDC = "NDC";
+  private static final String LEVEL = "LEVEL";
+  private static final String THREAD = "THREAD";
+  private static final String CLASS = "CLASS";
+  private static final String FILE = "FILE";
+  private static final String LINE = "LINE";
+  private static final String METHOD = "METHOD";
+  
+  private static final String DEFAULT_HOST = "file";
+  
+  //all lines other than first line of exception begin with tab followed by 'at' followed by text
+  private static final String EXCEPTION_PATTERN = "^\\s+at.*";
+  private static final String REGEXP_DEFAULT_WILDCARD = ".*?";
+  private static final String REGEXP_GREEDY_WILDCARD = ".*";
+  private static final String PATTERN_WILDCARD = "*";
+  private static final String NOSPACE_GROUP = "(\\S*\\s*?)";
+  private static final String DEFAULT_GROUP = "(" + REGEXP_DEFAULT_WILDCARD + ")";
+  private static final String GREEDY_GROUP = "(" + REGEXP_GREEDY_WILDCARD + ")";
+  private static final String MULTIPLE_SPACES_REGEXP = "[ ]+";
+  private final String newLine = System.getProperty("line.separator");
+
+  private final String[] emptyException = new String[] { "" };
+
+  private SimpleDateFormat dateFormat;
+  private String timestampFormat = "yyyy-MM-d HH:mm:ss,SSS";
+  private String logFormat;
+  private String customLevelDefinitions;
+  private String fileURL;
+  private String host;
+  private String path;
+  private boolean tailing;
+  private String filterExpression;
+  private long waitMillis = 2000; //default 2 seconds
+
+  private static final String VALID_DATEFORMAT_CHARS = "GyMwWDdFEaHkKhmsSzZ";
+  private static final String VALID_DATEFORMAT_CHAR_PATTERN = "[" + VALID_DATEFORMAT_CHARS + "]";
+
+  private Rule expressionRule;
+
+  private Map currentMap;
+  private List additionalLines;
+  private List matchingKeywords;
+
+  private String regexp;
+  private Reader reader;
+  private Pattern regexpPattern;
+  private Pattern exceptionPattern;
+  private String timestampPatternText;
+
+  private boolean useCurrentThread;
+  public static final int MISSING_FILE_RETRY_MILLIS = 10000;
+  private boolean appendNonMatches;
+  private final Map customLevelDefinitionMap = new HashMap();
+
+    public LogFilePatternReceiver() {
+    keywords.add(TIMESTAMP);
+    keywords.add(LOGGER);
+    keywords.add(LEVEL);
+    keywords.add(THREAD);
+    keywords.add(CLASS);
+    keywords.add(FILE);
+    keywords.add(LINE);
+    keywords.add(METHOD);
+    keywords.add(MESSAGE);
+    keywords.add(NDC);
+    try {
+        exceptionPattern = Pattern.compile(EXCEPTION_PATTERN);
+    } catch (PatternSyntaxException pse) {
+        //shouldn't happen
+    }
+  }
+
+  /**
+   * Accessor
+   * 
+   * @return file URL
+   */
+  public String getFileURL() {
+    return fileURL;
+  }
+
+  /**
+   * Mutator
+   * 
+   * @param fileURL
+   */
+  public void setFileURL(String fileURL) {
+    this.fileURL = fileURL;
+  }
+
+    /**
+     * If the log file contains non-log4j level strings, they can be mapped to log4j levels using the format (android example):
+     * V=TRACE,D=DEBUG,I=INFO,W=WARN,E=ERROR,F=FATAL,S=OFF
+     *
+     * @param customLevelDefinitions the level definition string
+     */
+  public void setCustomLevelDefinitions(String customLevelDefinitions) {
+    this.customLevelDefinitions = customLevelDefinitions;
+  }
+
+  public String getCustomLevelDefinitions() {
+    return customLevelDefinitions;
+  }
+
+  /**
+   * Accessor
+   * @return append non matches
+   */
+  public boolean isAppendNonMatches() {
+      return appendNonMatches;
+  }
+
+  /**
+   * Mutator
+   * @param appendNonMatches
+   */
+  public void setAppendNonMatches(boolean appendNonMatches) {
+      this.appendNonMatches = appendNonMatches;
+  }
+
+  /**
+   * Accessor
+   * 
+   * @return filter expression
+   */
+  public String getFilterExpression() {
+    return filterExpression;
+  }
+
+  /**
+   * Mutator
+   * 
+   * @param filterExpression
+   */
+  public void setFilterExpression(String filterExpression) {
+    this.filterExpression = filterExpression;
+  }
+
+  /**
+   * Accessor
+   * 
+   * @return tailing
+   */
+  public boolean isTailing() {
+    return tailing;
+  }
+
+  /**
+   * Mutator
+   * 
+   * @param tailing
+   */
+  public void setTailing(boolean tailing) {
+    this.tailing = tailing;
+  }
+
+  /**
+   * When true, this property uses the current Thread to perform the import,
+   * otherwise when false (the default), a new Thread is created and started to manage
+   * the import.
+   * @return
+   */ 
+ public final boolean isUseCurrentThread() {
+     return useCurrentThread;
+ }
+
+ /**
+  * Sets whether the current Thread or a new Thread is created to perform the import,
+  * the default being false (new Thread created).
+  * 
+  * @param useCurrentThread
+  */
+ public final void setUseCurrentThread(boolean useCurrentThread) {
+     this.useCurrentThread = useCurrentThread;
+ }
+
+  /**
+   * Accessor
+   * 
+   * @return log format
+   */
+  public String getLogFormat() {
+    return logFormat;
+  }
+
+    /**
+   * Mutator
+   *
+   * @param logFormat
+   *          the format
+   */
+  public void setLogFormat(String logFormat) {
+    this.logFormat = logFormat;
+  }
+
+    /**
+   * Mutator.  Specify a pattern from {@link java.text.SimpleDateFormat}
+   *
+   * @param timestampFormat
+   */
+  public void setTimestampFormat(String timestampFormat) {
+    this.timestampFormat = timestampFormat;
+  }
+
+    /**
+   * Accessor
+   *
+   * @return timestamp format
+   */
+  public String getTimestampFormat() {
+    return timestampFormat;
+  }
+
+  /**
+   * Accessor
+   * @return millis between retrieves of content
+   */
+  public long getWaitMillis() {
+    return waitMillis;
+  }
+
+  /**
+   * Mutator
+   * @param waitMillis
+   */
+  public void setWaitMillis(long waitMillis) {
+    this.waitMillis = waitMillis;
+  }
+
+    /**
+   * Walk the additionalLines list, looking for the EXCEPTION_PATTERN.
+   * <p>
+   * Return the index of the first matched line
+   * (the match may be the 1st line of an exception)
+   * <p>
+   * Assumptions: <br>
+   * - the additionalLines list may contain both message and exception lines<br>
+   * - message lines are added to the additionalLines list and then
+   * exception lines (all message lines occur in the list prior to all
+   * exception lines)
+   *
+   * @return -1 if no exception line exists, line number otherwise
+   */
+  private int getExceptionLine() {
+    for (int i = 0; i < additionalLines.size(); i++) {
+      Matcher exceptionMatcher = exceptionPattern.matcher((String)additionalLines.get(i));
+      if (exceptionMatcher.matches()) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+    /**
+   * Combine all message lines occuring in the additionalLines list, adding
+   * a newline character between each line
+   * <p>
+   * the event will already have a message - combine this message
+   * with the message lines in the additionalLines list
+   * (all entries prior to the exceptionLine index)
+   *
+   * @param firstMessageLine primary message line
+   * @param exceptionLine index of first exception line
+   * @return message
+   */
+  private String buildMessage(String firstMessageLine, int exceptionLine) {
+    if (additionalLines.size() == 0) {
+      return firstMessageLine;
+    }
+    StringBuffer message = new StringBuffer();
+    if (firstMessageLine != null) {
+      message.append(firstMessageLine);
+    }
+
+    int linesToProcess = (exceptionLine == -1?additionalLines.size(): exceptionLine);
+
+    for (int i = 0; i < linesToProcess; i++) {
+      message.append(newLine);
+      message.append(additionalLines.get(i));
+    }
+    return message.toString();
+  }
+
+    /**
+   * Combine all exception lines occuring in the additionalLines list into a
+   * String array
+   * <p>
+   * (all entries equal to or greater than the exceptionLine index)
+   *
+   * @param exceptionLine index of first exception line
+   * @return exception
+   */
+  private String[] buildException(int exceptionLine) {
+    if (exceptionLine == -1) {
+      return emptyException;
+    }
+    String[] exception = new String[additionalLines.size() - exceptionLine - 1];
+    for (int i = 0; i < exception.length; i++) {
+      exception[i] = (String) additionalLines.get(i + exceptionLine);
+    }
+    return exception;
+  }
+
+    /**
+   * Construct a logging event from currentMap and additionalLines
+   * (additionalLines contains multiple message lines and any exception lines)
+   * <p>
+   * CurrentMap and additionalLines are cleared in the process
+   *
+   * @return event
+   */
+  private LoggingEvent buildEvent() {
+    if (currentMap.size() == 0) {
+      if (additionalLines.size() > 0) {
+        for (Iterator iter = additionalLines.iterator();iter.hasNext();) {
+          getLogger().info("found non-matching line: " + iter.next());
+        }
+      }
+      additionalLines.clear();
+      return null;
+    }
+    //the current map contains fields - build an event
+    int exceptionLine = getExceptionLine();
+    String[] exception = buildException(exceptionLine);
+
+    //messages are listed before exceptions in additionallines
+    if (additionalLines.size() > 0 && exception.length > 0) {
+      currentMap.put(MESSAGE, buildMessage((String) currentMap.get(MESSAGE),
+          exceptionLine));
+    }
+    LoggingEvent event = convertToEvent(currentMap, exception);
+    currentMap.clear();
+    additionalLines.clear();
+    return event;
+  }
+
+    /**
+   * Read, parse and optionally tail the log file, converting entries into logging events.
+   *
+   * A runtimeException is thrown if the logFormat pattern is malformed.
+   *
+   * @param bufferedReader
+   * @throws IOException
+   */
+  protected void process(BufferedReader bufferedReader) throws IOException {
+        Matcher eventMatcher;
+        Matcher exceptionMatcher;
+        String line;
+        while ((line = bufferedReader.readLine()) != null) {
+            //skip empty line entries
+            eventMatcher = regexpPattern.matcher(line);
+            if (line.trim().equals("")) {continue;}
+            exceptionMatcher = exceptionPattern.matcher(line);
+            if (eventMatcher.matches()) {
+                //build an event from the previous match (held in current map)
+                LoggingEvent event = buildEvent();
+                if (event != null) {
+                    if (passesExpression(event)) {
+                        doPost(event);
+                    }
+                }
+                currentMap.putAll(processEvent(eventMatcher.toMatchResult()));
+            } else if (exceptionMatcher.matches()) {
+                //an exception line
+                additionalLines.add(line);
+            } else {
+                //neither...either post an event with the line or append as additional lines
+                //if this was a logging event with multiple lines, each line will show up as its own event instead of being
+                //appended as multiple lines on the same event..
+                //choice is to have each non-matching line show up as its own line, or append them all to a previous event
+                if (appendNonMatches) {
+                    //hold on to the previous time, so we can do our best to preserve time-based ordering if the event is a non-match
+                    String lastTime = (String)currentMap.get(TIMESTAMP);
+                    //build an event from the previous match (held in current map)
+                    if (currentMap.size() > 0) {
+                        LoggingEvent event = buildEvent();
+                        if (event != null) {
+                            if (passesExpression(event)) {
+                              doPost(event);
+                            }
+                        }
+                    }
+                    if (lastTime != null) {
+                        currentMap.put(TIMESTAMP, lastTime);
+                    }
+                    currentMap.put(MESSAGE, line);
+                } else {
+                    additionalLines.add(line);
+                }
+            }
+        }
+
+        //process last event if one exists
+        LoggingEvent event = buildEvent();
+        if (event != null) {
+            if (passesExpression(event)) {
+                doPost(event);
+            }
+        }
+    }
+
+    protected void createPattern() {
+        regexpPattern = Pattern.compile(regexp);
+    }
+
+    /**
+   * Helper method that supports the evaluation of the expression
+   *
+   * @param event
+   * @return true if expression isn't set, or the result of the evaluation otherwise
+   */
+  private boolean passesExpression(LoggingEvent event) {
+    if (event != null) {
+      if (expressionRule != null) {
+        return (expressionRule.evaluate(event, null));
+      }
+    }
+    return true;
+  }
+
+    /**
+   * Convert the match into a map.
+   * <p>
+   * Relies on the fact that the matchingKeywords list is in the same
+   * order as the groups in the regular expression
+   *
+   * @param result
+   * @return map
+   */
+  private Map processEvent(MatchResult result) {
+    Map map = new HashMap();
+    //group zero is the entire match - process all other groups
+    for (int i = 1; i < result.groupCount() + 1; i++) {
+      Object key = matchingKeywords.get(i - 1);
+      Object value = result.group(i);
+      map.put(key, value);
+
+    }
+    return map;
+  }
+
+    /**
+   * Helper method that will convert timestamp format to a pattern
+   *
+   *
+   * @return string
+   */
+  private String convertTimestamp() {
+    //some locales (for example, French) generate timestamp text with characters not included in \w -
+    // now using \S (all non-whitespace characters) instead of /w 
+    String result = timestampFormat.replaceAll(VALID_DATEFORMAT_CHAR_PATTERN + "+", "\\\\S+");
+    //make sure dots in timestamp are escaped
+    result = result.replaceAll(Pattern.quote("."), "\\\\.");
+    return result;
+  }
+
+    protected void setHost(String host) {
+	  this.host = host;
+  }
+
+    protected void setPath(String path) {
+	  this.path = path;
+  }
+
+  public String getPath() {
+      return path;
+  }
+
+    /**
+   * Build the regular expression needed to parse log entries
+   *
+   */
+  protected void initialize() {
+	if (host == null && path == null) {
+		try {
+			URL url = new URL(fileURL);
+			host = url.getHost();
+			path = url.getPath();
+		} catch (MalformedURLException e1) {
+			// TODO Auto-generated catch block
+			e1.printStackTrace();
+		}
+	}
+	if (host == null || host.trim().equals("")) {
+		host = DEFAULT_HOST;
+	}
+	if (path == null || path.trim().equals("")) {
+		path = fileURL;
+	}
+
+    currentMap = new HashMap();
+    additionalLines = new ArrayList();
+    matchingKeywords = new ArrayList();
+
+    if (timestampFormat != null) {
+      dateFormat = new SimpleDateFormat(quoteTimeStampChars(timestampFormat));
+      timestampPatternText = convertTimestamp();
+    }
+    //if custom level definitions exist, parse them
+    updateCustomLevelDefinitionMap();
+    try {
+      if (filterExpression != null) {
+        expressionRule = ExpressionRule.getRule(filterExpression);
+      }
+    } catch (Exception e) {
+      getLogger().warn("Invalid filter expression: " + filterExpression, e);
+    }
+
+    List buildingKeywords = new ArrayList();
+
+    String newPattern = logFormat;
+
+    int index = 0;
+    String current = newPattern;
+    //build a list of property names and temporarily replace the property with an empty string,
+    //we'll rebuild the pattern later
+    List propertyNames = new ArrayList();
+    while (index > -1) {
+        if (current.indexOf(PROP_START) > -1 && current.indexOf(PROP_END) > -1) {
+            index = current.indexOf(PROP_START);
+            String longPropertyName = current.substring(current.indexOf(PROP_START), current.indexOf(PROP_END) + 1);
+            String shortProp = getShortPropertyName(longPropertyName);
+            buildingKeywords.add(shortProp);
+            propertyNames.add(longPropertyName);
+            current = current.substring(longPropertyName.length() + 1 + index);
+            newPattern = singleReplace(newPattern, longPropertyName, new Integer(buildingKeywords.size() -1).toString());
+        } else {
+            //no properties
+            index = -1;
+        }
+    }
+
+    /*
+     * we're using a treemap, so the index will be used as the key to ensure
+     * keywords are ordered correctly
+     *
+     * examine pattern, adding keywords to an index-based map patterns can
+     * contain only one of these per entry...properties are the only 'keyword'
+     * that can occur multiple times in an entry
+     */
+    Iterator iter = keywords.iterator();
+    while (iter.hasNext()) {
+      String keyword = (String) iter.next();
+      int index2 = newPattern.indexOf(keyword);
+      if (index2 > -1) {
+        buildingKeywords.add(keyword);
+        newPattern = singleReplace(newPattern, keyword, new Integer(buildingKeywords.size() -1).toString());
+      }
+    }
+
+    String buildingInt = "";
+
+    for (int i=0;i<newPattern.length();i++) {
+        String thisValue = String.valueOf(newPattern.substring(i, i+1));
+        if (isInteger(thisValue)) {
+            buildingInt = buildingInt + thisValue;
+        } else {
+            if (isInteger(buildingInt)) {
+                matchingKeywords.add(buildingKeywords.get(Integer.parseInt(buildingInt)));
+            }
+            //reset
+            buildingInt = "";
+        }
+    }
+
+    //if the very last value is an int, make sure to add it
+    if (isInteger(buildingInt)) {
+        matchingKeywords.add(buildingKeywords.get(Integer.parseInt(buildingInt)));
+    }
+
+    newPattern = replaceMetaChars(newPattern);
+
+    //compress one or more spaces in the pattern into the [ ]+ regexp
+    //(supports padding of level in log files)
+    newPattern = newPattern.replaceAll(MULTIPLE_SPACES_REGEXP, MULTIPLE_SPACES_REGEXP);
+    newPattern = newPattern.replaceAll(Pattern.quote(PATTERN_WILDCARD), REGEXP_DEFAULT_WILDCARD);
+    //use buildingKeywords here to ensure correct order
+    for (int i = 0;i<buildingKeywords.size();i++) {
+      String keyword = (String) buildingKeywords.get(i);
+      //make the final keyword greedy (we're assuming it's the message)
+      if (i == (buildingKeywords.size() - 1)) {
+        newPattern = singleReplace(newPattern, String.valueOf(i), GREEDY_GROUP);
+      } else if (TIMESTAMP.equals(keyword)) {
+        newPattern = singleReplace(newPattern, String.valueOf(i), "(" + timestampPatternText + ")");
+      } else if (LOGGER.equals(keyword) || LEVEL.equals(keyword)) {
+        newPattern = singleReplace(newPattern, String.valueOf(i), NOSPACE_GROUP);
+      } else {
+        newPattern = singleReplace(newPattern, String.valueOf(i), DEFAULT_GROUP);
+      }
+    }
+
+    regexp = newPattern;
+    getLogger().debug("regexp is " + regexp);
+  }
+
+    private void updateCustomLevelDefinitionMap() {
+        if (customLevelDefinitions != null) {
+            StringTokenizer entryTokenizer = new StringTokenizer(customLevelDefinitions, ",");
+
+            customLevelDefinitionMap.clear();
+            while (entryTokenizer.hasMoreTokens()) {
+                StringTokenizer innerTokenizer = new StringTokenizer(entryTokenizer.nextToken(), "=");
+                customLevelDefinitionMap.put(innerTokenizer.nextToken(), Level.toLevel(innerTokenizer.nextToken()));
+            }
+        }
+    }
+
+    private boolean isInteger(String value) {
+        try {
+            Integer.parseInt(value);
+            return true;
+        } catch (NumberFormatException nfe) {
+            return false;
+        }
+    }
+
+    private String quoteTimeStampChars(String input) {
+        //put single quotes around text that isn't a supported dateformat char
+        StringBuffer result = new StringBuffer();
+        //ok to default to false because we also check for index zero below
+        boolean lastCharIsDateFormat = false;
+        for (int i = 0;i<input.length();i++) {
+            String thisVal = input.substring(i, i + 1);
+            boolean thisCharIsDateFormat = VALID_DATEFORMAT_CHARS.contains(thisVal);
+            //we have encountered a non-dateformat char
+            if (!thisCharIsDateFormat && (i == 0 || lastCharIsDateFormat)) {
+                result.append("'");
+            }
+            //we have encountered a dateformat char after previously encountering a non-dateformat char
+            if (thisCharIsDateFormat && i > 0 && !lastCharIsDateFormat) {
+                result.append("'");
+            }
+            lastCharIsDateFormat = thisCharIsDateFormat;
+            result.append(thisVal);
+        }
+        //append an end single-quote if we ended with non-dateformat char
+        if (!lastCharIsDateFormat) {
+            result.append("'");
+        }
+        return result.toString();
+    }
+
+    private String singleReplace(String inputString, String oldString, String newString)
+    {
+        int propLength = oldString.length();
+        int startPos = inputString.indexOf(oldString);
+        if (startPos == -1)
+        {
+            getLogger().info("string: " + oldString + " not found in input: " + inputString + " - returning input");
+            return inputString;
+        }
+        if (startPos == 0)
+        {
+            inputString = inputString.substring(propLength);
+            inputString = newString + inputString;
+        } else {
+            inputString = inputString.substring(0, startPos) + newString + inputString.substring(startPos + propLength);
+        }
+        return inputString;
+    }
+
+    private String getShortPropertyName(String longPropertyName)
+  {
+      String currentProp = longPropertyName.substring(longPropertyName.indexOf(PROP_START));
+      String prop = currentProp.substring(0, currentProp.indexOf(PROP_END) + 1);
+      String shortProp = prop.substring(PROP_START.length(), prop.length() - 1);
+      return shortProp;
+  }
+
+    /**
+   * Some perl5 characters may occur in the log file format.
+   * Escape these characters to prevent parsing errors.
+   *
+   * @param input
+   * @return string
+   */
+  private String replaceMetaChars(String input) {
+    //escape backslash first since that character is used to escape the remaining meta chars
+    input = input.replaceAll("\\\\", "\\\\\\");
+
+    //don't escape star - it's used as the wildcard
+    input = input.replaceAll(Pattern.quote("]"), "\\\\]");
+    input = input.replaceAll(Pattern.quote("["), "\\\\[");
+    input = input.replaceAll(Pattern.quote("^"), "\\\\^");
+    input = input.replaceAll(Pattern.quote("$"), "\\\\$");
+    input = input.replaceAll(Pattern.quote("."), "\\\\.");
+    input = input.replaceAll(Pattern.quote("|"), "\\\\|");
+    input = input.replaceAll(Pattern.quote("?"), "\\\\?");
+    input = input.replaceAll(Pattern.quote("+"), "\\\\+");
+    input = input.replaceAll(Pattern.quote("("), "\\\\(");
+    input = input.replaceAll(Pattern.quote(")"), "\\\\)");
+    input = input.replaceAll(Pattern.quote("-"), "\\\\-");
+    input = input.replaceAll(Pattern.quote("{"), "\\\\{");
+    input = input.replaceAll(Pattern.quote("}"), "\\\\}");
+    input = input.replaceAll(Pattern.quote("#"), "\\\\#");
+    return input;
+  }
+
+    /**
+   * Convert a keyword-to-values map to a LoggingEvent
+   *
+   * @param fieldMap
+   * @param exception
+   *
+   * @return logging event
+   */
+  private LoggingEvent convertToEvent(Map fieldMap, String[] exception) {
+    if (fieldMap == null) {
+      return null;
+    }
+
+    //a logger must exist at a minimum for the event to be processed
+    if (!fieldMap.containsKey(LOGGER)) {
+      fieldMap.put(LOGGER, "Unknown");
+    }
+    if (exception == null) {
+      exception = emptyException;
+    }
+
+    Logger logger = null;
+    long timeStamp = 0L;
+    String level = null;
+    String threadName = null;
+    Object message = null;
+    String ndc = null;
+    String className = null;
+    String methodName = null;
+    String eventFileName = null;
+    String lineNumber = null;
+    Hashtable properties = new Hashtable();
+
+    logger = Logger.getLogger((String) fieldMap.remove(LOGGER));
+
+    if ((dateFormat != null) && fieldMap.containsKey(TIMESTAMP)) {
+      try {
+        timeStamp = dateFormat.parse((String) fieldMap.remove(TIMESTAMP))
+            .getTime();
+      } catch (Exception e) {
+        e.printStackTrace();
+      }
+    }
+    //use current time if timestamp not parseable
+    if (timeStamp == 0L) {
+      timeStamp = System.currentTimeMillis();
+    }
+
+    message = fieldMap.remove(MESSAGE);
+    if (message == null) {
+      message = "";
+    }
+
+    level = (String) fieldMap.remove(LEVEL);
+    Level levelImpl;
+    if (level == null) {
+        levelImpl = Level.DEBUG;
+    } else {
+        //first try to resolve against custom level definition map, then fall back to regular levels
+        levelImpl = (Level) customLevelDefinitionMap.get(level);
+        if (levelImpl == null) {
+            levelImpl = Level.toLevel(level.trim());
+            if (!level.equals(levelImpl.toString())) {
+                //check custom level map
+                if (levelImpl == null) {
+                    levelImpl = Level.DEBUG;
+                    getLogger().debug("found unexpected level: " + level + ", logger: " + logger.getName() + ", msg: " + message);
+                    //make sure the text that couldn't match a level is added to the message
+                    message = level + " " + message;
+                }
+            }
+        }
+    }
+
+    threadName = (String) fieldMap.remove(THREAD);
+
+    ndc = (String) fieldMap.remove(NDC);
+
+    className = (String) fieldMap.remove(CLASS);
+
+    methodName = (String) fieldMap.remove(METHOD);
+
+    eventFileName = (String) fieldMap.remove(FILE);
+
+    lineNumber = (String) fieldMap.remove(LINE);
+
+    properties.put(Constants.HOSTNAME_KEY, host);
+    properties.put(Constants.APPLICATION_KEY, path);
+    properties.put(Constants.RECEIVER_NAME_KEY, getName());
+
+    //all remaining entries in fieldmap are properties
+    properties.putAll(fieldMap);
+
+    LocationInfo info = null;
+
+    if ((eventFileName != null) || (className != null) || (methodName != null)
+        || (lineNumber != null)) {
+      info = new LocationInfo(eventFileName, className, methodName, lineNumber);
+    } else {
+      info = LocationInfo.NA_LOCATION_INFO;
+    }
+
+    LoggingEvent event = new LoggingEvent(null,
+            logger, timeStamp, levelImpl, message,
+            threadName,
+            new ThrowableInformation(exception),
+            ndc,
+            info,
+            properties);
+
+    return event;
+  }
+
+//  public static void main(String[] args) {
+//    org.apache.log4j.Logger rootLogger = org.apache.log4j.Logger.getRootLogger();
+//    org.apache.log4j.ConsoleAppender appender = new org.apache.log4j.ConsoleAppender(new org.apache.log4j.SimpleLayout());
+//    appender.setName("console");
+//    rootLogger.addAppender(appender);
+//    LogFilePatternReceiver test = new LogFilePatternReceiver();
+//    org.apache.log4j.spi.LoggerRepository repo = new org.apache.log4j.LoggerRepositoryExImpl(org.apache.log4j.LogManager.getLoggerRepository());
+//    test.setLoggerRepository(repo);
+//    test.setLogFormat("PROP(RELATIVETIME) [THREAD] LEVEL LOGGER * - MESSAGE");
+//    test.setTailing(false);
+//    test.setAppendNonMatches(true);
+//    test.setTimestampFormat("yyyy-MM-d HH:mm:ss,SSS");
+//    test.setFileURL("file:///C:/log/test.log");
+//    test.initialize();
+//    test.activateOptions();
+//  }
+
+    /**
+   * Close the reader.
+   */
+  public void shutdown() {
+    getLogger().info(getPath() + " shutdown");
+    active = false;
+    try {
+      if (reader != null) {
+        reader.close();
+        reader = null;
+      }
+    } catch (IOException ioe) {
+      ioe.printStackTrace();
+    }
+  }
+
+    /**
+   * Read and process the log file.
+   */
+  public void activateOptions() {
+    getLogger().info("activateOptions");
+    active = true;
+	Runnable runnable = new Runnable() {
+	  public void run() {
+        initialize();
+            while (reader == null) {
+                getLogger().info("attempting to load file: " + getFileURL());
+                try {
+                    reader = new InputStreamReader(new URL(getFileURL()).openStream());
+                } catch (FileNotFoundException fnfe) {
+                    getLogger().info("file not available - will try again");
+                    synchronized (this) {
+                        try {
+                            wait(MISSING_FILE_RETRY_MILLIS);
+                        } catch (InterruptedException ie) {}
+                    }
+                } catch (IOException ioe) {
+                    getLogger().warn("unable to load file", ioe);
+                    return;
+                }
+            }
+            try {
+                BufferedReader bufferedReader = new BufferedReader(reader);
+                createPattern();
+                do {
+                    process(bufferedReader);
+                    try {
+                        synchronized (this) {
+                            wait(waitMillis);
+                        }
+                    } catch (InterruptedException ie) {}
+                    if (tailing) {
+                      getLogger().debug("tailing file");
+                    }
+                } while (tailing);
+
+            } catch (IOException ioe) {
+                //io exception - probably shut down
+                getLogger().info("stream closed");
+            }
+            getLogger().debug("processing " + path + " complete");
+            shutdown();
+          }
+        };
+        if(useCurrentThread) {
+            runnable.run();
+        }else {
+            new Thread(runnable, "LogFilePatternReceiver-"+getName()).start();
+        }
+    }
+}
diff --git a/src/main/java/org/apache/log4j/varia/LogFilePatternReceiverBeanInfo.java b/src/main/java/org/apache/log4j/varia/LogFilePatternReceiverBeanInfo.java
new file mode 100644
index 0000000..f99f289
--- /dev/null
+++ b/src/main/java/org/apache/log4j/varia/LogFilePatternReceiverBeanInfo.java
@@ -0,0 +1,53 @@
+/*
+ * 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.varia;
+
+import java.beans.PropertyDescriptor;
+import java.beans.SimpleBeanInfo;
+
+
+/**
+ * BeanInfo class for the meta-data of the LogFilePatternReceiver.
+ *
+ */
+public class LogFilePatternReceiverBeanInfo extends SimpleBeanInfo {
+  /* (non-Javadoc)
+   * @see java.beans.BeanInfo#getPropertyDescriptors()
+   */
+  public PropertyDescriptor[] getPropertyDescriptors() {
+    try {
+      return new PropertyDescriptor[] {
+        new PropertyDescriptor("fileURL", LogFilePatternReceiver.class),
+        new PropertyDescriptor(
+          "timestampFormat", LogFilePatternReceiver.class),
+        new PropertyDescriptor("logFormat", LogFilePatternReceiver.class),
+        new PropertyDescriptor("name", LogFilePatternReceiver.class),
+        new PropertyDescriptor("tailing", LogFilePatternReceiver.class),
+        new PropertyDescriptor(
+          "filterExpression", LogFilePatternReceiver.class),
+        new PropertyDescriptor("waitMillis", LogFilePatternReceiver.class),
+        new PropertyDescriptor("appendNonMatches", LogFilePatternReceiver.class),
+        new PropertyDescriptor("customLevelDefinitions", LogFilePatternReceiver.class),
+        new PropertyDescriptor("useCurrentThread", LogFilePatternReceiver.class),
+      };
+    } catch (Exception e) {
+    }
+
+    return null;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/xml/LogFileXMLReceiver.java b/src/main/java/org/apache/log4j/xml/LogFileXMLReceiver.java
new file mode 100644
index 0000000..945d4f0
--- /dev/null
+++ b/src/main/java/org/apache/log4j/xml/LogFileXMLReceiver.java
@@ -0,0 +1,310 @@
+/*

+ * 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.xml;

+

+import java.io.BufferedReader;

+import java.io.FileNotFoundException;

+import java.io.IOException;

+import java.io.InputStreamReader;

+import java.io.Reader;

+import java.net.MalformedURLException;

+import java.net.URL;

+import java.util.Collection;

+import java.util.Iterator;

+

+import org.apache.log4j.helpers.Constants;

+import org.apache.log4j.plugins.Receiver;

+import org.apache.log4j.rule.ExpressionRule;

+import org.apache.log4j.rule.Rule;

+import org.apache.log4j.spi.Decoder;

+import org.apache.log4j.spi.LoggingEvent;

+

+/**

+ * LogFileXMLReceiver will read an xml-formated log file and make the events in the log file

+ * available to the log4j framework.

+ * <p>

+ * This receiver supports log files created using log4j's XMLLayout, as well as java.util.logging

+ * XMLFormatter (via the org.apache.log4j.spi.Decoder interface).

+ * <p>

+ * By default, log4j's XMLLayout is supported (no need to specify a decoder in that case).

+ * <p>

+ * To configure this receiver to support java.util.logging's XMLFormatter, specify a 'decoder' param

+ * of org.apache.log4j.xml.UtilLoggingXMLDecoder.

+ * <p>

+ * Tailing -may- work, but not in all cases (try using a file:// URL). If a process has a log file

+ * open, the receiver may be able to read and tail the file. If the process closes the file and

+ * reopens the file, the receiver may not be able to continue tailing the file.

+ * <p>

+ * An expressionFilter may be specified. Only events passing the expression will be forwarded to the

+ * log4j framework.

+ * <p>

+ * Once the event has been "posted", it will be handled by the appenders currently configured in the

+ * LoggerRespository.

+ * 

+ * @author Scott Deboy <sdeboy@apache.org>

+ * @since 1.3

+ */

+

+public class LogFileXMLReceiver extends Receiver {

+    private String fileURL;

+    private Rule expressionRule;

+    private String filterExpression;

+    private String decoder = "org.apache.log4j.xml.XMLDecoder";

+    private boolean tailing = false;

+

+    private Decoder decoderInstance;

+    private Reader reader;

+    private static final String FILE_KEY = "file";

+    private String host;

+    private String path;

+    private boolean useCurrentThread;

+

+    /**

+     * Accessor

+     * 

+     * @return file URL

+     */

+    public String getFileURL() {

+        return fileURL;

+    }

+

+    /**

+     * Specify the URL of the XML-formatted file to process.

+     * 

+     * @param fileURL

+     */

+    public void setFileURL(String fileURL) {

+        this.fileURL = fileURL;

+    }

+

+    /**

+     * Accessor

+     * 

+     * @return

+     */

+    public String getDecoder() {

+        return decoder;

+    }

+

+    /**

+     * Specify the class name implementing org.apache.log4j.spi.Decoder that can process the file.

+     * 

+     * @param _decoder

+     */

+    public void setDecoder(String _decoder) {

+        decoder = _decoder;

+    }

+

+    /**

+     * Accessor

+     * 

+     * @return filter expression

+     */

+    public String getFilterExpression() {

+        return filterExpression;

+    }

+

+    /**

+     * Accessor

+     * 

+     * @return tailing flag

+     */

+    public boolean isTailing() {

+        return tailing;

+    }

+

+    /**

+     * Set the 'tailing' flag - may only work on file:// URLs and may stop tailing if the writing

+     * process closes the file and reopens.

+     * 

+     * @param tailing

+     */

+    public void setTailing(boolean tailing) {

+        this.tailing = tailing;

+    }

+

+    /**

+     * Set the filter expression that will cause only events which pass the filter to be forwarded

+     * to the log4j framework.

+     * 

+     * @param filterExpression

+     */

+    public void setFilterExpression(String filterExpression) {

+        this.filterExpression = filterExpression;

+    }

+

+    private boolean passesExpression(LoggingEvent event) {

+        if (event != null) {

+            if (expressionRule != null) {

+                return (expressionRule.evaluate(event, null));

+            }

+        }

+        return true;

+    }

+

+    public static void main(String[] args) {

+        /*

+         * LogFileXMLReceiver test = new LogFileXMLReceiver();

+         * test.setFileURL("file:///c:/samplelog.xml"); test.setFilterExpression("level >= TRACE");

+         * test.activateOptions();

+         */

+    }

+

+    /**

+     * Close the receiver, release any resources that are accessing the file.

+     */

+    public void shutdown() {

+        try {

+            if (reader != null) {

+                reader.close();

+                reader = null;

+            }

+        } catch (IOException ioe) {

+            ioe.printStackTrace();

+        }

+    }

+

+    /**

+     * Process the file

+     */

+    public void activateOptions() {

+        Runnable runnable = new Runnable() {

+            public void run() {

+                try {

+                    URL url = new URL(fileURL);

+                    host = url.getHost();

+                    if (host != null && host.equals("")) {

+                        host = FILE_KEY;

+                    }

+                    path = url.getPath();

+                } catch (MalformedURLException e1) {

+                    // TODO Auto-generated catch block

+                    e1.printStackTrace();

+                }

+

+                try {

+                    if (filterExpression != null) {

+                        expressionRule = ExpressionRule.getRule(filterExpression);

+                    }

+                } catch (Exception e) {

+                    getLogger().warn("Invalid filter expression: " + filterExpression, e);

+                }

+

+                Class c;

+                try {

+                    c = Class.forName(decoder);

+                    Object o = c.newInstance();

+                    if (o instanceof Decoder) {

+                        decoderInstance = (Decoder) o;

+                    }

+                } catch (ClassNotFoundException e) {

+                    // TODO Auto-generated catch block

+                    e.printStackTrace();

+                } catch (InstantiationException e) {

+                    // TODO Auto-generated catch block

+                    e.printStackTrace();

+                } catch (IllegalAccessException e) {

+                    // TODO Auto-generated catch block

+                    e.printStackTrace();

+                }

+

+                try {

+                    reader = new InputStreamReader(new URL(getFileURL()).openStream());

+                    process(reader);

+                } catch (FileNotFoundException fnfe) {

+                    getLogger().info("file not available");

+                } catch (IOException ioe) {

+                    getLogger().warn("unable to load file", ioe);

+                    return;

+                }

+            }

+        };

+        if (useCurrentThread) {

+            runnable.run();

+        } else {

+            Thread thread = new Thread(runnable, "LogFileXMLReceiver-" + getName());

+

+            thread.start();

+

+        }

+    }

+

+    private void process(Reader unbufferedReader) throws IOException {

+        BufferedReader bufferedReader = new BufferedReader(unbufferedReader);

+        char[] content = new char[10000];

+        getLogger().debug("processing starting: " + fileURL);

+        int length = 0;

+        do {

+            System.out.println("in do loop-about to process");

+            while ((length = bufferedReader.read(content)) > -1) {

+                processEvents(decoderInstance.decodeEvents(String.valueOf(content, 0, length)));

+            }

+            if (tailing) {

+                try {

+                    Thread.sleep(5000);

+                } catch (InterruptedException e) {

+                    // TODO Auto-generated catch block

+                    e.printStackTrace();

+                }

+            }

+        } while (tailing);

+        getLogger().debug("processing complete: " + fileURL);

+

+        shutdown();

+    }

+

+    private void processEvents(Collection c) {

+        if (c == null) {

+            return;

+        }

+

+        for (Iterator iter = c.iterator(); iter.hasNext();) {

+            LoggingEvent evt = (LoggingEvent) iter.next();

+            if (passesExpression(evt)) {

+                if (evt.getProperty(Constants.HOSTNAME_KEY) != null) {

+                    evt.setProperty(Constants.HOSTNAME_KEY, host);

+                }

+                if (evt.getProperty(Constants.APPLICATION_KEY) != null) {

+                    evt.setProperty(Constants.APPLICATION_KEY, path);

+                }

+                doPost(evt);

+            }

+        }

+    }

+

+    /**

+     * When true, this property uses the current Thread to perform the import, otherwise when false

+     * (the default), a new Thread is created and started to manage the import.

+     * 

+     * @return

+     */

+    public final boolean isUseCurrentThread() {

+        return useCurrentThread;

+    }

+

+    /**

+     * Sets whether the current Thread or a new Thread is created to perform the import, the default

+     * being false (new Thread created).

+     * 

+     * @param useCurrentThread

+     */

+    public final void setUseCurrentThread(boolean useCurrentThread) {

+        this.useCurrentThread = useCurrentThread;

+    }

+

+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/log4j/xml/UtilLoggingEntityResolver.java b/src/main/java/org/apache/log4j/xml/UtilLoggingEntityResolver.java
new file mode 100644
index 0000000..71a349c
--- /dev/null
+++ b/src/main/java/org/apache/log4j/xml/UtilLoggingEntityResolver.java
@@ -0,0 +1,50 @@
+/*
+ * 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.xml;
+
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+
+import java.io.ByteArrayInputStream;
+
+
+/**
+ * An {@link EntityResolver} specifically designed to return
+ * an empty InputSource for logger.dtd.
+ *
+ */
+public final class UtilLoggingEntityResolver implements EntityResolver {
+
+    /**
+     * Create new instance.
+     */
+    public UtilLoggingEntityResolver() {
+        super();
+    }
+
+
+    /** {@inheritDoc} */
+  public InputSource resolveEntity(final String publicId,
+                                   final String systemId) {
+    if (systemId.endsWith("logger.dtd")) {
+      return new InputSource(new ByteArrayInputStream(new byte[0]));
+    } else {
+      return null;
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/xml/UtilLoggingXMLDecoder.java b/src/main/java/org/apache/log4j/xml/UtilLoggingXMLDecoder.java
new file mode 100644
index 0000000..f9d8125
--- /dev/null
+++ b/src/main/java/org/apache/log4j/xml/UtilLoggingXMLDecoder.java
@@ -0,0 +1,468 @@
+/*
+ * 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.xml;
+
+import java.awt.Component;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.StringReader;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Vector;
+import java.util.zip.ZipInputStream;
+
+import javax.swing.ProgressMonitorInputStream;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.helpers.UtilLoggingLevel;
+import org.apache.log4j.spi.Decoder;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
+import org.apache.log4j.spi.LocationInfo;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+
+/**
+ * Decodes JDK 1.4's java.util.logging package events
+ * delivered via XML (using the logger.dtd).
+ *
+ * @author Scott Deboy (sdeboy@apache.org)
+ * @author Paul Smith (psmith@apache.org)
+ *
+ */
+public class UtilLoggingXMLDecoder implements Decoder {
+  //NOTE: xml section is only handed on first delivery of events
+  //on this first delivery of events, there is no end tag for the log element
+  /**
+   * Document prolog.
+   */
+  private static final String BEGIN_PART =
+    "<log>";
+    /**
+     * Document close.
+     */
+  private static final String END_PART = "</log>";
+    /**
+     * Document builder.
+     */
+  private DocumentBuilder docBuilder;
+    /**
+     * Additional properties.
+     */
+  private Map additionalProperties = new HashMap();
+    /**
+     * Partial event.
+     */
+  private String partialEvent;
+    /**
+     * Record end.
+     */
+  private static final String RECORD_END = "</record>";
+    /**
+     * Owner.
+     */
+  private Component owner = null;
+
+  private static final String ENCODING = "UTF-8";
+
+    /**
+     * Create new instance.
+     * @param o owner
+     */
+  public UtilLoggingXMLDecoder(final Component o) {
+      this();
+      this.owner = o;
+  }
+
+    /**
+     * Create new instance.
+     */
+  public UtilLoggingXMLDecoder() {
+    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+    dbf.setValidating(false);
+
+    try {
+      docBuilder = dbf.newDocumentBuilder();
+      docBuilder.setErrorHandler(new SAXErrorHandler());
+      docBuilder.setEntityResolver(new UtilLoggingEntityResolver());
+    } catch (ParserConfigurationException pce) {
+      System.err.println("Unable to get document builder");
+    }
+  }
+
+  /**
+   * Sets an additionalProperty map, where each Key/Value pair is
+   * automatically added to each LoggingEvent as it is decoded.
+   *
+   * This is useful, say, to include the source file name of the Logging events
+   * @param properties additional properties
+   */
+  public void setAdditionalProperties(final Map properties) {
+    this.additionalProperties = properties;
+  }
+
+  /**
+   * Converts the LoggingEvent data in XML string format into an actual
+   * XML Document class instance.
+   * @param data XML fragment
+   * @return  dom document
+   */
+  private Document parse(final String data) {
+    if (docBuilder == null || data == null) {
+      return null;
+    }
+
+    Document document = null;
+
+    try {
+      // we change the system ID to a valid URI so that Crimson won't
+      // complain. Indeed, "log4j.dtd" alone is not a valid URI which
+      // causes Crimson to barf. The Log4jEntityResolver only cares
+      // about the "log4j.dtd" ending.
+
+      /**
+       * resetting the length of the StringBuffer is dangerous, particularly
+       * on some JDK 1.4 impls, there's a known Bug that causes a memory leak
+       */
+      StringBuffer buf = new StringBuffer(1024);
+
+      if (!data.startsWith("<?xml")) {
+        buf.append(BEGIN_PART);
+      }
+
+      buf.append(data);
+
+      if (!data.endsWith(END_PART)) {
+        buf.append(END_PART);
+      }
+
+      InputSource inputSource =
+        new InputSource(new StringReader(buf.toString()));
+      document = docBuilder.parse(inputSource);
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+
+    return document;
+  }
+
+  /**
+   * Decodes a File into a Vector of LoggingEvents.
+   * @param url the url of a file containing events to decode
+   * @return Vector of LoggingEvents
+   * @throws IOException if IO error during processing.
+   */
+  public Vector decode(final URL url) throws IOException {
+    LineNumberReader reader;
+    boolean isZipFile = url.getPath().toLowerCase().endsWith(".zip");
+    InputStream inputStream;
+    if (isZipFile) {
+        inputStream = new ZipInputStream(url.openStream());
+        //move stream to next entry so we can read it
+        ((ZipInputStream)inputStream).getNextEntry();
+    } else {
+        inputStream = url.openStream();
+    }
+    if (owner != null) {
+        reader = new LineNumberReader(
+                new InputStreamReader(
+                        new ProgressMonitorInputStream(owner,
+                                "Loading " + url , inputStream), ENCODING));
+    } else {
+        reader = new LineNumberReader(new InputStreamReader(inputStream, ENCODING));
+    }
+    Vector v = new Vector();
+
+      String line;
+      Vector events;
+      try {
+          while ((line = reader.readLine()) != null) {
+              StringBuffer buffer = new StringBuffer(line);
+              for (int i = 0; i < 1000; i++) {
+                  buffer.append(reader.readLine()).append("\n");
+              }
+              events = decodeEvents(buffer.toString());
+              if (events != null) {
+                  v.addAll(events);
+              }
+          }
+    } finally {
+      partialEvent = null;
+      try {
+        if (reader != null) {
+          reader.close();
+        }
+      } catch (Exception e) {
+        e.printStackTrace();
+      }
+    }
+    return v;
+  }
+
+  /**
+   * Decodes a String representing a number of events into a
+   * Vector of LoggingEvents.
+   * @param document to decode events from
+   * @return Vector of LoggingEvents
+   */
+  public Vector decodeEvents(final String document) {
+
+      if (document != null) {
+
+          if (document.trim().equals("")) {
+              return null;
+          }
+
+          String newDoc;
+          String newPartialEvent = null;
+          //separate the string into the last portion ending with </record>
+          // (which will be processed) and the partial event which
+          // will be combined and processed in the next section
+
+          //if the document does not contain a record end,
+          // append it to the partial event string
+          if (document.lastIndexOf(RECORD_END) == -1) {
+              partialEvent = partialEvent + document;
+              return null;
+          }
+
+          if (document.lastIndexOf(RECORD_END) + RECORD_END.length()
+                  < document.length()) {
+              newDoc = document.substring(0,
+                      document.lastIndexOf(RECORD_END) + RECORD_END.length());
+              newPartialEvent = document.substring(
+                      document.lastIndexOf(RECORD_END) + RECORD_END.length());
+          } else {
+              newDoc = document;
+          }
+          if (partialEvent != null) {
+              newDoc = partialEvent + newDoc;
+          }
+          partialEvent = newPartialEvent;
+
+          Document doc = parse(newDoc);
+          if (doc == null) {
+              return null;
+          }
+          return decodeEvents(doc);
+      }
+      return null;
+  }
+
+    /**
+      * Converts the string data into an XML Document, and then soaks out the
+      * relevant bits to form a new LoggingEvent instance which can be used
+      * by any Log4j element locally.
+      * @param data XML fragment
+      * @return a single LoggingEvent or null
+      */
+  public LoggingEvent decode(final String data) {
+    Document document = parse(data);
+
+    if (document == null) {
+      return null;
+    }
+
+    Vector events = decodeEvents(document);
+
+    if (events.size() > 0) {
+      return (LoggingEvent) events.firstElement();
+    }
+
+    return null;
+  }
+
+  /**
+   * Given a Document, converts the XML into a Vector of LoggingEvents.
+   * @param document XML document
+   * @return Vector of LoggingEvents
+   */
+  private Vector decodeEvents(final Document document) {
+    Vector events = new Vector();
+
+    NodeList eventList = document.getElementsByTagName("record");
+
+    for (int eventIndex = 0; eventIndex < eventList.getLength();
+        eventIndex++) {
+      Node eventNode = eventList.item(eventIndex);
+
+      Logger logger = null;
+    long timeStamp = 0L;
+    Level level = null;
+    String threadName = null;
+    Object message = null;
+    String ndc = null;
+    String[] exception = null;
+    String className = null;
+    String methodName = null;
+    String fileName = null;
+    String lineNumber = null;
+    Hashtable properties = new Hashtable();
+
+      //format of date: 2003-05-04T11:04:52
+      //ignore date or set as a property? using millis in constructor instead
+      NodeList list = eventNode.getChildNodes();
+      int listLength = list.getLength();
+
+      if (listLength == 0) {
+        continue;
+      }
+
+      for (int y = 0; y < listLength; y++) {
+        String tagName = list.item(y).getNodeName();
+
+        if (tagName.equalsIgnoreCase("logger")) {
+          logger = Logger.getLogger(getCData(list.item(y)));
+        }
+
+        if (tagName.equalsIgnoreCase("millis")) {
+          timeStamp = Long.parseLong(getCData(list.item(y)));
+        }
+
+        if (tagName.equalsIgnoreCase("level")) {
+          level = UtilLoggingLevel.toLevel(getCData(list.item(y)));
+        }
+
+        if (tagName.equalsIgnoreCase("thread")) {
+          threadName = getCData(list.item(y));
+        }
+
+        if (tagName.equalsIgnoreCase("sequence")) {
+          properties.put("log4jid", getCData(list.item(y)));
+        }
+
+        if (tagName.equalsIgnoreCase("message")) {
+          message = getCData(list.item(y));
+        }
+
+        if (tagName.equalsIgnoreCase("class")) {
+          className = getCData(list.item(y));
+        }
+
+        if (tagName.equalsIgnoreCase("method")) {
+          methodName = getCData(list.item(y));
+        }
+
+        if (tagName.equalsIgnoreCase("exception")) {
+          ArrayList exceptionList = new ArrayList();
+          NodeList exList = list.item(y).getChildNodes();
+          int exlistLength = exList.getLength();
+
+          for (int i2 = 0; i2 < exlistLength; i2++) {
+            Node exNode = exList.item(i2);
+            String exName = exList.item(i2).getNodeName();
+
+            if (exName.equalsIgnoreCase("message")) {
+              exceptionList.add(getCData(exList.item(i2)));
+            }
+
+            if (exName.equalsIgnoreCase("frame")) {
+              NodeList exList2 = exNode.getChildNodes();
+              int exlist2Length = exList2.getLength();
+
+              for (int i3 = 0; i3 < exlist2Length; i3++) {
+                exceptionList.add(getCData(exList2.item(i3)) + "\n");
+              }
+            }
+          }
+          if (exceptionList.size() > 0) {
+              exception =
+                (String[]) exceptionList.toArray(new String[exceptionList.size()]);
+          }
+        }
+      }
+
+        /**
+         * We add all the additional properties to the properties
+         * hashtable. Override properties that already exist
+         */
+        if (additionalProperties.size() > 0) {
+            if (properties == null) {
+                properties = new Hashtable(additionalProperties);
+            }
+            Iterator i = additionalProperties.entrySet().iterator();
+            while (i.hasNext()) {
+                Map.Entry e = (Map.Entry) i.next();
+                properties.put(e.getKey(), e.getValue());
+            }
+        }
+
+      LocationInfo info;
+      if ((fileName != null)
+              || (className != null)
+              || (methodName != null)
+              || (lineNumber != null)) {
+          info = new LocationInfo(fileName, className, methodName, lineNumber);
+      } else {
+        info = LocationInfo.NA_LOCATION_INFO;
+      }
+
+        ThrowableInformation throwableInfo = null;
+        if (exception != null) {
+            throwableInfo = new ThrowableInformation(exception);
+        }
+
+        LoggingEvent loggingEvent = new LoggingEvent(null,
+                logger, timeStamp, level, message,
+                threadName,
+                throwableInfo,
+                ndc,
+                info,
+                properties);
+
+      events.add(loggingEvent);
+
+    }
+    return events;
+  }
+
+    /**
+     * Get contents of CDATASection.
+     * @param n CDATASection
+     * @return text content of all text or CDATA children of node.
+     */
+  private String getCData(final Node n) {
+    StringBuffer buf = new StringBuffer();
+    NodeList nl = n.getChildNodes();
+
+    for (int x = 0; x < nl.getLength(); x++) {
+      Node innerNode = nl.item(x);
+
+      if (
+        (innerNode.getNodeType() == Node.TEXT_NODE)
+          || (innerNode.getNodeType() == Node.CDATA_SECTION_NODE)) {
+        buf.append(innerNode.getNodeValue());
+      }
+    }
+
+    return buf.toString();
+  }
+}
diff --git a/src/main/java/org/apache/log4j/xml/XMLDecoder.java b/src/main/java/org/apache/log4j/xml/XMLDecoder.java
new file mode 100644
index 0000000..8ced851
--- /dev/null
+++ b/src/main/java/org/apache/log4j/xml/XMLDecoder.java
@@ -0,0 +1,495 @@
+/*
+ * 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.xml;
+
+import java.awt.Component;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.StringReader;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Vector;
+import java.util.zip.ZipInputStream;
+
+import javax.swing.ProgressMonitorInputStream;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.Decoder;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+
+/**
+ * Decodes Logging Events in XML formated into elements that are used by
+ * Chainsaw.
+ *
+ * This decoder can process a collection of log4j:event nodes ONLY
+ * (no XML declaration nor eventSet node)
+ *
+ * NOTE:  Only a single LoggingEvent is returned from the decode method
+ * even though the DTD supports multiple events nested in an eventSet.
+ *
+ * NOTE: This class has been created on the assumption that all XML log files
+ * are encoded in UTF-8. There is no current support for any other
+ * encoding format at this time.
+ * 
+ * @author Scott Deboy (sdeboy@apache.org)
+ * @author Paul Smith (psmith@apache.org)
+ *
+ */
+public class XMLDecoder implements Decoder {
+    
+  private static final String ENCODING = "UTF-8";
+    
+    /**
+     * Document prolog.
+     */
+  private static final String BEGINPART =
+    "<?xml version=\"1.0\" encoding=\"" + ENCODING + "\" ?>"
+    + "<!DOCTYPE log4j:eventSet SYSTEM \"http://localhost/log4j.dtd\">"
+    + "<log4j:eventSet version=\"1.2\" "
+    + "xmlns:log4j=\"http://jakarta.apache.org/log4j/\">";
+    /**
+     * Document close.
+     */
+  private static final String ENDPART = "</log4j:eventSet>";
+    /**
+     * Record end.
+     */
+  private static final String RECORD_END = "</log4j:event>";
+
+    /**
+     * Document builder.
+     */
+  private DocumentBuilder docBuilder;
+    /**
+     * Additional properties.
+     */
+  private Map additionalProperties = new HashMap();
+    /**
+     * Partial event.
+     */
+  private String partialEvent;
+    /**
+     * Owner.
+     */
+  private Component owner = null;
+
+    /**
+     * Create new instance.
+     * @param o owner
+     */
+  public XMLDecoder(final Component o) {
+      this();
+      this.owner = o;
+  }
+
+    /**
+     * Create new instance.
+     */
+   public XMLDecoder() {
+    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+    dbf.setValidating(false);
+
+    try {
+      docBuilder = dbf.newDocumentBuilder();
+      docBuilder.setErrorHandler(new SAXErrorHandler());
+      docBuilder.setEntityResolver(new Log4jEntityResolver());
+    } catch (ParserConfigurationException pce) {
+      System.err.println("Unable to get document builder");
+    }
+  }
+
+  /**
+   * Sets an additionalProperty map, where each Key/Value pair is
+   * automatically added to each LoggingEvent as it is decoded.
+   *
+   * This is useful, say, to include the source file name of the Logging events
+   * @param properties additional properties
+   */
+  public void setAdditionalProperties(final Map properties) {
+    this.additionalProperties = properties;
+  }
+
+  /**
+   * Converts the LoggingEvent data in XML string format into an actual
+   * XML Document class instance.
+   * @param data XML fragment
+   * @return dom document
+   */
+  private Document parse(final String data) {
+    if (docBuilder == null || data == null) {
+      return null;
+    }
+    Document document = null;
+
+    try {
+      // we change the system ID to a valid URI so that Crimson won't
+      // complain. Indeed, "log4j.dtd" alone is not a valid URI which
+      // causes Crimson to barf. The Log4jEntityResolver only cares
+      // about the "log4j.dtd" ending.
+
+      /**
+       * resetting the length of the StringBuffer is dangerous, particularly
+       * on some JDK 1.4 impls, there's a known Bug that causes a memory leak
+       */
+      StringBuffer buf = new StringBuffer(1024);
+
+      buf.append(BEGINPART);
+      buf.append(data);
+      buf.append(ENDPART);
+
+      InputSource inputSource =
+        new InputSource(new StringReader(buf.toString()));
+      document = docBuilder.parse(inputSource);
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+
+    return document;
+  }
+
+  /**
+   * Decodes a File into a Vector of LoggingEvents.
+   * @param url the url of a file containing events to decode
+   * @return Vector of LoggingEvents
+   * @throws IOException if IO error during processing.
+   */
+  public Vector decode(final URL url) throws IOException {
+    LineNumberReader reader;
+    boolean isZipFile = url.getPath().toLowerCase().endsWith(".zip");
+    InputStream inputStream;
+    if (isZipFile) {
+        inputStream = new ZipInputStream(url.openStream());
+        //move stream to next entry so we can read it
+        ((ZipInputStream)inputStream).getNextEntry();
+    } else {
+        inputStream = url.openStream();
+    }
+    if (owner != null) {
+        reader = new LineNumberReader(
+                new InputStreamReader(
+                        new ProgressMonitorInputStream(owner,
+                                "Loading " + url , inputStream), ENCODING));
+    } else {
+        reader = new LineNumberReader(new InputStreamReader(inputStream, ENCODING));
+    }
+
+    Vector v = new Vector();
+
+    String line;
+    Vector events;
+    try {
+        while ((line = reader.readLine()) != null) {
+            StringBuffer buffer = new StringBuffer(line);
+            for (int i = 0; i < 1000; i++) {
+                buffer.append(reader.readLine()).append("\n");
+            }
+            events = decodeEvents(buffer.toString());
+            if (events != null) {
+                v.addAll(events);
+            }
+        }
+    } finally {
+      partialEvent = null;
+      try {
+        if (reader != null) {
+          reader.close();
+        }
+      } catch (Exception e) {
+        e.printStackTrace();
+      }
+    }
+    return v;
+  }
+
+    /**
+     * Decodes a String representing a number of events into a
+     * Vector of LoggingEvents.
+     * @param document to decode events from
+     * @return Vector of LoggingEvents
+     */
+  public Vector decodeEvents(final String document) {
+    if (document != null) {
+      if (document.trim().equals("")) {
+        return null;
+      }
+        String newDoc = null;
+        String newPartialEvent = null;
+        //separate the string into the last portion ending with
+        // </log4j:event> (which will be processed) and the
+        // partial event which will be combined and
+        // processed in the next section
+
+        //if the document does not contain a record end,
+        // append it to the partial event string
+        if (document.lastIndexOf(RECORD_END) == -1) {
+            partialEvent = partialEvent + document;
+            return null;
+        }
+
+        if (document.lastIndexOf(RECORD_END)
+                + RECORD_END.length() < document.length()) {
+            newDoc = document.substring(0,
+                    document.lastIndexOf(RECORD_END) + RECORD_END.length());
+            newPartialEvent = document.substring(
+                    document.lastIndexOf(RECORD_END) + RECORD_END.length());
+        } else {
+            newDoc = document;
+        }
+        if (partialEvent != null) {
+            newDoc = partialEvent + newDoc;
+        }
+        partialEvent = newPartialEvent;
+        Document doc = parse(newDoc);
+        if (doc == null) {
+            return null;
+        }
+        return decodeEvents(doc);
+    }
+    return null;
+  }
+
+  /**
+   * Converts the string data into an XML Document, and then soaks out the
+   * relevant bits to form a new LoggingEvent instance which can be used
+   * by any Log4j element locally.
+   * @param data XML fragment
+   * @return a single LoggingEvent or null
+   */
+  public LoggingEvent decode(final String data) {
+    Document document = parse(data);
+
+    if (document == null) {
+      return null;
+    }
+
+    Vector events = decodeEvents(document);
+
+    if (events.size() > 0) {
+      return (LoggingEvent) events.firstElement();
+    }
+
+    return null;
+  }
+
+  /**
+   * Given a Document, converts the XML into a Vector of LoggingEvents.
+   * @param document XML document
+   * @return Vector of LoggingEvents
+   */
+  private Vector decodeEvents(final Document document) {
+    Vector events = new Vector();
+
+    Logger logger;
+    long timeStamp;
+    Level level;
+    String threadName;
+    Object message = null;
+    String ndc = null;
+    String[] exception = null;
+    String className = null;
+    String methodName = null;
+    String fileName = null;
+    String lineNumber = null;
+    Hashtable properties = null;
+
+    NodeList nl = document.getElementsByTagName("log4j:eventSet");
+    Node eventSet = nl.item(0);
+
+    NodeList eventList = eventSet.getChildNodes();
+
+    for (int eventIndex = 0; eventIndex < eventList.getLength();
+        eventIndex++) {
+        Node eventNode = eventList.item(eventIndex);
+      //ignore carriage returns in xml
+        if (eventNode.getNodeType() != Node.ELEMENT_NODE) {
+            continue;
+        }
+      logger = Logger.getLogger(eventNode.getAttributes().getNamedItem("logger").getNodeValue());
+      timeStamp = Long.parseLong(eventNode.getAttributes().getNamedItem("timestamp").getNodeValue());
+      level = Level.toLevel(eventNode.getAttributes().getNamedItem("level").getNodeValue());
+      threadName = eventNode.getAttributes().getNamedItem("thread").getNodeValue();
+
+      NodeList list = eventNode.getChildNodes();
+      int listLength = list.getLength();
+
+      if (listLength == 0) {
+        continue;
+      }
+
+      for (int y = 0; y < listLength; y++) {
+        String tagName = list.item(y).getNodeName();
+
+        if (tagName.equalsIgnoreCase("log4j:message")) {
+          message = getCData(list.item(y));
+        }
+
+        if (tagName.equalsIgnoreCase("log4j:NDC")) {
+          ndc = getCData(list.item(y));
+        }
+        //still support receiving of MDC and convert to properties
+        if (tagName.equalsIgnoreCase("log4j:MDC")) {
+          properties = new Hashtable();
+          NodeList propertyList = list.item(y).getChildNodes();
+          int propertyLength = propertyList.getLength();
+
+          for (int i = 0; i < propertyLength; i++) {
+            String propertyTag = propertyList.item(i).getNodeName();
+
+            if (propertyTag.equalsIgnoreCase("log4j:data")) {
+              Node property = propertyList.item(i);
+              String name =
+                property.getAttributes().getNamedItem("name").getNodeValue();
+              String value =
+                property.getAttributes().getNamedItem("value").getNodeValue();
+              properties.put(name, value);
+            }
+          }
+        }
+
+        if (tagName.equalsIgnoreCase("log4j:throwable")) {
+            String exceptionString = getCData(list.item(y));
+            if (exceptionString != null && !exceptionString.trim().equals("")) {
+                exception = new String[] {exceptionString.trim()
+            };
+          }
+        }
+
+        if (tagName.equalsIgnoreCase("log4j:locationinfo")) {
+          className =
+            list.item(y).getAttributes().getNamedItem("class").getNodeValue();
+          methodName =
+            list.item(y).getAttributes().getNamedItem("method").getNodeValue();
+          fileName =
+            list.item(y).getAttributes().getNamedItem("file").getNodeValue();
+          lineNumber =
+            list.item(y).getAttributes().getNamedItem("line").getNodeValue();
+        }
+
+        if (tagName.equalsIgnoreCase("log4j:properties")) {
+          if (properties == null) {
+              properties = new Hashtable();
+          }
+          NodeList propertyList = list.item(y).getChildNodes();
+          int propertyLength = propertyList.getLength();
+
+          for (int i = 0; i < propertyLength; i++) {
+            String propertyTag = propertyList.item(i).getNodeName();
+
+            if (propertyTag.equalsIgnoreCase("log4j:data")) {
+              Node property = propertyList.item(i);
+              String name =
+                property.getAttributes().getNamedItem("name").getNodeValue();
+              String value =
+                property.getAttributes().getNamedItem("value").getNodeValue();
+              properties.put(name, value);
+            }
+          }
+        }
+
+          /**
+           * We add all the additional properties to the properties
+           * hashtable. Override properties that already exist
+           */
+          if (additionalProperties.size() > 0) {
+              if (properties == null) {
+                  properties = new Hashtable(additionalProperties);
+              }
+              Iterator i = additionalProperties.entrySet().iterator();
+              while (i.hasNext()) {
+                  Map.Entry e = (Map.Entry) i.next();
+                  properties.put(e.getKey(), e.getValue());
+              }
+          }
+      }
+
+      LocationInfo info;
+      if ((fileName != null)
+              || (className != null)
+              || (methodName != null)
+              || (lineNumber != null)) {
+          info = new LocationInfo(fileName, className, methodName, lineNumber);
+      } else {
+        info = LocationInfo.NA_LOCATION_INFO;
+      }
+      ThrowableInformation throwableInfo = null;
+      if (exception != null) {
+          throwableInfo = new ThrowableInformation(exception);
+      }
+
+        LoggingEvent loggingEvent = new LoggingEvent(null,
+                logger, timeStamp, level, message,
+                threadName,
+                throwableInfo,
+                ndc,
+                info,
+                properties);
+
+
+      events.add(loggingEvent);
+
+      message = null;
+      ndc = null;
+      exception = null;
+      className = null;
+      methodName = null;
+      fileName = null;
+      lineNumber = null;
+      properties = null;
+    }
+
+    return events;
+  }
+
+    /**
+     * Get contents of CDATASection.
+     * @param n CDATASection
+     * @return text content of all text or CDATA children of node.
+     */
+  private String getCData(final Node n) {
+    StringBuffer buf = new StringBuffer();
+    NodeList nl = n.getChildNodes();
+
+    for (int x = 0; x < nl.getLength(); x++) {
+      Node innerNode = nl.item(x);
+
+      if (
+        (innerNode.getNodeType() == Node.TEXT_NODE)
+          || (innerNode.getNodeType() == Node.CDATA_SECTION_NODE)) {
+        buf.append(innerNode.getNodeValue());
+      }
+    }
+
+    return buf.toString();
+  }
+}
diff --git a/src/main/resources/org/apache/log4j/chainsaw/help/release-notes.html b/src/main/resources/org/apache/log4j/chainsaw/help/release-notes.html
index 98e26df..b25f6cd 100644
--- a/src/main/resources/org/apache/log4j/chainsaw/help/release-notes.html
+++ b/src/main/resources/org/apache/log4j/chainsaw/help/release-notes.html
@@ -12,6 +12,7 @@
 <h1>2.1</h1>
 <h2>2 Oct 2011</h2>
 <ul>
+<li>Moved component and receivers companion sources to the Chainsaw source tree - those companions won't be released.</li>
 <li>Added persistent "always display" expression support (button below the logger tree).  The 'always display' expression overrides hidden loggers and the hidden expression but not the refine focus filtering mechanism.  Often used with expressions like 'exception exists || level > warn' to ensure errors and exceptions are not filtered out due to the hidden expression or hidden logger mechanism. </li>
 </ul>
 <h2>11 Nov 2010</h2>
diff --git a/src/test/java/org/apache/log4j/VectorAppender.java b/src/test/java/org/apache/log4j/VectorAppender.java
new file mode 100644
index 0000000..d7833a8
--- /dev/null
+++ b/src/test/java/org/apache/log4j/VectorAppender.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+import java.util.Vector;
+import org.apache.log4j.spi.LoggingEvent;
+
+/**
+   An appender that appends logging events to a vector.
+   @author Ceki  G&uuml;lc&uuml;
+*/
+public class VectorAppender extends AppenderSkeleton {
+  public Vector vector;
+
+  long delay = 0;
+  
+  public VectorAppender() {
+    super(true);
+    vector = new Vector();
+  }
+
+
+  /**
+     This method is called by the {@link AppenderSkeleton#doAppend}
+     method.
+
+  */
+  public void append(LoggingEvent event) {
+    if(delay > 0) {
+      try {
+        Thread.sleep(delay);
+      } catch (Exception e) {
+      }
+    }
+
+    vector.addElement(event);
+  }
+
+  /**
+   * Returns a vector of {@link LoggingEvent}.
+   */
+  public Vector getVector() {
+    return vector;
+  }
+
+  public synchronized void close() {
+    if (this.closed) {
+      return;
+    }
+
+    this.closed = true;
+  }
+
+  public boolean isClosed() {
+    return closed;
+  }
+
+  public boolean requiresLayout() {
+    return false;
+  }
+  
+  /**
+   * Returns a delay to log.
+   */
+  public long getDelay() {
+    return delay;
+  }
+
+  /**
+   * Sets a delay to log.
+   */  
+  public void setDelay(long delay) {
+    this.delay = delay;
+  }
+
+}
diff --git a/src/test/java/org/apache/log4j/db/FullCycleDBTest.java b/src/test/java/org/apache/log4j/db/FullCycleDBTest.java
new file mode 100644
index 0000000..6352974
--- /dev/null
+++ b/src/test/java/org/apache/log4j/db/FullCycleDBTest.java
@@ -0,0 +1,327 @@
+/*
+ * 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.db;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+import org.apache.log4j.Hierarchy;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.MDC;
+import org.apache.log4j.VectorAppender;
+import org.apache.log4j.LoggerRepositoryExImpl;
+import org.apache.log4j.helpers.Constants;
+import org.apache.log4j.xml.DOMConfigurator;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.RootLogger;
+import org.apache.log4j.spi.LoggerRepository;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Vector;
+import java.util.HashMap;
+import java.io.InputStream;
+import java.io.IOException;
+
+
+/**
+ * This test case writes a few events into a databases and reads them
+ * back comparing the event written and read back.
+ * 
+ * <p>It relies heavily on the proper configuration of its environment
+ * in joran config files as well system properties.
+ * </p>
+ * 
+ * <p>See also the Ant build file in the tests/ directory.</p> 
+ * 
+ * @author Ceki G&uuml;lc&uuml
+ */
+public class FullCycleDBTest
+       extends TestCase {
+  
+  Vector witnessEvents;
+  Hierarchy lrWrite;
+  LoggerRepository lrRead;
+  String appendConfigFile = null;
+  String readConfigFile = null;
+  
+  
+  /*
+   * @see TestCase#setUp()
+   */
+  protected void setUp()
+         throws Exception {
+    super.setUp();
+    appendConfigFile = "append-with-drivermanager1.xml";
+    readConfigFile = "read-with-drivermanager1.xml";
+
+    witnessEvents = new Vector();
+    lrWrite = new Hierarchy(new RootLogger(Level.DEBUG));
+    lrRead = new LoggerRepositoryExImpl(new Hierarchy(new RootLogger(Level.DEBUG)));
+
+
+    //
+    //   attempt to define tables in in-memory database
+    //      will throw exception if already defined.
+    //
+        Class.forName("org.hsqldb.jdbcDriver");
+        Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:testdb");
+        try {
+            Statement s = connection.createStatement();
+            s.executeUpdate("CREATE TABLE logging_event " +
+              "( sequence_number   BIGINT NOT NULL, " +
+               " timestamp         BIGINT NOT NULL, " +
+               " rendered_message  LONGVARCHAR NOT NULL, " +
+               " logger_name       VARCHAR NOT NULL, " +
+               " level_string      VARCHAR NOT NULL, " +
+               " ndc               LONGVARCHAR, " +
+               " thread_name       VARCHAR, " +
+               " reference_flag    SMALLINT, " +
+               " caller_filename   VARCHAR, " +
+               " caller_class      VARCHAR, " +
+               " caller_method     VARCHAR, " +
+               " caller_line       CHAR(4), " +
+               " event_id          INT NOT NULL IDENTITY)");
+            s.executeUpdate("CREATE TABLE logging_event_property " +
+              "( event_id	      INT NOT NULL, " +
+               " mapped_key        VARCHAR(254) NOT NULL, " +
+               " mapped_value      LONGVARCHAR, " +
+               " PRIMARY KEY(event_id, mapped_key), " +
+               " FOREIGN KEY (event_id) REFERENCES logging_event(event_id))");
+            s.executeUpdate("CREATE TABLE logging_event_exception" +
+                    "  ( event_id         INT NOT NULL, " +
+                    "    i                SMALLINT NOT NULL," +
+                    "    trace_line       VARCHAR NOT NULL," +
+                    "    PRIMARY KEY(event_id, i)," +
+                    "    FOREIGN KEY (event_id) REFERENCES logging_event(event_id))");
+        } catch(SQLException ex) {
+            String s = ex.toString();
+        } finally {
+            connection.close();
+        }
+
+  }
+
+
+  /*
+   * @see TestCase#tearDown()
+   */
+  protected void tearDown()
+         throws Exception {
+    super.tearDown();
+    lrRead.shutdown();
+    witnessEvents = null;
+  }
+
+  /**
+   * Constructor for DBReeceiverTest.
+   * @param arg0
+   */
+  public FullCycleDBTest(String arg0) {
+    super(arg0);
+  }
+
+  
+  /**
+   * This test starts by writing a single event to a DB using DBAppender
+   * and then reads it back using DBReceiver.
+   * 
+   * DB related information is specified within the configuration files.
+   * @throws Exception
+   */
+  public void testSingleOutput()
+         throws Exception {
+    DOMConfigurator jc1 = new DOMConfigurator();
+    InputStream is = FullCycleDBTest.class.getResourceAsStream(appendConfigFile);
+    jc1.doConfigure(is, lrWrite);
+    is.close();
+  
+    long startTime = System.currentTimeMillis();
+    System.out.println("***startTime is  "+startTime);
+
+    // Write out just one log message
+    Logger out = lrWrite.getLogger("testSingleOutput.out");
+    out.debug("some message"+startTime);
+
+    VectorAppender witnessAppender = (VectorAppender) lrWrite.getRootLogger().getAppender("VECTOR");
+    witnessEvents = witnessAppender.getVector();
+    assertEquals(1, witnessEvents.size());    
+
+    // We have to close all appenders before starting to read
+    lrWrite.shutdown();
+
+    // now read it back
+    readBack(readConfigFile, startTime);
+
+  }
+
+  /**
+   * This test starts by writing a single event to a DB using DBAppender
+   * and then reads it back using DBReceiver.
+   * 
+   * The written event includes MDC and repository properties as well as
+   * exception info.
+   * 
+   * DB related information is specified within the configuration files.
+   * @throws Exception
+   */
+  public void testAllFields() throws IOException {
+    DOMConfigurator jc1 = new DOMConfigurator();
+    InputStream is = FullCycleDBTest.class.getResourceAsStream(appendConfigFile);
+    jc1.doConfigure(is, lrWrite);
+    is.close();
+  
+    long startTime = System.currentTimeMillis();
+    
+    // Write out just one log message
+    MDC.put("key1", "value1-"+startTime);
+    MDC.put("key2", "value2-"+startTime);
+    Map mdcMap = MDC.getContext();
+//    LogLog.info("**********"+mdcMap.size());
+    
+    // Write out just one log message
+    Logger out = lrWrite.getLogger("out"+startTime);
+
+    out.debug("some message"+startTime);
+    MDC.put("key3", "value2-"+startTime);
+    out.error("some error message"+startTime, new Exception("testing"));
+    
+    // we clear the MDC to avoid interference with the events read back from
+    // the db
+    MDC.remove("key1");
+    MDC.remove("key2");
+    MDC.remove("key3");
+
+    VectorAppender witnessAppender = (VectorAppender) lrWrite.getRootLogger().getAppender("VECTOR");
+    witnessEvents = witnessAppender.getVector();
+    assertEquals(2, witnessEvents.size());    
+
+    // We have to close all appenders just before starting to read
+    lrWrite.shutdown();
+    
+    readBack(readConfigFile, startTime);
+  }
+
+
+  void readBack(String configfile, long startTime) throws IOException {
+    DOMConfigurator jc2 = new DOMConfigurator();
+    InputStream is = FullCycleDBTest.class.getResourceAsStream(configfile);
+    jc2.doConfigure(is, lrRead);
+    is.close();
+    
+    // wait a little to allow events to be read
+    try { Thread.sleep(3100); } catch(Exception e) {}
+    VectorAppender va = (VectorAppender) lrRead.getRootLogger().getAppender("VECTOR");
+    Vector returnedEvents = getRelevantEventsFromVA(va, startTime);
+    
+    compareEvents(witnessEvents, returnedEvents);
+    
+  }
+  
+  void compareEvents(Vector l, Vector r) {
+    assertNotNull("left vector of events should not be null");
+    assertEquals(l.size(), r.size());
+    
+    for(int i = 0; i < r.size(); i++) {
+      LoggingEvent le = (LoggingEvent) l.get(i);
+      LoggingEvent re = (LoggingEvent) r.get(i);
+      assertEquals(le.getMessage(),        re.getMessage());
+      assertEquals(le.getLoggerName(),     re.getLoggerName());
+      assertEquals(le.getLevel(),          re.getLevel());
+      assertEquals(le.getThreadName(), re.getThreadName());
+      if(re.getTimeStamp() < le.getTimeStamp()) {
+        fail("Returned event cannot preceed witness timestamp");
+      }
+
+      Map sourceMap = re.getProperties();
+      Map remap;
+      if (sourceMap == null) {
+          remap = new HashMap();
+      } else {
+          remap = new HashMap(sourceMap);
+          if (remap.containsKey(Constants.LOG4J_ID_KEY)) {
+              remap.remove(Constants.LOG4J_ID_KEY);
+          }
+      }
+      if(le.getProperties() == null || le.getProperties().size() == 0) {
+        if(remap.size() != 0) {
+          System.out.println("properties are "+remap);
+          fail("Returned event should have been empty");
+        }
+      } else {
+        assertEquals(le.getProperties(), remap);
+      }
+      comprareStringArrays( le.getThrowableStrRep(),  re.getThrowableStrRep());
+      compareLocationInfo(le, re);
+    } 
+  }
+  
+  void comprareStringArrays(String[] la, String[] ra) {
+    if((la == null) && (ra == null)) {
+      return;
+    }
+    assertEquals(la.length, ra.length);
+    for(int i = 0; i < la.length; i++) {
+      assertEquals(la[i], ra[i]);
+    }
+  }
+  
+  void compareLocationInfo(LoggingEvent l, LoggingEvent r) {
+    if(l.locationInformationExists()) {
+      assertEquals(l.getLocationInformation().fullInfo, r.getLocationInformation().fullInfo);
+    } else {
+      assertEquals(LocationInfo.NA_LOCATION_INFO, r.getLocationInformation());
+    }
+  }
+  
+  Vector getRelevantEventsFromVA(VectorAppender va, long startTime) {
+    assertNotNull(va);
+    Vector v = va.getVector();
+    Vector r = new Vector();
+    // remove all elements older than startTime
+    for(Iterator i = v.iterator(); i.hasNext(); ) {
+      LoggingEvent event = (LoggingEvent) i.next();  
+      if(startTime > event.getTimeStamp()) {
+        System.out.println("***Removing event with timestamp "+event.getTimeStamp());
+      } else {
+        System.out.println("***Keeping event with timestamo"+event.getTimeStamp());
+        r.add(event);
+      }
+    }
+    return r;
+  }
+
+  void dump(Vector v) {
+    for(int i = 0; i < v.size(); i++) {
+      LoggingEvent le = (LoggingEvent) v.get(i);
+      System.out.println("---"+le.getLevel()+" "+le.getLoggerName()+" "+le.getMessage());
+    }
+  }
+  
+  public static Test XXsuite() {
+    TestSuite suite = new TestSuite();
+    suite.addTest(new FullCycleDBTest("testSingleOutput"));
+    suite.addTest(new FullCycleDBTest("testAllFields"));
+    return suite;
+  }
+}
diff --git a/src/test/java/org/apache/log4j/helpers/UtilLoggingLevelTest.java b/src/test/java/org/apache/log4j/helpers/UtilLoggingLevelTest.java
new file mode 100644
index 0000000..58105ee
--- /dev/null
+++ b/src/test/java/org/apache/log4j/helpers/UtilLoggingLevelTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.helpers;
+
+import junit.framework.*;
+
+
+/**
+ * Unit tests for UtilLoggingLevel.
+ */
+
+public class UtilLoggingLevelTest extends TestCase {
+
+    /**
+     * Create new instance of test.
+     *
+     * @param testName test name
+     */
+    public UtilLoggingLevelTest(final String testName) {
+        super(testName);
+    }
+
+    /**
+     * Test toLevel("fiNeSt").
+     */
+    public void testToLevelFINEST() {
+        assertSame(UtilLoggingLevel.FINEST, UtilLoggingLevel.toLevel("fiNeSt"));
+    }
+
+}
+
diff --git a/src/test/java/org/apache/log4j/rewrite/RewriteAppenderTest.java b/src/test/java/org/apache/log4j/rewrite/RewriteAppenderTest.java
new file mode 100644
index 0000000..f15700d
--- /dev/null
+++ b/src/test/java/org/apache/log4j/rewrite/RewriteAppenderTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.rewrite;
+
+import junit.framework.*;
+import org.apache.log4j.*;
+import org.apache.log4j.util.Compare;
+import org.apache.log4j.xml.*;
+
+import java.io.InputStream;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.TreeMap;
+import java.util.Hashtable;
+import javax.xml.parsers.*;
+import org.w3c.dom.*;
+
+public class RewriteAppenderTest extends TestCase {
+    public RewriteAppenderTest(final String name) {
+        super(name);
+    }
+
+    public void setUp() {
+        LogManager.getLoggerRepository().resetConfiguration();
+        Hashtable context = MDC.getContext();
+        if (context != null) {
+            context.clear();
+        }
+    }
+
+    public void tearDown() {
+        LogManager.getLoggerRepository().shutdown();
+    }
+
+    public void configure(final String resourceName) throws Exception {
+        InputStream is = RewriteAppenderTest.class.getResourceAsStream(resourceName);
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(false);
+        DocumentBuilder builder = factory.newDocumentBuilder();
+        Document doc = builder.parse(is);
+        DOMConfigurator.configure(doc.getDocumentElement());
+    }
+
+
+    public void testMapPolicy() throws Exception {
+        configure("map.xml");
+        Logger logger = Logger.getLogger(RewriteAppenderTest.class);
+        logger.info("Message 0");
+        MDC.put("p1", "Hola");
+
+        Map msg = new TreeMap();
+        msg.put("p1", "Hello");
+        msg.put("p2", "World");
+        msg.put("x1", "Mundo");
+        logger.info(msg);
+        msg.put("message", "Message 1");
+        logger.info(msg);
+        assertTrue(Compare.compare(RewriteAppenderTest.class, "temp", "map.log"));
+    }
+
+    private static class BaseBean {
+        private final Object p2;
+        private final Object x1;
+
+        public BaseBean(final Object p2,
+                        final Object x1) {
+             this.p2 = p2;
+             this.x1 = x1;
+        }
+
+        public Object getP2() {
+            return p2;
+        }
+
+        public Object getX1() {
+            return x1;
+        }
+
+        public String toString() {
+            return "I am bean.";
+        }
+    }
+
+    private static class MessageBean extends BaseBean {
+        private final Object msg;
+
+        public MessageBean(final Object msg,
+                           final Object p2,
+                           final Object x1) {
+            super(p2, x1);
+            this.msg = msg;
+        }
+
+        public Object getMessage() {
+            return msg;
+        }
+    }
+
+    public void testReflectionPolicy() throws Exception {
+        configure("reflection.xml");
+        Logger logger = Logger.getLogger(RewriteAppenderTest.class);
+        logger.info("Message 0");
+        logger.info(new BaseBean("Hello", "World" ));
+        MDC.put("p1", "Hola");
+        MDC.put("p2", "p2");
+        logger.info(new MessageBean("Welcome to The Hub", "Hello", "World" ));
+        assertTrue(Compare.compare(RewriteAppenderTest.class, "temp", "reflection.log"));
+    }
+
+    public void testPropertyPolicy() throws Exception {
+        configure("property.xml");
+        Logger logger = Logger.getLogger(RewriteAppenderTest.class);
+        logger.info("Message 0");
+        MDC.put("p1", "Hola");
+        logger.info("Message 1");
+        assertTrue(Compare.compare(RewriteAppenderTest.class, "temp", "property.log"));
+    }
+}
diff --git a/src/test/java/org/apache/log4j/util/Compare.java b/src/test/java/org/apache/log4j/util/Compare.java
new file mode 100644
index 0000000..42aa233
--- /dev/null
+++ b/src/test/java/org/apache/log4j/util/Compare.java
@@ -0,0 +1,202 @@
+/*
+ * 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.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.zip.GZIPInputStream;
+
+
+public class Compare {
+  static final int B1_NULL = -1;
+  static final int B2_NULL = -2;
+
+  private static final InputStream open(
+          final Class testClass,
+          final String fileName) throws IOException {
+      String resourceName = fileName;
+      if (fileName.startsWith("witness/")) {
+          resourceName = fileName.substring(fileName.lastIndexOf('/') + 1);
+      }
+      InputStream is = testClass.getResourceAsStream(resourceName);
+      if (is == null) {
+          File file = new File(fileName);
+          if (file.exists()) {
+              is = new FileInputStream(file);
+          } else {
+              throw new FileNotFoundException("Resource "
+                      + resourceName + " not found");
+          }
+      }
+      return is;
+  }
+
+  public static boolean compare(Class testClass,
+                                final String file1,
+                                final String file2)
+    throws IOException {
+    BufferedReader in1 = new BufferedReader(new FileReader(file1));
+    BufferedReader in2 = new BufferedReader(new InputStreamReader(
+            open(testClass, file2)));
+    try {
+      return compare(testClass, file1, file2, in1, in2);
+    } finally {
+      in1.close();
+      in2.close();
+    }
+  }
+    
+ public static boolean compare(
+         Class testClass, String file1, String file2, BufferedReader in1, BufferedReader in2) throws IOException {
+
+    String s1;
+    int lineCounter = 0;
+
+    while ((s1 = in1.readLine()) != null) {
+      lineCounter++;
+
+      String s2 = in2.readLine();
+
+      if (!s1.equals(s2)) {
+        System.out.println(
+          "Files [" + file1 + "] and [" + file2 + "] differ on line "
+          + lineCounter);
+        System.out.println("One reads:  [" + s1 + "].");
+        System.out.println("Other reads:[" + s2 + "].");
+        outputFile(testClass, file1);
+        outputFile(testClass, file2);
+
+        return false;
+      }
+    }
+
+    // the second file is longer
+    if (in2.read() != -1) {
+      System.out.println(
+        "File [" + file2 + "] longer than file [" + file1 + "].");
+      outputFile(testClass, file1);
+      outputFile(testClass, file2);
+
+      return false;
+    }
+
+    return true;
+  }
+
+  /** 
+   * 
+   * Prints file on the console.
+   *
+   */
+  private static void outputFile(Class testClass, String file)
+    throws IOException {
+    InputStream is = open(testClass, file);
+    BufferedReader in1 = new BufferedReader(new InputStreamReader(is));
+
+    String s1;
+    int lineCounter = 0;
+    System.out.println("--------------------------------");
+    System.out.println("Contents of " + file + ":");
+
+    while ((s1 = in1.readLine()) != null) {
+      lineCounter++;
+      System.out.print(lineCounter);
+
+      if (lineCounter < 10) {
+        System.out.print("   : ");
+      } else if (lineCounter < 100) {
+        System.out.print("  : ");
+      } else if (lineCounter < 1000) {
+        System.out.print(" : ");
+      } else {
+        System.out.print(": ");
+      }
+
+      System.out.println(s1);
+    }
+    in1.close();
+  }
+
+
+    public static boolean gzCompare(final Class testClass,
+                                    final String actual,
+                                    final String expected)
+      throws FileNotFoundException, IOException {
+      String resourceName = expected;
+      int lastSlash = expected.lastIndexOf("/");
+      if (lastSlash >= 0) {
+          resourceName = expected.substring(lastSlash + 1);
+      }
+      InputStream resourceStream = testClass.getResourceAsStream(resourceName);
+      if (resourceStream == null) {
+          throw new FileNotFoundException("Could not locate resource " + resourceName);
+      }
+
+      BufferedReader in1 = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(actual))));
+      BufferedReader in2 = new BufferedReader(new InputStreamReader(new GZIPInputStream(resourceStream)));
+      try {
+        return gzCompare(testClass, actual, expected, in1, in2);
+      } finally {
+        in1.close();
+        in2.close();
+      }
+    }
+
+    public static boolean gzCompare(Class testClass, String file1, String file2, BufferedReader in1, BufferedReader in2) throws IOException {
+
+      String s1;
+      int lineCounter = 0;
+
+      while ((s1 = in1.readLine()) != null) {
+        lineCounter++;
+
+        String s2 = in2.readLine();
+
+        if (!s1.equals(s2)) {
+          System.out.println(
+            "Files [" + file1 + "] and [" + file2 + "] differ on line "
+            + lineCounter);
+          System.out.println("One reads:  [" + s1 + "].");
+          System.out.println("Other reads:[" + s2 + "].");
+          outputFile(testClass, file1);
+          outputFile(testClass, file2);
+
+          return false;
+        }
+      }
+
+      // the second file is longer
+      if (in2.read() != -1) {
+        System.out.println(
+          "File [" + file2 + "] longer than file [" + file1 + "].");
+        outputFile(testClass, file1);
+        outputFile(testClass, file2);
+
+        return false;
+      }
+
+      return true;
+    }
+
+}
diff --git a/src/test/java/org/apache/log4j/xml/XMLDecoderTest.java b/src/test/java/org/apache/log4j/xml/XMLDecoderTest.java
new file mode 100644
index 0000000..bd9e7bc
--- /dev/null
+++ b/src/test/java/org/apache/log4j/xml/XMLDecoderTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.xml;
+
+import junit.framework.TestCase;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.CharBuffer;
+import java.util.Vector;
+import java.net.URL;
+
+/**
+ * Tests for XMLDecoder.
+ *
+ */
+public class XMLDecoderTest extends TestCase {
+
+
+  /**
+   * Constructor for XMLDecoderTest.
+   * @param arg0 test name.
+   */
+  public XMLDecoderTest(String arg0) {
+    super(arg0);
+  }
+
+  public String getStringFromResource(final String resourceName,
+                                      final int maxSize) throws Exception {
+      InputStream is = XMLDecoderTest.class.getResourceAsStream(resourceName);
+      if (is == null) {
+          throw new FileNotFoundException(resourceName);
+      }
+      InputStreamReader reader = new InputStreamReader(is, "UTF-8");
+      CharBuffer cb = CharBuffer.allocate(maxSize);
+      for(int chars = reader.read(cb);
+          chars != -1;
+          chars = reader.read(cb));
+      cb.flip();
+      return cb.toString();
+  }
+
+    public void testDecodeEventsString1() throws Exception {
+        String xmlStr = getStringFromResource("xmlLayout.1.xml", 10000);
+        XMLDecoder decoder = new XMLDecoder();
+        Vector events = decoder.decodeEvents(xmlStr);
+        assertEquals(17, events.size());
+    }
+
+  public void testDecodeEventsString2() throws Exception {
+      String xmlStr = getStringFromResource("xsltLayout.1.xml", 10000);
+      XMLDecoder decoder = new XMLDecoder();
+      Vector events = decoder.decodeEvents(xmlStr);
+      assertEquals(15, events.size());
+  }
+
+    public void testDecodeEventsURL1() throws Exception {
+        URL resource = XMLDecoderTest.class.getResource("xmlLayout.1.xml");
+        XMLDecoder decoder = new XMLDecoder();
+        Vector events = decoder.decode(resource);
+        assertEquals(17, events.size());
+    }
+
+    public void testDecodeEventsURL2() throws Exception {
+        URL resource = XMLDecoderTest.class.getResource("xsltLayout.1.xml");
+        XMLDecoder decoder = new XMLDecoder();
+        Vector events = decoder.decode(resource);
+        assertEquals(15, events.size());
+    }
+
+}
diff --git a/src/test/resources/org/apache/log4j/db/append-with-drivermanager1.xml b/src/test/resources/org/apache/log4j/db/append-with-drivermanager1.xml
new file mode 100644
index 0000000..04d02bd
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/db/append-with-drivermanager1.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ 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.
+
+-->
+<!DOCTYPE log4j:configuration SYSTEM 'http://logging.apache.org/log4j/1.2/log4j.dtd'>
+	  
+<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/' debug="true">
+	  
+  <appender name="DB" class="org.apache.log4j.db.DBAppender">
+     <param name="locationInfo" value="true"/>
+     <connectionSource class="org.apache.log4j.db.DriverManagerConnectionSource">
+       <param name="driverClass" value="org.hsqldb.jdbcDriver"/>
+       <param name="url" value="jdbc:hsqldb:mem:testdb"/>
+       <param name="user" value="sa"/>
+       <param name="password" value=""/>
+     </connectionSource>
+  </appender>
+
+  <appender name="VECTOR" class="org.apache.log4j.VectorAppender">
+  </appender>
+  
+  <!-- Prevent internal log4j DEBUG messages from polluting the output. -->
+  <logger name="org.apache.log4j.joran"><level value="INFO" /></logger>
+  <logger name="org.apache.log4j.config"><level value="INFO" /></logger>
+  <logger name="org.apache.log4j.db.DBAppender"><level value="INFO" /></logger>
+  
+  <root>
+    <level value ="debug"/>
+    <appender-ref ref="DB" />
+    <appender-ref ref="VECTOR" />
+  </root>  
+</log4j:configuration>
+
+
diff --git a/src/test/resources/org/apache/log4j/db/read-with-drivermanager1.xml b/src/test/resources/org/apache/log4j/db/read-with-drivermanager1.xml
new file mode 100644
index 0000000..3e77c49
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/db/read-with-drivermanager1.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ 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.
+
+-->
+<!DOCTYPE log4j:configuration SYSTEM 'http://logging.apache.org/log4j/1.2/log4j.dtd'>
+
+<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/' debug="true">
+	  
+  <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
+     <layout class="org.apache.log4j.PatternLayout">
+       <param name="ConversionPattern" value="READING %relative %level %logger - %message%n"/>
+     </layout>
+  </appender>
+  
+  <appender name="VECTOR" class="org.apache.log4j.VectorAppender">
+  </appender>
+  
+  <plugin name="DB" class="org.apache.log4j.db.DBReceiver">
+     <connectionSource class="org.apache.log4j.db.DriverManagerConnectionSource">
+       <param name="driverClass" value="org.hsqldb.jdbcDriver"/>
+       <param name="url" value="jdbc:hsqldb:mem:testdb"/>
+       <param name="user" value="sa"/>
+       <param name="password" value=""/>
+     </connectionSource>
+  </plugin>
+  
+
+  <!-- Prevent internal log4j DEBUG messages from polluting the output. -->
+  <logger name="org.apache.log4j.joran"><level value="INFO" /></logger>
+  <logger name="org.apache.log4j.config"><level value="INFO" /></logger>
+  <logger name="org.apache.log4j.db"><level value="INFO" /></logger>
+    
+  <root>
+    <level value="debug"/>
+    <appender-ref ref="VECTOR" />
+    <appender-ref ref="CONSOLE" />
+  </root>  
+</log4j:configuration>
+
+
+
diff --git a/src/test/resources/org/apache/log4j/rewrite/map.log b/src/test/resources/org/apache/log4j/rewrite/map.log
new file mode 100644
index 0000000..3ce933f
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/rewrite/map.log
@@ -0,0 +1,3 @@
+INFO org.apache.log4j.rewrite.RewriteAppenderTest - p1: p2: Message 0
+INFO org.apache.log4j.rewrite.RewriteAppenderTest - p1:Hello p2:World {p1=Hello, p2=World, x1=Mundo}
+INFO org.apache.log4j.rewrite.RewriteAppenderTest - p1:Hello p2:World Message 1
diff --git a/src/test/resources/org/apache/log4j/rewrite/map.xml b/src/test/resources/org/apache/log4j/rewrite/map.xml
new file mode 100644
index 0000000..7cb60b7
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/rewrite/map.xml
@@ -0,0 +1,38 @@
+<!--
+ 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.
+
+-->
+<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
+    <appender name="F1" class="org.apache.log4j.FileAppender">
+        <param name="file"   value="temp"/>
+        <param name="append" value="false"/>
+        <layout class="org.apache.log4j.PatternLayout">
+           <param name="ConversionPattern" value="%p %c - p1:%X{p1} p2:%X{p2} %m%n"/>
+        </layout>
+    </appender>
+
+
+  <appender name="A1" class="org.apache.log4j.rewrite.RewriteAppender">
+      <appender-ref ref="F1"/>
+      <rewritePolicy class="org.apache.log4j.rewrite.MapRewritePolicy"/>
+  </appender>
+
+  <root>
+    <level value ="debug" />
+    <appender-ref ref="A1" />
+  </root>
+
+</log4j:configuration>
diff --git a/src/test/resources/org/apache/log4j/rewrite/property.log b/src/test/resources/org/apache/log4j/rewrite/property.log
new file mode 100644
index 0000000..9aa2c49
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/rewrite/property.log
@@ -0,0 +1,2 @@
+INFO org.apache.log4j.rewrite.RewriteAppenderTest - p1:Hello p2:World Message 0
+INFO org.apache.log4j.rewrite.RewriteAppenderTest - p1:Hola p2:World Message 1
diff --git a/src/test/resources/org/apache/log4j/rewrite/property.xml b/src/test/resources/org/apache/log4j/rewrite/property.xml
new file mode 100644
index 0000000..13a04f8
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/rewrite/property.xml
@@ -0,0 +1,40 @@
+<!--
+ 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.
+
+-->
+<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
+    <appender name="F1" class="org.apache.log4j.FileAppender">
+        <param name="file"   value="temp"/>
+        <param name="append" value="false"/>
+        <layout class="org.apache.log4j.PatternLayout">
+           <param name="ConversionPattern" value="%p %c - p1:%X{p1} p2:%X{p2} %m%n"/>
+        </layout>
+    </appender>
+
+
+  <appender name="A1" class="org.apache.log4j.rewrite.RewriteAppender">
+      <appender-ref ref="F1"/>
+      <rewritePolicy class="org.apache.log4j.rewrite.PropertyRewritePolicy">
+          <param name="properties" value="p1=Hello,p2=World,x1=3.1415"/>
+      </rewritePolicy>
+  </appender>
+
+  <root>
+    <level value ="debug" />
+    <appender-ref ref="A1" />
+  </root>
+
+</log4j:configuration>
diff --git a/src/test/resources/org/apache/log4j/rewrite/reflection.log b/src/test/resources/org/apache/log4j/rewrite/reflection.log
new file mode 100644
index 0000000..da0b52f
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/rewrite/reflection.log
@@ -0,0 +1,3 @@
+INFO org.apache.log4j.rewrite.RewriteAppenderTest - p1: p2: Message 0
+INFO org.apache.log4j.rewrite.RewriteAppenderTest - p1: p2:Hello I am bean.
+INFO org.apache.log4j.rewrite.RewriteAppenderTest - p1:Hola p2:Hello Welcome to The Hub
diff --git a/src/test/resources/org/apache/log4j/rewrite/reflection.xml b/src/test/resources/org/apache/log4j/rewrite/reflection.xml
new file mode 100644
index 0000000..643850b
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/rewrite/reflection.xml
@@ -0,0 +1,38 @@
+<!--
+ 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.
+
+-->
+<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
+    <appender name="F1" class="org.apache.log4j.FileAppender">
+        <param name="file"   value="temp"/>
+        <param name="append" value="false"/>
+        <layout class="org.apache.log4j.PatternLayout">
+           <param name="ConversionPattern" value="%p %c - p1:%X{p1} p2:%X{p2} %m%n"/>
+        </layout>
+    </appender>
+
+
+  <appender name="A1" class="org.apache.log4j.rewrite.RewriteAppender">
+      <appender-ref ref="F1"/>
+      <rewritePolicy class="org.apache.log4j.rewrite.ReflectionRewritePolicy"/>
+  </appender>
+
+  <root>
+    <level value ="debug" />
+    <appender-ref ref="A1" />
+  </root>
+
+</log4j:configuration>
diff --git a/src/test/resources/org/apache/log4j/xml/xmlLayout.1.xml b/src/test/resources/org/apache/log4j/xml/xmlLayout.1.xml
new file mode 100644
index 0000000..06dc106
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/xml/xmlLayout.1.xml
@@ -0,0 +1,162 @@
+<!--
+  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.
+-->
+<log4j:event logger="org.apache.log4j.xml.XMLLayoutTestCase$X" timestamp="1187196936354" level="INFO" thread="main">
+<log4j:message><![CDATA[in X() constructor]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="org.apache.log4j.xml.XMLLayoutTestCase" timestamp="1187196936358" level="TRACE" thread="main">
+<log4j:message><![CDATA[Message 0]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="root" timestamp="1187196936358" level="TRACE" thread="main">
+<log4j:message><![CDATA[Message 0]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="org.apache.log4j.xml.XMLLayoutTestCase" timestamp="1187196936358" level="DEBUG" thread="main">
+<log4j:message><![CDATA[Message 1]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="root" timestamp="1187196936363" level="DEBUG" thread="main">
+<log4j:message><![CDATA[Message 1]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="org.apache.log4j.xml.XMLLayoutTestCase" timestamp="1187196936363" level="INFO" thread="main">
+<log4j:message><![CDATA[Message 2]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="root" timestamp="1187196936363" level="INFO" thread="main">
+<log4j:message><![CDATA[Message 2]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="org.apache.log4j.xml.XMLLayoutTestCase" timestamp="1187196936363" level="WARN" thread="main">
+<log4j:message><![CDATA[Message 3]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="root" timestamp="1187196936363" level="WARN" thread="main">
+<log4j:message><![CDATA[Message 3]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="org.apache.log4j.xml.XMLLayoutTestCase" timestamp="1187196936363" level="ERROR" thread="main">
+<log4j:message><![CDATA[Message 4]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="root" timestamp="1187196936363" level="ERROR" thread="main">
+<log4j:message><![CDATA[Message 4]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="org.apache.log4j.xml.XMLLayoutTestCase" timestamp="1187196936363" level="FATAL" thread="main">
+<log4j:message><![CDATA[Message 5]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="root" timestamp="1187196936363" level="FATAL" thread="main">
+<log4j:message><![CDATA[Message 5]]></log4j:message>
+</log4j:event>
+
+<log4j:event logger="org.apache.log4j.xml.XMLLayoutTestCase" timestamp="1187196936364" level="DEBUG" thread="main">
+<log4j:message><![CDATA[Message 6]]></log4j:message>
+<log4j:throwable><![CDATA[java.lang.Exception: Just testing
+	at org.apache.log4j.xml.XMLLayoutTestCase.common(XMLLayoutTestCase.java:219)
+	at org.apache.log4j.xml.XMLLayoutTestCase.basic(XMLLayoutTestCase.java:64)
+	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
+	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
+	at java.lang.reflect.Method.invoke(Method.java:585)
+	at junit.framework.TestCase.runTest(TestCase.java:154)
+	at junit.framework.TestCase.runBare(TestCase.java:127)
+	at junit.framework.TestResult$1.protect(TestResult.java:106)
+	at junit.framework.TestResult.runProtected(TestResult.java:124)
+	at junit.framework.TestResult.run(TestResult.java:109)
+	at junit.framework.TestCase.run(TestCase.java:118)
+	at junit.framework.TestSuite.runTest(TestSuite.java:208)
+	at junit.framework.TestSuite.run(TestSuite.java:203)
+	at junit.textui.TestRunner.doRun(TestRunner.java:116)
+	at com.intellij.rt.execution.junit.IdeaTestRunner.doRun(IdeaTestRunner.java:69)
+	at junit.textui.TestRunner.doRun(TestRunner.java:109)
+	at com.intellij.rt.execution.junit.IdeaTestRunner.startRunnerWithArgs(IdeaTestRunner.java:24)
+	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:118)
+	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:40)
+]]></log4j:throwable>
+</log4j:event>
+
+<log4j:event logger="root" timestamp="1187196936368" level="DEBUG" thread="main">
+<log4j:message><![CDATA[Message 6]]></log4j:message>
+<log4j:throwable><![CDATA[java.lang.Exception: Just testing
+	at org.apache.log4j.xml.XMLLayoutTestCase.common(XMLLayoutTestCase.java:219)
+	at org.apache.log4j.xml.XMLLayoutTestCase.basic(XMLLayoutTestCase.java:64)
+	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
+	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
+	at java.lang.reflect.Method.invoke(Method.java:585)
+	at junit.framework.TestCase.runTest(TestCase.java:154)
+	at junit.framework.TestCase.runBare(TestCase.java:127)
+	at junit.framework.TestResult$1.protect(TestResult.java:106)
+	at junit.framework.TestResult.runProtected(TestResult.java:124)
+	at junit.framework.TestResult.run(TestResult.java:109)
+	at junit.framework.TestCase.run(TestCase.java:118)
+	at junit.framework.TestSuite.runTest(TestSuite.java:208)
+	at junit.framework.TestSuite.run(TestSuite.java:203)
+	at junit.textui.TestRunner.doRun(TestRunner.java:116)
+	at com.intellij.rt.execution.junit.IdeaTestRunner.doRun(IdeaTestRunner.java:69)
+	at junit.textui.TestRunner.doRun(TestRunner.java:109)
+	at com.intellij.rt.execution.junit.IdeaTestRunner.startRunnerWithArgs(IdeaTestRunner.java:24)
+	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:118)
+	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:40)
+]]></log4j:throwable>
+</log4j:event>
+
+<log4j:event logger="org.apache.log4j.xml.XMLLayoutTestCase" timestamp="1187196936369" level="ERROR" thread="main">
+<log4j:message><![CDATA[Message 7]]></log4j:message>
+<log4j:throwable><![CDATA[java.lang.Exception: Just testing
+	at org.apache.log4j.xml.XMLLayoutTestCase.common(XMLLayoutTestCase.java:219)
+	at org.apache.log4j.xml.XMLLayoutTestCase.basic(XMLLayoutTestCase.java:64)
+	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
+	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
+	at java.lang.reflect.Method.invoke(Method.java:585)
+	at junit.framework.TestCase.runTest(TestCase.java:154)
+	at junit.framework.TestCase.runBare(TestCase.java:127)
+	at junit.framework.TestResult$1.protect(TestResult.java:106)
+	at junit.framework.TestResult.runProtected(TestResult.java:124)
+	at junit.framework.TestResult.run(TestResult.java:109)
+	at junit.framework.TestCase.run(TestCase.java:118)
+	at junit.framework.TestSuite.runTest(TestSuite.java:208)
+	at junit.framework.TestSuite.run(TestSuite.java:203)
+	at junit.textui.TestRunner.doRun(TestRunner.java:116)
+]]></log4j:throwable>
+</log4j:event>
+
+<log4j:event logger="root" timestamp="1187196936371" level="ERROR" thread="main">
+<log4j:message><![CDATA[Message 7]]></log4j:message>
+<log4j:throwable><![CDATA[java.lang.Exception: Just testing
+	at org.apache.log4j.xml.XMLLayoutTestCase.common(XMLLayoutTestCase.java:219)
+	at org.apache.log4j.xml.XMLLayoutTestCase.basic(XMLLayoutTestCase.java:64)
+	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
+	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
+	at java.lang.reflect.Method.invoke(Method.java:585)
+	at junit.framework.TestCase.runTest(TestCase.java:154)
+	at junit.framework.TestCase.runBare(TestCase.java:127)
+	at junit.framework.TestResult$1.protect(TestResult.java:106)
+	at junit.framework.TestResult.runProtected(TestResult.java:124)
+	at junit.framework.TestResult.run(TestResult.java:109)
+	at junit.framework.TestCase.run(TestCase.java:118)
+	at junit.framework.TestSuite.runTest(TestSuite.java:208)
+	at junit.framework.TestSuite.run(TestSuite.java:203)
+	at junit.textui.TestRunner.doRun(TestRunner.java:116)
+]]></log4j:throwable>
+</log4j:event>
+
diff --git a/src/test/resources/org/apache/log4j/xml/xsltLayout.1.xml b/src/test/resources/org/apache/log4j/xml/xsltLayout.1.xml
new file mode 100644
index 0000000..596e84d
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/xml/xsltLayout.1.xml
@@ -0,0 +1,139 @@
+<!--
+  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.
+-->
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="org.apache.log4j.xml.XSLTLayoutTestCase$X" time="2007-08-15T16:42:31.821Z" timestamp="1187196151821" level="INFO" thread="main">
+<log4j:message>in X() constructor</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="org.apache.log4j.xml.XSLTLayoutTestCase" time="2007-08-15T16:42:32.252Z" timestamp="1187196152252" level="DEBUG" thread="main">
+<log4j:message>Message 0</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="root" time="2007-08-15T16:42:32.254Z" timestamp="1187196152254" level="DEBUG" thread="main">
+<log4j:message>Message 0</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="org.apache.log4j.xml.XSLTLayoutTestCase" time="2007-08-15T16:42:32.263Z" timestamp="1187196152263" level="INFO" thread="main">
+<log4j:message>Message 1</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="root" time="2007-08-15T16:42:32.264Z" timestamp="1187196152264" level="INFO" thread="main">
+<log4j:message>Message 1</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="org.apache.log4j.xml.XSLTLayoutTestCase" time="2007-08-15T16:42:32.268Z" timestamp="1187196152268" level="WARN" thread="main">
+<log4j:message>Message 2</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="root" time="2007-08-15T16:42:32.269Z" timestamp="1187196152269" level="WARN" thread="main">
+<log4j:message>Message 2</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="org.apache.log4j.xml.XSLTLayoutTestCase" time="2007-08-15T16:42:32.271Z" timestamp="1187196152271" level="ERROR" thread="main">
+<log4j:message>Message 3</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="root" time="2007-08-15T16:42:32.272Z" timestamp="1187196152272" level="ERROR" thread="main">
+<log4j:message>Message 3</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="org.apache.log4j.xml.XSLTLayoutTestCase" time="2007-08-15T16:42:32.273Z" timestamp="1187196152273" level="FATAL" thread="main">
+<log4j:message>Message 4</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="root" time="2007-08-15T16:42:32.275Z" timestamp="1187196152275" level="FATAL" thread="main">
+<log4j:message>Message 4</log4j:message>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="org.apache.log4j.xml.XSLTLayoutTestCase" time="2007-08-15T16:42:32.279Z" timestamp="1187196152279" level="DEBUG" thread="main">
+<log4j:message>Message 5</log4j:message>
+<log4j:throwable>java.lang.Exception: Just testing
+	at org.apache.log4j.xml.XSLTLayoutTestCase.common(XSLTLayoutTestCase.java:201)
+	at org.apache.log4j.xml.XSLTLayoutTestCase.testBasic(XSLTLayoutTestCase.java:65)
+	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
+	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
+	at java.lang.reflect.Method.invoke(Method.java:585)
+	at junit.framework.TestCase.runTest(TestCase.java:154)
+	at junit.framework.TestCase.runBare(TestCase.java:127)
+	at junit.framework.TestResult$1.protect(TestResult.java:106)
+	at junit.framework.TestResult.runProtected(TestResult.java:124)
+	at junit.framework.TestResult.run(TestResult.java:109)
+	at junit.framework.TestCase.run(TestCase.java:118)
+	at junit.framework.TestSuite.runTest(TestSuite.java:208)
+	at junit.framework.TestSuite.run(TestSuite.java:203)
+	at junit.textui.TestRunner.doRun(TestRunner.java:116)
+	at com.intellij.rt.execution.junit.IdeaTestRunner.doRun(IdeaTestRunner.java:69)
+	at junit.textui.TestRunner.doRun(TestRunner.java:109)
+	at com.intellij.rt.execution.junit.IdeaTestRunner.startRunnerWithArgs(IdeaTestRunner.java:24)
+	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:118)
+	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:40)
+</log4j:throwable>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="root" time="2007-08-15T16:42:32.304Z" timestamp="1187196152304" level="DEBUG" thread="main">
+<log4j:message>Message 5</log4j:message>
+<log4j:throwable>java.lang.Exception: Just testing
+	at org.apache.log4j.xml.XSLTLayoutTestCase.common(XSLTLayoutTestCase.java:201)
+	at org.apache.log4j.xml.XSLTLayoutTestCase.testBasic(XSLTLayoutTestCase.java:65)
+	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
+	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
+	at java.lang.reflect.Method.invoke(Method.java:585)
+	at junit.framework.TestCase.runTest(TestCase.java:154)
+	at junit.framework.TestCase.runBare(TestCase.java:127)
+	at junit.framework.TestResult$1.protect(TestResult.java:106)
+	at junit.framework.TestResult.runProtected(TestResult.java:124)
+	at junit.framework.TestResult.run(TestResult.java:109)
+	at junit.framework.TestCase.run(TestCase.java:118)
+	at junit.framework.TestSuite.runTest(TestSuite.java:208)
+	at junit.framework.TestSuite.run(TestSuite.java:203)
+	at junit.textui.TestRunner.doRun(TestRunner.java:116)
+	at com.intellij.rt.execution.junit.IdeaTestRunner.doRun(IdeaTestRunner.java:69)
+	at junit.textui.TestRunner.doRun(TestRunner.java:109)
+	at com.intellij.rt.execution.junit.IdeaTestRunner.startRunnerWithArgs(IdeaTestRunner.java:24)
+	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:118)
+	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:40)
+</log4j:throwable>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="org.apache.log4j.xml.XSLTLayoutTestCase" time="2007-08-15T16:42:32.308Z" timestamp="1187196152308" level="ERROR" thread="main">
+<log4j:message>Message 6</log4j:message>
+<log4j:throwable>java.lang.Exception: Just testing
+	at org.apache.log4j.xml.XSLTLayoutTestCase.common(XSLTLayoutTestCase.java:201)
+	at org.apache.log4j.xml.XSLTLayoutTestCase.testBasic(XSLTLayoutTestCase.java:65)
+	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
+	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
+	at java.lang.reflect.Method.invoke(Method.java:585)
+	at junit.framework.TestCase.runTest(TestCase.java:154)
+	at junit.framework.TestCase.runBare(TestCase.java:127)
+	at junit.framework.TestResult$1.protect(TestResult.java:106)
+	at junit.framework.TestResult.runProtected(TestResult.java:124)
+	at junit.framework.TestResult.run(TestResult.java:109)
+	at junit.framework.TestCase.run(TestCase.java:118)
+	at junit.framework.TestSuite.runTest(TestSuite.java:208)
+	at junit.framework.TestSuite.run(TestSuite.java:203)
+	at junit.textui.TestRunner.doRun(TestRunner.java:116)
+</log4j:throwable>
+</log4j:event>
+<log4j:event xmlns:log4j="http://jakarta.apache.org/log4j/" logger="root" time="2007-08-15T16:42:32.312Z" timestamp="1187196152312" level="ERROR" thread="main">
+<log4j:message>Message 6</log4j:message>
+<log4j:throwable>java.lang.Exception: Just testing
+	at org.apache.log4j.xml.XSLTLayoutTestCase.common(XSLTLayoutTestCase.java:201)
+	at org.apache.log4j.xml.XSLTLayoutTestCase.testBasic(XSLTLayoutTestCase.java:65)
+	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
+	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
+	at java.lang.reflect.Method.invoke(Method.java:585)
+	at junit.framework.TestCase.runTest(TestCase.java:154)
+	at junit.framework.TestCase.runBare(TestCase.java:127)
+	at junit.framework.TestResult$1.protect(TestResult.java:106)
+	at junit.framework.TestResult.runProtected(TestResult.java:124)
+	at junit.framework.TestResult.run(TestResult.java:109)
+	at junit.framework.TestCase.run(TestCase.java:118)
+	at junit.framework.TestSuite.runTest(TestSuite.java:208)
+	at junit.framework.TestSuite.run(TestSuite.java:203)
+	at junit.textui.TestRunner.doRun(TestRunner.java:116)
+</log4j:throwable>
+</log4j:event>