de.fraunhofer.iosb.ilt.sta.persistence.postgres.EntityInserter.java Source code

Java tutorial

Introduction

Here is the source code for de.fraunhofer.iosb.ilt.sta.persistence.postgres.EntityInserter.java

Source

/*
 * Copyright (C) 2016 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131
 * Karlsruhe, Germany.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.fraunhofer.iosb.ilt.sta.persistence.postgres;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.querydsl.core.Tuple;
import com.querydsl.core.dml.StoreClause;
import com.querydsl.core.types.dsl.DateTimePath;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.StringPath;
import com.querydsl.spatial.GeometryPath;
import com.querydsl.sql.SQLQuery;
import com.querydsl.sql.SQLQueryFactory;
import com.querydsl.sql.dml.SQLInsertClause;
import com.querydsl.sql.dml.SQLUpdateClause;
import de.fraunhofer.iosb.ilt.sta.model.Datastream;
import de.fraunhofer.iosb.ilt.sta.model.FeatureOfInterest;
import de.fraunhofer.iosb.ilt.sta.model.HistoricalLocation;
import de.fraunhofer.iosb.ilt.sta.model.Location;
import de.fraunhofer.iosb.ilt.sta.model.MultiDatastream;
import de.fraunhofer.iosb.ilt.sta.model.Observation;
import de.fraunhofer.iosb.ilt.sta.model.ObservedProperty;
import de.fraunhofer.iosb.ilt.sta.model.Sensor;
import de.fraunhofer.iosb.ilt.sta.model.Thing;
import de.fraunhofer.iosb.ilt.sta.model.builder.DatastreamBuilder;
import de.fraunhofer.iosb.ilt.sta.model.builder.FeatureOfInterestBuilder;
import de.fraunhofer.iosb.ilt.sta.model.builder.MultiDatastreamBuilder;
import de.fraunhofer.iosb.ilt.sta.model.builder.SensorBuilder;
import de.fraunhofer.iosb.ilt.sta.model.builder.ThingBuilder;
import de.fraunhofer.iosb.ilt.sta.model.core.Entity;
import de.fraunhofer.iosb.ilt.sta.model.core.EntitySet;
import de.fraunhofer.iosb.ilt.sta.model.custom.geojson.GeoJsonSerializer;
import de.fraunhofer.iosb.ilt.sta.model.ext.TimeInstant;
import de.fraunhofer.iosb.ilt.sta.model.ext.TimeInterval;
import de.fraunhofer.iosb.ilt.sta.model.ext.TimeValue;
import de.fraunhofer.iosb.ilt.sta.model.ext.UnitOfMeasurement;
import de.fraunhofer.iosb.ilt.sta.model.id.Id;
import de.fraunhofer.iosb.ilt.sta.model.id.LongId;
import de.fraunhofer.iosb.ilt.sta.path.EntitySetPathElement;
import de.fraunhofer.iosb.ilt.sta.path.EntityType;
import de.fraunhofer.iosb.ilt.sta.path.ResourcePath;
import de.fraunhofer.iosb.ilt.sta.persistence.QDatastreams;
import de.fraunhofer.iosb.ilt.sta.persistence.QFeatures;
import de.fraunhofer.iosb.ilt.sta.persistence.QHistLocations;
import de.fraunhofer.iosb.ilt.sta.persistence.QLocations;
import de.fraunhofer.iosb.ilt.sta.persistence.QLocationsHistLocations;
import de.fraunhofer.iosb.ilt.sta.persistence.QMultiDatastreams;
import de.fraunhofer.iosb.ilt.sta.persistence.QMultiDatastreamsObsProperties;
import de.fraunhofer.iosb.ilt.sta.persistence.QObsProperties;
import de.fraunhofer.iosb.ilt.sta.persistence.QObservations;
import de.fraunhofer.iosb.ilt.sta.persistence.QSensors;
import de.fraunhofer.iosb.ilt.sta.persistence.QThings;
import de.fraunhofer.iosb.ilt.sta.persistence.QThingsLocations;
import de.fraunhofer.iosb.ilt.sta.util.IncompleteEntityException;
import de.fraunhofer.iosb.ilt.sta.util.NoSuchEntityException;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import org.geojson.Feature;
import org.geolatte.common.dataformats.json.jackson.JsonException;
import org.geolatte.common.dataformats.json.jackson.JsonMapper;
import org.geolatte.geom.Geometry;
import org.joda.time.Interval;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Hylke van der Schaaf
 */
public class EntityInserter {

