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.Timestamp;
029import java.sql.Types;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Date;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036import java.util.Objects;
037import java.util.concurrent.CountDownLatch;
038
039import org.apache.logging.log4j.core.Layout;
040import org.apache.logging.log4j.core.LogEvent;
041import org.apache.logging.log4j.core.StringLayout;
042import org.apache.logging.log4j.core.appender.AppenderLoggingException;
043import org.apache.logging.log4j.core.appender.ManagerFactory;
044import org.apache.logging.log4j.core.appender.db.AbstractDatabaseAppender;
045import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
046import org.apache.logging.log4j.core.appender.db.ColumnMapping;
047import org.apache.logging.log4j.core.appender.db.DbAppenderLoggingException;
048import org.apache.logging.log4j.core.config.plugins.convert.DateTypeConverter;
049import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
050import org.apache.logging.log4j.core.util.Closer;
051import org.apache.logging.log4j.core.util.Log4jThread;
052import org.apache.logging.log4j.message.MapMessage;
053import org.apache.logging.log4j.spi.ThreadContextMap;
054import org.apache.logging.log4j.spi.ThreadContextStack;
055import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
056import org.apache.logging.log4j.util.ReadOnlyStringMap;
057import org.apache.logging.log4j.util.Strings;
058
059/**
060 * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC.
061 */
062public final class JdbcDatabaseManager extends AbstractDatabaseManager {
063
064    /**
065     * Encapsulates data that {@link JdbcDatabaseManagerFactory} uses to create managers.
066     */
067    private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData {
068        private final ConnectionSource connectionSource;
069        private final String tableName;
070        private final ColumnConfig[] columnConfigs;
071        private final ColumnMapping[] columnMappings;
072        private final boolean immediateFail;
073        private final boolean retry;
074        private final long reconnectIntervalMillis;
075        private final boolean truncateStrings;
076
077        protected FactoryData(final int bufferSize, final Layout<? extends Serializable> layout,
078                final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs,
079                final ColumnMapping[] columnMappings, final boolean immediateFail, final long reconnectIntervalMillis,
080                final boolean truncateStrings) {
081            super(bufferSize, layout);
082            this.connectionSource = connectionSource;
083            this.tableName = tableName;
084            this.columnConfigs = columnConfigs;
085            this.columnMappings = columnMappings;
086            this.immediateFail = immediateFail;
087            this.retry = reconnectIntervalMillis > 0;
088            this.reconnectIntervalMillis = reconnectIntervalMillis;
089            this.truncateStrings = truncateStrings;
090        }
091
092        @Override
093        public String toString() {
094            return String.format(
095                    "FactoryData [connectionSource=%s, tableName=%s, columnConfigs=%s, columnMappings=%s, immediateFail=%s, retry=%s, reconnectIntervalMillis=%s, truncateStrings=%s]",
096                    connectionSource, tableName, Arrays.toString(columnConfigs), Arrays.toString(columnMappings),
097                    immediateFail, retry, reconnectIntervalMillis, truncateStrings);
098        }
099    }
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}