ch.elexis.data.PersistentObject.java Source code

Java tutorial

Introduction

Here is the source code for ch.elexis.data.PersistentObject.java

Source

/*******************************************************************************
 * Copyright (c) 2005-2011, G. Weirich and Elexis
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    G. Weirich - initial implementation
 *******************************************************************************/

package ch.elexis.data;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.URI;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import javax.xml.datatype.XMLGregorianCalendar;

import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.ConfigurationScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.statushandlers.StatusManager;

import ch.elexis.Hub;
import ch.elexis.StringConstants;
import ch.elexis.actions.ElexisEvent;
import ch.elexis.actions.ElexisEventDispatcher;
import ch.elexis.admin.AccessControl;
import ch.elexis.core.PersistenceException;
import ch.elexis.core.data.IChangeListener;
import ch.elexis.core.data.IPersistentObject;
import ch.elexis.core.data.ISticker;
import ch.elexis.core.data.IXid;
import ch.elexis.core.data.Query.Term;
import ch.elexis.data.Xid.XIDException;
import ch.elexis.data.cache.IPersistentObjectCache;
import ch.elexis.data.cache.SoftCache;
import ch.elexis.dialogs.ErsterMandantDialog;
import ch.elexis.preferences.PreferenceConstants;
import ch.elexis.preferences.PreferenceInitializer;
import ch.elexis.status.ElexisStatus;
import ch.elexis.util.DBUpdate;
import ch.elexis.util.Log;
import ch.elexis.util.SWTHelper;
import ch.elexis.util.SqlWithUiRunner;
import ch.elexis.wizards.DBConnectWizard;
import ch.rgw.compress.CompEx;
import ch.rgw.io.Settings;
import ch.rgw.io.SqlSettings;
import ch.rgw.tools.ExHandler;
import ch.rgw.tools.JdbcLink;
import ch.rgw.tools.JdbcLink.Stm;
import ch.rgw.tools.JdbcLinkConcurrencyException;
import ch.rgw.tools.JdbcLinkException;
import ch.rgw.tools.JdbcLinkResourceException;
import ch.rgw.tools.JdbcLinkSyntaxException;
import ch.rgw.tools.StringTool;
import ch.rgw.tools.TimeTool;
import ch.rgw.tools.VersionInfo;
import ch.rgw.tools.VersionedResource;
import ch.rgw.tools.net.NetTool;

/**
 * Base class for all objects to be stored in the database. A PersistentObject has an unique ID,
 * which is assigned as the object is created. Every object is accessed "lazily" which means that
 * "loading" an object instantiates only a proxy with the ID of the requested object. Members are
 * read only as needed. The class provides static functions to log into the database, and provides
 * methods for reading and writing of fields for derived classes. The get method uses a cache to
 * reduce the number of costly database operations. Repeated read-requests within a configurable
 * life-time (defaults to 15 seconds) are satisfied from the cache. PersistentObject can log every
 * write-access in a trace-table, as desired. get- and set- methods perform necessary
 * coding/decoding of fields as needed.
 * 
 * Basisklasse fr alle Objekte, die in der Datenbank gespeichert werden sollen. Ein
 * PersistentObject hat eine eindeutige ID, welche beim Erstellen des Objekts automatisch vergeben
 * wird. Grundstzlich wird jedes Objekt "lazy" geladen, indem jede Leseanforderung zunchst nur
 * einen mit der ID des Objekts versehenen Proxy instantiiert und jedes Member-Feld erst auf Anfrage
 * nachldt. Die Klasse stellt statische Funktionen zur Kontaktaufnahme mit der Datenbank und
 * member-Funktionen zum Lesen und Schreiben von Feldern der Tochterobjekte zur Verfgung. Die
 * get-Methode verwendet einen zeitlich limitierten Cache. um die Zahl teurer Datenbankoperationen
 * zu minimieren: Wiederholte Lesezugriffe innerhalb einer einstellbaren lifetime (Standardmssig 15
 * Sekunden) werden aus dem cache bedient. PersistentObject kann auch alle Schreibvorgnge in einer
 * speziellen Trace-Tabelle dokumentieren. Die get- und set- Methoden kmmern sich selbst um
 * codierung/decodierung der Felder, wenn ntig. Aufeinanderfolgende und streng zusammengehrende
 * Schreibvorgnge knnen auch in einer Transaktion zusammengefasst werden, welche nur ganz oder gar
 * nicht ausgefhrt wird. (begin()). Es ist aber zu beachten, das nicht alle Datenbanken
 * Transaktionen untersttzen. MySQL beispielsweise nur, wenn es mit InnoDB-Tabellen eingerichtet
 * wurde (welche langsamer sind, als die standardmssig verwendeten MyISAM-Tabellen).
 * 
 * @author gerry
 */
public abstract class PersistentObject implements IPersistentObject {
    protected static final String MAPPING_ERROR_MARKER = "**ERROR:";
    public static final String CFG_CONNECTSTRING = "connectionstring";
    public static final String CFG_TYPE = "typ";
    public static final String CFG_PWD = "pwd";
    public static final String CFG_USER = "user";
    public static final String CFG_DRIVER = "driver";
    public static final String FLD_EXTINFO = "ExtInfo";
    public static final String FLD_DELETED = "deleted";
    public static final String FLD_LASTUPDATE = "lastupdate";
    protected static final String DATE_COMPOUND = "Datum=S:D:Datum";
    public static final String FLD_DATE = "Datum";
    public static final int CACHE_DEFAULT_LIFETIME = 15;
    public static final int CACHE_MIN_LIFETIME = 5;

    // maximum character length of int fields in tables
    private static int MAX_INT_LENGTH = 10;

    private static JdbcLink j = null;
    private static JdbcLink testJdbcLink = null;
    protected static Log log = Log.get("PersistentObject");
    private String id;
    private static Hashtable<String, String> mapping;
    private static IPersistentObjectCache<String> cache;
    private static String username;
    private static String pcname;
    private static String tracetable;
    protected static int default_lifetime;
    private static boolean runningAsTest = false;
    private static String dbUser;
    private static String dbPw;

    static {
        mapping = new Hashtable<String, String>();
        default_lifetime = Hub.localCfg.get(PreferenceConstants.ABL_CACHELIFETIME, CACHE_DEFAULT_LIFETIME);
        if (default_lifetime < CACHE_MIN_LIFETIME) {
            default_lifetime = CACHE_MIN_LIFETIME;
            Hub.localCfg.set(PreferenceConstants.ABL_CACHELIFETIME, CACHE_MIN_LIFETIME);
        }

        cache = new SoftCache<String>(3000, 0.7f);
        // cache=new EhBasedCache<String>(null);
        /*
         * cacheCleaner=new Job("CacheCleaner"){ @Override protected IStatus run(final
         * IProgressMonitor monitor) { cache.purge(); schedule(60000L); return Status.OK_STATUS; }
         * }; cacheCleaner.setUser(false); cacheCleaner.setPriority(Job.DECORATE);
         */
        // cacheCleaner.schedule(300000L);
        log.log("Cache setup: default_lifetime " + default_lifetime, Log.INFOS);
    }

    public static enum FieldType {
        TEXT, LIST, JOINT
    };

