e8faec5380ee209bba4c5e73865f023bc1e53f56
[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.*;
49 import java.sql.*;
50 import java.text.ParseException;
51 import java.text.SimpleDateFormat;
52 import java.util.*;
53
54 /**
55  * Implements database access.
56  *
57  * @version $Id: Database.java,v 1.44.2.32 2005/04/16 18:37:23 zapata Exp $
58  * @author rk
59  * @author Zapata
60  *
61  */
62 public class Database {
63         private static int DEFAULT_LIMIT = 20;
64   private static Class GENERIC_ENTITY_CLASS = mir.entity.StorableObjectEntity.class;
65   protected static final ObjectStore o_store = ObjectStore.getInstance();
66   private static final int _millisPerHour = 60 * 60 * 1000;
67
68   protected LoggerWrapper logger;
69
70   protected String mainTable;
71   protected String primaryKeyField = "id";
72
73   protected List fieldNames;
74   private int[] fieldTypes;
75   private Map fieldNameToType;
76
77   protected Class entityClass;
78
79   //
80   private Set binaryFields;
81
82   TimeZone timezone;
83   SimpleDateFormat internalDateFormat;
84   SimpleDateFormat userInputDateFormat;
85
86   public Database() throws DatabaseFailure {
87     MirPropertiesConfiguration configuration = MirPropertiesConfiguration.instance();
88     logger = new LoggerWrapper("Database");
89     timezone = TimeZone.getTimeZone(configuration.getString("Mir.DefaultTimezone"));
90     internalDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
91     internalDateFormat.setTimeZone(timezone);
92
93     userInputDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
94     userInputDateFormat.setTimeZone(timezone);
95
96     binaryFields = new HashSet();
97
98     String theAdaptorName = configuration.getString("Database.Adaptor");
99
100     try {
101       entityClass = GENERIC_ENTITY_CLASS;
102     }
103     catch (Throwable e) {
104       logger.error("Error in Database() constructor with " + theAdaptorName + " -- " + e.getMessage());
105       throw new DatabaseFailure("Error in Database() constructor.", e);
106     }
107   }
108
109   public Class getEntityClass() {
110     return entityClass;
111   }
112
113   public Entity createNewEntity() throws DatabaseFailure {
114     try {
115       AbstractEntity result = (AbstractEntity) entityClass.newInstance();
116       result.setStorage(this);
117
118       return result;
119     }
120     catch (Throwable t) {
121       throw new DatabaseFailure(t);
122     }
123   }
124
125   public String getIdFieldName() {
126     return primaryKeyField;
127   }
128
129   public String getTableName() {
130     return mainTable;
131   }
132
133   /**
134    * Returns a list of field names for this <code>Database</code>
135    */
136   public List getFieldNames() throws DatabaseFailure {
137     if (fieldNames == null) {
138       acquireMetaData();
139     }
140
141     return fieldNames;
142   }
143
144   public boolean hasField(String aFieldName) {
145     return getFieldNames().contains(aFieldName);
146   }
147
148   /**
149    *   Gets value out of ResultSet according to type and converts to String
150    *
151    *   @param aResultSet  ResultSet.
152    *   @param aType  a type from java.sql.Types.*
153    *   @param aFieldIndex  index in ResultSet
154    *   @return returns the value as String. If no conversion is possible
155    *                             /unsupported value/ is returned
156    */
157   private String getValueAsString(ResultSet aResultSet, int aFieldIndex, int aType)
158     throws DatabaseFailure {
159     String outValue = null;
160
161     if (aResultSet != null) {
162       try {
163         switch (aType) {
164           case java.sql.Types.BIT:
165             outValue = (aResultSet.getBoolean(aFieldIndex) == true) ? "1" : "0";
166
167             break;
168
169           case java.sql.Types.INTEGER:
170           case java.sql.Types.SMALLINT:
171           case java.sql.Types.TINYINT:
172           case java.sql.Types.BIGINT:
173
174             int out = aResultSet.getInt(aFieldIndex);
175
176             if (!aResultSet.wasNull()) {
177               outValue = new Integer(out).toString();
178             }
179
180             break;
181
182           case java.sql.Types.NUMERIC:
183             /** todo Numeric can be float or double depending upon
184              *  metadata.getScale() / especially with oracle */
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     RecordRetriever retriever = new RecordRetriever(mainTable, aMainTablePrefix);
397
398     // check o_store for entitylist
399     // only if no relational select
400     if (anExtraTables==null) {
401       if (StoreUtil.extendsStorableEntity(entityClass)) {
402          StoreIdentifier searchSid = new StoreIdentifier(entityClass,
403                StoreContainerType.STOC_TYPE_ENTITYLIST,
404                StoreUtil.getEntityListUniqueIdentifierFor(mainTable,
405                 aWhereClause, anOrderByClause, anOffset, aLimit));
406          EntityList hit = (EntityList) o_store.use(searchSid);
407
408          if (hit != null) {
409             return hit;
410          }
411       }
412     }
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     PreparedStatement pstmt = null;
581
582     try {
583       String fieldName;
584
585       // make sql-string
586       for (int i = 0; i < getFieldNames().size(); i++) {
587         fieldName = (String) getFieldNames().get(i);
588
589         if (!fieldName.equals(primaryKeyField)) {
590           // exceptions
591           if (!anEntity.hasFieldValue(fieldName) && (
592               fieldName.equals("webdb_create") ||
593               fieldName.equals("webdb_lastchange"))) {
594             inserter.assignVerbatim(fieldName, "now()");
595           }
596           else {
597             if (anEntity.hasFieldValue(fieldName)) {
598               inserter.assignString(fieldName, anEntity.getFieldValue(fieldName));
599             }
600           }
601         }
602       }
603
604       con = obtainConnection();
605       returnId = inserter.execute(con);
606
607       anEntity.setId(returnId);
608     }
609     finally {
610       freeConnection(con);
611     }
612
613     /** todo store entity in o_store */
614     return returnId;
615   }
616
617   /**
618    * Updates an entity in the database
619    *
620    * @param theEntity
621    */
622   public void update(Entity theEntity) throws DatabaseFailure {
623     Connection connection = null;
624
625     invalidateStore();
626
627     RecordUpdater generator = new RecordUpdater(getTableName(), theEntity.getId());
628
629     String field;
630
631     // build sql statement
632     for (int i = 0; i < getFieldNames().size(); i++) {
633       field = (String) getFieldNames().get(i);
634
635       if (!(field.equals(primaryKeyField) ||
636             field.equals("webdb_create") ||
637             field.equals("webdb_lastchange") ||
638             binaryFields.contains(field))) {
639
640         if (theEntity.hasFieldValue(field)) {
641           generator.assignString(field, theEntity.getFieldValue(field));
642         }
643       }
644     }
645
646     // exceptions
647     if (hasField("webdb_lastchange")) {
648       generator.assignVerbatim("webdb_lastchange", "now()");
649     }
650
651     // special case: the webdb_create requires the field in yyyy-mm-dd HH:mm
652     // format so anything extra will be ignored. -mh
653     if (hasField("webdb_create") &&
654         theEntity.hasFieldValue("webdb_create")) {
655       // minimum of 10 (yyyy-mm-dd)...
656       if (theEntity.getFieldValue("webdb_create").length() >= 10) {
657         String dateString = theEntity.getFieldValue("webdb_create");
658
659         // if only 10, then add 00:00 so it doesn't throw a ParseException
660         if (dateString.length() == 10) {
661           dateString = dateString + " 00:00";
662         }
663
664         // TimeStamp stuff
665         try {
666           java.util.Date d = userInputDateFormat.parse(dateString);
667           generator.assignDateTime("webdb_create", d);
668         }
669         catch (ParseException e) {
670           throw new DatabaseFailure(e);
671         }
672       }
673     }
674
675     try {
676       connection = obtainConnection();
677       generator.execute(connection);
678     }
679     finally {
680       freeConnection(connection);
681     }
682   }
683   
684   private void invalidateObject(String anId) {
685     // ostore send notification
686     if (StoreUtil.extendsStorableEntity(entityClass)) {
687       String uniqueId = anId;
688
689       if (entityClass.equals(StorableObjectEntity.class)) {
690         uniqueId += ("@" + mainTable);
691       }
692
693       logger.debug("CACHE: (del) " + anId);
694
695       StoreIdentifier search_sid =
696         new StoreIdentifier(entityClass,
697           StoreContainerType.STOC_TYPE_ENTITY, uniqueId);
698       o_store.invalidate(search_sid);
699     }
700   }
701
702   /*
703   *   delete-Operator
704   *   @param id des zu loeschenden Datensatzes
705   *   @return boolean liefert true zurueck, wenn loeschen erfolgreich war.
706    */
707   public boolean delete(String id) throws DatabaseFailure {
708         invalidateObject(id);
709         
710     /** todo could be prepared Statement */
711     int resultCode = 0;
712     Connection connection = obtainConnection();
713     PreparedStatement statement = null;
714
715     try {
716         statement = connection.prepareStatement("delete from " + mainTable + " where " + primaryKeyField + "=?");
717             statement.setInt(1, Integer.parseInt(id));
718             logQueryBefore("delete from " + mainTable + " where " + primaryKeyField + "=" + id + "");
719             resultCode = statement.executeUpdate();
720     }
721     catch (SQLException e) {
722         logger.warn("Can't delete record", e);
723     }
724     finally {
725       freeConnection(connection, statement);
726     }
727
728     invalidateStore();
729
730     return (resultCode > 0) ? true : false;
731   }
732
733   /**
734    * Deletes entities based on a where clause
735    */
736   public int deleteByWhereClause(String aWhereClause) throws DatabaseFailure {
737     invalidateStore();
738
739     Statement stmt = null;
740     Connection con = null;
741     int res = 0;
742     String sql =
743       "delete from " + mainTable + " where " + aWhereClause;
744
745     //theLog.printInfo("DELETE " + sql);
746     try {
747       con = obtainConnection();
748       stmt = con.createStatement();
749       res = stmt.executeUpdate(sql);
750     }
751     catch (Throwable e) {
752       throw new DatabaseFailure(e);
753     }
754     finally {
755       freeConnection(con, stmt);
756     }
757
758     return res;
759   }
760
761   /* noch nicht implementiert.
762   * @return immer false
763    */
764   public boolean delete(EntityList theEntityList) {
765     return false;
766   }
767
768   public ResultSet executeSql(Statement stmt, String sql)
769                             throws DatabaseFailure, SQLException {
770     ResultSet rs;
771     logQueryBefore(sql);
772     long startTime = System.currentTimeMillis();
773     try {
774       rs = stmt.executeQuery(sql);
775
776       logQueryAfter(sql, (System.currentTimeMillis() - startTime));
777     }
778     catch (SQLException e) {
779       logQueryError(sql, (System.currentTimeMillis() - startTime), e);
780       throw e;
781     }
782
783     return rs;
784   }
785
786   private Map processRow(ResultSet aResultSet) throws DatabaseFailure {
787     try {
788       Map result = new HashMap();
789       ResultSetMetaData metaData = aResultSet.getMetaData();
790       int nrColumns = metaData.getColumnCount();
791       for (int i=0; i<nrColumns; i++) {
792         result.put(metaData.getColumnName(i+1), getValueAsString(aResultSet, i+1, metaData.getColumnType(i+1)));
793       }
794
795       return result;
796     }
797     catch (Throwable e) {
798       throw new DatabaseFailure(e);
799     }
800   }
801
802   /**
803    * Executes 1 sql statement and returns the results as a <code>List</code> of
804    * <code>Map</code>s
805    */
806   public List executeFreeSql(String sql, int aLimit) throws DatabaseFailure, DatabaseExc {
807     Connection connection = null;
808     Statement statement = null;
809     try {
810       List result = new ArrayList();
811       connection = obtainConnection();
812       statement = connection.createStatement();
813       ResultSet resultset = executeSql(statement, sql);
814       try {
815         while (resultset.next() && result.size() < aLimit) {
816           result.add(processRow(resultset));
817         }
818       }
819       finally {
820         resultset.close();
821       }
822
823       return result;
824     }
825     catch (Throwable e) {
826       throw new DatabaseFailure(e);
827     }
828     finally {
829       if (connection!=null) {
830         freeConnection(connection, statement);
831       }
832     }
833   }
834
835   /**
836    * Executes 1 sql statement and returns the first result row as a <code>Map</code>s
837    * (<code>null</code> if there wasn't any row)
838    */
839   public Map executeFreeSingleRowSql(String anSqlStatement) throws DatabaseFailure, DatabaseExc {
840     try {
841       List resultList = executeFreeSql(anSqlStatement, 1);
842       try {
843         if (resultList.size()>0)
844           return (Map) resultList.get(0);
845                                 return null;
846       }
847       finally {
848       }
849     }
850     catch (Throwable t) {
851       throw new DatabaseFailure(t);
852     }
853   }
854
855   /**
856    * Executes 1 sql statement and returns the first column of the first result row as a <code>String</code>s
857    * (<code>null</code> if there wasn't any row)
858    */
859   public String executeFreeSingleValueSql(String sql) throws DatabaseFailure, DatabaseExc {
860     Map row = executeFreeSingleRowSql(sql);
861
862     if (row==null)
863       return null;
864
865     Iterator i = row.values().iterator();
866     if (i.hasNext())
867       return (String) i.next();
868                 return null;
869   }
870
871   public int getSize(String where) throws SQLException, DatabaseFailure {
872     return getSize("", null, where);
873   }
874   /**
875    * returns the number of rows in the table
876    */
877   public int getSize(String mainTablePrefix, List extraTables, String where) throws SQLException, DatabaseFailure {
878
879     String useTable = mainTable;
880     if (mainTablePrefix!=null && mainTablePrefix.trim().length()>0) {
881       useTable+=" "+mainTablePrefix;
882     }
883     StringBuffer countSql =
884       new StringBuffer("select count(*) from ").append(useTable);
885         // append extratables, if necessary
886       if (extraTables!=null) {
887         for (int i=0;i < extraTables.size();i++) {
888           if (!extraTables.get(i).equals("")) {
889             countSql.append( ", " + extraTables.get(i));
890           }
891         }
892       }
893
894     if ((where != null) && (where.length() != 0)) {
895       countSql.append( " where " + where);
896     }
897
898     Connection con = null;
899     Statement stmt = null;
900     int result = 0;
901     logQueryBefore(countSql.toString());
902     long startTime = System.currentTimeMillis();
903
904     try {
905       con = obtainConnection();
906       stmt = con.createStatement();
907
908       ResultSet rs = executeSql(stmt, countSql.toString());
909
910       while (rs.next()) {
911         result = rs.getInt(1);
912       }
913     }
914     catch (SQLException e) {
915       logger.error("Database.getSize: " + e.getMessage());
916     }
917     finally {
918       freeConnection(con, stmt);
919     }
920     logQueryAfter(countSql.toString(), (System.currentTimeMillis() - startTime));
921
922     return result;
923   }
924
925   public int executeUpdate(Statement stmt, String sql)
926     throws DatabaseFailure, SQLException {
927     int rs;
928
929     logQueryBefore(sql);
930     long startTime = System.currentTimeMillis();
931
932     try {
933       rs = stmt.executeUpdate(sql);
934
935       logQueryAfter(sql, (System.currentTimeMillis() - startTime));
936     }
937     catch (SQLException e) {
938       logQueryError(sql, (System.currentTimeMillis() - startTime), e);
939       throw e;
940     }
941
942     return rs;
943   }
944
945   public int executeUpdate(String sql)
946     throws DatabaseFailure, SQLException {
947     int result = -1;
948     Connection con = null;
949     PreparedStatement pstmt = null;
950
951     logQueryBefore(sql);
952     long startTime = System.currentTimeMillis();
953     try {
954       con = obtainConnection();
955       pstmt = con.prepareStatement(sql);
956       result = pstmt.executeUpdate();
957       logQueryAfter(sql, System.currentTimeMillis() - startTime);
958     }
959     catch (Throwable e) {
960       logQueryError(sql, System.currentTimeMillis() - startTime, e);
961       throw new DatabaseFailure("Database.executeUpdate(" + sql + "): " + e.getMessage(), e);
962     }
963     finally {
964       freeConnection(con, pstmt);
965     }
966     return result;
967   }
968
969   /**
970    * Processes the metadata for the table this Database object is responsible for.
971    */
972   private void processMetaData(ResultSetMetaData aMetaData) throws DatabaseFailure {
973     fieldNames = new ArrayList();
974     fieldNameToType = new HashMap();
975
976     try {
977       int numFields = aMetaData.getColumnCount();
978       fieldTypes = new int[numFields];
979
980       for (int i = 1; i <= numFields; i++) {
981         fieldNames.add(aMetaData.getColumnName(i));
982         fieldTypes[i - 1] = aMetaData.getColumnType(i);
983         fieldNameToType.put(aMetaData.getColumnName(i), new Integer(aMetaData.getColumnType(i)));
984       }
985     }
986     catch (Throwable e) {
987       throw new DatabaseFailure(e);
988     }
989   }
990
991   /**
992    * Retrieves metadata from the table this Database object represents
993    */
994   private void acquireMetaData() throws DatabaseFailure {
995     Connection connection = null;
996     PreparedStatement statement = null;
997     String sql = "select * from " + mainTable + " where 0=1";
998
999     try {
1000       connection = obtainConnection();
1001       statement = connection.prepareStatement(sql);
1002
1003       logger.debug("METADATA: " + sql);
1004       ResultSet resultSet = statement.executeQuery();
1005       try {
1006         processMetaData(resultSet.getMetaData());
1007       }
1008       finally {
1009         resultSet.close();
1010       }
1011     }
1012     catch (Throwable e) {
1013       throw new DatabaseFailure(e);
1014     }
1015     finally {
1016       freeConnection(connection, statement);
1017     }
1018   }
1019
1020   public Connection obtainConnection() throws DatabaseFailure {
1021     try {
1022       return MirGlobal.getDatabaseEngine().obtainConnection();
1023     }
1024     catch (Exception e) {
1025       throw new DatabaseFailure(e);
1026     }
1027   }
1028
1029   public void freeConnection(Connection aConnection) throws DatabaseFailure {
1030     try {
1031       MirGlobal.getDatabaseEngine().releaseConnection(aConnection);
1032     }
1033     catch (Throwable t) {
1034       logger.warn("Can't release connection: " + t.toString());
1035     }
1036   }
1037
1038   public void freeConnection(Connection aConnection, Statement aStatement) throws DatabaseFailure {
1039     try {
1040       aStatement.close();
1041     }
1042     catch (Throwable t) {
1043       logger.warn("Can't close statement", t);
1044       t.printStackTrace(logger.asPrintWriter(LoggerWrapper.ERROR_MESSAGE));
1045     }
1046
1047     freeConnection(aConnection);
1048   }
1049
1050   protected void _throwStorageObjectException(Exception e, String aFunction)
1051     throws DatabaseFailure {
1052
1053     if (e != null) {
1054       logger.error(e.getMessage() + aFunction);
1055       throw new DatabaseFailure(aFunction, e);
1056     }
1057   }
1058
1059
1060   /**
1061    * Invalidates any cached entity list
1062    */
1063   private void invalidateStore() {
1064     // invalidating all EntityLists corresponding with entityClass
1065     if (StoreUtil.extendsStorableEntity(entityClass)) {
1066       StoreContainerType stoc_type =
1067         StoreContainerType.valueOf(entityClass, StoreContainerType.STOC_TYPE_ENTITYLIST);
1068       o_store.invalidate(stoc_type);
1069     }
1070   }
1071
1072   /**
1073    * Retrieves a binary value
1074    */
1075   public byte[] getBinaryField(String aQuery) throws DatabaseFailure, SQLException {
1076     Connection connection=null;
1077     Statement statement=null;
1078     InputStream inputStream;
1079
1080     try {
1081       connection = obtainConnection();
1082       try {
1083         connection.setAutoCommit(false);
1084         statement = connection.createStatement();
1085         ResultSet resultSet = executeSql(statement, aQuery);
1086
1087         if(resultSet!=null) {
1088           if (resultSet.next()) {
1089             if (resultSet.getMetaData().getColumnType(1) == java.sql.Types.BINARY) {
1090               return resultSet.getBytes(1);
1091             }
1092             else {
1093               inputStream = resultSet.getBlob(1).getBinaryStream();
1094               ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
1095               StreamCopier.copy(inputStream, outputStream);
1096               return outputStream.toByteArray();
1097             }
1098           }
1099           resultSet.close();
1100         }
1101       }
1102       finally {
1103         try {
1104           connection.setAutoCommit(true);
1105         }
1106         catch (Throwable e) {
1107           logger.error("EntityImages.getImage resetting transaction mode failed: " + e.toString());
1108           e.printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
1109         }
1110
1111         try {
1112           freeConnection(connection, statement);
1113         }
1114         catch (Throwable e) {
1115           logger.error("EntityImages.getImage freeing connection failed: " +e.toString());
1116         }
1117
1118       }
1119     }
1120     catch (Throwable t) {
1121       logger.error("EntityImages.getImage failed: " + t.toString());
1122       t.printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
1123
1124       throw new DatabaseFailure(t);
1125     }
1126
1127     return new byte[0];
1128   }
1129
1130   /**
1131    * Sets a binary value for a particular field in a record specified by its identifier
1132    */
1133   public void setBinaryField(String aFieldName, String anObjectId, byte aData[]) throws DatabaseFailure, SQLException {
1134     PreparedStatement statement = null;
1135     Connection connection = obtainConnection();
1136
1137     try {
1138       connection.setAutoCommit(false);
1139       try {
1140         // are we using bytea ?
1141         if (getFieldType(aFieldName) == java.sql.Types.BINARY) {
1142           statement = connection.prepareStatement(
1143                 "update " + mainTable + " set " + aFieldName + " = ? where " + getIdFieldName() + "=" + Integer.parseInt(anObjectId));
1144           statement.setBytes(1, aData);
1145           statement.execute();
1146           connection.commit();
1147         }
1148         // or the old oid's
1149         else {
1150           PGConnection postgresqlConnection = (org.postgresql.PGConnection) ((DelegatingConnection) connection).getDelegate();
1151           LargeObjectManager lobManager = postgresqlConnection.getLargeObjectAPI();
1152           int oid = lobManager.create(LargeObjectManager.READ | LargeObjectManager.WRITE);
1153           LargeObject obj = lobManager.open(oid, LargeObjectManager.WRITE);  // Now open the file File file =
1154           obj.write(aData);
1155           obj.close();
1156           statement = connection.prepareStatement(
1157                 "update " + mainTable + " set " + aFieldName + " = ? where " + getIdFieldName() + "=" + Integer.parseInt(anObjectId));
1158           statement.setInt(1, oid);
1159           statement.execute();
1160           connection.commit();
1161         }
1162       }
1163       finally {
1164         connection.setAutoCommit(true);
1165       }
1166     }
1167     finally {
1168       freeConnection(connection, statement);
1169     }
1170   }
1171
1172   /**
1173    * Can be overridden to specify a primary key sequence name not named according to
1174    * the convention (tablename _id_seq)
1175    */
1176   protected String getPrimaryKeySequence() {
1177     return mainTable+"_id_seq";
1178   }
1179
1180   /**
1181    * Can be called by subclasses to specify fields that are binary, and that shouldn't
1182    * be updated outside of {@link #setBinaryField}
1183    *
1184    * @param aBinaryField The field name of the binary field
1185    */
1186   protected void markBinaryField(String aBinaryField) {
1187     binaryFields.add(aBinaryField);
1188   }
1189
1190   private void logQueryBefore(String aQuery) {
1191     logger.debug("about to perform QUERY " + aQuery);
1192 //    (new Throwable()).printStackTrace(logger.asPrintWriter(LoggerWrapper.DEBUG_MESSAGE));
1193   }
1194
1195   private void logQueryAfter(String aQuery, long aTime) {
1196     logger.info("QUERY " + aQuery + " took " + aTime + "ms.");
1197   }
1198
1199   private void logQueryError(String aQuery, long aTime, Throwable anException) {
1200     logger.error("QUERY " + aQuery + " took " + aTime + "ms, but threw exception " + anException.toString());
1201   }
1202
1203   private int getFieldType(String aFieldName) {
1204     if (fieldNameToType == null) {
1205       acquireMetaData();
1206     }
1207
1208     return ((Integer) fieldNameToType.get(aFieldName)).intValue();
1209   }
1210 }