changeset 1329:46aa15c8cfb2

Merged branch release-2.5 into default.
author Tom Gottfried <tom@intevation.de>
date Mon, 27 Mar 2017 16:31:11 +0200
parents f2e574204e38 (diff) 212fe0cfd694 (current diff)
children b118dc7e5d63
files db_schema/setup-db.sh
diffstat 21 files changed, 1229 insertions(+), 30 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Fri Mar 24 11:59:19 2017 +0100
+++ b/.hgtags	Mon Mar 27 16:31:11 2017 +0200
@@ -40,3 +40,4 @@
 effef57f3f7e1587cae44b5907cf624105ced0db 2.3.0
 fb90e4d8bd0d09fb7bc638e67b4f482ba830e3b7 2.4.0
 204fb75b0c06e2f760dfe37d2fb0845ec31b68f1 2.4.1
+1f47e9fa10c4cd2319f96be262c0265168bb86a0 2.5
--- a/Dockerfile	Fri Mar 24 11:59:19 2017 +0100
+++ b/Dockerfile	Mon Mar 27 16:31:11 2017 +0200
@@ -76,7 +76,7 @@
 #
 # Build and deploy LADA-server
 #
-ENV LADA_VERSION 2.5
+ENV LADA_VERSION 2.6-SNAPSHOT
 RUN mvn clean compile package && \
     mv target/lada-server-$LADA_VERSION.war \
        $JBOSS_HOME/standalone/deployments && \
--- a/db_schema/Dockerfile	Fri Mar 24 11:59:19 2017 +0100
+++ b/db_schema/Dockerfile	Mon Mar 27 16:31:11 2017 +0200
@@ -1,4 +1,4 @@
-# Docker file for postgresql 9.4 on debain
+# Docker file for the LADA database on Debian
 #
 # build with e.g. `docker build --force-rm=true -t koala/lada_db .',
 # then run with e.g.
@@ -28,24 +28,52 @@
 # Install packages
 #
 RUN apt-get update && \
-    apt-get install -y postgresql-9.4-postgis-2.1 postgis curl unzip
+    apt-get install -y curl unzip make gcc
+RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main" \
+    >> /etc/apt/sources.list
+RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends \
+            postgresql-9.5-postgis-2.3 postgresql-9.5-postgis-scripts postgis
+
+#
+# Add context as working directory
+#
+ADD . /opt/lada_sql/
+WORKDIR /opt/lada_sql/
+
+#
+# Set environment variables
+#
+ENV PGCONF /etc/postgresql/9.5/main/postgresql.conf
+ENV PGDATA /var/lib/postgresql/9.5/main
 
 #
 # Use user postgres to run the next commands
 #
 USER postgres
 
-RUN /etc/init.d/postgresql start && \
-    psql --command "CREATE USER admin WITH SUPERUSER PASSWORD 'secret';"
+# XXX: Seems to fail on initdb issued by package installation
+# (due to /usr/sbin/policy-rc.d ?).
+# See also https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=739276
+RUN mkdir /var/run/postgresql/9.5-main.pg_stat_tmp
 
 #
 # Adjust PostgreSQL configuration so that remote connections to the
 # database are possible.
 #
 RUN echo "host all  all    0.0.0.0/0  md5" >> \
-    /etc/postgresql/9.4/main/pg_hba.conf
+    /etc/postgresql/9.5/main/pg_hba.conf
+RUN echo "listen_addresses='*'" >> $PGCONF
 
-RUN echo "listen_addresses='*'" >> /etc/postgresql/9.4/main/postgresql.conf
+#
+# Configure logging collector
+# (because we use postgres directly in CMD,
+# the usual collection from stderr does not work)
+#
+RUN echo "logging_collector = on" >> $PGCONF
+RUN echo "log_directory = '/var/log/postgresql'" >> $PGCONF
+RUN echo "log_filename = 'postgresql-9.5-main.log'" >> $PGCONF
 
 #
 # Expose the PostgreSQL port
@@ -59,15 +87,12 @@
 # 'FATAL: the database system is starting up'.
 # It's because of the -w
 #
-ADD . /opt/lada_sql/
-WORKDIR /opt/lada_sql/
-
-RUN /usr/lib/postgresql/9.4/bin/pg_ctl start -wD /etc/postgresql/9.4/main/ && \
-    /opt/lada_sql/setup-db.sh
+RUN /usr/lib/postgresql/9.5/bin/pg_ctl start -wo "--config_file=$PGCONF" && \
+    /opt/lada_sql/setup-db.sh && \
+    /usr/lib/postgresql/9.5/bin/pg_ctl stop
 
 #
 # Set the default command to run when starting the container
 #