    /**
     * Connect to a database.
     * 
     * In the first place, the method checks if there is a demoDB in the Elexis base directory. If
     * found, only this database will be used. If not, connection parameters are taken from the
     * provided Settings. If there ist no database found, it will be created newly, using the
     * createDB-Script. After successful connection, the global Settings (Hub.globalCfg) are linked
     * to the database.
     * 
     * For automated testing the following rules apply:
     * 
     * The methods check whether the properties ch.elexis.* are set. If set, Elexis will open the
     * corresponding database. E.g -Dch.elexis.username=test -Dch.elexis.password=test
     * -Dch.elexis.dbUser=elexis -Dch.elexis.dbPw=elexisTest -Dch.elexis.dbFlavor=mysql
     * -Dch.elexis.dbSpec=jdbc:mysql://jenkins-service:3306/miniDB
     * 
     * If the property elexis-run-mode is set to RunFromScratch then the connected database will be
     * wiped out and initialized with default values for the mandant (007, topsecret). For mysql and
     * postgresql this will only work if the database is empty! Therefore you mus call something
     * like ""drop database miniDB; create dabase miniDB;" before starting Elexis.
     * 
     * @return true on success
     * 
     *         Verbindung mit der Datenbank herstellen. Die Verbindungsparameter werden aus den
     *         bergebenen Settings entnommen. Falls am angegebenen Ort keine Datenbank gefunden
     *         wird, wird eine neue erstellt, falls ein create-Script fr diesen Datenbanktyp unter
     *         rsc gefunden wurde. Wenn die Verbindung hergestell werden konnte, werden die global
     *         Settings mit dieser Datenbank verbunden.
     * @return true fr ok, false wenn keine Verbindung hergestellt werden konnte.
     */
    public static boolean connect(final Settings cfg, final Shell loginshell) {
        dbUser = System.getProperty("ch.elexis.dbUser");
        dbPw = System.getProperty("ch.elexis.dbPw");
        String dbFlavor = System.getProperty("ch.elexis.dbFlavor");
        String dbSpec = System.getProperty("ch.elexis.dbSpec");
        if ("RunFromScratch".equals(System.getProperty("elexis-run-mode"))) {
            runningAsTest = true;
        }
        File base = new File(Hub.getBasePath());
        File demo = new File(base.getParentFile().getParent() + File.separator + "demoDB");
        log.log("Verzeichnis Demo-Datenbank via Hub.getBasePath(): " + demo.getAbsolutePath(), Log.INFOS);
        log.log("osgi.install.area: " + System.getProperty("osgi.install.area"), Log.INFOS);
        String demo2path = org.eclipse.core.runtime.Platform.getInstanceLocation().getURL().getPath() + "demoDB";
        File demo2 = new File(demo2path);
        if (demo2.exists()) {
            demo = demo2;
        }

        if (!demo.exists()) {
            URI demoName = URI.create(System.getProperty("osgi.install.area").replaceAll(" ", "%20"));
            demo = new File(demoName.getPath() + File.separator + "demoDB");
            log.log("Verzeichnis Demo-Datenbank via osgi.install.area: " + demo.getAbsolutePath(), Log.INFOS);
        }
        if (demo.exists() && demo.isDirectory()) {
            j = JdbcLink.createH2Link(demo.getAbsolutePath() + File.separator + "db");
            try {
                getConnection().connect("sa", StringTool.leer);
                return connect(getConnection());
            } catch (JdbcLinkException je) {
                ElexisStatus status = translateJdbcException(je);
                status.setMessage(status.getMessage()
                        + " Fehler mit Demo-Datenbank: Es wurde zwar ein demoDB-Verzeichnis gefunden, aber dort ist keine verwendbare Datenbank");
                throw new PersistenceException(status);
            }
        } else if (dbFlavor != null && dbFlavor.length() >= 2 && dbSpec != null && dbSpec.length() > 5
                && dbUser != null && dbPw != null) {
            log.log("Using " + dbFlavor + " " + dbSpec + " " + dbUser, Log.INFOS);
            String driver;
            if (dbFlavor.equalsIgnoreCase("mysql"))
                driver = "com.mysql.jdbc.Driver";
            else if (dbFlavor.equalsIgnoreCase("postgresql"))
                driver = "org.postgresql.Driver";
            else if (dbFlavor.equalsIgnoreCase("h2"))
                driver = "org.h2.Driver";
            else
                driver = "invalid";
            if (!driver.equalsIgnoreCase("invalid")) {
                try {
                    j = new JdbcLink(driver, dbSpec, dbFlavor);
                    if (getConnection().connect(dbUser, dbPw)) {
                        testJdbcLink = j;
                        return connect(getConnection());
                    } else {
                        log.log("can't connect to test database" + dbSpec, Log.FATALS);
                        System.exit(-6);
                    }
                } catch (Exception ex) {
                    log.log(ex, "can't connect to test database" + dbSpec, Log.FATALS);
                    System.exit(-7);
                }
            }
            log.log("can't connect to test database. invalid dbFlavor" + dbFlavor, Log.FATALS);
            System.exit(-7);
        } else if (runningAsTest) {
            try {
                File dbFile = File.createTempFile("elexis", "db");
                log.log("RunFromScratch test database created in " + dbFile.getAbsolutePath(), Log.INFOS);
                dbUser = "sa";
                dbPw = StringTool.leer;
                j = JdbcLink.createH2Link(dbFile.getAbsolutePath());
                if (getConnection().connect(dbUser, dbPw)) {
                    testJdbcLink = j;
                    return connect(getConnection());
                } else {
                    log.log("can't create test database", Log.FATALS);
                    System.exit(-6);
                }
            } catch (Exception ex) {
                log.log(ex, "can't create test database", Log.FATALS);
                System.exit(-7);
            }
        }

        // IPreferenceStore localstore = new SettingsPreferenceStore(cfg);
        /*
         * String driver = localstore.getString(PreferenceConstants.DB_CLASS); String connectstring
         * = localstore .getString(PreferenceConstants.DB_CONNECT);
         * 
         * String user = localstore.getString(PreferenceConstants.DB_USERNAME); String pwd =
         * localstore.getString(PreferenceConstants.DB_PWD); String typ =
         * localstore.getString(PreferenceConstants.DB_TYP);
         */
        String driver = "";
        String user = "";
        String pwd = "";
        String typ = "";
        String connectstring = "";
        Hashtable<Object, Object> hConn = null;
        String connection = Hub.getCfgVariant();
        ConfigurationScope pref = new ConfigurationScope();
        IEclipsePreferences node = pref.getNode("connection");
        String cnt = node.get(connection, null);
        if (cnt != null) {
            hConn = fold(StringTool.dePrintable(cnt));
            if (hConn != null) {
                driver = checkNull((String) hConn.get(CFG_DRIVER));
                user = checkNull((String) hConn.get(CFG_USER));
                pwd = checkNull((String) hConn.get(CFG_PWD));
                typ = checkNull((String) hConn.get(CFG_TYPE));
                connectstring = checkNull((String) hConn.get(CFG_CONNECTSTRING));
            }
        }
        log.log("Driver is " + driver, Log.INFOS);
        if (StringTool.leer.equals(driver)) {
            String provider = System.getProperty("elexis-provider");
            log.log("Provider is " + provider, Log.INFOS);
            if ((provider != null) && provider.startsWith("Medelexis")) {
                WizardDialog wd = new WizardDialog(loginshell, new DBConnectWizard());
                wd.create();
                SWTHelper.center(wd.getShell());
                wd.open();
                Hub.localCfg.flush();
                SWTHelper.showInfo("Datenbankverbindung gendert", "Bitte starten Sie Elexis erneut");
                System.exit(-1);
            } else {
                String d = PreferenceInitializer.getDefaultDBPath();
                j = JdbcLink.createH2Link(d + File.separator + "elexisdb");
                user = "sa";
                pwd = StringTool.leer;
                typ = getConnection().DBFlavor;
            }
        } else {
            j = new JdbcLink(driver, connectstring, typ);
        }
        try {
            getConnection().connect(user, pwd);
        } catch (JdbcLinkException je) {
            ElexisStatus status = translateJdbcException(je);
            status.setLogLevel(ElexisStatus.LOG_FATALS);
            throw new PersistenceException(status);
        }
        log.log("Verbunden mit " + getConnection().dbDriver() + ", " + connectstring, Log.SYNCMARK);
        return connect(getConnection());
    }

    public static boolean connect(final JdbcLink jd) {
        j = jd;
        if (tableExists("CONFIG")) {
            if (runningAsTest) {
                log.log("With elexis-run-mode=RunFromScratch and MySQL/postgres you must start with an empty database",
                        Log.ERRORS);
                System.exit(-8);

            }
            Hub.globalCfg = new SqlSettings(getConnection(), "CONFIG");
            String created = Hub.globalCfg.get("created", null);
            log.log("Database version " + created, Log.SYNCMARK);
        } else {
            log.log("No Version found. Creating new Database", Log.SYNCMARK);
            java.io.InputStream is = null;
            Stm stm = null;
            try {
                String createscript = Hub.getBasePath() + File.separator + "rsc" + File.separator
                        + "createDB.script";
                is = new FileInputStream(createscript);
                stm = getConnection().getStatement();
                if (stm.execScript(is, true, true) == true) {
                    Log.setAlertLevel(Log.FATALS);
                    Hub.globalCfg = new SqlSettings(getConnection(), "CONFIG");
                    Hub.globalCfg.undo();
                    Hub.globalCfg.set("created", new TimeTool().toString(TimeTool.FULL_GER));
                    Hub.acl.load();
                    Anwender.init();
                    Mandant.init();
                    Hub.pin.initializeGrants();
                    Hub.pin.initializeGlobalPreferences();
                    if (runningAsTest) {
                        Mandant m = new Mandant("007", "topsecret");
                        String clientEmail = System.getProperty("ch.elexis.clientEmail");
                        if (clientEmail == null)
                            clientEmail = "james@bond.invalid";
                        m.set(new String[] { Person.NAME, Person.FIRSTNAME, Person.TITLE, Person.SEX,
                                Person.FLD_E_MAIL, Person.FLD_PHONE1, Person.FLD_FAX, Kontakt.FLD_STREET,
                                Kontakt.FLD_ZIP, Kontakt.FLD_PLACE }, "Bond", "James", "Dr. med.", Person.MALE,
                                clientEmail, "0061 555 55 55", "0061 555 55 56", "10, Baker Street", "9999",
                                "Elexikon");
                        String gprs = m.getInfoString(AccessControl.KEY_GROUPS); //$NON-NLS-1$
                        gprs = StringConstants.ROLE_ADMIN + "," + StringConstants.ROLE_USERS;
                        m.setInfoElement(AccessControl.KEY_GROUPS, gprs);

                    } else {
                        new ErsterMandantDialog(Hub.getActiveShell()).open();
                    }
                    Hub.globalCfg.flush();
                    Hub.localCfg.flush();
                    disconnect();
                    if (runningAsTest) {
                        runningAsTest = false; // Avoid recursion!!
                        JdbcLink jReconnect = new JdbcLink(testJdbcLink.getDriverName(),
                                testJdbcLink.getConnectString(), testJdbcLink.DBFlavor);
                        jReconnect.connect(dbUser, dbPw);
                        return connect(jReconnect);
                    }
                    MessageDialog.openInformation(null, "Programmende",
                            "Es wurde eine neue Datenbank angelegt. Das Programm muss beendet werden. Bitte starten Sie danach neu.");
                    System.exit(1);
                } else {
                    log.log("Kein create script fr Datenbanktyp " + getConnection().DBFlavor + " gefunden.",
                            Log.ERRORS);
                    return false;
                }
            } catch (Throwable ex) {
                ExHandler.handle(ex);
                return false;
            } finally {
                getConnection().releaseStatement(stm);
                try {
                    is.close();
                } catch (Exception ex) {
                    /* Janusode */
                }
            }
        }
        // Zugriffskontrolle initialisieren
        Hub.acl.load();
        VersionInfo vi = new VersionInfo(Hub.globalCfg.get("dbversion", "0.0.0"));
        log.log("Verlangte Datenbankversion: " + Hub.DBVersion, Log.INFOS);
        log.log("Gefundene Datenbankversion: " + vi.version(), Log.INFOS);
        if (vi.isOlder(Hub.DBVersion)) {
            log.log("ltere Version der Datenbank gefunden ", Log.WARNINGS);
            DBUpdate.doUpdate();
        }
        vi = new VersionInfo(Hub.globalCfg.get("ElexisVersion", "0.0.0"));
        log.log("Verlangte Elexis-Version: " + vi.version(), Log.INFOS);
        log.log("Vorhandene Elexis-Version: " + Hub.Version, Log.INFOS);
        VersionInfo v2 = new VersionInfo(Hub.Version);
        if (vi.isNewerMinor(v2)) {
            String msg = String.format(
                    "Die Datenbank %1s ist fr eine neuere Elexisversion '%2s' als die aufgestartete '%3s'. Bitte machen Sie ein Update.",
                    jd.getConnectString(), vi.version().toString(), v2.version().toString());
            log.log(msg, Log.FATALS);
            SWTHelper.showError("Verbindung nicht mglich: Aufstartete Elexis-Version zu alt", msg);
            System.exit(2);
        }
        // Wenn trace global eingeschaltet ist, gilt es fr alle
        setTrace(Hub.globalCfg.get(PreferenceConstants.ABL_TRACE, null));
        // wenn trace global nicht eingeschaltet ist, kann es immer noch fr
        // diese
        // Station eingeschaltet sein
        if (tracetable == null) {
            setTrace(Hub.localCfg.get(PreferenceConstants.ABL_TRACE, null));
        }
        return true;
    }

    /**
     * Return the Object containing the cdecodeonnection. This should only in very specific
     * conditions be neccessary, if one needs a direkt access to the database. It is strongly
     * recommended to use this only very carefully, as callers must ensure for themselves that their
     * code works with different database engines equally.
     * 
     * Das Objekt, das die Connection enthlt zurckliefern. Sollte nur in Ausnahmefllen ntig
     * sein, wenn doch mal ein direkter Zugriff auf die Datenbank erforderlich ist.
     * 
     * @return den JdbcLink, der die Verbindung zur Datenbank enthlt
     */
    public static JdbcLink getConnection() {
        return j;
    }

