Final tidy up of addition of other receivers from log4j 1.3.

Reviewed by: Curt and Scott



git-svn-id: https://svn.apache.org/repos/asf/logging/sandbox/log4j/receivers@530249 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index 5da003c..bf9fd1c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -116,7 +116,12 @@
       <groupId>oro</groupId>
       <artifactId>oro</artifactId>
       <version>2.0.8</version>
-    </dependency>    
+    </dependency>  
+    <dependency>
+      <groupId>javax.jms</groupId>
+      <artifactId>jms</artifactId>
+      <version>1.1</version>
+    </dependency>  
   </dependencies>
   <reporting>
     <plugins>
diff --git a/src/main/java/org/apache/log4j/db/ConnectionSource.java b/src/main/java/org/apache/log4j/db/ConnectionSource.java
new file mode 100644
index 0000000..0578734
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/ConnectionSource.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db;
+
+import org.apache.log4j.spi.Component;
+import org.apache.log4j.spi.OptionHandler;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+
+/**
+ *  The <id>ConnectionSource</id> interface provides a pluggable means of
+ *  transparently obtaining JDBC {@link java.sql.Connection}s for log4j classes
+ *  that require the use of a {@link java.sql.Connection}.
+ *
+ *  @author <a href="mailto:rdecampo@twcny.rr.com">Ray DeCampo</a>
+ */
+public interface ConnectionSource extends Component, OptionHandler {
+
+  final int UNKNOWN_DIALECT = 0;
+  final int POSTGRES_DIALECT = 1;
+  final int MYSQL_DIALECT = 2;
+  final int ORACLE_DIALECT = 3;
+  final int MSSQL_DIALECT = 4;
+  final int HSQL_DIALECT = 5;  
+  /**
+   *  Obtain a {@link java.sql.Connection} for use.  The client is
+   *  responsible for closing the {@link java.sql.Connection} when it is no
+   *  longer required.
+   *
+   *  @throws SQLException  if a {@link java.sql.Connection} could not be
+   *                        obtained
+   */
+  Connection getConnection() throws SQLException;
+
+  /**
+   * Get the SQL dialect that should be used for this connection. Note that the
+   * dialect is not needed if the JDBC driver supports the getGeneratedKeys 
+   * method.
+   */
+  int getSQLDialectCode();
+  
+  /**
+   * If the connection supports the JDBC 3.0 getGeneratedKeys method, then
+   * we do not need any specific dialect support.
+   */
+  boolean supportsGetGeneratedKeys();
+  
+  /**
+   * If the connection does not support batch updates, we will avoid using them.
+   */  
+  public boolean supportsBatchUpdates();
+}
diff --git a/src/main/java/org/apache/log4j/db/ConnectionSourceSkeleton.java b/src/main/java/org/apache/log4j/db/ConnectionSourceSkeleton.java
new file mode 100644
index 0000000..fd4b014
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/ConnectionSourceSkeleton.java
@@ -0,0 +1,110 @@
+/*
+ * 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.db.dialect.Util;
+import org.apache.log4j.spi.ComponentBase;
+
+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 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() {
+    try {
+      Connection connection = getConnection();
+      if (connection == null) {
+        getLogger().warn("Could not get a conneciton");
+        return;
+      }
+      DatabaseMetaData meta = connection.getMetaData();
+      Util util = new Util();
+      util.setLoggerRepository(repository);
+      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);
+    }
+  }
+
+  /**
+   * 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;
+  }
+
+  /**
+   * Does this connection support batch updates?
+   */
+  public final boolean supportsBatchUpdates() {
+    return supportsBatchUpdates;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/CustomSQLDBReceiver.java b/src/main/java/org/apache/log4j/db/CustomSQLDBReceiver.java
new file mode 100644
index 0000000..9e0a9b8
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/CustomSQLDBReceiver.java
@@ -0,0 +1,447 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Hashtable;
+import java.util.StringTokenizer;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.scheduler.Job;
+import org.apache.log4j.scheduler.Scheduler;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.LoggerRepositoryEx;
+import org.apache.log4j.spi.ThrowableInformation;
+import org.apache.log4j.spi.LocationInfo;
+
+/**
+ * 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, EXCEPTION
+ * <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:
+ * <p>
+ * select logger as LOGGER, timestamp as TIMESTAMP, level as LEVEL, thread as
+ * THREAD, message as MESSAGE, ndc as NDC, mdc as MDC, class as CLASS, method as
+ * METHOD, file as FILE, line as LINE,
+ * concat("{{application,databaselogs,hostname,mymachine, log4jid,",
+ * COUNTER,"}}") as PROPERTIES, "" as EXCEPTION from logtable
+ * <p>
+ * @author Scott Deboy <sdeboy@apache.org>
+ * <p>
+ */
+public class CustomSQLDBReceiver extends Receiver implements Pauseable {
+
+    protected volatile Connection connection = null;
+
+    protected String sqlStatement = "";
+
+    /**
+     * By default we refresh data every 1000 milliseconds.
+     * 
+     * @see #setRefreshMillis
+     */
+    static int DEFAULT_REFRESH_MILLIS = 1000;
+
+    int refreshMillis = DEFAULT_REFRESH_MILLIS;
+
+    protected String idField = null;
+
+    int lastID = -1;
+
+    private static final String WHERE_CLAUSE = " WHERE ";
+
+    private static final String AND_CLAUSE = " AND ";
+
+    private boolean whereExists = false;
+
+    private boolean paused = false;
+
+    private ConnectionSource connectionSource;
+
+    public static final String LOG4J_ID_KEY = "log4jid";
+
+    private Job customReceiverJob;
+
+    public void activateOptions() {
+      
+      if(connectionSource == null)  {
+        throw new IllegalStateException(
+          "CustomSQLDBReceiver cannot function without a connection source");
+      }
+      whereExists = (sqlStatement.toUpperCase().indexOf(WHERE_CLAUSE) > -1);
+    
+      customReceiverJob = new CustomReceiverJob();
+        
+      if(this.repository == null) {
+        throw new IllegalStateException(
+        "CustomSQLDBReceiver cannot function without a reference to its owning repository");
+      }
+     
+    
+
+      if (repository instanceof LoggerRepositoryEx) {
+        Scheduler scheduler = ((LoggerRepositoryEx) repository).getScheduler();
+      
+        scheduler.schedule(
+          customReceiverJob, System.currentTimeMillis() + 500, refreshMillis);
+      }
+
+    }
+
+    void closeConnection(Connection connection) {
+        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() {
+            Connection connection = null;
+
+            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[] exception = 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());
+                        }
+                    }
+
+                    exception = new String[] { rs.getString("EXCEPTION") };
+                    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 name = tok2.nextToken();
+                            String value = tok2.nextToken();
+                            if (name.equals(LOG4J_ID_KEY)) {
+                                try {
+                                    int thisInt = Integer.parseInt(value);
+                                    value = String.valueOf(thisInt);
+                                    if (thisInt > lastID) {
+                                        lastID = thisInt;
+                                    }
+                                } catch (Exception e) {
+                                }
+                            }
+                            properties.put(name, value);
+                        }
+                    }
+
+                    Level levelImpl = Level.toLevel(level);
+
+
+					LocationInfo locationInfo = new LocationInfo(fileName,
+	                            className, methodName, lineNumber);
+	
+					ThrowableInformation throwableInfo =  new ThrowableInformation(
+		                            exception);
+	
+					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(connection);
+            }
+
+            // if paused, loop prior to executing sql query
+            synchronized (this) {
+                while (isPaused()) {
+                    try {
+                        wait(1000);
+                    } catch (InterruptedException ie) {
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/log4j/db/DBAppender.java b/src/main/java/org/apache/log4j/db/DBAppender.java
new file mode 100644
index 0000000..e8bbf5d
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DBAppender.java
@@ -0,0 +1,406 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.db.dialect.SQLDialect;
+import org.apache.log4j.db.dialect.Util;
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.LocationInfo;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import java.util.Iterator;
+import java.util.Set;
+import java.lang.reflect.*;
+
+
+/**
+ * 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.joran.JoranConfigurator JoranConfigurator}. Example
+ * scripts can be found in the <em>tests/input/db</em> directory.
+ *
+ * @author Ceki G&uuml;lc&uuml;
+ * @author Ray DeCampo
+ * @since 1.3
+ */
+public class DBAppender extends AppenderSkeleton {
+  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() {
+    getLogger().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) {
+    getLogger().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 =
+              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) {
+              getLogger().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) {
+                  getLogger().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) {
+              getLogger().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) {
+          getLogger().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;
+  }
+
+ /** Here Be Dragons. 
+   * This code is necessary because this class originally came from the log4j 1.3 area, before being backported to 1.3
+   * In 1.3, Appenders had their own logger, but 1.2 does not have this, so instead we embed a 'compatible' mechanism here
+   * The goal was to keep the code as similar as possible, to avoid as many conflicts as possble.
+   * Code suggested by Curt Arnold (and not Curty Arnold... ;) )
+  */
+
+   private static final class LogLogger {
+        public void debug(final String msg) {
+             LogLog.debug(msg);
+        }
+       public void warn(final String msg, final Throwable ex) {
+             LogLog.warn(msg, ex);
+       }
+       public void warn(final String msg) {
+             LogLog.warn(msg);
+       }
+      public void error(final String msg, final Throwable ex) {
+            LogLog.error(msg, ex);
+     }
+  }
+
+  private static final LogLogger getLogger() {
+     return new LogLogger();
+  }
+    
+}
diff --git a/src/main/java/org/apache/log4j/db/DBHelper.java b/src/main/java/org/apache/log4j/db/DBHelper.java
new file mode 100644
index 0000000..b400506
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DBHelper.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Set;
+
+import org.apache.log4j.spi.LoggingEvent;
+
+/**
+ * @author Ceki G&uuml;lc&uuml;
+ *
+ */
+public class DBHelper {
+  
+  public final static short PROPERTIES_EXIST = 0x01;
+  public final static short EXCEPTION_EXISTS = 0x02;
+  
+  public  static short computeReferenceMask(LoggingEvent event) {
+    short mask = 0;
+    Set propertiesKeys = event.getPropertyKeySet();
+    if(propertiesKeys.size() > 0) {
+      mask = PROPERTIES_EXIST;
+    }
+    String[] strRep = event.getThrowableStrRep();
+    if(strRep != null) {
+      mask |= EXCEPTION_EXISTS;
+    }
+    return mask;
+  }
+  
+  static public void closeConnection(Connection connection) {
+    if(connection != null) {
+      try { 
+        connection.close();
+      } catch(SQLException sqle) {
+        // static utility classes should not log without an explicit repository
+        // reference
+      }
+    }
+  }
+  
+  public static void closeStatement(Statement statement) {
+    if(statement != null) {
+      try {
+        statement.close();
+      } catch(SQLException sqle) {
+      }
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/DBReceiver.java b/src/main/java/org/apache/log4j/db/DBReceiver.java
new file mode 100644
index 0000000..69effde
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DBReceiver.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db;
+
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.scheduler.Scheduler;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggerRepositoryEx;
+
+/**
+ *
+ * @author Scott Deboy <sdeboy@apache.org>
+ * @author Ceki G&uuml;lc&uuml;
+ *
+ */
+public class DBReceiver extends Receiver implements Pauseable {
+  /**
+   * 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;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/DBReceiverJob.java b/src/main/java/org/apache/log4j/db/DBReceiverJob.java
new file mode 100644
index 0000000..d21f1dc
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DBReceiverJob.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.helpers.Constants;
+import org.apache.log4j.scheduler.Job;
+import org.apache.log4j.spi.ComponentBase;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
+import org.apache.log4j.spi.LocationInfo;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+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 = 0;
+
+  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(Constants.LOG4J_ID_KEY, 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
+   * @param event
+   * @throws SQLException
+   */
+  ThrowableInformation getException(Connection connection, long id)
+      throws SQLException {
+
+    PreparedStatement statement = null;
+
+    try {
+      statement = connection.prepareStatement(sqlException);
+      statement.setLong(1, id);
+      ResultSet rs = statement.executeQuery();
+
+      Vector v = new Vector();
+
+      while (rs.next()) {
+        //int i = rs.getShort(1);
+        v.add(rs.getString(1));
+      }
+
+      int len = v.size();
+      String[] strRep = new String[len];
+      for (int i = 0; i < len; i++) {
+        strRep[i] = (String) v.get(i);
+      }
+      // we've filled strRep, we now attach it to the event
+      return new ThrowableInformation(strRep);
+    } finally {
+      if (statement != null) {
+        statement.close();
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/log4j/db/DataSourceConnectionSource.java b/src/main/java/org/apache/log4j/db/DataSourceConnectionSource.java
new file mode 100644
index 0000000..1994bd0
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DataSourceConnectionSource.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.db;
+
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+
+/**
+ *  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 {
+
+  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;
+  }
+
+
+}
diff --git a/src/main/java/org/apache/log4j/db/DriverManagerConnectionSource.java b/src/main/java/org/apache/log4j/db/DriverManagerConnectionSource.java
new file mode 100644
index 0000000..9bfdf65
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/DriverManagerConnectionSource.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+
+
+/**
+ *  The DriverManagerConnectionSource is an implementation of {@link ConnectionSource}
+ *  that obtains the Connection in the traditional JDBC manner based on the
+ *  connection URL.
+ *  <p>
+ *  Note that this class will establish a new Connection for each call to
+ *  {@link #getConnection()}.  It is recommended that you either use a JDBC
+ *  driver that natively supported Connection pooling or that you create
+ *  your own implementation of {@link ConnectionSource} that taps into whatever
+ *  pooling mechanism you are already using.  (If you have access to a JNDI
+ *  implementation that supports {@link javax.sql.DataSource}s, e.g. within
+ *  a J2EE application server, see {@link JNDIConnectionSource}).  See
+ *  <a href="#dbcp">below</a> for a configuration example that uses the
+ *  <a href="http://jakarta.apache.org/commons/dbcp/index.html">commons-dbcp</a>
+ *  package from Apache.
+ *  <p>
+ *  Sample configuration:<br>
+ *  <pre>
+ *     &lt;connectionSource class="org.apache.log4j.jdbc.DriverManagerConnectionSource"&gt;
+ *        &lt;param name="driver" value="com.mysql.jdbc.Driver" /&gt;
+ *        &lt;param name="url" value="jdbc:mysql://localhost:3306/mydb" /&gt;
+ *        &lt;param name="username" value="myUser" /&gt;
+ *        &lt;param name="password" value="myPassword" /&gt;
+ *     &lt;/connectionSource&gt;
+ *  </pre>
+ *  <p>
+ *  <a name="dbcp">If</a> you do not have another connection pooling mechanism
+ *  built into your application, you can use  the
+ *  <a href="http://jakarta.apache.org/commons/dbcp/index.html">commons-dbcp</a>
+ *  package from Apache:<br>
+ *  <pre>
+ *     &lt;connectionSource class="org.apache.log4j.jdbc.DriverManagerConnectionSource"&gt;
+ *        &lt;param name="driver" value="org.apache.commons.dbcp.PoolingDriver" /&gt;
+ *        &lt;param name="url" value="jdbc:apache:commons:dbcp:/myPoolingDriver" /&gt;
+ *     &lt;/connectionSource&gt;
+ *  </pre>
+ *  Then the configuration information for the commons-dbcp package goes into
+ *  the file myPoolingDriver.jocl and is placed in the classpath.  See the
+ *  <a href="http://jakarta.apache.org/commons/dbcp/index.html">commons-dbcp</a>
+ *  documentation for details.
+ *
+ *  @author <a href="mailto:rdecampo@twcny.rr.com">Ray DeCampo</a>
+ */
+public class DriverManagerConnectionSource extends ConnectionSourceSkeleton {
+  private String driverClass = null;
+  private String url = null;
+
+  public void activateOptions() {
+    try {
+      if (driverClass != null) {
+        Class.forName(driverClass);
+        discoverConnnectionProperties();
+      } else {
+        getLogger().error(
+          "WARNING: No JDBC driver specified for log4j DriverManagerConnectionSource.");
+      }
+    } catch (final ClassNotFoundException cnfe) {
+     getLogger().error("Could not load JDBC driver class: " + driverClass, cnfe);
+    }
+  }
+
+
+  /**
+   * @see org.apache.log4j.db.ConnectionSource#getConnection()
+   */
+  public Connection getConnection() throws SQLException {
+    if (getUser() == null) {
+      return DriverManager.getConnection(url);
+    } else {
+      return DriverManager.getConnection(url, getUser(), getPassword());
+    }
+  }
+
+
+  /**
+   * Returns the url.
+   * @return String
+   */
+  public String getUrl() {
+    return url;
+  }
+
+
+  /**
+   * Sets the url.
+   * @param url The url to set
+   */
+  public void setUrl(String url) {
+    this.url = url;
+  }
+
+
+  /**
+   * Returns the name of the driver class.
+   * @return String
+   */
+  public String getDriverClass() {
+    return driverClass;
+  }
+
+
+  /**
+   * Sets the driver class.
+   * @param driverClass The driver class to set
+   */
+  public void setDriverClass(String driverClass) {
+    this.driverClass = driverClass;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/JNDIConnectionSource.java b/src/main/java/org/apache/log4j/db/JNDIConnectionSource.java
new file mode 100644
index 0000000..7073738
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/JNDIConnectionSource.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.log4j.db;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+
+// PortableRemoteObject was introduced in JDK 1.3. We won't use it.
+// import javax.rmi.PortableRemoteObject;
+import javax.sql.DataSource;
+
+
+/**
+ *  The <id>JNDIConnectionSource</id> is an implementation of
+ *  {@link ConnectionSource} that obtains a {@link javax.sql.DataSource} from a
+ *  JNDI provider and uses it to obtain a {@link java.sql.Connection}.  It is
+ *  primarily designed to be used inside of J2EE application servers or
+ *  application server clients, assuming the application server supports remote
+ *  access of {@link javax.sql.DataSource}s.  In this way one can take
+ *  advantage of  connection pooling and whatever other goodies the application
+ *  server provides.
+ *  <p>
+ *  Sample configuration:<br>
+ *  <pre>
+ *    &lt;connectionSource class="org.apache.log4j.jdbc.JNDIConnectionSource"&gt;
+ *        &lt;param name="jndiLocation" value="jdbc/MySQLDS" /&gt;
+ *    &lt;/connectionSource&gt;
+ *  </pre>
+ *  <p>
+ *  Sample configuration (with username and password):<br>
+ *  <pre>
+ *    &lt;connectionSource class="org.apache.log4j.jdbc.JNDIConnectionSource"&gt;
+ *        &lt;param name="jndiLocation" value="jdbc/MySQLDS" /&gt;
+ *        &lt;param name="username" value="myUser" /&gt;
+ *        &lt;param name="password" value="myPassword" /&gt;
+ *    &lt;/connectionSource&gt;
+ *  </pre>
+ *  <p>
+ *  Note that this class will obtain an {@link javax.naming.InitialContext}
+ *  using the no-argument constructor.  This will usually work when executing
+ *  within a J2EE environment.  When outside the J2EE environment, make sure
+ *  that you provide a jndi.properties file as described by your JNDI
+ *  provider's documentation.
+ *
+ *  @author <a href="mailto:rdecampo@twcny.rr.com">Ray DeCampo</a>
+ */
+public class JNDIConnectionSource
+       extends ConnectionSourceSkeleton {
+  private String jndiLocation = null;
+  private DataSource dataSource = null;
+
+  /**
+   * @see org.apache.log4j.spi.OptionHandler#activateOptions()
+   */
+  public void activateOptions() {
+    if (jndiLocation == null) {
+      getLogger().error("No JNDI location specified for JNDIConnectionSource.");
+    }
+    
+    discoverConnnectionProperties();
+
+  }
+  
+  /**
+   * @see org.apache.log4j.db.ConnectionSource#getConnection()
+   */
+  public Connection getConnection()
+         throws SQLException {
+    Connection conn = null;
+    try {
+
+      if(dataSource == null) {
+        dataSource = lookupDataSource();
+      }
+      if (getUser() == null) {
+        conn = dataSource.getConnection();
+      } else {
+        conn = dataSource.getConnection(getUser(), getPassword());
+      }
+    } catch (final NamingException ne) {
+         getLogger().error("Error while getting data source", ne);
+      throw new SQLException("NamingException while looking up DataSource: " + ne.getMessage());
+    } catch (final ClassCastException cce) {
+      getLogger().error("ClassCastException while looking up DataSource.", cce);
+      throw new SQLException("ClassCastException while looking up DataSource: " + cce.getMessage());
+    }
+
+    return conn;
+  }
+
+  /**
+   * Returns the jndiLocation.
+   * @return String
+   */
+  public String getJndiLocation() {
+    return jndiLocation;
+  }
+
+
+  /**
+   * Sets the jndiLocation.
+   * @param jndiLocation The jndiLocation to set
+   */
+  public void setJndiLocation(String jndiLocation) {
+    this.jndiLocation = jndiLocation;
+  }
+
+
+  private DataSource lookupDataSource()
+         throws NamingException, SQLException {
+    DataSource ds;
+    Context ctx = new InitialContext();
+    Object obj = ctx.lookup(jndiLocation);
+
+    // PortableRemoteObject was introduced in JDK 1.3. We won't use it.
+    //ds = (DataSource)PortableRemoteObject.narrow(obj, DataSource.class);
+    ds = (DataSource) obj;
+
+    if (ds == null) {
+      throw new SQLException("Failed to obtain data source from JNDI location " + jndiLocation);
+    } else {
+      return ds;
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/HSQLDBDialect.java b/src/main/java/org/apache/log4j/db/dialect/HSQLDBDialect.java
new file mode 100644
index 0000000..b558cd3
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/HSQLDBDialect.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db.dialect; 
+
+/** 
+ * The HSQLDB dialect. 
+ * 
+ * @author <a href="http://www.qos.ch/log4j/">Ceki G&uuml;lc&uuml;</a>
+*/ 
+public class HSQLDBDialect implements SQLDialect { 
+ public static final String SELECT_CURRVAL = "CALL IDENTITY()"; 
+
+ public String getSelectInsertId() { 
+   return SELECT_CURRVAL; 
+ } 
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/MsSQLDialect.java b/src/main/java/org/apache/log4j/db/dialect/MsSQLDialect.java
new file mode 100644
index 0000000..9b94e72
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/MsSQLDialect.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db.dialect; 
+
+/** 
+* The MS SQL Server dialect is untested. 
+* 
+* Note that the dialect is not needed if your JDBC driver supports 
+* the getGeneratedKeys method introduced in JDBC 3.0 specification.
+* 
+* @author James Stauffer 
+*/ 
+public class MsSQLDialect implements SQLDialect { 
+ public static final String SELECT_CURRVAL = "SELECT @@identity id"; 
+
+ public String getSelectInsertId() { 
+   return SELECT_CURRVAL; 
+ } 
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/MySQLDialect.java b/src/main/java/org/apache/log4j/db/dialect/MySQLDialect.java
new file mode 100644
index 0000000..f61db19
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/MySQLDialect.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db.dialect;
+
+/**
+ * 
+ * 
+ * @author Ceki
+ *
+ */
+public class MySQLDialect implements SQLDialect {
+  public static final String SELECT_LAST_INSERT_ID = "SELECT LAST_INSERT_ID()";
+  
+  public String getSelectInsertId() {
+    return SELECT_LAST_INSERT_ID;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/OracleDialect.java b/src/main/java/org/apache/log4j/db/dialect/OracleDialect.java
new file mode 100644
index 0000000..c040413
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/OracleDialect.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db.dialect;
+
+/**
+ * The Oracle dialect. Tested successfully on Oracle9i Release 9.2.0.3.0 by 
+ * James Stauffer.
+ * 
+ * @author Ceki G&uuml;lc&uuml;
+ */
+public class OracleDialect implements SQLDialect {
+  public static final String SELECT_CURRVAL = "SELECT logging_event_id_seq.currval from dual";
+
+  public String getSelectInsertId() {
+    return SELECT_CURRVAL;
+  }
+
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/PostgreSQLDialect.java b/src/main/java/org/apache/log4j/db/dialect/PostgreSQLDialect.java
new file mode 100644
index 0000000..0eff123
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/PostgreSQLDialect.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db.dialect;
+
+
+/**
+ * 
+ * @author ceki
+ *
+ * To change the template for this generated type comment go to
+ * Window>Preferences>Java>Code Generation>Code and Comments
+ */
+public class PostgreSQLDialect
+       implements SQLDialect {
+  public static final String SELECT_CURRVAL = "SELECT currval('logging_event_id_seq')";
+
+  public String getSelectInsertId() {
+    return SELECT_CURRVAL;
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/SQLDialect.java b/src/main/java/org/apache/log4j/db/dialect/SQLDialect.java
new file mode 100644
index 0000000..b24e5aa
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/SQLDialect.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.log4j.db.dialect;
+
+/**
+ * @author ceki
+ *
+ */
+public interface SQLDialect {
+  
+  public String getSelectInsertId();
+  
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/Util.java b/src/main/java/org/apache/log4j/db/dialect/Util.java
new file mode 100644
index 0000000..05f5ba3
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/Util.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.db.dialect;
+
+import org.apache.log4j.db.ConnectionSource;
+import org.apache.log4j.spi.ComponentBase;
+
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+
+
+/**
+ * 
+ * @author Ceki Gulcu
+ *
+ */
+public class Util extends ComponentBase {
+  private static final String POSTGRES_PART = "postgresql";
+  private static final String MYSQL_PART = "mysql";
+  private static final String ORACLE_PART = "oracle";
+  //private static final String MSSQL_PART = "mssqlserver4";
+  private static final String MSSQL_PART = "microsoft";
+  private static final String HSQL_PART = "hsql";
+  
+  public static int discoverSQLDialect(DatabaseMetaData meta) {
+    int dialectCode = 0;
+
+    try {
+
+      String dbName = meta.getDatabaseProductName().toLowerCase();
+
+      if (dbName.indexOf(POSTGRES_PART) != -1) {
+        return ConnectionSource.POSTGRES_DIALECT;
+      } else if (dbName.indexOf(MYSQL_PART) != -1) {
+        return ConnectionSource.MYSQL_DIALECT;
+      } else if (dbName.indexOf(ORACLE_PART) != -1) {
+        return ConnectionSource.ORACLE_DIALECT;
+      } else if (dbName.indexOf(MSSQL_PART) != -1) {
+        return ConnectionSource.MSSQL_DIALECT;
+      } else if (dbName.indexOf(HSQL_PART) != -1) {
+        return ConnectionSource.HSQL_DIALECT;
+      } else {
+        return ConnectionSource.UNKNOWN_DIALECT;
+      }
+    } catch (SQLException sqle) {
+      // we can't do much here
+    }
+
+    return dialectCode;
+  }
+
+  public static SQLDialect getDialectFromCode(int dialectCode) {
+    SQLDialect sqlDialect = null;
+
+    switch (dialectCode) {
+    case ConnectionSource.POSTGRES_DIALECT:
+      sqlDialect = new PostgreSQLDialect();
+
+      break;
+    case ConnectionSource.MYSQL_DIALECT:
+      sqlDialect = new MySQLDialect();
+
+      break;
+    case ConnectionSource.ORACLE_DIALECT:
+      sqlDialect = new OracleDialect();
+
+      break;
+    case ConnectionSource.MSSQL_DIALECT:
+      sqlDialect = new MsSQLDialect();
+
+      break;
+    case ConnectionSource.HSQL_DIALECT:
+      sqlDialect = new HSQLDBDialect();
+
+      break;
+    }
+    return sqlDialect;
+  }
+  
+  /**
+   * This method handles cases where the 
+   * {@link DatabaseMetaData#supportsGetGeneratedKeys} method is missing in the
+   * JDBC driver implementation.
+   */
+  public boolean supportsGetGeneratedKeys(DatabaseMetaData meta) {
+    try {
+      //
+      //   invoking JDK 1.4 method by reflection
+      //
+      return ((Boolean) DatabaseMetaData.class.getMethod("supportsGetGeneratedKeys", null).invoke(meta, null)).booleanValue();
+    } catch(Throwable e) {
+      getLogger().info("Could not call supportsGetGeneratedKeys method. This may be recoverable");
+      return false;
+    }
+  }
+  
+/** 
+  * This method handles cases where the 
+  * {@link DatabaseMetaData#supportsBatchUpdates} method is missing in the
+  * JDBC driver implementation.
+  */
+  public boolean supportsBatchUpdates(DatabaseMetaData meta) {
+    try {
+      return meta.supportsBatchUpdates();
+    } catch(Throwable e) {
+      getLogger().info("Missing DatabaseMetaData.supportsBatchUpdates method.");
+      return false;
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/db/dialect/db2.sql b/src/main/java/org/apache/log4j/db/dialect/db2.sql
new file mode 100644
index 0000000..1ff655d
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/db2.sql
@@ -0,0 +1,49 @@
+# This SQL script creates the required tables by org.apache.log4j.db.DBAppender and 
+# org.apache.log4j.db.DBReceiver.
+#
+# It is intended for IBM DB2 databases.
+#
+# WARNING  WARNING WARNING  WARNING 
+# =================================
+# This SQL script has not been tested on an actual DB2
+# instance. It may contain errors or even invalid SQL
+# statements.
+
+DROP TABLE  logging_event_property;
+DROP TABLE  logging_event_exception;
+DROP TABLE  logging_event;
+
+CREATE TABLE logging_event 
+  (
+    sequence_number   BIGINT NOT NULL,
+    timestamp         BIGINT NOT NULL,
+    rendered_message  VARCHAR(4000) NOT NULL,
+    logger_name       VARCHAR(254) NOT NULL,
+    level_string      VARCHAR(254) NOT NULL,
+    ndc               VARCHAR(4000),
+    thread_name       VARCHAR(254),
+    reference_flag    SMALLINT,
+    caller_filename   VARCHAR(254) NOT NULL,
+    caller_class      VARCHAR(254) NOT NULL,
+    caller_method     VARCHAR(254) NOT NULL,
+    caller_line       CHAR(4) NOT NULL,
+    event_id          INTEGER GENERATED ALWAYS AS IDENTITY (START WITH 1)
+  );
+
+CREATE TABLE logging_event_property
+  (
+    event_id	      INTEGER NOT NULL,
+    mapped_key        VARCHAR(254) NOT NULL,
+    mapped_value      VARCHAR(1024),
+    PRIMARY KEY(event_id, mapped_key),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+
+CREATE TABLE logging_event_exception
+  (
+    event_id         INTEGER NOT NULL,
+    i                SMALLINT NOT NULL,
+    trace_line       VARCHAR(254) NOT NULL,
+    PRIMARY KEY(event_id, i),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
diff --git a/src/main/java/org/apache/log4j/db/dialect/db2l.sql b/src/main/java/org/apache/log4j/db/dialect/db2l.sql
new file mode 100644
index 0000000..c3dd3fe
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/db2l.sql
@@ -0,0 +1,47 @@
+# This SQL script creates the required tables by org.apache.log4j.db.DBAppender and 
+# org.apache.log4j.db.DBReceiver.
+#
+# It is intended for PostgreSQL databases.
+
+DROP TABLE    logging_event_property;
+DROP TABLE    logging_event_exception;
+DROP TABLE    logging_event;
+
+
+CREATE SEQUENCE logging_event_id_seq MINVALUE 1 START 1;
+
+
+CREATE TABLE logging_event 
+  (
+    sequence_number   BIGINT NOT NULL,
+    timestamp         BIGINT NOT NULL,
+    rendered_message  TEXT NOT NULL,
+    logger_name       VARCHAR(254) NOT NULL,
+    level_string      VARCHAR(254) NOT NULL,
+    ndc               TEXT,
+    thread_name       VARCHAR(254),
+    reference_flag    SMALLINT,
+    caller_filename   VARCHAR(254) NOT NULL,
+    caller_class      VARCHAR(254) NOT NULL,
+    caller_method     VARCHAR(254) NOT NULL,
+    caller_line       CHAR(4) NOT NULL,
+    event_id          INT IDENTITY GENERATED ALWAYS PRIMARY KEY
+  );
+
+CREATE TABLE logging_event_property
+  (
+    event_id	      INT NOT NULL,
+    mapped_key        VARCHAR(254) NOT NULL,
+    mapped_value      VARCHAR(1024),
+    PRIMARY KEY(event_id, mapped_key),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+
+CREATE TABLE logging_event_exception
+  (
+    event_id         INT NOT NULL,
+    i                SMALLINT NOT NULL,
+    trace_line       VARCHAR(254) NOT NULL,
+    PRIMARY KEY(event_id, i),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
diff --git a/src/main/java/org/apache/log4j/db/dialect/hsqldb.sql b/src/main/java/org/apache/log4j/db/dialect/hsqldb.sql
new file mode 100644
index 0000000..cc841f5
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/hsqldb.sql
@@ -0,0 +1,46 @@
+// This SQL script creates the required tables by
+// org.apache.log4j.db.DBAppender and org.apache.log4j.db.DBReceiver.
+//
+// It is intended for HSQLDB. 
+//
+
+DROP TABLE logging_event_exception IF EXISTS;
+DROP TABLE logging_event_property IF EXISTS;
+DROP TABLE logging_event IF EXISTS;
+
+
+CREATE TABLE logging_event 
+  (
+    sequence_number   BIGINT NOT NULL,
+    timestamp         BIGINT NOT NULL,
+    rendered_message  LONGVARCHAR NOT NULL,
+    logger_name       VARCHAR NOT NULL,
+    level_string      VARCHAR NOT NULL,
+    ndc               LONGVARCHAR,
+    thread_name       VARCHAR,
+    reference_flag    SMALLINT,
+    caller_filename   VARCHAR, 
+    caller_class      VARCHAR, 
+    caller_method     VARCHAR, 
+    caller_line       CHAR(4), 
+    event_id          INT NOT NULL IDENTITY
+  );
+
+
+CREATE TABLE logging_event_property
+  (
+    event_id	      INT NOT NULL,
+    mapped_key        VARCHAR(254) NOT NULL,
+    mapped_value      LONGVARCHAR,
+    PRIMARY KEY(event_id, mapped_key),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+
+CREATE TABLE logging_event_exception
+  (
+    event_id         INT NOT NULL,
+    i                SMALLINT NOT NULL,
+    trace_line       VARCHAR NOT NULL,
+    PRIMARY KEY(event_id, i),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
diff --git a/src/main/java/org/apache/log4j/db/dialect/mssql.sql b/src/main/java/org/apache/log4j/db/dialect/mssql.sql
new file mode 100644
index 0000000..d041b34
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/mssql.sql
@@ -0,0 +1,45 @@
+-- This SQL script creates the required tables by org.apache.log4j.db.DBAppender and 
+-- org.apache.log4j.db.DBReceiver. 
+-- 
+-- It is intended for MS SQL Server databases.  This has been tested with version 7.0. 
+
+DROP TABLE logging_event_property 
+DROP TABLE logging_event_exception 
+DROP TABLE logging_event 
+
+CREATE TABLE logging_event 
+  ( 
+    sequence_number   DECIMAL(20) NOT NULL, 
+    timestamp         DECIMAL(20) NOT NULL, 
+    rendered_message  VARCHAR(4000) NOT NULL, 
+    logger_name       VARCHAR(254) NOT NULL, 
+    level_string      VARCHAR(254) NOT NULL, 
+    ndc               VARCHAR(4000), 
+    thread_name       VARCHAR(254), 
+    reference_flag    SMALLINT, 
+    caller_filename   VARCHAR(254) NOT NULL,
+    caller_class      VARCHAR(254) NOT NULL,
+    caller_method     VARCHAR(254) NOT NULL,
+    caller_line       CHAR(4) NOT NULL,
+    event_id          INT NOT NULL identity, 
+    PRIMARY KEY(event_id) 
+  ) 
+
+CREATE TABLE logging_event_property 
+  ( 
+    event_id          INT NOT NULL, 
+    mapped_key        VARCHAR(254) NOT NULL, 
+    mapped_value      VARCHAR(1024), 
+    PRIMARY KEY(event_id, mapped_key), 
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id) 
+  ) 
+
+CREATE TABLE logging_event_exception 
+  ( 
+    event_id         INT NOT NULL, 
+    i                SMALLINT NOT NULL, 
+    trace_line       VARCHAR(254) NOT NULL, 
+    PRIMARY KEY(event_id, i), 
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id) 
+  ) 
+
diff --git a/src/main/java/org/apache/log4j/db/dialect/mysql.sql b/src/main/java/org/apache/log4j/db/dialect/mysql.sql
new file mode 100644
index 0000000..34910e6
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/mysql.sql
@@ -0,0 +1,54 @@
+# 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;
\ No newline at end of file
diff --git a/src/main/java/org/apache/log4j/db/dialect/oracle.sql b/src/main/java/org/apache/log4j/db/dialect/oracle.sql
new file mode 100644
index 0000000..81fe9c5
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/oracle.sql
@@ -0,0 +1,61 @@
+-- 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)
+  );
+  
\ No newline at end of file
diff --git a/src/main/java/org/apache/log4j/db/dialect/postgresql.sql b/src/main/java/org/apache/log4j/db/dialect/postgresql.sql
new file mode 100644
index 0000000..464a6ac
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/dialect/postgresql.sql
@@ -0,0 +1,48 @@
+# This SQL script creates the required tables by org.apache.log4j.db.DBAppender and 
+# org.apache.log4j.db.DBReceiver.
+#
+# It is intended for PostgreSQL databases.
+
+DROP TABLE    logging_event_property;
+DROP TABLE    logging_event_exception;
+DROP SEQUENCE logging_event_id_seq;
+DROP TABLE    logging_event;
+
+
+CREATE SEQUENCE logging_event_id_seq MINVALUE 1 START 1;
+
+
+CREATE TABLE logging_event 
+  (
+    sequence_number   BIGINT NOT NULL,
+    timestamp         BIGINT NOT NULL,
+    rendered_message  TEXT NOT NULL,
+    logger_name       VARCHAR(254) NOT NULL,
+    level_string      VARCHAR(254) NOT NULL,
+    ndc               TEXT,
+    thread_name       VARCHAR(254),
+    reference_flag    SMALLINT,
+    caller_filename   VARCHAR(254) NOT NULL,
+    caller_class      VARCHAR(254) NOT NULL,
+    caller_method     VARCHAR(254) NOT NULL,
+    caller_line       CHAR(4) NOT NULL,
+    event_id          INT DEFAULT nextval('logging_event_id_seq') PRIMARY KEY
+  );
+
+CREATE TABLE logging_event_property
+  (
+    event_id	      INT NOT NULL,
+    mapped_key        VARCHAR(254) NOT NULL,
+    mapped_value      VARCHAR(1024),
+    PRIMARY KEY(event_id, mapped_key),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
+
+CREATE TABLE logging_event_exception
+  (
+    event_id         INT NOT NULL,
+    i                SMALLINT NOT NULL,
+    trace_line       VARCHAR(254) NOT NULL,
+    PRIMARY KEY(event_id, i),
+    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
+  );
diff --git a/src/main/java/org/apache/log4j/db/package.html b/src/main/java/org/apache/log4j/db/package.html
new file mode 100644
index 0000000..805ed88
--- /dev/null
+++ b/src/main/java/org/apache/log4j/db/package.html
@@ -0,0 +1,20 @@
+
+<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/net/JMSReceiver.java b/src/main/java/org/apache/log4j/net/JMSReceiver.java
new file mode 100644
index 0000000..b26541a
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/JMSReceiver.java
@@ -0,0 +1,302 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.net;
+
+import java.io.FileInputStream;
+import java.util.Properties;
+
+import javax.jms.Message;
+import javax.jms.MessageListener;
+import javax.jms.TopicConnection;
+import javax.jms.Topic;
+import javax.jms.TopicConnectionFactory;
+import javax.jms.TopicSubscriber;
+import javax.jms.Session;
+import javax.jms.TopicSession;
+import javax.jms.ObjectMessage;
+
+import javax.naming.InitialContext;
+import javax.naming.Context;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingException;
+
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.plugins.Plugin;
+import org.apache.log4j.plugins.Receiver;
+
+/**
+  JMSReceiver receives a remote logging event on a configured
+  JSM topic and "posts" it to a LoggerRepository as if the event was 
+  generated locally. This class is designed to receive events from 
+  the JMSAppender class (or classes that send compatible events).
+  
+  <p>Once the event has been "posted", it will be handled by the 
+  appenders currently configured in the LoggerRespository.
+  
+  <p>This implementation borrows heavily from the JMSSink
+  implementation.
+  
+  @author Mark Womack
+  @author Paul Smith
+  @author Stephen Pain
+  @since 1.3
+*/
+public class JMSReceiver extends Receiver implements MessageListener {
+
+  private boolean active = false;
+
+  protected String topicFactoryName;
+  protected String topicName;
+  protected String userId;
+  protected String password;
+  protected TopicConnection topicConnection;
+  protected String jndiPath;
+  
+  private String remoteInfo;
+  private String providerUrl;
+
+  public JMSReceiver() { }
+
+  public JMSReceiver(String _topicFactoryName, String _topicName,
+          String _userId, String _password, String _jndiPath) {      
+      topicFactoryName = _topicFactoryName;
+      topicName = _topicName;
+      userId = _userId;
+      password = _password;
+      jndiPath = _jndiPath;
+  }
+
+  /**
+         * Sets the path to a properties file containing
+         * the initial context and jndi provider url
+         */
+    public void setJndiPath(String _jndiPath) {
+          jndiPath = _jndiPath;
+    }
+  
+     /**
+         * Gets the path to a properties file containing
+         * the initial context and jndi provider url
+         */
+     public String getJndiPath() {
+          return jndiPath;
+     }
+  
+  /**
+    Sets the JMS topic factory name to use when creating the 
+    JMS connection. */
+  public void setTopicFactoryName(String _topicFactoryName) {
+    topicFactoryName = _topicFactoryName;
+  }
+  
+  /**
+    Gets the curernt JMS topic factory name property. */
+  public String getTopicFactoryName() {
+    return topicFactoryName;
+  }
+  
+  /**
+   * Sets the JMS topic name to use when creating the
+   * JMS connection.
+   */
+  public void setTopicName(String _topicName) {
+    topicName = _topicName;
+  }
+  
+  /**
+   * Gets the curernt JMS topic name property.
+   */
+  public String getTopicName() {
+    return topicName;
+  }
+
+  /**
+    Sets the user id to use when creating the 
+    JMS connection. */
+  public void setUserId(String _userId) {
+    userId = _userId;
+  }
+  
+  /**
+   * Gets the current user id property.
+   */
+  public String getUserId() {
+    return userId;
+  }
+
+  /**
+   * Sets the password to use when creating the
+   * JMS connection.
+   */
+  public void setPassword(String _password) {
+    password = _password;
+  }
+  
+  /**
+   * Gets the curernt password property.
+   */
+  public String getPassword() {
+    return password;
+  }
+ 
+  /**
+   * Returns true if the receiver is the same class and they are
+   * configured for the same properties, and super class also considers
+   * them to be equivalent. This is used by PluginRegistry when determining
+   * if the a similarly configured receiver is being started.
+   * 
+   * @param testPlugin The plugin to test equivalency against.
+   * @return boolean True if the testPlugin is equivalent to this plugin.
+   */
+  public boolean isEquivalent(Plugin testPlugin) {
+    // only do full check if an instance of this class
+    if (testPlugin instanceof JMSReceiver) {
+ 
+      JMSReceiver receiver = (JMSReceiver)testPlugin;
+      
+      // check for same topic name and super class equivalency
+      return (
+            topicFactoryName.equals(receiver.getTopicFactoryName()) && 
+            (jndiPath == null || jndiPath.equals(receiver.getJndiPath())) && 
+            super.isEquivalent(testPlugin)
+            );
+    }
+    
+    return false;
+  }
+  
+  /**
+    Returns true if this receiver is active. */
+  public synchronized boolean isActive() {
+    return active;
+  }
+  
+  /**
+    Sets the flag to indicate if receiver is active or not. */
+  protected synchronized void setActive(boolean _active) {
+    active = _active;
+  }
+  
+  /**
+    Starts the JMSReceiver with the current options. */
+  public void activateOptions() {
+    if (!isActive()) {
+      try {
+        remoteInfo = topicFactoryName + ":" + topicName;
+
+        Context ctx = null;
+        if (jndiPath == null || jndiPath.equals("")) {
+                ctx = new InitialContext();
+        } else {
+                FileInputStream is = new FileInputStream(jndiPath);
+                Properties p = new Properties();
+                p.load(is);
+                is.close();
+                ctx = new InitialContext(p);
+        }
+
+        // give some more flexibility about the choice of a tab name
+        providerUrl = (String)ctx.getEnvironment().get(Context.PROVIDER_URL);
+        TopicConnectionFactory topicConnectionFactory;
+        topicConnectionFactory = 
+          (TopicConnectionFactory) lookup(ctx, topicFactoryName);
+        
+        if (userId != null && password != null) {
+          topicConnection =
+    	       topicConnectionFactory.createTopicConnection(userId, password);
+        } else {
+          topicConnection =
+    	       topicConnectionFactory.createTopicConnection();
+        }
+  	       
+        TopicSession topicSession =
+          topicConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
+  
+        Topic topic = (Topic)ctx.lookup(topicName);
+  
+        TopicSubscriber topicSubscriber = topicSession.createSubscriber(topic);
+      
+        topicSubscriber.setMessageListener(this);
+ 
+        topicConnection.start();
+ 
+        setActive(true);
+      } catch(Exception e) {
+        setActive(false);
+        if (topicConnection != null) {
+          try {
+            topicConnection.close();
+          } catch (Exception e2) {
+            // do nothing
+          }
+          topicConnection = null;
+        }
+        getLogger().error("Could not start JMSReceiver.", e);
+      }
+    }
+  }
+  
+  /**
+    Called when the receiver should be stopped. */
+  public synchronized void shutdown() {
+    if (isActive()) {
+      // mark this as no longer running
+      setActive(false);
+    
+      if (topicConnection != null) {
+        try {
+          topicConnection.close();
+        } catch (Exception e) {
+          // do nothing
+        }
+        topicConnection = null;
+      }
+    }
+  }
+
+  public void onMessage(Message message) {
+    try {
+      if(message instanceof  ObjectMessage) {
+        // get the logging event and post it to the repository
+      	ObjectMessage objectMessage = (ObjectMessage) message;
+      	LoggingEvent event = (LoggingEvent) objectMessage.getObject();
+      	
+      	// store the known remote info in an event property
+      	event.setProperty("log4j.remoteSourceInfo", remoteInfo);
+        event.setProperty("log4j.jmsProviderUrl", providerUrl);
+        
+      	doPost(event);
+      } else {
+      	getLogger().warn("Received message is of type "+message.getJMSType()
+		    +", was expecting ObjectMessage.");
+      }      
+    } catch(Exception e) {
+      getLogger().error("Exception thrown while processing incoming message.", e);
+    }
+  }
+
+  protected Object lookup(Context ctx, String name) throws NamingException {
+    try {
+      return ctx.lookup(name);
+    } catch(NameNotFoundException e) {
+      getLogger().error("Could not find name ["+name+"].");
+      throw e;
+    }
+  }
+
+}
diff --git a/src/main/java/org/apache/log4j/net/JMSReceiverBeanInfo.java b/src/main/java/org/apache/log4j/net/JMSReceiverBeanInfo.java
new file mode 100644
index 0000000..eec19d3
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/JMSReceiverBeanInfo.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.log4j.net;
+
+import java.beans.PropertyDescriptor;
+import java.beans.SimpleBeanInfo;
+
+
+/**
+ * BeanInfo class for the JMSReceiver.
+ *
+ * @author Paul Smith <psmith@apache.org>
+ *
+ */
+public class JMSReceiverBeanInfo extends SimpleBeanInfo {
+
+    /* (non-Javadoc)
+     * @see java.beans.BeanInfo#getPropertyDescriptors()
+     */
+    public PropertyDescriptor[] getPropertyDescriptors() {
+
+        try {
+
+            return new PropertyDescriptor[] {
+                new PropertyDescriptor("name", JMSReceiver.class),
+                new PropertyDescriptor("topicFactoryName", JMSReceiver.class),
+                new PropertyDescriptor("topicName", JMSReceiver.class),
+                new PropertyDescriptor("threshold", JMSReceiver.class),
+                new PropertyDescriptor("jndiPath", JMSReceiver.class),
+                new PropertyDescriptor("userId",
+                        JMSReceiver.class),
+            };
+        } catch (Exception e) {
+        }
+
+        return null;
+    }
+}
diff --git a/src/main/java/org/apache/log4j/net/MulticastReceiver.java b/src/main/java/org/apache/log4j/net/MulticastReceiver.java
new file mode 100644
index 0000000..4788b03
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/MulticastReceiver.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.net;
+
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.Decoder;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.io.IOException;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ *  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 boolean isActive = false;
+  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;
+
+  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() {
+    isActive = false;
+    handlerThread.interrupt();
+    receiverThread.interrupt();
+    socket.close();
+  }
+
+  public void setAddress(String address) {
+    this.address = address;
+  }
+
+  public boolean isPaused() {
+    return paused;
+  }
+
+  public void setPaused(boolean b) {
+    paused = b;
+  }
+
+  /**
+    Returns true if this receiver is active. */
+  public synchronized boolean isActive() {
+    return isActive;
+  }
+
+  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 {
+      isActive = true;
+      socket = new MulticastSocket(port);
+      socket.joinGroup(addr);
+      receiverThread = new MulticastReceiverThread();
+      receiverThread.start();
+      handlerThread = new MulticastHandlerThread();
+      handlerThread.start();
+    } catch (IOException ioe) {
+      ioe.printStackTrace();
+    }
+  }
+
+  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() {
+      isActive = true;
+
+      byte[] b = new byte[PACKET_LENGTH];
+      DatagramPacket p = new DatagramPacket(b, b.length);
+
+      while (isActive) {
+        try {
+          socket.receive(p);
+
+          //this string constructor which accepts a charset throws an exception if it is 
+          //null
+            if (encoding == null) {
+            handlerThread.append(
+              new String(p.getData(), 0, p.getLength()));
+          } else {
+            handlerThread.append(
+              new String(p.getData(), 0, p.getLength(), encoding));
+          }
+        } catch (SocketException se) {
+          //disconnected
+        } catch (IOException ioe) {
+          ioe.printStackTrace();
+        }
+      }
+
+      getLogger().debug("{}'s thread is ending.", MulticastReceiver.this.getName());
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/net/MulticastReceiverBeanInfo.java b/src/main/java/org/apache/log4j/net/MulticastReceiverBeanInfo.java
new file mode 100644
index 0000000..32ff8df
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/MulticastReceiverBeanInfo.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.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),
+            };
+        } catch (Exception e) {
+        }
+
+        return null;
+    }
+}
diff --git a/src/main/java/org/apache/log4j/net/UDPReceiver.java b/src/main/java/org/apache/log4j/net/UDPReceiver.java
new file mode 100644
index 0000000..3783fc7
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/UDPReceiver.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.net;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.Decoder;
+import org.apache.log4j.spi.LoggingEvent;
+
+
+/**
+ *  Receive LoggingEvents encoded with an XMLLayout, convert the XML data to a
+ *  LoggingEvent and post the LoggingEvent.
+ *
+ *  @author Scott Deboy <sdeboy@apache.org>
+ *
+ */
+public class UDPReceiver extends Receiver implements PortBased, Pauseable {
+  private static final int PACKET_LENGTH = 16384;
+  private UDPReceiverThread receiverThread;
+  private String encoding;
+
+  //default to log4j xml decoder
+  private String decoder = "org.apache.log4j.xml.XMLDecoder";
+  private Decoder decoderImpl;
+  protected boolean paused;
+  private transient boolean closed = false;
+  private int port;
+  private DatagramSocket socket;
+  UDPHandlerThread handlerThread;
+
+  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 synchronized void shutdown() {
+    if(closed == true) {
+      return;
+    }
+    closed = true;
+    // Closing the datagram socket will unblock the UDPReceiverThread if it is
+    // was waiting to receive data from the socket.
+    socket.close();
+
+    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();
+    } 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();
+            }
+
+            if (list.size() > 0) {
+              list2.addAll(list);
+              list.clear();
+            }
+          } catch (InterruptedException ie) {
+          }
+        }
+
+        if (list2.size() > 0) {
+          Iterator iter = list2.iterator();
+
+          while (iter.hasNext()) {
+            String data = (String) iter.next();
+            List v = decoderImpl.decodeEvents(data);
+
+            if (v != null) {
+              Iterator eventIter = v.iterator();
+
+              while (eventIter.hasNext()) {
+                if (!isPaused()) {
+                  doPost((LoggingEvent) eventIter.next());
+                }
+              }
+            }
+          }
+
+          list2.clear();
+        } else {
+          try {
+            synchronized (this) {
+              wait(1000);
+            }
+          } catch (InterruptedException ie) {
+          }
+        }
+      } // while
+      getLogger().debug(UDPReceiver.this.getName()+ "'s handler thread is exiting");
+    } // run
+  } // UDPHandlerThread
+
+  class UDPReceiverThread extends Thread {
+    public UDPReceiverThread() {
+      setDaemon(true);
+    }
+    
+    public void run() {
+      byte[] b = new byte[PACKET_LENGTH];
+      DatagramPacket p = new DatagramPacket(b, b.length);
+
+      while (!UDPReceiver.this.closed) {
+        try {
+          socket.receive(p);
+          
+          //this string constructor which accepts a charset throws an exception if it is 
+          //null
+          if (encoding == null) {
+            handlerThread.append(
+              new String(p.getData(), 0, p.getLength()));
+          } else {
+            handlerThread.append(
+              new String(p.getData(), 0, p.getLength(), encoding));
+          }
+        } catch (SocketException se) {
+          //disconnected
+        } catch (IOException ioe) {
+          ioe.printStackTrace();
+        }
+      }
+
+      //LogLog.debug(UDPReceiver.this.getName() + "'s thread is ending.");
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/net/XMLSocketNode.java b/src/main/java/org/apache/log4j/net/XMLSocketNode.java
new file mode 100644
index 0000000..86718b1
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/XMLSocketNode.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.net;
+
+import org.apache.log4j.*;
+import org.apache.log4j.helpers.Constants;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.net.Socket;
+
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+   Read {@link LoggingEvent} objects sent from a remote client using XML over
+   Sockets (TCP). These logging events are logged according to local
+   policy, as if they were generated locally.
+
+   <p>For example, the socket node might decide to log events to a
+   local file and also resent them to a second socket node.
+
+    @author  Scott Deboy <sdeboy@apache.org>;
+
+    @since 0.8.4
+*/
+public class XMLSocketNode extends ComponentBase implements Runnable {
+  Socket socket;
+  Receiver receiver;
+  Decoder decoder;
+  SocketNodeEventListener listener;
+
+  /**
+    Constructor for socket and logger repository. */
+  public XMLSocketNode(
+    String decoder, Socket socket, LoggerRepository hierarchy) {
+    this.repository = hierarchy;
+    try {
+      Class c = Class.forName(decoder);
+      Object o = c.newInstance();
+
+      if (o instanceof Decoder) {
+        this.decoder = (Decoder) o;
+      }
+    } catch (ClassNotFoundException cnfe) {
+      getLogger().warn("Unable to find decoder", cnfe);
+    } catch (IllegalAccessException iae) {
+      getLogger().warn("Unable to construct decoder", iae);
+    } catch (InstantiationException ie) {
+      getLogger().warn("Unable to construct decoder", ie);
+    }
+
+    this.socket = socket;
+  }
+
+  /**
+    Constructor for socket and reciever. */
+  public XMLSocketNode(String decoder, Socket socket, Receiver receiver) {
+    try {
+      Class c = Class.forName(decoder);
+      Object o = c.newInstance();
+
+      if (o instanceof Decoder) {
+        this.decoder = (Decoder) o;
+      }
+    } catch (ClassNotFoundException cnfe) {
+      getLogger().warn("Unable to find decoder", cnfe);
+    } catch (IllegalAccessException iae) {
+      getLogger().warn("Unable to construct decoder", iae);
+    } catch (InstantiationException ie) {
+      getLogger().warn("Unable to construct decoder", ie);
+    }
+
+    this.socket = socket;
+    this.receiver = receiver;
+  }
+
+  /**
+    Set the event listener on this node. */
+  public void setListener(SocketNodeEventListener _listener) {
+    listener = _listener;
+  }
+
+  public void run() {
+    Logger remoteLogger;
+    Exception listenerException = null;
+    InputStream is = null;
+
+    if ((this.receiver == null) || (this.decoder == null)) {
+      is = null;
+      listenerException =
+        new Exception(
+          "No receiver or decoder provided.  Cannot process xml socket events");
+      getLogger().error(
+        "Exception constructing XML Socket Receiver", listenerException);
+    }
+
+    try {
+      is = socket.getInputStream();
+    } catch (Exception e) {
+      is = null;
+      listenerException = e;
+      getLogger().error("Exception opening ObjectInputStream to " + socket, e);
+    }
+
+    if (is != null) {
+      String remoteInfo =
+        socket.getInetAddress().getHostName() + ":" + 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();
+
+              //if machinename property was not set (the case if properties
+              //not supported by the DTD), use remoteinfo as machine name
+              if (e.getProperty(Constants.HOSTNAME_KEY) == null) {
+                e.setProperty(Constants.HOSTNAME_KEY, remoteInfo);
+              }
+
+              // store the known remote info in an event property
+              e.setProperty("log4j.remoteSourceInfo", remoteInfo);
+
+              // if configured with a receiver, tell it to post the event
+              if (receiver != null) {
+                receiver.doPost(e);
+
+                // else post it via the hierarchy
+              } else {
+                // get a logger from the hierarchy. The name of the logger
+                // is taken to be the name contained in the event.
+                remoteLogger = repository.getLogger(e.getLoggerName());
+
+                //event.logger = remoteLogger;
+                // apply the logger-level filter
+                if (
+                  e.getLevel().isGreaterOrEqual(
+                      remoteLogger.getEffectiveLevel())) {
+                  // finally log the event as if was generated locally
+                  remoteLogger.callAppenders(e);
+                }
+              }
+            }
+          }
+        }
+      } catch (java.io.EOFException e) {
+        getLogger().info("Caught java.io.EOFException closing connection.");
+        listenerException = e;
+      } catch (java.net.SocketException e) {
+        getLogger().info(
+          "Caught java.net.SocketException closing connection.");
+        listenerException = e;
+      } catch (IOException e) {
+        getLogger().info("Caught java.io.IOException: " + e);
+        getLogger().info("Closing connection.");
+        listenerException = e;
+      } catch (Exception e) {
+        getLogger().error("Unexpected exception. Closing connection.", e);
+        listenerException = e;
+      }
+    }
+
+    // close the socket
+    try {
+      if (is != null) {
+        is.close();
+      }
+    } catch (Exception e) {
+      //logger.info("Could not close connection.", e);
+    }
+
+    // send event to listener, if configured
+    if (listener != null) {
+      listener.socketClosedEvent(listenerException);
+    }
+  }
+}
diff --git a/src/main/java/org/apache/log4j/net/XMLSocketReceiver.java b/src/main/java/org/apache/log4j/net/XMLSocketReceiver.java
new file mode 100644
index 0000000..17a0041
--- /dev/null
+++ b/src/main/java/org/apache/log4j/net/XMLSocketReceiver.java
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.log4j.net;
+
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.List;
+import java.util.Vector;
+
+import org.apache.log4j.plugins.Pauseable;
+import org.apache.log4j.plugins.Plugin;
+import org.apache.log4j.plugins.Receiver;
+import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.spi.LoggingEvent;
+
+
+/**
+  XMLSocketReceiver receives a remote logging event via XML on a configured
+  socket and "posts" it to a LoggerRepository as if the event were
+  generated locally. This class is designed to receive events from
+  the XMLSocketAppender class (or classes that send compatible events).
+
+  <p>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>
+
+    @since 1.3
+*/
+public class XMLSocketReceiver extends Receiver implements Runnable, PortBased, Pauseable {
+  protected boolean active = false;
+  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;
+
+  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;
+  }
+
+  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));
+  }
+  	
+  /**
+    Returns true if this receiver is active. */
+  public synchronized boolean isActive() {
+    return active;
+  }
+
+  /**
+    Starts the SocketReceiver with the current options. */
+  public void activateOptions() {
+    if (!isActive()) {
+      rThread = new Thread(this);
+      rThread.setDaemon(true);
+      rThread.start();
+      active = true;
+    }
+  }
+
+  /**
+    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();
+    }
+
+    /**
+      * 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);
+    }
+  }
+
+
+}