fbd22384fddb37b7a52af461c76d63aab1fe4655
[mir.git] / source / mir / storage / Database.java
1 /*
2  * Copyright (C) 2001-2005 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.AbstractEntity;
34 import mir.entity.Entity;
35 import mir.entity.EntityList;
36 import mir.entity.StorableObjectEntity;
37 import mir.log.LoggerWrapper;
38 import mir.storage.store.*;
39 import mir.util.JDBCStringRoutines;
40 import mir.util.StreamCopier;
41 import mircoders.global.MirGlobal;
42 import org.apache.commons.dbcp.DelegatingConnection;
43 import org.postgresql.PGConnection;
44 import org.postgresql.largeobject.LargeObject;
45 import org.postgresql.largeobject.LargeObjectManager;
46
47 import java.io.ByteArrayOutputStream;
48 import java.io.InputStream;
49 import java.io.InputStreamReader;
50 import java.sql.*;
51 import java.text.ParseException;
52 import java.text.SimpleDateFormat;
53 import java.util.*;
54
55 /**
56  * Implements database access.
57  *
58  * @version $Id: Database.java,v 1.44.2.35 2005/12/23 21:56:27 zapata Exp $
59  * @author rk
60  * @author Zapata
61  *
62  */
63 public class Database {
64         private static final int DEFAULT_LIMIT = 20;
65   private static final Class GENERIC_ENTITY_CLASS = StorableObjectEntity.class;
66   protected static final ObjectStore o_store = ObjectStore.getInstance();
67
68   protected LoggerWrapper logger;
69
70   protected String mainTable;
71   protected String primaryKeyField = "id";
72
73   private List fieldNames;
74   private int[] fieldTypes;
75   private Map fieldNameToType;
76
77   protected Class entityClass;
78
79   //
80   private Set binaryFields;
81
82   private TimeZone timezone;
83   private SimpleDateFormat userInputDateFormat;
84
85   public Database() throws DatabaseFailure {
86     MirPropertiesConfiguration configuration = MirPropertiesConfiguration.instance();
87     logger = new LoggerWrapper("Database");
88     timezone = TimeZone.getTimeZone(configuration.getString("Mir.DefaultTimezone"));
89
90     userInputDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
91     userInputDateFormat.setTimeZone(timezone);
92
93     binaryFields = new HashSet();
94
95     String adapterName = configuration.getString("Database.Adaptor");
96
97     try {
98       entityClass = GENERIC_ENTITY_CLASS;
99     }
100     catch (Throwable e) {
101       logger.error("Error in Database() constructor with " + adapterName + " -- " + e.getMessage());
102       throw new DatabaseFailure("Error in Database() constructor.", e);
103     }
104   }
105
106   public Class getEntityClass() {
107     return entityClass;
108   }
109
110   public Entity createNewEntity() throws DatabaseFailure {
111     try {
112       AbstractEntity result = (AbstractEntity) entityClass.newInstance();
113       result.setStorage(this);
114
115       return result;
116     }
117     catch (Throwable t) {
118       throw new DatabaseFailure(t);
119     }
120   }
121
122   public String getIdFieldName() {
123     return primaryKeyField;
124   }
125
126   public String getTableName() {
127     return mainTable;
128   }
129
130   /**
131    * Returns a list of field names for this <code>Database</code>
132    */
133   public List getFieldNames() throws DatabaseFailure {
134     if (fieldNames == null) {
135       acquireMetaData();
136     }
137
138     return fieldNames;
139   }
140
141   public boolean hasField(String aFieldName) {
142     return getFieldNames().contains(aFieldName);
143   }
144
145   /**
146    *   Gets value out of ResultSet according to type and converts to String
147    *
148    *   @param aResultSet  ResultSet.
149    *   @param aType  a type from java.sql.Types.*
150    *   @param aFieldIndex  index in ResultSet
151    *   @return returns the value as String. If no conversion is possible
152    *                             /unsupported value/ is returned
153    */
154   private String getValueAsString(ResultSet aResultSet, int aFieldIndex, int aType)
155     throws DatabaseFailure {
156     String outValue = null;
157
158     if (aResultSet != null) {
159       try {
160         switch (aType) {
161           case java.sql.Types.BIT:
162             outValue = (aResultSet.getBoolean(aFieldIndex) == true) ? "1" : "0";
163
164             break;
165
166           case java.sql.Types.INTEGER:
167           case java.sql.Types.SMALLINT:
168           case java.sql.Types.TINYINT:
169           case java.sql.Types.BIGINT:
170
171             int out = aResultSet.getInt(aFieldIndex);
172
173             if (!aResultSet.wasNull()) {
174               outValue = new Integer(out).toString();
175             }
176
177             break;
178
179           case java.sql.Types.NUMERIC:
180             long outl = aResultSet.getLong(aFieldIndex);
181
182             if (!aResultSet.wasNull()) {
183               outValue = new Long(outl).toString();
184             }
185
186             break;
187
188           case java.sql.Types.REAL:
189
190             float tempf = aResultSet.getFloat(aFieldIndex);
191
192             if (!aResultSet.wasNull()) {
193               tempf *= 10;
194               tempf += 0.5;
195
196               int tempf_int = (int) tempf;
197               tempf = (float) tempf_int;
198               tempf /= 10;
199               outValue = "" + tempf;
200               outValue = outValue.replace('.', ',');
201             }
202
203             break;
204
205           case java.sql.Types.DOUBLE:
206
207             double tempd = aResultSet.getDouble(aFieldIndex);
208
209             if (!aResultSet.wasNull()) {
210               tempd *= 10;
211               tempd += 0.5;
212
213               int tempd_int = (int) tempd;
214               tempd = (double) tempd_int;
215               tempd /= 10;
216               outValue = "" + tempd;
217               outValue = outValue.replace('.', ',');
218             }
219
220             break;
221
222           case java.sql.Types.CHAR:
223           case java.sql.Types.VARCHAR:
224           case java.sql.Types.LONGVARCHAR:
225             outValue = aResultSet.getString(aFieldIndex);
226
227             break;
228
229           case java.sql.Types.LONGVARBINARY:
230             outValue = aResultSet.getString(aFieldIndex);
231
232             break;
233
234           case java.sql.Types.TIMESTAMP:
235
236             // it's important to use Timestamp here as getting it
237             // as a string is undefined and is only there for debugging
238             // according to the API. we can make it a string through formatting.
239             // -mh
240             Timestamp timestamp = (aResultSet.getTimestamp(aFieldIndex));
241
242             if (!aResultSet.wasNull()) {
243               java.util.Date date = new java.util.Date(timestamp.getTime());
244               outValue = DatabaseHelper.convertDateToInternalRepresenation(date);
245             }
246
247             break;
248
249           default:
250             outValue = "<unsupported value>";
251             logger.warn("Unsupported Datatype: at " + aFieldIndex + " (" + aType + ")");
252         }
253       }
254       catch (SQLException e) {
255         throw new DatabaseFailure("Could not get Value out of Resultset -- ",
256           e);
257       }
258     }
259
260     return outValue;
261   }
262
263   /**
264    * Return an entity specified by id
265    */
266   public Entity selectById(String anId) throws DatabaseExc {
267     if ((anId == null) || anId.equals("")) {
268       throw new DatabaseExc("Database.selectById: Missing id");
269     }
270
271     // ask object store for object
272     if (StoreUtil.extendsStorableEntity(entityClass)) {
273       String uniqueId = anId;
274
275       if (entityClass.equals(StorableObjectEntity.class)) {
276         uniqueId += ("@" + mainTable);
277       }
278
279       StoreIdentifier search_sid = new StoreIdentifier(entityClass, uniqueId);
280       logger.debug("CACHE: (dbg) looking for sid " + search_sid.toString());
281
282       Entity hit = (Entity) o_store.use(search_sid);
283
284       if (hit != null) {
285         return hit;
286       }
287     }
288
289     Connection con = obtainConnection();
290     Entity returnEntity = null;
291     PreparedStatement statement = null;
292
293     try {
294       ResultSet rs;
295       String query = "select * from " + mainTable + " where " + primaryKeyField + " = ?";
296
297       statement = con.prepareStatement(query);
298       statement.setString(1, anId);
299
300       logQueryBefore(query);
301
302       long startTime = System.currentTimeMillis();
303       try {
304         rs = statement.executeQuery();
305
306         logQueryAfter(query, (System.currentTimeMillis() - startTime));
307       }
308       catch (SQLException e) {
309         logQueryError(query, (System.currentTimeMillis() - startTime), e);
310         throw e;
311       }
312
313       if (rs != null) {
314         if (rs.next()) {
315           returnEntity = makeEntityFromResultSet(rs);
316         }
317         else {
318           logger.warn("No data for id: " + anId + " in table " + mainTable);
319         }
320
321         rs.close();
322       }
323       else {
324         logger.warn("No Data for Id " + anId + " in Table " + mainTable);
325       }
326     }
327     catch (Throwable e) {
328       throw new DatabaseFailure(e);
329     }
330     finally {
331       freeConnection(con, statement);
332     }
333
334     return returnEntity;
335   }
336
337   public EntityList selectByWhereClauseWithExtraTables(String mainTablePrefix, List extraTables, String aWhereClause) throws DatabaseExc, DatabaseFailure {
338         return selectByWhereClause( mainTablePrefix, extraTables, aWhereClause, "", 0, DEFAULT_LIMIT);
339   }
340
341   public EntityList selectByFieldValue(String aField, String aValue) throws DatabaseExc, DatabaseFailure {
342     return selectByFieldValue(aField, aValue, 0);
343   }
344
345   public EntityList selectByFieldValue(String aField, String aValue, int offset) throws DatabaseExc, DatabaseFailure {
346     return selectByWhereClause(aField + "='" + JDBCStringRoutines.escapeStringLiteral(aValue)+"'", offset);
347   }
348
349   public EntityList selectByWhereClause(String where) throws DatabaseExc, DatabaseFailure {
350     return selectByWhereClause(where, 0);
351   }
352
353   public EntityList selectByWhereClause(String whereClause, int offset) throws DatabaseExc, DatabaseFailure {
354     return selectByWhereClause(whereClause, null, offset);
355   }
356
357   public EntityList selectByWhereClause(String mainTablePrefix, List extraTables, String where, String order) throws DatabaseExc, DatabaseFailure {
358     return selectByWhereClause(mainTablePrefix, extraTables, where, order, 0, DEFAULT_LIMIT);
359   }
360
361   public EntityList selectByWhereClause(String whereClause, String orderBy, int offset) throws DatabaseExc, DatabaseFailure {
362     return selectByWhereClause(whereClause, orderBy, offset, DEFAULT_LIMIT);
363   }
364
365   public EntityList selectByWhereClause(String aWhereClause, String anOrderByClause,
366             int offset, int limit) throws DatabaseExc, DatabaseFailure {
367     return selectByWhereClause("", null, aWhereClause, anOrderByClause, offset, limit);
368   }
369
370   public EntityList selectByWhereClause(
371       String aMainTablePrefix, List anExtraTables,
372       String aWhereClause, String anOrderByClause,
373                         int anOffset, int aLimit) throws DatabaseExc, DatabaseFailure {
374
375     if (anExtraTables!=null && ((String) anExtraTables.get(0)).trim().equals("")){
376       anExtraTables=null;
377     }
378
379     // check o_store for entitylist
380     // only if no relational select
381     if (anExtraTables==null) {
382       if (StoreUtil.extendsStorableEntity(entityClass)) {
383          StoreIdentifier searchSid = new StoreIdentifier(entityClass,
384                StoreContainerType.STOC_TYPE_ENTITYLIST,
385                StoreUtil.getEntityListUniqueIdentifierFor(mainTable,
386                 aWhereClause, anOrderByClause, anOffset, aLimit));
387          EntityList hit = (EntityList) o_store.use(searchSid);
388
389          if (hit != null) {
390             return hit;
391          }
392       }
393     }
394
395     RecordRetriever retriever = new RecordRetriever(mainTable, aMainTablePrefix);
396
397     EntityList result = null;
398     Connection connection = null;
399
400     if (anExtraTables!=null) {
401       Iterator i = anExtraTables.iterator();
402       while (i.hasNext()) {
403         String table = (String) i.next();
404         if (!"".equals(table)) {
405           retriever.addExtraTable(table);
406         }
407       }
408     }
409
410     if (aWhereClause != null) {
411       retriever.appendWhereClause(aWhereClause);
412     }
413
414     if ((anOrderByClause != null) && !(anOrderByClause.trim().length() == 0)) {
415       retriever.appendOrderByClause(anOrderByClause);
416     }
417
418     if (anOffset>-1 && aLimit>-1) {
419       retriever.setLimit(aLimit+1);
420       retriever.setOffset(anOffset);
421     }
422
423     Iterator i = getFieldNames().iterator();
424     while (i.hasNext()) {
425       retriever.addField((String) i.next());
426     }
427
428     // execute sql
429     try {
430       connection = obtainConnection();
431       ResultSet resultSet = retriever.execute(connection);
432
433       boolean hasMore = false;
434
435       if (resultSet != null) {
436         result = new EntityList();
437         Entity entity;
438         int position = 0;
439
440         while (((aLimit == -1) || (position<aLimit)) && resultSet.next()) {
441           entity = makeEntityFromResultSet(resultSet);
442           result.add(entity);
443           position++;
444         }
445
446         hasMore = resultSet.next();
447         resultSet.close();
448       }
449
450       if (result != null) {
451         result.setOffset(anOffset);
452         result.setWhere(aWhereClause);
453         result.setOrder(anOrderByClause);
454         result.setStorage(this);
455         result.setLimit(aLimit);
456
457         if (hasMore) {
458           result.setNextBatch(anOffset + aLimit);
459         }
460
461         if (anExtraTables==null && StoreUtil.extendsStorableEntity(entityClass)) {
462           StoreIdentifier sid = result.getStoreIdentifier();
463           logger.debug("CACHE (add): " + sid.toString());
464           o_store.add(sid);
465         }
466       }
467     }
468     catch (Throwable e) {
469       throw new DatabaseFailure(e);
470     }
471     finally {
472       try {
473         if (connection != null) {
474           freeConnection(connection);
475         }
476       } catch (Throwable t) {
477       }
478     }
479
480     return result;
481   }
482
483   private Entity makeEntityFromResultSet(ResultSet rs) {
484     Map fields = new HashMap();
485     String theResult = null;
486     int type;
487     Entity returnEntity = null;
488
489     try {
490       if (StoreUtil.extendsStorableEntity(entityClass)) {
491          StoreIdentifier searchSid = StorableObjectEntity.getStoreIdentifier(this,
492                entityClass, rs);
493          Entity hit = (Entity) o_store.use(searchSid);
494          if (hit != null) return hit;
495       }
496
497       for (int i = 0; i < getFieldNames().size(); i++) {
498         type = fieldTypes[i];
499
500         if (type == java.sql.Types.LONGVARBINARY) {
501           InputStreamReader is =
502             (InputStreamReader) rs.getCharacterStream(i + 1);
503
504           if (is != null) {
505             char[] data = new char[32768];
506             StringBuffer theResultString = new StringBuffer();
507             int len;
508
509             while ((len = is.read(data)) > 0) {
510               theResultString.append(data, 0, len);
511             }
512
513             is.close();
514             theResult = theResultString.toString();
515           }
516           else {
517             theResult = null;
518           }
519         }
520         else {
521           theResult = getValueAsString(rs, (i + 1), type);
522         }
523
524         if (theResult != null) {
525           fields.put(getFieldNames().get(i), theResult);
526         }
527       }
528
529       if (entityClass != null) {
530         returnEntity = createNewEntity();
531         returnEntity.setFieldValues(fields);
532
533         if (returnEntity instanceof StorableObject) {
534           logger.debug("CACHE: ( in) " + returnEntity.getId() + " :" + mainTable);
535           o_store.add(((StorableObject) returnEntity).getStoreIdentifier());
536         }
537       }
538       else {
539         throw new DatabaseExc("Internal Error: entityClass not set!");
540       }
541     }
542     catch (Throwable e) {
543       throw new DatabaseFailure(e);
544     }
545
546     return returnEntity;
547   }
548
549   /**
550    * Inserts an entity into the database.
551    *
552    * @param anEntity
553    * @return the value of the primary key of the inserted record
554    */
555   public String insert(Entity anEntity) throws DatabaseFailure {
556     invalidateStore();
557
558     RecordInserter inserter =
559         new RecordInserter(mainTable, getPrimaryKeySequence());
560
561     String returnId = null;
562     Connection con = null;
563
564     try {
565       String fieldName;
566
567       // make sql-string
568       for (int i = 0; i < getFieldNames().size(); i++) {
569         fieldName = (String) getFieldNames().get(i);
570
571         if (!fieldName.equals(primaryKeyField)) {
572           // exceptions
573           if (!anEntity.hasFieldValue(fieldName) && (
574               fieldName.equals("webdb_create") ||
575               fieldName.equals("webdb_lastchange"))) {
576             inserter.assignVerbatim(fieldName, "now()");
577           }
578           else {
579             if (anEntity.hasFieldValue(fieldName)) {
580               inserter.assignString(fieldName, anEntity.getFieldValue(fieldName));
581             }
582           }
583         }
584       }
585
586       con = obtainConnection();
587       returnId = inserter.execute(con);
588
589       anEntity.setId(returnId);
590     }
591     finally {
592       freeConnection(con);
593     }
594
595     return returnId;
596   }
597
598   /**
599    * Updates an entity in the database
600    *
601    * @param theEntity
602    */
603   public void update(Entity theEntity) throws DatabaseFailure {
604     invalidateStore();
605
606     RecordUpdater generator = new RecordUpdater(getTableName(), theEntity.getId());
607
608     // build sql statement
609     for (int i = 0; i < getFieldNames().size(); i++) {
610       String field = (String) getFieldNames().get(i);
611
612       if (!(field.equals(primaryKeyField) ||
613             "webdb_create".equals(field) ||
614             "webdb_lastchange".equals(field) ||
615             binaryFields.contains(field))) {
616
617         if (theEntity.hasFieldValue(field)) {
618           generator.assignString(field, theEntity.getFieldValue(field));
619         }
620       }
621     }
622
623     // exceptions
624     if (hasField("webdb_lastchange")) {
625       generator.assignVerbatim("webdb_lastchange", "now()");
626     }
627
628     // special case: the webdb_create requires the field in yyyy-mm-dd HH:mm
629     // format so anything extra will be ignored. -mh
630     if (hasField("webdb_create") &&
631         theEntity.hasFieldValue("webdb_create")) {
632       // minimum of 10 (yyyy-mm-dd)...
633       if (theEntity.getFieldValue("webdb_create").length() >= 10) {
634         String dateString = theEntity.getFieldValue("webdb_create");
635
636         // if only 10, then add 00:00 so it doesn't throw a ParseException
637         if (dateString.length() == 10) {
638           dateString = dateString + " 00:00";
639         }
640
641         // TimeStamp stuff
642         try {
643           java.util.Date d = userInputDateFormat.parse(dateString);
644           generator.assignDateTime("webdb_create", d);
645         }
646         catch (ParseException e) {
647           throw new DatabaseFailure(e);
648         }
649       }
650     }
651     Connection connection = null;
652
653     try {
654       connection = obtainConnection();
655       generator.execute(connection);
656     }
657     finally {
658       freeConnection(connection);
659     }
660   }
661   
662   private void invalidateObject(String anId) {
663     // ostore send notification
664     if (StoreUtil.extendsStorableEntity(entityClass)) {
665       String uniqueId = anId;
666
667       if (entityClass.equals(StorableObjectEntity.class)) {
668         uniqueId += ("@" + mainTable);
669       }
670
671       logger.debug("CACHE: (del) " + anId);
672
673       StoreIdentifier search_sid =
674         new StoreIdentifier(entityClass,
675           StoreContainerType.STOC_TYPE_ENTITY, uniqueId);
676       o_store.invalidate(search_sid);
677     }
678   }
679
680   /*
681   *   delete-Operator
682   *   @param id des zu loeschenden Datensatzes
683   *   @return boolean liefert true zurueck, wenn loeschen erfolgreich war.
684    */
685   public boolean delete(String id) throws DatabaseFailure {
686         invalidateObject(id);
687         
688     int resultCode = 0;
689     Connection connection = obtainConnection();
690     PreparedStatement statement = null;
691
692     try {
693         statement = connection.prepareStatement("delete from " + mainTable + " where " + primaryKeyField + "=?");
694             statement.setInt(1, Integer.parseInt(id));
695             logQueryBefore("delete from " + mainTable + " where " + primaryKeyField + "=" + id + "");
696             resultCode = statement.executeUpdate();
697     }
698     catch (SQLException e) {
699         logger.warn("Can't delete record", e);
700     }
701     finally {
702       freeConnection(connection, statement);
703     }
704
705     invalidateStore();
706
707     return (resultCode > 0) ? true : false;
708   }
709
710   /**
711    * Deletes entities based on a where clause
712    */
713   public int deleteByWhereClause(String aWhereClause) throws DatabaseFailure {
714     invalidateStore();
715
716     Statement stmt = null;
717     Connection con = null;
718     int res = 0;
719     String sql =
720       "delete from " + mainTable + " where " + aWhereClause;
721
722     //theLog.printInfo("DELETE " + sql);
723     try {
724       con = obtainConnection();
725       stmt = con.createStatement();
726       res = stmt.executeUpdate(sql);
727     }
728     catch (Throwable e) {
729       throw new DatabaseFailure(e);
730     }
731     finally {
732       freeConnection(con, stmt);
733     }
734
735     return res;
736   }
737
738   /* noch nicht implementiert.
739   * @return immer false
740    */
741   public boolean delete(EntityList theEntityList) {
742     return false;
743   }
744
745   public ResultSet executeSql(Statement stmt, String sql)
746                             throws DatabaseFailure, SQLException {
747     ResultSet rs;
748     logQueryBefore(sql);
749     long startTime = System.currentTimeMillis();
750     try {
751       rs = stmt.executeQuery(sql);
752
753       logQueryAfter(sql, (System.currentTimeMillis() - startTime));
754     }
755     catch (SQLException e) {
756       logQueryError(sql, (System.currentTimeMillis() - startTime), e);
757       throw e;
758     }
759
760     return rs;
761   }
762
763   private Map processRow(ResultSet aResultSet) throws DatabaseFailure {
764     try {
765       Map result = new HashMap();
766       ResultSetMetaData metaData = aResultSet.getMetaData();
767       int nrColumns = metaData.getColumnCount();
768       for (int i=0; i<nrColumns; i++) {
769         result.put(metaData.getColumnName(i+1), getValueAsString(aResultSet, i+1, metaData.getColumnType(i+1)));
770       }
771
772       return result;
773     }
774     catch (Throwable e) {
775       throw new DatabaseFailure(e);
776     }
777   }
778
779   /**
780    * Executes 1 sql statement and returns the results as a <code>List</code> of
781    * <code>Map</code>s
782    */
783   public List executeFreeSql(String sql, int aLimit) throws DatabaseFailure, DatabaseExc {
784     Connection connection = null;
785     Statement statement = null;
786     try {
787       List result = new ArrayList();
788       connection = obtainConnection();
789       statement = connection.createStatement();
790       ResultSet resultset = executeSql(statement, sql);
791       try {
792         while (resultset.next() && result.size() < aLimit) {
793           result.add(processRow(resultset));
794         }
795       }
796       finally {
797         resultset.close();
798       }
799
800       return result;
801     }
802     catch (Throwable e) {
803       throw new DatabaseFailure(e);
804     }
805     finally {
806       if (connection!=null) {
807         freeConnection(connection, statement);
808       }
809     }
810   }
811
812   /**
813    * Executes 1 sql statement and returns the first result row as a <code>Map</code>s
814    * (<code>null</code> if there wasn't any row)
815    */
816   public Map executeFreeSingleRowSql(String anSqlStatement) throws DatabaseFailure, DatabaseExc {
817     try {
818       List resultList = executeFreeSql(anSqlStatement, 1);
819       try {
820         if (resultList.size()>0)
821           return (Map) resultList.get(0);
822                                 return null;
823       }
824       finally {
825       }
826     }
827     catch (Throwable t) {
828       throw new DatabaseFailure(t);
829     }
830   }
831
832   /**
833    * Executes 1 sql statement and returns the first column of the first result row as a <code>String</code>s
834    * (<code>null</code> if there wasn't any row)
835    */
836   public String executeFreeSingleValueSql(String sql) throws DatabaseFailure, DatabaseExc {
837     Map row = executeFreeSingleRowSql(sql);
838
839     if (row==null)
840       return null;
841
842     Iterator i = row.values().iterator();
843     if (i.hasNext())
844       return (String) i.next();
845                 return null;
846   }
847
848   public int getSize(String where) throws SQLException, DatabaseFailure {
849     return getSize("", null, where);
850   }
851   /**
852    * returns the number of rows in the table
853    */
854   public int getSize(String mainTablePrefix, List extraTables, String where) throws SQLException, DatabaseFailure {
855
856     String useTable = mainTable;
857     if (mainTablePrefix!=null && mainTablePrefix.trim().length()>0) {
858       useTable+=" "+mainTablePrefix;
859     }
860     StringBuffer countSql =
861       new StringBuffer("select count(*) from ").append(useTable);
862         // append extratables, if necessary
863       if (extraTables!=null) {
864         for (int i=0;i < extraTables.size();i++) {
865           if (!extraTables.get(i).equals("")) {
866             countSql.append( ", " + extraTables.get(i));
867           }
868         }
869       }
870
871     if ((where != null) && (where.length() != 0)) {
872       countSql.append( " where " + where);
873     }
874
875     Connection con = null;
876     Statement stmt = null;
877     int result = 0;
878     logQueryBefore(countSql.toString());
879     long startTime = System.currentTimeMillis();
880
881     try {
882       con = obtainConnection();
883       stmt = con.createStatement();
884
885       ResultSet rs = executeSql(stmt, countSql.toString());
886
887       while (rs.next()) {
888         result = rs.getInt(1);
889       }
890     }
891     catch (SQLException e) {
892       logger.error("Database.getSize: " + e.getMessage());
893     }
894     finally {
895       freeConnection(con, stmt);
896     }
897     logQueryAfter(countSql.toString(), (System.currentTimeMillis() - startTime));
898
899     return result;
900   }
901
902   public int executeUpdate(Statement stmt, String sql)
903     throws DatabaseFailure, SQLException {
904     int rs;
905
906     logQueryBefore(sql);
907     long startTime = System.currentTimeMillis();
908
909     try {
910       rs = stmt.executeUpdate(sql);
911
912       logQueryAfter(sql, (System.currentTimeMillis() - startTime));
913     }
914     catch (SQLException e) {
915       logQueryError(sql, (System.currentTimeMillis() - startTime), e);
916       throw e;
917     }
918
919     return rs;
920   }
921
922   public int executeUpdate(String sql)
923     throws DatabaseFailure, SQLException {
924     int result = -1;
925     Connection con = null;
926     PreparedStatement pstmt = null;
927
928     logQueryBefore(sql);
929     long startTime = System.currentTimeMillis();
930     try {
931       con = obtainConnection();
932       pstmt = con.prepareStatement(sql);
933       result = pstmt.executeUpdate();
934       logQueryAfter(sql, System.currentTimeMillis() - startTime);
935     }
936     catch (Throwable e) {
937       logQueryError(sql, System.currentTimeMillis() - startTime, e);
938       throw new DatabaseFailure("Database.executeUpdate(" + sql + "): " + e.getMessage(), e);
939     }
940     finally {
941       freeConnection(con, pstmt);
942     }
943     return result;
944   }
945
946   /**
947    * Processes the metadata for the table this Database object is responsible for.
948    */
949   private void processMetaData(ResultSetMetaData aMetaData) throws DatabaseFailure {
950     fieldNames = new ArrayList();
951     fieldNameToType = new HashMap();
952
953     try {
954       int numFields = aMetaData.getColumnCount();
955       fieldTypes = new int[numFields];
956
957       for (int i = 1; i <= numFields; i++) {
958         fieldNames.add(aMetaData.getColumnName(i));
959         fieldTypes[i - 1] = aMetaData.getColumnType(i);
960         fieldNameToType.put(aMetaData.getColumnName(i), new Integer(aMetaData.getColumnType(i)));
961       }
962     }
963     catch (Throwable e) {
964       throw new DatabaseFailure(e);
965     }
966   }
967
968   /**
969    * Retrieves metadata from the table this Database object represents
970    */
971   private void acquireMetaData() throws DatabaseFailure {
972     Connection connection = null;
973     PreparedStatement statement = null;
974     String sql = "select * from " + mainTable + " where 0=1";
975
976     try {
977       connection = obtainConnection();
978       statement = connection.prepareStatement(sql);
979
980       logger.debug("METADATA: " + sql);
981       ResultSet resultSet = statement.executeQuery();
982       try {
983         processMetaData(resultSet.getMetaData());
984       }
985       finally {
986         resultSet.close();
987       }
988     }
989     catch (Throwable e) {
990       throw new DatabaseFailure(e);
991     }
992     finally {
993       freeConnection(connection, statement);
994     }
995   }
996
997   public Connection obtainConnection() throws DatabaseFailure {
998     try {
999       return MirGlobal.getDatabaseEngine().obtainConnection();
1000     }
1001     catch (Exception e) {
1002       throw new DatabaseFailure(e);
1003     }
1004   }
1005
1006   public void freeConnection(Connection aConnection) throws DatabaseFailure {
1007     try {
1008       MirGlobal.getDatabaseEngine().releaseConnection(aConnection);
1009     }
1010     catch (Throwable t) {
1011       logger.warn("Can't release connection: " + t.toString());
1012     }
1013   }
1014
1015   public void freeConnection(Connection aConnection, Statement aStatement) throws DatabaseFailure {
1016     try {
1017       aStatement.close();
1018     }
1019     catch (Throwable t) {
1020       logger.warn("Can't close statement", t);
1021     }
1022
1023     freeConnection(aConnection);
1024   }
1025
1026   protected void _throwStorageObjectException(Exception e, String aFunction)
1027     throws DatabaseFailure {
1028
1029     if (e != null) {
1030       logger.error(e.getMessage() + aFunction);
1031       throw new DatabaseFailure(aFunction, e);
1032     }
1033   }
1034
1035
1036   /**
1037    * Invalidates any cached entity list
1038    */
1039   private void invalidateStore() {
1040     // invalidating all EntityLists corresponding with entityClass
1041     if (StoreUtil.extendsStorableEntity(entityClass)) {
1042       StoreContainerType stoc_type =
1043         StoreContainerType.valueOf(entityClass, StoreContainerType.STOC_TYPE_ENTITYLIST);
1044       o_store.invalidate(stoc_type);
1045     }
1046   }
1047
1048   /**
1049    * Retrieves a binary value
1050    */
1051   public byte[] getBinaryField(String aQuery) throws DatabaseFailure, SQLException {
1052     Connection connection=null;
1053     Statement statement=null;
1054     InputStream inputStream;
1055
1056     try {
1057       connection = obtainConnection();
1058       try {
1059         connection.setAutoCommit(false);
1060         statement = connection.createStatement();
1061         ResultSet resultSet = executeSql(statement, aQuery);
1062
1063         if(resultSet!=null) {
1064           if (resultSet.next()) {
1065             if (resultSet.getMetaData().getColumnType(1) == java.sql.Types.BINARY) {
1066               return resultSet.getBytes(1);
1067             }
1068             else {
1069               inputStream = resultSet.getBlob(1).getBinaryStream();
1070               ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
1071               StreamCopier.copy(inputStream, outputStream);
1072               return outputStream.toByteArray();
1073             }
1074           }
1075           resultSet.close();
1076         }
1077       }
1078       finally {
1079         try {
1080           connection.setAutoCommit(true);
1081         }
1082         catch (Throwable e) {
1083           logger.error("EntityImages.getImage resetting transaction mode failed: " + e.toString());
1084           e.printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
1085         }
1086
1087         try {
1088           freeConnection(connection, statement);
1089         }
1090         catch (Throwable e) {
1091           logger.error("EntityImages.getImage freeing connection failed: " +e.toString());
1092         }
1093
1094       }
1095     }
1096     catch (Throwable t) {
1097       logger.error("EntityImages.getImage failed: " + t.toString());
1098       t.printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
1099
1100       throw new DatabaseFailure(t);
1101     }
1102
1103     return new byte[0];
1104   }
1105
1106   /**
1107    * Sets a binary value for a particular field in a record specified by its identifier
1108    */
1109   public void setBinaryField(String aFieldName, String anObjectId, byte aData[]) throws DatabaseFailure, SQLException {
1110     PreparedStatement statement = null;
1111     Connection connection = obtainConnection();
1112
1113     try {
1114       connection.setAutoCommit(false);
1115       try {
1116         // are we using bytea ?
1117         if (getFieldType(aFieldName) == java.sql.Types.BINARY) {
1118           statement = connection.prepareStatement(
1119                 "update " + mainTable + " set " + aFieldName + " = ? where " + getIdFieldName() + "=" + Integer.parseInt(anObjectId));
1120           statement.setBytes(1, aData);
1121           statement.execute();
1122           connection.commit();
1123         }
1124         // or the old oid's
1125         else {
1126           PGConnection postgresqlConnection = (org.postgresql.PGConnection) ((DelegatingConnection) connection).getDelegate();
1127           LargeObjectManager lobManager = postgresqlConnection.getLargeObjectAPI();
1128           int oid = lobManager.create(LargeObjectManager.READ | LargeObjectManager.WRITE);
1129           LargeObject obj = lobManager.open(oid, LargeObjectManager.WRITE);  // Now open the file File file =
1130           obj.write(aData);
1131           obj.close();
1132           statement = connection.prepareStatement(
1133                 "update " + mainTable + " set " + aFieldName + " = ? where " + getIdFieldName() + "=" + Integer.parseInt(anObjectId));
1134           statement.setInt(1, oid);
1135           statement.execute();
1136           connection.commit();
1137         }
1138       }
1139       finally {
1140         connection.setAutoCommit(true);
1141       }
1142     }
1143     finally {
1144       freeConnection(connection, statement);
1145     }
1146   }
1147
1148   /**
1149    * Can be overridden to specify a primary key sequence name not named according to
1150    * the convention (tablename _id_seq)
1151    */
1152   protected String getPrimaryKeySequence() {
1153     return mainTable+"_id_seq";
1154   }
1155
1156   /**
1157    * Can be called by subclasses to specify fields that are binary, and that shouldn't
1158    * be updated outside of {@link #setBinaryField}
1159    *
1160    * @param aBinaryField The field name of the binary field
1161    */
1162   protected void markBinaryField(String aBinaryField) {
1163     binaryFields.add(aBinaryField);
1164   }
1165
1166   private void logQueryBefore(String aQuery) {
1167     logger.debug("about to perform QUERY " + aQuery);
1168 //    (new Throwable()).printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
1169   }
1170
1171   private void logQueryAfter(String aQuery, long aTime) {
1172     logger.info("QUERY " + aQuery + " took " + aTime + "ms.");
1173   }
1174
1175   private void logQueryError(String aQuery, long aTime, Throwable anException) {
1176     logger.error("QUERY " + aQuery + " took " + aTime + "ms, but threw exception " + anException.toString());
1177   }
1178
1179   private int getFieldType(String aFieldName) {
1180     if (fieldNameToType == null) {
1181       acquireMetaData();
1182     }
1183
1184     return ((Integer) fieldNameToType.get(aFieldName)).intValue();
1185   }
1186 }