[LOG4J2-424]: Add non-string data type support to JdbcAppender

This utilises the new ColumnMapping plugin created for CassandraAppender. 
However, this also means that JdbcAppender has two ways to configure columns, 
and both methods can be used at the same time.


Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo
Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/commit/6a1339b8
Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/6a1339b8
Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/6a1339b8

Branch: refs/heads/master
Commit: 6a1339b800add2a009a4ba62a1235ad9878abd1c
Parents: 230e953
Author: Matt Sicker <[email protected]>
Authored: Sat Jan 7 03:39:12 2017 -0600
Committer: Matt Sicker <[email protected]>
Committed: Sat Jan 7 03:39:12 2017 -0600

----------------------------------------------------------------------
 .../core/appender/db/jdbc/JdbcAppender.java     |  32 ++--
 .../appender/db/jdbc/JdbcDatabaseManager.java   | 175 ++++++++++++-------
 .../db/jdbc/AbstractJdbcAppenderTest.java       |   6 +
 .../appender/db/jdbc/JdbcH2AppenderTest.java    |   2 +-
 .../db/jdbc/JdbcHyperSqlAppenderTest.java       |   2 +-
 .../db/jdbc/log4j2-h2-factory-method.xml        |   1 +
 .../db/jdbc/log4j2-hsqldb-factory-method.xml    |   1 +
 src/changes/changes.xml                         |   5 +-
 src/site/xdoc/manual/appenders.xml              |  31 +++-
 9 files changed, 178 insertions(+), 77 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6a1339b8/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java
index 3aa5644..af0f93f 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java
@@ -18,6 +18,7 @@ package org.apache.logging.log4j.core.appender.db.jdbc;
 
 import java.io.Serializable;
 import java.nio.charset.Charset;
+import java.sql.PreparedStatement;
 import java.util.Arrays;
 import java.util.Objects;
 
@@ -27,21 +28,26 @@ import org.apache.logging.log4j.core.Filter;
 import org.apache.logging.log4j.core.Layout;
 import org.apache.logging.log4j.core.appender.AbstractAppender;
 import org.apache.logging.log4j.core.appender.db.AbstractDatabaseAppender;
+import org.apache.logging.log4j.core.appender.db.ColumnMapping;
 import org.apache.logging.log4j.core.config.plugins.Plugin;
 import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
 import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
 import org.apache.logging.log4j.core.config.plugins.PluginElement;
+import org.apache.logging.log4j.core.config.plugins.convert.TypeConverter;
 import 
org.apache.logging.log4j.core.config.plugins.validation.constraints.Required;
 import org.apache.logging.log4j.core.util.Assert;
 import org.apache.logging.log4j.core.util.Booleans;
 
 /**
  * This Appender writes logging events to a relational database using standard 
JDBC mechanisms. It takes a list of
- * {@link ColumnConfig}s with which it determines how to save the event data 
into the appropriate columns in the table.
+ * {@link ColumnConfig}s and/or {@link ColumnMapping}s with which it 
determines how to save the event data into the
+ * appropriate columns in the table. ColumnMapping is new as of Log4j 2.8 and 
supports
+ * {@linkplain TypeConverter type conversion} and persistence using {@link 
PreparedStatement#setObject(int, Object)}.
  * A {@link ConnectionSource} plugin instance instructs the appender (and 
{@link JdbcDatabaseManager}) how to connect to
  * the database. This appender can be reconfigured at run time.
  *
  * @see ColumnConfig
+ * @see ColumnMapping
  * @see ConnectionSource
  */
 @Plugin(name = "JDBC", category = Core.CATEGORY_NAME, elementType = 
Appender.ELEMENT_TYPE, printObject = true)
@@ -113,9 +119,8 @@ public final class JdbcAppender extends 
AbstractDatabaseAppender<JdbcDatabaseMan
         @PluginElement("ColumnConfigs")
         private ColumnConfig[] columnConfigs;
 
