View Javadoc

1   /*
2    * Copyright 2012-2013 smartics, Kronseder & Reiner GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package de.smartics.properties.spi.config.ds;
17  
18  import java.io.PrintStream;
19  import java.sql.Connection;
20  import java.sql.PreparedStatement;
21  import java.sql.ResultSet;
22  import java.sql.SQLException;
23  import java.sql.Statement;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Map.Entry;
28  
29  import org.apache.commons.lang.StringUtils;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  import de.smartics.util.lang.Arg;
34  
35  /**
36   * Helper to create database configurations with a data source.
37   */
38  public class DefaultDataSourceManager extends AbstractDataSourceDescriptor
39      implements PropertiesDataSourceManager
40  { // NOPMD
41    // ********************************* Fields *********************************
42  
43    // --- constants ------------------------------------------------------------
44  
45    /**
46     * The class version identifier.
47     */
48    private static final long serialVersionUID = 1L;
49  
50    /**
51     * Reference to the logger for this class.
52     */
53    private static final Logger LOG = LoggerFactory
54        .getLogger(DefaultDataSourceManager.class);
55  
56    // --- members --------------------------------------------------------------
57  
58    /**
59     * The proxy to provide access to a data source.
60     *
61     * @serial
62     */
63    private final DataSourceProxy dataSourceProxy;
64  
65    /**
66     * The properties to fill the database initially. May be <code>null</code>.
67     *
68     * @serial
69     */
70    private final Map<String, Map<String, String>> initialProperties;
71  
72    /**
73     * Signals to drop the properties table before creation.
74     *
75     * @serial
76     */
77    protected final boolean dropTable;
78  
79    /**
80     * The flag signals that any problems encountered on creating the table should
81     * be ignored. Useful if there are no means to check is a database table
82     * already exists.
83     *
84     * @serial
85     */
86    protected final boolean ignoreTableCreationProblems;
87  
88    /**
89     * The SQL statement to create the configuration table.
90     */
91    private final String createTableSqlStatement;
92  
93    /**
94     * The SQL statement to insert or update a value in the table, dependent on if
95     * the value is already in the database or not.
96     */
97    private final String insertOrUpdateSqlStatementTemplate;
98  
99    // ****************************** Initializer *******************************
100 
101   // ****************************** Constructors ******************************
102 
103   /**
104    * Default constructor.
105    *
106    * @param builder the builder of manager instance.
107    */
108   protected DefaultDataSourceManager(final Builder builder)
109   {
110     super(builder);
111 
112     this.dataSourceProxy = builder.dataSourceProxy;
113     this.initialProperties = builder.initialProperties;
114     this.dropTable = builder.dropTable;
115     this.ignoreTableCreationProblems = builder.ignoreTableCreationProblems;
116     this.createTableSqlStatement =
117         filterStatement(builder.createTableSqlStatementTemplate);
118     this.insertOrUpdateSqlStatementTemplate =
119         filterStatement(builder.insertOrUpdateSqlStatementTemplate);
120   }
121 
122   // ****************************** Inner Classes *****************************
123 
124   /**
125    * The builder of manager instances.
126    */
127   public static class Builder extends AbstractDataSourceDescriptor.Builder
128   {
129     // ******************************** Fields ********************************
130 
131     // --- constants ----------------------------------------------------------
132 
133     /**
134      * The default table template.
135      * <p>
136      * The value of this constant is {@value}.
137      * </p>
138      */
139     public static final String DEFAULT_CREATE_TABLE_TEMPLATE =
140         "CREATE TABLE IF NOT EXISTS ${table}"
141             + " (${configColumn} VARCHAR(128) NOT NULL,"
142             + " ${nameColumn}    VARCHAR(64) NOT NULL,"
143             + " ${valueColumn}   VARCHAR(255),"
144             + " CONSTRAINT prime UNIQUE (${configColumn}, ${nameColumn}))";
145 
146     // --- members ------------------------------------------------------------
147 
148     /**
149      * The proxy to provide access to a data source.
150      */
151     protected DataSourceProxy dataSourceProxy;
152 
153     /**
154      * The properties to fill the database initially. May be <code>null</code>.
155      */
156     private Map<String, Map<String, String>> initialProperties;
157 
158     /**
159      * Signals to drop the properties table before creation.
160      */
161     private boolean dropTable = true;
162 
163     /**
164      * The flag signals that any problems encountered on creating the table
165      * should be ignored. Useful if there are no means to check is a database
166      * table already exists.
167      */
168     private boolean ignoreTableCreationProblems = true;
169 
170     /**
171      * The template to create an SQL statement to create the configuration
172      * table. Use <code>${table}</code>, <code>${configColumn}</code>,
173      * <code>${nameColumn}</code> and <code>${valueColumn}</code> to reference
174      * the name of the configuration table, the column that stores the property
175      * names and the column storing the property values.
176      */
177     private String createTableSqlStatementTemplate =
178         DEFAULT_CREATE_TABLE_TEMPLATE;
179 
180     /**
181      * The template to create an SQL statement to insert or update a value in
182      * the table, dependent on if the value is already in the database or not.
183      * Use <code>${table}</code>, <code>${configColumn}</code>,
184      * <code>${nameColumn}</code> and <code>${valueColumn}</code> to reference
185      * the name of the configuration table, the column that stores the property
186      * names and the column storing the property values.
187      */
188     protected String insertOrUpdateSqlStatementTemplate;
189 
190     // ***************************** Initializer ******************************
191 
192     // ***************************** Constructors *****************************
193 
194     // ***************************** Inner Classes ****************************
195 
196     // ******************************** Methods *******************************
197 
198     // --- init ---------------------------------------------------------------
199 
200     // --- get&set ------------------------------------------------------------
201 
202     /**
203      * Sets the template to create an SQL statement to create the configuration
204      * table. Use <code>${table}</code>, <code>${configColumn}</code>,
205      * <code>${nameColumn}</code> and <code>${valueColumn}</code> to reference
206      * the name of the configuration table, the column that stores the property
207      * names and the column storing the property values.
208      * <p>
209      * Per default the {@link #DEFAULT_CREATE_TABLE_TEMPLATE default table
210      * template} is used.
211      * </p>
212      *
213      * @param createTableSqlStatementTemplate the template to create an SQL
214      *          statement to create the configuration table.
215      * @see #DEFAULT_CREATE_TABLE_TEMPLATE
216      */
217     public final void setCreateTableSqlStatementTemplate(
218         final String createTableSqlStatementTemplate)
219     {
220       this.createTableSqlStatementTemplate = createTableSqlStatementTemplate;
221     }
222 
223     /**
224      * Set the template to insert or update a value in the configuration table.
225      *
226      * @param insertOrUpdateSqlStatementTemplate the template to set.
227      */
228     public void setInsertOrUpdateSqlStatementTemplate(
229         final String insertOrUpdateSqlStatementTemplate)
230     {
231       this.insertOrUpdateSqlStatementTemplate =
232           insertOrUpdateSqlStatementTemplate;
233     }
234 
235     /**
236      * Sets the proxy to provide access to a data source.
237      *
238      * @param dataSourceProxy the proxy to provide access to a data source.
239      * @throws NullPointerException of {@code dataSourceProxy} is
240      *           <code>null</code>.
241      */
242     public final void setDataSourceProxy(final DataSourceProxy dataSourceProxy)
243       throws NullPointerException
244     {
245       this.dataSourceProxy =
246           Arg.checkNotNull("dataSourceProxy", dataSourceProxy);
247     }
248 
249     /**
250      * Sets the properties to fill the database initially. May be
251      * <code>null</code>.
252      *
253      * @param initialProperties the properties to fill the database initially.
254      */
255     public final void setInitialProperties(
256         final Map<String, Map<String, String>> initialProperties)
257     {
258       this.initialProperties = initialProperties;
259     }
260 
261     /**
262      * Sets the value for dropTable.
263      * <p>
264      * Signals to drop the properties table before creation.
265      *
266      * @param dropTable the value for dropTable.
267      */
268     public final void setDropTable(final boolean dropTable)
269     {
270       this.dropTable = dropTable;
271     }
272 
273     /**
274      * Sets the flag signals that any problems encountered on creating the table
275      * should be ignored. Useful if there are no means to check is a database
276      * table already exists.
277      *
278      * @param ignoreTableCreationProblems the flag signals that any problems
279      *          encountered on creating the table should be ignored.
280      */
281     public final void setIgnoreTableCreationProblems(
282         final boolean ignoreTableCreationProblems)
283     {
284       this.ignoreTableCreationProblems = ignoreTableCreationProblems;
285     }
286 
287     // --- business -----------------------------------------------------------
288 
289     /**
290      * Creates the instance.
291      *
292      * @return the created instance.
293      * @throws NullPointerException if no data source proxy has been provided.
294      */
295     public DefaultDataSourceManager build() throws NullPointerException
296     {
297       Arg.checkNotNull("dataSourceProxy", dataSourceProxy);
298       Arg.checkNotNull("insertOrUpdateSqlStatementTemplate",
299           insertOrUpdateSqlStatementTemplate);
300 
301       return new DefaultDataSourceManager(this);
302     }
303 
304     // --- object basics ------------------------------------------------------
305   }
306 
307   // ********************************* Methods ********************************
308 
309   // --- init -----------------------------------------------------------------
310 
311   private String filterStatement(final String template)
312   {
313     if (template == null)
314     {
315       return null;
316     }
317 
318     final String sql =
319         StringUtils.replaceEach(template, new String[] { "${table}",
320                                                         "${configColumn}",
321                                                         "${nameColumn}",
322                                                         "${valueColumn}" },
323             new String[] { getTable(), getConfigColumn(), getNameColumn(),
324                           getValueColumn() });
325     return sql;
326   }
327 
328   // --- get&set --------------------------------------------------------------
329 
330   @Override
331   public final String getDataSourceId()
332   {
333     return dataSourceProxy.getDataSourceId();
334   }
335 
336   @Override
337   public final DataSourceProxy getDataSourceProxy()
338   {
339     return dataSourceProxy;
340   }
341 
342   /**
343    * Returns the SQL statement to insert or update a value in the table,
344    * dependent on if the value is already in the database or not.
345    *
346    * @return the SQL statement to insert or update a value in the table,
347    *         dependent on if the value is already in the database or not.
348    */
349   public String getInsertOrUpdateSqlStatementTemplate()
350   {
351     return insertOrUpdateSqlStatementTemplate;
352   }
353 
354   // --- business -------------------------------------------------------------
355 
356   @Override
357   public final void createConfigTable() throws DataSourceException
358   {
359     try
360     {
361       initDataSource();
362     }
363     catch (final SQLException e)
364     {
365       throw new DataSourceException(new DataSourceMessageBean(
366           DataSourceCode.CANNOT_CREATE_TABLE, e, getDataSourceId()));
367     }
368   }
369 
370   private void initDataSource() throws SQLException
371   {
372     if (createTableSqlStatement != null)
373     {
374       if (dropTable)
375       {
376         dropConfigTable();
377       }
378 
379       initTables();
380     }
381   }
382 
383   private void initTables() throws SQLException
384   {
385     final Connection connection =
386         dataSourceProxy.getDataSource().getConnection();
387 
388     try
389     {
390       if (!initializedTableExists(connection))
391       {
392         initTable(connection);
393 
394         initProperties(connection);
395       }
396     }
397     finally
398     {
399       connection.close();
400     }
401   }
402 
403   private boolean initializedTableExists(final Connection connection)
404     throws SQLException
405   {
406     final Statement statement = connection.createStatement();
407     try
408     {
409 
410       final String querySql = "SELECT COUNT(*) FROM " + getTable();
411       final ResultSet resultSet = statement.executeQuery(querySql);
412       if (resultSet.next())
413       {
414         final int count = resultSet.getInt(1);
415         return count > 0;
416       }
417 
418       return false;
419     }
420     catch (final SQLException e)
421     {
422       return false;
423     }
424     finally
425     {
426       statement.close();
427     }
428   }
429 
430   private void initTable(final Connection connection) throws SQLException
431   {
432     try
433     {
434       execute(connection, createTableSqlStatement);
435     }
436     catch (final SQLException e)
437     {
438       if (!ignoreTableCreationProblems)
439       {
440         throw e;
441       }
442     }
443   }
444 
445   private void initProperties(final Connection connection) throws SQLException
446   {
447     if (initialProperties != null && !initialProperties.isEmpty())
448     {
449       final String sql = "INSERT INTO " + getTable() + " VALUES (?, ?, ?)";
450       final PreparedStatement statement = connection.prepareStatement(sql);
451       for (final Entry<String, Map<String, String>> configEntry : initialProperties
452           .entrySet())
453       {
454         final String configName = configEntry.getKey();
455         final Map<String, String> config = configEntry.getValue();
456 
457         for (final Entry<String, String> property : config.entrySet())
458         {
459           statement.setString(1, configName);
460           statement.setString(2, property.getKey());
461           statement.setString(3, property.getValue());
462           statement.execute();
463         }
464       }
465 
466       statement.close();
467     }
468   }
469 
470   private void execute(final Connection connection, final String createTableSql)
471     throws SQLException
472   {
473     final Statement statement = connection.createStatement();
474     try
475     {
476       statement.executeUpdate(createTableSql);
477     }
478     finally
479     {
480       statement.close();
481     }
482   }
483 
484   @Override
485   public final void dropConfigTable() throws DataSourceException
486   {
487     try
488     {
489       final Connection connection =
490           dataSourceProxy.getDataSource().getConnection();
491 
492       try
493       {
494         final Statement statement = connection.createStatement();
495 
496         final String sql = "DROP TABLE " + getTable();
497         statement.executeUpdate(sql);
498       }
499       catch (final SQLException e)
500       {
501         LOG.warn("Dropping failed:  " + e.getMessage());
502       }
503       finally
504       {
505         connection.close();
506       }
507     }
508     catch (final SQLException e)
509     {
510       throw new DataSourceException(new DataSourceMessageBean(
511           DataSourceCode.CANNOT_DROP_TABLE, e, getDataSourceId()));
512     }
513   }
514 
515   @Override
516   public final void print(final PrintStream out) throws DataSourceException
517   {
518     try
519     {
520       final Connection connection =
521           dataSourceProxy.getDataSource().getConnection();
522 
523       try
524       {
525         final Statement statement = connection.createStatement();
526 
527         final String querySql =
528             "SELECT " + getConfigColumn() + ", " + getNameColumn() + ", "
529                 + getValueColumn() + " FROM " + getTable();
530         final ResultSet resultSet = statement.executeQuery(querySql);
531         while (resultSet.next())
532         {
533           final String config = resultSet.getString(1);
534           final String name = resultSet.getString(2);
535           final String value = resultSet.getString(3);
536 
537           out.println("  " + config + " | " + name + "=" + value);
538         }
539       }
540       finally
541       {
542         connection.close();
543       }
544     }
545     catch (final SQLException e)
546     {
547       throw new DataSourceException(new DataSourceMessageBean(
548           DataSourceCode.CANNOT_PRINT_TABLE, e, getDataSourceId()));
549     }
550   }
551 
552   @Override
553   public final List<String> getConfigurationKeys() throws DataSourceException
554   {
555     try
556     {
557       final Connection connection =
558           dataSourceProxy.getDataSource().getConnection();
559       final List<String> keys = new ArrayList<String>(32);
560 
561       try
562       {
563         final Statement statement = connection.createStatement();
564 
565         final String sqlKey = getConfigColumn();
566         final String querySql =
567             "SELECT DISTINCT " + sqlKey + " FROM " + getTable();
568         final ResultSet resultSet = statement.executeQuery(querySql);
569         while (resultSet.next())
570         {
571           final String key = resultSet.getString(sqlKey);
572           keys.add(key);
573         }
574       }
575       finally
576       {
577         connection.close();
578       }
579 
580       return keys;
581     }
582     catch (final SQLException e)
583     {
584       throw new DataSourceException(
585           new DataSourceMessageBean(
586               DataSourceCode.CANNOT_ACCESS_CONFIGURATION_KEYS, e,
587               getDataSourceId()));
588     }
589   }
590 
591   // --- object basics --------------------------------------------------------
592 
593   /**
594    * Returns the string representation of the object.
595    *
596    * @return the string representation of the object.
597    */
598   @Override
599   public String toString()
600   {
601     final StringBuilder buffer = new StringBuilder();
602 
603     buffer.append(dataSourceProxy);
604 
605     return buffer.toString();
606   }
607 }