    /**
     * Die Zuordnung von Membervariablen zu Datenbankfeldern geschieht ber statische mappings: Jede
     * abgeleitete Klassen muss ihre mappings in folgender Form deklarieren:
     * addMapping("Tabellenname","Variable=Feld"...); wobei:
     * <ul>
     * <li>"Variable=Feld" - Einfache Zuordnung, Variable wird zu Feld</li>
     * <li>"Variable=S:x:Feld" - Spezielle Abspeicherung<br>
     * x=D - Datumsfeld, wird automatisch in Standardformat gebracht<br>
     * x=C - Feld wird vor Abspeicherung komprimiert</li>
     * X=N - Feld wird als Long interrpetiert
     * <li>"Variable=JOINT:FremdID:EigeneID:Tabelle[:type]" - n:m - Zuordnungen</li>
     * <li>"Variable=LIST:EigeneID:Tabelle:orderby[:type]" - 1:n - Zuordnungen</li>
     * <li>"Variable=EXT:tabelle:feld" - Das Feld ist in der genannten externen Tabelle
     * </ul>
     */
    static protected void addMapping(final String prefix, final String... map) {
        for (String s : map) {
            String[] def = s.trim().split("[ \t]*=[ \t]*");
            if (def.length != 2) {
                mapping.put(prefix + def[0], def[0]);
            } else {
                mapping.put(prefix + def[0], def[1]);
            }
        }
        mapping.put(prefix + "deleted", "deleted");
        mapping.put(prefix + "lastupdate", "lastupdate");
    }

    /**
     * Trace (protokollieren aller Schreibvorgnge) ein- und ausschalten. Die Trace-Tabelle muss
     * folgende Spalten haben: logtime (long), Workstation (VARCHAR), Username(Varchar), action
     * (Text/Longvarchar)
     * 
     * @param Tablename
     *            Name der Trace-tabelle oder null: Trace aus.
     */
    public static void setTrace(String Tablename) {
        if ((Tablename != null) && (Tablename.equals("none") || Tablename.equals(""))) {
            Tablename = null;
        }
        tracetable = Tablename;
        username = JdbcLink.wrap(System.getProperty("user.name"));
        pcname = JdbcLink.wrap(NetTool.hostname);
    }

    /**
     * Exklusiven Zugriff auf eine Ressource verlangen. Die Sperre kann fr maximal zwei Sekunden
     * beansprucht werden, dann wird sie gelst. Dies ist eine sehr teure Methode, die eigentlich
     * nur notwendig ist, weil es keine standardisierte JDBC-Methode fr Locks gibt... Die Sperre
     * ist kooperativ: Sie verhindert konkurrierende Zugriffe nicht wirklich, sondern verlsst sich
     * darauf, dass Zugreifende freiwillig zuerst die Sperre abfragen. Sie bezieht sich auch nicht
     * direkt auf eine bestimmte Tabelle, sondern immer nur auf eine willkrliche frei whlbare
     * Bezeichnung. Diese muss fr jedes zu schtzende Objekt standardisiert werden.
     * 
     * @param name
     *            Name der gewnschten Sperre
     * @param wait
     *            wenn True, warten bis die sperre frei oder abgelaufen ist
     * @return null, wenn die Sperre belegt war, sonst eine id fr unlock
     */
    public static synchronized String lock(final String name, final boolean wait) {
        Stm stm = getConnection().getStatement();
        String lockname = "lock" + name;
        String lockid = StringTool.unique("lock");
        while (true) {
            long timestamp = System.currentTimeMillis();
            // Gibt es das angeforderte Lock schon?
            String oldlock = stm.queryString("SELECT wert FROM CONFIG WHERE param=" + JdbcLink.wrap(lockname));
            if (!StringTool.isNothing(oldlock)) {
                // Ja, wie alt ist es?
                String[] def = oldlock.split("#");
                long locktime = Long.parseLong(def[1]);
                long age = timestamp - locktime;
                if (age > 2000L) { // lter als zwei Sekunden -> Lschen
                    stm.exec("DELETE FROM CONFIG WHERE param=" + JdbcLink.wrap(lockname));
                } else {
                    if (wait == false) {
                        return null;
                    } else {
                        continue;
                    }
                }
            }
            // Neues Lock erstellen
            String lockstring = lockid + "#" + Long.toString(System.currentTimeMillis());
            StringBuilder sb = new StringBuilder();
            sb.append("INSERT INTO CONFIG (param,wert) VALUES (").append(JdbcLink.wrap(lockname)).append(",")
                    .append("'").append(lockstring).append("')");
            stm.exec(sb.toString());
            // Prfen, ob wir es wirklich haben, oder ob doch jemand anders
            // schneller war.
            String check = stm.queryString("SELECT wert FROM CONFIG WHERE param=" + JdbcLink.wrap(lockname));
            if (check.equals(lockstring)) {
                break;
            }
        }
        getConnection().releaseStatement(stm);
        return lockid;
    }

    /**
     * Exklusivzugriff wieder aufgeben
     * 
     * @param name
     *            Name des Locks
     * @param id
     *            bei "lock" erhaltene LockID
     * @return true bei Erfolg
     */
    public static synchronized boolean unlock(final String name, final String id) {
        String lockname = "lock" + name;
        String lock = getConnection().queryString("SELECT wert from CONFIG WHERE param=" + JdbcLink.wrap(lockname));
        if (StringTool.isNothing(lock)) {
            return false;
        }
        String[] res = lock.split("#");
        if (res[0].equals(id)) {
            getConnection().exec("DELETE FROM CONFIG WHERE param=" + JdbcLink.wrap(lockname));
            return true;
        }
        return false;
    }

    /**
     * Einschrnkende Bedingungen fr Suche nach diesem Objekt definieren
     * 
     * @return ein Constraint fr eine Select-Abfrage
     */
    protected String getConstraint() {
        return "";
    }

    /**
     * Bedingungen fr dieses Objekt setzen
     */
    protected void setConstraint() {
        /* Standardimplementation ist leer */
    }

    /** Einen menschenlesbaren Identifikationsstring fr dieses Objet liefern */
    abstract public String getLabel();

    /**
     * Jede abgeleitete Klasse muss deklarieren, in welcher Tabelle sie gespeichert werden will.
     * 
     * @return Der Name einer bereits existierenden Tabelle der Datenbank
     */
    abstract protected String getTableName();

    /**
     * Angeben, ob dieses Objekt gltig ist.
     * 
     * @return true wenn die Daten gltig (nicht notwendigerweise korrekt) sind
     */
    public boolean isValid() {
        if (state() < EXISTS) {
            return false;
        }
        return true;
    }

    /**
     * Die eindeutige Identifikation dieses Objektes/Datensatzes liefern. Diese ID wird jeweils
     * automatisch beim Anlegen eines Objekts dieser oder einer abgeleiteten Klasse erstellt und
     * bleibt dann unvernderlich.
     * 
     * @return die ID.
     */
    public String getId() {
        return id;
    }

    /**
     * Die ID in einen datenbankgeeigneten Wrapper verpackt (je nach Datenbank; meist Hochkommata).
     */
    public String getWrappedId() {
        return JdbcLink.wrap(id);
    }

    /** Der Konstruktor erstellt die ID */
    protected PersistentObject() {
        id = StringTool.unique("prso");
    }

    /**
     * Konstruktor mit vorgegebener ID (zum Deserialisieren) Wird nur von xx::load gebraucht.
     */
    protected PersistentObject(final String id) {
        this.id = id;
    }