-        // TODO: LOG4J2-424
-//        @PluginElement("ColumnMappings")
-//        private ColumnMapping[] columnMappings;
+        @PluginElement("ColumnMappings")
+        private ColumnMapping[] columnMappings;
 
         /**
          * The connections source from which database connections should be 
retrieved.
@@ -150,17 +155,22 @@ public final class JdbcAppender extends 
AbstractDatabaseAppender<JdbcDatabaseMan
             return asBuilder();
         }
 
-//        public B setColumnMappings(final ColumnMapping... columnMappings) {
-//            this.columnMappings = columnMappings;
-//            return asBuilder();
-//        }
+        public B setColumnMappings(final ColumnMapping... columnMappings) {
+            this.columnMappings = columnMappings;
+            return asBuilder();
+        }
 
         @Override
         public JdbcAppender build() {
+            if (Assert.isEmpty(columnConfigs) && 
Assert.isEmpty(columnMappings)) {
+                LOGGER.error("Cannot create JdbcAppender without any columns 
configured.");
+                return null;
+            }
             final String managerName = "JdbcManager{name=" + getName() + ", 
bufferSize=" + bufferSize + ", tableName=" +
-                tableName + ", columnConfigs=" + 
Arrays.toString(columnConfigs) + '}';
-            final JdbcDatabaseManager manager = 
JdbcDatabaseManager.getJDBCDatabaseManager(managerName, bufferSize,
-                connectionSource, tableName, columnConfigs);
+                tableName + ", columnConfigs=" + 
Arrays.toString(columnConfigs) + ", columnMappings=" +
+                Arrays.toString(columnMappings) + '}';
+            final JdbcDatabaseManager manager = 
JdbcDatabaseManager.getManager(managerName, bufferSize,
+                connectionSource, tableName, columnConfigs, columnMappings);
             if (manager == null) {
                 return null;
             }

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6a1339b8/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcDatabaseManager.java
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcDatabaseManager.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcDatabaseManager.java
index 6f9c66a..2d80f70 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcDatabaseManager.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcDatabaseManager.java
@@ -17,20 +17,30 @@
 package org.apache.logging.log4j.core.appender.db.jdbc;
 
 import java.io.StringReader;
+import java.sql.Clob;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
+import java.sql.NClob;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.sql.Timestamp;
+import java.sql.Types;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.appender.AppenderLoggingException;
 import org.apache.logging.log4j.core.appender.ManagerFactory;
 import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
-import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.core.appender.db.ColumnMapping;
+import org.apache.logging.log4j.core.config.plugins.convert.DateTypeConverter;
+import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
 import org.apache.logging.log4j.core.util.Closer;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.apache.logging.log4j.spi.ThreadContextStack;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.Strings;
 
 /**
  * An {@link AbstractDatabaseManager} implementation for relational databases 
accessed via JDBC.
@@ -39,7 +49,9 @@ public final class JdbcDatabaseManager extends 
AbstractDatabaseManager {
 
     private static final JdbcDatabaseManagerFactory INSTANCE = new 
JdbcDatabaseManagerFactory();
 
-    private final List<Column> columns;
+    // NOTE: prepared statements are prepared in this order: column mappings, 
then column configs
+    private final List<ColumnMapping> columnMappings;
+    private final List<ColumnConfig> columnConfigs;
     private final ConnectionSource connectionSource;
     private final String sqlStatement;
 
@@ -48,11 +60,13 @@ public final class JdbcDatabaseManager extends 
AbstractDatabaseManager {
     private boolean isBatchSupported;
 
     private JdbcDatabaseManager(final String name, final int bufferSize, final 
ConnectionSource connectionSource,
-                                final String sqlStatement, final List<Column> 
columns) {
+                                final String sqlStatement, final 
List<ColumnConfig> columnConfigs,
+                                final List<ColumnMapping> columnMappings) {
         super(name, bufferSize);
         this.connectionSource = connectionSource;
         this.sqlStatement = sqlStatement;
-        this.columns = columns;
+        this.columnConfigs = columnConfigs;
+        this.columnMappings = columnMappings;
     }
 
     @Override
@@ -95,26 +109,45 @@ public final class JdbcDatabaseManager extends 
AbstractDatabaseManager {
             }
 
             int i = 1;
-            for (final Column column : this.columns) {
-                if (column.isEventTimestamp) {
-                    this.statement.setTimestamp(i++, new 
Timestamp(event.getTimeMillis()));
+            for (final ColumnMapping mapping : this.columnMappings) {
+                if (ThreadContextMap.class.isAssignableFrom(mapping.getType())
+                    || 
ReadOnlyStringMap.class.isAssignableFrom(mapping.getType())) {
+                    this.statement.setObject(i++, 
event.getContextData().toMap());
+                } else if 
(ThreadContextStack.class.isAssignableFrom(mapping.getType())) {
+                    this.statement.setObject(i++, 
event.getContextStack().asList());
+                } else if (Date.class.isAssignableFrom(mapping.getType())) {
+                    this.statement.setObject(i++,
+                        DateTypeConverter.fromMillis(event.getTimeMillis(), 
mapping.getType().asSubclass(Date.class)));
+                } else if (Clob.class.isAssignableFrom(mapping.getType())) {
+                    this.statement.setClob(i++, new 
StringReader(mapping.getLayout().toSerializable(event)));
+                } else if (NClob.class.isAssignableFrom(mapping.getType())) {
+                    this.statement.setNClob(i++, new 
StringReader(mapping.getLayout().toSerializable(event)));
                 } else {
-                    if (column.isClob) {
-                        reader = new 
StringReader(column.layout.toSerializable(event));
-                        if (column.isUnicode) {
-                            this.statement.setNClob(i++, reader);
-                        } else {
-                            this.statement.setClob(i++, reader);
-                        }
+                    final Object value = 
TypeConverters.convert(mapping.getLayout().toSerializable(event),
+                        mapping.getType(), null);
+                    if (value == null) {
+                        this.statement.setNull(i++, Types.NULL);
                     } else {
-                        if (column.isUnicode) {
-                            this.statement.setNString(i++, 
column.layout.toSerializable(event));
-                        } else {
-                            this.statement.setString(i++, 
column.layout.toSerializable(event));
-                        }
+                        this.statement.setObject(i++, value);
                     }
                 }
             }
+            for (final ColumnConfig column : this.columnConfigs) {
+                if (column.isEventTimestamp()) {
+                    this.statement.setTimestamp(i++, new 
Timestamp(event.getTimeMillis()));
+                } else if (column.isClob()) {
+                    reader = new 
StringReader(column.getLayout().toSerializable(event));
+                    if (column.isUnicode()) {
+                        this.statement.setNClob(i++, reader);
+                    } else {
+                        this.statement.setClob(i++, reader);
+                    }
+                } else if (column.isUnicode()) {
+                    this.statement.setNString(i++, 
column.getLayout().toSerializable(event));
+                } else {
+                    this.statement.setString(i++, 
column.getLayout().toSerializable(event));
+                }
+            }
 
             if (this.isBatchSupported) {
                 this.statement.addBatch();
@@ -173,15 +206,38 @@ public final class JdbcDatabaseManager extends 
AbstractDatabaseManager {
      * @param tableName The name of the database table to insert log events 
into.
      * @param columnConfigs Configuration information about the log table 
columns.
      * @return a new or existing JDBC manager as applicable.
+     * @deprecated use {@link #getManager(String, int, ConnectionSource, 
String, ColumnConfig[], ColumnMapping[])}
      */
