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