-CMD ["/usr/lib/postgresql/9.4/bin/postgres", "-D", \
-     "/var/lib/postgresql/9.4/main", "-c", \
-     "config_file=/etc/postgresql/9.4/main/postgresql.conf"]
+CMD ["/usr/lib/postgresql/9.5/bin/postgres", \
+     "--config_file=/etc/postgresql/9.5/main/postgresql.conf"]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/db_schema/audit.sql	Mon Mar 27 16:31:11 2017 +0200
@@ -0,0 +1,213 @@
+-- Based on https://github.com/xdimedrolx/audit-trigger/
+--
+-- which is licensed by "The PostgreSQL License", effectively equivalent to the BSD
+-- license.
+
+SET search_path TO land;
+CREATE TABLE audit_trail(
+      id bigserial primary key,
+      table_name varchar(50) not null,
+      tstamp TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+      action varchar(1) NOT NULL CHECK (action IN ('I','D','U', 'T')),
+      object_id integer not null,
+      row_data JSONB,
+      changed_fields JSONB
+);
+
+CREATE OR REPLACE FUNCTION jsonb_delete_left(a jsonb, b jsonb)
+  RETURNS jsonb AS
+  $BODY$
+       SELECT COALESCE(
+              (
+              SELECT ('{' || string_agg(to_json(key) || ':' || value, ',') || '}')
+              FROM jsonb_each(a)
+              WHERE NOT ('{' || to_json(key) || ':' || value || '}')::jsonb <@ b
+              )
+       , '{}')::jsonb;
+       $BODY$
+LANGUAGE sql IMMUTABLE STRICT;
+COMMENT ON FUNCTION jsonb_delete_left(jsonb, jsonb) IS 'delete matching pairs in second argument from first argument';
+DROP OPERATOR IF EXISTS - (jsonb, jsonb);
+CREATE OPERATOR - ( PROCEDURE = jsonb_delete_left, LEFTARG = jsonb, RIGHTARG = jsonb);
+COMMENT ON OPERATOR - (jsonb, jsonb) IS 'delete matching pairs from left operand';
+
+
+CREATE OR REPLACE FUNCTION jsonb_delete_left(a jsonb, b text[])
+  RETURNS jsonb AS
+  $BODY$
+       SELECT COALESCE(
+              (
+              SELECT ('{' || string_agg(to_json(key) || ':' || value, ',') || '}')
+              FROM jsonb_each(a)
+              WHERE key <> ALL(b)
+              )
+       , '{}')::jsonb;
+       $BODY$
+LANGUAGE sql IMMUTABLE STRICT;
+COMMENT ON FUNCTION jsonb_delete_left(jsonb, text[]) IS 'delete keys in second argument from first argument';
+DROP OPERATOR IF EXISTS - (jsonb, text[]);
+CREATE OPERATOR - ( PROCEDURE = jsonb_delete_left, LEFTARG = jsonb, RIGHTARG = text[]);
+COMMENT ON OPERATOR - (jsonb, text[]) IS 'delete keys from left operand';
+
+
+CREATE OR REPLACE FUNCTION if_modified_func() RETURNS TRIGGER AS $body$
+DECLARE
+    audit_row land.audit_trail;
+    include_values boolean;
+    log_diffs boolean;
+    h_old jsonb;
+    h_new jsonb;
+    excluded_cols text[] = ARRAY[]::text[];
+BEGIN
+    IF TG_WHEN <> 'AFTER' THEN
+        RAISE EXCEPTION 'land.if_modified_func() may only run as an AFTER trigger';
+    END IF;
+
+    -- Do nothing on delete.
+    IF (TG_OP = 'DELETE') THEN
+        RETURN NULL;
+    END IF;
+
+    audit_row = ROW(
+        nextval('land.audit_trail_id_seq'), -- id
+        TG_TABLE_NAME::varchar,             -- table_name
+        current_timestamp,                  -- tstamp
+        substring(TG_OP,1,1),               -- action
+        NEW.id,                             -- object_id
+        NULL, NULL                          -- row_data, changed_fields
+        );
+
+    IF TG_ARGV[1] IS NOT NULL THEN
+        excluded_cols = TG_ARGV[1]::text[];
+    END IF;
+
+    IF (TG_OP = 'UPDATE' AND TG_LEVEL = 'ROW') THEN
+        audit_row.row_data = row_to_json(OLD)::JSONB;
+        audit_row.changed_fields = row_to_json(NEW)::JSONB - audit_row.row_data - excluded_cols;
+        IF audit_row.changed_fields = '{}'::jsonb THEN
+            -- All changed fields are ignored. Skip this update.
+            RETURN NULL;
+        END IF;
+    ELSIF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
+        audit_row.row_data = row_to_json(NEW)::JSONB;
+        audit_row.changed_fields = jsonb_strip_nulls(row_to_json(NEW)::JSONB - excluded_cols);
+    ELSE
+        RAISE EXCEPTION '[land.if_modified_func] - Trigger func added as trigger for unhandled case: %, %',TG_OP, TG_LEVEL;
+        RETURN NULL;
+    END IF;
+    INSERT INTO land.audit_trail VALUES (audit_row.*);
+    RETURN NULL;
+END;
+$body$
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = land, public;
+
+CREATE OR REPLACE FUNCTION audit_table(
+    target_table regclass,
+    audit_rows boolean,
+    audit_query_text boolean,
+    ignored_cols text[]
+) RETURNS void AS
+$body$
+DECLARE
+  stm_targets text = 'INSERT OR UPDATE OR DELETE OR TRUNCATE';
+  _q_txt text;
+  _ignored_cols_snip text = '';
+BEGIN
+    EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_row ON ' || quote_ident(target_table::TEXT);
+    EXECUTE 'DROP TRIGGER IF EXISTS audit_trigger_stm ON ' || quote_ident(target_table::TEXT);
+
+    IF audit_rows THEN
+        IF array_length(ignored_cols,1) > 0 THEN
+            _ignored_cols_snip = ', ' || quote_literal(ignored_cols);
+        END IF;
+        _q_txt = 'CREATE TRIGGER audit_trigger_row AFTER INSERT OR UPDATE OR DELETE ON ' ||
+                 quote_ident(target_table::TEXT) ||
+                 ' FOR EACH ROW EXECUTE PROCEDURE land.if_modified_func(' ||
+                 quote_literal(audit_query_text) || _ignored_cols_snip || ');';
+        RAISE NOTICE '%',_q_txt;
+        EXECUTE _q_txt;
+        stm_targets = 'TRUNCATE';
+    ELSE
+    END IF;
+
+    _q_txt = 'CREATE TRIGGER audit_trigger_stm AFTER ' || stm_targets || ' ON ' ||
+             target_table ||
+             ' FOR EACH STATEMENT EXECUTE PROCEDURE land.if_modified_func('||
+             quote_literal(audit_query_text) || ');';
+    RAISE NOTICE '%',_q_txt;
+    EXECUTE _q_txt;
+
+END;
+$body$
+language 'plpgsql';
+
+COMMENT ON FUNCTION audit_table(regclass, boolean, boolean, text[]) IS $body$
+Add auditing support to a table.
+
+Arguments:
+   target_table:     Table name, schema qualified if not on search_path
+   audit_rows:       Record each row change, or only audit at a statement level
+   audit_query_text: Record the text of the client query that triggered the audit event?
+   ignored_cols:     Columns to exclude from update diffs, ignore updates that change only ignored cols.
+$body$;
+
+-- Pg doesn't allow variadic calls with 0 params, so provide a wrapper
+CREATE OR REPLACE FUNCTION audit_table(target_table regclass, audit_rows boolean, audit_query_text boolean) RETURNS void AS $body$
+SELECT audit_table($1, $2, $3, ARRAY[]::text[]);
+$body$ LANGUAGE SQL;
+
+-- And provide a convenience call wrapper for the simplest case
+-- of row-level logging with no excluded cols and query logging enabled.
+--
+CREATE OR REPLACE FUNCTION audit_table(target_table regclass) RETURNS void AS $body$
+SELECT audit_table($1, BOOLEAN 't', BOOLEAN 't');
+$body$ LANGUAGE 'sql';
+
+COMMENT ON FUNCTION audit_table(regclass) IS $body$
+Add auditing support to the given table. Row-level changes will be logged with full client query text. No cols are ignored.
+$body$;
+
+CREATE INDEX probe_id_ndx ON audit_trail(cast("row_data"->>'probe_id' AS int));
+CREATE INDEX messung_id_ndx ON audit_trail(cast("row_data"->>'messung_id' AS int));
+
+-- View for probe audit trail
+CREATE OR REPLACE VIEW audit_trail_probe AS
+SELECT
+    id,
+    table_name,
+    action,
+    object_id,
+    tstamp,
+    cast(row_data ->> 'messungs_id' AS integer) AS messungs_id,
+    coalesce(cast(row_data ->> 'probe_id' AS integer),
+        (SELECT probe_id FROM messung WHERE id = cast(
+            row_data ->> 'messungs_id' AS integer))) AS probe_id,
+    row_data,
+    changed_fields
+FROM audit_trail;
+
+
+-- View for messung audit trail
+CREATE OR REPLACE VIEW audit_trail_messung AS
+SELECT audit_trail.id,
+    audit_trail.table_name,
+    audit_trail.tstamp,
+    audit_trail.action,
+    audit_trail.object_id,
+    audit_trail.row_data,
+    audit_trail.changed_fields,
+    cast(row_data ->> 'messungs_id' AS int) AS messungs_id
+FROM audit_trail;
+
+
+SELECT audit_table('probe', true, false, '{id, tree_modified, letzte_aenderung}'::text[]);
+SELECT audit_table('messung', true, false, '{id, probe_id, tree_modified, letzte_aenderung, status}'::text[]);
+SELECT audit_table('messwert', true, false, '{id, messungs_id, tree_modified, letzte_aenderung}'::text[]);
+SELECT audit_table('kommentar_p', true, false, '{id, probe_id, tree_modified, letzte_aenderung}'::text[]);
+SELECT audit_table('kommentar_m', true, false, '{id, messungs_id, tree_modified, letzte_aenderung}'::text[]);
+SELECT audit_table('zusatz_wert', true, false, '{id, probe_id, tree_modified, letzte_aenderung}'::text[]);
+SELECT audit_table('ortszuordnung', true, false, '{id, probe_id, tree_modified, letzte_aenderung}'::text[]);
+
+SET search_path TO public;
--- a/db_schema/setup-db.sh	Fri Mar 24 11:59:19 2017 +0100
+++ b/db_schema/setup-db.sh	Mon Mar 27 16:31:11 2017 +0200
@@ -32,6 +32,9 @@
 DB_NAME=${3:-$ROLE_NAME}
 echo "DB_NAME = $DB_NAME"
 
