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     */
017    package org.apache.logging.log4j.core.appender.db.jdbc;
018    
019    import java.io.StringReader;
020    import java.sql.Connection;
021    import java.sql.PreparedStatement;
022    import java.sql.SQLException;
023    import java.sql.Timestamp;
024    import java.util.ArrayList;
025    import java.util.List;
026    
027    import org.apache.logging.log4j.core.LogEvent;
028    import org.apache.logging.log4j.core.appender.ManagerFactory;
029    import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager;
030    import org.apache.logging.log4j.core.layout.PatternLayout;
031    
032    /**
033     * An {@link AbstractDatabaseManager} implementation for relational databases accessed via JDBC.
034     */
035    public final class JDBCDatabaseManager extends AbstractDatabaseManager {
036        private static final JDBCDatabaseManagerFactory FACTORY = new JDBCDatabaseManagerFactory();
037    
038        private final List<Column> columns;
039        private final ConnectionSource connectionSource;
040        private final String sqlStatement;
041    
042        private Connection connection;
043        private PreparedStatement statement;
044    
045        private JDBCDatabaseManager(final String name, final int bufferSize, final ConnectionSource connectionSource,
046                                    final String sqlStatement, final List<Column> columns) {
047            super(name, bufferSize);
048            this.connectionSource = connectionSource;
049            this.sqlStatement = sqlStatement;
050            this.columns = columns;
051        }
052    
053        @Override
054        protected void connectInternal() {
055            try {
056                this.connection = this.connectionSource.getConnection();
057                this.statement = this.connection.prepareStatement(this.sqlStatement);
058            } catch (final SQLException e) {
059                LOGGER.error("Failed to connect to relational database using JDBC connection source [{}] in manager [{}].",
060                        this.connectionSource, this.getName(), e);
061            }
062        }
063    
064        @Override
065        protected void disconnectInternal() {
066            try {
067                if (this.statement != null && !this.statement.isClosed()) {
068                    this.statement.close();
069                }
070            } catch (final SQLException e) {
071                LOGGER.warn("Error while closing prepared statement in database manager [{}].", this.getName(), e);
072            }
073    
074            try {
075                if (this.connection != null && !this.connection.isClosed()) {
076                    this.connection.close();
077                }
078            } catch (final SQLException e) {
079                LOGGER.warn("Error while disconnecting from relational database in manager [{}].", this.getName(), e);
080            }
081        }
082    
083        @Override
084        protected void writeInternal(final LogEvent event) {
085            StringReader reader = null;
086            try {
087                if (!this.isConnected() || this.connection == null || this.connection.isClosed()) {
088                    LOGGER.error("Cannot write logging event; manager [{}] not connected to the database.", this.getName());
089                    return;
090                }
091    
092                int i = 1;
093                for (final Column column : this.columns) {
094                    if (column.isEventTimestamp) {
095                        this.statement.setTimestamp(i++, new Timestamp(event.getMillis()));
096                    } else {
097                        if (column.isClob) {
098                            reader = new StringReader(column.layout.toSerializable(event));
099                            if (column.isUnicode) {
100                                this.statement.setNClob(i++, reader);
101                            } else {
102                                this.statement.setClob(i++, reader);
103                            }
104                        } else {
105                            if (column.isUnicode) {
106                                this.statement.setNString(i++, column.layout.toSerializable(event));
107                            } else {
108                                this.statement.setString(i++, column.layout.toSerializable(event));
109                            }
110                        }
111                    }
112                }
113    
114                if (this.statement.executeUpdate() == 0) {
115                    LOGGER.warn("No records inserted in database table for log event in manager [{}].", this.getName());
116                }
117            } catch (final SQLException e) {
118                LOGGER.error("Failed to insert record for log event in manager [{}].", this.getName(), e);
119            } finally {
120                if (reader != null) {
121                    reader.close();
122                }
123            }
124        }
125    
126        /**
127         * Creates a JDBC manager for use within the {@link JDBCAppender}, or returns a suitable one if it already exists.
128         *
129         * @param name The name of the manager, which should include connection details and hashed passwords where possible.
130         * @param bufferSize The size of the log event buffer.
131         * @param connectionSource The source for connections to the database.
132         * @param tableName The name of the database table to insert log events into.
133         * @param columnConfigs Configuration information about the log table columns.
134         * @return a new or existing JDBC manager as applicable.
135         */
136        public static JDBCDatabaseManager getJDBCDatabaseManager(final String name, final int bufferSize,
137                                                                 final ConnectionSource connectionSource,
138                                                                 final String tableName,
139                                                                 final ColumnConfig[] columnConfigs) {
140    
141            return AbstractDatabaseManager.getManager(
142                    name, new FactoryData(bufferSize, connectionSource, tableName, columnConfigs), FACTORY
143            );
144        }
145    
146        /**
147         * Encapsulates data that {@link JDBCDatabaseManagerFactory} uses to create managers.
148         */
149        private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData {
150            private final ColumnConfig[] columnConfigs;
151            private final ConnectionSource connectionSource;
152            private final String tableName;
153    
154            protected FactoryData(final int bufferSize, final ConnectionSource connectionSource, final String tableName,
155                                  final ColumnConfig[] columnConfigs) {
156                super(bufferSize);
157                this.connectionSource = connectionSource;
158                this.tableName = tableName;
159                this.columnConfigs = columnConfigs;
160            }
161        }
162    
163        /**
164         * Creates managers.
165         */
166        private static final class JDBCDatabaseManagerFactory implements ManagerFactory<JDBCDatabaseManager, FactoryData> {
167            @Override
168            public JDBCDatabaseManager createManager(final String name, final FactoryData data) {
169                final StringBuilder columnPart = new StringBuilder();
170                final StringBuilder valuePart = new StringBuilder();
171                final List<Column> columns = new ArrayList<Column>();
172                int i = 0;
173                for (final ColumnConfig config : data.columnConfigs) {
174                    if (i++ > 0) {
175                        columnPart.append(',');
176                        valuePart.append(',');
177                    }
178    
179                    columnPart.append(config.getColumnName());
180    
181                    if (config.getLiteralValue() != null) {
182                        valuePart.append(config.getLiteralValue());
183                    } else {
184                        columns.add(new Column(
185                                config.getLayout(), config.isEventTimestamp(), config.isUnicode(), config.isClob()
186                        ));
187                        valuePart.append('?');
188                    }
189                }
190    
191                final String sqlStatement = "INSERT INTO " + data.tableName + " (" + columnPart + ") VALUES (" +
192                        valuePart + ")";
193    
194                return new JDBCDatabaseManager(name, data.getBufferSize(), data.connectionSource, sqlStatement, columns);
195            }
196        }
197    
198        /**
199         * Encapsulates information about a database column and how to persist data to it.
200         */
201        private static final class Column {
202            private final PatternLayout layout;
203            private final boolean isEventTimestamp;
204            private final boolean isUnicode;
205            private final boolean isClob;
206    
207            private Column(final PatternLayout layout, final boolean isEventDate, final boolean isUnicode,
208                           final boolean isClob) {
209                this.layout = layout;
210                this.isEventTimestamp = isEventDate;
211                this.isUnicode = isUnicode;
212                this.isClob = isClob;
213            }
214        }
215    }