/*******************************************************************************
* Copyright (c) 2005-2014, 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
* MEDEVIT <office@medevit.at>
*******************************************************************************/
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.sql.Connection;
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.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.elexis.core.constants.Preferences;
import ch.elexis.core.constants.StringConstants;
import ch.elexis.core.constants.XidConstants;
import ch.elexis.core.data.activator.CoreHub;
import ch.elexis.core.data.constants.ElexisSystemPropertyConstants;
import ch.elexis.core.data.events.ElexisEvent;
import ch.elexis.core.data.events.ElexisEventDispatcher;
import ch.elexis.core.data.extension.AbstractCoreOperationAdvisor;
import ch.elexis.core.data.extension.CoreOperationExtensionPoint;
import ch.elexis.core.data.interfaces.events.MessageEvent;
import ch.elexis.core.data.status.ElexisStatus;
import ch.elexis.core.data.util.DBUpdate;
import ch.elexis.core.data.util.SqlRunner;
import ch.elexis.core.exceptions.PersistenceException;
import ch.elexis.core.jdt.NonNull;
import ch.elexis.core.jdt.Nullable;
import ch.elexis.core.model.IChangeListener;
import ch.elexis.core.model.IPersistentObject;
import ch.elexis.core.model.ISticker;
import ch.elexis.core.model.IXid;
import ch.elexis.data.Xid.XIDException;
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.Log;
import ch.rgw.tools.StringTool;
import ch.rgw.tools.TimeTool;
import ch.rgw.tools.VersionInfo;
import ch.rgw.tools.VersionedResource;
/**
* 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 für alle Objekte, die in der Datenbank gespeichert werden sollen. Ein
* PersistentObject hat eine eindeutige ID, welche beim Erstellen des Objekts automatisch vergeben
* wird. Grundsätzlich wird jedes Objekt "lazy" geladen, indem jede Leseanforderung zunächst nur
* einen mit der ID des Objekts versehenen Proxy instantiiert und jedes Member-Feld erst auf Anfrage
* nachlädt. Die Klasse stellt statische Funktionen zur Kontaktaufnahme mit der Datenbank und
* member-Funktionen zum Lesen und Schreiben von Feldern der Tochterobjekte zur Verfügung. Die
* get-Methode verwendet einen zeitlich limitierten Cache. um die Zahl teurer Datenbankoperationen
* zu minimieren: Wiederholte Lesezugriffe innerhalb einer einstellbaren lifetime (Standardmässig 15
* Sekunden) werden aus dem cache bedient. PersistentObject kann auch alle Schreibvorgänge in einer
* speziellen Trace-Tabelle dokumentieren. Die get- und set- Methoden kümmern sich selbst um
* codierung/decodierung der Felder, wenn nötig. Aufeinanderfolgende und streng zusammengehörende
* Schreibvorgänge können auch in einer Transaktion zusammengefasst werden, welche nur ganz oder gar
* nicht ausgeführt wird. (begin()). Es ist aber zu beachten, das nicht alle Datenbanken
* Transaktionen unterstützen. MySQL beispielsweise nur, wenn es mit InnoDB-Tabellen eingerichtet
* wurde (welche langsamer sind, als die standardmässig verwendeten MyISAM-Tabellen).
*
* @author gerry
*/
public abstract class PersistentObject implements IPersistentObject {
public static final String MAPPING_ERROR_MARKER = "**ERROR:";
/** predefined field name for the GUID */
public static final String FLD_ID = "id";
/** predefined property to handle a field that is a compressed HashMap */
public static final String FLD_EXTINFO = "ExtInfo";
/** predefined property to hande a field that marks the Object as deleted */
public static final String FLD_DELETED = "deleted";
/**
* predefined property that holds an automatically updated field containing the last update of
* this object as long value (milliseconds as in Date())
*/
public static final String FLD_LASTUPDATE = "lastupdate";
/**
* predefined property that holds the date of creation of this object in the form YYYYMMDD
*/
public static final String FLD_DATE = "Datum";
protected static final String DATE_COMPOUND = "Datum=S:D:Datum";
// maximum character length of int fields in tables
private static int MAX_INT_LENGTH = 10;
protected static Logger log = LoggerFactory.getLogger(PersistentObject.class.getName());
private String id;
private static DBConnection defaultConnection;
public static DBConnection getDefaultConnection(){
return defaultConnection;
}
private DBConnection connection;
protected DBConnection getDBConnection(){
if (connection == null) {
connection = defaultConnection;
}
return connection;
}
public void setDBConnection(DBConnection connection){
this.connection = connection;
}
protected static AbstractCoreOperationAdvisor cod =
CoreOperationExtensionPoint.getCoreOperationAdvisor();
public static enum FieldType {
TEXT, LIST, JOINT
};
/**
* the possible states of a tristate checkbox: true/checked, false/unchecked, undefined/
* "filled with a square"/"partly selected"
*
* @since 3.0.0
*/
static public enum TristateBoolean {
TRUE, FALSE, UNDEF
};
private static Hashtable<String, String> mapping;
static {
mapping = new Hashtable<String, String>();
}
/**
* 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 defaultconnection is set and the Settings
* (CoreHub.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 für diesen Datenbanktyp unter
* rsc gefunden wurde. Wenn die Verbindung hergestell werden konnte, werden die global
* Settings mit dieser Datenbank verbunden.
* @return true für ok, false wenn keine Verbindung hergestellt werden konnte.
*/
public static boolean connect(final Settings cfg){
DBConnection dbConnection = new DBConnection();
dbConnection.setDBUser(System.getProperty(ElexisSystemPropertyConstants.CONN_DB_USERNAME));
dbConnection
.setDBPassword(System.getProperty(ElexisSystemPropertyConstants.CONN_DB_PASSWORD));
dbConnection.setDBFlavor(System.getProperty(ElexisSystemPropertyConstants.CONN_DB_FLAVOR));
dbConnection
.setDBConnectString(System.getProperty(ElexisSystemPropertyConstants.CONN_DB_SPEC));
if (ElexisSystemPropertyConstants.RUN_MODE_FROM_SCRATCH
.equals(System.getProperty(ElexisSystemPropertyConstants.RUN_MODE))) {
dbConnection.setRunningFromScratch(true);
}
log.debug("osgi.install.area: " + System.getProperty("osgi.install.area"));
String demoDBLocation = System.getProperty(ElexisSystemPropertyConstants.DEMO_DB_LOCATION);
if (demoDBLocation == null) {
demoDBLocation = CoreHub.getWritableUserDir() + File.separator + "demoDB";
}
File demo = new File(demoDBLocation);
log.info("Checking demo database availability in " + demo.getAbsolutePath());
// --
// returns if either, demo db, direct connection or run from scratch
// --
if (demo.exists() && demo.isDirectory()) {
// open demo database connection
log.info("Using demoDB in " + demo.getAbsolutePath());
dbConnection.createH2Link(demo.getAbsolutePath() + File.separator + "db");
try {
String username =
System.getProperty(ElexisSystemPropertyConstants.CONN_DB_USERNAME);
if (username == null) {
dbConnection.setDBUser("sa");
}
String password =
System.getProperty(ElexisSystemPropertyConstants.CONN_DB_PASSWORD);
if (password == null) {
dbConnection.setDBPassword(StringTool.leer);
}
return dbConnection.connect() && connect(dbConnection);
} 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 (dbConnection.isDirectConnectConfigured()) {
// open direct database connection according to system properties
return (PersistentObject.connect(dbConnection, true) && connect(dbConnection));
} else if (dbConnection.isRunningFromScratch()) {
// run from scratch configuration with a temporary database
try {
dbConnection.runFromScatch();
if (dbConnection.connect()) {
return connect(dbConnection);
} else {
log.error("can't create test database");
System.exit(-6);
}
} catch (Exception ex) {
log.error("can't create test database");
System.exit(-7);
}
}
// --
// initialize a regular database connection
// --
Hashtable<Object, Object> hConn = getConnectionHashtable();
if (hConn != null) {
dbConnection.setDBDriver(
checkNull((String) hConn.get(Preferences.CFG_FOLDED_CONNECTION_DRIVER)));
dbConnection
.setDBUser(checkNull((String) hConn.get(Preferences.CFG_FOLDED_CONNECTION_USER)));
dbConnection.setDBPassword(
checkNull((String) hConn.get(Preferences.CFG_FOLDED_CONNECTION_PASS)));
dbConnection
.setDBFlavor(checkNull((String) hConn.get(Preferences.CFG_FOLDED_CONNECTION_TYPE)));
dbConnection.setDBConnectString(
checkNull((String) hConn.get(Preferences.CFG_FOLDED_CONNECTION_CONNECTSTRING)));
}
log.info("Driver is " + dbConnection.getDBDriver());
try {
log.info("Current work directory is " + new java.io.File(".").getCanonicalPath());
} catch (IOException e) {
log.error("Error determining current work directory", e);
}
if (StringTool.leer.equals(dbConnection.getDBDriver())) {
cod.requestDatabaseConnectionConfiguration();
MessageEvent.fireInformation("Datenbankverbindung geändert",
"Bitte starten Sie Elexis erneut");
System.exit(-1);
}
try {
dbConnection.connect();
} catch (JdbcLinkException je) {
ElexisStatus status = translateJdbcException(je);
status.setLogLevel(ElexisStatus.LOG_FATALS);
throw new PersistenceException(status);
}
return connect(dbConnection);
}
/**
*
* @return a {@link Hashtable} containing the connection parameters, use
* {@link Preferences#CFG_FOLDED_CONNECTION} to retrieve the required parameters,
* castable to {@link String}
*/
public static @NonNull Hashtable<Object, Object> getConnectionHashtable(){
Hashtable<Object, Object> ret = new Hashtable<>();
String cnt = CoreHub.localCfg.get(Preferences.CFG_FOLDED_CONNECTION, null);
if (cnt != null) {
log.debug("Read connection string from localCfg");
ret = fold(StringTool.dePrintable(cnt));
}
return ret;
}
/**
* Directly connect to the database using the combined connection information.
*
* @param dbFlavor
* either <code>mysql</code>, <code>postgresql</code> or <code>h2</code>
* @param dbSpec
* connection string fitting to dbFlavor, e.g.
* <code>jdbc:postgresql://192.168.0.3:5432/elexis</code>
* @param dbUser
* the <code>username</code> to connect to the database with
* @param dbPw
* the <code>password</code> to connect to the database with
* @param exitOnFail
* @return
* @since 3.0.0
*/
private static boolean connect(DBConnection dbConnection, boolean exitOnFail){
String msg;
try {
boolean connected = dbConnection.directConnect();
if (!connected) {
msg = "can't connect to test database: " + dbConnection.getDBConnectString()
+ " using " + dbConnection.getDBFlavor();
log.error(msg);
if (exitOnFail) {
System.exit(-6);
}
}
return connected;
} catch (Exception ex) {
msg = "Exception connecting to test database:" + dbConnection.getDBConnectString()
+ " using " + dbConnection.getDBFlavor() + ": " + ex.getMessage();
log.error(msg);
if (exitOnFail) {
System.exit(-7);
}
return false;
}
}
/**
* Connect using an already connected {@link JdbcLink}. Creates a new {@link DBConnection} and
* uses it as default connection.
*
* @param jdbcLink
* @return
*/
public static boolean connect(final JdbcLink jdbcLink){
DBConnection dbConnection = new DBConnection();
dbConnection.setJdbcLink(jdbcLink);
return connect(dbConnection);
}
/**
* Set the default {@link DBConnection} used by all PersistentObject instances. </br>
* </br>
* For connecting to other Elexis Databases use {@link Query} with another DBConnection
* instance.
*
* @param connection
* the already connected DBConnection
* @return
*/
public static boolean connect(final DBConnection connection){
defaultConnection = connection;
if (connection.isRunningFromScratch()) {
deleteAllTables();
}
if (tableExists("CONFIG")) {
CoreHub.globalCfg = new SqlSettings(connection.getJdbcLink(), "CONFIG");
String created = CoreHub.globalCfg.get("created", null);
log.debug("Database version " + created);
} else {
log.debug("No Version found. Creating new Database");
Stm stm = null;
try (InputStream is =
PersistentObject.class.getResourceAsStream("/rsc/createDB.script")) {
stm = connection.getStatement();
if (stm.execScript(is, true, true) == true) {
executeDBInitScriptForClass(User.class, null);
executeDBInitScriptForClass(Role.class, null);
CoreHub.globalCfg = new SqlSettings(connection.getJdbcLink(), "CONFIG");
CoreHub.globalCfg.undo();
CoreHub.globalCfg.set("created", new TimeTool().toString(TimeTool.FULL_GER));
Mandant.initializeAdministratorUser();
CoreHub.pin.initializeGrants();
CoreHub.pin.initializeGlobalPreferences();
if (connection.isRunningFromScratch()) {
Mandant m = new Mandant("007", "topsecret");
String clientEmail =
System.getProperty(ElexisSystemPropertyConstants.CLIENT_EMAIL);
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");
} else {
cod.requestInitialMandatorConfiguration();
}
CoreHub.globalCfg.flush();
CoreHub.localCfg.flush();
if (!connection.isRunningFromScratch()) {
MessageEvent.fireInformation("Neue Datenbank",
"Es wurde eine neue Datenbank angelegt.");
}
} else {
log.error("Kein create script für Datenbanktyp " + connection.getDBFlavor()
+ " gefunden.");
return false;
}
} catch (Throwable ex) {
ExHandler.handle(ex);
return false;
} finally {
connection.releaseStatement(stm);
}
}
// Zugriffskontrolle initialisieren
VersionInfo vi = new VersionInfo(CoreHub.globalCfg.get("dbversion", "0.0.0"));
log.info("Verlangte Datenbankversion: " + CoreHub.DBVersion);
log.info("Gefundene Datenbankversion: " + vi.version());
if (vi.isOlder(CoreHub.DBVersion)) {
log.warn("Ältere Version der Datenbank gefunden ");
DBUpdate.doUpdate();
}
vi = new VersionInfo(CoreHub.globalCfg.get("ElexisVersion", "0.0.0"));
log.info("Verlangte Elexis-Version: " + vi.version());
log.info("Vorhandene Elexis-Version: " + CoreHub.Version);
VersionInfo v2 = new VersionInfo(CoreHub.Version);
if (vi.isNewerMinor(v2)) {
String msg = String.format(
"Die Datenbank %1s ist für eine neuere Elexisversion '%2s' als die aufgestartete '%3s'. Wollen Sie trotzdem fortsetzen?",
connection.getDBConnectString(), vi.version().toString(), v2.version().toString());
log.error(msg);
if (!cod.openQuestion("Diskrepanz in der Datenbank-Version ", msg)) {
System.exit(2);
} else {
log.error("User continues with Elexis / database version mismatch");
}
}
// verify locale
Locale locale = Locale.getDefault();
String dbStoredLocale = CoreHub.globalCfg.get(Preferences.CFG_LOCALE, null);
if (dbStoredLocale == null) {
CoreHub.globalCfg.set(Preferences.CFG_LOCALE, locale.toString());
CoreHub.globalCfg.flush();
} else {
if (!locale.toString().equals(dbStoredLocale)) {
String msg = String.format(
"Your locale [%1s] does not match the required database locale [%2s]. Ignore?",
locale.toString(), dbStoredLocale);
log.error(msg);
if (!cod.openQuestion("Difference in locale setting ", msg)) {
System.exit(2);
} else {
log.error("User continues with difference locale set");
}
}
}
connection.initTrace();
return true;
}
/**
* Return the Object containing the connection. This should only in very specific conditions be
* neccessary, if one needs a direct 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 enthält zurückliefern. Sollte nur in Ausnahmefällen nötig
* sein, wenn doch mal ein direkter Zugriff auf die Datenbank erforderlich ist.
*
* @deprecated do not use direct JdbcLink access
* @return den JdbcLink, der die Verbindung zur Datenbank enthält
*/
public static JdbcLink getConnection(){
return defaultConnection.getJdbcLink();
}
/**
* Die Zuordnung von Membervariablen zu Datenbankfeldern geschieht über statische mappings:<br>
* Jede abgeleitete Klassen muss ihre mappings in folgender Form deklarieren:
* <code>addMapping("Tabellenname","Variable=Feld"...)</code>; wobei:
* <ul>
* <li><code>Variable=Feld</code> - Einfache Zuordnung, Variable wird zu Feld</li>
* <li><code>Variable=S:x:Feld</code> - Spezielle Abspeicherung
* <ul>
* <li><code>x=D</code> - Datumsfeld, wird automatisch in Standardformat gebracht></li>
* <li><code>x=C</code> - Feld wird vor Abspeicherung komprimiert</li>
* <li><code>X=N</code> - Feld wird als Long interrpetiert</li>
* </ul>
* <li><code>Variable=JOINT:FremdID:EigeneID:Tabelle[:type]</code> - n:m - Zuordnungen</li>
* <li><code>Variable=LIST:EigeneID:Tabelle:orderby[:type]</code> - 1:n - Zuordnungen</li>
* <li><code>Variable=EXT:tabelle:feld</code> - 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 + FLD_LASTUPDATE, FLD_LASTUPDATE);
}
/**
* Exklusiven Zugriff auf eine Ressource verlangen. Die Sperre kann für maximal zwei Sekunden
* beansprucht werden, dann wird sie gelöst. Dies ist eine sehr teure Methode, die eigentlich
* nur notwendig ist, weil es keine standardisierte JDBC-Methode für Locks gibt... Die Sperre
* ist kooperativ: Sie verhindert konkurrierende Zugriffe nicht wirklich, sondern verlässt sich
* darauf, dass Zugreifende freiwillig zuerst die Sperre abfragen. Sie bezieht sich auch nicht
* direkt auf eine bestimmte Tabelle, sondern immer nur auf eine willkürliche frei wählbare
* Bezeichnung. Diese muss für jedes zu schützende Objekt standardisiert werden.
*
* @param name
* Name der gewünschten Sperre
* @param wait
* wenn True, warten bis die sperre frei oder abgelaufen ist
* @return null, wenn die Sperre belegt war, sonst eine id für 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");
try {
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 -> Löschen
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());
// Prüfen, 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;
}
}
return lockid;
} finally {
getConnection().releaseStatement(stm);
}
}
/**
* 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;
}
/**
* Einschränkende Bedingungen für Suche nach diesem Objekt definieren
*
* @return ein Constraint für eine Select-Abfrage
*/
protected String getConstraint(){
return "";
}
/**
* Bedingungen für dieses Objekt setzen
*/
protected void setConstraint(){
/* Standardimplementation ist leer */
}
/** Einen menschenlesbaren Identifikationsstring für 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 gültig ist.
*
* @return true wenn die Daten gültig (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 unveränderlich.
*
* @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 Textrepräsentation des Objektes erstellt, mit deren Hilfe das Objekt später
* 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(){
return getClass().getName() + StringConstants.DOUBLECOLON + getId();
}
/** 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 instantiation 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 = getDBConnection().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 gelöscht 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(XidConstants.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_ID_IN_DOMAIN);
}
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, XidConstants.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_ID_IN_DOMAIN, 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 "höchstwertigen" 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.FLD_LINKTABLE + " WHERE obj=?";
/**
* Return all Stickers attributed to this objecz
*
* @return A possibly empty list of Stickers
*/
@SuppressWarnings("unchecked")
public List<ISticker> getStickers(){
DBConnection dbConnection = getDBConnection();
String ID = new StringBuilder().append("ETK").append(getId()).toString();
ArrayList<ISticker> ret =
(ArrayList<ISticker>) dbConnection.getCache().get(ID, getCacheTime());
if (ret != null) {
return ret;
}
ret = new ArrayList<ISticker>();
PreparedStatement queryStickers = dbConnection.getPreparedStatement(queryStickersString);
try {
queryStickers.setString(1, id);
ResultSet res = queryStickers.executeQuery();
while (res.next()) {
Sticker et = Sticker.load(res.getString(1));
et.setDBConnection(dbConnection);
if (et != null && et.exists()) {
ret.add(et);
}
}
res.close();
} catch (Exception ex) {
ExHandler.handle(ex);
return ret;
} finally {
try {
queryStickers.close();
} catch (SQLException e) {
// ignore
}
dbConnection.releasePreparedStatement(queryStickers);
}
Collections.sort(ret);
dbConnection.getCache().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){
DBConnection dbConnection = getDBConnection();
String ID = new StringBuilder().append("ETK").append(getId()).toString();
ArrayList<Sticker> ret =
(ArrayList<Sticker>) dbConnection.getCache().get(ID, getCacheTime());
if (ret != null) {
ret.remove(et);
}
StringBuilder sb = new StringBuilder();
sb.append("DELETE FROM ").append(Sticker.FLD_LINKTABLE).append(" WHERE obj=")
.append(getWrappedId()).append(" AND etikette=").append(JdbcLink.wrap(et.getId()));
dbConnection.exec(sb.toString());
}
/**
* Add a Sticker to this object
*
* @param st
* the Sticker to add
*/
@SuppressWarnings("unchecked")
public void addSticker(ISticker st){
DBConnection dbConnection = getDBConnection();
String ID = new StringBuilder().append("STK").append(getId()).toString();
List<ISticker> ret = (List<ISticker>) dbConnection.getCache().get(ID, getCacheTime());
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.FLD_LINKTABLE)
.append("(obj,etikette) VALUES (").append(getWrappedId()).append(",")
.append(JdbcLink.wrap(st.getId())).append(");");
dbConnection.exec(sb.toString());
}
}
/**
* Feststellen, ob ein PersistentObject als gelöscht markiert wurde
*
* @return true wenn es gelöscht 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 dazugehörige Datenbankfeld ermitteln, beinhaltet das jeweilige Prefix
*
* @param f
* Der Feldname
* @return Das Datenbankfeld oder **ERROR**, wenn kein mapping für das angegebene Feld
* existiert.
*/
public String map(final String f){
String prefix = getTableName();
return map(prefix, f);
}
/**
* Return the database field corresponding to an internal Elexis field value
*
* @param tableName
* the tableName
* @param field
* the field name
* @return the database field or ERROR* if no mapping exists
* @since 3.1
*/
public static String map(final String tableName, final String field){
return map(tableName, field, true);
}
/**
* Return the database field corresponding to an internal Elexis field value
*
* @param tableName
* @param field
* @param markAsError
* whether to return ERROR* or <code>null</code> if no entry found
* @return
* @since 3.2
*/
public static String map(final String tableName, final String field, boolean markAsError){
if (FLD_ID.equalsIgnoreCase(field))
return field;
String res = mapping.get(tableName + field);
if (res == null) {
if (markAsError) {
log.info("field is not mapped " + field);
return MAPPING_ERROR_MARKER + field + "**";
}
}
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 während 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){
if (getId() == null || getId().isEmpty()) {
log.error("Get with no ID on object of type [" + this.getClass().getName() + "]");
}
DBConnection dbConnection = getDBConnection();
String key = getKey(field);
Object ret = dbConnection.getCache().get(key, getCacheTime());
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.error("Fehlerhaftes Mapping bei " + field);
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], getDBConnection());
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, getDBConnection());
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.warn("Fehler bei Felddefinition " + field);
ElexisStatus status = new ElexisStatus(ElexisStatus.WARNING, CoreHub.PLUGIN_ID,
ElexisStatus.CODE_NOFEEDBACK, "Fehler bei Felddefinition", nmex);
ElexisEventDispatcher.fireElexisStatusEvent(status);
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("'");
Stm stm = getDBConnection().getStatement();
String res = null;
try (ResultSet rs = executeSqlQuery(sql.toString(), stm)) {
if ((rs != null) && (rs.next() == true)) {
if (decrypt) {
res = decode(field, rs);
} else {
res = rs.getString(mapped);
}
if (res == null) {
res = "";
}
getDBConnection().getCache().put(key, res, getCacheTime());
}
} catch (SQLException ex) {
ExHandler.handle(ex);
} finally {
getDBConnection().releaseStatement(stm);
}
return res;
}
public byte[] getBinary(final String field){
String key = getKey(field);
Object o = getDBConnection().getCache().get(key, getCacheTime());
if (o instanceof byte[]) {
return (byte[]) o;
}
byte[] ret = getBinaryRaw(field);
getDBConnection().getCache().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("'");
Stm stm = getDBConnection().getStatement();
try (ResultSet rs = executeSqlQuery(sql.toString(), stm)) {
if ((rs != null) && (rs.next() == true)) {
return rs.getBytes(mapped);
}
} catch (Exception ex) {
ExHandler.handle(ex);
} finally {
getDBConnection().releaseStatement(stm);
}
return null;
}
protected VersionedResource getVersionedResource(final String field, final boolean flushCache){
String key = getKey(field);
if (flushCache == false) {
Object o = getDBConnection().getCache().get(key, getCacheTime());
if (o instanceof VersionedResource) {
return (VersionedResource) o;
}
}
byte[] blob = getBinaryRaw(field);
VersionedResource ret = VersionedResource.load(blob);
getDBConnection().getCache().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 @NonNull Map getMap(final String field){
String key = getKey(field);
Object o = getDBConnection().getCache().get(key, getCacheTime());
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();
}
getDBConnection().getCache().put(key, ret, getCacheTime());
return ret;
}
/**
* Retrieves an object out of the {@link #FLD_EXTINFO} if it exists
*
* @param key
* @return the {@link Object} stored for the given key in ExtInfo, or <code>null</code>
* @since 3.0
*/
public @Nullable Object getExtInfoStoredObjectByKey(final Object key){
// query cache?
byte[] binaryRaw = getBinaryRaw(FLD_EXTINFO);
if (binaryRaw == null)
return null;
@SuppressWarnings("unchecked")
Map<Object, Object> ext = getMap(FLD_EXTINFO);
return ext.get(key);
}
/**
* Set a value in the {@link #FLD_EXTINFO} field, will create an ExtInfo field if required
*
* @param key
* @param value
* to store, if <code>null</code> removes the respective entry
* @since 3.0
* @since 3.1 if value <code>null</code> removes the respective entry
*/
public void setExtInfoStoredObjectByKey(final Object key, final Object value){
Map extinfo = getMap(FLD_EXTINFO);
if (value == null) {
extinfo.remove(key);
} else {
extinfo.put(key, value);
}
setMap(FLD_EXTINFO, extinfo);
}
/**
* 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));
}
/**
* convenience method to read a boolean value, write it using {@link #ts(Object)} and
* {@link #set(String, String)}
*
* @param field
* @return <code>true</code> iff the stored value is <code>1</code>
* @since 3.1
*/
public boolean getBoolean(final String field){
String val = get(field);
return (StringConstants.ONE.equals(val)) ? true : false;
}
/**
* returns the selected TristateBoolean value (for a tristate checkbox)
*
* @param field
* the name of the field to be tested
* @return the current tristate selection state, one of TristateBoolean (TRUE/FALSE/UNDEF)
* @author H. Marlovits
* @since 3.0.0
*/
public TristateBoolean getTriStateBoolean(final String field){
String value = get(field);
if (value == null)
return TristateBoolean.UNDEF;
if (value.equalsIgnoreCase(StringConstants.ONE))
return TristateBoolean.TRUE;
else if (value.equalsIgnoreCase(StringConstants.ZERO))
return TristateBoolean.FALSE;
else
return TristateBoolean.UNDEF;
}
/**
* save the selected TristateBoolean value (of a tristate checkbox)
*
* @param field
* the name of the field to be set
* @param newVal
* the new state to save to the cb, one of TristateBoolean (TRUE/FALSE/UNDEF)
* @author H. Marlovits
* @since 3.0.0
*/
public void setTriStateBoolean(final String field, TristateBoolean newVal)
throws IllegalArgumentException, PersistenceException{
if (newVal == null)
throw new IllegalArgumentException(
"PersistentObject.setTriStateBoolean(): param newVal == null");
String saveVal = "";
if (newVal == TristateBoolean.TRUE)
saveVal = StringConstants.ONE;
if (newVal == TristateBoolean.FALSE)
saveVal = StringConstants.ZERO;
if (newVal == TristateBoolean.UNDEF)
saveVal = StringConstants.EMPTY;
boolean result = set(field, saveVal);
if (!result) {
throw new PersistenceException(
new ElexisStatus(Status.ERROR, CoreHub.PLUGIN_ID, ElexisStatus.CODE_NONE,
"PersistentObject.setTriStateBoolean(): Error on saving value " + newVal
+ " to field " + field,
null));
}
}
/**
* Eine 1:n Verknüpfung 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 rückwärts sortiert
* @return eine Liste mit den IDs (String!) der verknüpften Datensätze oder null, wenn das Feld
* keine 1:n-Verknüofung ist
*/
@SuppressWarnings("unchecked")
public List<String> getList(final String field, final boolean reverse){
return getList(field, reverse, false);
}
/**
* Read a 1:n association from the database
*
* @param field
* the field, as provided in the mapping declaration
* @param reverse
* if <code>true</code> perform reverse ordering
* @param includeDeleted
* if <code>true</code> include objects marked as deleted
* @return
* @since 3.2
*/
public List<String> getList(final String field, final boolean reverse,
final boolean includeDeleted){
StringBuffer sql = new StringBuffer();
String mapped = map(field);
if (mapped.startsWith("LIST:")) {
// LIST:EigeneID:Tabelle:orderby[:type]
String[] m = mapped.split(":");
if (m.length > 2) {
// String order=null;
sql.append("SELECT ID FROM ").append(m[2]).append(" WHERE ");
if (!includeDeleted) {
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 = getDBConnection().getStatement();
List<String> ret = stm.queryList(sql.toString(), new String[] {
"ID"
});
getDBConnection().releaseStatement(stm);
return ret;
}
} else {
log.error("Fehlerhaftes Mapping " + mapped);
}
return null;
}
/**
* Eine n:m - Verknüpfung auslesen
*
* @param field
* Das Feld, für 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
*/
@SuppressWarnings("unchecked")
public List<String[]> getList(final String field, String[] extra){
if (extra == null) {
extra = new String[0];
}
String mapped = map(field);
if (mapped.startsWith("JOINT:")) {
StringBuffer sql = new StringBuffer();
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());
Stm stm = getDBConnection().getStatement();
LinkedList<String[]> list = new LinkedList<String[]>();
try (ResultSet rs = executeSqlQuery(sql.toString(), stm)) {
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);
}
return list;
} catch (Exception ex) {
ElexisStatus status =
new ElexisStatus(ElexisStatus.ERROR, CoreHub.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;
} finally {
getDBConnection().releaseStatement(stm);
}
} else {
log.error("Fehlerhaftes Mapping " + mapped);
}
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) {
getDBConnection().getCache().remove(key);
sql.append("UPDATE ").append(table).append(" SET ");
int i = -1;
while ((i = mapped.indexOf(StringConstants.COLON)) > 0) {
mapped = mapped.substring(i + 1);
}
sql.append(mapped);
sql.append("=NULL, " + FLD_LASTUPDATE + "=" + Long.toString(ts) + " WHERE ID=")
.append(getWrappedId());
getDBConnection().exec(sql.toString());
return true;
}
Object oldval = getDBConnection().getCache().get(key, getCacheTime());
if (mapped.startsWith("S:")) {
getDBConnection().getCache().remove(key); // clear cache
} else {
getDBConnection().getCache().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.error("Fehlerhaftes Mapping bei " + field);
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("=?, " + FLD_LASTUPDATE + "=? WHERE ID=").append(getWrappedId());
String cmd = sql.toString();
DBConnection dbConnection = getDBConnection();
PreparedStatement pst = null;
try {
pst = dbConnection.getPreparedStatement(cmd);
encode(1, pst, field, value);
if (dbConnection.isTrace()) {
StringBuffer params = new StringBuffer();
params.append("[");
params.append(value);
params.append("]");
dbConnection.doTrace(cmd + " " + params);
}
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, CoreHub.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 {
if(pst!=null) {
try {
pst.close();
} catch (SQLException e) {}
dbConnection.releasePreparedStatement(pst);
}
}
}
/**
* Eine Hashtable speichern. Diese wird zunächst in ein byte[] geplättet, und so gespeichert.
*
* @param field
* @param map
* @return 0 bei Fehler
*/
@SuppressWarnings("rawtypes")
public void setMap(final String field, final Map<Object, Object> map){
if (map == null) {
throw new PersistenceException(new ElexisStatus(Status.ERROR, CoreHub.PLUGIN_ID,
ElexisStatus.CODE_NONE, "Attempt to store Null map", null));
}
byte[] bin = flatten((Hashtable) map);
getDBConnection().getCache().put(getKey(field), map, getCacheTime());
setBinary(field, bin);
}
/**
* Eine VersionedResource zurückschreiben. Um Datenverlust durch gleichzeitigen Zugriff zu
* vermeiden, wird zunächst 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, CoreHub.actUser.getLabel()) == true) {
getDBConnection().getCache().put(getKey(field), old, getCacheTime());
setBinary(field, old.serialize());
}
unlock("VersionedResource", lockid);
}
public void setBinary(final String field, final byte[] value){
String key = getKey(field);
getDBConnection().getCache().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("=?, " + FLD_LASTUPDATE + "=?").append(" WHERE ID=").append(getWrappedId());
String cmd = sql.toString();
DBConnection dbConnection = getDBConnection();
if (dbConnection.isTrace()) {
dbConnection.doTrace(cmd);
}
PreparedStatement stm = dbConnection.getPreparedStatement(cmd);
try {
stm.setBytes(1, value);
stm.setLong(2, System.currentTimeMillis());
stm.executeUpdate();
} catch (Exception ex) {
log.error("Fehler beim Ausführen der Abfrage " + cmd, ex);
throw new PersistenceException(
new ElexisStatus(Status.ERROR, CoreHub.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());
}
dbConnection.releasePreparedStatement(stm);
}
}
/**
* 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;
}
}
/**
* Eine Element einer n:m Verknüpfung eintragen. Zur Tabellendefinition wird das mapping
* verwendet.
*
* @param field
* Das n:m Feld, für das ein neuer Eintrag erstellt werden soll.
* @param oID
* ID des Zielobjekts, auf das der Eintrag zeigen soll
* @param extra
* Definition der zusätzlichen 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);
DBConnection dbConnection = getDBConnection();
int numberOfAffectedRows = 0;
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.error("Fehlerhafter Aufruf addToList " + s);
return 0;
}
head.append(",").append(def[0]);
tail.append(",").append(JdbcLink.wrap(def[1]));
}
}
head.append(tail).append(")");
if (dbConnection.isTrace()) {
String sql = head.toString();
dbConnection.doTrace(sql);
return dbConnection.exec(sql);
}
numberOfAffectedRows = dbConnection.exec(head.toString());
}
} else if (mapped.startsWith("LIST:")) {
// LIST:EigeneID:Tabelle:orderby[:type]
String[] m = mapped.split(":");
if (m.length > 2) {
PreparedStatement ps = null;
try {
String psString =
"INSERT INTO " + m[2] + " (ID, deleted, " + m[1] + ") VALUES (?, 0, ?);";
ps = dbConnection.getPreparedStatement(psString);
ps.setString(1, oID);
ps.setString(2, getId());
numberOfAffectedRows = ps.executeUpdate();
} catch (SQLException e) {
log.error("Error executing prepared statement.", e);
} finally {
dbConnection.releasePreparedStatement(ps);
}
}
} else {
log.error("Fehlerhaftes Mapping: " + mapped);
return 0;
}
if (numberOfAffectedRows > 0) {
refreshLastUpdateAndSendUpdateEvent(field);
}
return numberOfAffectedRows;
}
/**
* Remove all relations to this object from link
*
* @param field
*/
public void removeFromList(String field){
String mapped = map(field);
DBConnection dbConnection = getDBConnection();
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 (dbConnection.isTrace()) {
String sq = sql.toString();
dbConnection.doTrace(sq);
}
int numberOfAffectedRows = dbConnection.exec(sql.toString());
if (numberOfAffectedRows > 0) {
refreshLastUpdateAndSendUpdateEvent(field);
}
return;
}
}
log.error("Fehlerhaftes Mapping: " + mapped);
}
/**
* Remove a relation to this object from link
*
* @param field
* @param oID
*/
public void removeFromList(String field, String oID){
String mapped = map(field);
String[] m = mapped.split(":");
int numberOfAffectedRows = 0;
DBConnection dbConnection = getDBConnection();
if (mapped.startsWith("JOINT:")) {
//m: m[1] FremdID, m[2] eigene ID, m[3] table
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 (dbConnection.isTrace()) {
String sq = sql.toString();
dbConnection.doTrace(sq);
}
numberOfAffectedRows = dbConnection.exec(sql.toString());
}
} else if (mapped.startsWith("LIST:")) {
//m: m[1] FremdID, m[2] table
if (m.length > 2) {
PreparedStatement ps = null;
try {
String psString = "DELETE FROM " + m[2] + " WHERE " + m[1] + "= ? AND ID = ?;";
ps = dbConnection.getPreparedStatement(psString);
ps.setString(1, getId());
ps.setString(2, oID);
numberOfAffectedRows = ps.executeUpdate();
} catch (SQLException e) {
log.error("Error executing prepared statement.", e);
} finally {
dbConnection.releasePreparedStatement(ps);
}
}
} else {
log.error("Fehlerhaftes Mapping: " + mapped);
}
if (numberOfAffectedRows > 0) {
refreshLastUpdateAndSendUpdateEvent(field);
}
}
/**
* 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){
return create(customID, null, null);
}
/**
* Create a new object, persisting it into database, including the given fields and values.
*
* @param customID
* if <code>null</code> generates an ID, else uses the provided
* @param fields
* the fields to include in the insert statement, does currently NOT support special
* field types as defined in {@link #addMapping(String, String...)}. Use
* <code>null</code> if not applied
* @param values
* the values, the length of this array must be equal to the fields array length. Use
* <code>null</code> if not applied
* @return <code>true</code> if operation successful
* @since 3.2
*/
protected boolean create(final String customID, final String[] fields, final String[] values){
if (customID != null) {
id = customID;
}
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO " + getTableName() + " (");
List<String> fieldS = new ArrayList<String>();
fieldS.add(FLD_ID);
fieldS.add(FLD_LASTUPDATE);
List<String> valuesS = new ArrayList<String>();
valuesS.add(id);
valuesS.add(Long.toString(System.currentTimeMillis()));
if (fields != null && values != null && fields.length == values.length
&& fields.length > 0) {
fieldS.addAll(Arrays.asList(fields));
valuesS.addAll(Arrays.asList(values));
}
sql.append(
fieldS.stream().map(s -> map(s)).reduce((u, t) -> u + StringConstants.COMMA + t).get());
sql.append(") VALUES (");
sql.append(valuesS.stream().map(s -> JdbcLink.wrap(ts(s)))
.reduce((u, t) -> u + StringConstants.COMMA + t).get());
sql.append(")");
if (getDBConnection().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 löschen 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(FLD_DELETED, StringConstants.ONE)) {
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);
IPersistentObject 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));
getDBConnection().getCache().remove(getKey(FLD_DELETED));
return true;
}
return false;
}
/**
* Effectively removes the object from the database by performing an SQL delete call.
*
* @return whether the operation was successful
* @since 3.2
*/
public boolean removeFromDatabase(){
log.debug("removeFromDatabase() [" + this.getClass().getName() + "@" + getId() + "]");
int result =
getDBConnection().exec("DELETE FROM " + getTableName() + " WHERE ID=" + getWrappedId());
return (result == 1);
}
/**
* Alle Bezüge aus einer n:m-Verknüpfung zu diesem Objekt löschen
*
* @param field
* Feldname, der die Liste definiert
* @return
*/
public boolean deleteList(final String field){
String mapped = map(field);
if (!mapped.startsWith("JOINT:")) {
ElexisStatus status =
new ElexisStatus(ElexisStatus.ERROR, CoreHub.PLUGIN_ID, ElexisStatus.CODE_NONE,
"Feld " + field + " ist keine n:m Verknüpfung", null, ElexisStatus.LOG_ERRORS);
ElexisEventDispatcher.fireElexisStatusEvent(status);
return false;
}
String[] m = mapped.split(":");// m[1] FremdID, m[2] eigene ID, m[3]
// Name Joint
getDBConnection().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.error("Falsche Felddefinition für set");
return false;
}
DBConnection dbConnection = getDBConnection();
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);
dbConnection.getCache().put(getKey(fields[i]), values[i], getCacheTime());
}
sql.append("=?,");
}
sql.append(FLD_LASTUPDATE + "=?");
// sql.delete(sql.length() - 1, 100000);
sql.append(" WHERE ID=").append(getWrappedId());
String cmd = sql.toString();
PreparedStatement pst = dbConnection.getPreparedStatement(cmd);
for (int i = 0; i < fields.length; i++) {
encode(i + 1, pst, fields[i], values[i]);
}
if (dbConnection.isTrace()) {
StringBuffer params = new StringBuffer();
params.append("[");
params.append(StringTool.join(values, ", "));
params.append("]");
dbConnection.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, CoreHub.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) {}
dbConnection.releasePreparedStatement(pst);
}
}
/**
* @param checkNulls
* wether the returned values should be <code>null</code> safe, that is no
* <code>null</code> values, but only ""
* @param fields
* @return array containing the required fields in order
* @since 3.1
*/
public String[] get(boolean checkNulls, String... fields){
String[] ret = new String[fields.length];
get(fields, ret);
if (checkNulls) {
for (int i = 0; i < ret.length; i++) {
ret[i] = checkNull(ret[i]);
}
}
return ret;
}
/**
* Read multiple fields, as defined in fields into the values array
*
* @param fields
* the field to read
* @param values
* the storage array for the fields
* @return true if values were set, else <code>false</code> and exception is created
*/
public boolean get(final String[] fields, final String[] values){
if (getId() == null || getId().isEmpty()) {
log.error("Get with no ID on object of type [" + this.getClass().getName() + "]");
}
if ((fields == null) || (values == null) || (fields.length != values.length)) {
log.error("Falscher Aufruf von get(String[],String[]");
return false;
}
DBConnection dbConnection = getDBConnection();
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 = dbConnection.getCache().get(key, getCacheTime());
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());
Stm stm = dbConnection.getStatement();
try (ResultSet rs = executeSqlQuery(sql.toString(), stm)) {
if ((rs != null) && rs.next()) {
for (int i = 0; i < values.length; i++) {
if (values[i] == null) {
if (decode[i] == true) {
values[i] = decode(fields[i], rs);
} else {
values[i] = checkNull(rs.getString(map(fields[i])));
}
dbConnection.getCache().put(getKey(fields[i]), values[i], getCacheTime());
}
}
}
return true;
} catch (Exception ex) {
ExHandler.handle(ex);
return false;
} finally {
dbConnection.releaseStatement(stm);
}
}
/**
* 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 exp != null ? StringTool.createString(exp) : null;
case 'V':
byte[] in = rs.getBytes(mapped.substring(4));
VersionedResource vr = VersionedResource.load(in);
return vr.getHead();
}
}
} catch (Exception ex) {
log.error("Fehler bei decode in field [{}]", field, ex);
// 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.error("Unbekannter encode code " + typ);
}
} else {
pst.setString(num, value);
}
} catch (Exception ex) {
ElexisStatus status = new ElexisStatus(ElexisStatus.ERROR, CoreHub.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.error("Fehler beim String encoder: " + ex.getMessage());
}
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 bezüglich 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 genäss
* 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 für 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;
}
/**
* 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){
return getTableName() + "." + getId() + "#" + field;
}
/**
* Verbindung zur Datenbank trennen
*
*/
public static void disconnect(){
if (defaultConnection != null) {
defaultConnection.disconnect();
}
}
@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
* @since 3.1 use direct db access
*/
public long getLastUpdate(){
String result = getDBConnection().queryString(
"SELECT LASTUPDATE FROM " + getTableName() + " WHERE ID=" + getWrappedId());
if (result != null) {
return Long.parseLong(result);
} else {
return 0L;
}
}
/**
* Notify the system about a change in this object and refresh the {@link #FLD_LASTUPDATE} value
* of this entry to {@link System#currentTimeMillis()}
*
* @param updatedAttribute
* the attribute that was updated or <code>null</code>
* @since 3.1
*/
public void refreshLastUpdateAndSendUpdateEvent(@Nullable String updatedAttribute){
getDBConnection().exec("UPDATE " + getTableName() + " SET " + FLD_LASTUPDATE + "="
+ Long.toString(System.currentTimeMillis()) + " WHERE ID=" + getWrappedId());
ElexisEventDispatcher.getInstance()
.fire(new ElexisEvent(this, getClass(), ElexisEvent.EVENT_UPDATE, updatedAttribute));
}
/**
* Determine the highest last update value over all database entries of the given table
*
* @return the retrieved value or 0 in any error case
* @param tableName
* the database table name
* @since 3.1
*/
public static long getHighestLastUpdate(String tableName){
DBConnection dbConnection = getDefaultConnection();
PreparedStatement ps =
dbConnection.getPreparedStatement("SELECT MAX(LASTUPDATE) FROM " + tableName);
try {
ResultSet res = ps.executeQuery();
while (res.next()) {
return res.getLong(1);
}
return 0l;
} catch (Exception ex) {
ExHandler.handle(ex);
return 0l;
} finally {
try {
ps.close();
} catch (SQLException e) {
// ignore
}
dbConnection.releasePreparedStatement(ps);
}
}
@Override
public int hashCode(){
return getId().hashCode();
}
public static void clearCache(){
synchronized (defaultConnection.getCache()) {
defaultConnection.getCache().clear();
}
}
public static void resetCache(){
synchronized (defaultConnection.getCache()) {
defaultConnection.getCache().reset();
}
}
/**
* Return time-to-live in cache for this object
*
* @return the time in seconds
*/
public int getCacheTime(){
return getDBConnection().getDefaultLifeTime();
}
/**
* 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;
SqlRunner runner = new SqlRunner(sql, CoreHub.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 SqlRunner(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("Führe 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 ausführen"); 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 ausführen"); log.log(e,
* "Cannot execute db script: " + sqlScript, Log.ERRORS); } }
*/
protected static boolean executeScript(final String pathname){
Stm stm = defaultConnection.getStatement();
try {
FileInputStream is = new FileInputStream(pathname);
return stm.execScript(is, true, true);
} catch (Exception ex) {
ExHandler.handle(ex);
return false;
} finally {
defaultConnection.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();
}
defaultConnection.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){
return flattenObject(hash);
}
/**
*
* @param object
* @return
* @since 3.1
*/
public static byte[] flattenObject(final Object object){
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zos = new ZipOutputStream(baos);
zos.putNextEntry(new ZipEntry("hash"));
ObjectOutputStream oos = new ObjectOutputStream(zos);
oos.writeObject(object);
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){
return (Hashtable<Object, Object>) foldObject(flat);
}
/**
* Unfold a byte array as stored by {@link #flatten(Hashtable)}
*
* @param flat
* @return
* @since 3.1
*/
public static Object foldObject(final byte[] flat){
if (flat.length == 0) {
return null;
}
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(flat))) {
ZipEntry entry = zis.getNextEntry();
if (entry != null) {
try (ObjectInputStream ois = new ObjectInputStream(zis)) {
return ois.readObject();
}
} else {
return null;
}
} catch (Exception ex) {
log.error("Error unfolding object", ex);
return null;
}
}
/**
* Returns array of field names of the database fields.<br>
* Used for export functionality
*/
protected String[] getExportFields(){
DBConnection dbConnection = getDBConnection();
Stm stm = null;
try {
stm = dbConnection.getStatement();
ResultSet res = stm.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;
} finally {
dbConnection.releaseStatement(stm);
}
/*
* 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){
ResultSet res = null;
try {
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)
ElexisEventDispatcher.fireElexisStatusEvent(status);
} else {
status.setLogLevel(ElexisStatus.LOG_FATALS);
throw new PersistenceException(status);
}
}
return res;
}
private static ElexisStatus translateJdbcException(JdbcLinkException jdbc){
if (jdbc instanceof JdbcLinkSyntaxException) {
return new ElexisStatus(ElexisStatus.ERROR, CoreHub.PLUGIN_ID, ElexisStatus.CODE_NONE,
"Fehler in der Datenbanksyntax.", jdbc, ElexisStatus.LOG_ERRORS);
} else if (jdbc instanceof JdbcLinkConcurrencyException) {
return new ElexisStatus(ElexisStatus.ERROR, CoreHub.PLUGIN_ID, ElexisStatus.CODE_NONE,
"Fehler bei einer Datenbanktransaktion.", jdbc, ElexisStatus.LOG_ERRORS);
} else if (jdbc instanceof JdbcLinkResourceException) {
return new ElexisStatus(ElexisStatus.ERROR, CoreHub.PLUGIN_ID, ElexisStatus.CODE_NONE,
"Fehler bei der Datenbankkommunikation.", jdbc, ElexisStatus.LOG_ERRORS);
} else {
return new ElexisStatus(ElexisStatus.ERROR, CoreHub.PLUGIN_ID, ElexisStatus.CODE_NONE,
"Fehler in der Datenbankschnittstelle.", jdbc, ElexisStatus.LOG_ERRORS);
}
}
/**
* Utility procedure for unit tests which need to start with a clean database
*/
public static boolean deleteAllTables(){
int nrTables = 0;
String tableName = "none";
DatabaseMetaData dmd;
Connection conn = null;
try {
conn = defaultConnection.getConnection();
dmd = conn.getMetaData();
// we drop views before dropping the tables
ResultSet rsViews = dmd.getTables(null, null, "%", new String[] {
"VIEW"
});
if (rsViews != null) {
while (rsViews.next()) {
// DatabaseMetaData#getTables() specifies TABLE_NAME is in
// column 3
tableName = rsViews.getString(3);
defaultConnection.exec("DROP VIEW " + tableName);
nrTables++;
}
}
ResultSet rsTables = dmd.getTables(null, null, "%", new String[] {
"TABLE"
});
if (rsTables != null) {
List<String> failedFirstRunTables = new ArrayList<String>();
while (rsTables.next()) {
try {
// DatabaseMetaData#getTables() specifies TABLE_NAME is in
// column 3
tableName = rsTables.getString(3);
defaultConnection.exec("DROP TABLE " + tableName + " CASCADE");
nrTables++;
} catch (JdbcLinkException e) {
log.warn("Failed table [{}] in first run with " + e.getMessage());
failedFirstRunTables.add(tableName);
}
}
for (String table : failedFirstRunTables) {
defaultConnection.exec("DROP TABLE " + table + " CASCADE");
}
}
} catch (JdbcLinkException | SQLException e1) {
log.error("Error deleting table " + tableName);
return false;
} finally {
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
log.error("Error closing connection" + e);
}
}
log.info("Deleted " + nrTables + " tables");
return true;
}
public static boolean tableExistsSelect(String tableName){
try {
defaultConnection.exec("SELECT 1 FROM " + tableName);
return true;
} catch (Exception e) {
return false;
}
}
/**
* Utility procedure
*
* @param tableName
* name of the table to check existence for
*/
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
Connection conn = null;
try {
conn = defaultConnection.getConnection();
DatabaseMetaData dmd = conn.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.error("Fehler beim abrufen der Datenbank Tabellen Information.", je);
} finally {
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
log.error("Error closing connection " + e);
}
}
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.error("Tabelle " + tableName + " " + nrFounds + "-mal gefunden!!");
}
return nrFounds == 1;
}
/**
* Convert an arbitrary value into the database format
*
* @author Marco Descher
* @since 2.1.6
* @since 3.1 supports {@link List}; will be returned as comma-separated-values
* @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) ? StringConstants.ONE : StringConstants.ZERO;
}
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());
}
if (in instanceof List) {
List<?> inList = (List<?>) in;
return (String) inList.stream().map(o -> o.toString())
.reduce((u, t) -> u + StringConstants.COMMA + t).get();
}
return "";
}
public void addChangeListener(IChangeListener listener, String fieldToObserve){
}
public void removeChangeListener(IChangeListener listener, String fieldObserved){
}
/**
* put the value into the cache, will use the cache time as delievered by
* {@link PersistentObject#getCacheTime()}
*
* @param field
* name, must map to a database column, see {@link PersistentObject#map(String)}
* @param value
* the value to cache
* @since 3.1
*/
public void putInCache(String field, Object value){
String key = getKey(field);
if (value == null)
value = "";
getDBConnection().getCache().put(key, value, getCacheTime());
}
/**
*
* @param clazz
* @since 3.1
*/
public static void executeDBInitScriptForClass(Class<?> clazz, @Nullable VersionInfo vi){
String resourceName = "/rsc/dbScripts/" + clazz.getName();
if (vi == null) {
resourceName += ".sql";
} else {
resourceName += "_" + vi.version() + ".sql";
}
Stm stm = defaultConnection.getStatement();
try (InputStream is = PersistentObject.class.getResourceAsStream(resourceName)) {
boolean result = stm.execScript(is, true, true);
if (!result) {
log.warn("Error in executing script from " + resourceName);
}
} catch (IOException e) {
log.error("Error executing script from " + resourceName, e);
} finally {
defaultConnection.releaseStatement(stm);
}
}
}