moved components to extras

git-svn-id: https://svn.apache.org/repos/asf/logging/log4j/companions/extras/trunk@1342917 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index 793aea4..1b1d473 100644
--- a/pom.xml
+++ b/pom.xml
@@ -328,6 +328,20 @@
       </plugin>	 
     </plugins>
   </build>
+  <dependencies>
+      <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>
   <distributionManagement>
     <repository>
       <id>logging.repo</id>
diff --git a/src/main/java/org/apache/log4j/component/LoggerRepositoryExImpl.java b/src/main/java/org/apache/log4j/component/LoggerRepositoryExImpl.java
new file mode 100644
index 0000000..c2f5aaf
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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/component/ULogger.java b/src/main/java/org/apache/log4j/component/ULogger.java
new file mode 100644
index 0000000..f3bf1d1
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component;
+
+
+/**
+ * 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/component/helpers/Constants.java b/src/main/java/org/apache/log4j/component/helpers/Constants.java
new file mode 100644
index 0000000..53946b0
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/component/helpers/MessageFormatter.java b/src/main/java/org/apache/log4j/component/helpers/MessageFormatter.java
new file mode 100644
index 0000000..cf84a97
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/component/plugins/Pauseable.java b/src/main/java/org/apache/log4j/component/plugins/Pauseable.java
new file mode 100644
index 0000000..b9790bd
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/component/plugins/Plugin.java b/src/main/java/org/apache/log4j/component/plugins/Plugin.java
new file mode 100644
index 0000000..7c38bdc
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/component/plugins/PluginEvent.java b/src/main/java/org/apache/log4j/component/plugins/PluginEvent.java
new file mode 100644
index 0000000..15fb6ab
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/component/plugins/PluginListener.java b/src/main/java/org/apache/log4j/component/plugins/PluginListener.java
new file mode 100644
index 0000000..c566d48
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/component/plugins/PluginRegistry.java b/src/main/java/org/apache/log4j/component/plugins/PluginRegistry.java
new file mode 100644
index 0000000..ba75dfc
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/plugins/PluginRegistry.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.component.plugins;
+
+import org.apache.log4j.component.spi.LoggerRepositoryEventListener;
+import org.apache.log4j.component.spi.LoggerRepositoryEx;
+import org.apache.log4j.spi.LoggerRepository;
+
+
+import java.util.*;
+
+
+/**
+ * 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/component/plugins/PluginSkeleton.java b/src/main/java/org/apache/log4j/component/plugins/PluginSkeleton.java
new file mode 100644
index 0000000..77b496e
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.plugins;
+
+import org.apache.log4j.component.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/component/plugins/Receiver.java b/src/main/java/org/apache/log4j/component/plugins/Receiver.java
new file mode 100644
index 0000000..3b762e1
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.plugins;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.component.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/component/scheduler/Job.java b/src/main/java/org/apache/log4j/component/scheduler/Job.java
new file mode 100644
index 0000000..ad0e8eb
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/component/scheduler/Scheduler.java b/src/main/java/org/apache/log4j/component/scheduler/Scheduler.java
new file mode 100644
index 0000000..974afe7
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/component/spi/Component.java b/src/main/java/org/apache/log4j/component/spi/Component.java
new file mode 100644
index 0000000..820ec43
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/spi/Component.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.component.spi;
+
+
+import org.apache.log4j.spi.LoggerRepository;
+
+/**
+ * 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/component/spi/ComponentBase.java b/src/main/java/org/apache/log4j/component/spi/ComponentBase.java
new file mode 100644
index 0000000..e6302a9
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/spi/ComponentBase.java
@@ -0,0 +1,127 @@
+/*
+ * 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.component.spi;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.component.ULogger;
+import org.apache.log4j.spi.LoggerRepository;
+
+
+/**
+ * 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/component/spi/ErrorItem.java b/src/main/java/org/apache/log4j/component/spi/ErrorItem.java
new file mode 100644
index 0000000..972bf69
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/component/spi/Log4JULogger.java b/src/main/java/org/apache/log4j/component/spi/Log4JULogger.java
new file mode 100644
index 0000000..828857f
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/spi/Log4JULogger.java
@@ -0,0 +1,228 @@
+/*
+ * 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.component.spi;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.component.ULogger;
+import org.apache.log4j.component.helpers.MessageFormatter;
+
+/**
+ * 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/component/spi/LoggerEventListener.java b/src/main/java/org/apache/log4j/component/spi/LoggerEventListener.java
new file mode 100644
index 0000000..ef4d9f1
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/component/spi/LoggerRepositoryEventListener.java b/src/main/java/org/apache/log4j/component/spi/LoggerRepositoryEventListener.java
new file mode 100644
index 0000000..97c9149
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/spi/LoggerRepositoryEventListener.java
@@ -0,0 +1,55 @@
+/*
+ * 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.component.spi;
+
+
+import org.apache.log4j.spi.LoggerRepository;
+
+/**
+  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/component/spi/LoggerRepositoryEx.java b/src/main/java/org/apache/log4j/component/spi/LoggerRepositoryEx.java
new file mode 100644
index 0000000..59509d4
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/spi/LoggerRepositoryEx.java
@@ -0,0 +1,201 @@
+/*
+ * 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.component.spi;
+
+import org.apache.log4j.Appender;
+import org.apache.log4j.Category;
+import org.apache.log4j.Logger;
+import org.apache.log4j.component.plugins.PluginRegistry;
+import org.apache.log4j.component.scheduler.Scheduler;
+
+import org.apache.log4j.spi.LoggerFactory;
+import org.apache.log4j.spi.LoggerRepository;
+
+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/component/spi/NOPULogger.java b/src/main/java/org/apache/log4j/component/spi/NOPULogger.java
new file mode 100644
index 0000000..c65aa8b
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.spi;
+
+import org.apache.log4j.component.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/component/spi/SimpleULogger.java b/src/main/java/org/apache/log4j/component/spi/SimpleULogger.java
new file mode 100644
index 0000000..830014e
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.spi;
+
+import org.apache.log4j.component.ULogger;
+import org.apache.log4j.component.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/component/spi/Thresholdable.java b/src/main/java/org/apache/log4j/component/spi/Thresholdable.java
new file mode 100644
index 0000000..b570555
--- /dev/null
+++ b/src/main/java/org/apache/log4j/component/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.component.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/receivers/db/ConnectionSource.java b/src/main/java/org/apache/log4j/receivers/db/ConnectionSource.java
new file mode 100644
index 0000000..c850a98
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/db/ConnectionSource.java
@@ -0,0 +1,70 @@
+/*
+ * 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.receivers.db;
+
+
+import org.apache.log4j.component.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/receivers/db/ConnectionSourceSkeleton.java b/src/main/java/org/apache/log4j/receivers/db/ConnectionSourceSkeleton.java
new file mode 100644
index 0000000..3b7c9dd
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/db/ConnectionSourceSkeleton.java
@@ -0,0 +1,151 @@
+/*
+ * 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.receivers.db;
+
+
+import org.apache.log4j.component.spi.ComponentBase;
+import org.apache.log4j.receivers.db.dialect.Util;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+
+
+/**
+ * @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/receivers/db/CustomSQLDBReceiver.java b/src/main/java/org/apache/log4j/receivers/db/CustomSQLDBReceiver.java
new file mode 100644
index 0000000..55efc72
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/db/CustomSQLDBReceiver.java
@@ -0,0 +1,469 @@
+/*
+ * 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.receivers.db;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.component.plugins.Pauseable;
+import org.apache.log4j.component.plugins.Receiver;
+import org.apache.log4j.component.scheduler.Job;
+import org.apache.log4j.component.scheduler.Scheduler;
+import org.apache.log4j.component.spi.LoggerRepositoryEx;
+import org.apache.log4j.spi.LocationInfo;
+
+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;
+
+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;
+
+/**
+ * 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 CustomReceiverJob 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/receivers/db/DBAppender.java b/src/main/java/org/apache/log4j/receivers/db/DBAppender.java
new file mode 100644
index 0000000..402b4cc
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/db/DBAppender.java
@@ -0,0 +1,401 @@
+/*
+ * 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.helpers.LogLog;
+import org.apache.log4j.receivers.db.ConnectionSource;
+import org.apache.log4j.receivers.db.DBHelper;
+import org.apache.log4j.receivers.db.dialect.SQLDialect;
+import org.apache.log4j.receivers.db.dialect.Util;
+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.*;
+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/receivers/db/DBHelper.java b/src/main/java/org/apache/log4j/receivers/db/DBHelper.java
new file mode 100644
index 0000000..9754c5e
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.db;
+
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Set;
+
+/**
+ * @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/receivers/db/DBReceiver.java b/src/main/java/org/apache/log4j/receivers/db/DBReceiver.java
new file mode 100644
index 0000000..6877a97
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.db;
+
+import org.apache.log4j.component.plugins.Pauseable;
+import org.apache.log4j.component.plugins.Receiver;
+import org.apache.log4j.component.scheduler.Scheduler;
+import org.apache.log4j.component.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/receivers/db/DBReceiverJob.java b/src/main/java/org/apache/log4j/receivers/db/DBReceiverJob.java
new file mode 100644
index 0000000..cc03769
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.db;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.component.scheduler.Job;
+import org.apache.log4j.component.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/receivers/db/DataSourceConnectionSource.java b/src/main/java/org/apache/log4j/receivers/db/DataSourceConnectionSource.java
new file mode 100644
index 0000000..6590b42
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/db/DriverManagerConnectionSource.java b/src/main/java/org/apache/log4j/receivers/db/DriverManagerConnectionSource.java
new file mode 100644
index 0000000..a1e777a
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/db/JNDIConnectionSource.java b/src/main/java/org/apache/log4j/receivers/db/JNDIConnectionSource.java
new file mode 100644
index 0000000..fe95c42
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/db/JNDIConnectionSource.java
@@ -0,0 +1,142 @@
+/*
+ * 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.receivers.db;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+// PortableRemoteObject was introduced in JDK 1.3. We won't use it.
+// import javax.rmi.PortableRemoteObject;
+
+
+/**
+ *  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/receivers/db/dialect/HSQLDBDialect.java b/src/main/java/org/apache/log4j/receivers/db/dialect/HSQLDBDialect.java
new file mode 100644
index 0000000..c8a8c45
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/db/dialect/MsSQLDialect.java b/src/main/java/org/apache/log4j/receivers/db/dialect/MsSQLDialect.java
new file mode 100644
index 0000000..d4cf4d6
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/db/dialect/MySQLDialect.java b/src/main/java/org/apache/log4j/receivers/db/dialect/MySQLDialect.java
new file mode 100644
index 0000000..4bebd71
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/db/dialect/OracleDialect.java b/src/main/java/org/apache/log4j/receivers/db/dialect/OracleDialect.java
new file mode 100644
index 0000000..9f652e7
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/db/dialect/PostgreSQLDialect.java b/src/main/java/org/apache/log4j/receivers/db/dialect/PostgreSQLDialect.java
new file mode 100644
index 0000000..cd17fa1
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/db/dialect/SQLDialect.java b/src/main/java/org/apache/log4j/receivers/db/dialect/SQLDialect.java
new file mode 100644
index 0000000..109554f
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.db.dialect;
+
+/**
+ * @author ceki
+ *
+ */
+public interface SQLDialect {
+  
+  public String getSelectInsertId();
+  
+}
diff --git a/src/main/java/org/apache/log4j/receivers/db/dialect/SybaseDialect.java b/src/main/java/org/apache/log4j/receivers/db/dialect/SybaseDialect.java
new file mode 100644
index 0000000..cc463db
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/db/dialect/Util.java b/src/main/java/org/apache/log4j/receivers/db/dialect/Util.java
new file mode 100644
index 0000000..7d0ba4c
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.db.dialect;
+
+import org.apache.log4j.component.spi.ComponentBase;
+import org.apache.log4j.receivers.db.ConnectionSource;
+
+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/receivers/db/dialect/db2.sql b/src/main/java/org/apache/log4j/receivers/db/dialect/db2.sql
new file mode 100644
index 0000000..47d2164
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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/receivers/db/dialect/db2l.sql b/src/main/java/org/apache/log4j/receivers/db/dialect/db2l.sql
new file mode 100644
index 0000000..0f91315
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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/receivers/db/dialect/hsqldb.sql b/src/main/java/org/apache/log4j/receivers/db/dialect/hsqldb.sql
new file mode 100644
index 0000000..50f8f78
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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/receivers/db/dialect/mssql.sql b/src/main/java/org/apache/log4j/receivers/db/dialect/mssql.sql
new file mode 100644
index 0000000..d87e0a0
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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/receivers/db/dialect/mysql.sql b/src/main/java/org/apache/log4j/receivers/db/dialect/mysql.sql
new file mode 100644
index 0000000..e3a2be1
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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/receivers/db/dialect/oracle.sql b/src/main/java/org/apache/log4j/receivers/db/dialect/oracle.sql
new file mode 100644
index 0000000..84bf9e5
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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/receivers/db/dialect/postgresql.sql b/src/main/java/org/apache/log4j/receivers/db/dialect/postgresql.sql
new file mode 100644
index 0000000..c38757b
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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/receivers/db/package.html b/src/main/java/org/apache/log4j/receivers/db/package.html
new file mode 100644
index 0000000..55652fb
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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/receivers/helpers/UtilLoggingLevel.java b/src/main/java/org/apache/log4j/receivers/helpers/UtilLoggingLevel.java
new file mode 100644
index 0000000..0dc8f38
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.helpers;
+
+import org.apache.log4j.Level;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 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/receivers/net/AddressBased.java b/src/main/java/org/apache/log4j/receivers/net/AddressBased.java
new file mode 100644
index 0000000..b1d39d8
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/net/JMSReceiver.java b/src/main/java/org/apache/log4j/receivers/net/JMSReceiver.java
new file mode 100644
index 0000000..fada8e7
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/net/JMSReceiver.java
@@ -0,0 +1,291 @@
+/*
+ * 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.receivers.net;
+
+import org.apache.log4j.component.plugins.Plugin;
+import org.apache.log4j.component.plugins.Receiver;
+import org.apache.log4j.spi.LoggingEvent;
+
+import javax.jms.*;
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingException;
+import java.io.FileInputStream;
+import java.util.Properties;
+
+/**
+  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/receivers/net/JMSReceiverBeanInfo.java b/src/main/java/org/apache/log4j/receivers/net/JMSReceiverBeanInfo.java
new file mode 100644
index 0000000..13ccef0
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/net/MulticastAppender.java b/src/main/java/org/apache/log4j/receivers/net/MulticastAppender.java
new file mode 100644
index 0000000..a6f76b9
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/net/MulticastAppender.java
@@ -0,0 +1,346 @@
+/*
+ * 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.receivers.net;
+
+import org.apache.log4j.AppenderSkeleton;
+
+import org.apache.log4j.component.helpers.Constants;
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.net.ZeroConfSupport;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.xml.XMLLayout;
+
+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.Map;
+
+
+/**
+ *  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/receivers/net/MulticastReceiver.java b/src/main/java/org/apache/log4j/receivers/net/MulticastReceiver.java
new file mode 100644
index 0000000..d6b01d7
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/net/MulticastReceiver.java
@@ -0,0 +1,274 @@
+/*
+ * 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.receivers.net;
+
+
+import org.apache.log4j.component.plugins.Pauseable;
+import org.apache.log4j.component.plugins.Receiver;
+import org.apache.log4j.net.ZeroConfSupport;
+import org.apache.log4j.receivers.spi.Decoder;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.io.IOException;
+import java.net.*;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ *  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/receivers/net/MulticastReceiverBeanInfo.java b/src/main/java/org/apache/log4j/receivers/net/MulticastReceiverBeanInfo.java
new file mode 100644
index 0000000..e0ea6a4
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/net/NetworkBased.java b/src/main/java/org/apache/log4j/receivers/net/NetworkBased.java
new file mode 100644
index 0000000..abbde16
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/net/PortBased.java b/src/main/java/org/apache/log4j/receivers/net/PortBased.java
new file mode 100644
index 0000000..8ef8e69
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/net/SocketHubReceiver.java b/src/main/java/org/apache/log4j/receivers/net/SocketHubReceiver.java
new file mode 100644
index 0000000..3256fce
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/net/SocketHubReceiver.java
@@ -0,0 +1,411 @@
+/*
+ * 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.receivers.net;
+
+
+import org.apache.log4j.component.plugins.Plugin;
+import org.apache.log4j.component.plugins.Receiver;
+import org.apache.log4j.net.ZeroConfSupport;
+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/receivers/net/SocketNode13.java b/src/main/java/org/apache/log4j/receivers/net/SocketNode13.java
new file mode 100644
index 0000000..2b3e8cd
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.net;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.component.helpers.Constants;
+import org.apache.log4j.component.plugins.Pauseable;
+import org.apache.log4j.component.plugins.Receiver;
+import org.apache.log4j.component.spi.ComponentBase;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggingEvent;
+
+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;
+
+
+// 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/receivers/net/SocketNodeEventListener.java b/src/main/java/org/apache/log4j/receivers/net/SocketNodeEventListener.java
new file mode 100644
index 0000000..a8c56ac
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/net/SocketReceiver.java b/src/main/java/org/apache/log4j/receivers/net/SocketReceiver.java
new file mode 100644
index 0000000..71d757c
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/net/SocketReceiver.java
@@ -0,0 +1,474 @@
+/*
+ * 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.receivers.net;
+
+
+import org.apache.log4j.component.plugins.Pauseable;
+import org.apache.log4j.component.plugins.Plugin;
+import org.apache.log4j.component.plugins.Receiver;
+import org.apache.log4j.net.ZeroConfSupport;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.*;
+
+
+/**
+  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/receivers/net/UDPAppender.java b/src/main/java/org/apache/log4j/receivers/net/UDPAppender.java
new file mode 100644
index 0000000..a5ae94b
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/net/UDPAppender.java
@@ -0,0 +1,332 @@
+/*
+ * 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.receivers.net;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.component.helpers.Constants;
+import org.apache.log4j.helpers.LogLog;
+
+import org.apache.log4j.net.ZeroConfSupport;
+import org.apache.log4j.spi.LoggingEvent;
+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/receivers/net/UDPReceiver.java b/src/main/java/org/apache/log4j/receivers/net/UDPReceiver.java
new file mode 100644
index 0000000..c762efc
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/net/UDPReceiver.java
@@ -0,0 +1,282 @@
+/*
+ * 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.receivers.net;
+
+import org.apache.log4j.component.plugins.Pauseable;
+import org.apache.log4j.component.plugins.Receiver;
+import org.apache.log4j.net.ZeroConfSupport;
+import org.apache.log4j.receivers.spi.Decoder;
+import org.apache.log4j.spi.LoggingEvent;
+
+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;
+
+
+/**
+ *  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/receivers/net/XMLSocketNode.java b/src/main/java/org/apache/log4j/receivers/net/XMLSocketNode.java
new file mode 100644
index 0000000..3ead0f6
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/net/XMLSocketNode.java
@@ -0,0 +1,208 @@
+/*
+ * 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.receivers.net;
+
+import org.apache.log4j.Logger;
+
+
+import org.apache.log4j.component.helpers.Constants;
+import org.apache.log4j.component.plugins.Receiver;
+import org.apache.log4j.component.spi.ComponentBase;
+import org.apache.log4j.receivers.spi.Decoder;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggingEvent;
+
+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/receivers/net/XMLSocketReceiver.java b/src/main/java/org/apache/log4j/receivers/net/XMLSocketReceiver.java
new file mode 100644
index 0000000..9501754
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/net/XMLSocketReceiver.java
@@ -0,0 +1,317 @@
+/*
+ * 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.receivers.net;
+
+import org.apache.log4j.component.plugins.Pauseable;
+import org.apache.log4j.component.plugins.Plugin;
+import org.apache.log4j.component.plugins.Receiver;
+import org.apache.log4j.net.ZeroConfSupport;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+  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/receivers/rewrite/MapRewritePolicy.java b/src/main/java/org/apache/log4j/receivers/rewrite/MapRewritePolicy.java
new file mode 100644
index 0000000..0f29d73
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.rewrite;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * 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/receivers/rewrite/PropertyRewritePolicy.java b/src/main/java/org/apache/log4j/receivers/rewrite/PropertyRewritePolicy.java
new file mode 100644
index 0000000..69e86a6
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/rewrite/PropertyRewritePolicy.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.receivers.rewrite;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.util.*;
+
+/**
+ * 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/receivers/rewrite/ReflectionRewritePolicy.java b/src/main/java/org/apache/log4j/receivers/rewrite/ReflectionRewritePolicy.java
new file mode 100644
index 0000000..96b3bfe
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.rewrite;
+
+import org.apache.log4j.Logger;
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 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/receivers/rewrite/RewriteAppender.java b/src/main/java/org/apache/log4j/receivers/rewrite/RewriteAppender.java
new file mode 100644
index 0000000..c768c37
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/rewrite/RewritePolicy.java b/src/main/java/org/apache/log4j/receivers/rewrite/RewritePolicy.java
new file mode 100644
index 0000000..a351ed6
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/rewrite/RewritePolicy.java
@@ -0,0 +1,37 @@
+package org.apache.log4j.receivers.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/receivers/spi/Decoder.java b/src/main/java/org/apache/log4j/receivers/spi/Decoder.java
new file mode 100644
index 0000000..7b9c603
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.spi;
+
+
+import org.apache.log4j.spi.LoggingEvent;
+
+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/receivers/varia/ListModelAppender.java b/src/main/java/org/apache/log4j/receivers/varia/ListModelAppender.java
new file mode 100644
index 0000000..702e028
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/varia/ListModelAppender.java
@@ -0,0 +1,78 @@
+/*
+ * 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.receivers.varia;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.spi.LoggingEvent;
+
+import javax.swing.*;
+
+
+/**
+ * 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/receivers/varia/LogFilePatternReceiver.java b/src/main/java/org/apache/log4j/receivers/varia/LogFilePatternReceiver.java
new file mode 100644
index 0000000..c673c4a
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/varia/LogFilePatternReceiver.java
@@ -0,0 +1,1033 @@
+/*
+ * 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.receivers.varia;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.component.helpers.Constants;
+import org.apache.log4j.component.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;
+
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.regex.MatchResult;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * 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/receivers/varia/LogFilePatternReceiverBeanInfo.java b/src/main/java/org/apache/log4j/receivers/varia/LogFilePatternReceiverBeanInfo.java
new file mode 100644
index 0000000..21bbcbf
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/xml/LogFileXMLReceiver.java b/src/main/java/org/apache/log4j/receivers/xml/LogFileXMLReceiver.java
new file mode 100644
index 0000000..495bf2c
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/xml/LogFileXMLReceiver.java
@@ -0,0 +1,306 @@
+/*

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

+

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

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

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

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

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

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

+

+import java.io.*;

+import java.net.MalformedURLException;

+import java.net.URL;

+import java.util.Collection;

+import java.util.Iterator;

+

+/**

+ * 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/receivers/xml/UtilLoggingEntityResolver.java b/src/main/java/org/apache/log4j/receivers/xml/UtilLoggingEntityResolver.java
new file mode 100644
index 0000000..422535e
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/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.receivers.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/receivers/xml/UtilLoggingXMLDecoder.java b/src/main/java/org/apache/log4j/receivers/xml/UtilLoggingXMLDecoder.java
new file mode 100644
index 0000000..1e9b5cb
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/xml/UtilLoggingXMLDecoder.java
@@ -0,0 +1,460 @@
+/*
+ * 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.receivers.xml;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.helpers.UtilLoggingLevel;
+
+import org.apache.log4j.receivers.spi.Decoder;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
+import org.apache.log4j.xml.SAXErrorHandler;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import javax.swing.*;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.awt.*;
+import java.io.*;
+import java.net.URL;
+import java.util.*;
+import java.util.zip.ZipInputStream;
+
+
+/**
+ * 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/receivers/xml/XMLDecoder.java b/src/main/java/org/apache/log4j/receivers/xml/XMLDecoder.java
new file mode 100644
index 0000000..124383d
--- /dev/null
+++ b/src/main/java/org/apache/log4j/receivers/xml/XMLDecoder.java
@@ -0,0 +1,488 @@
+/*
+ * 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.receivers.xml;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.receivers.spi.Decoder;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
+import org.apache.log4j.xml.Log4jEntityResolver;
+import org.apache.log4j.xml.SAXErrorHandler;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import javax.swing.*;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.awt.*;
+import java.io.*;
+import java.net.URL;
+import java.util.*;
+import java.util.zip.ZipInputStream;
+
+
+/**
+ * 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/test/java/org/apache/log4j/component/plugins/MockReceiver.java b/src/test/java/org/apache/log4j/component/plugins/MockReceiver.java
new file mode 100644
index 0000000..06a0462
--- /dev/null
+++ b/src/test/java/org/apache/log4j/component/plugins/MockReceiver.java
@@ -0,0 +1,106 @@
+/*
+ * 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.component.plugins;
+
+/**
+ * Mock receiver used by PluginConfiguratorTest.
+ */
+public final class MockReceiver extends PluginSkeleton {
+    /**
+     * Is active.
+     */
+    private boolean active = false;
+    /**
+     * Host name.
+     */
+    private String host;
+    /**
+     * Port.
+     */
+    private int port = 0;
+
+    /**
+     * Create new instance.
+     */
+    public MockReceiver() {
+        super();
+    }
+
+    /**
+     * Shutdown.
+     */
+    public void shutdown() {
+        active = false;
+    }
+
+    /**
+     * Is plugin active.
+     * @return true if active.
+     */
+    public boolean isActive() {
+        return active;
+    }
+
+    /**
+     * Activate options.
+     */
+    public void activateOptions() {
+        active = true;
+    }
+
+    /**
+      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;
+    }
+
+}
diff --git a/src/test/java/org/apache/log4j/component/plugins/PluginTestCase.java b/src/test/java/org/apache/log4j/component/plugins/PluginTestCase.java
new file mode 100644
index 0000000..e706358
--- /dev/null
+++ b/src/test/java/org/apache/log4j/component/plugins/PluginTestCase.java
@@ -0,0 +1,619 @@
+/*
+ * 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.component.plugins;
+
+import junit.framework.TestCase;
+import org.apache.log4j.*;
+import org.apache.log4j.LoggerRepositoryExImpl;
+import org.apache.log4j.util.Compare;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+
+public class PluginTestCase extends TestCase {
+
+    static String FILE = "plugins.PluginTestCase";
+    static String WITNESS = "witness/plugins.PluginTestCase";
+    private static boolean verbosePluginOutput = true;
+    private static HashMap repositoryMap = new HashMap();
+
+    PluginRegistry pluginRegistry;
+    public PluginTestCase(String name) {
+        super(name);
+    }
+
+    public void setUp() {
+        pluginRegistry = new LoggerRepositoryExImpl(
+                LogManager.getLoggerRepository()).getPluginRegistry();
+
+        // delete the output file if they happen to exist
+        new File(getOutputFile("test1")).delete();
+    }
+
+    private String getOutputFile(String caseName) {
+
+        return FILE + "." + caseName + ".txt";
+    }
+
+    private String getWitnessFile(String caseName) {
+
+        return WITNESS + "." + caseName + ".txt";
+    }
+
+    private void setupAppender(String caseName) throws IOException {
+
+        Logger root = Logger.getRootLogger();
+        root.removeAllAppenders();
+
+        // set up appender
+        FileAppender appender = new FileAppender(new SimpleLayout(),
+                getOutputFile(caseName), false);
+
+        //FileAppender appender = new FileAppender(new PatternLayout("%c{1}: %m%n"),
+        //  getOutputFile(caseName), false);
+        root.addAppender(appender);
+        root.setLevel(Level.DEBUG);
+    }
+
+    // basic test of plugin in standalone mode
+    public void test1() throws Exception {
+
+        String testName = "test1";
+        Logger logger = Logger.getLogger(testName);
+
+        setupAppender(testName);
+
+        PluginTester plugin1 = new PluginTester1("plugin1", 1);
+        PluginTester plugin2 = new PluginTester1("plugin1", 2);
+        PluginTester plugin3 = new PluginTester2("plugin1", 3);
+        PluginTester plugin4 = new PluginTester2("plugin2", 4);
+        PluginTester retPlugin;
+
+        repositoryMap.clear();
+        repositoryMap.put(LogManager.getLoggerRepository(),
+            "default repository");
+
+        // test basic starting/stopping
+        logger.info("test 1.1 - basic starting/stopping");
+        logger.info("starting " + plugin1.getIdentifier());
+        pluginRegistry.addPlugin(plugin1);
+        logger.info("stopping " + plugin1.getIdentifier() +
+            " using plugin object");
+        pluginRegistry.stopPlugin(plugin1.getName());
+
+        // test restarting and starting when already started
+        logger.info("test 1.2 - restarting and starting when already started");
+        logger.info("restarting " + plugin1.getIdentifier());
+        pluginRegistry.addPlugin(plugin1);
+        logger.info("restarting " + plugin1.getIdentifier() + " again");
+        pluginRegistry.addPlugin(plugin1);
+
+        // test stopping and stopping when already stopped
+        logger.info("test 1.3- stopping and stopping when already stopped");
+        logger.info("stopping " + plugin1.getIdentifier());
+        pluginRegistry.stopPlugin(plugin1.getName());
+        logger.info("stopping " + plugin1.getIdentifier() + " again");
+        pluginRegistry.stopPlugin(plugin1.getName());
+
+        logger.info("test 1.4 - restarting then stopping by plugin name");
+        logger.info("starting " + plugin1.getIdentifier());
+        pluginRegistry.addPlugin(plugin1);
+        logger.info("stopping " + plugin1.getIdentifier() +
+            " using plugin name");
+        pluginRegistry.stopPlugin(plugin1.getName());
+
+//        // test starting of an "equal" plugin
+//        logger.info("test 1.5 - starting of an \"equal\" plugin");
+//        logger.info("starting " + plugin1.getIdentifier());
+//        retPlugin = (PluginTester) pluginRegistry.startPlugin(plugin1);
+//        logger.info("returned plugin is " + retPlugin.getIdentifier());
+//        logger.info("starting " + plugin2.getIdentifier());
+//        retPlugin = (PluginTester) pluginRegistry.startPlugin(plugin2);
+//        logger.info("returned plugin is " + retPlugin.getIdentifier());
+//        logger.info("stopping " + retPlugin.getIdentifier());
+//        pluginRegistry.stopPlugin(retPlugin.getName());
+//
+//        // test starting an "equal" plugin after original stopped
+//        logger.info(
+//            "test 1.6 - starting an \"equal\" plugin after original stopped");
+//        logger.info("starting " + plugin2.getIdentifier());
+//        retPlugin = (PluginTester) pluginRegistry.startPlugin(plugin2);
+//        logger.info("returned plugin is " + retPlugin.getIdentifier());
+//        logger.info("stopping " + retPlugin.getIdentifier());
+//        pluginRegistry.stopPlugin(retPlugin.getName());
+//
+//        // test starting of an "unequal" plugin with same name
+//        logger.info(
+//            "test 1.7 - starting of an \"unequal\" plugin with same name");
+//        logger.info("starting " + plugin1.getIdentifier());
+//        retPlugin = (PluginTester) pluginRegistry.startPlugin(plugin1);
+//        logger.info("returned plugin is " + retPlugin.getIdentifier());
+//        logger.info("starting " + plugin3.getIdentifier());
+//        retPlugin = (PluginTester) pluginRegistry.startPlugin(plugin3);
+//        logger.info("returned plugin is " + retPlugin.getIdentifier());
+//        logger.info("stopping " + retPlugin.getIdentifier());
+//        pluginRegistry.stopPlugin(retPlugin.getName());
+//
+//        // test starting of multiple plugins and stopAll
+//        logger.info("test 1.8 - starting of multiple plugins and stopAll");
+//        logger.info("starting " + plugin1.getIdentifier());
+//        retPlugin = (PluginTester) pluginRegistry.startPlugin(plugin1);
+//        logger.info("returned plugin is " + retPlugin.getIdentifier());
+//        logger.info("starting " + plugin4.getIdentifier());
+//        retPlugin = (PluginTester) pluginRegistry.startPlugin(plugin4);
+//        logger.info("returned plugin is " + retPlugin.getIdentifier());
+//        verbosePluginOutput = false;
+//        logger.info("stopping all plugins");
+//        pluginRegistry.stopAllPlugins();
+//        verbosePluginOutput = true;
+//        logger.info(plugin1.getIdentifier() + " is " +
+//            (plugin1.isActive() ? "active" : "inactive"));
+//        logger.info(plugin4.getIdentifier() + " is " +
+//            (plugin4.isActive() ? "active" : "inactive"));
+//        logger.info("stopping all plugins again");
+//        pluginRegistry.stopAllPlugins();
+//
+//        // test starting of multiple plugins and stopAll
+//        logger.info(
+//            "test 1.9 - starting of multiple plugins, stopping, and stopAll");
+//        logger.info("starting " + plugin1.getIdentifier());
+//        retPlugin = (PluginTester) pluginRegistry.startPlugin(plugin1);
+//        logger.info("returned plugin is " + retPlugin.getIdentifier());
+//        logger.info("starting " + plugin4.getIdentifier());
+//        retPlugin = (PluginTester) pluginRegistry.startPlugin(plugin4);
+//        logger.info("returned plugin is " + retPlugin.getIdentifier());
+        logger.info("stopping " + plugin1.getIdentifier() +
+            " using plugin object");
+        pluginRegistry.stopPlugin(plugin1.getName());
+        verbosePluginOutput = false;
+        logger.info("stopping all plugins");
+        pluginRegistry.stopAllPlugins();
+        verbosePluginOutput = true;
+        logger.info(plugin1.getIdentifier() + " is " +
+            (plugin1.isActive() ? "active" : "inactive"));
+        logger.info(plugin4.getIdentifier() + " is " +
+            (plugin4.isActive() ? "active" : "inactive"));
+        logger.info("stopping all plugins again");
+        //
+        //  Warning about removing non-registered LoggerRepositoryEventListener
+        //     goes to console on log4j 1.2 instead of log file with log4j 1.3.
+        //
+        pluginRegistry.stopAllPlugins();
+
+        assertTrue(Compare.compare(PluginTestCase.class,
+                getOutputFile(testName),
+                getWitnessFile(testName)));
+    }
+
+    // basic test of plugin with repositories
+    public void test2() throws Exception {
+//
+//        String testName = "test2";
+//        Logger logger = Logger.getLogger(testName);
+//
+//        setupAppender(testName);
+//
+//        PluginTester plugin1 = new PluginTester1("plugin1", 1);
+//        PluginTester plugin2 = new PluginTester1("plugin2", 2);
+//        PluginTester retPlugin;
+//        LoggerRepository repo1 = new Hierarchy(new RootLogger(Level.DEBUG));
+//        LoggerRepository repo2 = new Hierarchy(new RootLogger(Level.DEBUG));
+//        
+//        PluginRegistry pr1 = repo1.getPluginRegistry();
+//        PluginRegistry pr2 = repo2.getPluginRegistry();
+//        
+//        repositoryMap.clear();
+//        repositoryMap.put(repo1, "repository1");
+//        repositoryMap.put(repo2, "repository2");
+//
+//        logger.info("test 2.1 - starting plugins in multiple repositories");
+//        logger.info("starting " + plugin1.getIdentifier() + " in " +
+//            repositoryMap.get(repo1));
+//        retPlugin = (PluginTester) pr1.startPlugin(plugin1);
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//        logger.info("starting " + plugin2.getIdentifier() + " in " +
+//            repositoryMap.get(repo2));
+//        retPlugin = (PluginTester) pr2.startPlugin(plugin2);
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//
+//        logger.info("test 2.2 - stopping plugins in multiple repositories");
+//        logger.info("stopping " + plugin1.getIdentifier() + " in " +
+//            repositoryMap.get(plugin1.getLoggerRepository()));
+//        retPlugin = (PluginTester) pr1.stopPlugin(plugin1.getName());
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//        logger.info("stopping " + plugin2.getIdentifier() + " in " +
+//            repositoryMap.get(plugin2.getLoggerRepository()));
+//        retPlugin = (PluginTester) pr2.stopPlugin(plugin2.getName());
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//
+//        logger.info("test 2.3 - restarting plugins in different repositories");
+//        logger.info("starting " + plugin1.getIdentifier() + " in " +
+//            repositoryMap.get(repo2));
+//        retPlugin = (PluginTester) pr2.startPlugin(plugin1);
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//        logger.info("starting " + plugin2.getIdentifier() + " in " +
+//            repositoryMap.get(repo1));
+//        retPlugin = (PluginTester) pr1.startPlugin(plugin2);
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//
+//        logger.info("test 2.4 - stopping plugins using stopAll");
+//        logger.info("stopping all plugins in " + repositoryMap.get(repo1));
+//        pr1.stopAllPlugins();
+//        logger.info("stopping all plugins in " + repositoryMap.get(repo2));
+//        pr2.stopAllPlugins();
+//
+//        logger.info(
+//            "test 2.5 - starting a plugin already active in another repository");
+//        logger.info("starting " + plugin1.getIdentifier() + " in " +
+//            repositoryMap.get(repo1));
+//        retPlugin = (PluginTester) pr1.startPlugin(plugin1);
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//        logger.info("starting " + plugin2.getIdentifier() + " in " +
+//            repositoryMap.get(repo2));
+//        retPlugin = (PluginTester) pr2.startPlugin(plugin2);
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//        logger.info("restarting " + plugin1.getIdentifier() + " in " +
+//            repositoryMap.get(repo2));
+//        retPlugin = (PluginTester) pr2.startPlugin(plugin1);
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//        logger.info("restarting " + plugin2.getIdentifier() + " in " +
+//            repositoryMap.get(repo1));
+//        retPlugin = (PluginTester) pr1.startPlugin(plugin2);
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//
+//        logger.info("test 2.6 - handle repository reset");
+//        logger.info("resetting " + repositoryMap.get(repo1));
+//        repo1.resetConfiguration();
+//        logger.info("resetting " + repositoryMap.get(repo2));
+//        repo2.resetConfiguration();
+//
+//        logger.info("test 2.7 - handle repository shutdown");
+//        logger.info("starting " + plugin1.getIdentifier() + " in " +
+//            repositoryMap.get(repo1));
+//        retPlugin = (PluginTester) pr1.startPlugin(plugin1);
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//        logger.info("starting " + plugin2.getIdentifier() + " in " +
+//            repositoryMap.get(repo2));
+//        retPlugin = (PluginTester) pr2.startPlugin(plugin2);
+//        logger.info(
+//            "returned plugin is " + retPlugin.getIdentifier() + " in " +
+//            repositoryMap.get(retPlugin.getLoggerRepository()));
+//        logger.info("shutting down " + repositoryMap.get(repo1));
+//        repo1.shutdown();
+//        logger.info("shutting down " + repositoryMap.get(repo2));
+//        repo2.shutdown();
+//
+//        assertTrue(Compare.compare(getOutputFile(testName),
+//                getWitnessFile(testName)));
+    }
+
+    public void testPluginListeners() {
+
+        Plugin p = new PluginTester1("MyNewPlugin", 1);
+        PluginListenerLatch l = new PluginListenerLatch();
+        pluginRegistry.stopAllPlugins();
+        pluginRegistry.addPluginListener(l);
+        pluginRegistry.addPlugin(p);
+
+        PluginEvent e = l.LastEvent;
+
+        assertTrue("PluginListener should have been notified of start",
+            l.StartLatch);
+        assertTrue("PluginListener stop latch should not be activated",
+            !l.StopLatch);
+        assertTrue("PluginListener should be given reference to Plugin",
+            e.getPlugin() == p);
+
+        l.reset();
+        pluginRegistry.stopAllPlugins();
+        assertTrue("PluginListener should have been notified of stop",
+            l.StopLatch);
+        assertTrue("PluginListener should not have been notified of start",
+            !l.StartLatch);
+        assertTrue("PluginListener should be given reference to Plugin",
+            l.LastEvent.getPlugin() == p);
+        assertTrue(
+            "PluginListener should have received a distinct event object",
+            l.LastEvent != e);
+    }
+
+    public void testPropertyChangeListeners() {
+
+        Plugin plugin = new PluginTester1("PluginTest1", 1);
+
+        final PropertyChangeListenerLatch l = new PropertyChangeListenerLatch();
+        plugin.addPropertyChangeListener(l);
+
+        /**
+         * Test the basic properties and ensure they get latched by notification
+         */
+        plugin.setName("NewName");
+        assertTrue("PropertyChange latch should have been detected",
+            l.isLatched());
+        assertTrue("Old value unexpected: '" + l.getLastEvent().getOldValue() +
+            "'", l.getLastEvent().getOldValue().equals("PluginTest1"));
+        assertTrue("New value unexpected: '" + l.getLastEvent().getNewValue() +
+            "'", l.getLastEvent().getNewValue().equals("NewName"));
+
+        l.reset();
+
+        plugin.removePropertyChangeListener(l);
+        plugin.setName("SecondNewName");
+
+        assertTrue("Should not have been notified/latched", !l.isLatched());
+
+        l.reset();
+
+        /**
+         * Test when only listening for specific property
+         */
+        plugin.addPropertyChangeListener("name", l);
+
+        plugin.setName("NewName2");
+        assertTrue("PropertyChange latch should have been detected",
+            l.isLatched());
+        assertTrue("Old value unexpected: '" + l.getLastEvent().getOldValue() +
+            "'", l.getLastEvent().getOldValue().equals("SecondNewName"));
+        assertTrue("New value unexpected: '" + l.getLastEvent().getNewValue() +
+            "'", l.getLastEvent().getNewValue().equals("NewName2"));
+
+        plugin.removePropertyChangeListener("name", l);
+
+        l.reset();
+
+        /**
+         * setup some assertions before continuing testing to make sure the test code isn't broken
+         */
+        assertTrue("Plugin should not be active just yet", !plugin.isActive());
+        assertTrue("Latch should not be latched", !l.isLatched());
+
+        plugin.addPropertyChangeListener("active", l);
+
+        pluginRegistry.addPlugin(plugin);
+/*
+        assertTrue(
+            "Should have been notified of activation when pluginRegistry.start(plugin)",
+            l.isLatched());
+        assertTrue("Active old value should have been false",
+            l.getLastEvent().getOldValue().equals(Boolean.FALSE));
+        assertTrue("Active New value should have been true",
+            l.getLastEvent().getNewValue().equals(Boolean.TRUE));
+
+        pluginRegistry.stopAllPlugins();
+        l.reset();
+        assertTrue("Latch should have been reset", !l.isLatched());
+*/
+
+        /**
+         * start afresh
+         */
+/*
+        plugin = new PluginTester1("LoggerRepositoryProperty", 2);
+
+        LoggerRepository oldValue = plugin.getLoggerRepository();
+        plugin.addPropertyChangeListener("loggerRepository", l);
+
+        LoggerRepository rep = new Hierarchy(new RootLogger(Level.DEBUG));
+        plugin.setLoggerRepository(rep);
+
+        assertTrue("Should be notified of LoggerRepository property change",
+            l.isLatched());
+        assertTrue("LoggerRepository Old value mismatch",
+            l.getLastEvent().getOldValue() == oldValue);
+        assertTrue("LoggerRepository New vale mismatch",
+            l.getLastEvent().getNewValue() == rep);
+*/
+    }
+
+    private static class PluginListenerLatch implements PluginListener {
+
+        private boolean StartLatch;
+        private boolean StopLatch;
+        private PluginEvent LastEvent;
+
+        /* (non-Javadoc)
+         * @see org.apache.log4j.plugins.PluginListener#pluginStarted(org.apache.log4j.plugins.PluginEvent)
+         */
+        public void pluginStarted(PluginEvent e) {
+            StartLatch = true;
+            LastEvent = e;
+        }
+
+        /* (non-Javadoc)
+         * @see org.apache.log4j.plugins.PluginListener#pluginStopped(org.apache.log4j.plugins.PluginEvent)
+         */
+        public void pluginStopped(PluginEvent e) {
+            StopLatch = true;
+            LastEvent = e;
+        }
+
+        void reset() {
+            StartLatch = false;
+            StopLatch = false;
+            LastEvent = null;
+        }
+    }
+
+    private static class PropertyChangeListenerLatch
+        implements PropertyChangeListener {
+
+        boolean latch = false;
+        PropertyChangeEvent lastEvent = null;
+
+        /* (non-Javadoc)
+         * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
+         */
+        public void propertyChange(PropertyChangeEvent evt) {
+            latch = true;
+            lastEvent = evt;
+        }
+
+        boolean isLatched() {
+
+            return latch;
+        }
+
+        void reset() {
+            latch = false;
+            lastEvent = null;
+        }
+
+        PropertyChangeEvent getLastEvent() {
+
+            return lastEvent;
+        }
+    }
+
+    /**
+      Class to test the Plugin and PluginRegistry functionality. */
+    private static class PluginTester extends PluginSkeleton {
+
+        protected Logger logger;
+        private boolean active = false;
+        public int id;
+
+        public synchronized boolean isActive() {
+            logger.debug(this.getIdentifier() + " is " +
+                (active ? "active" : "inactive"));
+
+            return active;
+        }
+
+        private synchronized boolean setActive(boolean _active) {
+
+            boolean oldValue = active;
+
+            if (active != _active) {
+                active = _active;
+                firePropertyChange("active", oldValue, active);
+
+                return true;
+            } else {
+
+                return false;
+            }
+        }
+
+        public String getIdentifier() {
+
+            if (verbosePluginOutput) {
+
+                return this.getName() + "-id" + id;
+            } else {
+
+                return "plugin in " +
+                repositoryMap.get(this.getLoggerRepository());
+            }
+        }
+
+        public void activateOptions() {
+
+            if (setActive(true)) {
+                logger.debug(this.getIdentifier() + " activated");
+            } else {
+                logger.debug(this.getIdentifier() + " already activated");
+            }
+        }
+
+        public void shutdown() {
+
+            if (setActive(false)) {
+                logger.debug(this.getIdentifier() + " shutdown");
+            } else {
+                logger.debug(this.getIdentifier() + " already shutdown");
+            }
+        }
+
+        /* (non-Javadoc)
+         * @see org.apache.log4j.plugins.Plugin#isEquivalent(org.apache.log4j.plugins.Plugin)
+         */
+        public boolean isEquivalent(Plugin testPlugin) {
+
+            boolean equiv = super.isEquivalent(testPlugin);
+
+            if (equiv) {
+                logger.debug("plugin equal");
+            } else if (!(testPlugin.getClass() == this.getClass())) {
+                logger.debug(
+                    "plugin not equal, different class: " +
+                    this.getClass().getName() + " != " +
+                    testPlugin.getClass().getName());
+
+            } else if (!this.getName().equals(testPlugin.getName())) {
+                logger.debug(
+                    "plugin not equal, different name: " + this.getName() +
+                    " != " + testPlugin.getName());
+
+            } else if (!this.getLoggerRepository().equals(
+                        testPlugin.getLoggerRepository())) {
+                logger.debug(
+                    "plugin not equal, different repository: " +
+                    repositoryMap.get(this.getLoggerRepository()) + " != " +
+                    repositoryMap.get(testPlugin.getLoggerRepository()));
+            }
+
+            return equiv;
+        }
+    }
+
+    /**
+      Class to test the Plugin and PluginRegistry functionality. */
+    private static class PluginTester1 extends PluginTester {
+        public PluginTester1(String _name, int _id) {
+            logger = Logger.getLogger(this.getClass());
+            setName(_name);
+            id = _id;
+        }
+    }
+
+    /**
+      Class to test the Plugin and PluginRegistry functionality. */
+    private static class PluginTester2 extends PluginTester {
+        public PluginTester2(String _name, int _id) {
+            logger = Logger.getLogger(this.getClass());
+            setName(_name);
+            id = _id;
+        }
+    }
+}
diff --git a/src/test/java/org/apache/log4j/component/util/Compare.java b/src/test/java/org/apache/log4j/component/util/Compare.java
new file mode 100644
index 0000000..279c546
--- /dev/null
+++ b/src/test/java/org/apache/log4j/component/util/Compare.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.component.util;
+
+import java.io.*;
+
+
+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();
+  }
+}
diff --git a/src/test/java/org/apache/log4j/receivers/VectorAppender.java b/src/test/java/org/apache/log4j/receivers/VectorAppender.java
new file mode 100644
index 0000000..54c461f
--- /dev/null
+++ b/src/test/java/org/apache/log4j/receivers/VectorAppender.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.receivers;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.util.Vector;
+
+/**
+   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/receivers/db/FullCycleDBTest.java b/src/test/java/org/apache/log4j/receivers/db/FullCycleDBTest.java
new file mode 100644
index 0000000..030758a
--- /dev/null
+++ b/src/test/java/org/apache/log4j/receivers/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.receivers.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.LoggerRepositoryExImpl;
+import org.apache.log4j.MDC;
+import org.apache.log4j.component.helpers.Constants;
+import org.apache.log4j.receivers.VectorAppender;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.RootLogger;
+import org.apache.log4j.xml.DOMConfigurator;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Vector;
+
+
+/**
+ * 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/receivers/helpers/UtilLoggingLevelTest.java b/src/test/java/org/apache/log4j/receivers/helpers/UtilLoggingLevelTest.java
new file mode 100644
index 0000000..9e7705c
--- /dev/null
+++ b/src/test/java/org/apache/log4j/receivers/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.receivers.helpers;
+
+import junit.framework.TestCase;
+
+
+/**
+ * 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/receivers/rewrite/RewriteAppenderTest.java b/src/test/java/org/apache/log4j/receivers/rewrite/RewriteAppenderTest.java
new file mode 100644
index 0000000..6167021
--- /dev/null
+++ b/src/test/java/org/apache/log4j/receivers/rewrite/RewriteAppenderTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.receivers.rewrite;
+
+import junit.framework.TestCase;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.MDC;
+import org.apache.log4j.util.Compare;
+import org.apache.log4j.xml.DOMConfigurator;
+import org.w3c.dom.Document;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.InputStream;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.TreeMap;
+
+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/receivers/util/Compare.java b/src/test/java/org/apache/log4j/receivers/util/Compare.java
new file mode 100644
index 0000000..98aa0c7
--- /dev/null
+++ b/src/test/java/org/apache/log4j/receivers/util/Compare.java
@@ -0,0 +1,195 @@
+/*
+ * 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.receivers.util;
+
+import java.io.*;
+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/receivers/xml/XMLDecoderTest.java b/src/test/java/org/apache/log4j/receivers/xml/XMLDecoderTest.java
new file mode 100644
index 0000000..2fef84b
--- /dev/null
+++ b/src/test/java/org/apache/log4j/receivers/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.receivers.xml;
+
+import junit.framework.TestCase;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.CharBuffer;
+import java.util.Vector;
+
+/**
+ * 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/component/plugins/plugins.PluginTestCase.test1.txt b/src/test/resources/org/apache/log4j/component/plugins/plugins.PluginTestCase.test1.txt
new file mode 100644
index 0000000..8b13af0
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/component/plugins/plugins.PluginTestCase.test1.txt
@@ -0,0 +1,23 @@
+INFO - test 1.1 - basic starting/stopping
+INFO - starting plugin1-id1
+INFO - stopping plugin1-id1 using plugin object
+DEBUG - plugin1-id1 already shutdown
+INFO - test 1.2 - restarting and starting when already started
+INFO - restarting plugin1-id1
+INFO - restarting plugin1-id1 again
+DEBUG - plugin1-id1 already shutdown
+INFO - test 1.3- stopping and stopping when already stopped
+INFO - stopping plugin1-id1
+DEBUG - plugin1-id1 already shutdown
+INFO - stopping plugin1-id1 again
+INFO - test 1.4 - restarting then stopping by plugin name
+INFO - starting plugin1-id1
+INFO - stopping plugin1-id1 using plugin name
+DEBUG - plugin1-id1 already shutdown
+INFO - stopping plugin1-id1 using plugin object
+INFO - stopping all plugins
+DEBUG - plugin1-id1 is inactive
+INFO - plugin1-id1 is inactive
+DEBUG - plugin2-id4 is inactive
+INFO - plugin2-id4 is inactive
+INFO - stopping all plugins again
diff --git a/src/test/resources/org/apache/log4j/receivers/db/append-with-drivermanager1.xml b/src/test/resources/org/apache/log4j/receivers/db/append-with-drivermanager1.xml
new file mode 100644
index 0000000..5d701b5
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/receivers/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.receivers.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.receivers.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/receivers/db/read-with-drivermanager1.xml b/src/test/resources/org/apache/log4j/receivers/db/read-with-drivermanager1.xml
new file mode 100644
index 0000000..3e77c49
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/receivers/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/receivers/rewrite/map.log b/src/test/resources/org/apache/log4j/receivers/rewrite/map.log
new file mode 100644
index 0000000..3ce933f
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/receivers/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/receivers/rewrite/map.xml b/src/test/resources/org/apache/log4j/receivers/rewrite/map.xml
new file mode 100644
index 0000000..7cb60b7
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/receivers/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/receivers/rewrite/property.log b/src/test/resources/org/apache/log4j/receivers/rewrite/property.log
new file mode 100644
index 0000000..9aa2c49
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/receivers/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/receivers/rewrite/property.xml b/src/test/resources/org/apache/log4j/receivers/rewrite/property.xml
new file mode 100644
index 0000000..13a04f8
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/receivers/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/receivers/rewrite/reflection.log b/src/test/resources/org/apache/log4j/receivers/rewrite/reflection.log
new file mode 100644
index 0000000..da0b52f
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/receivers/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/receivers/rewrite/reflection.xml b/src/test/resources/org/apache/log4j/receivers/rewrite/reflection.xml
new file mode 100644
index 0000000..643850b
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/receivers/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/receivers/xml/xmlLayout.1.xml b/src/test/resources/org/apache/log4j/receivers/xml/xmlLayout.1.xml
new file mode 100644
index 0000000..06dc106
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/receivers/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/receivers/xml/xsltLayout.1.xml b/src/test/resources/org/apache/log4j/receivers/xml/xsltLayout.1.xml
new file mode 100644
index 0000000..596e84d
--- /dev/null
+++ b/src/test/resources/org/apache/log4j/receivers/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>