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