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
018package org.apache.commons.configuration2;
019
020import java.sql.Clob;
021import java.sql.Connection;
022import java.sql.PreparedStatement;
023import java.sql.ResultSet;
024import java.sql.SQLException;
025import java.sql.Statement;
026import java.util.ArrayList;
027import java.util.Collection;
028import java.util.Iterator;
029import java.util.List;
030
031import javax.sql.DataSource;
032
033import org.apache.commons.configuration2.convert.DisabledListDelimiterHandler;
034import org.apache.commons.configuration2.convert.ListDelimiterHandler;
035import org.apache.commons.configuration2.event.ConfigurationErrorEvent;
036import org.apache.commons.configuration2.event.ConfigurationEvent;
037import org.apache.commons.configuration2.event.EventType;
038import org.apache.commons.configuration2.io.ConfigurationLogger;
039import org.apache.commons.lang3.StringUtils;
040
041/**
042 * Configuration stored in a database. The properties are retrieved from a table containing at least one column for the
043 * keys, and one column for the values. It's possible to store several configurations in the same table by adding a
044 * column containing the name of the configuration. The name of the table and the columns have to be specified using the
045 * corresponding properties.
046 * <p>
047 * The recommended way to create an instance of {@code DatabaseConfiguration} is to use a <em>configuration
048 * builder</em>. The builder is configured with a special parameters object defining the database structures used by the
049 * configuration. Such an object can be created using the {@code database()} method of the {@code Parameters} class. See
050 * the examples below for more details.
051 * </p>
052 *
053 * <p>
054 * <strong>Example 1 - One configuration per table</strong>
055 * </p>
056 *
057 * <pre>
058 * CREATE TABLE myconfig (
059 *     `key`   VARCHAR NOT NULL PRIMARY KEY,
060 *     `value` VARCHAR
061 * );
062 *
063 * INSERT INTO myconfig (key, value) VALUES ('foo', 'bar');
064 *
065 * BasicConfigurationBuilder&lt;DatabaseConfiguration&gt; builder =
066 *     new BasicConfigurationBuilder&lt;DatabaseConfiguration&gt;(DatabaseConfiguration.class);
067 * builder.configure(
068 *     Parameters.database()
069 *         .setDataSource(dataSource)
070 *         .setTable("myconfig")
071 *         .setKeyColumn("key")
072 *         .setValueColumn("value")
073 * );
074 * Configuration config = builder.getConfiguration();
075 * String value = config.getString("foo");
076 * </pre>
077 *
078 * <p>
079 * <strong>Example 2 - Multiple configurations per table</strong>
080 * </p>
081 *
082 * <pre>
083 * CREATE TABLE myconfigs (
084 *     `name`  VARCHAR NOT NULL,
085 *     `key`   VARCHAR NOT NULL,
086 *     `value` VARCHAR,
087 *     CONSTRAINT sys_pk_myconfigs PRIMARY KEY (`name`, `key`)
088 * );
089 *
090 * INSERT INTO myconfigs (name, key, value) VALUES ('config1', 'key1', 'value1');
091 * INSERT INTO myconfigs (name, key, value) VALUES ('config2', 'key2', 'value2');
092 *
093 * BasicConfigurationBuilder&lt;DatabaseConfiguration&gt; builder =
094 *     new BasicConfigurationBuilder&lt;DatabaseConfiguration&gt;(DatabaseConfiguration.class);
095 * builder.configure(
096 *     Parameters.database()
097 *         .setDataSource(dataSource)
098 *         .setTable("myconfigs")
099 *         .setKeyColumn("key")
100 *         .setValueColumn("value")
101 *         .setConfigurationNameColumn("name")
102 *         .setConfigurationName("config1")
103 * );
104 * Configuration config1 = new DatabaseConfiguration(dataSource, "myconfigs", "name", "key", "value", "config1");
105 * String value1 = conf.getString("key1");
106 * </pre>
107 *
108 * The configuration can be instructed to perform commits after database updates. This is achieved by setting the
109 * {@code commits} parameter of the constructors to <b>true</b>. If commits should not be performed (which is the
110 * default behavior), it should be ensured that the connections returned by the {@code DataSource} are in auto-commit
111 * mode.
112 * <p>
113 * <strong>Note: Like JDBC itself, protection against SQL injection is left to the user.</strong>
114 * </p>
115 *
116 * @since 1.0
117 */
118public class DatabaseConfiguration extends AbstractConfiguration {
119    /** Constant for the statement used by getProperty. */
120    private static final String SQL_GET_PROPERTY = "SELECT * FROM %s WHERE %s =?";
121
122    /** Constant for the statement used by isEmpty. */
123    private static final String SQL_IS_EMPTY = "SELECT count(*) FROM %s WHERE 1 = 1";
124
125    /** Constant for the statement used by clearProperty. */
126    private static final String SQL_CLEAR_PROPERTY = "DELETE FROM %s WHERE %s =?";
127
128    /** Constant for the statement used by clear. */
129    private static final String SQL_CLEAR = "DELETE FROM %s WHERE 1 = 1";
130
131    /** Constant for the statement used by getKeys. */
132    private static final String SQL_GET_KEYS = "SELECT DISTINCT %s FROM %s WHERE 1 = 1";
133
134    /** The data source to connect to the database. */
135    private DataSource dataSource;
136
137    /** The configurationName of the table containing the configurations. */
138    private String table;
139
140    /** The column containing the configurationName of the configuration. */
141    private String configurationNameColumn;
142
143    /** The column containing the keys. */
144    private String keyColumn;
145
146    /** The column containing the values. */
147    private String valueColumn;
148
149    /** The configurationName of the configuration. */
150    private String configurationName;
151
152    /** A flag whether commits should be performed by this configuration. */
153    private boolean autoCommit;
154
155    /**
156     * Creates a new instance of {@code DatabaseConfiguration}.
157     */
158    public DatabaseConfiguration() {
159        initLogger(new ConfigurationLogger(DatabaseConfiguration.class));
160        addErrorLogListener();
161    }
162
163    /**
164     * Gets the {@code DataSource} for obtaining database connections.
165     *
166     * @return the {@code DataSource}
167     */
168    public DataSource getDataSource() {
169        return dataSource;
170    }
171
172    /**
173     * Sets the {@code DataSource} for obtaining database connections.
174     *
175     * @param dataSource the {@code DataSource}
176     */
177    public void setDataSource(final DataSource dataSource) {
178        this.dataSource = dataSource;
179    }
180
181    /**
182     * Gets the name of the table containing configuration data.
183     *
184     * @return the name of the table to be queried
185     */
186    public String getTable() {
187        return table;
188    }
189
190    /**
191     * Sets the name of the table containing configuration data.
192     *
193     * @param table the table name
194     */
195    public void setTable(final String table) {
196        this.table = table;
197    }
198
199    /**
200     * Gets the name of the table column with the configuration name.
201     *
202     * @return the name of the configuration name column
203     */
204    public String getConfigurationNameColumn() {
205        return configurationNameColumn;
206    }
207
208    /**
209     * Sets the name of the table column with the configuration name.
210     *
211     * @param configurationNameColumn the name of the column with the configuration name
212     */
213    public void setConfigurationNameColumn(final String configurationNameColumn) {
214        this.configurationNameColumn = configurationNameColumn;
215    }
216
217    /**
218     * Gets the name of the column containing the configuration keys.
219     *
220     * @return the name of the key column
221     */
222    public String getKeyColumn() {
223        return keyColumn;
224    }
225
226    /**
227     * Sets the name of the column containing the configuration keys.
228     *
229     * @param keyColumn the name of the key column
230     */
231    public void setKeyColumn(final String keyColumn) {
232        this.keyColumn = keyColumn;
233    }
234
235    /**
236     * Gets the name of the column containing the configuration values.
237     *
238     * @return the name of the value column
239     */
240    public String getValueColumn() {
241        return valueColumn;
242    }
243
244    /**
245     * Sets the name of the column containing the configuration values.
246     *
247     * @param valueColumn the name of the value column
248     */
249    public void setValueColumn(final String valueColumn) {
250        this.valueColumn = valueColumn;
251    }
252
253    /**
254     * Gets the name of this configuration instance.
255     *
256     * @return the name of this configuration
257     */
258    public String getConfigurationName() {
259        return configurationName;
260    }
261
262    /**
263     * Sets the name of this configuration instance.
264     *
265     * @param configurationName the name of this configuration
266     */
267    public void setConfigurationName(final String configurationName) {
268        this.configurationName = configurationName;
269    }
270
271    /**
272     * Returns a flag whether this configuration performs commits after database updates.
273     *
274     * @return a flag whether commits are performed
275     */
276    public boolean isAutoCommit() {
277        return autoCommit;
278    }
279
280    /**
281     * Sets the auto commit flag. If set to <b>true</b>, this configuration performs a commit after each database update.
282     *
283     * @param autoCommit the auto commit flag
284     */
285    public void setAutoCommit(final boolean autoCommit) {
286        this.autoCommit = autoCommit;
287    }
288
289    /**
290     * Gets the value of the specified property. If this causes a database error, an error event will be generated of
291     * type {@code READ} with the causing exception. The event's {@code propertyName} is set to the passed in property key,
292     * the {@code propertyValue} is undefined.
293     *
294     * @param key the key of the desired property
295     * @return the value of this property
296     */
297    @Override
298    protected Object getPropertyInternal(final String key) {
299        final AbstractJdbcOperation<Object> op = new AbstractJdbcOperation<Object>(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null) {
300            @Override
301            protected Object performOperation() throws SQLException {
302                final List<Object> results = new ArrayList<>();
303                try (ResultSet rs = openResultSet(String.format(SQL_GET_PROPERTY, table, keyColumn), true, key)) {
304                    while (rs.next()) {
305                        // Split value if it contains the list delimiter
306                        getListDelimiterHandler().parse(extractPropertyValue(rs)).forEach(results::add);
307                    }
308                }
309                if (!results.isEmpty()) {
310                    return results.size() > 1 ? results : results.get(0);
311                }
312                return null;
313            }
314        };
315
316        return op.execute();
317    }
318
319    /**
320     * Adds a property to this configuration. If this causes a database error, an error event will be generated of type
321     * {@code ADD_PROPERTY} with the causing exception. The event's {@code propertyName} is set to the passed in property
322     * key, the {@code propertyValue} points to the passed in value.
323     *
324     * @param key the property key
325     * @param obj the value of the property to add
326     */
327    @Override
328    protected void addPropertyDirect(final String key, final Object obj) {
329        new AbstractJdbcOperation<Void>(ConfigurationErrorEvent.WRITE, ConfigurationEvent.ADD_PROPERTY, key, obj) {
330            @Override
331            protected Void performOperation() throws SQLException {
332                final StringBuilder query = new StringBuilder("INSERT INTO ");
333                query.append(table).append(" (");
334                query.append(keyColumn).append(", ");
335                query.append(valueColumn);
336                if (configurationNameColumn != null) {
337                    query.append(", ").append(configurationNameColumn);
338                }
339                query.append(") VALUES (?, ?");
340                if (configurationNameColumn != null) {
341                    query.append(", ?");
342                }
343                query.append(")");
344
345                try (PreparedStatement pstmt = initStatement(query.toString(), false, key, String.valueOf(obj))) {
346                    if (configurationNameColumn != null) {
347                        pstmt.setString(3, configurationName);
348                    }
349
350                    pstmt.executeUpdate();
351                    return null;
352                }
353            }
354        }.execute();
355    }
356
357    /**
358     * Adds a property to this configuration. This implementation temporarily disables list delimiter parsing, so that even
359     * if the value contains the list delimiter, only a single record is written into the managed table. The implementation
360     * of {@code getProperty()} takes care about delimiters. So list delimiters are fully supported by
361     * {@code DatabaseConfiguration}, but internally treated a bit differently.
362     *
363     * @param key the key of the new property
364     * @param value the value to be added
365     */
366    @Override
367    protected void addPropertyInternal(final String key, final Object value) {
368        final ListDelimiterHandler oldHandler = getListDelimiterHandler();
369        try {
370            // temporarily disable delimiter parsing
371            setListDelimiterHandler(DisabledListDelimiterHandler.INSTANCE);
372            super.addPropertyInternal(key, value);
373        } finally {
374            setListDelimiterHandler(oldHandler);
375        }
376    }
377
378    /**
379     * Checks if this configuration is empty. If this causes a database error, an error event will be generated of type
380     * {@code READ} with the causing exception. Both the event's {@code propertyName} and {@code propertyValue} will be
381     * undefined.
382     *
383     * @return a flag whether this configuration is empty.
384     */
385    @Override
386    protected boolean isEmptyInternal() {
387        final AbstractJdbcOperation<Integer> op = new AbstractJdbcOperation<Integer>(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null) {
388            @Override
389            protected Integer performOperation() throws SQLException {
390                try (ResultSet rs = openResultSet(String.format(SQL_IS_EMPTY, table), true)) {
391                    return rs.next() ? Integer.valueOf(rs.getInt(1)) : null;
392                }
393            }
394        };
395
396        final Integer count = op.execute();
397        return count == null || count.intValue() == 0;
398    }
399
400    /**
401     * Checks whether this configuration contains the specified key. If this causes a database error, an error event will be
402     * generated of type {@code READ} with the causing exception. The event's {@code propertyName} will be set to the passed
403     * in key, the {@code propertyValue} will be undefined.
404     *
405     * @param key the key to be checked
406     * @return a flag whether this key is defined
407     */
408    @Override
409    protected boolean containsKeyInternal(final String key) {
410        final AbstractJdbcOperation<Boolean> op = new AbstractJdbcOperation<Boolean>(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, key, null) {
411            @Override
412            protected Boolean performOperation() throws SQLException {
413                try (ResultSet rs = openResultSet(String.format(SQL_GET_PROPERTY, table, keyColumn), true, key)) {
414                    return rs.next();
415                }
416            }
417        };
418
419        final Boolean result = op.execute();
420        return result != null && result.booleanValue();
421    }
422
423    /**
424     * Removes the specified value from this configuration. If this causes a database error, an error event will be
425     * generated of type {@code CLEAR_PROPERTY} with the causing exception. The event's {@code propertyName} will be set to
426     * the passed in key, the {@code propertyValue} will be undefined.
427     *
428     * @param key the key of the property to be removed
429     */
430    @Override
431    protected void clearPropertyDirect(final String key) {
432        new AbstractJdbcOperation<Void>(ConfigurationErrorEvent.WRITE, ConfigurationEvent.CLEAR_PROPERTY, key, null) {
433            @Override
434            protected Void performOperation() throws SQLException {
435                try (PreparedStatement ps = initStatement(String.format(SQL_CLEAR_PROPERTY, table, keyColumn), true, key)) {
436                    ps.executeUpdate();
437                    return null;
438                }
439            }
440        }.execute();
441    }
442
443    /**
444     * Removes all entries from this configuration. If this causes a database error, an error event will be generated of
445     * type {@code CLEAR} with the causing exception. Both the event's {@code propertyName} and the {@code propertyValue}
446     * will be undefined.
447     */
448    @Override
449    protected void clearInternal() {
450        new AbstractJdbcOperation<Void>(ConfigurationErrorEvent.WRITE, ConfigurationEvent.CLEAR, null, null) {
451            @Override
452            protected Void performOperation() throws SQLException {
453                try (PreparedStatement statement = initStatement(String.format(SQL_CLEAR, table), true)) {
454                    statement.executeUpdate();
455                }
456                return null;
457            }
458        }.execute();
459    }
460
461    /**
462     * Returns an iterator with the names of all properties contained in this configuration. If this causes a database
463     * error, an error event will be generated of type {@code READ} with the causing exception. Both the event's
464     * {@code propertyName} and the {@code propertyValue} will be undefined.
465     *
466     * @return an iterator with the contained keys (an empty iterator in case of an error)
467     */
468    @Override
469    protected Iterator<String> getKeysInternal() {
470        final Collection<String> keys = new ArrayList<>();
471        new AbstractJdbcOperation<Collection<String>>(ConfigurationErrorEvent.READ, ConfigurationErrorEvent.READ, null, null) {
472            @Override
473            protected Collection<String> performOperation() throws SQLException {
474                try (ResultSet rs = openResultSet(String.format(SQL_GET_KEYS, keyColumn, table), true)) {
475                    while (rs.next()) {
476                        keys.add(rs.getString(1));
477                    }
478                    return keys;
479                }
480            }
481        }.execute();
482
483        return keys.iterator();
484    }
485
486    /**
487     * Gets the used {@code DataSource} object.
488     *
489     * @return the data source
490     * @since 1.4
491     */
492    public DataSource getDatasource() {
493        return dataSource;
494    }
495
496    /**
497     * Close the specified database objects. Avoid closing if null and hide any SQLExceptions that occur.
498     *
499     * @param conn The database connection to close
500     * @param stmt The statement to close
501     * @param rs the result set to close
502     */
503    protected void close(final Connection conn, final Statement stmt, final ResultSet rs) {
504        try {
505            if (rs != null) {
506                rs.close();
507            }
508        } catch (final SQLException e) {
509            getLogger().error("An error occurred on closing the result set", e);
510        }
511
512        try {
513            if (stmt != null) {
514                stmt.close();
515            }
516        } catch (final SQLException e) {
517            getLogger().error("An error occurred on closing the statement", e);
518        }
519
520        try {
521            if (conn != null) {
522                conn.close();
523            }
524        } catch (final SQLException e) {
525            getLogger().error("An error occurred on closing the connection", e);
526        }
527    }
528
529    /**
530     * Extracts the value of a property from the given result set. The passed in {@code ResultSet} was created by a SELECT
531     * statement on the underlying database table. This implementation reads the value of the column determined by the
532     * {@code valueColumn} property. Normally the contained value is directly returned. However, if it is of type
533     * {@code CLOB}, text is extracted as string.
534     *
535     * @param rs the current {@code ResultSet}
536     * @return the value of the property column
537     * @throws SQLException if an error occurs
538     */
539    protected Object extractPropertyValue(final ResultSet rs) throws SQLException {
540        Object value = rs.getObject(valueColumn);
541        if (value instanceof Clob) {
542            value = convertClob((Clob) value);
543        }
544        return value;
545    }
546
547    /**
548     * Converts a CLOB to a string.
549     *
550     * @param clob the CLOB to be converted
551     * @return the extracted string value
552     * @throws SQLException if an error occurs
553     */
554    private static Object convertClob(final Clob clob) throws SQLException {
555        final int len = (int) clob.length();
556        return len > 0 ? clob.getSubString(1, len) : StringUtils.EMPTY;
557    }
558
559    /**
560     * An internally used helper class for simplifying database access through plain JDBC. This class provides a simple
561     * framework for creating and executing a JDBC statement. It especially takes care of proper handling of JDBC resources
562     * even in case of an error.
563     *
564     * @param <T> the type of the results produced by a JDBC operation
565     */
566    private abstract class AbstractJdbcOperation<T> {
567        /** Stores the connection. */
568        private Connection conn;
569
570        /** Stores the statement. */
571        private PreparedStatement pstmt;
572
573        /** Stores the result set. */
574        private ResultSet resultSet;
575
576        /** The type of the event to send in case of an error. */
577        private final EventType<? extends ConfigurationErrorEvent> errorEventType;
578
579        /** The type of the operation which caused an error. */
580        private final EventType<?> operationEventType;
581
582        /** The property configurationName for an error event. */
583        private final String errorPropertyName;
584
585        /** The property value for an error event. */
586        private final Object errorPropertyValue;
587
588        /**
589         * Creates a new instance of {@code JdbcOperation} and initializes the properties related to the error event.
590         *
591         * @param errEvType the type of the error event
592         * @param opType the operation event type
593         * @param errPropName the property configurationName for the error event
594         * @param errPropVal the property value for the error event
595         */
596        protected AbstractJdbcOperation(final EventType<? extends ConfigurationErrorEvent> errEvType, final EventType<?> opType, final String errPropName,
597            final Object errPropVal) {
598            errorEventType = errEvType;
599            operationEventType = opType;
600            errorPropertyName = errPropName;
601            errorPropertyValue = errPropVal;
602        }
603
604        /**
605         * Executes this operation. This method obtains a database connection and then delegates to {@code performOperation()}.
606         * Afterwards it performs the necessary clean up. Exceptions that are thrown during the JDBC operation are caught and
607         * transformed into configuration error events.
608         *
609         * @return the result of the operation
610         */
611        public T execute() {
612            T result = null;
613
614            if (getDatasource() != null) {
615                try {
616                    conn = getDatasource().getConnection();
617                    result = performOperation();
618
619                    if (isAutoCommit()) {
620                        conn.commit();
621                    }
622                } catch (final SQLException e) {
623                    fireError(errorEventType, operationEventType, errorPropertyName, errorPropertyValue, e);
624                } finally {
625                    close(conn, pstmt, resultSet);
626                }
627            }
628
629            return result;
630        }
631
632        /**
633         * Gets the current connection. This method can be called while {@code execute()} is running. It returns <b>null</b>
634         * otherwise.
635         *
636         * @return the current connection
637         */
638        protected Connection getConnection() {
639            return conn;
640        }
641
642        /**
643         * Creates a {@code PreparedStatement} object for executing the specified SQL statement.
644         *
645         * @param sql the statement to be executed
646         * @param nameCol a flag whether the configurationName column should be taken into account
647         * @return the prepared statement object
648         * @throws SQLException if an SQL error occurs
649         */
650        protected PreparedStatement createStatement(final String sql, final boolean nameCol) throws SQLException {
651            final String statement;
652            if (nameCol && configurationNameColumn != null) {
653                final StringBuilder buf = new StringBuilder(sql);
654                buf.append(" AND ").append(configurationNameColumn).append("=?");
655                statement = buf.toString();
656            } else {
657                statement = sql;
658            }
659
660            pstmt = getConnection().prepareStatement(statement);
661            return pstmt;
662        }
663
664        /**
665         * Creates an initializes a {@code PreparedStatement} object for executing an SQL statement. This method first calls
666         * {@code createStatement()} for creating the statement and then initializes the statement's parameters.
667         *
668         * @param sql the statement to be executed
669         * @param nameCol a flag whether the configurationName column should be taken into account
670         * @param params the parameters for the statement
671         * @return the initialized statement object
672         * @throws SQLException if an SQL error occurs
673         */
674        protected PreparedStatement initStatement(final String sql, final boolean nameCol, final Object... params) throws SQLException {
675            final PreparedStatement ps = createStatement(sql, nameCol);
676
677            int idx = 1;
678            for (final Object param : params) {
679                ps.setObject(idx++, param);
680            }
681            if (nameCol && configurationNameColumn != null) {
682                ps.setString(idx, configurationName);
683            }
684
685            return ps;
686        }
687
688        /**
689         * Creates a {@code PreparedStatement} for a query, initializes it and executes it. The resulting {@code ResultSet} is
690         * returned.
691         *
692         * @param sql the statement to be executed
693         * @param nameCol a flag whether the configurationName column should be taken into account
694         * @param params the parameters for the statement
695         * @return the {@code ResultSet} produced by the query
696         * @throws SQLException if an SQL error occurs
697         */
698        protected ResultSet openResultSet(final String sql, final boolean nameCol, final Object... params) throws SQLException {
699            return resultSet = initStatement(sql, nameCol, params).executeQuery();
700        }
701
702        /**
703         * Performs the JDBC operation. This method is called by {@code execute()} after this object has been fully initialized.
704         * Here the actual JDBC logic has to be placed.
705         *
706         * @return the result of the operation
707         * @throws SQLException if an SQL error occurs
708         */
709        protected abstract T performOperation() throws SQLException;
710    }
711}