+# Stop on error any execution of SQL via psql
+DB_CONNECT_STRING="-v ON_ERROR_STOP=on "
+
 # if variable DB_SRV and otional DB_PORT is set a remote database connection will be used
 if [ -n "$DB_SRV" ] ; then DB_CONNECT_STRING="-h $DB_SRV" ; fi
 if [ -n "$DB_SRV" -a -n "$DB_PORT"  ] ; then
@@ -63,6 +66,10 @@
 
 echo create lada schema
 psql -q $DB_CONNECT_STRING -d $DB_NAME -f $DIR/lada_schema.sql
+
+echo create audit-trail table/trigger/views
+psql -q $DB_CONNECT_STRING -d $DB_NAME -f $DIR/audit.sql
+
 echo set grants
 psql $DB_CONNECT_STRING -d $DB_NAME --command \
      "GRANT USAGE ON SCHEMA stammdaten, land TO $ROLE_NAME;
--- a/pom.xml	Fri Mar 24 11:59:19 2017 +0100
+++ b/pom.xml	Mon Mar 27 16:31:11 2017 +0200
@@ -369,5 +369,5 @@
             <url>http://www.hibernatespatial.org/repository</url>
         </repository>
     </repositories>
-    <version>2.5</version>
+    <version>2.6-SNAPSHOT</version>
 </project>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/intevation/lada/model/land/AuditTrail.java	Mon Mar 27 16:31:11 2017 +0200
