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.io.ByteArrayInputStream;
24  import java.io.IOException;
25  import java.io.UnsupportedEncodingException;
26  import java.sql.Connection;
27  import java.sql.DatabaseMetaData;
28  import java.sql.ResultSet;
29  import java.sql.ResultSetMetaData;
30  import java.sql.SQLException;
31  import java.sql.Statement;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.zip.Adler32;
35  import java.util.zip.CheckedInputStream;
36  
37  import org.apache.commons.dbutils.BasicRowProcessor;
38  import org.apache.commons.dbutils.RowProcessor;
39  import org.efaps.db.databases.information.TableInformation;
40  import org.efaps.util.EFapsException;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
45  
46  /**
47   * The database driver is used for Oracle databases starting with version 9i.
48   * It does not support auto generated keys. To generate a new id number,
49   * the Oracle sequences are used.
50   *
51   * @author The eFaps Team
52   * @version $Id$
53   */
54  public class OracleDatabase
55      extends AbstractDatabase<OracleDatabase>
56  {
57  
58      /**
59       * Logging instance used in this class.
60       */
61      private static final Logger LOG = LoggerFactory.getLogger(OracleDatabase.class);
62  
63      /**
64       * Select statement to select all unique keys for current logged in
65       * PostgreSQL database user.
66       *
67       * @see #initTableInfoUniqueKeys(Connection, String, Map)
68       */
69      private static final String SQL_UNIQUE_KEYS = "select "
70              + "a.index_name as INDEX_NAME, "
71              + "a.table_name as TABLE_NAME, "
72              + "b.column_name as COLUMN_NAME, "
73              + "b.position as ORDINAL_POSITION "
74          + "from "
75              + "user_constraints a, "
76              + "user_cons_columns b "
77          + "where "
78              + "a.constraint_type='U' "
79              + "and a.index_name = b.constraint_name";
80  
81      /**
82       * Select statement for all foreign keys for current logged in PostgreSQL
83       * database user.
84       *
85       * @see #initTableInfoForeignKeys(Connection, String, Map)
86       */
87      private static final String SQL_FOREIGN_KEYS = "select "
88              + "ucc1.TABLE_NAME as TABLE_NAME, "
89              + "uc.constraint_name as FK_NAME, "
90              + "ucc1.column_name as FKCOLUMN_NAME, "
91              + "case "
92                      + "when uc.delete_rule='NO ACTION' then '" + DatabaseMetaData.importedKeyNoAction + "' "
93                      + "when uc.delete_rule='CASCASE' then '" + DatabaseMetaData.importedKeyCascade + "' "
94                      + "else '' end as DELETE_RULE, "
95              + "ucc2.table_name as PKTABLE_NAME, "
96              + "ucc2.column_name as PKCOLUMN_NAME "
97          + "from "
98              + "user_constraints uc, "
99              + "user_cons_columns ucc1, "
100             + "user_cons_columns ucc2 "
101         + "where "
102             + "uc.constraint_name = ucc1.constraint_name "
103             + "and uc.r_constraint_name = ucc2.constraint_name "
104             + "and ucc1.POSITION = ucc2.POSITION "
105             + "and uc.constraint_type = 'R'";
106 
107 
108 
109     /**
110      * Singleton processor instance that handlers share to save memory. Notice
111      * the default scoping to allow only classes in this package to use this
112      * instance.
113      */
114     private static final RowProcessor ROWPROCESSOR = new BasicRowProcessor()
115     {
116 
117         /**
118          * Convert a <code>ResultSet</code> row into an <code>Object[]</code>.
119          * This implementation copies column values into the array in the same
120          * order they're returned from the <code>ResultSet</code>. Array
121          * elements will be set to <code>null</code> if the column was SQL NULL.
122          *
123          * @see org.apache.commons.dbutils.RowProcessor#toArray(java.sql.ResultSet)
124          * @param _rs ResultSet that supplies the array data
125          * @throws SQLException if a database access error occurs
126          * @return the newly created array
127          */
128         @Override
129         public Object[] toArray(final ResultSet _rs)
130             throws SQLException
131         {
132             final ResultSetMetaData metaData = _rs.getMetaData();
133             final int cols = metaData.getColumnCount();
134             final Object[] result = new Object[cols];
135 
136             for (int i = 0; i < cols; i++) {
137                 switch (metaData.getColumnType(i + 1)) {
138                     case java.sql.Types.TIMESTAMP:
139                         result[i] = _rs.getTimestamp(i + 1);
140                         break;
141                     case java.sql.Types.NUMERIC:
142                         if (metaData.getScale(i + 1) > 0) {
143                             result[i] = _rs.getBigDecimal(i + 1);
144                         } else {
145                             result[i] = _rs.getLong(i + 1);
146                         }
147                         break;
148                     default:
149                         result[i] = _rs.getObject(i + 1);
150                 }
151             }
152 
153             return result;
154         }
155     };
156 
157     /**
158      * The instance is initialised and sets the columns map used for this
159      * database.
160      */
161     public OracleDatabase()
162     {
163         super();
164         addMapping(ColumnType.INTEGER,      "number(*,0)",     "null", "number(38,0)");
165         addMapping(ColumnType.DECIMAL,      "numeric",    "null", "decimal", "numeric");
166         addMapping(ColumnType.REAL,         "number",     "null", "number");
167         addMapping(ColumnType.STRING_SHORT, "varchar2",   "null", "varchar2", "char");
168         addMapping(ColumnType.STRING_LONG,  "varchar2",   "null", "varchar2");
169         addMapping(ColumnType.DATETIME,     "timestamp",  "null", "timestamp", "timestamp(6)", "date");
170         addMapping(ColumnType.BLOB,         "blob",       "null", "blob");
171         addMapping(ColumnType.CLOB,         "nclob",      "null", "nclob");
172         addMapping(ColumnType.BOOLEAN,      "number",     "null", "number");
173     }
174 
175     /**
176      * {@inheritDoc}
177      * @throws SQLException
178      */
179     @Override
180     public boolean isConnected(final Connection _connection)
181         throws SQLException
182     {
183         boolean ret = false;
184         final Statement stmt = _connection.createStatement();
185         try {
186             final ResultSet resultset = stmt
187                         .executeQuery("select product from product_component_version where product like 'Oracle%'");
188             ret = resultset.next();
189             resultset.close();
190         } finally {
191             stmt.close();
192         }
193         return ret;
194     }
195 
196     /**
197      * {@inheritDoc}
198      */
199     @Override
200     public int getMaxExpressions()
201     {
202         return 999;
203     }
204 
205     /**
206      * The method returns string <code>sysdate</code> which let Oracle set the
207      * timestamp automatically from the database server.
208      *
209      * @return string <code>sysdate</code>
210      */
211     @Override
212     public String getCurrentTimeStamp()
213     {
214         return "sysdate";
215     }
216 
217     /**
218      * {@inheritDoc}
219      */
220     @Override
221     public String getTimestampValue(final String _isoDateTime)
222     {
223         final String format = "'yyyy-mm-dd\"T\"hh24:mi:ss.ff3'";
224         return "to_timestamp('" + _isoDateTime + "', " + format + ")";
225     }
226 
227     /**
228      * {@inheritDoc}
229      */
230     @Override
231     public Object getBooleanValue(final Boolean _value)
232     {
233         Integer ret = 0;
234         if (_value) {
235             ret = 1;
236         }
237         return ret;
238     }
239 
240     /**
241      * This is the Oracle specific implementation of an all deletion. Following
242      * order is used to remove all eFaps specific information of the current
243      * Oracle database user:
244      * <ul>
245      * <li>remove all user views</li>
246      * <li>remove all user tables</li>
247      * <li>remove all user sequences</li>
248      * </ul>
249      * Attention! If application specific tables, views or constraints are
250      * defined, this database objects are also removed!
251      *
252      * @param _con  sql connection
253      * @throws SQLException if delete of the views, tables or sequences failed
254      */
255     @Override
256     @SuppressFBWarnings("SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE")
257     public void deleteAll(final Connection _con)
258         throws SQLException
259     {
260         final Statement stmtSel = _con.createStatement();
261         final Statement stmtExec = _con.createStatement();
262 
263         try  {
264             // remove all views
265             if (OracleDatabase.LOG.isInfoEnabled())  {
266                 OracleDatabase.LOG.info("Remove all Views");
267             }
268             ResultSet rs = stmtSel.executeQuery("select VIEW_NAME from USER_VIEWS");
269             while (rs.next())  {
270                 final String viewName = rs.getString(1);
271                 if (OracleDatabase.LOG.isDebugEnabled())  {
272                     OracleDatabase.LOG.debug("  - View '" + viewName + "'");
273                 }
274                 stmtExec.execute("drop view " + viewName);
275             }
276             rs.close();
277 
278             // remove all tables
279             if (OracleDatabase.LOG.isInfoEnabled())  {
280                 OracleDatabase.LOG.info("Remove all Tables");
281             }
282             rs = stmtSel.executeQuery("select TABLE_NAME from USER_TABLES");
283             while (rs.next())  {
284                 final String tableName = rs.getString(1);
285                 if (OracleDatabase.LOG.isDebugEnabled())  {
286                     OracleDatabase.LOG.debug("  - Table '" + tableName + "'");
287                 }
288                 stmtExec.execute("drop table " + tableName + " cascade constraints");
289             }
290             rs.close();
291 
292             // remove all sequences
293             if (OracleDatabase.LOG.isInfoEnabled())  {
294                 OracleDatabase.LOG.info("Remove all Sequences");
295             }
296             rs = stmtSel.executeQuery("select SEQUENCE_NAME from USER_SEQUENCES");
297             while (rs.next())  {
298                 final String seqName = rs.getString(1);
299                 if (OracleDatabase.LOG.isDebugEnabled())  {
300                     OracleDatabase.LOG.debug("  - Sequence '" + seqName + "'");
301                 }
302                 stmtExec.execute("drop sequence " + seqName);
303             }
304             rs.close();
305         } finally  {
306             stmtSel.close();
307             stmtExec.close();
308         }
309     }
310 
311     /**
312      * {@inheritDoc}
313      */
314     @Override
315     public OracleDatabase deleteView(final Connection _con,
316                                      final String _name)
317         throws SQLException
318     {
319         final Statement stmtExec = _con.createStatement();
320         stmtExec.execute("drop view " + _name);
321         return this;
322     }
323 
324     /**
325      * For the database from vendor Oracle. An eFaps SQL table
326      * is created in this steps:
327      * <ul>
328      * <li>sql table itself with column <code>ID</code> and unique key on the
329      *     column is created</li>
330      * <li>sequence with same name of table and suffix <code>_SEQ</code> is
331      *     created</li>
332      * </ul>
333      * An eFaps sql table with parent table is created in this steps:
334      * <ul>
335      * <li>sql table itself with column <code>ID</code> and unique key on the
336      *     column is created</li>
337      * <li>the foreign key to the parent table is automatically set</li>
338      * </ul>
339      *
340      * @param _con          SQL connection
341      * @param _table        name of the table to create
342      * @throws SQLException if the table or sequence could not be created
343      * @return this
344      */
345     @Override
346     public OracleDatabase createTable(final Connection _con,
347                                       final String _table)
348         throws SQLException
349     {
350         final Statement stmt = _con.createStatement();
351 
352         try  {
353 
354             // create table itself
355             final StringBuilder cmd = new StringBuilder()
356                 .append("create table ").append(getTableName(_table)).append(" (")
357                 .append("  ID number not null,")
358                 .append("  constraint ");
359 
360             final String consName = getConstrainName(_table + "_UK_ID");
361             cmd.append(consName).append(" unique(ID)");
362 
363             cmd.append(")");
364             stmt.executeUpdate(cmd.toString());
365 
366         } catch (final EFapsException e) {
367             e.printStackTrace();
368         } finally  {
369             stmt.close();
370         }
371 
372         return this;
373     }
374 
375     /**
376      * {@inheritDoc}
377      */
378     @Override
379     public OracleDatabase defineTableAutoIncrement(final Connection _con,
380                                                    final String _table)
381         throws SQLException
382     {
383         throw new Error("not implemented");
384     }
385 
386     /**
387      * A new id for given column of a SQL table is returned (with
388      * sequences!).
389      *
390      * @param _con          sql connection
391      * @param _table        sql table for which a new id must returned
392      * @param _column       sql table column for which a new id must returned
393      * @return new ID of the used sequence
394      * @throws SQLException if a new id could not be retrieved
395      */
396     @Override
397     public long getNewId(final Connection _con,
398                          final String _table,
399                          final String _column)
400         throws SQLException
401     {
402         long ret = 0;
403         final Statement stmt = _con.createStatement();
404 
405         try  {
406             final StringBuilder cmd = new StringBuilder()
407                 .append("select ").append(_table).append("_SEQ.nextval from DUAL");
408 
409             final ResultSet rs = stmt.executeQuery(cmd.toString());
410             if (rs.next())  {
411                 ret = rs.getLong(1);
412             }
413             rs.close();
414         } finally  {
415             stmt.close();
416         }
417         return ret;
418     }
419 
420     /**
421      * {@inheritDoc}
422      */
423     @Override
424     public OracleDatabase createSequence(final Connection _con,
425                                          final String _name,
426                                          final long _startValue)
427         throws SQLException
428     {
429         final Statement stmt = _con.createStatement();
430         // create sequence
431         final StringBuilder cmd = new StringBuilder()
432             .append("create sequence ").append(_name)
433             .append("  increment by 1 ")
434             .append("  start with ").append(_startValue)
435             .append("  nocache");
436         try {
437             stmt.executeUpdate(cmd.toString());
438         } finally {
439             stmt.close();
440         }
441 
442         nextSequence(_con, _name);
443         return this;
444     }
445 
446     /**
447      * {@inheritDoc}
448      * @throws SQLException
449      */
450     @Override
451     public OracleDatabase deleteSequence(final Connection _con,
452                                          final String _name)
453         throws SQLException
454     {
455         final String cmd = new StringBuilder()
456                         .append("drop sequence ").append(_name)
457                         .toString();
458         final Statement stmt = _con.createStatement();
459         try {
460             stmt.executeUpdate(cmd);
461         } finally {
462             stmt.close();
463         }
464         return this;
465     }
466 
467     /**
468      * {@inheritDoc}
469      */
470     @Override
471     @SuppressFBWarnings("SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE")
472     public boolean existsSequence(final Connection _con,
473                                   final String _name)
474         throws SQLException
475     {
476         final boolean ret;
477         final String cmd = new StringBuilder()
478                         .append("SELECT sequence_name FROM user_sequences WHERE sequence_name='")
479                         .append(_name.toLowerCase()).append("'")
480                         .toString();
481         final Statement stmt = _con.createStatement();
482         try {
483             final ResultSet resultset = stmt.executeQuery(cmd);
484             ret = resultset.next();
485             resultset.close();
486         } finally {
487             stmt.close();
488         }
489         return ret;
490     }
491 
492     /**
493      * {@inheritDoc}
494      */
495     @Override
496     public long nextSequence(final Connection _con,
497                              final String _name)
498         throws SQLException
499     {
500         final long ret;
501         final String cmd = new StringBuilder()
502                 .append("SELECT " + _name + ".nextval from dual")
503                 .toString();
504         final Statement stmt = _con.createStatement();
505         try {
506             final ResultSet resultset = stmt.executeQuery(cmd);
507             if (resultset.next()) {
508                 ret = resultset.getLong(1);
509             } else  {
510                 throw new SQLException("fetching new value from sequence '" + _name + "' failed");
511             }
512             resultset.close();
513         } finally {
514             stmt.close();
515         }
516         return ret;
517     }
518 
519     /**
520      * {@inheritDoc}
521      */
522     @Override
523     public OracleDatabase setSequence(final Connection _con,
524                                       final String _name,
525                                       final long _value)
526         throws SQLException
527     {
528         deleteSequence(_con, _name);
529         createSequence(_con, _name, _value);
530         return this;
531     }
532 
533     /**
534      * Overwrites the original method to specify SQL statement
535      * {@link #SQL_UNIQUE_KEYS} as replacement because the JDBC driver for
536      * PostgreSQL does not handle matching table names.
537      *
538      * @param _con          SQL connection
539      * @param _sql          SQL statement (not used)
540      * @param _cache4Name   map used to fetch depending on the table name the
541      *                      related table information
542      * @throws SQLException if unique keys could not be fetched
543      * @see #SQL_UNIQUE_KEYS
544      */
545     @Override
546     protected void initTableInfoUniqueKeys(final Connection _con,
547                                            final String _sql,
548                                            final Map<String, TableInformation> _cache4Name)
549         throws SQLException
550     {
551         super.initTableInfoUniqueKeys(_con, OracleDatabase.SQL_UNIQUE_KEYS, _cache4Name);
552     }
553 
554     /**
555      * Overwrites the original method to specify SQL statement
556      * {@link #SQL_FOREIGN_KEYS} as replacement because the JDBC driver for
557      * PostgreSQL does not handle matching table names.
558      *
559      * @param _con          SQL connection
560      * @param _sql          SQL statement (not used)
561      * @param _cache4Name   map used to fetch depending on the table name the
562      *                      related table information
563      * @throws SQLException if foreign keys could not be fetched
564      * @see #SQL_FOREIGN_KEYS
565      */
566     @Override
567     protected void initTableInfoForeignKeys(final Connection _con,
568                                             final String _sql,
569                                             final Map<String, TableInformation> _cache4Name)
570         throws SQLException
571     {
572         super.initTableInfoForeignKeys(_con, OracleDatabase.SQL_FOREIGN_KEYS, _cache4Name);
573     }
574 
575     @Override
576     protected void initTableInfoColumns(final Connection _con,
577                                         final String _sql,
578                                         final Map<String, TableInformation> _cache4Name)
579         throws SQLException
580     {
581         Statement stmt = null;
582         final ResultSet rs;
583         if (_sql == null) {
584             rs = _con.getMetaData().getColumns(getCatalog(), getSchemaPattern(), "%", "%");
585         } else        {
586             stmt = _con.createStatement();
587             rs = stmt.executeQuery(_sql);
588         }
589         try {
590             while (rs.next()) {
591                 final String tableName = rs.getString("TABLE_NAME").toUpperCase();
592                 if (_cache4Name.containsKey(tableName))  {
593                     final String colName = rs.getString("COLUMN_NAME").toUpperCase();
594                     final String typeName = rs.getString("TYPE_NAME").toLowerCase();
595                     final Set<AbstractDatabase.ColumnType> colTypes
596                         = OracleDatabase.this.getReadColumnTypes(typeName);
597                     if (colTypes == null)  {
598                         throw new SQLException("read unknown column type '"
599                                             + typeName + "' in column '" + colName + "' for table '" + tableName + "'");
600                     }
601                     final int size = rs.getInt("COLUMN_SIZE");
602                     final int scale = rs.getInt("DECIMAL_DIGITS");
603                     final boolean isNullable = !"NO".equalsIgnoreCase(rs.getString("IS_NULLABLE"));
604                     _cache4Name.get(tableName).addColInfo(colName, colTypes, size, scale, isNullable);
605                 }
606             }
607         } finally {
608             rs.close();
609             if (stmt != null) {
610                 stmt.close();
611             }
612         }
613     }
614 
615     /**
616      * {@inheritDoc}
617      */
618     @Override
619     public String getConstrainName(final String _name)
620         throws EFapsException
621     {
622         return getName4DB(_name, 30);
623     }
624 
625     /**
626      * @param _name name
627      * @param _maxLength maximum length
628      * @return new name
629      * @throws EFapsException on error
630      */
631     protected String getName4DB(final String _name,
632                                 final int _maxLength)
633         throws EFapsException
634     {
635         String ret = _name;
636         if (_name.length() > 30) {
637             try {
638                 final byte[] buffer = _name.getBytes("UTF8");
639                 final ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
640                 final CheckedInputStream cis = new CheckedInputStream(bais, new Adler32());
641                 final byte[] readBuffer = new byte[5];
642                 long value = 0;
643                 while (cis.read(readBuffer) >= 0) {
644                     value = cis.getChecksum().getValue();
645                 }
646                 final String valueSt = String.valueOf(value);
647                 ret = ret.substring(0, 30);
648                 final int sizeSuf = ret.length() - valueSt.length();
649                 ret = ret.substring(0, sizeSuf) + value;
650             } catch (final UnsupportedEncodingException e) {
651                 throw new EFapsException("UnsupportedEncodingException", e);
652             } catch (final IOException e) {
653                 throw new EFapsException("IOException", e);
654             }
655         }
656         return ret;
657     }
658 
659     /**
660      * {@inheritDoc}
661      */
662     @Override
663     public String getTableName(final String _name)
664         throws EFapsException
665     {
666         return getName4DB(_name, 30);
667     }
668 
669     /**
670      * Returns a single " used to select columns within
671      * SQL statements for a Oracle database..
672      *
673      * @return always single reversed apostrophe
674      */
675     @Override
676     public String getColumnQuote()
677     {
678         return "\"";
679     }
680 
681     /**
682      * {@inheritDoc}
683      */
684     @Override
685     public String getHibernateDialect()
686     {
687         return "org.hibernate.dialect.Oracle10gDialect";
688     }
689 
690     /**
691      * {@inheritDoc}
692      */
693     @Override
694     protected StringBuilder getAlterColumn(final String _columnName,
695                                            final org.efaps.db.databases.AbstractDatabase.ColumnType _columnType)
696     {
697         final StringBuilder ret = new StringBuilder()
698             .append(" modify ").append(getColumnQuote()).append(_columnName).append(getColumnQuote())
699             .append(" ")
700             .append(getWriteSQLTypeName(_columnType));
701         return ret;
702     }
703 
704     /**
705      * {@inheritDoc}
706      */
707     @Override
708     protected StringBuilder getAlterColumnIsNotNull(final String _columnName,
709                                                     final boolean _isNotNull)
710     {
711         final StringBuilder ret = new StringBuilder()
712                         .append(" modify ").append(getColumnQuote()).append(_columnName).append(getColumnQuote())
713                         .append(" ");
714         if (_isNotNull) {
715             ret.append(" not null ");
716         } else {
717             ret.append(" null ");
718         }
719         return ret;
720     }
721 
722     /**
723      * {@inheritDoc}
724      */
725     @Override
726     protected boolean check4NullValues(final Connection _con,
727                                        final String _tableName,
728                                        final String _columnName)
729         throws SQLException
730     {
731         boolean ret = true;
732         final StringBuilder cmd = new StringBuilder();
733         cmd.append("select count(*) from ").append(getTableQuote()).append(_tableName).append(getTableQuote())
734                 .append(" where ").append(getColumnQuote()).append(_columnName).append(getColumnQuote())
735                 .append(" is null");
736 
737         OracleDatabase.LOG.debug("    ..SQL> {}", cmd);
738 
739         final Statement stmt = _con.createStatement();
740         ResultSet rs = null;
741         try {
742             rs = stmt.executeQuery(cmd.toString());
743             rs.next();
744             ret = rs.getInt(1) > 0;
745         } finally {
746             if (rs != null) {
747                 rs.close();
748             }
749             stmt.close();
750         }
751         return ret;
752     }
753 
754     /**
755      * {@inheritDoc}
756      */
757     @Override
758     public RowProcessor getRowProcessor()
759     {
760         return OracleDatabase.ROWPROCESSOR;
761     }
762 }