media handling fixes, gotten rid of StorageObject, set the default method for blots...
[mir.git] / source / mir / storage / Database.java
1 /*
2  * Copyright (C) 2001, 2002 The Mir-coders group
3  *
4  * This file is part of Mir.
5  *
6  * Mir is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * Mir is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Mir; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  *
20  * In addition, as a special exception, The Mir-coders gives permission to link
21  * the code of this program with  any library licensed under the Apache Software License,
22  * The Sun (tm) Java Advanced Imaging library (JAI), The Sun JIMI library
23  * (or with modified versions of the above that use the same license as the above),
24  * and distribute linked combinations including the two.  You must obey the
25  * GNU General Public License in all respects for all of the code used other than
26  * the above mentioned libraries.  If you modify this file, you may extend this
27  * exception to your version of the file, but you are not obligated to do so.
28  * If you do not wish to do so, delete this exception statement from your version.
29  */
30 package mir.storage;
31
32 import mir.config.MirPropertiesConfiguration;
33 import mir.entity.Entity;
34 import mir.entity.EntityList;
35 import mir.entity.StorableObjectEntity;
36 import mir.entity.AbstractEntity;
37 import mir.log.LoggerWrapper;
38 import mir.misc.StringUtil;
39 import mir.storage.store.*;
40 import mir.util.JDBCStringRoutines;
41 import mircoders.global.MirGlobal;
42
43 import java.io.*;
44 import java.sql.*;
45 import java.text.ParseException;
46 import java.text.SimpleDateFormat;
47 import java.util.*;
48
49 import org.apache.commons.dbcp.DelegatingConnection;
50 import org.postgresql.PGConnection;
51 import org.postgresql.largeobject.LargeObjectManager;
52 import org.postgresql.largeobject.LargeObject;
53
54 /**
55  * Implements database access.
56  *
57  * @version $Id: Database.java,v 1.44.2.26 2005/01/23 15:36:03 zapata Exp $
58  * @author rk
59  *
60  */
61 public class Database {
62   private static Class GENERIC_ENTITY_CLASS = mir.entity.StorableObjectEntity.class;
63   protected static final ObjectStore o_store = ObjectStore.getInstance();
64   private static final int _millisPerHour = 60 * 60 * 1000;
65
66   protected LoggerWrapper logger;
67
68   protected MirPropertiesConfiguration configuration;
69   protected String mainTable;
70   protected String primaryKeySequence = null;
71   protected String primaryKeyField = "id";
72
73   protected List fieldNames;
74   protected int[] fieldTypes;
75   protected Map fieldNameToType;
76
77   protected Class entityClass;
78   private int defaultLimit;
79
80   TimeZone timezone;
81   SimpleDateFormat internalDateFormat;
82   SimpleDateFormat userInputDateFormat;
83
84   /**
85    * Kontruktor bekommt den Filenamen des Konfigurationsfiles ?bergeben.
86    * Aus diesem file werden <code>Database.Logfile</code>,
87    * <code>Database.Username</code>,<code>Database.Password</code>,
88    * <code>Database.Host</code> und <code>Database.Adaptor</code>
89    * ausgelesen und ein Broker f?r die Verbindugen zur Datenbank
90    * erzeugt.
91    */
92   public Database() throws StorageObjectFailure {
93     configuration = MirPropertiesConfiguration.instance();
94     logger = new LoggerWrapper("Database");
95     timezone = TimeZone.getTimeZone(configuration.getString("Mir.DefaultTimezone"));
96     internalDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
97     internalDateFormat.setTimeZone(timezone);
98
99     userInputDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
100     userInputDateFormat.setTimeZone(timezone);
101
102     String theAdaptorName = configuration.getString("Database.Adaptor");
103     defaultLimit = Integer.parseInt(configuration.getString("Database.Limit"));
104
105     try {
106       entityClass = GENERIC_ENTITY_CLASS;
107     }
108     catch (Throwable e) {
109       logger.error("Error in Database() constructor with " + theAdaptorName + " -- " + e.getMessage());
110       throw new StorageObjectFailure("Error in Database() constructor.", e);
111     }
112   }
113
114   public java.lang.Class getEntityClass() {
115     return entityClass;
116   }
117
118   public Entity createNewEntity() throws StorageObjectFailure {
119     try {
120       AbstractEntity result = (AbstractEntity) entityClass.newInstance();
121       result.setStorage(this);
122
123       return result;
124     }
125     catch (Throwable t) {
126       throw new StorageObjectFailure(t);
127     }
128   }
129
130   /**
131    * Liefert die Standardbeschr?nkung von select-Statements zur?ck, also
132    * wieviel Datens?tze per Default selektiert werden.
133    *
134    * @return Standard-Anzahl der Datens?tze
135    */
136   public int getLimit() {
137     return defaultLimit;
138   }
139
140   public String getIdFieldName() {
141     return primaryKeyField;
142   }
143
144   /**
145    * Liefert den Namen der Tabelle, auf das sich das Datenbankobjekt bezieht.
146    *
147    * @return Name der Tabelle
148    */
149   public String getTableName() {
150     return mainTable;
151   }
152
153   /**
154    * Returns the id that was most recently added to the database
155    */
156   private String getLatestInsertedId(Connection aConnection) throws SQLException {
157     if (primaryKeySequence==null)
158       primaryKeySequence = mainTable+"_id_seq";
159
160     PreparedStatement statement = aConnection.prepareStatement("select currval('" + primaryKeySequence + "')");
161
162     ResultSet rs = statement.executeQuery();
163     rs.next();
164     return rs.getString(1);
165   }
166
167   /**
168    * Returns a list of field names for this <code>Database</code>
169    */
170   public List getFieldNames() throws StorageObjectFailure {
171     if (fieldNames == null) {
172       acquireMetaData();
173     }
174
175     return fieldNames;
176   }
177
178   /**
179    *   Gets value out of ResultSet according to type and converts to String
180    *   @param rs  ResultSet.
181    *   @param aType  a type from java.sql.Types.*
182    *   @param valueIndex  index in ResultSet
183    *   @return returns the value as String. If no conversion is possible
184    *                             /unsupported value/ is returned
185    */
186   private String getValueAsString(ResultSet rs, int valueIndex, int aType)
187     throws StorageObjectFailure {
188     String outValue = null;
189
190     if (rs != null) {
191       try {
192         switch (aType) {
193           case java.sql.Types.BIT:
194             outValue = (rs.getBoolean(valueIndex) == true) ? "1" : "0";
195
196             break;
197
198           case java.sql.Types.INTEGER:
199           case java.sql.Types.SMALLINT:
200           case java.sql.Types.TINYINT:
201           case java.sql.Types.BIGINT:
202
203             int out = rs.getInt(valueIndex);
204
205             if (!rs.wasNull()) {
206               outValue = new Integer(out).toString();
207             }
208
209             break;
210
211           case java.sql.Types.NUMERIC:
212
213             /** todo Numeric can be float or double depending upon
214              *  metadata.getScale() / especially with oracle */
215             long outl = rs.getLong(valueIndex);
216
217             if (!rs.wasNull()) {
218               outValue = new Long(outl).toString();
219             }
220
221             break;
222
223           case java.sql.Types.REAL:
224
225             float tempf = rs.getFloat(valueIndex);
226
227             if (!rs.wasNull()) {
228               tempf *= 10;
229               tempf += 0.5;
230
231               int tempf_int = (int) tempf;
232               tempf = (float) tempf_int;
233               tempf /= 10;
234               outValue = "" + tempf;
235               outValue = outValue.replace('.', ',');
236             }
237
238             break;
239
240           case java.sql.Types.DOUBLE:
241
242             double tempd = rs.getDouble(valueIndex);
243
244             if (!rs.wasNull()) {
245               tempd *= 10;
246               tempd += 0.5;
247
248               int tempd_int = (int) tempd;
249               tempd = (double) tempd_int;
250               tempd /= 10;
251               outValue = "" + tempd;
252               outValue = outValue.replace('.', ',');
253             }
254
255             break;
256
257           case java.sql.Types.CHAR:
258           case java.sql.Types.VARCHAR:
259           case java.sql.Types.LONGVARCHAR:
260             outValue = rs.getString(valueIndex);
261
262             break;
263
264           case java.sql.Types.LONGVARBINARY:
265             outValue = rs.getString(valueIndex);
266
267             break;
268
269           case java.sql.Types.TIMESTAMP:
270
271             // it's important to use Timestamp here as getting it
272             // as a string is undefined and is only there for debugging
273             // according to the API. we can make it a string through formatting.
274             // -mh
275             Timestamp timestamp = (rs.getTimestamp(valueIndex));
276
277             if (!rs.wasNull()) {
278               java.util.Date date = new java.util.Date(timestamp.getTime());
279
280               Calendar calendar = new GregorianCalendar();
281               calendar.setTime(date);
282               calendar.setTimeZone(timezone);
283               outValue = internalDateFormat.format(date);
284
285               int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
286               String tzOffset = StringUtil.zeroPaddingNumber(Math.abs(offset) / _millisPerHour, 2, 2);
287
288               if (offset<0)
289                 outValue = outValue + "-";
290               else
291                 outValue = outValue + "+";
292               outValue = outValue + tzOffset;
293             }
294
295             break;
296
297           default:
298             outValue = "<unsupported value>";
299             logger.warn("Unsupported Datatype: at " + valueIndex + " (" + aType + ")");
300         }
301       } catch (SQLException e) {
302         throw new StorageObjectFailure("Could not get Value out of Resultset -- ",
303           e);
304       }
305     }
306
307     return outValue;
308   }
309
310   /**
311    *   select-Operator um einen Datensatz zu bekommen.
312    *   @param id Primaerschluessel des Datensatzes.
313    *   @return liefert EntityObject des gefundenen Datensatzes oder null.
314    */
315   public Entity selectById(String id) throws StorageObjectExc {
316     if ((id == null) || id.equals("")) {
317       throw new StorageObjectExc("Database.selectById: Missing id");
318     }
319
320     // ask object store for object
321     if (StoreUtil.extendsStorableEntity(entityClass)) {
322       String uniqueId = id;
323
324       if (entityClass.equals(StorableObjectEntity.class)) {
325         uniqueId += ("@" + mainTable);
326       }
327
328       StoreIdentifier search_sid = new StoreIdentifier(entityClass, uniqueId);
329       logger.debug("CACHE: (dbg) looking for sid " + search_sid.toString());
330
331       Entity hit = (Entity) o_store.use(search_sid);
332
333       if (hit != null) {
334         return hit;
335       }
336     }
337
338     Statement stmt = null;
339     Connection con = obtainConnection();
340     Entity returnEntity = null;
341
342     try {
343       ResultSet rs;
344
345       /** todo better prepared statement */
346       String selectSql =
347         "select * from " + mainTable + " where " + primaryKeyField + "=" + id;
348       stmt = con.createStatement();
349       rs = executeSql(stmt, selectSql);
350
351       if (rs != null) {
352         if (rs.next()) {
353           returnEntity = makeEntityFromResultSet(rs);
354         }
355         else {
356           logger.warn("No data for id: " + id + " in table " + mainTable);
357         }
358
359         rs.close();
360       }
361       else {
362         logger.warn("No Data for Id " + id + " in Table " + mainTable);
363       }
364     }
365     catch (SQLException sqe) {
366       throwSQLException(sqe, "selectById");
367       return null;
368     }
369     catch (NumberFormatException e) {
370       logger.error("ID is no number: " + id);
371     }
372     finally {
373       freeConnection(con, stmt);
374     }
375
376     return returnEntity;
377   }
378
379   /**
380    * This method makes it possible to make selects across multiple tables
381    *
382    * @param mainTablePrefix prefix for the mainTable
383    * @param extraTables a vector of tables for relational select
384    * @param aWhereClause whereClause
385    * @return EntityList of selected Objects
386    * @throws StorageObjectFailure
387    */
388
389   public EntityList selectByWhereClauseWithExtraTables(String mainTablePrefix,
390                                                 List extraTables, String aWhereClause )
391    throws StorageObjectFailure {
392         return selectByWhereClause( mainTablePrefix, extraTables, aWhereClause, "", 0, defaultLimit);
393   }
394
395   public EntityList selectByFieldValue(String aField, String aValue) throws StorageObjectFailure {
396     return selectByFieldValue(aField, aValue, 0);
397   }
398
399   public EntityList selectByFieldValue(String aField, String aValue, int offset) throws StorageObjectFailure {
400     return selectByWhereClause(aField + "='" + JDBCStringRoutines.escapeStringLiteral(aValue)+"'", offset);
401   }
402
403   /**
404    * select-Operator liefert eine EntityListe mit den gematchten Datens?tzen zur?ck.
405    * Also offset wird der erste Datensatz genommen.
406    *
407    * @param where where-Clause
408    * @return EntityList mit den gematchten Entities
409    * @exception StorageObjectFailure
410    */
411   public EntityList selectByWhereClause(String where) throws StorageObjectFailure {
412     return selectByWhereClause(where, 0);
413   }
414
415   /**
416    * select-Operator liefert eine EntityListe mit den gematchten Datens?tzen zur?ck.
417    * Als maximale Anzahl wird das Limit auf der Konfiguration genommen.
418    *
419    * @param whereClause where-Clause
420    * @param offset ab welchem Datensatz.
421    * @return EntityList mit den gematchten Entities
422    * @exception StorageObjectFailure
423    */
424   public EntityList selectByWhereClause(String whereClause, int offset) throws StorageObjectFailure {
425     return selectByWhereClause(whereClause, null, offset);
426   }
427
428   /**
429    * select-Operator liefert eine EntityListe mit den gematchten Datens?tzen zur?ck.
430    * Also offset wird der erste Datensatz genommen.
431    * Als maximale Anzahl wird das Limit auf der Konfiguration genommen.
432    *
433    * @param where where-Clause
434    * @param order orderBy-Clause
435    * @return EntityList mit den gematchten Entities
436    * @exception StorageObjectFailure
437    */
438   public EntityList selectByWhereClause(String where, String order) throws StorageObjectFailure {
439     return selectByWhereClause(where, order, 0);
440   }
441
442   public EntityList selectByWhereClause(String mainTablePrefix, List extraTables, String where, String order) throws StorageObjectFailure {
443     return selectByWhereClause(mainTablePrefix, extraTables, where, order, 0, defaultLimit);
444   }
445
446   /**
447    * select-Operator liefert eine EntityListe mit den gematchten Datens?tzen zur?ck.
448    * Als maximale Anzahl wird das Limit auf der Konfiguration genommen.
449    *
450    * @param whereClause where-Clause
451    * @param orderBy orderBy-Clause
452    * @param offset ab welchem Datensatz
453    * @return EntityList mit den gematchten Entities
454    * @exception StorageObjectFailure
455    */
456   public EntityList selectByWhereClause(String whereClause, String orderBy, int offset) throws StorageObjectFailure {
457     return selectByWhereClause(whereClause, orderBy, offset, defaultLimit);
458   }
459
460   /**
461    * select-Operator returns EntityList with matching rows in Database.
462    * @param aWhereClause where-Clause
463    * @param anOrderByClause orderBy-Clause
464    * @param offset ab welchem Datensatz
465    * @param limit wieviele Datens?tze
466    * @return EntityList mit den gematchten Entities
467    * @exception StorageObjectFailure
468    */
469   public EntityList selectByWhereClause(String aWhereClause, String anOrderByClause,
470             int offset, int limit) throws StorageObjectFailure {
471     return selectByWhereClause("", null, aWhereClause, anOrderByClause, offset, limit);
472   }
473
474
475   /**
476    * select-Operator returns EntityList with matching rows in Database.
477    * @param aWhereClause where-Clause
478    * @param anOrderByClause orderBy-Clause
479    * @param anOffset ab welchem Datensatz
480    * @param aLimit wieviele Datens?tze
481    * @return EntityList mit den gematchten Entities
482    * @exception StorageObjectFailure
483    */
484   public EntityList selectByWhereClause(
485       String aMainTablePrefix, List anExtraTables,
486       String aWhereClause, String anOrderByClause,
487                         int anOffset, int aLimit) throws StorageObjectFailure {
488
489     // TODO get rid of emtpy Strings in anExtraTables
490     // make anExtraTables null, if single empty String in it
491     // cause StringUtil.splitString puts in emptyString
492
493     if (anExtraTables!=null && ((String) anExtraTables.get(0)).trim().equals("")){
494       anExtraTables=null;
495     }
496
497     String useTable = mainTable;
498     String selection = "*";
499
500     if (aMainTablePrefix != null && aMainTablePrefix.trim().length() > 0) {
501       useTable += " " + aMainTablePrefix;
502       selection = aMainTablePrefix.trim() + ".*";
503     }
504
505     // check o_store for entitylist
506     // only if no relational select
507     if (anExtraTables==null) {
508       if (StoreUtil.extendsStorableEntity(entityClass)) {
509          StoreIdentifier searchSid = new StoreIdentifier(entityClass,
510                StoreContainerType.STOC_TYPE_ENTITYLIST,
511                StoreUtil.getEntityListUniqueIdentifierFor(mainTable,
512                 aWhereClause, anOrderByClause, anOffset, aLimit));
513          EntityList hit = (EntityList) o_store.use(searchSid);
514
515          if (hit != null) {
516             return hit;
517          }
518       }
519     }
520
521     // local
522     EntityList theReturnList = null;
523     Connection connection = null;
524     Statement statement = null;
525     ResultSet resultSet;
526
527     // build sql-statement
528
529     if ((aWhereClause != null) && (aWhereClause.trim().length() == 0)) {
530       aWhereClause = null;
531     }
532
533     StringBuffer selectSql =
534       new StringBuffer("select "+selection+" from ").append(useTable);
535
536     // append extratables, if necessary
537     if (anExtraTables!=null) {
538       for (int i=0;i < anExtraTables.size();i++) {
539         if (!anExtraTables.get(i).equals("")) {
540           selectSql.append( ", " + anExtraTables.get(i));
541         }
542       }
543     }
544
545     if (aWhereClause != null) {
546       selectSql.append(" where ").append(aWhereClause);
547     }
548
549     if ((anOrderByClause != null) && !(anOrderByClause.trim().length() == 0)) {
550       selectSql.append(" order by ").append(anOrderByClause);
551     }
552
553     if ((aLimit > -1) && (anOffset > -1)) {
554       selectSql.append(" LIMIT ").append(aLimit+1).append(" OFFSET ").append(anOffset);
555     }
556
557     // execute sql
558     try {
559       connection = obtainConnection();
560       statement = connection.createStatement();
561       boolean hasMore = false;
562
563       // selecting...
564       resultSet = executeSql(statement, selectSql.toString());
565
566       if (resultSet != null) {
567         theReturnList = new EntityList();
568         Entity theResultEntity;
569         int position = 0;
570         while (((aLimit == -1) || (position<aLimit)) && resultSet.next()) {
571           theResultEntity = makeEntityFromResultSet(resultSet);
572           theReturnList.add(theResultEntity);
573           position++;
574         }
575         hasMore = resultSet.next();
576         resultSet.close();
577       }
578
579       if (theReturnList != null) {
580         // now we decide if we have to know an overall count...
581         theReturnList.setOffset(anOffset);
582         theReturnList.setWhere(aWhereClause);
583         theReturnList.setOrder(anOrderByClause);
584         theReturnList.setStorage(this);
585         theReturnList.setLimit(aLimit);
586
587         if (hasMore) {
588           theReturnList.setNextBatch(anOffset + aLimit);
589         }
590
591         if (anExtraTables==null && StoreUtil.extendsStorableEntity(entityClass)) {
592           StoreIdentifier sid = theReturnList.getStoreIdentifier();
593           logger.debug("CACHE (add): " + sid.toString());
594           o_store.add(sid);
595         }
596       }
597     }
598     catch (SQLException sqe) {
599       throwSQLException(sqe, "selectByWhereClause");
600     }
601     finally {
602       try {
603         if (connection != null) {
604           freeConnection(connection, statement);
605         }
606       } catch (Throwable t) {
607       }
608     }
609
610     return theReturnList;
611   }
612
613   private Entity makeEntityFromResultSet(ResultSet rs)
614     throws StorageObjectFailure {
615     Map theResultHash = new HashMap();
616     String theResult = null;
617     int type;
618     Entity returnEntity = null;
619
620     try {
621       if (StoreUtil.extendsStorableEntity(entityClass)) {
622          StoreIdentifier searchSid = StorableObjectEntity.getStoreIdentifier(this,
623                entityClass, rs);
624          Entity hit = (Entity) o_store.use(searchSid);
625          if (hit != null) return hit;
626       }
627
628       for (int i = 0; i < getFieldNames().size(); i++) {
629         // alle durchlaufen bis nix mehr da
630         type = fieldTypes[i];
631
632         if (type == java.sql.Types.LONGVARBINARY) {
633           InputStreamReader is =
634             (InputStreamReader) rs.getCharacterStream(i + 1);
635
636           if (is != null) {
637             char[] data = new char[32768];
638             StringBuffer theResultString = new StringBuffer();
639             int len;
640
641             while ((len = is.read(data)) > 0) {
642               theResultString.append(data, 0, len);
643             }
644
645             is.close();
646             theResult = theResultString.toString();
647           }
648           else {
649             theResult = null;
650           }
651         }
652         else {
653           theResult = getValueAsString(rs, (i + 1), type);
654         }
655
656         if (theResult != null) {
657           theResultHash.put(getFieldNames().get(i), theResult);
658         }
659       }
660
661       if (entityClass != null) {
662         returnEntity = createNewEntity();
663         returnEntity.setFieldValues(theResultHash);
664
665         if (returnEntity instanceof StorableObject) {
666           logger.debug("CACHE: ( in) " + returnEntity.getId() + " :" + mainTable);
667           o_store.add(((StorableObject) returnEntity).getStoreIdentifier());
668         }
669       } else {
670         throwStorageObjectException("Internal Error: entityClass not set!");
671       }
672     }
673     catch (IOException e) {
674       throwStorageObjectException("IOException! -- " + e.getMessage());
675     }
676     catch (SQLException sqe) {
677       throwSQLException(sqe, "makeEntityFromResultSet");
678
679       return null;
680     }
681
682     return returnEntity;
683   }
684
685   /**
686    * Inserts an entity into the database.
687    *
688    * @param anEntity
689    * @return der Wert des Primary-keys der eingef?gten Entity
690    */
691   public String insert(Entity anEntity) throws StorageObjectFailure {
692     invalidateStore();
693
694     String returnId = null;
695     Connection con = null;
696     PreparedStatement pstmt = null;
697
698     try {
699       StringBuffer f = new StringBuffer();
700       StringBuffer v = new StringBuffer();
701       String aField;
702       String aValue;
703       boolean firstField = true;
704
705       // make sql-string
706       for (int i = 0; i < getFieldNames().size(); i++) {
707         aField = (String) getFieldNames().get(i);
708
709         if (!aField.equals(primaryKeyField)) {
710           aValue = null;
711
712           // exceptions
713           if (!anEntity.hasFieldValue(aField) && (
714               aField.equals("webdb_create") ||
715               aField.equals("webdb_lastchange"))) {
716             aValue = "NOW()";
717           }
718           else {
719               if (anEntity.hasFieldValue(aField)) {
720                 aValue =
721                   "'" +
722                    JDBCStringRoutines.escapeStringLiteral(anEntity.getFieldValue(aField)) + "'";
723               }
724           }
725
726           // wenn Wert gegeben, dann einbauen
727           if (aValue != null) {
728             if (firstField == false) {
729               f.append(",");
730               v.append(",");
731             }
732             else {
733               firstField = false;
734             }
735
736             f.append(aField);
737             v.append(aValue);
738           }
739         }
740       }
741        // end for
742
743       // insert into db
744       StringBuffer sqlBuf =
745         new StringBuffer("insert into ").append(mainTable).append("(").append(f)
746                                         .append(") values (").append(v).append(")");
747       String sql = sqlBuf.toString();
748
749       logQueryBefore(sql);
750       con = obtainConnection();
751       con.setAutoCommit(false);
752       pstmt = con.prepareStatement(sql);
753
754       int ret = pstmt.executeUpdate();
755
756       if (ret == 0) {
757         //insert failed
758         return null;
759       }
760
761 //      pstmt = con.prepareStatement("select currval('" +  + "_id_seq')");
762
763       returnId = getLatestInsertedId(con);
764       anEntity.setId(returnId);
765     }
766     catch (SQLException sqe) {
767       throwSQLException(sqe, "insert");
768     }
769     finally {
770       try {
771         con.setAutoCommit(true);
772       }
773       catch (Exception e) {
774       }
775
776       freeConnection(con, pstmt);
777     }
778
779     /** todo store entity in o_store */
780     return returnId;
781   }
782
783   /**
784    * Updates an entity in the database
785    *
786    * @param theEntity
787    */
788   public void update(Entity theEntity) throws StorageObjectFailure {
789     Connection con = null;
790     PreparedStatement pstmt = null;
791
792     /** todo this is stupid: why do we prepare statement, when we
793      *  throw it away afterwards. should be regular statement
794      *  update/insert could better be one routine called save()
795      *  that chooses to either insert or update depending if we
796      *  have a primary key in the entity. i don't know if we
797      *  still need the streamed input fields. // rk  */
798
799     /** todo extension: check if Entity did change, otherwise we don't need
800      *  the roundtrip to the database */
801     /** invalidating corresponding entitylists in o_store*/
802
803     invalidateStore();
804
805     String id = theEntity.getId();
806     String aField;
807     StringBuffer fv = new StringBuffer();
808     boolean firstField = true;
809
810     // build sql statement
811     for (int i = 0; i < getFieldNames().size(); i++) {
812       aField = (String) getFieldNames().get(i);
813
814       // only normal cases
815       // todo if entity.hasFieldValue returns false, then the value should be stored as null
816       if (!(aField.equals(primaryKeyField) ||
817             aField.equals("webdb_create") ||
818             aField.equals("webdb_lastchange"))) {
819         if (theEntity.hasFieldValue(aField)) {
820           if (firstField == false) {
821             fv.append(", ");
822           }
823           else {
824             firstField = false;
825           }
826
827           fv.append(aField).append("='").append(JDBCStringRoutines.escapeStringLiteral(theEntity.getFieldValue(aField))).append("'");
828
829           //              fv.append(aField).append("='").append(StringUtil.quote((String)theEntity.getFieldValue(aField))).append("'");
830         }
831       }
832     }
833
834     StringBuffer sql =
835       new StringBuffer("update ").append(mainTable).append(" set ").append(fv);
836
837     // exceptions
838     if (getFieldNames().contains("webdb_lastchange")) {
839       sql.append(",webdb_lastchange=NOW()");
840     }
841
842     // special case: the webdb_create requires the field in yyyy-mm-dd HH:mm
843     // format so anything extra will be ignored. -mh
844     if (getFieldNames().contains("webdb_create") &&
845         theEntity.hasFieldValue("webdb_create")) {
846       // minimum of 10 (yyyy-mm-dd)...
847       if (theEntity.getFieldValue("webdb_create").length() >= 10) {
848         String dateString = theEntity.getFieldValue("webdb_create");
849
850         // if only 10, then add 00:00 so it doesn't throw a ParseException
851         if (dateString.length() == 10) {
852           dateString = dateString + " 00:00";
853         }
854
855         // TimeStamp stuff
856         try {
857           java.util.Date d = userInputDateFormat.parse(dateString);
858 //          Timestamp tStamp = new Timestamp(d.getTime());
859           sql.append(",webdb_create='" + JDBCStringRoutines.formatDate(d) + "'");
860         }
861         catch (ParseException e) {
862           throw new StorageObjectFailure(e);
863         }
864       }
865     }
866
867     sql.append(" where id=").append(id);
868     logQueryBefore(sql.toString());
869
870     try {
871       con = obtainConnection();
872       con.setAutoCommit(false);
873       pstmt = con.prepareStatement(sql.toString());
874
875       pstmt.executeUpdate();
876     }
877     catch (SQLException sqe) {
878       throwSQLException(sqe, "update");
879     }
880     finally {
881       try {
882         con.setAutoCommit(true);
883       }
884       catch (Exception e) {
885         ;
886       }
887
888       freeConnection(con, pstmt);
889     }
890   }
891
892   /*
893   *   delete-Operator
894   *   @param id des zu loeschenden Datensatzes
895   *   @return boolean liefert true zurueck, wenn loeschen erfolgreich war.
896    */
897   public boolean delete(String id) throws StorageObjectFailure {
898     // ostore send notification
899     if (StoreUtil.extendsStorableEntity(entityClass)) {
900       String uniqueId = id;
901
902       if (entityClass.equals(StorableObjectEntity.class)) {
903         uniqueId += ("@" + mainTable);
904       }
905
906       logger.debug("CACHE: (del) " + id);
907
908       StoreIdentifier search_sid =
909         new StoreIdentifier(entityClass,
910           StoreContainerType.STOC_TYPE_ENTITY, uniqueId);
911       o_store.invalidate(search_sid);
912     }
913
914     /** todo could be prepared Statement */
915     Statement stmt = null;
916     Connection con = null;
917     int res = 0;
918     String sql =
919       "delete from " + mainTable + " where " + primaryKeyField + "='" + id + "'";
920
921     logQueryBefore(sql);
922     try {
923       con = obtainConnection();
924       stmt = con.createStatement();
925       res = stmt.executeUpdate(sql);
926     }
927     catch (SQLException sqe) {
928       throwSQLException(sqe, "delete");
929     }
930     finally {
931       freeConnection(con, stmt);
932     }
933
934     invalidateStore();
935
936     return (res > 0) ? true : false;
937   }
938
939   /**
940    * Deletes entities based on a where clause
941    */
942   public int deleteByWhereClause(String aWhereClause) throws StorageObjectFailure {
943     invalidateStore();
944
945     Statement stmt = null;
946     Connection con = null;
947     int res = 0;
948     String sql =
949       "delete from " + mainTable + " where " + aWhereClause;
950
951     //theLog.printInfo("DELETE " + sql);
952     try {
953       con = obtainConnection();
954       stmt = con.createStatement();
955       res = stmt.executeUpdate(sql);
956     }
957     catch (SQLException sqe) {
958       throwSQLException(sqe, "delete");
959     }
960     finally {
961       freeConnection(con, stmt);
962     }
963
964     return res;
965   }
966
967   /* noch nicht implementiert.
968   * @return immer false
969    */
970   public boolean delete(EntityList theEntityList) {
971     return false;
972   }
973
974   /**
975    * Diese Methode fuehrt den Sqlstring <i>sql</i> aus und timed im Logfile.
976    * @param stmt Statemnt
977    * @param sql Sql-String
978    */
979   public ResultSet executeSql(Statement stmt, String sql)
980                             throws StorageObjectFailure, SQLException {
981     ResultSet rs;
982     logQueryBefore(sql);
983     long startTime = System.currentTimeMillis();
984     try {
985       rs = stmt.executeQuery(sql);
986
987       logQueryAfter(sql, (System.currentTimeMillis() - startTime));
988     }
989     catch (SQLException e) {
990       logQueryError(sql, (System.currentTimeMillis() - startTime), e);
991       throw e;
992     }
993
994     return rs;
995   }
996
997   private Map processRow(ResultSet aResultSet) throws StorageObjectFailure {
998     try {
999       Map result = new HashMap();
1000       ResultSetMetaData metaData = aResultSet.getMetaData();
1001       int nrColumns = metaData.getColumnCount();
1002       for (int i=0; i<nrColumns; i++) {
1003         result.put(metaData.getColumnName(i+1), getValueAsString(aResultSet, i+1, metaData.getColumnType(i+1)));
1004       }
1005
1006       return result;
1007     }
1008     catch (Throwable e) {
1009       throw new StorageObjectFailure(e);
1010     }
1011   }
1012
1013   /**
1014    * Executes 1 sql statement and returns the results as a <code>List</code> of
1015    * <code>Map</code>s
1016    */
1017   public List executeFreeSql(String sql, int aLimit) throws StorageObjectFailure, StorageObjectExc {
1018     Connection connection = null;
1019     Statement statement = null;
1020     try {
1021       List result = new ArrayList();
1022       connection = obtainConnection();
1023       statement = connection.createStatement();
1024       ResultSet resultset = executeSql(statement, sql);
1025       try {
1026         while (resultset.next() && result.size() < aLimit) {
1027           result.add(processRow(resultset));
1028         }
1029       }
1030       finally {
1031         resultset.close();
1032       }
1033
1034       return result;
1035     }
1036     catch (Throwable e) {
1037       throw new StorageObjectFailure(e);
1038     }
1039     finally {
1040       if (connection!=null) {
1041         freeConnection(connection, statement);
1042       }
1043     }
1044   };
1045
1046   /**
1047    * Executes 1 sql statement and returns the first result row as a <code>Map</code>s
1048    * (<code>null</code> if there wasn't any row)
1049    */
1050   public Map executeFreeSingleRowSql(String anSqlStatement) throws StorageObjectFailure, StorageObjectExc {
1051     try {
1052       List resultList = executeFreeSql(anSqlStatement, 1);
1053       try {
1054         if (resultList.size()>0)
1055           return (Map) resultList.get(0);
1056         else
1057           return null;
1058       }
1059       finally {
1060       }
1061     }
1062     catch (Throwable t) {
1063       throw new StorageObjectFailure(t);
1064     }
1065   };
1066
1067   /**
1068    * Executes 1 sql statement and returns the first column of the first result row as a <code>String</code>s
1069    * (<code>null</code> if there wasn't any row)
1070    */
1071   public String executeFreeSingleValueSql(String sql) throws StorageObjectFailure, StorageObjectExc {
1072     Map row = executeFreeSingleRowSql(sql);
1073
1074     if (row==null)
1075       return null;
1076
1077     Iterator i = row.values().iterator();
1078     if (i.hasNext())
1079       return (String) i.next();
1080     else
1081       return null;
1082   };
1083
1084   public int getSize(String where) throws SQLException, StorageObjectFailure {
1085     return getSize("", null, where);
1086   }
1087   /**
1088    * returns the number of rows in the table
1089    */
1090   public int getSize(String mainTablePrefix, List extraTables, String where) throws SQLException, StorageObjectFailure {
1091
1092     String useTable = mainTable;
1093     if (mainTablePrefix!=null && mainTablePrefix.trim().length()>0) {
1094       useTable+=" "+mainTablePrefix;
1095     }
1096     StringBuffer countSql =
1097       new StringBuffer("select count(*) from ").append(useTable);
1098         // append extratables, if necessary
1099       if (extraTables!=null) {
1100         for (int i=0;i < extraTables.size();i++) {
1101           if (!extraTables.get(i).equals("")) {
1102             countSql.append( ", " + extraTables.get(i));
1103           }
1104         }
1105       }
1106
1107     if ((where != null) && (where.length() != 0)) {
1108       countSql.append( " where " + where);
1109     }
1110
1111     Connection con = null;
1112     Statement stmt = null;
1113     int result = 0;
1114     logQueryBefore(countSql.toString());
1115     long startTime = System.currentTimeMillis();
1116
1117     try {
1118       con = obtainConnection();
1119       stmt = con.createStatement();
1120
1121       ResultSet rs = executeSql(stmt, countSql.toString());
1122
1123       while (rs.next()) {
1124         result = rs.getInt(1);
1125       }
1126     }
1127     catch (SQLException e) {
1128       logger.error("Database.getSize: " + e.getMessage());
1129     }
1130     finally {
1131       freeConnection(con, stmt);
1132     }
1133     logQueryAfter(countSql.toString(), (System.currentTimeMillis() - startTime));
1134
1135     return result;
1136   }
1137
1138   public int executeUpdate(Statement stmt, String sql)
1139     throws StorageObjectFailure, SQLException {
1140     int rs;
1141
1142     logQueryBefore(sql);
1143     long startTime = System.currentTimeMillis();
1144
1145     try {
1146       rs = stmt.executeUpdate(sql);
1147
1148       logQueryAfter(sql, (System.currentTimeMillis() - startTime));
1149     }
1150     catch (SQLException e) {
1151       logQueryError(sql, (System.currentTimeMillis() - startTime), e);
1152       throw e;
1153     }
1154
1155     return rs;
1156   }
1157
1158   public int executeUpdate(String sql)
1159     throws StorageObjectFailure, SQLException {
1160     int result = -1;
1161     Connection con = null;
1162     PreparedStatement pstmt = null;
1163
1164     logQueryBefore(sql);
1165     long startTime = System.currentTimeMillis();
1166     try {
1167       con = obtainConnection();
1168       pstmt = con.prepareStatement(sql);
1169       result = pstmt.executeUpdate();
1170       logQueryAfter(sql, System.currentTimeMillis() - startTime);
1171     }
1172     catch (Throwable e) {
1173       logQueryError(sql, System.currentTimeMillis() - startTime, e);
1174       throw new StorageObjectFailure("Database.executeUpdate(" + sql + "): " + e.getMessage(), e);
1175     }
1176     finally {
1177       freeConnection(con, pstmt);
1178     }
1179     return result;
1180   }
1181
1182   /**
1183    * Processes the metadata for the table this Database object is responsible for.
1184    */
1185   private void processMetaData(ResultSetMetaData aMetaData) throws StorageObjectFailure {
1186     fieldNames = new ArrayList();
1187     fieldNameToType = new HashMap();
1188
1189     try {
1190       int numFields = aMetaData.getColumnCount();
1191       fieldTypes = new int[numFields];
1192
1193       for (int i = 1; i <= numFields; i++) {
1194         fieldNames.add(aMetaData.getColumnName(i));
1195         fieldTypes[i - 1] = aMetaData.getColumnType(i);
1196         fieldNameToType.put(aMetaData.getColumnName(i), new Integer(aMetaData.getColumnType(i)));
1197       }
1198     }
1199     catch (SQLException e) {
1200       throwSQLException(e, "processMetaData");
1201     }
1202   }
1203
1204   /**
1205    * Retrieves metadata from the table this Database object represents
1206    */
1207   private void acquireMetaData() throws StorageObjectFailure {
1208     Connection connection = null;
1209     PreparedStatement statement = null;
1210     String sql = "select * from " + mainTable + " where 0=1";
1211
1212     try {
1213       connection = obtainConnection();
1214       statement = connection.prepareStatement(sql);
1215
1216       logger.debug("METADATA: " + sql);
1217       ResultSet resultSet = statement.executeQuery();
1218       try {
1219         processMetaData(resultSet.getMetaData());
1220       }
1221       finally {
1222         resultSet.close();
1223       }
1224     }
1225     catch (SQLException e) {
1226       throwSQLException(e, "acquireMetaData");
1227     }
1228     finally {
1229       freeConnection(connection, statement);
1230     }
1231   }
1232
1233   public Connection obtainConnection() throws StorageObjectFailure {
1234     try {
1235       return MirGlobal.getDatabaseEngine().obtainConnection();
1236     }
1237     catch (Exception e) {
1238       throw new StorageObjectFailure(e);
1239     }
1240   }
1241
1242   public void freeConnection(Connection aConnection, Statement aStatement) throws StorageObjectFailure {
1243     try {
1244       aStatement.close();
1245     }
1246     catch (Throwable t) {
1247       logger.warn("Can't close statemnet: " + t.toString());
1248     }
1249
1250     try {
1251       MirGlobal.getDatabaseEngine().releaseConnection(aConnection);
1252     }
1253     catch (Throwable t) {
1254       logger.warn("Can't release connection: " + t.toString());
1255     }
1256   }
1257
1258   /**
1259    * Wertet SQLException aus und wirft dannach eine StorageObjectException
1260    * @param sqe SQLException
1261    * @param aFunction Funktonsname, in der die SQLException geworfen wurde
1262    */
1263   protected void throwSQLException(SQLException sqe, String aFunction) throws StorageObjectFailure {
1264     String state = "";
1265     String message = "";
1266     int vendor = 0;
1267
1268     if (sqe != null) {
1269       state = sqe.getSQLState();
1270       message = sqe.getMessage();
1271       vendor = sqe.getErrorCode();
1272     }
1273
1274     String information =
1275         "SQL Error: " +
1276         "state= " + state +
1277         ", vendor= " + vendor +
1278         ", message=" + message +
1279         ", function= " + aFunction;
1280
1281     logger.error(information);
1282
1283     throw new StorageObjectFailure(information, sqe);
1284   }
1285
1286   protected void _throwStorageObjectException(Exception e, String aFunction)
1287     throws StorageObjectFailure {
1288
1289     if (e != null) {
1290       logger.error(e.getMessage() + aFunction);
1291       throw new StorageObjectFailure(aFunction, e);
1292     }
1293   }
1294
1295   /**
1296    * Loggt Fehlermeldung mit dem Parameter Message und wirft dannach
1297    * eine StorageObjectException
1298    * @param aMessage Nachricht mit dem Fehler
1299    * @exception StorageObjectFailure
1300    */
1301   void throwStorageObjectException(String aMessage) throws StorageObjectFailure {
1302     logger.error(aMessage);
1303     throw new StorageObjectFailure(aMessage, null);
1304   }
1305
1306   /**
1307    * Invalidates any cached entity list
1308    */
1309   private void invalidateStore() {
1310     // invalidating all EntityLists corresponding with entityClass
1311     if (StoreUtil.extendsStorableEntity(entityClass)) {
1312       StoreContainerType stoc_type =
1313         StoreContainerType.valueOf(entityClass, StoreContainerType.STOC_TYPE_ENTITYLIST);
1314       o_store.invalidate(stoc_type);
1315     }
1316   }
1317
1318   /**
1319    * Retrieves a binary value
1320    */
1321   public InputStream getBinaryField(String aQuery) throws StorageObjectFailure, SQLException {
1322     Connection connection=null;
1323     Statement statement=null;
1324     InputStream inputStream;
1325     InputStream imageInputStream = null;
1326
1327     try {
1328       connection = obtainConnection();
1329       try {
1330         connection.setAutoCommit(false);
1331         statement = connection.createStatement();
1332         ResultSet resultSet = executeSql(statement, aQuery);
1333
1334         if(resultSet!=null) {
1335           if (resultSet.next()) {
1336             if (resultSet.getMetaData().getColumnType(1) == java.sql.Types.BINARY) {
1337               byte[] data = resultSet.getBytes(1);
1338               imageInputStream = new ByteArrayInputStream(data);
1339             }
1340             else {
1341               inputStream = resultSet.getBlob(1).getBinaryStream();
1342               imageInputStream = new BinaryFieldInputStream(inputStream, connection, statement);
1343             }
1344           }
1345           resultSet.close();
1346         }
1347       }
1348       finally {
1349       }
1350     }
1351     catch (Throwable t) {
1352       logger.error("EntityImages.getImage failed: " + t.toString());
1353       t.printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
1354
1355       try {
1356         connection.setAutoCommit(true);
1357       }
1358       catch (Throwable e) {
1359         logger.error("EntityImages.getImage resetting transaction mode failed: " + e.toString());
1360         e.printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
1361       }
1362
1363       try {
1364         freeConnection(connection, statement);
1365       }
1366       catch (Throwable e) {
1367         logger.error("EntityImages.getImage freeing connection failed: " +e.toString());
1368       }
1369
1370       throw new StorageObjectFailure(t);
1371     }
1372
1373     return imageInputStream;
1374   }
1375
1376   /**
1377    * Sets a binary value for a particular field in a record specified by its identifier
1378    */
1379   public void setBinaryField(String aFieldName, String anObjectId, byte aData[]) throws StorageObjectFailure, SQLException {
1380     PreparedStatement statement = null;
1381     Connection connection = obtainConnection();
1382
1383     try {
1384       connection.setAutoCommit(false);
1385       try {
1386         // are we using bytea ?
1387         if (getFieldType(aFieldName) == java.sql.Types.BINARY) {
1388           statement = connection.prepareStatement(
1389                 "update " + mainTable + " set " + aFieldName + " = ? where " + getIdFieldName() + "=" + Integer.parseInt(anObjectId));
1390           statement.setBytes(1, aData);
1391           statement.execute();
1392           connection.commit();
1393         }
1394         // or the old oid's
1395         else {
1396           PGConnection postgresqlConnection = (org.postgresql.PGConnection) ((DelegatingConnection) connection).getDelegate();
1397           LargeObjectManager lobManager = postgresqlConnection.getLargeObjectAPI();
1398           int oid = lobManager.create(LargeObjectManager.READ | LargeObjectManager.WRITE);
1399           LargeObject obj = lobManager.open(oid, LargeObjectManager.WRITE);  // Now open the file File file =
1400           obj.write(aData);
1401           obj.close();
1402           statement = connection.prepareStatement(
1403                 "update " + mainTable + " set " + aFieldName + " = ? where " + getIdFieldName() + "=" + Integer.parseInt(anObjectId));
1404           statement.setInt(1, oid);
1405           statement.execute();
1406           connection.commit();
1407         }
1408       }
1409       finally {
1410         connection.setAutoCommit(true);
1411       }
1412     }
1413     finally {
1414       freeConnection(connection, statement);
1415     }
1416   }
1417
1418   private void logQueryBefore(String aQuery) {
1419     logger.debug("about to perform QUERY " + aQuery);
1420 //    (new Throwable()).printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
1421   }
1422
1423   private void logQueryAfter(String aQuery, long aTime) {
1424     logger.info("QUERY " + aQuery + " took " + aTime + "ms.");
1425   }
1426
1427   private void logQueryError(String aQuery, long aTime, Throwable anException) {
1428     logger.error("QUERY " + aQuery + " took " + aTime + "ms, but threw exception " + anException.toString());
1429   }
1430
1431   private int getFieldType(String aFieldName) {
1432     if (fieldNameToType == null) {
1433       acquireMetaData();
1434     }
1435
1436     return ((Integer) fieldNameToType.get(aFieldName)).intValue();
1437   }
1438
1439
1440   /**
1441    * a small wrapper class that allows us to store the DB connection resources
1442    * that the BlobInputStream is using and free them upon closing of the stream
1443    */
1444   private class BinaryFieldInputStream extends InputStream {
1445     InputStream inputStream;
1446     Connection connection;
1447     Statement statement;
1448
1449     public BinaryFieldInputStream(InputStream aBlobInputStream, Connection aConnection, Statement aStatement ) {
1450       inputStream = aBlobInputStream;
1451       connection = aConnection;
1452       statement = aStatement;
1453     }
1454
1455     public void close () throws IOException {
1456       inputStream.close();
1457       try {
1458         connection.setAutoCommit(true);
1459         freeConnection(connection, statement);
1460       }
1461       catch (Exception e) {
1462         throw new IOException("close(): "+e.toString());
1463       }
1464     }
1465
1466     public int read() throws IOException {
1467       return inputStream.read();
1468     }
1469   }
1470 }