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