+    @Deprecated
     public static JdbcDatabaseManager getJDBCDatabaseManager(final String 
name, final int bufferSize,
                                                              final 
ConnectionSource connectionSource,
                                                              final String 
tableName,
                                                              final 
ColumnConfig[] columnConfigs) {
 
-        return AbstractDatabaseManager.getManager(
-                name, new FactoryData(bufferSize, connectionSource, tableName, 
columnConfigs), getFactory()
-        );
+        return getManager(name,
+            new FactoryData(bufferSize, connectionSource, tableName, 
columnConfigs, new ColumnMapping[0]),
+            getFactory());
+    }
+
+    /**
+     * Creates a JDBC manager for use within the {@link JdbcAppender}, or 
returns a suitable one if it already exists.
+     *
+     * @param name The name of the manager, which should include connection 
details and hashed passwords where possible.
+     * @param bufferSize The size of the log event buffer.
+     * @param connectionSource The source for connections to the database.
+     * @param tableName The name of the database table to insert log events 
into.
+     * @param columnConfigs Configuration information about the log table 
columns.
+     * @param columnMappings column mapping configuration (including type 
conversion).
+     * @return a new or existing JDBC manager as applicable.
+     */
+    public static JdbcDatabaseManager getManager(final String name,
+                                                 final int bufferSize,
+                                                 final ConnectionSource 
connectionSource,
+                                                 final String tableName,
+                                                 final ColumnConfig[] 
columnConfigs,
+                                                 final ColumnMapping[] 
columnMappings) {
+        return getManager(name, new FactoryData(bufferSize, connectionSource, 
tableName, columnConfigs, columnMappings),
+            getFactory());
     }
 
     private static JdbcDatabaseManagerFactory getFactory() {
@@ -192,16 +248,18 @@ public final class JdbcDatabaseManager extends 
AbstractDatabaseManager {
      * Encapsulates data that {@link JdbcDatabaseManagerFactory} uses to 
create managers.
      */
     private static final class FactoryData extends 
AbstractDatabaseManager.AbstractFactoryData {
-        private final ColumnConfig[] columnConfigs;
         private final ConnectionSource connectionSource;
         private final String tableName;
+        private final ColumnConfig[] columnConfigs;
+        private final ColumnMapping[] columnMappings;
 
         protected FactoryData(final int bufferSize, final ConnectionSource 
connectionSource, final String tableName,
-                              final ColumnConfig[] columnConfigs) {
+                              final ColumnConfig[] columnConfigs, final 
ColumnMapping[] columnMappings) {
             super(bufferSize);
             this.connectionSource = connectionSource;
             this.tableName = tableName;
             this.columnConfigs = columnConfigs;
+            this.columnMappings = columnMappings;
         }
     }
 
@@ -211,50 +269,45 @@ public final class JdbcDatabaseManager extends 
AbstractDatabaseManager {
     private static final class JdbcDatabaseManagerFactory implements 
ManagerFactory<JdbcDatabaseManager, FactoryData> {
         @Override
         public JdbcDatabaseManager createManager(final String name, final 
FactoryData data) {
-            final StringBuilder columnPart = new StringBuilder();
-            final StringBuilder valuePart = new StringBuilder();
-            final List<Column> columns = new ArrayList<>();
-            int i = 0;
+            final StringBuilder sb = new StringBuilder("INSERT INTO 
").append(data.tableName).append(" (");
+            // so this gets a little more complicated now that there are two 
ways to configure column mappings, but
+            // both mappings follow the same exact pattern for the prepared 
statement
+            for (final ColumnMapping mapping : data.columnMappings) {
+                sb.append(mapping.getName()).append(',');
+            }
             for (final ColumnConfig config : data.columnConfigs) {
-                if (i++ > 0) {
-                    columnPart.append(',');
-                    valuePart.append(',');
+                sb.append(config.getColumnName()).append(',');
+            }
+            // at least one of those arrays is guaranteed to be non-empty
+            sb.setCharAt(sb.length() - 1, ')');
+            sb.append(" VALUES (");
+            final List<ColumnMapping> columnMappings = new 
ArrayList<>(data.columnMappings.length);
+            for (final ColumnMapping mapping : data.columnMappings) {
+                if (Strings.isNotEmpty(mapping.getLiteralValue())) {
+                    sb.append(mapping.getLiteralValue());
+                } else {
+                    sb.append('?');
+                    columnMappings.add(mapping);
                 }
-
-                columnPart.append(config.getColumnName());
-
-                if (config.getLiteralValue() != null) {
-                    valuePart.append(config.getLiteralValue());
+                sb.append(',');
+            }
+            final List<ColumnConfig> columnConfigs = new 
ArrayList<>(data.columnConfigs.length);
+            for (final ColumnConfig config : data.columnConfigs) {
+                if (Strings.isNotEmpty(config.getLiteralValue())) {
+                    sb.append(config.getLiteralValue());
                 } else {
-                    columns.add(new Column(
-                            config.getLayout(), config.isEventTimestamp(), 
config.isUnicode(), config.isClob()
-                    ));
-                    valuePart.append('?');
+                    sb.append('?');
+                    columnConfigs.add(config);
                 }
+                sb.append(',');
             }
+            // at least one of those arrays is guaranteed to be non-empty
+            sb.setCharAt(sb.length() - 1, ')');
+            final String sqlStatement = sb.toString();
 
-            final String sqlStatement = "INSERT INTO " + data.tableName + " (" 
+ columnPart + ") VALUES (" +
-                    valuePart + ')';
-
-            return new JdbcDatabaseManager(name, data.getBufferSize(), 
data.connectionSource, sqlStatement, columns);
+            return new JdbcDatabaseManager(name, data.getBufferSize(), 
data.connectionSource, sqlStatement,
+                columnConfigs, columnMappings);
         }
     }
 
-    /**
-     * Encapsulates information about a database column and how to persist 
data to it.
-     */
-    private static final class Column {
-        private final PatternLayout layout;
-        private final boolean isEventTimestamp;
-        private final boolean isUnicode;
-        private final boolean isClob;
-
-        private Column(final PatternLayout layout, final boolean isEventDate, 
final boolean isUnicode,
-                       final boolean isClob) {
-            this.layout = layout;
-            this.isEventTimestamp = isEventDate;
-            this.isUnicode = isUnicode;
-            this.isClob = isClob;
-        }
-    }
 }

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6a1339b8/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/AbstractJdbcAppenderTest.java
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/AbstractJdbcAppenderTest.java
 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/AbstractJdbcAppenderTest.java
index 99c229d..e8855dc 100644
--- 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/AbstractJdbcAppenderTest.java
+++ 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/AbstractJdbcAppenderTest.java
@@ -27,6 +27,7 @@ import javax.sql.DataSource;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.Appender;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.config.ConfigurationFactory;
@@ -163,6 +164,7 @@ public abstract class AbstractJdbcAppenderTest {
 
         final long millis = System.currentTimeMillis();
 
+        ThreadContext.put("some_int", "42");
         final Logger logger = LogManager.getLogger(this.getClass().getName() + 
".testFactoryMethodConfig");
         logger.debug("Factory logged message 01.");
         logger.error("Error from factory 02.", exception);
@@ -173,6 +175,8 @@ public abstract class AbstractJdbcAppenderTest {
         assertTrue("There should be at least one row.", resultSet.next());
 
         long date = resultSet.getTimestamp("eventDate").getTime();
+        long anotherDate = resultSet.getTimestamp("anotherDate").getTime();
+        assertEquals(date, anotherDate);
         assertTrue("The date should be later than pre-logging (1).", date >= 
millis);
         assertTrue("The date should be earlier than now (1).", date <= 
System.currentTimeMillis());
         assertEquals("The literal column is not correct (1).", "Some Other 
Literal Value",
@@ -187,6 +191,8 @@ public abstract class AbstractJdbcAppenderTest {
         assertTrue("There should be two rows.", resultSet.next());
 
         date = resultSet.getTimestamp("eventDate").getTime();
+        anotherDate = resultSet.getTimestamp("anotherDate").getTime();
+        assertEquals(date, anotherDate);
         assertTrue("The date should be later than pre-logging (2).", date >= 
millis);
         assertTrue("The date should be earlier than now (2).", date <= 
System.currentTimeMillis());
         assertEquals("The literal column is not correct (2).", "Some Other 
Literal Value",

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6a1339b8/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcH2AppenderTest.java
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcH2AppenderTest.java
 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcH2AppenderTest.java
index 5f906a0..2d94acd 100644
--- 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcH2AppenderTest.java
+++ 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcH2AppenderTest.java
@@ -49,7 +49,7 @@ public class JdbcH2AppenderTest extends 
AbstractJdbcAppenderTest {
     protected String toCreateTableSqlString(final String tableName) {
         return "CREATE TABLE " + tableName + " ( " +
                     "id INTEGER IDENTITY, eventDate DATETIME, literalColumn 
VARCHAR(255), level NVARCHAR(10), " +
-                    "logger NVARCHAR(255), message VARCHAR(1024), exception 
NCLOB" +
+                    "logger NVARCHAR(255), message VARCHAR(1024), exception 
NCLOB, anotherDate TIMESTAMP" +
                 " )";
     }
 }

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6a1339b8/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcHyperSqlAppenderTest.java
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcHyperSqlAppenderTest.java
 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcHyperSqlAppenderTest.java
index 11ca769..9a066c8 100644
--- 
a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcHyperSqlAppenderTest.java
+++ 
b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcHyperSqlAppenderTest.java
@@ -50,7 +50,7 @@ public class JdbcHyperSqlAppenderTest extends 
AbstractJdbcAppenderTest {
     protected String toCreateTableSqlString(final String tableName) {
         return "CREATE TABLE " + tableName + " ( " +
                     "id INTEGER IDENTITY, eventDate DATETIME, literalColumn 
VARCHAR(255), level VARCHAR(10), " +
-                    "logger VARCHAR(255), message VARCHAR(1024), exception 
CLOB" +
+                    "logger VARCHAR(255), message VARCHAR(1024), exception 
CLOB, anotherDate TIMESTAMP" +
                 " )";
     }
 }

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6a1339b8/log4j-core/src/test/resources/org/apache/logging/log4j/core/appender/db/jdbc/log4j2-h2-factory-method.xml
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/test/resources/org/apache/logging/log4j/core/appender/db/jdbc/log4j2-h2-factory-method.xml
 
b/log4j-core/src/test/resources/org/apache/logging/log4j/core/appender/db/jdbc/log4j2-h2-factory-method.xml
index ac45e75..6b72c83 100644
--- 
a/log4j-core/src/test/resources/org/apache/logging/log4j/core/appender/db/jdbc/log4j2-h2-factory-method.xml
+++ 
b/log4j-core/src/test/resources/org/apache/logging/log4j/core/appender/db/jdbc/log4j2-h2-factory-method.xml
@@ -30,6 +30,7 @@
       <Column name="logger" pattern="%logger" />
       <Column name="message" pattern="%message" isUnicode="false" />
       <Column name="exception" pattern="%ex{full}" isClob="true" />
+      <ColumnMapping name="anotherDate" type="java.sql.Timestamp"/>
     </Jdbc>
   </Appenders>
 

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6a1339b8/log4j-core/src/test/resources/org/apache/logging/log4j/core/appender/db/jdbc/log4j2-hsqldb-factory-method.xml
----------------------------------------------------------------------
diff --git 
a/log4j-core/src/test/resources/org/apache/logging/log4j/core/appender/db/jdbc/log4j2-hsqldb-factory-method.xml
 
b/log4j-core/src/test/resources/org/apache/logging/log4j/core/appender/db/jdbc/log4j2-hsqldb-factory-method.xml
index eff391c..5faced5 100644
--- 
a/log4j-core/src/test/resources/org/apache/logging/log4j/core/appender/db/jdbc/log4j2-hsqldb-factory-method.xml
+++ 
b/log4j-core/src/test/resources/org/apache/logging/log4j/core/appender/db/jdbc/log4j2-hsqldb-factory-method.xml
@@ -30,6 +30,7 @@
       <Column name="logger" pattern="%logger" />
       <Column name="message" pattern="%message" isUnicode="false" />
       <Column name="exception" pattern="%ex{full}" isClob="true" />
+      <ColumnMapping name="anotherDate" type="java.sql.Timestamp"/>
     </Jdbc>
   </Appenders>
 

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6a1339b8/src/changes/changes.xml
----------------------------------------------------------------------
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index bfefc24..f553cc4 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -228,6 +228,9 @@
       <action issue="LOG4J2-1302" dev="rpopma" type="update">
         The log4j-slf4j-impl module now declares a runtime dependency on 
log4j-core. While not technically required, this makes the log4j-slf4j-impl 
module behave similarly to slf4j-log4j12, and facilitates migration to Log4j 2.
       </action>
+      <action issue="LOG4J2-424" dev="mattsicker" type="add">
+        Add non-string data type support to JdbcAppender via new ColumnMapping 
plugin.
+      </action>
       <action issue="LOG4J2-1771" dev="mattsicker" type="add">
         Add a Builder to ColumnConfig and deprecate 
ColumnConfig.createColumnConfig().
       </action>
@@ -238,7 +241,7 @@
         Use MethodHandle in ContextDataFactory cached constructor.
       </action>
       <action issue="LOG4J2-1730" dev="mattsicker" type="add">
-        Add Apache Cassandra appender.
+        Add Apache Cassandra appender and ColumnMapping plugin.
       </action>
       <action issue="LOG4J2-1759" dev="mattsicker" type="add">
         Add TypeConverter for java.util.UUID.

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6a1339b8/src/site/xdoc/manual/appenders.xml
----------------------------------------------------------------------
diff --git a/src/site/xdoc/manual/appenders.xml 
b/src/site/xdoc/manual/appenders.xml
index d1f6223..233bf10 100644
--- a/src/site/xdoc/manual/appenders.xml
+++ b/src/site/xdoc/manual/appenders.xml
@@ -955,7 +955,12 @@ CREATE TABLE logs (
           <p>The JDBCAppender writes log events to a relational database table 
using standard JDBC. It can be configured
             to obtain JDBC connections using a JNDI <code>DataSource</code> or 
a custom factory method. Whichever
             approach you take, it <strong><em>must</em></strong> be backed by 
a connection pool. Otherwise, logging
-            performance will suffer greatly.</p>
+            performance will suffer greatly. If batch statements are supported 
by the configured JDBC driver and a
+            <code>bufferSize</code> is configured to be a positive number, 
then log events will be batched. Note that as
+            of Log4j 2.8, there are two ways to configure log event to column 
mappings: the original <code>ColumnConfig</code>
+            style that only allows strings and timestamps, and the new 
<code>ColumnMapping</code> plugin that uses Log4j's
+            built-in type conversion to allow for more data types (this is the 
same plugin as in the
+            <a href="#CassandraAppender">Cassandra Appender</a>).</p>
           <table>
             <caption align="top">JDBCAppender Parameters</caption>
             <tr>
@@ -1001,9 +1006,31 @@ CREATE TABLE logs (
             <tr>
               <td>columnConfigs</td>
               <td>ColumnConfig[]</td>
-              <td><em>Required.</em> Information about the columns that log 
event data should be inserted into and how
+              <td><em>Required (and/or columnMappings).</em> Information about 
the columns that log event data should be inserted into and how
                 to insert that data. This is represented with multiple 
<code>&lt;Column&gt;</code> elements.</td>
             </tr>
+            <tr>
+              <td>columnMappings</td>
+              <td>ColumnMapping[]</td>
+              <td><em>Required (and/or columnConfigs).</em> A list of column 
mapping configurations. Each column must
+                specify a column name. Each column can have a conversion type 
specified by its fully qualified class
+                name. By default, the conversion type is <code>String</code>. 
If the configured type is
+                assignment-compatible with
+                <a class="javadoc" 
href="../log4j-api/apidocs/org/apache/logging/log4j/util/ReadOnlyStringMap.html">ReadOnlyStringMap</a>
+                /
+                <a class="javadoc" 
href="../log4j-api/apidocs/org/apache/logging/log4j/spi/ThreadContextMap.html">ThreadContextMap</a>
+                or
+                <a class="javadoc" 
href="../log4j-api/apidocs/org/apache/logging/log4j/spi/ThreadContextStack.html">ThreadContextStack</a>,
+                then that column will be populated with the MDC or NDC 
respectively (this is database-specific how they
+                handle inserting a <code>Map</code> or <code>List</code> 
value). If the configured type is
+                assignment-compatible with <code>java.util.Date</code>, then 
the log timestamp will be converted to
+                that configured date type. If the configured type is 
assignment-compatible with <code>java.sql.Clob</code>
+                or <code>java.sql.NClob</code>, then the formatted event will 
be set as a Clob or NClob respectively
+                (similar to the traditional ColumnConfig plugin). If a 
<code>literal</code> attribute is given, then its
+                value will be used as is in the <code>INSERT</code> query 
without any escaping. Otherwise, the layout or
+                pattern specified will be converted into the configured type 
and stored in that column.
+              </td>
+            </tr>
           </table>
           <p>When configuring the JDBCAppender, you must specify a 
<code>ConnectionSource</code> implementation from
             which the Appender gets JDBC connections. You must use exactly one 
of the <code>&lt;DataSource&gt;</code>

Reply via email to