    /**
     * Objekt in einen String serialisieren. Diese Standardimplementation macht eine "cheap copy":
     * Es wird eine Textreprsentation des Objektes erstellt, mit deren Hilfe das Objekt spter
     * wieder aus der Datenbank erstellt werden kann. Dies funktioniert nur innerhalb derselben
     * Datenbank.
     * 
     * @return der code-String, aus dem mit {@link PersistentObjectFactory} .createFromString wieder
     *         das Objekt erstellt werden kann
     */
    public String storeToString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getName()).append("::").append(getId());
        return sb.toString();
    }

    /** An object with this ID does not exist */
    public static final int INEXISTENT = 0;
    /** This id is not valid */
    public static final int INVALID_ID = 1;
    /** An object with this ID exists but is marked deleted */
    public static final int DELETED = 2;
    /** This is an existing object */
    public static final int EXISTS = 3;

    /**
     * Check the state of an object with this ID Note: This method accesses the database and
     * therefore is much more costly than the simple instantaniation of a PersistentObject
     * 
     * @return a value between INEXISTENT and EXISTS
     */

    public int state() {
        if (StringTool.isNothing(getId())) {
            return INVALID_ID;
        }

        StringBuilder sb = new StringBuilder("SELECT ID FROM ");
        sb.append(getTableName()).append(" WHERE ID=").append(getWrappedId());
        try {
            String obj = j.queryString(sb.toString());

            if (id.equalsIgnoreCase(obj)) {
                String deleted = get("deleted");
                if (deleted == null) { // if we cant't find the column called
                    // 'deleted', the object exists anyway
                    return EXISTS;
                }
                return deleted.equals("1") ? DELETED : EXISTS;

            } else {
                return INEXISTENT;
            }
        } catch (JdbcLinkSyntaxException ex) {
            return INEXISTENT;
        }
    }

    /**
     * Feststellen, ob ein PersistentObject bereits in der Datenbank existiert
     * 
     * @return true wenn es existiert, false wenn es nicht existiert oder gelscht wurde
     */

    public boolean exists() {
        return state() == EXISTS;
    }

    /**
     * Check whether the object exists in the database. This is the case for all objects in the
     * database for which state() returns neither INVALID_ID nor INEXISTENT. Note: objects marked as
     * deleted will also return true!
     * 
     * @return true, if the object is available in the database, false otherwise
     */
    public boolean isAvailable() {
        return (state() >= DELETED);
    }

    /**
     * Return a xid (domain_id) for a specified domain
     * 
     * @param domain
     * @return an identifier that may be empty but will never be null
     */

    public String getXid(final String domain) {
        if (domain.equals(Xid.DOMAIN_ELEXIS)) {
            return getId();
        }
        Query<Xid> qbe = new Query<Xid>(Xid.class);
        qbe.add(Xid.FLD_OBJECT, Query.EQUALS, getId());
        qbe.add(Xid.FLD_DOMAIN, Query.EQUALS, domain);
        List<Xid> res = qbe.execute();
        if (res.size() > 0) {
            return res.get(0).get(Xid.FLD_DOMAIN_ID);
        }
        return "";
    }

    /**
     * return the "best" xid for a given object. This is the xid with the highest quality. If no xid
     * is given for this object, a newly created xid of local quality will be returned
     */
    public IXid getXid() {
        List<IXid> res = getXids();
        if (res.size() == 0) {
            try {
                return new Xid(this, Xid.DOMAIN_ELEXIS, getId());
            } catch (XIDException xex) { // Should never happen, uh?
                ExHandler.handle(xex);
                return null;
            }
        }
        int quality = 0;
        IXid ret = null;
        for (IXid xid : res) {
            if (xid.getQuality() > quality) {
                quality = xid.getQuality();
                ret = xid;
            }
        }
        if (ret == null) {
            return res.get(0);
        }
        return ret;
    }

    /**
     * retrieve all XIDs of this object
     * 
     * @return a List that might be empty but is never null
     */
    public List<IXid> getXids() {
        Query<IXid> qbe = new Query<IXid>(Xid.class);
        qbe.add(Xid.FLD_OBJECT, Query.EQUALS, getId());
        return qbe.execute();
    }

    /**
     * Assign a XID to this object.
     * 
     * @param domain
     *            the domain whose ID will be assigned
     * @param domain_id
     *            the id out of the given domain fot this object
     * @param updateIfExists
     *            if true update values if Xid with same domain and domain_id exists. Otherwise the
     *            method will fail if a collision occurs.
     * @return true on success, false on failure
     */
    public boolean addXid(final String domain, final String domain_id, final boolean updateIfExists) {
        Xid oldXID = Xid.findXID(this, domain);
        if (oldXID != null) {
            if (updateIfExists) {
                oldXID.set(Xid.FLD_DOMAIN_ID, domain_id);
                return true;
            }
            return false;
        }

        try {
            new Xid(this, domain, domain_id);
            return true;
        } catch (XIDException e) {
            ExHandler.handle(e);
            if (updateIfExists) {
                Xid xid = Xid.findXID(domain, domain_id);
                if (xid != null) {
                    xid.set(Xid.FLD_OBJECT, getId());
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * holt den "hchstwertigen" Sticker, falls mehrere existieren
     * 
     * @return
     */
    public ISticker getSticker() {
        List<ISticker> list = getStickers();
        return list.size() > 0 ? list.get(0) : null;
    }

    /**
     * get all stickers of this object
     * 
     * @return a List of Sticker objects
     */
    private static String queryStickersString = "SELECT etikette FROM " + Sticker.LINKTABLE + " WHERE obj=?";
    private static PreparedStatement queryStickers = null;

    /**
     * Return all Stickers attributed to this objecz
     * 
     * @return A possibly empty list of Stickers
     */
    @SuppressWarnings("unchecked")
    public List<ISticker> getStickers() {
        String ID = new StringBuilder().append("ETK").append(getId()).toString();
        ArrayList<ISticker> ret = (ArrayList<ISticker>) cache.get(ID);
        if (ret != null) {
            return ret;
        }
        ret = new ArrayList<ISticker>();
        if (queryStickers == null) {
            queryStickers = j.prepareStatement(queryStickersString);
        }

        try {
            queryStickers.setString(1, id);
            ResultSet res = queryStickers.executeQuery();
            while (res != null && res.next()) {
                Sticker et = Sticker.load(res.getString(1));
                if (et != null && et.exists()) {
                    ret.add(Sticker.load(res.getString(1)));
                }
            }
            res.close();
        } catch (Exception ex) {
            ExHandler.handle(ex);
            return ret;
        }
        Collections.sort(ret);
        cache.put(ID, ret, getCacheTime());
        return ret;
    }

    /**
     * Remove a Sticker from this object
     * 
     * @param et
     *            the Sticker to remove
     */
    @SuppressWarnings("unchecked")
    public void removeSticker(ISticker et) {
        String ID = new StringBuilder().append("ETK").append(getId()).toString();
        ArrayList<Sticker> ret = (ArrayList<Sticker>) cache.get(ID);
        if (ret != null) {
            ret.remove(et);
        }
        StringBuilder sb = new StringBuilder();
        sb.append("DELETE FROM ").append(Sticker.LINKTABLE).append(" WHERE obj=").append(getWrappedId())
                .append(" AND etikette=").append(JdbcLink.wrap(et.getId()));
        getConnection().exec(sb.toString());
    }

    /**
     * Add a Sticker to this object
     * 
     * @param st
     *            the Sticker to add
     */
    @SuppressWarnings("unchecked")
    public void addSticker(ISticker st) {
        String ID = new StringBuilder().append("STK").append(getId()).toString();
        List<ISticker> ret = (List<ISticker>) cache.get(ID);
        if (ret == null) {
            ret = getStickers();
        }
        if (!ret.contains(st)) {
            ret.add(st);
            Collections.sort(ret);
            StringBuilder sb = new StringBuilder();
            sb.append("INSERT INTO ").append(Sticker.LINKTABLE).append("(obj,etikette) VALUES (")
                    .append(getWrappedId()).append(",").append(JdbcLink.wrap(st.getId())).append(");");
            getConnection().exec(sb.toString());
        }
    }

    /**
     * Feststellen, ob ein PersistentObject als gelscht markiert wurde
     * 
     * @return true wenn es gelscht ist
     */
    public boolean isDeleted() {
        return get("deleted").equals("1");
    }

    /**
     * Darf dieses Objekt mit Drag&Drop verschoben werden?
     * 
     * @return true wenn ja.
     */
    public boolean isDragOK() {
        return false;
    }

    /**
     * Aus einem Feldnamen das dazugehrige Datenbankfeld ermitteln
     * 
     * @param f
     *            Der Feldname
     * @return Das Datenbankfeld oder **ERROR**, wenn kein mapping fr das angegebene Feld
     *         existiert.
     */
    public String map(final String f) {
        if (f.equals("ID")) {
            return f;
        }
        String prefix = getTableName();
        String res = mapping.get(prefix + f);
        if (res == null) {
            log.log("field is not mapped " + f, Log.INFOS);
            return MAPPING_ERROR_MARKER + f + "**";
        }
        return res;
    }

    public FieldType getFieldType(final String f) {
        String mapped = map(f);
        if (mapped.startsWith("LIST:")) {
            return FieldType.LIST;
        } else if (mapped.startsWith("JOINT:")) {
            return FieldType.JOINT;
        } else {
            return FieldType.TEXT;
        }
    }

    /**
     * Ein Feld aus der Datenbank auslesen. Die Tabelle wird ber getTableName() erfragt. Das Feld
     * wird beim ersten Aufruf in jedem Fall aus der Datenbank gelesen. Dann werden weitere
     * Lesezugriffe whrend der <i>lifetime</i> aus dem cache bedient, um die Zahl der
     * Datenbankzugriffe zu minimieren. Nach Ablauf der lifetime erfolgt wieder ein Zugriff auf die
     * Datenbank, wobei auch der cache wieder erneuert wird. Wenn das Feld nicht als Tabellenfeld
     * existiert, wird es in EXTINFO gesucht. Wenn es auch dort nicht gefunden wird, wird eine
     * Methode namens getFeldname gesucht.
     * 
     * @param field
     *            Name des Felds
     * @return Der Inhalt des Felds (kann auch null sein), oder **ERROR**, wenn versucht werden
     *         sollte, ein nicht existierendes Feld auszulesen
     */
    public String get(final String field) {
        String key = getKey(field);
        Object ret = cache.get(key);
        if (ret instanceof String) {
            return (String) ret;
        }
        boolean decrypt = false;
        StringBuffer sql = new StringBuffer();
        String mapped = map(field);
        String table = getTableName();
        if (mapped.startsWith("EXT:")) {
            int ix = mapped.indexOf(':', 5);
            if (ix == -1) {
                log.log("Fehlerhaftes Mapping bei " + field, Log.ERRORS);
                return MAPPING_ERROR_MARKER + " " + field + "**";
            }
            table = mapped.substring(4, ix);
            mapped = mapped.substring(ix + 1);
        } else if (mapped.startsWith("S:")) {
            mapped = mapped.substring(4);
            decrypt = true;
        } else if (mapped.startsWith("JOINT:")) {
            String[] dwf = mapped.split(":");
            if (dwf.length > 4) {
                String objdef = dwf[4] + "::";
                StringBuilder sb = new StringBuilder();
                List<String[]> list = getList(field, new String[0]);
                PersistentObjectFactory fac = new PersistentObjectFactory();
                for (String[] s : list) {
                    PersistentObject po = fac.createFromString(objdef + s[0]);
                    sb.append(po.getLabel()).append("\n");
                }
                return sb.toString();
            }

        } else if (mapped.startsWith("LIST:")) {
            String[] dwf = mapped.split(":");
            if (dwf.length > 4) {
                String objdef = dwf[4] + "::";
                StringBuilder sb = new StringBuilder();
                List<String> list = getList(field, false);
                PersistentObjectFactory fac = new PersistentObjectFactory();
                for (String s : list) {
                    PersistentObject po = fac.createFromString(objdef + s);
                    sb.append(po.getLabel()).append("\n");
                }
                return sb.toString();
            }
        } else if (mapped.startsWith(MAPPING_ERROR_MARKER)) { // If the field
            // could not be
            // mapped
            String exi = map(FLD_EXTINFO); // Try to find it in ExtInfo
            if (!exi.startsWith(MAPPING_ERROR_MARKER)) {
                Map ht = getMap(FLD_EXTINFO);
                Object res = ht.get(field);
                if (res instanceof String) {
                    return (String) res;
                }
            }
            // try to find an XID with that name
            String xid = getXid(field);
            if (xid.length() > 0) {
                return xid;
            }
            // or try to find a "getter" Method
            // for the field
            String method = "get" + field;
            try {
                Method mx = getClass().getMethod(method, new Class[0]);
                Object ro = mx.invoke(this, new Object[0]);
                if (ro == null) {
                    return "";
                } else if (ro instanceof String) {
                    return (String) ro;
                } else if (ro instanceof Integer) {
                    return Integer.toString((Integer) ro);
                } else if (ro instanceof PersistentObject) {
                    return ((PersistentObject) ro).getLabel();
                } else {
                    return "?invalid field? " + mapped;
                }
            } catch (NoSuchMethodException nmex) {
                log.log("Fehler bei Felddefinition " + field, Log.WARNINGS);
                ElexisStatus status = new ElexisStatus(ElexisStatus.WARNING, Hub.PLUGIN_ID,
                        ElexisStatus.CODE_NOFEEDBACK, "Fehler bei Felddefinition", nmex);
                StatusManager.getManager().handle(status, StatusManager.LOG);
                return mapped;
            } catch (Exception ex) {
                // ignore the exceptions calling functions look for
                // MAPPING_ERROR_MARKER
                ExHandler.handle(ex);
                return mapped;
            }
        }
        sql.append("SELECT ").append(mapped).append(" FROM ").append(table).append(" WHERE ID='").append(id)
                .append("'");

        ResultSet rs = executeSqlQuery(sql.toString());
        String res = null;
        try {
            if ((rs != null) && (rs.next() == true)) {
                if (decrypt) {
                    res = decode(field, rs);
                } else {
                    res = rs.getString(mapped);
                }
                if (res == null) {
                    res = "";
                }
                cache.put(key, res, getCacheTime());
            }
        } catch (SQLException ex) {
            ExHandler.handle(ex);
        }
        return res;
    }

    protected byte[] getBinary(final String field) {
        String key = getKey(field);
        Object o = cache.get(key);
        if (o instanceof byte[]) {
            return (byte[]) o;
        }
        byte[] ret = getBinaryRaw(field);
        cache.put(key, ret, getCacheTime());
        return ret;
    }

    private byte[] getBinaryRaw(final String field) {
        StringBuilder sql = new StringBuilder();
        String mapped = (field);
        String table = getTableName();
        sql.append("SELECT ").append(mapped).append(" FROM ").append(table).append(" WHERE ID='").append(id)
                .append("'");

        ResultSet res = executeSqlQuery(sql.toString());
        try {
            if ((res != null) && (res.next() == true)) {
                return res.getBytes(mapped);
            }
        } catch (Exception ex) {
            ExHandler.handle(ex);
        }
        return null;
    }

    protected VersionedResource getVersionedResource(final String field, final boolean flushCache) {
        String key = getKey(field);
        if (flushCache == false) {
            Object o = cache.get(key);
            if (o instanceof VersionedResource) {
                return (VersionedResource) o;
            }
        }
        byte[] blob = getBinaryRaw(field);
        VersionedResource ret = VersionedResource.load(blob);
        cache.put(key, ret, getCacheTime());
        return ret;
    }

    /**
     * Eine Hashtable auslesen
     * 
     * @param field
     *            Feldname der Hashtable
     * @return eine Hashtable (ggf. leer). Nie null.
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public Map getMap(final String field) {
        String key = getKey(field);
        Object o = cache.get(key);
        if (o instanceof Hashtable) {
            return (Hashtable) o;
        }
        byte[] blob = getBinaryRaw(field);
        if (blob == null) {
            return new Hashtable();
        }
        Hashtable<Object, Object> ret = fold(blob);
        if (ret == null) {
            return new Hashtable();
        }
        cache.put(key, ret, getCacheTime());
        return ret;
    }

    /**
     * Bequemlichkeitsmethode zum lesen eines Integer.
     * 
     * @param field
     * @return einen Integer. 0 bei 0 oder unlesbar
     */
    public int getInt(final String field) {
        return checkZero(get(field));
    }

    /**
     * Eine 1:n Verknpfung aus der Datenbank auslesen.
     * 
     * Does not include elements marked as deleted.
     * 
     * @param field
     *            das Feld, wie in der mapping-Deklaration angegeben
     * @param reverse
     *            wenn true wird rckwrts sortiert
     * @return eine Liste mit den IDs (String!) der verknpften Datenstze oder null, wenn das Feld
     *         keine 1:n-Verknofung ist
     */
    @SuppressWarnings("unchecked")
    public List<String> getList(final String field, final boolean reverse) {
        StringBuffer sql = new StringBuffer();
        String mapped = map(field);
        if (mapped.startsWith("LIST:")) {
            String[] m = mapped.split(":");
            if (m.length > 2) {
                // String order=null;

                sql.append("SELECT ID FROM ").append(m[2]).append(" WHERE ");

                sql.append("deleted=").append(JdbcLink.wrap("0")).append(" AND ");

                sql.append(m[1]).append("=").append(getWrappedId());
                if (m.length > 3) {
                    sql.append(" ORDER by ").append(m[3]);
                    if (reverse) {
                        sql.append(" DESC");
                    }
                }
                Stm stm = getConnection().getStatement();
                List<String> ret = stm.queryList(sql.toString(), new String[] { "ID" });
                getConnection().releaseStatement(stm);
                return ret;
            }
        } else {
            log.log("Fehlerhaftes Mapping " + mapped, Log.ERRORS);
        }
        return null;
    }

    /**
     * Eine n:m - Verknpfung auslesen
     * 
     * @param field
     *            Das Feld, fr das ein entsprechendes mapping existiert
     * @param extra
     *            Extrafelder, die aus der joint-Tabelle ausgelesen werden sollen
     * @return eine Liste aus String-Arrays, welche jeweils die ID des gefundenen Objekts und den
     *         Inhalt der Extra-Felder enthalten. Null bei Mapping-Fehler
     */
    public List<String[]> getList(final String field, String[] extra) {
        if (extra == null) {
            extra = new String[0];
        }
        StringBuffer sql = new StringBuffer();
        String mapped = map(field);
        if (mapped.startsWith("JOINT:")) {
            String[] abfr = mapped.split(":");
            sql.append("SELECT ").append(abfr[1]);
            for (String ex : extra) {
                sql.append(",").append(ex);
            }
            sql.append(" FROM ").append(abfr[3]).append(" WHERE ").append(abfr[2]).append("=")
                    .append(getWrappedId());

            ResultSet rs = executeSqlQuery(sql.toString());
            LinkedList<String[]> list = new LinkedList<String[]>();
            try {
                while ((rs != null) && rs.next()) {
                    String[] line = new String[extra.length + 1];
                    line[0] = rs.getString(abfr[1]);
                    for (int i = 1; i < extra.length + 1; i++) {
                        line[i] = rs.getString(extra[i - 1]);
                    }
                    list.add(line);
                }
                rs.close();
                return list;

            } catch (Exception ex) {
                ElexisStatus status = new ElexisStatus(ElexisStatus.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                        "Fehler beim Lesen der Liste ", ex, ElexisStatus.LOG_ERRORS);
                // This is not an exception but a misconfiguration. No need to stop program flow.
                // Just return null
                // as the documentation of the method states.
                // throw new PersistenceException(status);
                return null;
            }
        } else {
            log.log("Fehlerhaftes Mapping " + mapped, Log.ERRORS);
        }
        return null;

    }

    /**
     * Ein Feld in die Datenbank bertragen. Gleichzeitig Cache-update Die Tabelle wird ber
     * getTableName() erfragt.
     * 
     * @param field
     *            Name des Feldes
     * @param value
     *            Einzusetzender Wert (der vorherige Wert wird berschrieben)
     * @return true bei Erfolg
     */
    public boolean set(final String field, String value) {
        String mapped = map(field);
        String table = getTableName();
        String key = getKey(field);
        StringBuilder sql = new StringBuilder();
        long ts = System.currentTimeMillis();

        if (value == null) {
            cache.remove(key);
            sql.append("UPDATE ").append(table).append(" SET ").append(mapped)
                    .append("=NULL, lastupdate=" + Long.toString(ts) + " WHERE ID=").append(getWrappedId());
            getConnection().exec(sql.toString());
            return true;
        }
        Object oldval = cache.get(key);
        cache.put(key, value, getCacheTime()); // refresh cache
        if (value.equals(oldval)) {
            return true; // no need to write data if it ws already in cache
        }

        if (mapped.startsWith("EXT:")) {
            int ix = mapped.indexOf(':', 5);
            if (ix == -1) {
                log.log("Fehlerhaftes Mapping bei " + field, Log.ERRORS);
                return false;
            }
            table = mapped.substring(4, ix);
            mapped = mapped.substring(ix + 1);
            sql.append("UPDATE ").append(table).append(" SET ").append(mapped);
        } else {
            sql.append("UPDATE ").append(table).append(" SET ");
            if (mapped.startsWith("S:")) {
                sql.append(mapped.substring(4));
            } else {
                sql.append(mapped);
            }
        }
        sql.append("=?, lastupdate=? WHERE ID=").append(getWrappedId());
        String cmd = sql.toString();
        PreparedStatement pst = getConnection().prepareStatement(cmd);

        encode(1, pst, field, value);
        if (tracetable != null) {
            StringBuffer params = new StringBuffer();
            params.append("[");
            params.append(value);
            params.append("]");
            doTrace(cmd + " " + params);
        }
        try {
            pst.setLong(2, ts);
            pst.executeUpdate();
            // ElexisEventDispatcher.getInstance().fire(new
            // ElexisEvent(this,this.getClass(),ElexisEvent.EVENT_UPDATE));
            return true;
        } catch (Exception ex) {
            ElexisStatus status = new ElexisStatus(ElexisStatus.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                    "Fehler bei: " + cmd + "(" + field + "=" + value + ")", ex, ElexisStatus.LOG_ERRORS);
            throw new PersistenceException(status); // See api doc. check this whether it breaks
            // existing code.
            // return false; // See api doc. Return false on errors.
        } finally {
            try {
                pst.close();
            } catch (SQLException e) {
            }
        }

    }

    /**
     * Eine Hashtable speichern. Diese wird zunchst in ein byte[] geplttet, und so gespeichert.
     * 
     * @param field
     * @param map
     * @return 0 bei Fehler
     */
    @SuppressWarnings("rawtypes")
    @Override
    public void setMap(final String field, final Map<Object, Object> map) {
        if (map == null) {
            throw new PersistenceException(new ElexisStatus(Status.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                    "Attempt to store Null map", null));
        }
        byte[] bin = flatten((Hashtable) map);
        cache.put(getKey(field), map, getCacheTime());
        setBinary(field, bin);
    }

    /**
     * Eine VersionedResource zurckschreiben. Um Datenverlust durch gleichzeitigen Zugriff zu
     * vermeiden, wird zunchst die aktuelle Version in der Datenbank gelesen und mit der neuen
     * Version berlagert.
     */
    protected void setVersionedResource(final String field, final String entry) {
        String lockid = lock("VersionedResource", true);
        VersionedResource old = getVersionedResource(field, true);
        if (old.update(entry, Hub.actUser.getLabel()) == true) {
            cache.put(getKey(field), old, getCacheTime());
            setBinary(field, old.serialize());
        }
        unlock("VersionedResource", lockid);
    }

    protected void setBinary(final String field, final byte[] value) {
        String key = getKey(field);
        cache.put(key, value, getCacheTime());
        setBinaryRaw(field, value);
    }

    private void setBinaryRaw(final String field, final byte[] value) {
        StringBuilder sql = new StringBuilder(1000);
        sql.append("UPDATE ").append(getTableName()).append(" SET ").append(/* map */(field))
                .append("=?, lastupdate=?").append(" WHERE ID=").append(getWrappedId());
        String cmd = sql.toString();
        if (tracetable != null) {
            doTrace(cmd);
        }
        PreparedStatement stm = getConnection().prepareStatement(cmd);
        try {
            stm.setBytes(1, value);
            stm.setLong(2, System.currentTimeMillis());
            stm.executeUpdate();
        } catch (Exception ex) {
            log.log("Fehler beim Ausfhren der Abfrage " + cmd, Log.ERRORS);
            throw new PersistenceException(new ElexisStatus(Status.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                    "setBytes: Es trat ein Fehler beim Schreiben auf. " + ex.getMessage(), ex, Log.ERRORS));
        } finally {
            try {
                stm.close();
            } catch (SQLException e) {
                ExHandler.handle(e);
                throw new PersistenceException("Could not close statement " + e.getMessage());
            }
        }
    }

    /**
     * Set a value of type int.
     * 
     * @param field
     *            a table field of numeric type
     * @param value
     *            the value to be set
     * @return true on success, false else
     */
    public boolean setInt(final String field, final int value) {
        String stringValue = new Integer(value).toString();
        if (stringValue.length() <= MAX_INT_LENGTH) {
            return set(field, stringValue);
        } else {
            return false;
        }
    }

    private void doTrace(final String sql) {
        StringBuffer tracer = new StringBuffer();
        tracer.append("INSERT INTO ").append(tracetable);
        tracer.append(" (logtime,Workstation,Username,action) VALUES (");
        tracer.append(System.currentTimeMillis()).append(",");
        tracer.append(pcname).append(",");
        tracer.append(username).append(",");
        tracer.append(JdbcLink.wrap(sql.replace('\'', '/'))).append(")");
        getConnection().exec(tracer.toString());
    }

    /**
     * Eine Element einer n:m Verknpfung eintragen. Zur Tabellendefinition wird das mapping
     * verwendet.
     * 
     * @param field
     *            Das n:m Feld, fr das ein neuer Eintrag erstellt werden soll.
     * @param oID
     *            ID des Zielobjekts, auf das der Eintrag zeigen soll
     * @param extra
     *            Definition der zustzlichen Felder der Joint-Tabelle. Jeder Eintrag in der Form
     *            Feldname=Wert
     * @return 0 bei Fehler
     */
    public int addToList(final String field, final String oID, final String... extra) {
        String mapped = map(field);
        if (mapped.startsWith("JOINT:")) {
            String[] m = mapped.split(":");// m[1] FremdID, m[2] eigene ID, m[3]
            // Name Joint
            if (m.length > 3) {
                StringBuffer head = new StringBuffer(100);
                StringBuffer tail = new StringBuffer(100);
                head.append("INSERT INTO ").append(m[3]).append("(ID,").append(m[2]).append(",").append(m[1]);
                tail.append(") VALUES (").append(JdbcLink.wrap(StringTool.unique("aij"))).append(",")
                        .append(getWrappedId()).append(",").append(JdbcLink.wrap(oID));
                if (extra != null) {
                    for (String s : extra) {
                        String[] def = s.split("=");
                        if (def.length != 2) {
                            log.log("Fehlerhafter Aufruf addToList " + s, Log.ERRORS);
                            return 0;
                        }
                        head.append(",").append(def[0]);
                        tail.append(",").append(JdbcLink.wrap(def[1]));
                    }
                }
                head.append(tail).append(")");
                if (tracetable != null) {
                    String sql = head.toString();
                    doTrace(sql);
                    return getConnection().exec(sql);
                }
                return getConnection().exec(head.toString());
            }
        }
        log.log("Fehlerhaftes Mapping: " + mapped, Log.ERRORS);
        return 0;
    }

    /**
     * Remove all relations to this object from link
     * 
     * @param field
     */
    public void removeFromList(String field) {
        String mapped = map(field);
        if (mapped.startsWith("JOINT:")) {
            String[] m = mapped.split(":");// m[1] FremdID, m[2] eigene ID, m[3]
            // Name Joint
            if (m.length > 3) {
                StringBuilder sql = new StringBuilder(200);
                sql.append("DELETE FROM ").append(m[3]).append(" WHERE ").append(m[2]).append("=")
                        .append(getWrappedId());
                if (tracetable != null) {
                    String sq = sql.toString();
                    doTrace(sq);
                }
                getConnection().exec(sql.toString());
                return;
            }
        }
        log.log("Fehlerhaftes Mapping: " + mapped, Log.ERRORS);
    }

    /**
     * Remove a relation to this object from link
     * 
     * @param field
     * @param oID
     */
    public void removeFromList(String field, String oID) {
        String mapped = map(field);
        if (mapped.startsWith("JOINT:")) {
            String[] m = mapped.split(":");// m[1] FremdID, m[2] eigene ID, m[3]
            // Name Joint
            if (m.length > 3) {
                StringBuilder sql = new StringBuilder(200);
                sql.append("DELETE FROM ").append(m[3]).append(" WHERE ").append(m[2]).append("=")
                        .append(getWrappedId()).append(" AND ").append(m[1]).append("=").append(JdbcLink.wrap(oID));
                if (tracetable != null) {
                    String sq = sql.toString();
                    doTrace(sq);
                }
                getConnection().exec(sql.toString());
                return;
            }
        }
        log.log("Fehlerhaftes Mapping: " + mapped, Log.ERRORS);
    }

    /**
     * Ein neues Objekt erstellen und in die Datenbank eintragen
     * 
     * @param customID
     *            Wenn eine ID (muss eindeutig sein!) vorgegeben werden soll. Bei null wird eine
     *            generiert.
     * @return true bei Erfolg
     */
    protected boolean create(final String customID) {
        // String pattern=this.getClass().getSimpleName();
        if (customID != null) {
            id = customID;
        }
        StringBuffer sql = new StringBuffer(300);
        sql.append("INSERT INTO ").append(getTableName()).append("(ID) VALUES (").append(getWrappedId())
                .append(")");
        if (getConnection().exec(sql.toString()) != 0) {
            setConstraint();
            ElexisEventDispatcher.getInstance().fire(new ElexisEvent(this, getClass(), ElexisEvent.EVENT_CREATE));
            return true;
        }
        return false;
    }

    /**
     * Ein Objekt und ggf. dessen XID's aus der Datenbank lschen the object is not deleted but
     * rather marked as deleted. A purge must be applied to remove the object really
     * 
     * @return true on success
     */
    public boolean delete() {
        if (set("deleted", "1")) {
            List<Xid> xids = new Query<Xid>(Xid.class, Xid.FLD_OBJECT, getId()).execute();
            for (Xid xid : xids) {
                xid.delete();
            }
            new DBLog(this, DBLog.TYP.DELETE);
            PersistentObject sel = ElexisEventDispatcher.getSelected(this.getClass());
            if ((sel != null) && sel.equals(this)) {
                ElexisEventDispatcher.clearSelection(this.getClass());
            }
            ElexisEventDispatcher.getInstance().fire(new ElexisEvent(this, getClass(), ElexisEvent.EVENT_DELETE));
            return true;
        }
        return false;
    }

    /**
     * Alle Bezge aus einer n:m-Verknpfung zu diesem Objekt lschen
     * 
     * @param field
     *            Feldname, der die Liste definiert
     * @return
     */
    public boolean deleteList(final String field) {
        String mapped = map(field);
        if (!mapped.startsWith("JOINT:")) {
            SWTHelper.alert("Interer Fehler", "Feld " + field + " ist keine n:m Verknpfung");
            return false;
        }
        String[] m = mapped.split(":");// m[1] FremdID, m[2] eigene ID, m[3]
        // Name Joint
        getConnection().exec("DELETE FROM " + m[3] + " WHERE " + m[2] + "=" + getWrappedId());
        return true;
    }

    /**
     * We can undelete any object by simply clearing the deleted-flag and reanimate dependend XID's
     * 
     * @return true on success
     */
    public boolean undelete() {
        if (set("deleted", "0")) {
            Query<Xid> qbe = new Query<Xid>(Xid.class);
            qbe.clear(true);
            qbe.add(Xid.FLD_OBJECT, Query.EQUALS, getId());
            List<Xid> xids = qbe.execute();
            for (Xid xid : xids) {
                xid.undelete();
            }
            new DBLog(this, DBLog.TYP.UNDELETE);
            ElexisEventDispatcher.getInstance().fire(new ElexisEvent(this, getClass(), ElexisEvent.EVENT_CREATE));
            return true;
        }
        return false;
    }

    /**
     * Mehrere Felder auf einmal setzen (Effizienter als einzelnes set)
     * 
     * @param fields
     *            die Feldnamen
     * @param values
     *            die Werte
     * @return false bei Fehler
     */
    public boolean set(final String[] fields, final String... values) {
        if ((fields == null) || (values == null) || (fields.length != values.length)) {
            log.log("Falsche Felddefinition fr set", Log.ERRORS);
            return false;
        }
        StringBuffer sql = new StringBuffer(200);
        sql.append("UPDATE ").append(getTableName()).append(" SET ");
        for (int i = 0; i < fields.length; i++) {
            String mapped = map(fields[i]);
            if (mapped.startsWith("S:")) {
                sql.append(mapped.substring(4));
            } else {
                sql.append(mapped);
            }
            sql.append("=?,");
            cache.put(getKey(fields[i]), values[i], getCacheTime());
        }
        sql.append("lastupdate=?");
        // sql.delete(sql.length() - 1, 100000);
        sql.append(" WHERE ID=").append(getWrappedId());
        String cmd = sql.toString();
        PreparedStatement pst = getConnection().prepareStatement(cmd);
        for (int i = 0; i < fields.length; i++) {
            encode(i + 1, pst, fields[i], values[i]);
        }
        if (tracetable != null) {
            StringBuffer params = new StringBuffer();
            params.append("[");
            params.append(StringTool.join(values, ", "));
            params.append("]");
            doTrace(cmd + " " + params);
        }
        try {
            pst.setLong(fields.length + 1, System.currentTimeMillis());
            pst.executeUpdate();
            ElexisEventDispatcher.getInstance()
                    .fire(new ElexisEvent(this, this.getClass(), ElexisEvent.EVENT_UPDATE));
            return true;
        } catch (Exception ex) {
            ExHandler.handle(ex);
            StringBuilder sb = new StringBuilder();
            sb.append("Fehler bei ").append(cmd).append("\nFelder:\n");
            for (int i = 0; i < fields.length; i++) {
                sb.append(fields[i]).append("=").append(values[i]).append("\n");
            }
            ElexisStatus status = new ElexisStatus(ElexisStatus.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                    sb.toString(), ex, ElexisStatus.LOG_ERRORS);
            // DONT Throw an Exception. The API doc states: return false on errors!!
            // throw new PersistenceException(status);
            return false;
        } finally {
            try {
                pst.close();
            } catch (SQLException e) {
            }
        }
    }

    /**
     * Mehrere Felder auf einmal auslesen
     * 
     * @param fields
     *            die Felder
     * @param values
     *            String Array fr die gelesenen Werte
     * @return true ok, values wurden gesetzt
     */
    public boolean get(final String[] fields, final String[] values) {
        if ((fields == null) || (values == null) || (fields.length != values.length)) {
            log.log("Falscher Aufruf von get(String[],String[]", Log.ERRORS);
            return false;
        }
        StringBuffer sql = new StringBuffer(200);
        sql.append("SELECT ");
        boolean[] decode = new boolean[fields.length];
        for (int i = 0; i < fields.length; i++) {
            String key = getKey(fields[i]);
            Object ret = cache.get(key);
            if (ret instanceof String) {
                values[i] = (String) ret;
            } else {
                String f1 = map(fields[i]);
                if (f1.startsWith("S:")) {
                    sql.append(f1.substring(4));
                    decode[i] = true;
                } else {
                    sql.append(f1);
                }
                sql.append(",");
            }
        }
        if (sql.length() < 8) {
            return true;
        }
        sql.delete(sql.length() - 1, 1000);
        sql.append(" FROM ").append(getTableName()).append(" WHERE ID=").append(getWrappedId());

        ResultSet res = executeSqlQuery(sql.toString());
        try {
            if ((res != null) && res.next()) {
                for (int i = 0; i < values.length; i++) {
                    if (values[i] == null) {
                        if (decode[i] == true) {
                            values[i] = decode(fields[i], res);
                        } else {
                            values[i] = checkNull(res.getString(map(fields[i])));
                        }
                        cache.put(getKey(fields[i]), values[i], getCacheTime());
                    }
                }

            }
            return true;
        } catch (Exception ex) {
            ExHandler.handle(ex);
            return false;
        }
    }

    /**
     * Apply some magic to the input parameters, and return a decoded string object. TODO describe
     * magic
     * 
     * @param field
     * @param rs
     * @return decoded string or null if decode was not possible
     */
    private String decode(final String field, final ResultSet rs) {

        try {
            String mapped = map(field);
            if (mapped.startsWith("S:")) {
                char mode = mapped.charAt(2);
                switch (mode) {
                case 'D':
                    String dat = rs.getString(mapped.substring(4));
                    if (dat == null) {
                        return "";
                    }
                    TimeTool t = new TimeTool();
                    if (t.set(dat) == true) {
                        return t.toString(TimeTool.DATE_GER);
                    } else {
                        return "";
                    }
                case 'N':
                    int val = rs.getInt(mapped.substring(4));
                    return Integer.toString(val);
                case 'C':
                    InputStream is = rs.getBinaryStream(mapped.substring(4));
                    if (is == null) {
                        return "";
                    }
                    byte[] exp = CompEx.expand(is);
                    return StringTool.createString(exp);

                case 'V':
                    byte[] in = rs.getBytes(mapped.substring(4));
                    VersionedResource vr = VersionedResource.load(in);
                    return vr.getHead();
                }
            }
        } catch (Exception ex) {
            ElexisStatus status = new ElexisStatus(ElexisStatus.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                    "Fehler bei decode ", ex, ElexisStatus.LOG_ERRORS);

            log.log("Fehler bei decode ", Log.ERRORS);

            // Dont throw an exception. Null is an acceptable (and normally testes)
            // return value if something went wrong.
            // throw new PersistenceException(status);

        }
        return null;
    }

    private String encode(final int num, final PreparedStatement pst, final String field, final String value) {
        String mapped = map(field);
        String ret = value;
        try {
            if (mapped.startsWith("S:")) {
                String typ = mapped.substring(2, 3);
                mapped = mapped.substring(4);
                byte[] enc;

                if (typ.startsWith("D")) { // datum
                    TimeTool t = new TimeTool();
                    if ((!StringTool.isNothing(value)) && (t.set(value) == true)) {
                        ret = t.toString(TimeTool.DATE_COMPACT);
                        pst.setString(num, ret);
                    } else {
                        ret = "";
                        pst.setString(num, "");
                    }

                } else if (typ.startsWith("C")) { // string enocding
                    enc = CompEx.Compress(value, CompEx.ZIP);
                    pst.setBytes(num, enc);
                } else if (typ.startsWith("N")) { // Number encoding
                    pst.setInt(num, Integer.parseInt(value));
                } else {
                    log.log("Unbekannter encode code " + typ, Log.ERRORS);
                }
            } else {
                pst.setString(num, value);
            }
        } catch (Exception ex) {
            ElexisStatus status = new ElexisStatus(ElexisStatus.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                    "Fehler beim String encoder", ex, ElexisStatus.LOG_ERRORS);
            // Dont throw an exeption. returning the original value is an acceptable way if encoding
            // is not possible. Frequently it's just
            // a configuration problem, so just log it and let the user decide if they want to fix
            // it later.
            // DONT throw new PersistenceException(status);
            log.log("Fehler beim String encoder: " + ex.getMessage(), Log.ERRORS);

        }
        return ret;
    }

    /** Strings must match exactly (but ignore case) */
    public static final int MATCH_EXACT = 0;
    /** String must start with test (ignoring case) */
    public static final int MATCH_START = 1;
    /** String must match as regular expression */
    public static final int MATCH_REGEXP = 2;
    /** String must contain test (ignoring case) */
    public static final int MATCH_CONTAINS = 3;
    /**
     * Try to find match method.
     * <ul>
     * <li>If test starts with % or * use MATCH_CONTAINS</li>
     * <li>If test is enclosed in / use MATCH_REGEXP</li>
     * </ul>
     * 
     */
    public static final int MATCH_AUTO = 4;

    /**
     * Testet ob zwei Objekte bezglich definierbarer Felder bereinstimmend sind
     * 
     * @param other
     *            anderes Objekt
     * @param mode
     *            gleich, LIKE oder Regexp
     * @param fields
     *            die interessierenden Felder
     * @return true wenn this und other vom selben typ sind und alle interessierenden Felder genss
     *         mode bereinstimmen.
     */
    public boolean isMatching(final IPersistentObject other, final int mode, final String... fields) {
        if (getClass().equals(other.getClass())) {
            String[] others = new String[fields.length];
            other.get(fields, others);
            return isMatching(fields, mode, others);
        }
        return false;
    }

    /**
     * testet, ob die angegebenen Felder den angegebenen Werten entsprechen.
     * 
     * @param fields
     *            die zu testenden Felde
     * @param mode
     *            Testmodus (MATCH_EXACT, MATCH_LIKE oder MATCH_REGEXP)
     * @param others
     *            die Vergleichswerte
     * @return true bei bereinsteimmung
     */
    public boolean isMatching(final String[] fields, final int mode, final String... others) {
        String[] mine = new String[fields.length];
        get(fields, mine);

        for (int i = 0; i < fields.length; i++) {
            if (mine[i] == null) {
                if (others[i] == null) {
                    return true;
                }
                return false;
            }
            if (others[i] == null) {
                return false;
            }
            switch (mode) {
            case MATCH_EXACT:
                if (!mine[i].toLowerCase().equals(others[i].toLowerCase())) {
                    return false;
                }
                break;
            case MATCH_START:
                if (!mine[i].toLowerCase().startsWith(others[i].toLowerCase())) {
                    return false;
                }
                break;
            case MATCH_REGEXP:
                if (!mine[i].matches(others[i])) {
                    return false;
                }
            case MATCH_CONTAINS:
                if (!mine[i].toLowerCase().contains(others[i].toLowerCase())) {
                    return false;
                }
            }

        }
        return true;
    }

    /**
     * Testet ob dieses Objekt den angegebenen Feldern entspricht.
     * 
     * @param fields
     *            HashMap mit name,wert paaren fr die Felder
     * @param mode
     *            Testmodus (MATCH_EXACT, MATCH_BEGIN, MATCH_REGEXP, MATCH_CONTAIN oder MATCH_AUTO)
     * @param bSkipInexisting
     *            don't return false if a fieldname is not found but skip this field instead
     * @return true wenn dieses Objekt die entsprechenden Felder hat
     */
    public boolean isMatching(final Map<String, String> fields, final int mode, final boolean bSkipInexisting) {
        for (Entry<String, String> entry : fields.entrySet()) {
            String mine = get(entry.getKey());
            String others = entry.getValue();
            if (bSkipInexisting) {
                if (mine.startsWith(MAPPING_ERROR_MARKER) || others.startsWith(MAPPING_ERROR_MARKER)) {
                    continue;
                }
            }
            switch (mode) {
            case MATCH_EXACT:
                if (!mine.toLowerCase().equals(others.toLowerCase())) {
                    return false;
                }
                break;
            case MATCH_START:
                if (!mine.toLowerCase().startsWith(others.toLowerCase())) {
                    return false;
                }
                break;
            case MATCH_REGEXP:
                if (!mine.matches(others)) {
                    return false;
                }
            case MATCH_CONTAINS:
                if (!mine.toLowerCase().contains(others.toLowerCase())) {
                    return false;
                }
            case MATCH_AUTO:
                String my = mine.toLowerCase();
                if (others.startsWith("%") || others.startsWith("*")) {
                    if (!my.contains(others.substring(1).toLowerCase())) {
                        return false;
                    }
                } else {
                    if (!my.startsWith(others.toLowerCase())) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    @Override
    public boolean isMatching(List<Term> terms) {
        // TODO Auto-generated method stub
        return false;
    }

    /**
     * Eine Transaktion beginnen. schreiboperationen mssen auf das zurckgelieferte
     * Transactions-Objekt erfolgen. (Und knnen mit Schreiboperationen ausserhalb der Transaktion
     * konkurrieren)
     * 
     * @return Ein Transaktionsobjekt, ber das Schreiboperationen gettigt werden kann, und das am
     *         Ende mit commit() oder rollback() ausgefhrt resp. gestoppt werden kann.
     */
    public Transaction begin() {
        return new Transaction(this);
    }

    /**
     * Get a unique key for a value, suitable for identifying a key in a cache. The current
     * implementation uses the table name, the id of the PersistentObject and the field name.
     * 
     * @param field
     *            the field to get a key for
     * @return a unique key
     */
    private String getKey(final String field) {
        StringBuffer key = new StringBuffer();

        key.append(getTableName());
        key.append(".");
        key.append(getId());
        key.append("#");
        key.append(field);

        return key.toString();
    }

    /**
     * Verbindung zur Datenbank trennen
     * 
     */
    public static void disconnect() {
        if (getConnection() != null) {
            if (getConnection().DBFlavor.startsWith("hsqldb")) {
                getConnection().exec("SHUTDOWN COMPACT");
            }
            getConnection().disconnect();
            j = null;
            log.log("Verbindung zur Datenbank getrennt.", Log.INFOS);
            cache.stat();
        }
    }

    @Override
    public boolean equals(final Object arg0) {
        if (arg0 instanceof PersistentObject) {
            return getId().equals(((PersistentObject) arg0).getId());
        }
        return false;
    }

    /**
     * Return a String field making sure that it will never be null
     * 
     * @param in
     *            name of the field to retrieve
     * @return the field contents or "" if it was null
     */
    public static String checkNull(final Object in) {
        if (in == null) {
            return "";
        }
        if (!(in instanceof String)) {
            return "";
        }
        return (String) in;
    }

    /**
     * return a numeric field making sure the call will not fail on illegal values
     * 
     * @param in
     *            name of the field
     * @return the value of the field as integer or 0 if it was null or not nomeric.
     */
    public static int checkZero(final Object in) {
        if (StringTool.isNothing(in)) {
            return 0;
        }
        try {
            return Integer.parseInt(((String) in).trim()); // We're sure in is a String at this
            // point
        } catch (NumberFormatException ex) {
            ExHandler.handle(ex);
            return 0;
        }
    }

    /**
     * return a numeric field making sure the call will not fail on illegal values
     * 
     * @param in
     *            name of the field
     * @return the value of the field as double or 0.0 if it was null or not a Double.
     */
    public static double checkZeroDouble(final String in) {
        if (StringTool.isNothing(in)) {
            return 0.0;
        }
        try {
            return Double.parseDouble(in.trim());
        } catch (NumberFormatException ex) {
            ExHandler.handle(ex);
            return 0.0;
        }
    }

    /**
     * return the time of the last update of this object
     * 
     * @return the time (as given in System.currentTimeMillis()) of the last write operation on this
     *         object or 0 if there was no valid lastupdate time
     */
    public long getLastUpdate() {
        try {
            return Long.parseLong(get("lastupdate"));
        } catch (Exception ex) {
            // ExHandler.handle(ex);
            return 0L;
        }
    }

    @Override
    public int hashCode() {
        return getId().hashCode();
    }

    public static void clearCache() {
        synchronized (cache) {
            cache.clear();
        }
    }

    public static void resetCache() {
        synchronized (cache) {
            cache.reset();
        }
    }

    /**
     * Return time-to-live in cache for this object
     * 
     * @return the time in seconds
     */
    public int getCacheTime() {
        return default_lifetime;
    }

    public static void setDefaultCacheLifetime(int seconds) {
        default_lifetime = seconds;
    }

    public static int getDefaultCacheLifetime() {
        return default_lifetime;
    }

    /**
     * Utility function to create or modify a table consistently. Should be used by all plugins that
     * contribute data types derived from PersistentObject
     * 
     * @param sqlScript
     *            create string
     */
    protected static void createOrModifyTable(final String sqlScript) {
        String[] sql = new String[1];
        sql[0] = sqlScript;
        SqlWithUiRunner runner = new SqlWithUiRunner(sql, Hub.PLUGIN_ID);
        runner.runSql();
    }

    /**
     * public helper to execute an sql script iven as file path. SQL Errors will be
     * handeld/displayed by SqlWithUiRunner
     * 
     * @param filepath
     *            where the script is
     * @param plugin
     *            name of the originating plugin
     * @throws IOException
     *             file not found or not readable
     */
    public static void executeSQLScript(String filepath, String plugin) throws IOException {
        FileInputStream is = new FileInputStream(filepath);
        InputStreamReader isr = new InputStreamReader(is);
        char[] buf = new char[4096];
        int l = 0;
        StringBuilder sb = new StringBuilder();
        while ((l = isr.read(buf)) > 0) {
            sb.append(buf, 0, l);
        }
        new SqlWithUiRunner(new String[] { sb.toString() }, plugin).runSql();

    }

    /*
     * protected static void createOrModifyTable(final String sqlScript) { try {
     * PlatformUI.getWorkbench().getProgressService() .busyCursorWhile(new IRunnableWithProgress() {
     * public void run(IProgressMonitor moni) { moni.beginTask("Fhre Datenbankmodifikation aus",
     * IProgressMonitor.UNKNOWN); try { final ByteArrayInputStream bais; bais = new
     * ByteArrayInputStream(sqlScript .getBytes("UTF-8")); if (getConnection().execScript(bais,
     * true, false) == false) { SWTHelper .showError("Datenbank-Fehler",
     * "Konnte Datenbank-Script nicht ausfhren"); log.log("Cannot execute db script: " + sqlScript,
     * Log.WARNINGS); } moni.done(); } catch (UnsupportedEncodingException e) { // should really
     * never happen e.printStackTrace(); } } }); } catch (Exception e) {
     * SWTHelper.showError("Interner-Fehler", "Konnte Datenbank-Script nicht ausfhren"); log.log(e,
     * "Cannot execute db script: " + sqlScript, Log.ERRORS); } }
     */
    protected static boolean executeScript(final String pathname) {
        Stm stm = getConnection().getStatement();
        try {
            FileInputStream is = new FileInputStream(pathname);
            return stm.execScript(is, true, true);
        } catch (Exception ex) {
            ExHandler.handle(ex);
            return false;
        } finally {
            getConnection().releaseStatement(stm);
        }
    }

    /**
     * Utility function to remove a table and all objects defined therein consistentliy To make sure
     * dependent data are deleted as well, we call each object's delete operator individually before
     * dropping the table
     * 
     * @param name
     *            the name of the table
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected static void removeTable(final String name, final Class oclas) {
        Query qbe = new Query(oclas);
        for (Object o : qbe.execute()) {
            ((PersistentObject) o).delete();
        }
        getConnection().exec("DROP TABLE " + name);
    }

    /**
     * Convert a Hashtable into a compressed byte array. Note: the resulting array is java-specific,
     * but stable through jre Versions (serialVersionUID: 1421746759512286392L)
     * 
     * @param hash
     *            the hashtable to store
     * @return
     */
    @SuppressWarnings("unchecked")
    public static byte[] flatten(final Hashtable hash) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream(hash.size() * 30);
            ZipOutputStream zos = new ZipOutputStream(baos);
            zos.putNextEntry(new ZipEntry("hash"));
            ObjectOutputStream oos = new ObjectOutputStream(zos);
            oos.writeObject(hash);
            zos.close();
            baos.close();
            return baos.toByteArray();
        } catch (Exception ex) {
            ExHandler.handle(ex);
            return null;
        }
    }

    /**
     * Recreate a Hashtable from a byte array as created by flatten()
     * 
     * @param flat
     *            the byte array
     * @return the original Hashtable or null if no Hashtable could be created from the array
     */
    @SuppressWarnings("unchecked")
    public static Hashtable<Object, Object> fold(final byte[] flat) {
        try {
            ByteArrayInputStream bais = new ByteArrayInputStream(flat);
            ZipInputStream zis = new ZipInputStream(bais);
            zis.getNextEntry();
            ObjectInputStream ois = new ObjectInputStream(zis);
            Hashtable<Object, Object> res = (Hashtable<Object, Object>) ois.readObject();
            ois.close();
            bais.close();
            return res;
        } catch (Exception ex) {
            ExHandler.handle(ex);
            return null;
        }
    }

    /**
     * Returns array of field names of the database fields.<br>
     * Used for export functionality
     */
    protected String[] getExportFields() {
        try {
            ResultSet res = getConnection().getStatement().query("Select count(id) from " + getTableName());
            ResultSetMetaData rmd = res.getMetaData();
            String[] ret = new String[rmd.getColumnCount()];
            for (int i = 0; i < ret.length; i++) {
                ret[i] = rmd.getColumnName(i + 1);
            }
            return ret;
        } catch (Exception ex) {
            ExHandler.handle(ex);
            return null;
        }
        /*
         * throw new IllegalArgumentException("No export fields for " + getClass().getSimpleName() +
         * " available");
         */
    }

    /**
     * Returns uid value. The uid should be world wide universal.<br>
     * If this code changes, then the method getExportUIDVersion has to be overwritten<br>
     * and the returned value incremented.
     * 
     */
    protected String getExportUIDValue() {
        throw new IllegalArgumentException("No export uid value for " + getClass().getSimpleName() + " available");
    }

    /**
     * Checks the version of the export functionality. If the method<br>
     * getExportUIDValue() changes, this method should return a new number.<br>
     */
    protected String getExportUIDVersion() {
        return "1";
    }

    /**
     * Exports a persistentobject to an xml string
     * 
     * @return
     */
    public String exportData() {
        return XML2Database.exportData(this);
    }

    /**
     * Execute the sql string and handle exceptions appropriately.
     * <p>
     * <b>ATTENTION:</b> JdbcLinkResourceException will trigger a restart of Elexis in
     * at.medevit.medelexis.ui.statushandler.
     * </p>
     * 
     * @param sql
     * @return
     */
    private ResultSet executeSqlQuery(String sql) {
        Stm stm = null;
        ResultSet res = null;
        try {
            stm = getConnection().getStatement();
            res = stm.query(sql);
        } catch (JdbcLinkException je) {
            ElexisStatus status = translateJdbcException(je);
            // trigger restart for severe communication error
            if (je instanceof JdbcLinkResourceException) {
                status.setCode(ElexisStatus.CODE_RESTART | ElexisStatus.CODE_NOFEEDBACK);
                status.setMessage(status.getMessage() + "\nACHTUNG: Elexis wird neu gestarted!\n");
                status.setLogLevel(ElexisStatus.LOG_FATALS);
                // TODO throw PersistenceException to UI code ...
                // calling StatusManager directly here was not intended,
                // but throwing the exception without handling it apropreately
                // in the UI code makes it impossible for the status handler
                // to display a blocking error dialog
                // (this is executed in a Runnable where Exception handling is
                // not blocking UI
                // thread)
                StatusManager.getManager().handle(status);
            } else {
                status.setLogLevel(ElexisStatus.LOG_FATALS);
                throw new PersistenceException(status);
            }
        } finally {
            getConnection().releaseStatement(stm);
        }
        return res;
    }

    private static ElexisStatus translateJdbcException(JdbcLinkException jdbc) {
        if (jdbc instanceof JdbcLinkSyntaxException) {
            return new ElexisStatus(ElexisStatus.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                    "Fehler in der Datenbanksyntax.", jdbc, ElexisStatus.LOG_ERRORS);
        } else if (jdbc instanceof JdbcLinkConcurrencyException) {
            return new ElexisStatus(ElexisStatus.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                    "Fehler bei einer Datenbanktransaktion.", jdbc, ElexisStatus.LOG_ERRORS);
        } else if (jdbc instanceof JdbcLinkResourceException) {
            return new ElexisStatus(ElexisStatus.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                    "Fehler bei der Datenbankkommunikation.", jdbc, ElexisStatus.LOG_ERRORS);
        } else {
            return new ElexisStatus(ElexisStatus.ERROR, Hub.PLUGIN_ID, ElexisStatus.CODE_NONE,
                    "Fehler in der Datenbankschnittstelle.", jdbc, ElexisStatus.LOG_ERRORS);
        }
    }

    public static boolean tableExists(String tableName) {
        int nrFounds = 0;
        // Vergleich schaut nicht auf Gross/Klein-Schreibung, da thomas
        // schon H2-DB gesehen hat, wo entweder alles gross oder alles klein war
        try {
            DatabaseMetaData dmd = j.getConnection().getMetaData();
            String[] onlyTables = { "TABLE" };

            ResultSet rs = dmd.getTables(null, null, "%", onlyTables);
            if (rs != null) {
                while (rs.next()) {
                    // DatabaseMetaData#getTables() specifies TABLE_NAME is in
                    // column 3
                    if (rs.getString(3).equalsIgnoreCase(tableName))
                        nrFounds++;
                }
            }
        } catch (SQLException je) {
            log.log(je, "Fehler beim abrufen der Datenbank Tabellen Information.", Log.ERRORS);
        }
        if (nrFounds > 1) {
            // Dies kann vorkommen, wenn man eine MySQL-datenbank von Windows ->
            // Linuz kopiert
            // und dort nicht die System-Variable lower_case_table_names nicht
            // gesetzt ist
            // Anmerkung von Niklaus Giger
            log.log("Komisch!!! Tabelle " + tableName + " " + nrFounds + "-mal gefunden!!", Log.ERRORS);
        }
        return nrFounds == 1;
    }

    /**
     * Convert an arbitrary value into the database format
     * 
     * @author Marco Descher
     * @since 2.1.6
     * @param in
     *            {@link Object}
     * @return String representing the value in database storage conform format
     */
    public static String ts(Object in) {
        if (in == null)
            return "";
        if (in instanceof String)
            return (String) in;
        if (in instanceof Boolean) {
            return ((Boolean) in) ? "1" : "0";
        }
        if (in instanceof Long)
            return Long.toString((Long) in);
        if (in instanceof Integer)
            return Integer.toString((Integer) in);
        if (in instanceof Double)
            return Double.toString((Double) in);
        if (in instanceof Date) {
            return new SimpleDateFormat("dd.MM.yyyy").format((Date) in);
        }
        if (in instanceof XMLGregorianCalendar) {
            XMLGregorianCalendar dt = (XMLGregorianCalendar) in;
            return new SimpleDateFormat("dd.MM.yyyy").format(dt.toGregorianCalendar().getTime());
        }
        return "";
    }

    public void addChangeListener(IChangeListener listener, String fieldToObserve) {

    }

    public void removeChangeListener(IChangeListener listener, String fieldObserved) {

    }

}