    /**
     * The logger for this class.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(EntityInserter.class);
    private final PostgresPersistenceManager pm;
    private ObjectMapper formatter;

    public EntityInserter(PostgresPersistenceManager pm) {
        this.pm = pm;
    }

    public boolean insertDatastream(Datastream ds) throws NoSuchEntityException, IncompleteEntityException {
        // First check ObservedPropery, Sensor and Thing
        ObservedProperty op = ds.getObservedProperty();
        entityExistsOrCreate(op);

        Sensor s = ds.getSensor();
        entityExistsOrCreate(s);

        Thing t = ds.getThing();
        entityExistsOrCreate(t);

        SQLQueryFactory qFactory = pm.createQueryFactory();

        QDatastreams qd = QDatastreams.datastreams;
        SQLInsertClause insert = qFactory.insert(qd);
        insert.set(qd.name, ds.getName());
        insert.set(qd.description, ds.getDescription());
        insert.set(qd.observationType, ds.getObservationType());
        // TODO: ObservedArea should be set using a trigger+stored procedure.
        insert.set(qd.unitDefinition, ds.getUnitOfMeasurement().getDefinition());
        insert.set(qd.unitName, ds.getUnitOfMeasurement().getName());
        insert.set(qd.unitSymbol, ds.getUnitOfMeasurement().getSymbol());

        insert.set(qd.phenomenonTimeStart, new Timestamp(PostgresPersistenceManager.DATETIME_MAX.getMillis()));
        insert.set(qd.phenomenonTimeEnd, new Timestamp(PostgresPersistenceManager.DATETIME_MIN.getMillis()));
        insert.set(qd.resultTimeStart, new Timestamp(PostgresPersistenceManager.DATETIME_MAX.getMillis()));
        insert.set(qd.resultTimeEnd, new Timestamp(PostgresPersistenceManager.DATETIME_MIN.getMillis()));

        insert.set(qd.obsPropertyId, (Long) op.getId().getValue());
        insert.set(qd.sensorId, (Long) s.getId().getValue());
        insert.set(qd.thingId, (Long) t.getId().getValue());

        Long datastreamId = insert.executeWithKey(qd.id);
        LOGGER.info("Inserted datastream. Created id = {}.", datastreamId);
        ds.setId(new LongId(datastreamId));

        // Create Observations, if any.
        for (Observation o : ds.getObservations()) {
            o.setDatastream(new DatastreamBuilder().setId(ds.getId()).build());
            o.complete();
            pm.insert(o);
        }

        return true;
    }

    public boolean updateDatastream(Datastream d, long dsId) throws NoSuchEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QDatastreams qd = QDatastreams.datastreams;
        SQLUpdateClause update = qFactory.update(qd);
        if (d.isSetName()) {
            if (d.getName() == null) {
                throw new IncompleteEntityException("name can not be null.");
            }
            update.set(qd.name, d.getName());
        }
        if (d.isSetDescription()) {
            if (d.getDescription() == null) {
                throw new IncompleteEntityException("description can not be null.");
            }
            update.set(qd.description, d.getDescription());
        }
        if (d.isSetObservationType()) {
            if (d.getObservationType() == null) {
                throw new IncompleteEntityException("observationType can not be null.");
            }
            update.set(qd.observationType, d.getObservationType());
        }
        if (d.isSetObservedProperty()) {
            if (!entityExists(d.getObservedProperty())) {
                throw new NoSuchEntityException("ObservedProperty with no id or not found.");
            }
            update.set(qd.obsPropertyId, (Long) d.getObservedProperty().getId().getValue());
        }
        if (d.isSetSensor()) {
            if (!entityExists(d.getSensor())) {
                throw new NoSuchEntityException("Sensor with no id or not found.");
            }
            update.set(qd.sensorId, (Long) d.getSensor().getId().getValue());
        }
        if (d.isSetThing()) {
            if (!entityExists(d.getThing())) {
                throw new NoSuchEntityException("Thing with no id or not found.");
            }
            update.set(qd.thingId, (Long) d.getThing().getId().getValue());
        }
        if (d.isSetUnitOfMeasurement()) {
            if (d.getUnitOfMeasurement() == null) {
                throw new IncompleteEntityException("unitOfMeasurement can not be null.");
            }
            UnitOfMeasurement uom = d.getUnitOfMeasurement();
            update.set(qd.unitDefinition, uom.getDefinition());
            update.set(qd.unitName, uom.getName());
            update.set(qd.unitSymbol, uom.getSymbol());
        }

        update.where(qd.id.eq(dsId));
        long count = 0;
        if (!update.isEmpty()) {
            count = update.execute();
        }
        if (count > 1) {
            LOGGER.error("Updating Datastream {} caused {} rows to change!", dsId, count);
            throw new IllegalStateException("Update changed multiple rows.");
        }

        // Link existing Observations to the Datastream.
        for (Observation o : d.getObservations()) {
            if (o.getId() == null || !entityExists(o)) {
                throw new NoSuchEntityException("Observation with no id or non existing.");
            }
            Long obsId = (Long) o.getId().getValue();
            QObservations qo = QObservations.observations;
            long oCount = qFactory.update(qo).set(qo.datastreamId, dsId).where(qo.id.eq(obsId)).execute();
            if (oCount > 0) {
                LOGGER.info("Assigned datastream {} to Observation {}.", dsId, obsId);
            }
        }

        LOGGER.debug("Updated Datastream {}", dsId);
        return true;
    }

    public boolean insertMultiDatastream(MultiDatastream ds)
            throws NoSuchEntityException, IncompleteEntityException {
        // First check Sensor and Thing
        Sensor s = ds.getSensor();
        entityExistsOrCreate(s);

        Thing t = ds.getThing();
        entityExistsOrCreate(t);

        SQLQueryFactory qFactory = pm.createQueryFactory();

        QMultiDatastreams qd = QMultiDatastreams.multiDatastreams;
        SQLInsertClause insert = qFactory.insert(qd);
        insert.set(qd.name, ds.getName());
        insert.set(qd.description, ds.getDescription());
        insert.set(qd.observationTypes, objectToJson(ds.getMultiObservationDataTypes()));
        insert.set(qd.unitOfMeasurements, objectToJson(ds.getUnitOfMeasurements()));

        insert.set(qd.phenomenonTimeStart, new Timestamp(PostgresPersistenceManager.DATETIME_MAX.getMillis()));
        insert.set(qd.phenomenonTimeEnd, new Timestamp(PostgresPersistenceManager.DATETIME_MIN.getMillis()));
        insert.set(qd.resultTimeStart, new Timestamp(PostgresPersistenceManager.DATETIME_MAX.getMillis()));
        insert.set(qd.resultTimeEnd, new Timestamp(PostgresPersistenceManager.DATETIME_MIN.getMillis()));

        insert.set(qd.sensorId, (Long) s.getId().getValue());
        insert.set(qd.thingId, (Long) t.getId().getValue());

        Long multiDatastreamId = insert.executeWithKey(qd.id);
        LOGGER.info("Inserted multiDatastream. Created id = {}.", multiDatastreamId);
        ds.setId(new LongId(multiDatastreamId));

        // Create new Locations, if any.
        EntitySet<ObservedProperty> ops = ds.getObservedProperties();
        int rank = 0;
        for (ObservedProperty op : ops) {
            entityExistsOrCreate(op);
            Long opId = (Long) op.getId().getValue();

            QMultiDatastreamsObsProperties qMdOp = QMultiDatastreamsObsProperties.multiDatastreamsObsProperties;
            insert = qFactory.insert(qMdOp);
            insert.set(qMdOp.multiDatastreamId, multiDatastreamId);
            insert.set(qMdOp.obsPropertyId, opId);
            insert.set(qMdOp.rank, rank);
            insert.execute();
            LOGGER.debug("Linked MultiDatastream {} to ObservedProperty {} with rank {}.", multiDatastreamId, opId,
                    rank);
            rank++;
        }

        // Create Observations, if any.
        for (Observation o : ds.getObservations()) {
            o.setMultiDatastream(new MultiDatastreamBuilder().setId(ds.getId()).build());
            o.complete();
            pm.insert(o);
        }

        return true;
    }

    public boolean updateMultiDatastream(MultiDatastream d, long dsId) throws NoSuchEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QMultiDatastreams qd = QMultiDatastreams.multiDatastreams;
        SQLUpdateClause update = qFactory.update(qd);
        if (d.isSetName()) {
            if (d.getName() == null) {
                throw new IncompleteEntityException("name can not be null.");
            }
            update.set(qd.name, d.getName());
        }
        if (d.isSetDescription()) {
            if (d.getDescription() == null) {
                throw new IncompleteEntityException("description can not be null.");
            }
            update.set(qd.description, d.getDescription());
        }

        if (d.isSetSensor()) {
            if (!entityExists(d.getSensor())) {
                throw new NoSuchEntityException("Sensor with no id or not found.");
            }
            update.set(qd.sensorId, (Long) d.getSensor().getId().getValue());
        }
        if (d.isSetThing()) {
            if (!entityExists(d.getThing())) {
                throw new NoSuchEntityException("Thing with no id or not found.");
            }
            update.set(qd.thingId, (Long) d.getThing().getId().getValue());
        }

        MultiDatastream original = (MultiDatastream) pm.getEntityById(null, EntityType.MultiDatastream,
                new LongId(dsId));
        int countOrig = original.getMultiObservationDataTypes().size();

        int countUom = countOrig;
        if (d.isSetUnitOfMeasurements()) {
            if (d.getUnitOfMeasurements() == null) {
                throw new IncompleteEntityException("unitOfMeasurements can not be null.");
            }
            List<UnitOfMeasurement> uoms = d.getUnitOfMeasurements();
            countUom = uoms.size();
            update.set(qd.unitOfMeasurements, objectToJson(uoms));
        }
        int countDataTypes = countOrig;
        if (d.isSetMultiObservationDataTypes()) {
            List<String> dataTypes = d.getMultiObservationDataTypes();
            if (dataTypes == null) {
                throw new IncompleteEntityException("multiObservationDataTypes can not be null.");
            }
            countDataTypes = dataTypes.size();
            update.set(qd.observationTypes, objectToJson(dataTypes));
        }
        EntitySet<ObservedProperty> ops = d.getObservedProperties();
        int countOps = countOrig + ops.size();
        for (ObservedProperty op : ops) {
            if (op.getId() == null || !entityExists(op)) {
                throw new NoSuchEntityException("ObservedProperty with no id or not found.");
            }
        }

        if (countUom != countDataTypes) {
            throw new IllegalArgumentException(
                    "New number of unitOfMeasurements does not match new number of multiObservationDataTypes.");
        }
        if (countUom != countOps) {
            throw new IllegalArgumentException(
                    "New number of unitOfMeasurements does not match new number of ObservedProperties.");
        }

        update.where(qd.id.eq(dsId));
        long count = 0;
        if (!update.isEmpty()) {
            count = update.execute();
        }
        if (count > 1) {
            LOGGER.error("Updating Datastream {} caused {} rows to change!", dsId, count);
            throw new IllegalStateException("Update changed multiple rows.");
        }

        // Link existing ObservedProperties to the MultiDatastream.
        int rank = countOrig;
        for (ObservedProperty op : ops) {
            Long opId = (Long) op.getId().getValue();
            QMultiDatastreamsObsProperties qMdOp = QMultiDatastreamsObsProperties.multiDatastreamsObsProperties;
            long oCount = qFactory.insert(qMdOp).set(qMdOp.multiDatastreamId, dsId).set(qMdOp.obsPropertyId, opId)
                    .set(qMdOp.rank, rank).execute();
            if (oCount > 0) {
                LOGGER.info("Assigned datastream {} to ObservedProperty {} with rank {}.", dsId, opId, rank);
            }
            rank++;
        }

        // Link existing Observations to the MultiDatastream.
        for (Observation o : d.getObservations()) {
            if (o.getId() == null || !entityExists(o)) {
                throw new NoSuchEntityException("Observation with no id or non existing.");
            }
            Long obsId = (Long) o.getId().getValue();
            QObservations qo = QObservations.observations;
            long oCount = qFactory.update(qo).set(qo.datastreamId, dsId).where(qo.id.eq(obsId)).execute();
            if (oCount > 0) {
                LOGGER.info("Assigned datastream {} to Observation {}.", dsId, obsId);
            }
        }

        LOGGER.debug("Updated Datastream {}", dsId);
        return true;
    }

    public boolean insertFeatureOfInterest(FeatureOfInterest foi) throws NoSuchEntityException {
        // No linked entities to check first.
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QFeatures qfoi = QFeatures.features;
        SQLInsertClause insert = qFactory.insert(qfoi);
        insert.set(qfoi.name, foi.getName());
        insert.set(qfoi.description, foi.getDescription());
        String encodingType = foi.getEncodingType();
        insert.set(qfoi.encodingType, encodingType);
        insertGeometry(insert, qfoi.feature, qfoi.geom, encodingType, foi.getFeature());

        Long generatedId = insert.executeWithKey(qfoi.id);
        LOGGER.info("Inserted FeatureOfInterest. Created id = {}.", generatedId);
        foi.setId(new LongId(generatedId));
        return true;
    }

    public boolean updateFeatureOfInterest(FeatureOfInterest foi, long foiId) throws NoSuchEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QFeatures qfoi = QFeatures.features;
        SQLUpdateClause update = qFactory.update(qfoi);
        if (foi.isSetName()) {
            if (foi.getName() == null) {
                throw new IncompleteEntityException("name can not be null.");
            }
            update.set(qfoi.name, foi.getName());
        }
        if (foi.isSetDescription()) {
            if (foi.getDescription() == null) {
                throw new IncompleteEntityException("description can not be null.");
            }
            update.set(qfoi.description, foi.getDescription());
        }

        if (foi.isSetEncodingType() && foi.getEncodingType() == null) {
            throw new IncompleteEntityException("encodingType can not be null.");
        }
        if (foi.isSetFeature() && foi.getFeature() == null) {
            throw new IncompleteEntityException("feature can not be null.");
        }
        if (foi.isSetEncodingType() && foi.getEncodingType() != null && foi.isSetFeature()
                && foi.getFeature() != null) {
            String encodingType = foi.getEncodingType();
            update.set(qfoi.encodingType, encodingType);
            insertGeometry(update, qfoi.feature, qfoi.geom, encodingType, foi.getFeature());
        }
        if (foi.isSetEncodingType() && foi.getEncodingType() != null && foi.isSetFeature()
                && foi.getFeature() != null) {
            String encodingType = foi.getEncodingType();
            update.set(qfoi.encodingType, encodingType);
            insertGeometry(update, qfoi.feature, qfoi.geom, encodingType, foi.getFeature());
        } else if (foi.isSetEncodingType() && foi.getEncodingType() != null) {
            String encodingType = foi.getEncodingType();
            update.set(qfoi.encodingType, encodingType);
        } else if (foi.isSetFeature() && foi.getFeature() != null) {
            String encodingType = qFactory.select(qfoi.encodingType).from(qfoi).where(qfoi.id.eq(foiId))
                    .fetchFirst();
            Object parsedObject = reParseGeometry(encodingType, foi.getFeature());
            insertGeometry(update, qfoi.feature, qfoi.geom, encodingType, parsedObject);
        }

        update.where(qfoi.id.eq(foiId));
        long count = 0;
        if (!update.isEmpty()) {
            count = update.execute();
        }
        if (count > 1) {
            LOGGER.error("Updating FeatureOfInterest {} caused {} rows to change!", foiId, count);
            throw new IllegalStateException("Update changed multiple rows.");
        }

        // Link existing Observations to the FeatureOfInterest.
        for (Observation o : foi.getObservations()) {
            if (o.getId() == null || !entityExists(o)) {
                throw new NoSuchEntityException("Observation with no id or non existing.");
            }
            Long obsId = (Long) o.getId().getValue();
            QObservations qo = QObservations.observations;
            long oCount = qFactory.update(qo).set(qo.featureId, foiId).where(qo.id.eq(obsId)).execute();
            if (oCount > 0) {
                LOGGER.info("Assigned FeatureOfInterest {} to Observation {}.", foiId, obsId);
            }
        }

        LOGGER.debug("Updated FeatureOfInterest {}", foiId);
        return true;
    }

    public FeatureOfInterest generateFeatureOfInterest(Id datastreamId, boolean isMultiDatastream)
            throws NoSuchEntityException {
        Long dsId = (Long) datastreamId.getValue();
        SQLQueryFactory qf = pm.createQueryFactory();
        QLocations ql = QLocations.locations;
        QThingsLocations qtl = QThingsLocations.thingsLocations;
        QThings qt = QThings.things;
        QDatastreams qd = QDatastreams.datastreams;
        QMultiDatastreams qmd = QMultiDatastreams.multiDatastreams;
        // TODO: Should probably contain a where that only returns locations
        // with a supported encoding type.
        SQLQuery<Tuple> query = qf.select(ql.id, ql.genFoiId).from(ql).innerJoin(qtl).on(ql.id.eq(qtl.locationId))
                .innerJoin(qt).on(qt.id.eq(qtl.thingId));
        if (isMultiDatastream) {
            query.innerJoin(qmd).on(qmd.thingId.eq(qt.id)).where(qmd.id.eq(dsId));
        } else {
            query.innerJoin(qd).on(qd.thingId.eq(qt.id)).where(qd.id.eq(dsId));
        }
        Tuple tuple = query.fetchOne();
        if (tuple == null) {
            // Can not generate foi from Thing with no locations.
            throw new NoSuchEntityException("Can not generate foi for Thing with no locations.");
        }
        Long genFoiId = tuple.get(ql.genFoiId);
        Long locationId = tuple.get(ql.id);

        FeatureOfInterest foi;
        if (genFoiId == null) {
            query = qf.select(ql.id, ql.encodingType, ql.location).from(ql).where(ql.id.eq(locationId));
            tuple = query.fetchOne();
            if (tuple == null) {
                // Can not generate foi from Thing with no locations.
                // Should not happen, since the query succeeded just before.
                throw new NoSuchEntityException("Can not generate foi for Thing with no locations.");
            }
            String encoding = tuple.get(ql.encodingType);
            String locString = tuple.get(ql.location);
            Object locObject = PropertyHelper.locationFromEncoding(encoding, locString);
            foi = new FeatureOfInterestBuilder().setName("FoI for location " + locationId)
                    .setDescription("Generated from location " + locationId).setEncodingType(encoding)
                    .setFeature(locObject).build();
            insertFeatureOfInterest(foi);
            Long foiId = (Long) foi.getId().getValue();
            qf.update(ql).set(ql.genFoiId, (Long) foi.getId().getValue()).where(ql.id.eq(locationId)).execute();
            LOGGER.debug("Generated foi {} from Location {}.", foiId, locationId);
        } else {
            foi = new FeatureOfInterest();
            foi.setId(new LongId(genFoiId));
        }
        return foi;
    }

    public boolean insertHistoricalLocation(HistoricalLocation h)
            throws NoSuchEntityException, IncompleteEntityException {
        Thing t = h.getThing();
        entityExistsOrCreate(t);

        SQLQueryFactory qFactory = pm.createQueryFactory();
        QHistLocations qhl = QHistLocations.histLocations;
        SQLInsertClause insert = qFactory.insert(qhl);
        insert.set(qhl.time, new Timestamp(h.getTime().getDateTime().getMillis()));
        insert.set(qhl.thingId, (Long) h.getThing().getId().getValue());

        Long generatedId = insert.executeWithKey(qhl.id);
        LOGGER.info("Inserted HistoricalLocation. Created id = {}.", generatedId);
        h.setId(new LongId(generatedId));

        EntitySet<Location> locations = h.getLocations();
        for (Location l : locations) {
            entityExistsOrCreate(l);
            Long lId = (Long) l.getId().getValue();
            QLocationsHistLocations qlhl = QLocationsHistLocations.locationsHistLocations;
            insert = qFactory.insert(qlhl);
            insert.set(qlhl.histLocationId, generatedId);
            insert.set(qlhl.locationId, lId);
            insert.execute();
            LOGGER.debug("Linked Location {} to HistoricalLocation {}.", lId, generatedId);
        }
        return true;
    }

    public boolean updateHistoricalLocation(HistoricalLocation hl, long id) {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QHistLocations qhl = QHistLocations.histLocations;
        SQLUpdateClause update = qFactory.update(qhl);
        if (hl.isSetThing()) {
            if (!entityExists(hl.getThing())) {
                throw new IncompleteEntityException("Thing can not be null.");
            }
            update.set(qhl.thingId, (Long) hl.getThing().getId().getValue());
        }
        if (hl.isSetTime()) {
            if (hl.getTime() == null) {
                throw new IncompleteEntityException("time can not be null.");
            }
            insertTimeInstant(update, qhl.time, hl.getTime());
        }
        update.where(qhl.id.eq(id));
        long count = 0;
        if (!update.isEmpty()) {
            count = update.execute();
        }
        if (count > 1) {
            LOGGER.error("Updating Location {} caused {} rows to change!", id, count);
            throw new IllegalStateException("Update changed multiple rows.");
        }
        LOGGER.debug("Updated Location {}", id);

        // Link existing locations to the HistoricalLocation.
        for (Location l : hl.getLocations()) {
            if (!entityExists(l)) {
                throw new IllegalArgumentException("Unknown Location or Location with no id.");
            }
            Long lId = (Long) l.getId().getValue();

            QLocationsHistLocations qlhl = QLocationsHistLocations.locationsHistLocations;
            SQLInsertClause insert = qFactory.insert(qlhl);
            insert.set(qlhl.histLocationId, id);
            insert.set(qlhl.locationId, lId);
            insert.execute();
            LOGGER.debug("Linked Location {} to HistoricalLocation {}.", lId, id);
        }
        return true;
    }

    public boolean insertLocation(Location l) throws NoSuchEntityException, IncompleteEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QLocations ql = QLocations.locations;
        SQLInsertClause insert = qFactory.insert(ql);
        insert.set(ql.name, l.getName());
        insert.set(ql.description, l.getDescription());
        String encodingType = l.getEncodingType();
        insert.set(ql.encodingType, encodingType);
        insertGeometry(insert, ql.location, ql.geom, encodingType, l.getLocation());

        Long locationId = insert.executeWithKey(ql.id);
        LOGGER.info("Inserted Location. Created id = {}.", locationId);
        l.setId(new LongId(locationId));

        // Link Things
        EntitySet<Thing> things = l.getThings();
        for (Thing t : things) {
            entityExistsOrCreate(t);
            Long thingId = (Long) t.getId().getValue();

            // Unlink old Locations from Thing.
            QThingsLocations qtl = QThingsLocations.thingsLocations;
            long count = qFactory.delete(qtl).where(qtl.thingId.eq(thingId)).execute();
            LOGGER.info("Unlinked {} locations from Thing {}.", count, thingId);

            // Link new Location to thing.
            insert = qFactory.insert(qtl);
            insert.set(qtl.thingId, thingId);
            insert.set(qtl.locationId, locationId);
            insert.execute();
            LOGGER.debug("Linked Location {} to Thing {}.", locationId, thingId);

            // Create HistoricalLocation for Thing
            QHistLocations qhl = QHistLocations.histLocations;
            insert = qFactory.insert(qhl);
            insert.set(qhl.thingId, thingId);
            insert.set(qhl.time, new Timestamp(Calendar.getInstance().getTimeInMillis()));
            Long histLocationId = insert.executeWithKey(qhl.id);
            LOGGER.debug("Created historicalLocation {}", histLocationId);

            // Link Location to HistoricalLocation.
            QLocationsHistLocations qlhl = QLocationsHistLocations.locationsHistLocations;
            qFactory.insert(qlhl).set(qlhl.histLocationId, histLocationId).set(qlhl.locationId, locationId)
                    .execute();
            LOGGER.info("Linked location {} to historicalLocation {}.", locationId, histLocationId);
        }

        return true;
    }

    public boolean updateLocation(Location l, long locationId) throws NoSuchEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QLocations ql = QLocations.locations;
        SQLUpdateClause update = qFactory.update(ql);
        if (l.isSetName()) {
            if (l.getName() == null) {
                throw new IncompleteEntityException("name can not be null.");
            }
            update.set(ql.name, l.getName());
        }
        if (l.isSetDescription()) {
            if (l.getDescription() == null) {
                throw new IncompleteEntityException("description can not be null.");
            }
            update.set(ql.description, l.getDescription());
        }

        if (l.isSetEncodingType() && l.getEncodingType() == null) {
            throw new IncompleteEntityException("encodingType can not be null.");
        }
        if (l.isSetLocation() && l.getLocation() == null) {
            throw new IncompleteEntityException("locations can not be null.");
        }
        if (l.isSetEncodingType() && l.getEncodingType() != null && l.isSetLocation() && l.getLocation() != null) {
            String encodingType = l.getEncodingType();
            update.set(ql.encodingType, encodingType);
            insertGeometry(update, ql.location, ql.geom, encodingType, l.getLocation());
        } else if (l.isSetEncodingType() && l.getEncodingType() != null) {
            String encodingType = l.getEncodingType();
            update.set(ql.encodingType, encodingType);
        } else if (l.isSetLocation() && l.getLocation() != null) {
            String encodingType = qFactory.select(ql.encodingType).from(ql).where(ql.id.eq(locationId))
                    .fetchFirst();
            Object parsedObject = reParseGeometry(encodingType, l.getLocation());
            insertGeometry(update, ql.location, ql.geom, encodingType, parsedObject);
        }

        update.where(ql.id.eq(locationId));
        long count = 0;
        if (!update.isEmpty()) {
            count = update.execute();
        }
        if (count > 1) {
            LOGGER.error("Updating Location {} caused {} rows to change!", locationId, count);
            throw new IllegalStateException("Update changed multiple rows.");
        }
        LOGGER.debug("Updated Location {}", locationId);

        // Link HistoricalLocation.
        for (HistoricalLocation hl : l.getHistoricalLocations()) {
            if (hl.getId() == null) {
                throw new IllegalArgumentException("HistoricalLocation with no id.");
            }
            Long hlId = (Long) hl.getId().getValue();

            QLocationsHistLocations qlhl = QLocationsHistLocations.locationsHistLocations;
            SQLInsertClause insert = qFactory.insert(qlhl);
            insert.set(qlhl.histLocationId, hlId);
            insert.set(qlhl.locationId, locationId);
            insert.execute();
            LOGGER.debug("Linked Location {} to HistoricalLocation {}.", locationId, hlId);
        }

        // Link Things
        EntitySet<Thing> things = l.getThings();
        for (Thing t : things) {
            if (!entityExists(t)) {
                throw new NoSuchEntityException("Thing not found.");
            }
            Long thingId = (Long) t.getId().getValue();

            // Unlink old Locations from Thing.
            QThingsLocations qtl = QThingsLocations.thingsLocations;
            count = qFactory.delete(qtl).where(qtl.thingId.eq(thingId)).execute();
            LOGGER.info("Unlinked {} locations from Thing {}.", count, thingId);

            // Link new Location to thing.
            SQLInsertClause insert = qFactory.insert(qtl);
            insert.set(qtl.thingId, thingId);
            insert.set(qtl.locationId, locationId);
            insert.execute();
            LOGGER.debug("Linked Location {} to Thing {}.", locationId, thingId);

            // Create HistoricalLocation for Thing
            QHistLocations qhl = QHistLocations.histLocations;
            insert = qFactory.insert(qhl);
            insert.set(qhl.thingId, thingId);
            insert.set(qhl.time, new Timestamp(Calendar.getInstance().getTimeInMillis()));
            Long histLocationId = insert.executeWithKey(qhl.id);
            LOGGER.debug("Created historicalLocation {}", histLocationId);

            // Link Location to HistoricalLocation.
            QLocationsHistLocations qlhl = QLocationsHistLocations.locationsHistLocations;
            qFactory.insert(qlhl).set(qlhl.histLocationId, histLocationId).set(qlhl.locationId, locationId)
                    .execute();
            LOGGER.info("Linked location {} to historicalLocation {}.", locationId, histLocationId);
        }
        return true;
    }

    public boolean insertObservation(Observation o) throws NoSuchEntityException, IncompleteEntityException {
        Datastream ds = o.getDatastream();
        MultiDatastream mds = o.getMultiDatastream();
        Id streamId;
        boolean isMultiDatastream = false;
        if (ds != null) {
            entityExistsOrCreate(ds);
            streamId = ds.getId();
        } else if (mds != null) {
            entityExistsOrCreate(mds);
            streamId = mds.getId();
            isMultiDatastream = true;
        } else {
            throw new IncompleteEntityException("Missing Datastream or MultiDatastream.");
        }

        FeatureOfInterest f = o.getFeatureOfInterest();
        if (f == null) {
            f = generateFeatureOfInterest(streamId, isMultiDatastream);
        } else {
            entityExistsOrCreate(f);
        }

        SQLQueryFactory qFactory = pm.createQueryFactory();
        QObservations qo = QObservations.observations;
        SQLInsertClause insert = qFactory.insert(qo);

        insert.set(qo.parameters, objectToJson(o.getParameters()));
        TimeValue phenomenonTime = o.getPhenomenonTime();
        if (phenomenonTime == null) {
            phenomenonTime = TimeInstant.now();
        }
        insertTimeValue(insert, qo.phenomenonTimeStart, qo.phenomenonTimeEnd, phenomenonTime);
        insertTimeInstant(insert, qo.resultTime, o.getResultTime());
        insertTimeInterval(insert, qo.validTimeStart, qo.validTimeEnd, o.getValidTime());

        Object result = o.getResult();
        if (isMultiDatastream) {
            if (!(result instanceof List)) {
                throw new IllegalArgumentException("Multidatastream only accepts array results.");
            }
            List list = (List) result;
            ResourcePath path = mds.getPath();
            path.addPathElement(new EntitySetPathElement(EntityType.ObservedProperty, null), false, false);
            long count = pm.count(path, null);
            if (count != list.size()) {
                throw new IllegalArgumentException("Size of result array (" + list.size()
                        + ") must match number of observed properties (" + count + ") in the MultiDatastream.");
            }
        }

        if (result instanceof Number) {
            insert.set(qo.resultType, ResultType.NUMBER.sqlValue());
            insert.set(qo.resultString, result.toString());
            insert.set(qo.resultNumber, ((Number) result).doubleValue());
        } else if (result instanceof Boolean) {
            insert.set(qo.resultType, ResultType.BOOLEAN.sqlValue());
            insert.set(qo.resultString, result.toString());
            insert.set(qo.resultBoolean, (Boolean) result);
        } else if (result instanceof String) {
            insert.set(qo.resultType, ResultType.STRING.sqlValue());
            insert.set(qo.resultString, result.toString());
        } else {
            insert.set(qo.resultType, ResultType.OBJECT_ARRAY.sqlValue());
            insert.set(qo.resultJson, objectToJson(result));
        }

        if (o.getResultQuality() != null) {
            insert.set(qo.resultQuality, o.getResultQuality().toString());
        }
        if (ds != null) {
            insert.set(qo.datastreamId, (Long) ds.getId().getValue());
        }
        if (mds != null) {
            insert.set(qo.multiDatastreamId, (Long) mds.getId().getValue());
        }
        insert.set(qo.featureId, (Long) f.getId().getValue());

        Long generatedId = insert.executeWithKey(qo.id);
        LOGGER.debug("Inserted Observation. Created id = {}.", generatedId);
        o.setId(new LongId(generatedId));
        return true;
    }

    public boolean updateObservation(Observation o, long id) {
        Observation oldObservation = (Observation) pm.getEntityById(null, EntityType.Observation, new LongId(id));
        boolean newHasDatastream = oldObservation.getDatastream() != null;
        boolean newHasMultiDatastream = oldObservation.getMultiDatastream() != null;

        SQLQueryFactory qFactory = pm.createQueryFactory();
        QObservations qo = QObservations.observations;
        SQLUpdateClause update = qFactory.update(qo);
        if (o.isSetDatastream()) {
            if (o.getDatastream() == null) {
                newHasDatastream = false;
                update.setNull(qo.datastreamId);
            } else {
                if (!entityExists(o.getDatastream())) {
                    throw new IncompleteEntityException("Datastream not found.");
                }
                newHasDatastream = true;
                update.set(qo.datastreamId, (Long) o.getDatastream().getId().getValue());
            }
        }
        if (o.isSetMultiDatastream()) {
            if (o.getMultiDatastream() == null) {
                newHasMultiDatastream = false;
                update.setNull(qo.multiDatastreamId);
            } else {
                if (!entityExists(o.getMultiDatastream())) {
                    throw new IncompleteEntityException("MultiDatastream not found.");
                }
                newHasMultiDatastream = true;
                update.set(qo.multiDatastreamId, (Long) o.getMultiDatastream().getId().getValue());
            }
        }
        if (newHasDatastream == newHasMultiDatastream) {
            throw new IllegalArgumentException("Observation must have either a Datastream or a MultiDatastream.");
        }
        if (o.isSetFeatureOfInterest()) {
            if (!entityExists(o.getFeatureOfInterest())) {
                throw new IncompleteEntityException("FeatureOfInterest not found.");
            }
            update.set(qo.featureId, (Long) o.getFeatureOfInterest().getId().getValue());
        }
        if (o.isSetParameters()) {
            update.set(qo.parameters, objectToJson(o.getParameters()));
        }
        if (o.isSetPhenomenonTime()) {
            if (o.getPhenomenonTime() == null) {
                throw new IncompleteEntityException("phenomenonTime can not be null.");
            }
            insertTimeValue(update, qo.phenomenonTimeStart, qo.phenomenonTimeEnd, o.getPhenomenonTime());
        }

        if (o.isSetResult() && o.getResult() != null) {
            Object result = o.getResult();
            if (result instanceof Number) {
                update.set(qo.resultType, ResultType.NUMBER.sqlValue());
                update.set(qo.resultString, result.toString());
                update.set(qo.resultNumber, ((Number) result).doubleValue());
            } else {
                update.set(qo.resultType, ResultType.STRING.sqlValue());
                update.set(qo.resultString, result.toString());
                update.setNull(qo.resultNumber);
                update.setNull(qo.resultBoolean);
                update.setNull(qo.resultJson);
            }
        }

        if (o.isSetResultQuality()) {
            update.set(qo.resultQuality, objectToJson(o.getResultQuality()));
        }
        if (o.isSetResultTime()) {
            insertTimeInstant(update, qo.resultTime, o.getResultTime());
        }
        if (o.isSetValidTime()) {
            insertTimeInterval(update, qo.validTimeStart, qo.validTimeEnd, o.getValidTime());
        }
        update.where(qo.id.eq(id));
        long count = 0;
        if (!update.isEmpty()) {
            count = update.execute();
        }
        if (count > 1) {
            LOGGER.error("Updating Observation {} caused {} rows to change!", id, count);
            throw new IllegalStateException("Update changed multiple rows.");
        }
        LOGGER.debug("Updated Observation {}", id);
        return true;
    }

    public boolean insertObservedProperty(ObservedProperty op)
            throws NoSuchEntityException, IncompleteEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QObsProperties qop = QObsProperties.obsProperties;
        SQLInsertClause insert = qFactory.insert(qop);
        insert.set(qop.definition, op.getDefinition());
        insert.set(qop.name, op.getName());
        insert.set(qop.description, op.getDescription());

        Long generatedId = insert.executeWithKey(qop.id);
        LOGGER.info("Inserted ObservedProperty. Created id = {}.", generatedId);
        op.setId(new LongId(generatedId));
        return true;
    }

    public boolean updateObservedProperty(ObservedProperty op, long opId) throws NoSuchEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QObsProperties qop = QObsProperties.obsProperties;
        SQLUpdateClause update = qFactory.update(qop);
        if (op.isSetDefinition()) {
            if (op.getDefinition() == null) {
                throw new IncompleteEntityException("definition can not be null.");
            }
            update.set(qop.definition, op.getDefinition());
        }
        if (op.isSetDescription()) {
            if (op.getDescription() == null) {
                throw new IncompleteEntityException("description can not be null.");
            }
            update.set(qop.description, op.getDescription());
        }
        if (op.isSetName()) {
            if (op.getName() == null) {
                throw new IncompleteEntityException("name can not be null.");
            }
            update.set(qop.name, op.getName());
        }
        update.where(qop.id.eq(opId));
        long count = 0;
        if (!update.isEmpty()) {
            count = update.execute();
        }
        if (count > 1) {
            LOGGER.error("Updating ObservedProperty {} caused {} rows to change!", opId, count);
            throw new IllegalStateException("Update changed multiple rows.");
        }

        // Link existing Datastreams to the observedProperty.
        for (Datastream ds : op.getDatastreams()) {
            if (ds.getId() == null || !entityExists(ds)) {
                throw new NoSuchEntityException("ObservedProperty with no id or non existing.");
            }
            Long dsId = (Long) ds.getId().getValue();
            QDatastreams qds = QDatastreams.datastreams;
            long dsCount = qFactory.update(qds).set(qds.obsPropertyId, opId).where(qds.id.eq(dsId)).execute();
            if (dsCount > 0) {
                LOGGER.info("Assigned datastream {} to ObservedProperty {}.", dsId, opId);
            }
        }

        if (!op.getMultiDatastreams().isEmpty()) {
            throw new IllegalArgumentException("Can not add MultiDatastreams to an ObservedProperty.");
        }

        LOGGER.debug("Updated ObservedProperty {}", opId);
        return true;
    }

    public boolean insertSensor(Sensor s) throws NoSuchEntityException, IncompleteEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QSensors qs = QSensors.sensors;
        SQLInsertClause insert = qFactory.insert(qs);
        insert.set(qs.name, s.getName());
        insert.set(qs.description, s.getDescription());
        insert.set(qs.encodingType, s.getEncodingType());
        // TODO: Check metadata serialisation.
        insert.set(qs.metadata, s.getMetadata().toString());

        Long generatedId = insert.executeWithKey(qs.id);
        LOGGER.info("Inserted Sensor. Created id = {}.", generatedId);
        s.setId(new LongId(generatedId));

        // Create new datastreams, if any.
        for (Datastream ds : s.getDatastreams()) {
            ds.setSensor(new SensorBuilder().setId(s.getId()).build());
            ds.complete();
            pm.insert(ds);
        }

        return true;
    }

    public boolean updateSensor(Sensor s, long sensorId) throws NoSuchEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QSensors qs = QSensors.sensors;
        SQLUpdateClause update = qFactory.update(qs);
        if (s.isSetName()) {
            if (s.getName() == null) {
                throw new IncompleteEntityException("name can not be null.");
            }
            update.set(qs.name, s.getName());
        }
        if (s.isSetDescription()) {
            if (s.getDescription() == null) {
                throw new IncompleteEntityException("description can not be null.");
            }
            update.set(qs.description, s.getDescription());
        }
        if (s.isSetEncodingType()) {
            if (s.getEncodingType() == null) {
                throw new IncompleteEntityException("encodingType can not be null.");
            }
            update.set(qs.encodingType, s.getEncodingType());
        }
        if (s.isSetMetadata()) {
            if (s.getMetadata() == null) {
                throw new IncompleteEntityException("metadata can not be null.");
            }
            // TODO: Check metadata serialisation.
            update.set(qs.metadata, s.getMetadata().toString());
        }
        update.where(qs.id.eq(sensorId));
        long count = 0;
        if (!update.isEmpty()) {
            count = update.execute();
        }
        if (count > 1) {
            LOGGER.error("Updating Sensor {} caused {} rows to change!", sensorId, count);
            throw new IllegalStateException("Update changed multiple rows.");
        }

        // Link existing Datastreams to the sensor.
        for (Datastream ds : s.getDatastreams()) {
            if (ds.getId() == null || !entityExists(ds)) {
                throw new NoSuchEntityException("Datastream with no id or non existing.");
            }
            Long dsId = (Long) ds.getId().getValue();
            QDatastreams qds = QDatastreams.datastreams;
            long dsCount = qFactory.update(qds).set(qds.sensorId, sensorId).where(qds.id.eq(dsId)).execute();
            if (dsCount > 0) {
                LOGGER.info("Assigned datastream {} to sensor {}.", dsId, sensorId);
            }
        }

        // Link existing MultiDatastreams to the sensor.
        for (MultiDatastream mds : s.getMultiDatastreams()) {
            if (mds.getId() == null || !entityExists(mds)) {
                throw new NoSuchEntityException("MultiDatastream with no id or non existing.");
            }
            Long mdsId = (Long) mds.getId().getValue();
            QMultiDatastreams qmds = QMultiDatastreams.multiDatastreams;
            long mdsCount = qFactory.update(qmds).set(qmds.sensorId, sensorId).where(qmds.id.eq(mdsId)).execute();
            if (mdsCount > 0) {
                LOGGER.info("Assigned multiDatastream {} to sensor {}.", mdsId, sensorId);
            }
        }

        LOGGER.debug("Updated Sensor {}", sensorId);
        return true;
    }

    public boolean insertThing(Thing t) throws NoSuchEntityException, IncompleteEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QThings qt = QThings.things;
        SQLInsertClause insert = qFactory.insert(qt);
        insert.set(qt.name, t.getName());
        insert.set(qt.description, t.getDescription());
        insert.set(qt.properties, objectToJson(t.getProperties()));

        Long thingId = insert.executeWithKey(qt.id);
        LOGGER.info("Inserted Thing. Created id = {}.", thingId);
        t.setId(new LongId(thingId));

        // Create new Locations, if any.
        List<Long> locationIds = new ArrayList<>();
        for (Location l : t.getLocations()) {
            entityExistsOrCreate(l);
            Long lId = (Long) l.getId().getValue();

            QThingsLocations qtl = QThingsLocations.thingsLocations;
            insert = qFactory.insert(qtl);
            insert.set(qtl.thingId, thingId);
            insert.set(qtl.locationId, lId);
            insert.execute();
            LOGGER.debug("Linked Location {} to Thing {}.", lId, thingId);
            locationIds.add(lId);
        }

        // Now link the new locations also to a historicalLocation.
        if (!locationIds.isEmpty()) {
            QHistLocations qhl = QHistLocations.histLocations;
            insert = qFactory.insert(qhl);
            insert.set(qhl.thingId, thingId);
            insert.set(qhl.time, new Timestamp(Calendar.getInstance().getTimeInMillis()));
            Long histLocationId = insert.executeWithKey(qhl.id);
            LOGGER.debug("Created historicalLocation {}", histLocationId);

            QLocationsHistLocations qlhl = QLocationsHistLocations.locationsHistLocations;
            for (Long locId : locationIds) {
                qFactory.insert(qlhl).set(qlhl.histLocationId, histLocationId).set(qlhl.locationId, locId)
                        .execute();
                LOGGER.info("Linked location {} to historicalLocation {}.", locId, histLocationId);
            }
        }

        // Create new datastreams, if any.
        for (Datastream ds : t.getDatastreams()) {
            ds.setThing(new ThingBuilder().setId(t.getId()).build());
            ds.complete();
            pm.insert(ds);
        }

        // TODO: if we allow the creation of historicalLocations through Things
        // then we have to be able to link those to Locations we might have just created.
        // However, id juggling will be needed!
        return true;
    }

    public boolean updateThing(Thing t, long thingId) throws NoSuchEntityException {
        SQLQueryFactory qFactory = pm.createQueryFactory();
        QThings qt = QThings.things;
        SQLUpdateClause update = qFactory.update(qt);
        if (t.isSetName()) {
            if (t.getName() == null) {
                throw new IncompleteEntityException("name can not be null.");
            }
            update.set(qt.name, t.getName());
        }
        if (t.isSetDescription()) {
            if (t.getDescription() == null) {
                throw new IncompleteEntityException("description can not be null.");
            }
            update.set(qt.description, t.getDescription());
        }
        if (t.isSetProperties()) {
            update.set(qt.properties, objectToJson(t.getProperties()));
        }
        update.where(qt.id.eq(thingId));
        long count = 0;
        if (!update.isEmpty()) {
            count = update.execute();
        }
        if (count > 1) {
            LOGGER.error("Updating Thing {} caused {} rows to change!", thingId, count);
            throw new IllegalStateException("Update changed multiple rows.");
        }
        LOGGER.debug("Updated Thing {}", thingId);

        // Link existing Datastreams to the thing.
        for (Datastream ds : t.getDatastreams()) {
            if (ds.getId() == null || !entityExists(ds)) {
                throw new NoSuchEntityException("Datastream with no id or non existing.");
            }
            Long dsId = (Long) ds.getId().getValue();
            QDatastreams qds = QDatastreams.datastreams;
            long dsCount = qFactory.update(qds).set(qds.thingId, thingId).where(qds.id.eq(dsId)).execute();
            if (dsCount > 0) {
                LOGGER.info("Assigned datastream {} to thing {}.", dsId, thingId);
            }
        }

        // Link existing MultiDatastreams to the thing.
        for (MultiDatastream mds : t.getMultiDatastreams()) {
            if (mds.getId() == null || !entityExists(mds)) {
                throw new NoSuchEntityException("MultiDatastream with no id or non existing.");
            }
            Long mdsId = (Long) mds.getId().getValue();
            QMultiDatastreams qmds = QMultiDatastreams.multiDatastreams;
            long mdsCount = qFactory.update(qmds).set(qmds.thingId, thingId).where(qmds.id.eq(mdsId)).execute();
            if (mdsCount > 0) {
                LOGGER.info("Assigned multiDatastream {} to thing {}.", mdsId, thingId);
            }
        }

        // Link existing locations to the thing.
        if (!t.getLocations().isEmpty()) {
            // Unlink old Locations from Thing.
            QThingsLocations qtl = QThingsLocations.thingsLocations;
            count = qFactory.delete(qtl).where(qtl.thingId.eq(thingId)).execute();
            LOGGER.info("Unlinked {} locations from Thing {}.", count, thingId);

            // Link new locations to Thing, track the ids.
            List<Long> locationIds = new ArrayList<>();
            for (Location l : t.getLocations()) {
                if (l.getId() == null || !entityExists(l)) {
                    throw new NoSuchEntityException("Location with no id.");
                }
                Long locationId = (Long) l.getId().getValue();

                SQLInsertClause insert = qFactory.insert(qtl);
                insert.set(qtl.thingId, thingId);
                insert.set(qtl.locationId, locationId);
                insert.execute();
                LOGGER.debug("Linked Location {} to Thing {}.", locationId, thingId);
                locationIds.add(locationId);
            }

            // Now link the newly linked locations also to a historicalLocation.
            if (!locationIds.isEmpty()) {
                QHistLocations qhl = QHistLocations.histLocations;
                SQLInsertClause insert = qFactory.insert(qhl);
                insert.set(qhl.thingId, thingId);
                insert.set(qhl.time, new Timestamp(Calendar.getInstance().getTimeInMillis()));
                Long histLocationId = insert.executeWithKey(qhl.id);
                LOGGER.debug("Created historicalLocation {}", histLocationId);

                QLocationsHistLocations qlhl = QLocationsHistLocations.locationsHistLocations;
                for (Long locId : locationIds) {
                    qFactory.insert(qlhl).set(qlhl.histLocationId, histLocationId).set(qlhl.locationId, locId)
                            .execute();
                    LOGGER.info("Linked location {} to historicalLocation {}.", locId, histLocationId);
                }
            }
        }
        return true;
    }

    private static <T extends StoreClause> T insertTimeValue(T clause, DateTimePath<Timestamp> startPath,
            DateTimePath<Timestamp> endPath, TimeValue time) {
        if (time instanceof TimeInstant) {
            TimeInstant timeInstant = (TimeInstant) time;
            insertTimeInstant(clause, endPath, timeInstant);
            return insertTimeInstant(clause, startPath, timeInstant);
        } else if (time instanceof TimeInterval) {
            TimeInterval timeInterval = (TimeInterval) time;
            return insertTimeInterval(clause, startPath, endPath, timeInterval);
        }
        return clause;
    }

    private static <T extends StoreClause> T insertTimeInstant(T clause, DateTimePath<Timestamp> path,
            TimeInstant time) {
        if (time == null) {
            return clause;
        }
        clause.set(path, new Timestamp(time.getDateTime().getMillis()));
        return clause;
    }

    private static <T extends StoreClause> T insertTimeInterval(T clause, DateTimePath<Timestamp> startPath,
            DateTimePath<Timestamp> endPath, TimeInterval time) {
        if (time == null) {
            return clause;
        }
        Interval interval = time.getInterval();
        clause.set(startPath, new Timestamp(interval.getStartMillis()));
        clause.set(endPath, new Timestamp(interval.getEndMillis()));
        return clause;
    }

    /**
     * Sets both the geometry and location in the clause.
     *
     * @param <T> The type of the clause.
     * @param clause The insert or update clause to add to.
     * @param locationPath The path to the location column.
     * @param geomPath The path to the geometry column.
     * @param encodingType The encoding type.
     * @param location The location.
     * @return The insert or update clause.
     */
    private <T extends StoreClause> T insertGeometry(T clause, StringPath locationPath,
            GeometryPath<Geometry> geomPath, String encodingType, final Object location) {
        if ("application/vnd.geo+json".equalsIgnoreCase(encodingType)) {
            // TODO: Postgres does not support Feature.
            Object geoLocation = location;
            if (location instanceof Feature) {
                geoLocation = ((Feature) location).getGeometry();
            }
            String geoJson;
            String locJson;
            try {
                geoJson = new GeoJsonSerializer().serialize(geoLocation);
                if (geoLocation == location) {
                    locJson = geoJson;
                } else {
                    locJson = new GeoJsonSerializer().serialize(location);
                }
            } catch (JsonProcessingException ex) {
                LOGGER.error("Failed to store.", ex);
                throw new IllegalArgumentException(
                        "encoding specifies geoJson, but location not parsable as such.");
            }

            try {
                // geojson.jackson allows invalid polygons, geolatte catches those.
                new JsonMapper().fromJson(geoJson, Geometry.class);
            } catch (JsonException ex) {
                throw new IllegalArgumentException("Invalid geoJson: " + ex.getMessage());
            }
            clause.set(geomPath,
                    Expressions.template(Geometry.class, "ST_Force3D(ST_GeomFromGeoJSON({0}))", geoJson));
            clause.set(locationPath, locJson);
        } else {
            String json;
            json = objectToJson(location);
            clause.setNull(geomPath);
            clause.set(locationPath, json);
        }
        return clause;
    }

