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