1   /*
2    * Copyright 2003 - 2013 The eFaps Team
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   * Revision:        $Rev$
17   * Last Changed:    $Date$
18   * Last Changed By: $Author$
19   */
20  
21  package org.efaps.db.databases;
22  
23  import java.sql.Connection;
24  import java.sql.DatabaseMetaData;
25  import java.sql.PreparedStatement;
26  import java.sql.ResultSet;
27  import java.sql.ResultSetMetaData;
28  import java.sql.SQLException;
29  import java.sql.Statement;
30  import java.util.Map;
31  
32  import org.apache.commons.dbutils.BasicRowProcessor;
33  import org.apache.commons.dbutils.RowProcessor;
34  import org.efaps.db.databases.information.TableInformation;
35  import org.joda.time.ReadableDateTime;
36  import org.joda.time.format.ISODateTimeFormat;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
41  
42  /**
43   * Database class for the PostgreSQL database.
44   *
45   * @author The eFaps Team
46   * @version $Id$
47   */
48  public class PostgreSQLDatabase
49      extends AbstractDatabase<PostgreSQLDatabase>
50  {
51      /**
52       * Logging instance used in this class.
53       */
54      private static final Logger LOG = LoggerFactory.getLogger(PostgreSQLDatabase.class);
55  
56      /**
57       * Select statement to select all unique keys for current logged in
58       * PostgreSQL database user.
59       *
60       * @see #initTableInfoUniqueKeys(Connection, String, Map)
61       */
62      private static final String SQL_UNIQUE_KEYS = "select "
63              + "a.constraint_name as INDEX_NAME, "
64              + "a.table_name as TABLE_NAME, "
65              + "b.column_name as COLUMN_NAME, "
66              + "b.ordinal_position as ORDINAL_POSITION "
67          + "from "
68              + "information_schema.table_constraints a,"
69              + "information_schema.key_column_usage b "
70          + "where "
71              + "a.constraint_type='UNIQUE' "
72              + "and a.table_schema=b.table_schema "
73              + "and a.table_name=b.table_name "
74              + "and a.constraint_name=b.constraint_name";
75  
76      /**
77       * Select statement for all foreign keys for current logged in PostgreSQL
78       * database user.
79       *
80       * @see #initTableInfoForeignKeys(Connection, String, Map)
81       */
82      private static final String SQL_FOREIGN_KEYS = "select "
83              + "a.table_name as TABLE_NAME, "
84              + "a.constraint_name as FK_NAME, "
85              + "b.column_name as FKCOLUMN_NAME, "
86              + "case "
87                      + "when c.delete_rule='NO ACTION' then '" + DatabaseMetaData.importedKeyNoAction + "' "
88                      + "when c.delete_rule='CASCASE' then '" + DatabaseMetaData.importedKeyCascade + "' "
89                      + "else '' end as DELETE_RULE, "
90              + "d.table_name as PKTABLE_NAME, "
91              + "d.column_name as PKCOLUMN_NAME "
92          + "from "
93              + "information_schema.table_constraints a, "
94              + "information_schema.constraint_column_usage b, "
95              + "information_schema.referential_constraints c, "
96              + "information_schema.constraint_column_usage d "
97          + "where "
98              + "a.constraint_type='FOREIGN KEY' "
99              + "and a.constraint_name=b.constraint_name "
100             + "and a.constraint_name=c.constraint_name "
101             + "and c.unique_constraint_name=d.constraint_name";
102 
103     /**
104      * Singleton processor instance that handlers share to save memory. Notice
105      * the default scoping to allow only classes in this package to use this
106      * instance.
107      */
108     private static final RowProcessor ROWPROCESSOR = new BasicRowProcessor()
109     {
110 
111         /**
112          * Convert a <code>ResultSet</code> row into an <code>Object[]</code>.
113          * This implementation copies column values into the array in the same
114          * order they're returned from the <code>ResultSet</code>. Array
115          * elements will be set to <code>null</code> if the column was SQL NULL.
116          *
117          * @see org.apache.commons.dbutils.RowProcessor#toArray(java.sql.ResultSet)
118          * @param _rs ResultSet that supplies the array data
119          * @throws SQLException if a database access error occurs
120          * @return the newly created array
121          */
122         @Override
123         public Object[] toArray(final ResultSet _rs)
124             throws SQLException
125         {
126             final ResultSetMetaData metaData = _rs.getMetaData();
127             final int cols = metaData.getColumnCount();
128             final Object[] result = new Object[cols];
129 
130             for (int i = 0; i < cols; i++) {
131                 switch (metaData.getColumnType(i + 1)) {
132                     case java.sql.Types.TIMESTAMP:
133                         result[i] = _rs.getTimestamp(i + 1);
134                         break;
135                     default:
136                         result[i] = _rs.getObject(i + 1);
137                 }
138             }
139             return result;
140         }
141     };
142 
143     /**
144      * Constructor.
145      */
146     public PostgreSQLDatabase()
147     {
148         addMapping(ColumnType.INTEGER,      "bigint",    "null", "int8", "int4", "bigserial");
149         addMapping(ColumnType.DECIMAL,      "numeric",   "null", "decimal", "numeric");
150         addMapping(ColumnType.REAL,         "real",      "null", "float4");
151         addMapping(ColumnType.STRING_SHORT, "char",      "null", "bpchar");
152         addMapping(ColumnType.STRING_LONG,  "varchar",   "null", "varchar");
153         addMapping(ColumnType.DATETIME,     "timestamp", "null", "timestamp");
154         addMapping(ColumnType.BLOB,         "bytea",     "null", "bytea");
155         addMapping(ColumnType.CLOB,         "text",      "null", "text");
156         addMapping(ColumnType.BOOLEAN,      "boolean",   "null", "bool");
157     }
158 
159 
160     /**
161      * {@inheritDoc}
162      */
163     @Override
164     public boolean isConnected(final Connection _connection)
165         throws SQLException
166     {
167         boolean ret = false;
168         final StringBuilder cmd = new StringBuilder();
169         cmd.append(" SELECT version();");
170         PreparedStatement stmt = null;
171         stmt = _connection.prepareStatement(cmd.toString());
172         try {
173             final ResultSet resultset = stmt.executeQuery();
174             if (resultset.next()) {
175                 final String str = resultset.getString(1);
176                 ret = str.toUpperCase().contains("POSTGRESQL");
177             }
178             resultset.close();
179         } finally {
180             stmt.close();
181         }
182         return ret;
183     }
184 
185     /**
186      * {@inheritDoc}
187      */
188     @Override
189     public String getCurrentTimeStamp()
190     {
191         return "current_timestamp";
192     }
193 
194     /**
195      * {@inheritDoc}
196      */
197     @Override
198     public String getTimestampValue(final String _dateTimeStr)
199     {
200         return "timestamp '" + _dateTimeStr + "'";
201     }
202 
203     /**
204      * {@inheritDoc} <br/>
205      * Postgres does not work with the year "ZERO" therefore the year
206      * is translated like a Gregorian/Julian Calendar (ZERO = -1)
207      */
208     @Override
209     public String getStr4DateTime(final ReadableDateTime _value)
210     {
211         String ret;
212         if (_value.getEra() == 0) {
213             ret = _value.toDateTime().minusYears(1).toString(ISODateTimeFormat.dateHourMinuteSecondFraction());
214             ret = ret.substring(1) + " BC";
215         } else {
216             ret = _value.toDateTime().toString(ISODateTimeFormat.dateHourMinuteSecondFraction());
217         }
218         return ret;
219     }
220 
221     /**
222      * {@inheritDoc}
223      */
224     @Override
225     public Object getBooleanValue(final Boolean _value)
226     {
227         return _value;
228     }
229 
230     /**
231      * <p>This is the PostgreSQL specific implementation of an all deletion.
232      * Following order is used to remove all eFaps specific information:
233      * <ul>
234      * <li>remove all views of the user</li>
235      * <li>remove all tables of the user</li>
236      * <li>remove all sequences of the user</li>
237      * </ul></p>
238      * <p>The table are dropped with cascade, so all depending sequences etc.
239      * are also dropped automatically. </p>
240      * <p>Attention! If application specific tables, views or constraints are
241      * defined, this database objects are also removed!</p>
242      *
243      * @param _con sql connection
244      * @throws SQLException on error while executing sql statements
245      */
246     @Override
247     @SuppressFBWarnings("SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE")
248     public void deleteAll(final Connection _con)
249         throws SQLException
250     {
251 
252         final Statement stmtSel = _con.createStatement();
253         final Statement stmtExec = _con.createStatement();
254 
255         try {
256             if (PostgreSQLDatabase.LOG.isInfoEnabled()) {
257                 PostgreSQLDatabase.LOG.info("Remove all Tables");
258             }
259 
260             final DatabaseMetaData metaData = _con.getMetaData();
261 
262             // delete all views
263             final ResultSet rsViews = metaData.getTables(null, null, "%", new String[] { "VIEW" });
264             while (rsViews.next()) {
265                 final String viewName = rsViews.getString("TABLE_NAME");
266                 if (PostgreSQLDatabase.LOG.isDebugEnabled()) {
267                     PostgreSQLDatabase.LOG.debug("  - View '" + viewName + "'");
268                 }
269                 stmtExec.execute("drop view " + viewName);
270             }
271             rsViews.close();
272 
273             // delete all tables
274             final ResultSet rsTables = metaData.getTables(null, null, "%", new String[] { "TABLE" });
275             while (rsTables.next()) {
276                 final String tableName = rsTables.getString("TABLE_NAME");
277                 if (PostgreSQLDatabase.LOG.isDebugEnabled()) {
278                     PostgreSQLDatabase.LOG.debug("  - Table '" + tableName + "'");
279                 }
280                 stmtExec.execute("drop table " + tableName + " cascade");
281             }
282             rsTables.close();
283 
284             //delete all sequences
285             final ResultSet rsSeq = stmtSel.executeQuery("SELECT sequence_name FROM information_schema.sequences");
286             while (rsSeq.next()) {
287                 final String seqName = rsSeq.getString("sequence_name");
288                 if (PostgreSQLDatabase.LOG.isDebugEnabled()) {
289                     PostgreSQLDatabase.LOG.debug("  - Sequence '" + seqName + "'");
290                 }
291                 stmtExec.execute("drop sequence " + seqName);
292             }
293             rsSeq.close();
294 
295         } finally {
296             stmtSel.close();
297             stmtExec.close();
298         }
299     }
300 
301     /**
302      * {@inheritDoc}
303      */
304     @Override
305     public PostgreSQLDatabase deleteView(final Connection _con,
306                                     final String _name)
307         throws SQLException
308     {
309         final Statement stmtExec = _con.createStatement();
310         try {
311             stmtExec.execute("drop view " + _name);
312         } finally {
313             stmtExec.close();
314         }
315         return this;
316     }
317 
318     /**
319      * For the PostgreSQL database, an eFaps SQL table is created in this steps.
320      * <ul>
321      * <li>SQL table itself with column <code>ID</code> and unique key on the
322      * column is created</li>
323      * <li>if the table is an auto increment table (parent table is
324      * <code>null</code>, the column <code>ID</code> is set as auto increment
325      * column</li>
326      * <li>if no parent table is defined, the foreign key to the parent table is
327      * automatically set</li>
328      * </ul>
329      *
330      * @see org.efaps.db.databases.AbstractDatabase#createTable(java.sql.Connection, java.lang.String, java.lang.String)
331      * @param _con          Connection to be used for the SQL statements
332      * @param _table        name for the table
333      * @return this PostgreSQL DB definition instance
334      * @throws SQLException if the table could not be created
335      */
336     @Override
337     public PostgreSQLDatabase createTable(final Connection _con,
338                                           final String _table)
339         throws SQLException
340     {
341         final Statement stmt = _con.createStatement();
342         try {
343             stmt.executeUpdate(new StringBuilder()
344                 .append("create table ").append(_table).append(" (")
345                     .append("ID bigint")
346                     .append(",").append("constraint ").append(_table).append("_PK_ID primary key (ID)")
347                 .append(") without OIDS;")
348                 .toString());
349         } finally {
350             stmt.close();
351         }
352 
353         return this;
354     }
355 
356     /**
357      * {@inheritDoc}
358      */
359     @Override
360     public PostgreSQLDatabase defineTableAutoIncrement(final Connection _con,
361                                                        final String _table)
362         throws SQLException
363     {
364         final Statement stmt = _con.createStatement();
365         try {
366             // create sequence
367             stmt.execute(new StringBuilder()
368                 .append("create sequence ").append(_table).append("_id_seq")
369                 .toString());
370             // define for ID column the auto increment value
371             stmt.execute(new StringBuilder()
372                 .append("alter table ").append(_table)
373                 .append(" alter column id set default nextval('")
374                 .append(_table).append("_id_seq')")
375                 .toString());
376             // sequence owned by table
377             stmt.execute(new StringBuilder()
378                 .append("alter sequence ").append(_table).append("_id_seq owned by ")
379                 .append(_table).append(".id")
380                 .toString());
381         } finally {
382             stmt.close();
383         }
384         return this;
385     }
386 
387     /**
388      * A new id for given column of a SQL table is returned (with sequences!).
389      * The method must be implemented because the JDBC driver from PostgreSQL
390      * does not support that the generated ID of a new table row is returned
391      * while the row is inserted.
392      *
393      * @param _con      sql connection
394      * @param _table    sql table for which a new id must returned
395      * @param _column   sql table column for which a new id must returned
396      * @throws SQLException if a new id could not be retrieved
397      * @return new id for the sequence
398      */
399     @Override
400     public long getNewId(final Connection _con,
401                          final String _table,
402                          final String _column)
403         throws SQLException
404     {
405 
406         long ret = 0;
407         final Statement stmt = _con.createStatement();
408 
409         try {
410             final StringBuilder cmd = new StringBuilder();
411             cmd.append("select nextval('").append(_table).append("_").append(_column).append("_SEQ')");
412 
413             final ResultSet rs = stmt.executeQuery(cmd.toString());
414             if (rs.next()) {
415                 ret = rs.getLong(1);
416             }
417             rs.close();
418         } finally {
419             stmt.close();
420         }
421         return ret;
422     }
423 
424     /**
425      * @return always <i>true</i> because supported by PostgreSQL database
426      */
427     @Override
428     public boolean supportsBinaryInputStream()
429     {
430         return true;
431     }
432 
433     /**
434      * <p>Creates sequence <code>_name</code> in PostgreSQL. As name of the
435      * sequence the lower case of <code>_name</code> is used.</p>
436      * <p>The minimum and starting value is set to <code>_startValue</code>
437      * minus one and then updated to current value (by fetching a value from
438      * the sequence). The current value is <code>_startValue</code> minus one
439      * so that a call to {@link #nextSequence(Connection, String)} returns the
440      * expected <code>_startValue</code>.</p>
441      *
442      * @param _con          SQL connection
443      * @param _name         name of the sequence to update
444      * @param _startValue   start value of the sequence
445      * @return this database instance
446      * @throws SQLException if sequence could not be created
447      * @see #nextSequence(Connection, String)
448      */
449     @Override
450     public PostgreSQLDatabase createSequence(final Connection _con,
451                                              final String _name,
452                                              final long _startValue)
453         throws SQLException
454     {
455         final long value = _startValue - 1;
456         final StringBuilder cmd = new StringBuilder();
457         cmd.append("CREATE SEQUENCE \"").append(_name.toLowerCase())
458             .append("\" INCREMENT 1")
459             .append(" MINVALUE  ").append(value)
460             .append(" MAXVALUE 9223372036854775807 ")
461             .append(" START ").append(value)
462             .append(" CACHE 1;");
463 
464         final PreparedStatement stmt = _con.prepareStatement(cmd.toString());
465         try {
466             stmt.execute();
467         } finally {
468             stmt.close();
469         }
470         if (!_con.getAutoCommit()) {
471             _con.commit();
472         }
473 
474         nextSequence(_con, _name);
475         return this;
476     }
477 
478     /**
479      * {@inheritDoc}
480      */
481     @Override
482     public PostgreSQLDatabase deleteSequence(final Connection _con,
483                                              final String _name)
484         throws SQLException
485     {
486         final String cmd = new StringBuilder()
487                 .append("DROP SEQUENCE \"").append(_name.toLowerCase()).append("\" RESTRICT")
488                 .toString();
489         final Statement stmt = _con.createStatement();
490         try {
491             stmt.executeUpdate(cmd);
492         } finally {
493             stmt.close();
494         }
495         return this;
496     }
497 
498     /**
499      * <p>Checks in the database schema if the sequence <code>_name</code>
500      * exists.</p>
501      * <p>As name of the sequence the lower case of <code>_name</code> is
502      * used.</p>
503      *
504      * @param _con          SQL connection
505      * @param _name         name of the sequence to update
506      * @return <i>true</i> if sequence exists; otherwise <i>false</i>
507      * @throws SQLException if it could not be checked that the sequence exists
508      */
509     @Override
510     @SuppressFBWarnings("SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE")
511     public boolean existsSequence(final Connection _con,
512                                   final String _name)
513         throws SQLException
514     {
515         final boolean ret;
516         final String cmd = new StringBuilder()
517                 .append("SELECT relname FROM pg_class WHERE relkind = 'S' AND relname='")
518                 .append(_name.toLowerCase()).append("'")
519                 .toString();
520         final Statement stmt = _con.createStatement();
521         try {
522             final ResultSet resultset = stmt.executeQuery(cmd);
523             ret = resultset.next();
524             resultset.close();
525         } finally {
526             stmt.close();
527         }
528         return ret;
529     }
530 
531     /**
532      * {@inheritDoc}
533      */
534     @Override
535     public long nextSequence(final Connection _con,
536                              final String _name)
537         throws SQLException
538     {
539         final long ret;
540         final String cmd = new StringBuilder()
541                 .append("SELECT NEXTVAL('\"" + _name.toLowerCase() + "\"') ")
542                 .toString();
543         final Statement stmt = _con.createStatement();
544         try {
545             final ResultSet resultset = stmt.executeQuery(cmd);
546             if (resultset.next()) {
547                 ret = resultset.getLong(1);
548             } else  {
549                 throw new SQLException("fetching new value from sequence '" + _name + "' failed");
550             }
551             resultset.close();
552         } finally {
553             stmt.close();
554         }
555         return ret;
556     }
557 
558     /**
559      * <p>Defines new <code>_value</code> for sequence <code>_name</code>.
560      * Because it could be that the new <code>_value</code> is lower than the
561      * current defined minimum value of the sequence <code>_name</code>, the
562      * sequence is {@link #deleteSequence(Connection, String) deleted} and then
563      * {@link #createSequence(Connection, String, long) recreated}.</p>
564      * <p>As name of the sequence the lower case of <code>_name</code> is
565      * used.</p>
566      *
567      * @param _con          SQL connection
568      * @param _name         name of the sequence to update
569      * @param _value        new value of the sequence
570      * @return this database instance
571      * @throws SQLException if sequence could not be deleted or created
572      * @see #deleteSequence(Connection, String)
573      * @see #createSequence(Connection, String, long)
574      */
575     @Override
576     public PostgreSQLDatabase setSequence(final Connection _con,
577                                           final String _name,
578                                           final long _value)
579         throws SQLException
580     {
581         deleteSequence(_con, _name);
582         createSequence(_con, _name, _value);
583         return this;
584     }
585 
586     /**
587      * {@inheritDoc}
588      */
589     @Override
590     public String getHibernateDialect()
591     {
592         return "org.hibernate.dialect.PostgreSQL82Dialect";
593     }
594 
595     /**
596      * Overwrites the original method to specify SQL statement
597      * {@link #SQL_UNIQUE_KEYS} as replacement because the JDBC driver for
598      * PostgreSQL does not handle matching table names.
599      *
600      * @param _con          SQL connection
601      * @param _sql          SQL statement (not used)
602      * @param _cache4Name   map used to fetch depending on the table name the
603      *                      related table information
604      * @throws SQLException if unique keys could not be fetched
605      * @see #SQL_UNIQUE_KEYS
606      */
607     @Override
608     protected void initTableInfoUniqueKeys(final Connection _con,
609                                            final String _sql,
610                                            final Map<String, TableInformation> _cache4Name)
611         throws SQLException
612     {
613         super.initTableInfoUniqueKeys(_con, PostgreSQLDatabase.SQL_UNIQUE_KEYS, _cache4Name);
614     }
615 
616     /**
617      * Overwrites the original method to specify SQL statement
618      * {@link #SQL_FOREIGN_KEYS} as replacement because the JDBC driver for
619      * PostgreSQL does not handle matching table names.
620      *
621      * @param _con          SQL connection
622      * @param _sql          SQL statement (not used)
623      * @param _cache4Name   map used to fetch depending on the table name the
624      *                      related table information
625      * @throws SQLException if foreign keys could not be fetched
626      * @see #SQL_FOREIGN_KEYS
627      */
628     @Override
629     protected void initTableInfoForeignKeys(final Connection _con,
630                                             final String _sql,
631                                             final Map<String, TableInformation> _cache4Name)
632         throws SQLException
633     {
634         super.initTableInfoForeignKeys(_con, PostgreSQLDatabase.SQL_FOREIGN_KEYS, _cache4Name);
635     }
636 
637     /**
638      * {@inheritDoc}
639      */
640     @Override
641     protected StringBuilder getAlterColumn(final String _columnName,
642                                            final org.efaps.db.databases.AbstractDatabase.ColumnType _columnType)
643     {
644         final StringBuilder ret = new StringBuilder()
645             .append(" alter ").append(getColumnQuote()).append(_columnName).append(getColumnQuote())
646             .append(" type ")
647             .append(getWriteSQLTypeName(_columnType));
648         return ret;
649     }
650 
651     /**
652      * {@inheritDoc}
653      */
654     @Override
655     protected StringBuilder getAlterColumnIsNotNull(final String _columnName,
656                                                     final boolean _isNotNull)
657     {
658         final StringBuilder ret = new StringBuilder()
659             .append(" alter column ").append(getColumnQuote()).append(_columnName).append(getColumnQuote());
660         if (_isNotNull) {
661             ret.append(" set ");
662         } else {
663             ret.append(" drop ");
664         }
665         ret.append(" not null");
666         return ret;
667     }
668 
669     /**
670      * {@inheritDoc}
671      */
672     @Override
673     protected boolean check4NullValues(final Connection _con,
674                                        final String _tableName,
675                                        final String _columnName)
676         throws SQLException
677     {
678         boolean ret = true;
679         final StringBuilder cmd = new StringBuilder();
680         cmd.append("select count(*) from ").append(getTableQuote()).append(_tableName).append(getTableQuote())
681                 .append(" where ").append(getColumnQuote()).append(_columnName).append(getColumnQuote())
682                 .append(" is null");
683 
684         PostgreSQLDatabase.LOG.debug("    ..SQL> {}", cmd);
685 
686         final Statement stmt = _con.createStatement();
687         ResultSet rs = null;
688         try {
689             rs = stmt.executeQuery(cmd.toString());
690             rs.next();
691             ret = rs.getInt(1) > 0;
692         } finally {
693             if (rs != null) {
694                 rs.close();
695             }
696             stmt.close();
697         }
698         return ret;
699     }
700 
701     /**
702      * {@inheritDoc}
703      */
704     @Override
705     public RowProcessor getRowProcessor()
706     {
707         return PostgreSQLDatabase.ROWPROCESSOR;
708     }
709 }