@@ -0,0 +1,109 @@
+package de.intevation.lada.model.land;
+
+import java.io.Serializable;
+import java.sql.Timestamp;
+
+import javax.json.JsonObject;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+import org.hibernate.annotations.Type;
+import org.hibernate.annotations.TypeDef;
+import org.hibernate.annotations.TypeDefs;
+
+import de.intevation.lada.util.data.JsonObjectType;
+
+/**
+ * The persistent class for the audit_trail database table.
+ * 
+ */
+@Entity
+@Table(name="audit_trail")
+@TypeDefs({ @TypeDef(name = "JsonObject", typeClass = JsonObjectType.class) })
+public class AuditTrail implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @Id
+    private Long id;
+
+    private String action;
+
+    @Column(name="action_tstamp_clk")
+    private Timestamp actionTstampClk;
+
+    @Column(name="changed_fields")
+    @Type(type="JsonObject")
+    private JsonObject changedFields;
+
+    @Column(name="object_id")
+    private Integer objectId;
+
+    @Column(name="row_data")
+    @Type(type="JsonObject")
+    private JsonObject rowData;
+
+    @Column(name="table_name")
+    private String tableName;
+
+    public AuditTrail() {
+    }
+
+    public Long getId() {
+        return this.id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getAction() {
+        return this.action;
+    }
+
+    public void setAction(String action) {
+        this.action = action;
+    }
+
+    public Timestamp getActionTstampClk() {
+        return this.actionTstampClk;
+    }
+
+    public void setActionTstampClk(Timestamp actionTstampClk) {
+        this.actionTstampClk = actionTstampClk;
+    }
+
+    public JsonObject getChangedFields() {
+        return this.changedFields;
+    }
+
+    public void setChangedFields(JsonObject changedFields) {
+        this.changedFields = changedFields;
+    }
+
+    public Integer getObjectId() {
+        return this.objectId;
+    }
+
+    public void setObjectId(Integer objectId) {
+        this.objectId = objectId;
+    }
+
+    public JsonObject getRowData() {
+        return this.rowData;
+    }
+
+    public void setRowData(JsonObject rowData) {
+        this.rowData = rowData;
+    }
+
+    public String getTableName() {
+        return this.tableName;
+    }
+
+    public void setTableName(String tableName) {
+        this.tableName = tableName;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/intevation/lada/model/land/AuditTrailMessung.java	Mon Mar 27 16:31:11 2017 +0200
@@ -0,0 +1,122 @@
+package de.intevation.lada.model.land;
+
+import java.io.Serializable;
+import java.sql.Timestamp;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+import org.hibernate.annotations.Type;
+import org.hibernate.annotations.TypeDef;
+import org.hibernate.annotations.TypeDefs;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.intevation.lada.util.data.JsonObjectType;
+
+
+/**
+ * The persistent class for the audit_trail_messung database table.
+ * 
+ */
+@Entity
+@Table(name="audit_trail_messung")
+@TypeDefs({ @TypeDef(name = "JsonObject", typeClass = JsonObjectType.class) })
+public class AuditTrailMessung implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @Id
+    private Long id;
+
+    private String action;
+
+    @Column(name="tstamp")
+    private Timestamp tstamp;
+
+    @Column(name="changed_fields")
+    @Type(type="JsonObject")
+    private JsonNode changedFields;
+
+    @Column(name="messungs_id")
+    private String messungsId;
+
+    @Column(name="object_id")
+    private Integer objectId;
+
+    @Column(name="row_data")
+    @Type(type="JsonObject")
+    private JsonNode rowData;
+
+    @Column(name="table_name")
+    private String tableName;
+
+    public AuditTrailMessung() {
+    }
+
+    public String getAction() {
+        return this.action;
+    }
+
+    public void setAction(String action) {
+        this.action = action;
+    }
+
+    public Timestamp getTstamp() {
+        return this.tstamp;
+    }
+
+    public void setTstamp(Timestamp tstamp) {
+        this.tstamp = tstamp;
+    }
+
+    public JsonNode getChangedFields() {
+        return this.changedFields;
+    }
+
+    public void setChangedFields(JsonNode changedFields) {
+        this.changedFields = changedFields;
+    }
+
+    public Long getId() {
+        return this.id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getMessungsId() {
+        return this.messungsId;
+    }
+
+    public void setMessungsId(String messungsId) {
+        this.messungsId = messungsId;
+    }
+
+    public Integer getObjectId() {
+        return this.objectId;
+    }
+
+    public void setObjectId(Integer objectId) {
+        this.objectId = objectId;
+    }
+
+    public JsonNode getRowData() {
+        return this.rowData;
+    }
+
+    public void setRowData(JsonNode rowData) {
+        this.rowData = rowData;
+    }
+
+    public String getTableName() {
+        return this.tableName;
+    }
+
+    public void setTableName(String tableName) {
+        this.tableName = tableName;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/intevation/lada/model/land/AuditTrailProbe.java	Mon Mar 27 16:31:11 2017 +0200
@@ -0,0 +1,126 @@
+package de.intevation.lada.model.land;
+
+import java.io.Serializable;
+import java.sql.Timestamp;
+
+import javax.json.JsonObject;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+import org.hibernate.annotations.Type;
+import org.hibernate.annotations.TypeDef;
+import org.hibernate.annotations.TypeDefs;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.intevation.lada.util.data.JsonObjectType;
+
+/**
+ * The persistent class for the audit_trail_probe database table.
+ * 
+ */
+@Entity
+@Table(name="audit_trail_probe")
+@TypeDefs({ @TypeDef(name = "JsonObject", typeClass = JsonObjectType.class) })
+public class AuditTrailProbe implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @Id
+    private Long id;
+
+    @Column(name="changed_fields")
+    @Type(type="JsonObject")
+    private JsonNode changedFields;
+
+    @Column(name="row_data")
+    @Type(type="JsonObject")
+    private JsonNode rowData;
+
+    @Column(name="tstamp")
+    private Timestamp tstamp;
+
+    @Column(name="action")
+    private String action;
+
+    @Column(name="messungs_id")
+    private Integer messungsId;
+
+    @Column(name="object_id")
+    private Integer objectId;
+
+    @Column(name="probe_id")
+    private Integer probeId;
+
+    @Column(name="table_name")
+    private String tableName;
+
+    public AuditTrailProbe() {
+    }
+
+    public JsonNode getChangedFields() {
+        return this.changedFields;
+    }
+
+    public void setChangedFields(JsonNode changedFields) {
+        this.changedFields = changedFields;
+    }
+
+    public JsonNode getRowData() {
+        return rowData;
+    }
+
+    public void setRowData(JsonNode rowData) {
+        this.rowData = rowData;
+    }
+
+    public Timestamp getTstamp() {
+        return tstamp;
+    }
+
+    public void setTstamp(Timestamp tstamp) {
+        this.tstamp = tstamp;
+    }
+
+    public String getAction() {
+        return action;
+    }
+
+    public void setAction(String action) {
+        this.action = action;
+    }
+
+    public Integer getMessungsId() {
+        return this.messungsId;
+    }
+
+    public void setMessungsId(Integer messungsId) {
+        this.messungsId = messungsId;
+    }
+
+    public Integer getObjectId() {
+        return this.objectId;
+    }
+
+    public void setObjectId(Integer objectId) {
+        this.objectId = objectId;
+    }
+
+    public Integer getProbeId() {
+        return this.probeId;
+    }
+
+    public void setProbeId(Integer probeId) {
+        this.probeId = probeId;
+    }
+
+    public String getTableName() {
+        return this.tableName;
+    }
+
+    public void setTableName(String tableName) {
+        this.tableName = tableName;
+    }
+
+}
--- a/src/main/java/de/intevation/lada/model/land/Messprogramm.java	Fri Mar 24 11:59:19 2017 +0100
+++ b/src/main/java/de/intevation/lada/model/land/Messprogramm.java	Mon Mar 27 16:31:11 2017 +0200
@@ -64,7 +64,7 @@
     @Column(name="labor_mst_id")
     private String laborMstId;
 
-    @Column(name="letzte_aenderung")
+    @Column(name="letzte_aenderung", insertable=false)
     private Timestamp letzteAenderung;
 
     @Column(name="media_desk")
--- a/src/main/java/de/intevation/lada/model/land/MessprogrammMmt.java	Fri Mar 24 11:59:19 2017 +0100
+++ b/src/main/java/de/intevation/lada/model/land/MessprogrammMmt.java	Mon Mar 27 16:31:11 2017 +0200
@@ -29,7 +29,7 @@
     @GeneratedValue(strategy=GenerationType.IDENTITY)
     private Integer id;
 
-    @Column(name="letzte_aenderung")
+    @Column(name="letzte_aenderung", insertable=false)
     private Timestamp letzteAenderung;
 
     @Type(type="IntegerArray")
--- a/src/main/java/de/intevation/lada/model/land/Messung.java	Fri Mar 24 11:59:19 2017 +0100
+++ b/src/main/java/de/intevation/lada/model/land/Messung.java	Mon Mar 27 16:31:11 2017 +0200
@@ -38,7 +38,7 @@
     @Column(name="id_alt")
     private Integer idAlt;
 
-    @Column(name="letzte_aenderung")
+    @Column(name="letzte_aenderung", insertable=false)
     private Timestamp letzteAenderung;
 
     private Integer messdauer;
--- a/src/main/java/de/intevation/lada/model/land/Messwert.java	Fri Mar 24 11:59:19 2017 +0100
+++ b/src/main/java/de/intevation/lada/model/land/Messwert.java	Mon Mar 27 16:31:11 2017 +0200
@@ -27,7 +27,7 @@
 
     private Boolean grenzwertueberschreitung;
 
-    @Column(name="letzte_aenderung")
+    @Column(name="letzte_aenderung", insertable=false)
     private Timestamp letzteAenderung;
 
     @Column(name="meh_id")
--- a/src/main/java/de/intevation/lada/model/land/Ortszuordnung.java	Fri Mar 24 11:59:19 2017 +0100
+++ b/src/main/java/de/intevation/lada/model/land/Ortszuordnung.java	Mon Mar 27 16:31:11 2017 +0200
@@ -25,7 +25,7 @@
     @GeneratedValue(strategy=GenerationType.IDENTITY)
     private Integer id;
 
-    @Column(name="letzte_aenderung")
+    @Column(name="letzte_aenderung", insertable=false)
     private Timestamp letzteAenderung;
 
     @Column(name="ort_id")
--- a/src/main/java/de/intevation/lada/model/land/OrtszuordnungMp.java	Fri Mar 24 11:59:19 2017 +0100
+++ b/src/main/java/de/intevation/lada/model/land/OrtszuordnungMp.java	Mon Mar 27 16:31:11 2017 +0200
@@ -32,7 +32,7 @@
     @GeneratedValue(strategy=GenerationType.IDENTITY)
     private Integer id;
 
-    @Column(name="letzte_aenderung")
+    @Column(name="letzte_aenderung", insertable=false)
     private Timestamp letzteAenderung;
 
     @Column(name="messprogramm_id")
--- a/src/main/java/de/intevation/lada/model/land/Probe.java	Fri Mar 24 11:59:19 2017 +0100
+++ b/src/main/java/de/intevation/lada/model/land/Probe.java	Mon Mar 27 16:31:11 2017 +0200
@@ -44,7 +44,7 @@
     @Column(name="labor_mst_id")
     private String laborMstId;
 
-    @Column(name="letzte_aenderung")
+    @Column(name="letzte_aenderung", insertable=false)
     private Timestamp letzteAenderung;
 
     private String media;
--- a/src/main/java/de/intevation/lada/model/land/ZusatzWert.java	Fri Mar 24 11:59:19 2017 +0100
+++ b/src/main/java/de/intevation/lada/model/land/ZusatzWert.java	Mon Mar 27 16:31:11 2017 +0200
@@ -27,7 +27,7 @@
     @GeneratedValue(strategy=GenerationType.IDENTITY)
     private Integer id;
 
-    @Column(name="letzte_aenderung")
+    @Column(name="letzte_aenderung", insertable=false)
     private Timestamp letzteAenderung;
 
     private Float messfehler;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/intevation/lada/rest/AuditTrailService.java	Mon Mar 27 16:31:11 2017 +0200
@@ -0,0 +1,391 @@
+/* Copyright (C) 2013 by Bundesamt fuer Strahlenschutz
+ * Software engineering by Intevation GmbH
+ *
+ * This file is Free Software under the GNU GPL (v>=3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out
+ * the documentation coming with IMIS-Labordaten-Application for details.
+ */
+package de.intevation.lada.rest;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.persistence.EntityManager;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.log4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.intevation.lada.model.land.AuditTrailMessung;
+import de.intevation.lada.model.land.AuditTrailProbe;
+import de.intevation.lada.model.land.Messung;
+import de.intevation.lada.model.land.Probe;
+import de.intevation.lada.util.annotation.AuthorizationConfig;
+import de.intevation.lada.util.annotation.RepositoryConfig;
+import de.intevation.lada.util.auth.Authorization;
+import de.intevation.lada.util.auth.AuthorizationType;
+import de.intevation.lada.util.data.QueryBuilder;
+import de.intevation.lada.util.data.Repository;
+import de.intevation.lada.util.data.RepositoryType;
+
+/**
+ * REST service for AuditTrail.
+ * <p>
+ * The services produce data in the application/json media type.
+ * All HTTP methods use the authorization module to determine if the user is
+ * allowed to perform the requested action.
+ * A typical response holds information about the action performed and the data.
+ * <pre>
+ * <code>
+ * {
+ *  "success": [boolean];
+ *  "message": [string],
+ *  "data":[{
+ *      "id": [number],
+ *      "identifier: [string]
+ *      "audit": [array]
+ *  }],
+ * }
+ * </code>
+ * </pre>
+ *
+ * @author <a href="mailto:rrenkert@intevation.de">Raimund Renkert</a>
+ */
+@Path("rest/audit")
+@RequestScoped
+public class AuditTrailService {
+
+    /**
+     * Class to store tablename and value field for foreign key mappings.
+     */
+    private class TableMapper {
+        public String mappingTable;
+        public String valueField;
+
+        public TableMapper(
+            String mappingTable,
+            String valueField
+        ) {
+            this.mappingTable = mappingTable;
+            this.valueField = valueField;
+        }
+    }
+
+    @Inject Logger logger;
+    /**
+     * The data repository granting read/write access.
+     */
+    @Inject
+    @RepositoryConfig(type=RepositoryType.RO)
+    private Repository repository;
+
+    /**
+     * The authorization module.
+     */
+    @Inject
+    @AuthorizationConfig(type=AuthorizationType.HEADER)
+    private Authorization authorization;
+
+    /**
+     * Map foreign key to their associated table and the display value.
+     */
+    private Map<String, TableMapper> mappings;
+
+    /**
+     * Initialize the object with key <-> table mappings.
+     */
+    @PostConstruct
+    public void initialize() {
+        mappings = new HashMap<String, TableMapper>();
+        mappings.put("messgroesse_id",
+            new TableMapper("messgroesse", "messgroesse"));
+        mappings.put("meh_id",
+            new TableMapper("mess_einheit", "einheit"));
+        mappings.put("ort_id",
+            new TableMapper("ort", "ort_id"));
+        mappings.put("datenbasis_id",
+            new TableMapper("datenbasis", "datenbasis"));
+        mappings.put("ba_id",
+            new TableMapper("betriebsart", "name"));
+        mappings.put("mpl_id",
+            new TableMapper("messprogramm_kategorie", "code"));
+        mappings.put("probenart_id",
+            new TableMapper("probenart", "probenart"));
+        mappings.put("probe_nehmer_id",
+            new TableMapper("probenehmer", "prn_id"));
+        mappings.put("probeentnahme_beginn",
+            new TableMapper("date", "dd.MM.yy HH:mm"));
+        mappings.put("probeentnahme_ende",
+            new TableMapper("date", "dd.MM.yy HH:mm"));
+    }
+
+    /**
+     * Service to generate audit trail for probe objects.
+     */
+    @GET
+    @Path("/probe/{id}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public String getProbe(
+        @Context HttpServletRequest request,
+        @PathParam("id") String id
+    ) {
+        if (id == null || "".equals(id)) {
+            String ret = "{\"success\": false," +
+                "\"message\":698,\"data\":null}";
+            return ret;
+        }
+
+        Integer pId = null;
+        String ret = "{\"success\": false," +
+            "\"message\":600,\"data\":null}";
+        try {
+            pId = Integer.valueOf(id);
+        }
+        catch(NumberFormatException nfe) {
+            return ret;
+        }
+        // Get the plain probe object to have the hauptproben_nr.
+        Probe probe = repository.getByIdPlain(Probe.class, pId, "land");
+        if (probe == null) {
+            return ret;
+        }
+
+        // Get all entries for the probe and its sub objects.
+        QueryBuilder<AuditTrailProbe> builder =
+            new QueryBuilder<AuditTrailProbe>(
+                repository.entityManager("land"),
+                AuditTrailProbe.class);
+        builder.and("objectId", id);
+        builder.and("tableName", "probe");
+        builder.or("probeId", id);
+        builder.orderBy("tstamp", true);
+        List<AuditTrailProbe> audit =
+            repository.filterPlain(builder.getQuery(), "land");
+
+        // Create an empty JsonObject
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode responseNode = mapper.createObjectNode();
+        responseNode.put("success", true);
+        responseNode.put("message", 200);
+        ObjectNode auditJson = responseNode.putObject("data");
+        ArrayNode entries = auditJson.putArray("audit");
+        auditJson.put("id", probe.getId());
+        auditJson.put("identifier", probe.getHauptprobenNr());
+        for (AuditTrailProbe a : audit) {
+            entries.add(createEntry(a, mapper));
+        }
+        return responseNode.toString();
+    }
+
+    /**
+     * Create a JSON object for an AuditTrailProbe entry.
+     *
+     * @param audit The table entry
+     * @param mapper JSON object mapper
+     */
+    private ObjectNode createEntry(AuditTrailProbe audit, ObjectMapper mapper) {
+        ObjectNode node = mapper.createObjectNode();
+        node.put("timestamp", audit.getTstamp().getTime());
+        node.put("type", audit.getTableName());
+        node.put("action", audit.getAction());
+        ObjectNode data = (ObjectNode)audit.getChangedFields();
+        data = translateValues(data);
+        node.putPOJO("changedFields", data);
+        if ("kommentar_p".equals(audit.getTableName())) {
+            node.put("identifier", audit.getRowData().get("datum").toString());
+        }
+        if ("zusatz_wert".equals(audit.getTableName())) {
+            node.put("identifier", audit.getRowData().get("pzs_id").toString());
+        }
+        if ("ortszuordnung".equals(audit.getTableName())) {
+            String value = translateId(
+                "ort",
+                "ort_id",
+                audit.getRowData().get("ort_id").toString(),
+                "id",
+                "stamm");
+            node.put("identifier", value);
+        }
+        if ("messung".equals(audit.getTableName())) {
+            logger.debug("npr: " + audit.getRowData());
+            node.put("identifier", 
+                audit.getRowData()
+                    .get("nebenproben_nr").toString().replaceAll("\"", ""));
+        }
+        if (audit.getMessungsId() != null) {
+            Messung m = repository.getByIdPlain(
+                Messung.class, audit.getMessungsId(), "land");
+            ObjectNode identifier = node.putObject("identifier");
+            identifier.put("messung", m.getNebenprobenNr());
+            if ("kommentar_m".equals(audit.getTableName())) {
+                identifier.put("identifier",
+                    audit.getRowData().get("datum").toString());
+            }
+            if ("messwert".equals(audit.getTableName())) {
+                String value = translateId(
+                    "messgroesse",
+                    "messgroesse",
+                    audit.getRowData().get("messgroesse_id").toString(),
+                    "id",
+                    "stamm");
+                identifier.put("identifier", value);
+            }
+        }
+        return node;
+    }
+
+    /**
+     * Service to generate audit trail for messung objects.
+     */
+    @GET
+    @Path("/messung/{id}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public String getMessung(
+        @Context HttpServletRequest request,
+        @PathParam("id") String id
+    ) {
+        if (id == null || "".equals(id)) {
+            String ret = "{\"success\": false," +
+                "\"message\":698,\"data\":null}";
+            return ret;
+        }
+
+        Integer mId = null;
+        String ret = "{\"success\": false," +
+            "\"message\":600,\"data\":null}";
+        try {
+            mId = Integer.valueOf(id);
+        }
+        catch(NumberFormatException nfe) {
+            return ret;
+        }
+        Messung messung = repository.getByIdPlain(Messung.class, mId, "land");
+        if (messung == null) {
+            return ret;
+        }
+
+        QueryBuilder<AuditTrailMessung> builder =
+            new QueryBuilder<AuditTrailMessung>(
+                repository.entityManager("land"),
+                AuditTrailMessung.class);
+        builder.and("objectId", mId);
+        builder.and("tableName", "messung");
+        builder.or("messungsId", mId);
+        builder.orderBy("tstamp", true);
+        List<AuditTrailMessung> audit =
+            repository.filterPlain(builder.getQuery(), "land");
+
+        // Create an empty JsonObject
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode responseNode = mapper.createObjectNode();
+        responseNode.put("success", true);
+        responseNode.put("message", 200);
+        ObjectNode auditJson = responseNode.putObject("data");
+        ArrayNode entries = auditJson.putArray("audit");
+        auditJson.put("id", messung.getId());
+        auditJson.put("identifier", messung.getNebenprobenNr());
+        for (AuditTrailMessung a : audit) {
+            entries.add(createEntry(a, mapper));
+        }
+        return responseNode.toString();
+    }
+
+    /**
+     * Create a JSON object for an AuditTrailMessung entry.
+     *
+     * @param audit The table entry
+     * @param mapper JSON object mapper
+     */
+    private ObjectNode createEntry(AuditTrailMessung audit, ObjectMapper mapper) {
+        ObjectNode node = mapper.createObjectNode();
+        node.put("timestamp", audit.getTstamp().getTime());
+        node.put("type", audit.getTableName());
+        node.put("action", audit.getAction());
+        ObjectNode data = (ObjectNode)audit.getChangedFields();
+        node.putPOJO("changedFields", data);
+        if ("kommentar_m".equals(audit.getTableName())) {
+            node.put("identifier", audit.getRowData().get("datum").toString());
+        }
+        if ("messwert".equals(audit.getTableName())) {
+            String value = translateId(
+                "messgroesse",
+                "messgroesse",
+                audit.getRowData().get("messgroesse_id").toString(),
+                "id",
+                "stamm");
+            node.put("identifier", value);
+        }
+        return node;
+    }
+
+    /**
+     * Translate a foreign key into the associated value.
+     */
+    private String translateId(
+        String table,
+        String field,
+        String id,
+        String idField,
+        String source
+    ) {
+        EntityManager manager = repository.entityManager(source);
+        String sql = "SELECT " + field + " FROM " + table +
+            " WHERE " + idField + " = " + id + ";";
+        javax.persistence.Query query = manager.createNativeQuery(sql);
+        List<String> result = query.getResultList();
+        return result.get(0);
+    }
+
+    private Long formatDate(String format, String date) {
+        DateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssXXX");
+        try {
+            return inFormat.parse(date).getTime();
+        } catch (ParseException e) {
+            return 0L;
+        }
+    }
+
+    /**
+     * Translate all known foreign keys
+     */
+    private ObjectNode translateValues(ObjectNode node) {
+        for (Iterator<String> i = node.fieldNames(); i.hasNext();) {
+            String key = i.next();
+            if (mappings.containsKey(key)) {
+                TableMapper m = mappings.get(key);
+                if (m.mappingTable.equals("date")) {
+                    Long value = formatDate(m.valueField, node.get(key).asText());
+                    node.put(key, value);
+                }
+                else {
+                    String value = translateId(
+                        m.mappingTable,
+                        m.valueField,
+                        node.get(key).asText(),
+                        "id",
+                        "stamm");
+                    node.put(key, value);
+                }
+            }
+        }
+        return node;
+    }
+}
--- a/src/main/java/de/intevation/lada/util/auth/HeaderAuthorization.java	Fri Mar 24 11:59:19 2017 +0100
+++ b/src/main/java/de/intevation/lada/util/auth/HeaderAuthorization.java	Mon Mar 27 16:31:11 2017 +0200
@@ -137,9 +137,8 @@
             return data;
         }
         Authorizer authorizer = authorizers.get(clazz);
-        //This is a hack... Allows wildcard for unknown classes.
         if (authorizer == null) {
-            return data;
+            return new Response(false, 699, null);
         }
         return authorizer.filter(data, userInfo, clazz);
     }
@@ -242,9 +241,8 @@
         Class<T> clazz
     ) {
         Authorizer authorizer = authorizers.get(clazz);
-        //This is a hack... Allows wildcard for unknown classes.
         if (authorizer == null) {
-            return true;
+            return false;
         }
         return authorizer.isAuthorized(data, RequestMethod.GET, userInfo, clazz);
     }
