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