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;
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            this.schemaName = schemaName;
249            this.catalogName = catalogName;
250            this.tableName = tableName;
251            this.name = name;
252            this.nameKey = ColumnMapping.toKey(name);
253            this.label = label;
254            this.displaySize = displaySize;
255            this.type = type;
256            this.typeName = typeName;
257            this.className = className;
258            this.precision = precision;
259            this.scale = scale;
260            // TODO How about also using the className?
261            // @formatter:off
262            this.isStringType =
263                    type == Types.CHAR ||
264                    type == Types.LONGNVARCHAR ||
265                    type == Types.LONGVARCHAR ||
266                    type == Types.NVARCHAR ||
267                    type == Types.VARCHAR;
268            // @formatter:on
269        }
270
271        public String getCatalogName() {
272            return catalogName;
273        }
274
275        public String getClassName() {
276            return className;
277        }
278
279        public int getDisplaySize() {
280            return displaySize;
281        }
282
283        public String getLabel() {
284            return label;
285        }
286
287        public String getName() {
288            return name;
289        }
290
291        public String getNameKey() {
292            return nameKey;
293        }
294
295        public int getPrecision() {
296            return precision;
297        }
298
299        public int getScale() {
300            return scale;
301        }
302
303        public String getSchemaName() {
304            return schemaName;
305        }
306
307        public String getTableName() {
308            return tableName;
309        }
310
311        public int getType() {
312            return type;
313        }
314
315        public String getTypeName() {
316            return typeName;
317        }
318
319        public boolean isStringType() {
320            return this.isStringType;
321        }
322
323        @Override
324        public String toString() {
325            return String.format(
326                    "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]",
327                    schemaName, catalogName, tableName, name, nameKey, label, displaySize, type, typeName, className,
328                    precision, scale, isStringType);
329        }
330
331        public String truncate(final String string) {
332            return precision > 0 ? Strings.left(string, precision) : string;
333        }
334    }
335
336    private static final JdbcDatabaseManagerFactory INSTANCE = new JdbcDatabaseManagerFactory();
337
338    private static void appendColumnName(final int i, final String columnName, final StringBuilder sb) {
339        if (i > 1) {
340            sb.append(',');
341        }
342        sb.append(columnName);
343    }
344
345    /**
346     * Appends column names to the given buffer in the format {@code "A,B,C"}.
347     */
348    private static void appendColumnNames(final String sqlVerb, final FactoryData data, final StringBuilder sb) {
349        // so this gets a little more complicated now that there are two ways to
350        // configure column mappings, but
351        // both mappings follow the same exact pattern for the prepared statement
352        int i = 1;
353        final String messagePattern = "Appending {} {}[{}]: {}={} ";
354        if (data.columnMappings != null) {
355            for (final ColumnMapping colMapping : data.columnMappings) {
356                final String columnName = colMapping.getName();
357                appendColumnName(i, columnName, sb);
358                logger().trace(messagePattern, sqlVerb, colMapping.getClass().getSimpleName(), i, columnName,
359                        colMapping);
360                i++;
361            }
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    private static JdbcDatabaseManagerFactory getFactory() {
375        return INSTANCE;
376    }
377
378    /**
379     * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
380     *
381     * @param name The name of the manager, which should include connection details and hashed passwords where possible.
382     * @param bufferSize The size of the log event buffer.
383     * @param connectionSource The source for connections to the database.
384     * @param tableName The name of the database table to insert log events into.
385     * @param columnConfigs Configuration information about the log table columns.
386     * @return a new or existing JDBC manager as applicable.
387     * @deprecated use
388     * {@link #getManager(String, int, Layout, ConnectionSource, String, ColumnConfig[], ColumnMapping[], boolean, long)}
389     */
390    @Deprecated
391    public static JdbcDatabaseManager getJDBCDatabaseManager(final String name, final int bufferSize,
392            final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs) {
393        return getManager(
394                name, new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs,
395                        new ColumnMapping[0], false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true),
396                getFactory());
397    }
398
399    /**
400     * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
401     *
402     * @param name The name of the manager, which should include connection details and hashed passwords where possible.
403     * @param bufferSize The size of the log event buffer.
404     * @param layout The Appender-level layout
405     * @param connectionSource The source for connections to the database.
406     * @param tableName The name of the database table to insert log events into.
407     * @param columnConfigs Configuration information about the log table columns.
408     * @param columnMappings column mapping configuration (including type conversion).
409     * @return a new or existing JDBC manager as applicable.
410     */
411    @Deprecated
412    public static JdbcDatabaseManager getManager(final String name, final int bufferSize,
413            final Layout<? extends Serializable> layout, final ConnectionSource connectionSource,
414            final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings) {
415        return getManager(name, new FactoryData(bufferSize, layout, connectionSource, tableName, columnConfigs,
416                columnMappings, false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true), getFactory());
417    }
418
419    /**
420     * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
421     *
422     * @param name The name of the manager, which should include connection details and hashed passwords where possible.
423     * @param bufferSize The size of the log event buffer.
424     * @param layout
425     * @param connectionSource The source for connections to the database.
426     * @param tableName The name of the database table to insert log events into.
427     * @param columnConfigs Configuration information about the log table columns.
428     * @param columnMappings column mapping configuration (including type conversion).
429     * @param reconnectIntervalMillis
430     * @param immediateFail
431     * @return a new or existing JDBC manager as applicable.
432     * @deprecated use
433     * {@link #getManager(String, int, Layout, ConnectionSource, String, ColumnConfig[], ColumnMapping[], boolean, long)}
434     */
435    @Deprecated
436    public static JdbcDatabaseManager getManager(final String name, final int bufferSize,
437            final Layout<? extends Serializable> layout, final ConnectionSource connectionSource,
438            final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings,
439            final boolean immediateFail, final long reconnectIntervalMillis) {
440        return getManager(name, new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs,
441                columnMappings, false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true), getFactory());
442    }
443
444    /**
445     * Creates a JDBC manager for use within the {@link JdbcAppender}, or returns a suitable one if it already exists.
446     *
447     * @param name The name of the manager, which should include connection details and hashed passwords where possible.
448     * @param bufferSize The size of the log event buffer.
449     * @param layout The Appender-level layout
450     * @param connectionSource The source for connections to the database.
451     * @param tableName The name of the database table to insert log events into.
452     * @param columnConfigs Configuration information about the log table columns.
453     * @param columnMappings column mapping configuration (including type conversion).
454     * @param immediateFail Whether or not to fail immediately with a {@link AppenderLoggingException} when connecting
455     * to JDBC fails.
456     * @param reconnectIntervalMillis How often to reconnect to the database when a SQL exception is detected.
457     * @param truncateStrings Whether or not to truncate strings to match column metadata.
458     * @return a new or existing JDBC manager as applicable.
459     */
460    public static JdbcDatabaseManager getManager(final String name, final int bufferSize,
461            final Layout<? extends Serializable> layout, final ConnectionSource connectionSource,
462            final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings,
463            final boolean immediateFail, final long reconnectIntervalMillis, final boolean truncateStrings) {
464        return getManager(name, new FactoryData(bufferSize, layout, connectionSource, tableName, columnConfigs,
465                columnMappings, immediateFail, reconnectIntervalMillis, truncateStrings), getFactory());
466    }
467
468    // NOTE: prepared statements are prepared in this order: column mappings, then column configs
469    private final List<ColumnConfig> columnConfigs;
470    private final String sqlStatement;
471    private final FactoryData factoryData;
472    private volatile Connection connection;
473    private volatile PreparedStatement statement;
474    private volatile Reconnector reconnector;
475    private volatile boolean isBatchSupported;
476    private volatile Map<String, ResultSetColumnMetaData> columnMetaData;
477
478    private JdbcDatabaseManager(final String name, final String sqlStatement, final List<ColumnConfig> columnConfigs,
479            final FactoryData factoryData) {
480        super(name, factoryData.getBufferSize());
481        this.sqlStatement = sqlStatement;
482        this.columnConfigs = columnConfigs;
483        this.factoryData = factoryData;
484    }
485
486    private void checkConnection() {
487        boolean connClosed = true;
488        try {
489            connClosed = isClosed(this.connection);
490        } catch (final SQLException e) {
491            // Be quiet
492        }
493        boolean stmtClosed = true;
494        try {
495            stmtClosed = isClosed(this.statement);
496        } catch (final SQLException e) {
497            // Be quiet
498        }
499        if (!this.isRunning() || connClosed || stmtClosed) {
500            // If anything is closed, close it all down before we reconnect
501            closeResources(false);
502            // Reconnect
503            if (reconnector != null && !factoryData.immediateFail) {
504                reconnector.latch();
505                if (connection == null) {
506                    throw new AppenderLoggingException(
507                            "Error writing to JDBC Manager '%s': JDBC connection not available [%s]", getName(), fieldsToString());
508                }
509                if (statement == null) {
510                    throw new AppenderLoggingException(
511                            "Error writing to JDBC Manager '%s': JDBC statement not available [%s].", getName(), connection, fieldsToString());
512                }
513            }
514        }
515    }
516
517    protected void closeResources(final boolean logExceptions) {
518        final PreparedStatement tempPreparedStatement = this.statement;
519        this.statement = null;
520        try {
521            // Closing a statement returns it to the pool when using Apache Commons DBCP.
522            // Closing an already closed statement has no effect.
523            Closer.close(tempPreparedStatement);
524        } catch (final Exception e) {
525            if (logExceptions) {
526                logWarn("Failed to close SQL statement logging event or flushing buffer", e);
527            }
528        }
529
530        final Connection tempConnection = this.connection;
531        this.connection = null;
532        try {
533            // Closing a connection returns it to the pool when using Apache Commons DBCP.
534            // Closing an already closed connection has no effect.
535            Closer.close(tempConnection);
536        } catch (final Exception e) {
537            if (logExceptions) {
538                logWarn("Failed to close database connection logging event or flushing buffer", e);
539            }
540        }
541    }
542
543    @Override
544    protected boolean commitAndClose() {
545        final boolean closed = true;
546        try {
547            if (this.connection != null && !this.connection.isClosed()) {
548                if (isBuffered() && this.isBatchSupported && this.statement != null) {
549                    logger().debug("Executing batch PreparedStatement {}", this.statement);
550                    int[] result;
551                    try {
552                        result = this.statement.executeBatch();
553                    } catch (SQLTransactionRollbackException e) {
554                        logger().debug("{} executing batch PreparedStatement {}, retrying.", e, this.statement);
555                        result = this.statement.executeBatch();
556                    }
557                    logger().debug("Batch result: {}", Arrays.toString(result));
558                }
559                logger().debug("Committing Connection {}", this.connection);
560                this.connection.commit();
561            }
562        } catch (final SQLException e) {
563            throw new DbAppenderLoggingException(e, "Failed to commit transaction logging event or flushing buffer [%s]",
564                    fieldsToString());
565        } finally {
566            closeResources(true);
567        }
568        return closed;
569    }
570
571    private boolean commitAndCloseAll() {
572        if (this.connection != null || this.statement != null) {
573            try {
574                this.commitAndClose();
575                return true;
576            } catch (final AppenderLoggingException e) {
577                // Database connection has likely gone stale.
578                final Throwable cause = e.getCause();
579                final Throwable actual = cause == null ? e : cause;
580                logger().debug("{} committing and closing connection: {}", actual, actual.getClass().getSimpleName(),
581                        e.toString(), e);
582            }
583        }
584        if (factoryData.connectionSource != null) {
585            factoryData.connectionSource.stop();
586        }
587        return true;
588    }
589
590    private void connectAndPrepare() throws SQLException {
591        logger().debug("Acquiring JDBC connection from {}", this.getConnectionSource());
592        this.connection = getConnectionSource().getConnection();
593        logger().debug("Acquired JDBC connection {}", this.connection);
594        logger().debug("Getting connection metadata {}", this.connection);
595        final DatabaseMetaData databaseMetaData = this.connection.getMetaData();
596        logger().debug("Connection metadata {}", databaseMetaData);
597        this.isBatchSupported = databaseMetaData.supportsBatchUpdates();
598        logger().debug("Connection supportsBatchUpdates: {}", this.isBatchSupported);
599        this.connection.setAutoCommit(false);
600        logger().debug("Preparing SQL {}", this.sqlStatement);
601        this.statement = this.connection.prepareStatement(this.sqlStatement);
602        logger().debug("Prepared SQL {}", this.statement);
603        if (this.factoryData.truncateStrings) {
604            initColumnMetaData();
605        }
606    }
607
608    @Override
609    protected void connectAndStart() {
610        checkConnection();
611        synchronized (this) {
612            try {
613                connectAndPrepare();
614            } catch (final SQLException e) {
615                reconnectOn(e);
616            }
617        }
618    }
619
620    private Reconnector createReconnector() {
621        final Reconnector recon = new Reconnector();
622        recon.setDaemon(true);
623        recon.setPriority(Thread.MIN_PRIORITY);
624        return recon;
625    }
626
627    private String createSqlSelect() {
628        final StringBuilder sb = new StringBuilder("select ");
629        appendColumnNames("SELECT", this.factoryData, sb);
630        sb.append(" from ");
631        sb.append(this.factoryData.tableName);
632        sb.append(" where 1=0");
633        return sb.toString();
634    }
635
636    private String fieldsToString() {
637        return String.format(
638                "columnConfigs=%s, sqlStatement=%s, factoryData=%s, connection=%s, statement=%s, reconnector=%s, isBatchSupported=%s, columnMetaData=%s",
639                columnConfigs, sqlStatement, factoryData, connection, statement, reconnector, isBatchSupported,
640                columnMetaData);
641    }
642
643    public ConnectionSource getConnectionSource() {
644        return factoryData.connectionSource;
645    }
646
647    public String getSqlStatement() {
648        return sqlStatement;
649    }
650
651    public String getTableName() {
652        return factoryData.tableName;
653    }
654
655    private void initColumnMetaData() throws SQLException {
656        // Could use:
657        // this.connection.getMetaData().getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern);
658        // But this returns more data than we need for now, so do a SQL SELECT with 0 result rows instead.
659        final String sqlSelect = createSqlSelect();
660        logger().debug("Getting SQL metadata for table {}: {}", this.factoryData.tableName, sqlSelect);
661        try (final PreparedStatement mdStatement = this.connection.prepareStatement(sqlSelect)) {
662            final ResultSetMetaData rsMetaData = mdStatement.getMetaData();
663            logger().debug("SQL metadata: {}", rsMetaData);
664            if (rsMetaData != null) {
665                final int columnCount = rsMetaData.getColumnCount();
666                columnMetaData = new HashMap<>(columnCount);
667                for (int i = 0, j = 1; i < columnCount; i++, j++) {
668                    final ResultSetColumnMetaData value = new ResultSetColumnMetaData(rsMetaData, j);
669                    columnMetaData.put(value.getNameKey(), value);
670                }
671            } else {
672                logger().warn(
673                        "{}: truncateStrings is true and ResultSetMetaData is null for statement: {}; manager will not perform truncation.",
674                        getClass().getSimpleName(), mdStatement);
675            }
676        }
677    }
678
679    /**
680     * Checks if a statement is closed. A null statement is considered closed.
681     *
682     * @param statement The statement to check.
683     * @return true if a statement is closed, false if null.
684     * @throws SQLException if a database access error occurs
685     */
686    private boolean isClosed(final Statement statement) throws SQLException {
687        return statement == null || statement.isClosed();
688    }
689
690    /**
691     * Checks if a connection is closed. A null connection is considered closed.
692     *
693     * @param connection The connection to check.
694     * @return true if a connection is closed, false if null.
695     * @throws SQLException if a database access error occurs
696     */
697    private boolean isClosed(final Connection connection) throws SQLException {
698        return connection == null || connection.isClosed();
699    }
700
701    private void reconnectOn(final Exception exception) {
702        if (!factoryData.retry) {
703            throw new AppenderLoggingException("Cannot connect and prepare", exception);
704        }
705        if (reconnector == null) {
706            reconnector = createReconnector();
707            try {
708                reconnector.reconnect();
709            } catch (final SQLException reconnectEx) {
710                logger().debug("Cannot reestablish JDBC connection to {}: {}; starting reconnector thread {}",
711                        factoryData, reconnectEx, reconnector.getName(), reconnectEx);
712                reconnector.start();
713                reconnector.latch();
714                if (connection == null || statement == null) {
715                    throw new AppenderLoggingException(exception, "Error sending to %s for %s [%s]", getName(),
716                            factoryData, fieldsToString());
717                }
718            }
719        }
720    }
721
722    private void setFields(final MapMessage<?, ?> mapMessage) throws SQLException {
723        final IndexedReadOnlyStringMap map = mapMessage.getIndexedReadOnlyStringMap();
724        final String simpleName = statement.getClass().getName();
725        int j = 1; // JDBC indices start at 1
726        if (this.factoryData.columnMappings != null) {
727            for (final ColumnMapping mapping : this.factoryData.columnMappings) {
728                if (mapping.getLiteralValue() == null) {
729                    final String source = mapping.getSource();
730                    final String key = Strings.isEmpty(source) ? mapping.getName() : source;
731                    final Object value = map.getValue(key);
732                    if (logger().isTraceEnabled()) {
733                        final String valueStr = value instanceof String ? "\"" + value + "\""
734                                : Objects.toString(value, null);
735                        logger().trace("{} setObject({}, {}) for key '{}' and mapping '{}'", simpleName, j, valueStr,
736                                key, mapping.getName());
737                    }
738                    setStatementObject(j, mapping.getNameKey(), value);
739                    j++;
740                }
741            }
742        }
743    }
744
745    /**
746     * Sets the given Object in the prepared statement. The value is truncated if needed.
747     */
748    private void setStatementObject(final int j, final String nameKey, final Object value) throws SQLException {
749        if (statement == null) {
750            throw new AppenderLoggingException("Cannot set a value when the PreparedStatement is null.");
751        }
752        if (value == null) {
753            if (columnMetaData == null) {
754                throw new AppenderLoggingException("Cannot set a value when the column metadata is null.");
755            }
756            // [LOG4J2-2762] [JDBC] MS-SQL Server JDBC driver throws SQLServerException when
757            // inserting a null value for a VARBINARY column.
758            // Calling setNull() instead of setObject() for null values fixes [LOG4J2-2762].
759            this.statement.setNull(j, columnMetaData.get(nameKey).getType());
760        } else {
761            statement.setObject(j, truncate(nameKey, value));
762        }
763    }
764
765    @Override
766    protected boolean shutdownInternal() {
767        if (reconnector != null) {
768            reconnector.shutdown();
769            reconnector.interrupt();
770            reconnector = null;
771        }
772        return commitAndCloseAll();
773    }
774
775    @Override
776    protected void startupInternal() throws Exception {
777        // empty
778    }
779
780    /**
781     * Truncates the value if needed.
782     */
783    private Object truncate(final String nameKey, Object value) {
784        if (value != null && this.factoryData.truncateStrings && columnMetaData != null) {
785            final ResultSetColumnMetaData resultSetColumnMetaData = columnMetaData.get(nameKey);
786            if (resultSetColumnMetaData != null) {
787                if (resultSetColumnMetaData.isStringType()) {
788                    value = resultSetColumnMetaData.truncate(value.toString());
789                }
790            } else {
791                logger().error("Missing ResultSetColumnMetaData for {}, connection={}, statement={}", nameKey,
792                        connection, statement);
793            }
794        }
795        return value;
796    }
797
798    @Override
799    protected void writeInternal(final LogEvent event, final Serializable serializable) {
800        StringReader reader = null;
801        try {
802            if (!this.isRunning() || isClosed(this.connection) || isClosed(this.statement)) {
803                throw new AppenderLoggingException(
804                        "Cannot write logging event; JDBC manager not connected to the database, running=%s, [%s]).",
805                        isRunning(), fieldsToString());
806            }
807            // Clear in case there are leftovers.
808            statement.clearParameters();
809            if (serializable instanceof MapMessage) {
810                setFields((MapMessage<?, ?>) serializable);
811            }
812            int j = 1; // JDBC indices start at 1
813            if (this.factoryData.columnMappings != null) {
814                for (final ColumnMapping mapping : this.factoryData.columnMappings) {
815                    if (ThreadContextMap.class.isAssignableFrom(mapping.getType())
816                            || ReadOnlyStringMap.class.isAssignableFrom(mapping.getType())) {
817                        this.statement.setObject(j++, event.getContextData().toMap());
818                    } else if (ThreadContextStack.class.isAssignableFrom(mapping.getType())) {
819                        this.statement.setObject(j++, event.getContextStack().asList());
820                    } else if (Date.class.isAssignableFrom(mapping.getType())) {
821                        this.statement.setObject(j++, DateTypeConverter.fromMillis(event.getTimeMillis(),
822                                mapping.getType().asSubclass(Date.class)));
823                    } else {
824                        final StringLayout layout = mapping.getLayout();
825                        if (layout != null) {
826                            if (Clob.class.isAssignableFrom(mapping.getType())) {
827                                this.statement.setClob(j++, new StringReader(layout.toSerializable(event)));
828                            } else if (NClob.class.isAssignableFrom(mapping.getType())) {
829                                this.statement.setNClob(j++, new StringReader(layout.toSerializable(event)));
830                            } else {
831                                final Object value = TypeConverters.convert(layout.toSerializable(event),
832                                        mapping.getType(), null);
833                                setStatementObject(j++, mapping.getNameKey(), value);
834                            }
835                        }
836                    }
837                }
838            }
839            for (final ColumnConfig column : this.columnConfigs) {
840                if (column.isEventTimestamp()) {
841                    this.statement.setTimestamp(j++, new Timestamp(event.getTimeMillis()));
842                } else if (column.isClob()) {
843                    reader = new StringReader(column.getLayout().toSerializable(event));
844                    if (column.isUnicode()) {
845                        this.statement.setNClob(j++, reader);
846                    } else {
847                        this.statement.setClob(j++, reader);
848                    }
849                } else if (column.isUnicode()) {
850                    this.statement.setNString(j++, Objects.toString(
851                            truncate(column.getColumnNameKey(), column.getLayout().toSerializable(event)), null));
852                } else {
853                    this.statement.setString(j++, Objects.toString(
854                            truncate(column.getColumnNameKey(), column.getLayout().toSerializable(event)), null));
855                }
856            }
857
858            if (isBuffered() && this.isBatchSupported) {
859                logger().debug("addBatch for {}", this.statement);
860                this.statement.addBatch();
861            } else {
862                final int executeUpdate = this.statement.executeUpdate();
863                logger().debug("executeUpdate = {} for {}", executeUpdate, this.statement);
864                if (executeUpdate == 0) {
865                    throw new AppenderLoggingException(
866                            "No records inserted in database table for log event in JDBC manager [%s].", fieldsToString());
867                }
868            }
869        } catch (final SQLException e) {
870            throw new DbAppenderLoggingException(e, "Failed to insert record for log event in JDBC manager: %s [%s]", e,
871                    fieldsToString());
872        } finally {
873            // Release ASAP
874            try {
875                // statement can be null when a AppenderLoggingException is thrown at the start of this method
876                if (statement != null) {
877                    statement.clearParameters();
878                }
879            } catch (final SQLException e) {
880                // Ignore
881            }
882            Closer.closeSilently(reader);
883        }
884    }
885
886    @Override
887    protected void writeThrough(final LogEvent event, final Serializable serializable) {
888        this.connectAndStart();
889        try {
890            try {
891                this.writeInternal(event, serializable);
892            } finally {
893                this.commitAndClose();
894            }
895        } catch (final DbAppenderLoggingException e) {
896            reconnectOn(e);
897            try {
898                this.writeInternal(event, serializable);
899            } finally {
900                this.commitAndClose();
901            }
902        }
903    }
904
905}