View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.appender.db.jdbc;
18  
19  import java.io.Serializable;
20  import java.io.StringReader;
21  import java.sql.Clob;
22  import java.sql.Connection;
23  import java.sql.DatabaseMetaData;
24  import java.sql.NClob;
25  import java.sql.PreparedStatement;
26  import java.sql.ResultSetMetaData;
27  import java.sql.SQLException;
28  import java.sql.Timestamp;
29  import java.sql.Types;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Date;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Objects;
37  import java.util.concurrent.CountDownLatch;
38  
39  import org.apache.logging.log4j.core.Layout;
40  import org.apache.logging.log4j.core.LogEvent;
41  import org.apache.logging.log4j.core.StringLayout;
42  import org.apache.logging.log4j.core.appender.AppenderLoggingException;
43  import org.apache.logging.log4j.core.appender.ManagerFactory;
44  import org.apache.logging.log4j.core.appender.db.AbstractDatabaseAppender;
45  import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
46  import org.apache.logging.log4j.core.appender.db.ColumnMapping;
47  import org.apache.logging.log4j.core.appender.db.DbAppenderLoggingException;
48  import org.apache.logging.log4j.core.config.plugins.convert.DateTypeConverter;
49  import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
50  import org.apache.logging.log4j.core.util.Closer;
51  import org.apache.logging.log4j.core.util.Log4jThread;
52  import org.apache.logging.log4j.message.MapMessage;
53  import org.apache.logging.log4j.spi.ThreadContextMap;
54  import org.apache.logging.log4j.spi.ThreadContextStack;
55  import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
56  import org.apache.logging.log4j.util.ReadOnlyStringMap;
57  import org.apache.logging.log4j.util.Strings;
58  
59  /**
60   * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC.
61   */
62  public final class JdbcDatabaseManager extends AbstractDatabaseManager {
63  
64      /**
65       * Encapsulates data that {@link JdbcDatabaseManagerFactory} uses to create managers.
66       */
67      private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData {
68          private final ConnectionSource connectionSource;
69          private final String tableName;
70          private final ColumnConfig[] columnConfigs;
71          private final ColumnMapping[] columnMappings;
72          private final boolean immediateFail;
73          private final boolean retry;
74          private final long reconnectIntervalMillis;
75          private final boolean truncateStrings;
76  
77          protected FactoryData(final int bufferSize, final Layout<? extends Serializable> layout,
78                  final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs,
79                  final ColumnMapping[] columnMappings, final boolean immediateFail, final long reconnectIntervalMillis,
80                  final boolean truncateStrings) {
81              super(bufferSize, layout);
82              this.connectionSource = connectionSource;
83              this.tableName = tableName;
84              this.columnConfigs = columnConfigs;
85              this.columnMappings = columnMappings;
86              this.immediateFail = immediateFail;
87              this.retry = reconnectIntervalMillis > 0;
88              this.reconnectIntervalMillis = reconnectIntervalMillis;
89              this.truncateStrings = truncateStrings;
90          }
91  
92          @Override
93          public String toString() {
94              return String.format(
95                      "FactoryData [connectionSource=%s, tableName=%s, columnConfigs=%s, columnMappings=%s, immediateFail=%s, retry=%s, reconnectIntervalMillis=%s, truncateStrings=%s]",
96                      connectionSource, tableName, Arrays.toString(columnConfigs), Arrays.toString(columnMappings),
97                      immediateFail, retry, reconnectIntervalMillis, truncateStrings);
98          }
99      }
100 
101     /**
102      * Creates managers.
103      */
104     private static final class JdbcDatabaseManagerFactory implements ManagerFactory<JdbcDatabaseManager, FactoryData> {
105 
106         private static final char PARAMETER_MARKER = '?';
107 
108         @Override
109         public JdbcDatabaseManager createManager(final String name, final FactoryData data) {
110             final StringBuilder sb = new StringBuilder("insert into ").append(data.tableName).append(" (");
111             // so this gets a little more complicated now that there are two ways to configure column mappings, but
112             // both mappings follow the same exact pattern for the prepared statement
113             appendColumnNames("INSERT", data, sb);
114             sb.append(") values (");
115             int i = 1;
116             for (final ColumnMapping mapping : data.columnMappings) {
117                 final String mappingName = mapping.getName();
118                 if (Strings.isNotEmpty(mapping.getLiteralValue())) {
119                     logger().trace("Adding INSERT VALUES literal for ColumnMapping[{}]: {}={} ", i, mappingName,
120                             mapping.getLiteralValue());
121                     sb.append(mapping.getLiteralValue());
122                 } else if (Strings.isNotEmpty(mapping.getParameter())) {
123                     logger().trace("Adding INSERT VALUES parameter for ColumnMapping[{}]: {}={} ", i, mappingName,
124                             mapping.getParameter());
125                     sb.append(mapping.getParameter());
126                 } else {
127                     logger().trace("Adding INSERT VALUES parameter marker for ColumnMapping[{}]: {}={} ", i,
128                             mappingName, PARAMETER_MARKER);
129                     sb.append(PARAMETER_MARKER);
130                 }
131                 sb.append(',');
132                 i++;
133             }
134             final List<ColumnConfig> columnConfigs = new ArrayList<>(data.columnConfigs.length);
135             for (final ColumnConfig config : data.columnConfigs) {
136                 if (Strings.isNotEmpty(config.getLiteralValue())) {
137                     sb.append(config.getLiteralValue());
138                 } else {
139                     sb.append(PARAMETER_MARKER);
140                     columnConfigs.add(config);
141                 }
142                 sb.append(',');
143             }
144             // at least one of those arrays is guaranteed to be non-empty
145             sb.setCharAt(sb.length() - 1, ')');
146             final String sqlStatement = sb.toString();
147 
148             return new JdbcDatabaseManager(name, sqlStatement, columnConfigs, data);
149         }
150     }
151 
152     /**
153      * Handles reconnecting to JDBC once on a Thread.
154      */
155     private final class Reconnector extends Log4jThread {
156 
157         private final CountDownLatch latch = new CountDownLatch(1);
158         private volatile boolean shutdown = false;
159 
160         private Reconnector() {
161             super("JdbcDatabaseManager-Reconnector");
162         }
163 
164         public void latch() {
165             try {
166                 latch.await();
167             } catch (final InterruptedException ex) {
168                 // Ignore the exception.
169             }
170         }
171 
172         void reconnect() throws SQLException {
173             closeResources(false);
174             connectAndPrepare();
175             reconnector = null;
176             shutdown = true;
177             logger().debug("Connection reestablished to {}", factoryData);
178         }
179 
180         @Override
181         public void run() {
182             while (!shutdown) {
183                 try {
184                     sleep(factoryData.reconnectIntervalMillis);
185                     reconnect();
186                 } catch (final InterruptedException | SQLException e) {
187                     logger().debug("Cannot reestablish JDBC connection to {}: {}", factoryData, e.getLocalizedMessage(),
188                             e);
189                 } finally {
190                     latch.countDown();
191                 }
192             }
193         }
194 
195         public void shutdown() {
196             shutdown = true;
197         }
198 
199     }
200 
201     private static final class ResultSetColumnMetaData {
202 
203         private final String schemaName;
204         private final String catalogName;
205         private final String tableName;
206         private final String name;
207         private final String nameKey;
208         private final String label;
209         private final int displaySize;
210         private final int type;
211         private final String typeName;
212         private final String className;
213         private final int precision;
214         private final int scale;
215         private final boolean isStringType;
216 
217         public ResultSetColumnMetaData(final ResultSetMetaData rsMetaData, final int j) throws SQLException {
218             // @formatter:off
219             this(rsMetaData.getSchemaName(j),
220                  rsMetaData.getCatalogName(j),
221                  rsMetaData.getTableName(j),
222                  rsMetaData.getColumnName(j),
223                  rsMetaData.getColumnLabel(j),
224                  rsMetaData.getColumnDisplaySize(j),
225                  rsMetaData.getColumnType(j),
226                  rsMetaData.getColumnTypeName(j),
227                  rsMetaData.getColumnClassName(j),
228                  rsMetaData.getPrecision(j),
229                  rsMetaData.getScale(j));
230             // @formatter:on
231         }
232 
233         private ResultSetColumnMetaData(final String schemaName, final String catalogName, final String tableName,
234                 final String name, final String label, final int displaySize, final int type, final String typeName,
235                 final String className, final int precision, final int scale) {
236             super();
237             this.schemaName = schemaName;
238             this.catalogName = catalogName;
239             this.tableName = tableName;
240             this.name = name;
241             this.nameKey = ColumnMapping.toKey(name);
242             this.label = label;
243             this.displaySize = displaySize;
244             this.type = type;
245             this.typeName = typeName;
246             this.className = className;
247             this.precision = precision;
248             this.scale = scale;
249             // TODO How about also using the className?
250             // @formatter:off
251             this.isStringType =
252                     type == Types.CHAR ||
253                     type == Types.LONGNVARCHAR ||
254                     type == Types.LONGVARCHAR ||
255                     type == Types.NVARCHAR ||
256                     type == Types.VARCHAR;
257             // @formatter:on
258         }
259 
260         public String getCatalogName() {
261             return catalogName;
262         }
263 
264         public String getClassName() {
265             return className;
266         }
267 
268         public int getDisplaySize() {
269             return displaySize;
270         }
271 
272         public String getLabel() {
273             return label;
274         }
275 
276         public String getName() {
277             return name;
278         }
279 
280         public String getNameKey() {
281             return nameKey;
282         }
283 
284         public int getPrecision() {
285             return precision;
286         }
287 
288         public int getScale() {
289             return scale;
290         }
291 
292         public String getSchemaName() {
293             return schemaName;
294         }
295 
296         public String getTableName() {
297             return tableName;
298         }
299 
300         public int getType() {
301             return type;
302         }
303 
304         public String getTypeName() {
305             return typeName;
306         }
307 
308         public boolean isStringType() {
309             return this.isStringType;
310         }
311 
312         @Override
313         public String toString() {
314             return String.format(
315                     "ColumnMetaData [schemaName=%s, catalogName=%s, tableName=%s, name=%s, nameKey=%s, label=%s, displaySize=%s, type=%s, typeName=%s, className=%s, precision=%s, scale=%s, isStringType=%s]",
316                     schemaName, catalogName, tableName, name, nameKey, label, displaySize, type, typeName, className,
317                     precision, scale, isStringType);
318         }
319 
320         public String truncate(final String string) {
321             return precision > 0 ? Strings.left(string, precision) : string;
322         }
323     }
324 
325     private static final JdbcDatabaseManagerFactory INSTANCE = new JdbcDatabaseManagerFactory();
326 
327     private static void appendColumnName(final int i, final String columnName, final StringBuilder sb) {
328         if (i > 1) {
329             sb.append(',');
330         }
331         sb.append(columnName);
332     }
333 
334     /**
335      * Appends column names to the given buffer in the format {@code "A,B,C"}.
336      */
337     private static void appendColumnNames(final String sqlVerb, final FactoryData data, final StringBuilder sb) {
338         // so this gets a little more complicated now that there are two ways to configure column mappings, but
339         // both mappings follow the same exact pattern for the prepared statement
340         int i = 1;
341         final String messagePattern = "Appending {} {}[{}]: {}={} ";
342         for (final ColumnMapping colMapping : data.columnMappings) {
343             final String columnName = colMapping.getName();
344             appendColumnName(i, columnName, sb);
345             logger().trace(messagePattern, sqlVerb, colMapping.getClass().getSimpleName(), i, columnName, colMapping);
346             i++;
347         }
348         for (final ColumnConfig colConfig : data.columnConfigs) {
349             final String columnName = colConfig.getColumnName();
350             appendColumnName(i, columnName, sb);
351             logger().trace(messagePattern, sqlVerb, colConfig.getClass().getSimpleName(), i, columnName, colConfig);
352             i++;
353         }
354     }
355 
356     private static JdbcDatabaseManagerFactory getFactory() {
357         return INSTANCE;
358     }
359 
360     /**
361      * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
362      *
363      * @param name The name of the manager, which should include connection details and hashed passwords where possible.
364      * @param bufferSize The size of the log event buffer.
365      * @param connectionSource The source for connections to the database.
366      * @param tableName The name of the database table to insert log events into.
367      * @param columnConfigs Configuration information about the log table columns.
368      * @return a new or existing JDBC manager as applicable.
369      * @deprecated use
370      * {@link #getManager(String, int, Layout, ConnectionSource, String, ColumnConfig[], ColumnMapping[], boolean, long)}
371      */
372     @Deprecated
373     public static JdbcDatabaseManager getJDBCDatabaseManager(final String name, final int bufferSize,
374             final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs) {
375         return getManager(
376                 name, new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs,
377                         new ColumnMapping[0], false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true),
378                 getFactory());
379     }
380 
381     /**
382      * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
383      *
384      * @param name The name of the manager, which should include connection details and hashed passwords where possible.
385      * @param bufferSize The size of the log event buffer.
386      * @param layout The Appender-level layout
387      * @param connectionSource The source for connections to the database.
388      * @param tableName The name of the database table to insert log events into.
389      * @param columnConfigs Configuration information about the log table columns.
390      * @param columnMappings column mapping configuration (including type conversion).
391      * @return a new or existing JDBC manager as applicable.
392      */
393     @Deprecated
394     public static JdbcDatabaseManager getManager(final String name, final int bufferSize,
395             final Layout<? extends Serializable> layout, final ConnectionSource connectionSource,
396             final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings) {
397         return getManager(name, new FactoryData(bufferSize, layout, connectionSource, tableName, columnConfigs,
398                 columnMappings, false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true), getFactory());
399     }
400 
401     /**
402      * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
403      *
404      * @param name The name of the manager, which should include connection details and hashed passwords where possible.
405      * @param bufferSize The size of the log event buffer.
406      * @param layout
407      * @param connectionSource The source for connections to the database.
408      * @param tableName The name of the database table to insert log events into.
409      * @param columnConfigs Configuration information about the log table columns.
410      * @param columnMappings column mapping configuration (including type conversion).
411      * @param reconnectIntervalMillis
412      * @param immediateFail
413      * @return a new or existing JDBC manager as applicable.
414      * @deprecated use
415      * {@link #getManager(String, int, Layout, ConnectionSource, String, ColumnConfig[], ColumnMapping[], boolean, long)}
416      */
417     @Deprecated
418     public static JdbcDatabaseManager getManager(final String name, final int bufferSize,
419             final Layout<? extends Serializable> layout, final ConnectionSource connectionSource,
420             final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings,
421             final boolean immediateFail, final long reconnectIntervalMillis) {
422         return getManager(name, new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs,
423                 columnMappings, false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true), getFactory());
424     }
425 
426     /**
427      * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
428      *
429      * @param name The name of the manager, which should include connection details and hashed passwords where possible.
430      * @param bufferSize The size of the log event buffer.
431      * @param layout The Appender-level layout
432      * @param connectionSource The source for connections to the database.
433      * @param tableName The name of the database table to insert log events into.
434      * @param columnConfigs Configuration information about the log table columns.
435      * @param columnMappings column mapping configuration (including type conversion).
436      * @param immediateFail Whether or not to fail immediately with a {@link AppenderLoggingException} when connecting
437      * to JDBC fails.
438      * @param reconnectIntervalMillis How often to reconnect to the database when a SQL exception is detected.
439      * @param truncateStrings Whether or not to truncate strings to match column metadata.
440      * @return a new or existing JDBC manager as applicable.
441      */
442     public static JdbcDatabaseManager getManager(final String name, final int bufferSize,
443             final Layout<? extends Serializable> layout, final ConnectionSource connectionSource,
444             final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings,
445             final boolean immediateFail, final long reconnectIntervalMillis, final boolean truncateStrings) {
446         return getManager(name, new FactoryData(bufferSize, layout, connectionSource, tableName, columnConfigs,
447                 columnMappings, immediateFail, reconnectIntervalMillis, truncateStrings), getFactory());
448     }
449 
450     // NOTE: prepared statements are prepared in this order: column mappings, then column configs
451     private final List<ColumnConfig> columnConfigs;
452     private final String sqlStatement;
453     private final FactoryData factoryData;
454     private volatile Connection connection;
455     private volatile PreparedStatement statement;
456     private volatile Reconnector reconnector;
457     private volatile boolean isBatchSupported;
458     private volatile Map<String, ResultSetColumnMetaData> columnMetaData;
459 
460     private JdbcDatabaseManager(final String name, final String sqlStatement, final List<ColumnConfig> columnConfigs,
461             final FactoryData factoryData) {
462         super(name, factoryData.getBufferSize());
463         this.sqlStatement = sqlStatement;
464         this.columnConfigs = columnConfigs;
465         this.factoryData = factoryData;
466     }
467 
468     private void checkConnection() {
469         boolean connClosed = true;
470         try {
471             connClosed = this.connection == null || this.connection.isClosed();
472         } catch (final SQLException e) {
473             // Be quiet
474         }
475         boolean stmtClosed = true;
476         try {
477             stmtClosed = this.statement == null || this.statement.isClosed();
478         } catch (final SQLException e) {
479             // Be quiet
480         }
481         if (!this.isRunning() || connClosed || stmtClosed) {
482             // If anything is closed, close it all down before we reconnect
483             closeResources(false);
484             // Reconnect
485             if (reconnector != null && !factoryData.immediateFail) {
486                 reconnector.latch();
487                 if (connection == null) {
488                     throw new AppenderLoggingException(
489                             "Error writing to JDBC Manager '" + getName() + "': JDBC connection not available.");
490                 }
491                 if (statement == null) {
492                     throw new AppenderLoggingException(
493                             "Error writing to JDBC Manager '" + getName() + "': JDBC statement not available.");
494                 }
495             }
496         }
497     }
498 
499     protected void closeResources(final boolean logExceptions) {
500         try {
501             // Closing a statement returns it to the pool when using Apache Commons DBCP.
502             // Closing an already closed statement has no effect.
503             Closer.close(this.statement);
504         } catch (final Exception e) {
505             if (logExceptions) {
506                 logWarn("Failed to close SQL statement logging event or flushing buffer", e);
507             }
508         } finally {
509             this.statement = null;
510         }
511 
512         try {
513             // Closing a connection returns it to the pool when using Apache Commons DBCP.
514             // Closing an already closed connection has no effect.
515             Closer.close(this.connection);
516         } catch (final Exception e) {
517             if (logExceptions) {
518                 logWarn("Failed to close database connection logging event or flushing buffer", e);
519             }
520         } finally {
521             this.connection = null;
522         }
523     }
524 
525     @Override
526     protected boolean commitAndClose() {
527         final boolean closed = true;
528         try {
529             if (this.connection != null && !this.connection.isClosed()) {
530                 if (this.isBatchSupported && this.statement != null) {
531                     logger().debug("Executing batch PreparedStatement {}", this.statement);
532                     final int[] result = this.statement.executeBatch();
533                     logger().debug("Batch result: {}", Arrays.toString(result));
534                 }
535                 logger().debug("Committing Connection {}", this.connection);
536                 this.connection.commit();
537             }
538         } catch (final SQLException e) {
539             throw new DbAppenderLoggingException("Failed to commit transaction logging event or flushing buffer.", e);
540         } finally {
541             closeResources(true);
542         }
543         return closed;
544     }
545 
546     private boolean commitAndCloseAll() {
547         if (this.connection != null || this.statement != null) {
548             try {
549                 this.commitAndClose();
550                 return true;
551             } catch (final AppenderLoggingException e) {
552                 // Database connection has likely gone stale.
553                 final Throwable cause = e.getCause();
554                 final Throwable actual = cause == null ? e : cause;
555                 logger().debug("{} committing and closing connection: {}", actual, actual.getClass().getSimpleName(),
556                         e.toString(), e);
557             }
558         }
559         if (factoryData.connectionSource != null) {
560             factoryData.connectionSource.stop();
561         }
562         return true;
563     }
564 
565     private void connectAndPrepare() throws SQLException {
566         logger().debug("Acquiring JDBC connection from {}", this.getConnectionSource());
567         this.connection = getConnectionSource().getConnection();
568         logger().debug("Acquired JDBC connection {}", this.connection);
569         logger().debug("Getting connection metadata {}", this.connection);
570         final DatabaseMetaData databaseMetaData = this.connection.getMetaData();
571         logger().debug("Connection metadata {}", databaseMetaData);
572         this.isBatchSupported = databaseMetaData.supportsBatchUpdates();
573         logger().debug("Connection supportsBatchUpdates: {}", this.isBatchSupported);
574         this.connection.setAutoCommit(false);
575         logger().debug("Preparing SQL {}", this.sqlStatement);
576         this.statement = this.connection.prepareStatement(this.sqlStatement);
577         logger().debug("Prepared SQL {}", this.statement);
578         if (this.factoryData.truncateStrings) {
579             initColumnMetaData();
580         }
581     }
582 
583     @Override
584     protected void connectAndStart() {
585         checkConnection();
586         synchronized (this) {
587             try {
588                 connectAndPrepare();
589             } catch (final SQLException e) {
590                 reconnectOn(e);
591             }
592         }
593     }
594 
595     private Reconnector createReconnector() {
596         final Reconnector recon = new Reconnector();
597         recon.setDaemon(true);
598         recon.setPriority(Thread.MIN_PRIORITY);
599         return recon;
600     }
601 
602     private String createSqlSelect() {
603         final StringBuilder sb = new StringBuilder("select ");
604         appendColumnNames("SELECT", this.factoryData, sb);
605         sb.append(" from ");
606         sb.append(this.factoryData.tableName);
607         sb.append(" where 1=0");
608         return sb.toString();
609     }
610 
611     public ConnectionSource getConnectionSource() {
612         return factoryData.connectionSource;
613     }
614 
615     public String getSqlStatement() {
616         return sqlStatement;
617     }
618 
619     public String getTableName() {
620         return factoryData.tableName;
621     }
622 
623     private void initColumnMetaData() throws SQLException {
624         // Could use:
625         // this.connection.getMetaData().getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern);
626         // But this returns more data than we need for now, so do a SQL SELECT with 0 result rows instead.
627         final String sqlSelect = createSqlSelect();
628         logger().debug("Getting SQL metadata for table {}: {}", this.factoryData.tableName, sqlSelect);
629         try (final PreparedStatement mdStatement = this.connection.prepareStatement(sqlSelect)) {
630             final ResultSetMetaData rsMetaData = mdStatement.getMetaData();
631             logger().debug("SQL metadata: {}", rsMetaData);
632             if (rsMetaData != null) {
633                 final int columnCount = rsMetaData.getColumnCount();
634                 columnMetaData = new HashMap<>(columnCount);
635                 for (int i = 0, j = 1; i < columnCount; i++, j++) {
636                     final ResultSetColumnMetaData value = new ResultSetColumnMetaData(rsMetaData, j);
637                     columnMetaData.put(value.getNameKey(), value);
638                 }
639             } else {
640                 logger().warn(
641                         "{}: truncateStrings is true and ResultSetMetaData is null for statement: {}; manager will not perform truncation.",
642                         getClass().getSimpleName(), mdStatement);
643             }
644         }
645     }
646 
647     private void reconnectOn(final Exception exception) {
648         if (!factoryData.retry) {
649             throw new AppenderLoggingException("Cannot connect and prepare", exception);
650         }
651         if (reconnector == null) {
652             reconnector = createReconnector();
653             try {
654                 reconnector.reconnect();
655             } catch (final SQLException reconnectEx) {
656                 logger().debug("Cannot reestablish JDBC connection to {}: {}; starting reconnector thread {}",
657                         factoryData, reconnectEx, reconnector.getName(), reconnectEx);
658                 reconnector.start();
659                 reconnector.latch();
660                 if (connection == null || statement == null) {
661                     throw new AppenderLoggingException(
662                             String.format("Error sending to %s for %s", getName(), factoryData), exception);
663                 }
664             }
665         }
666     }
667 
668     private void setFields(final MapMessage<?, ?> mapMessage) throws SQLException {
669         final IndexedReadOnlyStringMap map = mapMessage.getIndexedReadOnlyStringMap();
670         final String simpleName = statement.getClass().getName();
671         int j = 1; // JDBC indices start at 1
672         for (final ColumnMapping mapping : this.factoryData.columnMappings) {
673             if (mapping.getLiteralValue() == null) {
674                 final String source = mapping.getSource();
675                 final String key = Strings.isEmpty(source) ? mapping.getName() : source;
676                 final Object value = map.getValue(key);
677                 if (logger().isTraceEnabled()) {
678                     final String valueStr = value instanceof String ? "\"" + value + "\""
679                             : Objects.toString(value, null);
680                     logger().trace("{} setObject({}, {}) for key '{}' and mapping '{}'", simpleName, j, valueStr, key,
681                             mapping.getName());
682                 }
683                 setStatementObject(j, mapping.getNameKey(), value);
684                 j++;
685             }
686         }
687     }
688 
689     /**
690      * Sets the given Object in the prepared statement. The value is truncated if needed.
691      */
692     private void setStatementObject(final int j, final String nameKey, final Object value) throws SQLException {
693         statement.setObject(j, truncate(nameKey, value));
694     }
695 
696     @Override
697     protected boolean shutdownInternal() {
698         if (reconnector != null) {
699             reconnector.shutdown();
700             reconnector.interrupt();
701             reconnector = null;
702         }
703         return commitAndCloseAll();
704     }
705 
706     @Override
707     protected void startupInternal() throws Exception {
708         // empty
709     }
710 
711     /**
712      * Truncates the value if needed.
713      */
714     private Object truncate(final String nameKey, Object value) {
715         if (value != null && this.factoryData.truncateStrings && columnMetaData != null) {
716             final ResultSetColumnMetaData resultSetColumnMetaData = columnMetaData.get(nameKey);
717             if (resultSetColumnMetaData != null) {
718                 if (resultSetColumnMetaData.isStringType()) {
719                     value = resultSetColumnMetaData.truncate(value.toString());
720                 }
721             } else {
722                 logger().error("Missing ResultSetColumnMetaData for {}", nameKey);
723             }
724         }
725         return value;
726     }
727 
728     @Override
729     protected void writeInternal(final LogEvent event, final Serializable serializable) {
730         StringReader reader = null;
731         try {
732             if (!this.isRunning() || this.connection == null || this.connection.isClosed() || this.statement == null
733                     || this.statement.isClosed()) {
734                 throw new AppenderLoggingException(
735                         "Cannot write logging event; JDBC manager not connected to the database.");
736             }
737             // Clear in case there are leftovers.
738             statement.clearParameters();
739             if (serializable instanceof MapMessage) {
740                 setFields((MapMessage<?, ?>) serializable);
741             }
742             int j = 1; // JDBC indices start at 1
743             for (final ColumnMapping mapping : this.factoryData.columnMappings) {
744                 if (ThreadContextMap.class.isAssignableFrom(mapping.getType())
745                         || ReadOnlyStringMap.class.isAssignableFrom(mapping.getType())) {
746                     this.statement.setObject(j++, event.getContextData().toMap());
747                 } else if (ThreadContextStack.class.isAssignableFrom(mapping.getType())) {
748                     this.statement.setObject(j++, event.getContextStack().asList());
749                 } else if (Date.class.isAssignableFrom(mapping.getType())) {
750                     this.statement.setObject(j++, DateTypeConverter.fromMillis(event.getTimeMillis(),
751                             mapping.getType().asSubclass(Date.class)));
752                 } else {
753                     final StringLayout layout = mapping.getLayout();
754                     if (layout != null) {
755                         if (Clob.class.isAssignableFrom(mapping.getType())) {
756                             this.statement.setClob(j++, new StringReader(layout.toSerializable(event)));
757                         } else if (NClob.class.isAssignableFrom(mapping.getType())) {
758                             this.statement.setNClob(j++, new StringReader(layout.toSerializable(event)));
759                         } else {
760                             final Object value = TypeConverters.convert(layout.toSerializable(event), mapping.getType(),
761                                     null);
762                             if (value == null) {
763                                 // TODO We might need to always initialize the columnMetaData to specify the type.
764                                 this.statement.setNull(j++, Types.NULL);
765                             } else {
766                                 setStatementObject(j++, mapping.getNameKey(), value);
767                             }
768                         }
769                     }
770                 }
771             }
772             for (final ColumnConfig column : this.columnConfigs) {
773                 if (column.isEventTimestamp()) {
774                     this.statement.setTimestamp(j++, new Timestamp(event.getTimeMillis()));
775                 } else if (column.isClob()) {
776                     reader = new StringReader(column.getLayout().toSerializable(event));
777                     if (column.isUnicode()) {
778                         this.statement.setNClob(j++, reader);
779                     } else {
780                         this.statement.setClob(j++, reader);
781                     }
782                 } else if (column.isUnicode()) {
783                     this.statement.setNString(j++, Objects.toString(
784                             truncate(column.getColumnNameKey(), column.getLayout().toSerializable(event)), null));
785                 } else {
786                     this.statement.setString(j++, Objects.toString(
787                             truncate(column.getColumnNameKey(), column.getLayout().toSerializable(event)), null));
788                 }
789             }
790 
791             if (this.isBatchSupported) {
792                 this.statement.addBatch();
793             } else if (this.statement.executeUpdate() == 0) {
794                 throw new AppenderLoggingException(
795                         "No records inserted in database table for log event in JDBC manager.");
796             }
797         } catch (final SQLException e) {
798             throw new DbAppenderLoggingException(
799                     "Failed to insert record for log event in JDBC manager: " + e.getMessage(), e);
800         } finally {
801             // Release ASAP
802             try {
803                 statement.clearParameters();
804             } catch (final SQLException e) {
805                 // Ignore
806             }
807             Closer.closeSilently(reader);
808         }
809     }
810 
811     @Override
812     protected void writeThrough(final LogEvent event, final Serializable serializable) {
813         this.connectAndStart();
814         try {
815             try {
816                 this.writeInternal(event, serializable);
817             } finally {
818                 this.commitAndClose();
819             }
820         } catch (final DbAppenderLoggingException e) {
821             reconnectOn(e);
822             try {
823                 this.writeInternal(event, serializable);
824             } finally {
825                 this.commitAndClose();
826             }
827         }
828     }
829 
830 }