@@ -263,9 +261,8 @@
         Class<T> clazz
     ) {
         Authorizer authorizer = authorizers.get(clazz);
-        //This is a hack... Allows wildcard for unknown classes.
         if (authorizer == null) {
-            return true;
+            return false;
         }
         return authorizer.isAuthorized(data, RequestMethod.POST, userInfo, clazz);
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/intevation/lada/util/data/JsonObjectType.java	Mon Mar 27 16:31:11 2017 +0200
@@ -0,0 +1,207 @@
+/* Copyright (C) 2013 by Bundesamt fuer Strahlenschutz
+ * Software engineering by Intevation GmbH
+ *
+ * This file is Free Software under the GNU GPL (v>=3)
+ * and comes with ABSOLUTELY NO WARRANTY! Check out
+ * the documentation coming with IMIS-Labordaten-Application for details.
+ */
+package de.intevation.lada.util.data;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+
+import org.hibernate.HibernateException;
+import org.hibernate.engine.spi.SessionImplementor;
+import org.hibernate.usertype.UserType;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Implementation for a new data type in the postgresql/postgis jdbc driver.
+ *
+ * @author <a href="mailto:rrenkert@intevation.de">Raimund Renkert</a>
+ */
+public class JsonObjectType implements UserType {
+    /**
+     * Reconstruct an object from the cacheable representation. At the very
+     * least this method should perform a deep copy if the type is mutable.
+     * (optional <span id="IL_AD5" class="IL_AD">operation</span>)
+     *
+     * @param cached
+     *            the object to be cached
+     * @param owner
+     *            the owner of the cached object
+     * @return a reconstructed object from the cachable representation
+     * @throws HibernateException
+     */
+    @Override
+    public Object assemble(Serializable cached, Object owner) throws HibernateException {
+        return this.deepCopy(cached);
+    }
+
+    /**
+     * Return a deep copy of the persistent state, stopping at entities and st
+     * collections. It is not necessary to copy immutable objects, or null
+     * values, in which case it is safe to simple return the argument.
+     *
+     * @param value
+     *            the object to be cloned, which may be null
+     *
+     * @return object a copy
+     * @throws HibernateException
+     */
+    @Override
+    public Object deepCopy(Object value) throws HibernateException {
+        return value;
+    }
+
+    /**
+     * Transform the object into its cacheable representation. At the very least
+     * this method should perform a deep copy if the type is mutable. That may
+     * not be enough for some implementations, however; for example,
+     * <span id="IL_AD11" class="IL_AD">associations</span> must be cached as <span id="IL_AD9" class="IL_AD">identifier</span> values. (optional operation)
+     *
+     * @param value
+     *            the object to be cached
+     * @return a cachable representation of the object
+     * @throws HibernateException
+     */
+    @Override
+    public Serializable disassemble(Object value) throws HibernateException {
+        return (String) this.deepCopy(value);
+    }
+
+    /**
+     * Compare two instances of the class mapped by this type for persistence
+     * "equality". Equality of the persistence state.
+     *
+     * @param x
+     * @param y
+     * @return <span id="IL_AD12" class="IL_AD">boolean</span>
+     * @throws HibernateException
+     */
+    @Override
+    public boolean equals(Object x, Object y) throws HibernateException {
+        if (x == null) {
+            return y == null;
+        }
+        return x.equals(y);
+    }
+
+    /**
+     * Get a hashcode for the instance, consistent with persistence "equality".
+     */
+    @Override
+    public int hashCode(Object x) throws HibernateException {
+        return x.hashCode();
+    }
+
+    /**
+     * Are objects of this type mutable?
+     *
+     * @return boolean
+     */
+    @Override
+    public boolean isMutable() {
+        return true;
+    }
+
+    /**
+     * Retrieve an instance of the mapped class from a JDBC resultset.
+     * Implementors should handle possibility of null values.
+     *
+     * @param rs
+     *            a JDBC result set
+     * @param names
+     *            the column names
+     * @param session
+     * @param owner
+     *            the containing entity
+     * @return
+     * @throws HibernateException
+     * @throws SQLException
+     */
+    @Override
+    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode node = null;
+        try {
+            node = mapper.readTree(rs.getString(names[0]));
+            return node;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return mapper.createObjectNode();
+    }
+
+    /**
+     * Write an instance of the mapped class to a prepared statement.
+     * Implementors should handle possibility of null values. A multi-column
+     * type should be written to parameters starting from <tt>index</tt>
+     *
+     * @param st
+     *            a JDBC prepared statement
+     * @param value
+     *            the object to write
+     * @param index
+     *            statement parameter index
+     * @param session
+     * @throws HibernateException
+     * @throws SQLException
+     */
+    @Override
+    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
+        if (value == null) {
+            st.setNull(index, Types.OTHER);
+            return;
+        }
+        st.setObject(index, value, Types.OTHER);
+    }
+
+    /**
+     * During merge, <span id="IL_AD7" class="IL_AD">replace</span> the existing (target) values in the entity we are
+     * merging to with a new (original) value from the detched entity we are
+     * merging. For immutable objects, or null values, it is safe to return a
+     * copy of the first parameter. For the objects with component values, it
+     * might make sense to recursively replace component values
+     *
+     * @param original
+     *            the value from the detched entity being merged
+     * @param target
+     *            the value in the managed entity
+     * @param owner
+     * @return the value to be merged
+     * @throws HibernateException
+     */
+    @Override
+    public Object replace(Object original, Object target, Object owner) throws HibernateException {
+        return original;
+    }
+
+    /**
+     * The class returned by <tt>nullSafeGet()</tt>
+     *
+     * @return Class
+     */
+    @Override
+    public Class returnedClass() {
+        return String.class;
+    }
+
+    /**
+     * Returns the SQL type <span id="IL_AD3" class="IL_AD">codes</span> for the columns mapped by this type. The codes
+     * are defined on <tt>java.sql.Types</tt>
+     *
+     * @return int[] the typecodes
+     * @see java.sql.Types
+     */
+    @Override
+    public int[] sqlTypes() {
+        return new int[] { Types.JAVA_OBJECT };
+    }
+}
--- a/src/main/java/de/intevation/lada/util/data/LadaPostgisDialect.java	Fri Mar 24 11:59:19 2017 +0100
+++ b/src/main/java/de/intevation/lada/util/data/LadaPostgisDialect.java	Mon Mar 27 16:31:11 2017 +0200
@@ -23,5 +23,6 @@
     public LadaPostgisDialect() {
         super();
         this.registerColumnType(Types.ARRAY, "integer[]");
+        this.registerColumnType(Types.JAVA_OBJECT, "json");
     }
 }
This site is hosted by Intevation GmbH (Datenschutzerklärung und Impressum | Privacy Policy and Imprint)