    private Object reParseGeometry(String encodingType, Object object) {
        String json = objectToJson(object);
        return PropertyHelper.locationFromEncoding(encodingType, json);
    }

    /**
     * Throws an exception if the entity has an id, but does not exist or if the
     * entity can not be created.
     *
     * @param pm the persistenceManager
     * @param e The Entity to check.
     * @throws NoSuchEntityException If the entity has an id, but does not
     * exist.
     * @throws IncompleteEntityException If the entity has no id, but is not
     * complete and can thus not be created.
     */
    private void entityExistsOrCreate(Entity e) throws NoSuchEntityException, IncompleteEntityException {
        if (e != null && e.getId() == null) {
            e.complete();
            pm.insert(e);
        } else if (e == null || !entityExists(e)) {
            if (e == null) {
                throw new NoSuchEntityException("No entity!");
            }
            throw new NoSuchEntityException(
                    "No such entity '" + e.getEntityType() + "' with id " + e.getId().getValue());
        }
    }

    public boolean entityExists(Entity e) {
        if (e == null || e.getId() == null) {
            return false;
        }
        long id = (long) e.getId().getValue();
        SQLQueryFactory qFactory = pm.createQueryFactory();
        long count = 0;
        switch (e.getEntityType()) {
        case Datastream:
            QDatastreams d = QDatastreams.datastreams;
            count = qFactory.select().from(d).where(d.id.eq(id)).fetchCount();
            break;

        case MultiDatastream:
            QMultiDatastreams md = QMultiDatastreams.multiDatastreams;
            count = qFactory.select().from(md).where(md.id.eq(id)).fetchCount();
            break;

        case FeatureOfInterest:
            QFeatures foi = QFeatures.features;
            count = qFactory.select().from(foi).where(foi.id.eq(id)).fetchCount();
            break;

        case HistoricalLocation:
            QHistLocations h = QHistLocations.histLocations;
            count = qFactory.select().from(h).where(h.id.eq(id)).fetchCount();
            break;

        case Location:
            QLocations l = QLocations.locations;
            count = qFactory.select().from(l).where(l.id.eq(id)).fetchCount();
            break;

        case Observation:
            QObservations o = QObservations.observations;
            count = qFactory.select().from(o).where(o.id.eq(id)).fetchCount();
            break;

        case ObservedProperty:
            QObsProperties op = QObsProperties.obsProperties;
            count = qFactory.select().from(op).where(op.id.eq(id)).fetchCount();
            break;

        case Sensor:
            QSensors s = QSensors.sensors;
            count = qFactory.select().from(s).where(s.id.eq(id)).fetchCount();
            break;

        case Thing:
            QThings t = QThings.things;
            count = qFactory.select().from(t).where(t.id.eq(id)).fetchCount();
            break;

        default:
            throw new AssertionError(e.getEntityType().name());
        }
        if (count > 1) {
            LOGGER.error("More than one instance of {} with id {}.", e.getEntityType(), id);
        }
        return count > 0;
    }

    public boolean entityExists(ResourcePath path) {
        long count = pm.count(path, null);
        if (count > 1) {
            LOGGER.error("More than one instance of {}", path.toString());
        }
        return count > 0;
    }

    public String objectToJson(Object object) {
        if (object == null) {
            return null;
        }
        try {
            return getFormatter().writeValueAsString(object);
        } catch (IOException ex) {
            throw new IllegalStateException("Could not serialise object.", ex);
        }
    }

    public ObjectMapper getFormatter() {
        if (formatter == null) {
            formatter = new ObjectMapper();
        }
        return formatter;
    }

}