uk.ac.ucl.excites.sapelli.transmission.db.TransmissionStore.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.ucl.excites.sapelli.transmission.db.TransmissionStore.java

Source

/**
 * Sapelli data collection platform: http://sapelli.org
 * 
 * Copyright 2012-2016 University College London - ExCiteS group
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */

package uk.ac.ucl.excites.sapelli.transmission.db;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections4.map.LRUMap;
import org.apache.commons.io.Charsets;

import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;

import uk.ac.ucl.excites.sapelli.shared.db.exceptions.DBException;
import uk.ac.ucl.excites.sapelli.shared.io.BitArray;
import uk.ac.ucl.excites.sapelli.shared.util.CollectionUtils;
import uk.ac.ucl.excites.sapelli.shared.util.Objects;
import uk.ac.ucl.excites.sapelli.storage.db.RecordStore;
import uk.ac.ucl.excites.sapelli.storage.db.RecordStoreWrapper;
import uk.ac.ucl.excites.sapelli.storage.model.Model;
import uk.ac.ucl.excites.sapelli.storage.model.Record;
import uk.ac.ucl.excites.sapelli.storage.model.RecordReference;
import uk.ac.ucl.excites.sapelli.storage.model.Schema;
import uk.ac.ucl.excites.sapelli.storage.model.columns.BooleanColumn;
import uk.ac.ucl.excites.sapelli.storage.model.columns.ByteArrayColumn;
import uk.ac.ucl.excites.sapelli.storage.model.columns.ForeignKeyColumn;
import uk.ac.ucl.excites.sapelli.storage.model.columns.IntegerColumn;
import uk.ac.ucl.excites.sapelli.storage.model.columns.StringColumn;
import uk.ac.ucl.excites.sapelli.storage.model.indexes.AutoIncrementingPrimaryKey;
import uk.ac.ucl.excites.sapelli.storage.model.indexes.Index;
import uk.ac.ucl.excites.sapelli.storage.model.indexes.PrimaryKey;
import uk.ac.ucl.excites.sapelli.storage.queries.FirstRecordQuery;
import uk.ac.ucl.excites.sapelli.storage.queries.Order;
import uk.ac.ucl.excites.sapelli.storage.queries.RecordsQuery;
import uk.ac.ucl.excites.sapelli.storage.queries.SingleRecordQuery;
import uk.ac.ucl.excites.sapelli.storage.queries.constraints.AndConstraint;
import uk.ac.ucl.excites.sapelli.storage.queries.constraints.Constraint;
import uk.ac.ucl.excites.sapelli.storage.queries.constraints.EqualityConstraint;
import uk.ac.ucl.excites.sapelli.storage.queries.constraints.RuleConstraint;
import uk.ac.ucl.excites.sapelli.storage.queries.constraints.RuleConstraint.Comparison;
import uk.ac.ucl.excites.sapelli.storage.queries.sources.Source;
import uk.ac.ucl.excites.sapelli.storage.types.TimeStamp;
import uk.ac.ucl.excites.sapelli.storage.types.TimeStampColumn;
import uk.ac.ucl.excites.sapelli.storage.util.ColumnPointer;
import uk.ac.ucl.excites.sapelli.transmission.TransmissionClient;
import uk.ac.ucl.excites.sapelli.transmission.model.Correspondent;
import uk.ac.ucl.excites.sapelli.transmission.model.Payload;
import uk.ac.ucl.excites.sapelli.transmission.model.Transmission;
import uk.ac.ucl.excites.sapelli.transmission.model.Transmission.Type;
import uk.ac.ucl.excites.sapelli.transmission.model.transport.geokey.GeoKeyServer;
import uk.ac.ucl.excites.sapelli.transmission.model.transport.geokey.GeoKeyTransmission;
import uk.ac.ucl.excites.sapelli.transmission.model.transport.sms.Message;
import uk.ac.ucl.excites.sapelli.transmission.model.transport.sms.SMSCorrespondent;
import uk.ac.ucl.excites.sapelli.transmission.model.transport.sms.SMSTransmission;
import uk.ac.ucl.excites.sapelli.transmission.model.transport.sms.binary.BinaryMessage;
import uk.ac.ucl.excites.sapelli.transmission.model.transport.sms.binary.BinarySMSTransmission;
import uk.ac.ucl.excites.sapelli.transmission.model.transport.sms.text.TextMessage;
import uk.ac.ucl.excites.sapelli.transmission.model.transport.sms.text.TextSMSTransmission;
import uk.ac.ucl.excites.sapelli.transmission.util.UnknownCorrespondentException;

/**
 * Class to handle storage of transmissions and their parts. Based on {@link RecordStore}.
 * 
 * @author mstevens, Michalis Vitos, benelliott
 */
public class TransmissionStore extends RecordStoreWrapper<TransmissionClient> {

    // STATICS---------------------------------------------
    static private byte[] StringToBytes(String str) {
        return str.getBytes(Charsets.UTF_8);
    }

    static private String BytesToString(byte[] bytes) {
        return new String(bytes, Charsets.UTF_8);
    }

    // Transmission storage model:
    //   Model:
    static public final Model TRANSMISSION_MANAGEMENT_MODEL = new Model(
            TransmissionClient.TRANSMISSION_MANAGEMENT_MODEL_ID, "TransmissionManagement",
            TransmissionClient.SCHEMA_FLAGS_TRANSMISSION_INTERNAL);
    // Schema(s) & columns:
    //   Correspondent schemas:
    static final public Schema CORRESPONDENT_SCHEMA = TransmissionClient.CreateSchemaWithSuffixedTableName(
            TRANSMISSION_MANAGEMENT_MODEL, Correspondent.class.getSimpleName(), "s");
    //   Correspondent columns:
    static final public IntegerColumn CORRESPONDENT_COLUMN_ID = CORRESPONDENT_SCHEMA
            .addColumn(new IntegerColumn("ID", false, false)); // unsigned 32 bits
    static final public StringColumn CORRESPONDENT_COLUMN_NAME = CORRESPONDENT_SCHEMA.addColumn(
            StringColumn.ForCharacterCount("Name", false, Correspondent.CORRESPONDENT_NAME_MAX_LENGTH_CHARS));
    static final public IntegerColumn CORRESPONDENT_COLUMN_TRANSMISSION_TYPE = CORRESPONDENT_SCHEMA
            .addColumn(new IntegerColumn("TransmissionType", false));
    static final public StringColumn CORRESPONDENT_COLUMN_ADDRESS = CORRESPONDENT_SCHEMA.addColumn(
            StringColumn.ForCharacterCount("Address", true, Correspondent.CORRESPONDENT_ADDRESS_MAX_LENGTH_CHARS));
    //static final public StringColumn CORRESPONDENT_COLUMN_ENCRYPTION_KEY = CORRESPONDENT_SCHEMA.addColumn(new StringColumn("Key", false, Correspondent.CORRESPONDENT_ENCRYPTION_KEY_MAX_LENGTH_BYTES));
    static final public BooleanColumn CORRESPONDENT_COLUMN_USER_DELETED = CORRESPONDENT_SCHEMA
            .addColumn(new BooleanColumn("UserDeleted", false, Boolean.FALSE));
    //   Set primary key, add indexes & seal schema:
    static {
        CORRESPONDENT_SCHEMA.addIndex(new Index(CORRESPONDENT_COLUMN_TRANSMISSION_TYPE, false));
        CORRESPONDENT_SCHEMA.addIndex(new Index(CORRESPONDENT_COLUMN_USER_DELETED, false));
        CORRESPONDENT_SCHEMA.setPrimaryKey(
                new AutoIncrementingPrimaryKey(CORRESPONDENT_SCHEMA.getName() + "_PK", CORRESPONDENT_COLUMN_ID),
                true /*seal!*/);
    }
    //   Transmission schemas:
    static final public Schema OUTGOING_TRANSMISSION_SCHEMA = TransmissionClient.CreateSchemaWithSuffixedTableName(
            TRANSMISSION_MANAGEMENT_MODEL, "Outgoing" + Transmission.class.getSimpleName(), "s");
    static final public Schema INCOMING_TRANSMISSION_SCHEMA = TransmissionClient.CreateSchemaWithSuffixedTableName(
            TRANSMISSION_MANAGEMENT_MODEL, "Incoming" + Transmission.class.getSimpleName(), "s");
    //   Transmission columns:
    static final public IntegerColumn TRANSMISSION_COLUMN_ID = new IntegerColumn("ID", false,
            Transmission.TRANSMISSION_ID_FIELD);
    static final public IntegerColumn TRANSMISSION_COLUMN_REMOTE_ID = new IntegerColumn("RemoteID", true,
            Transmission.TRANSMISSION_ID_FIELD);
    static final public IntegerColumn TRANSMISSION_COLUMN_TYPE = new IntegerColumn("Type", false);
    static final public IntegerColumn TRANSMISSION_COLUMN_PAYLOAD_HASH = new IntegerColumn("PayloadHash", false,
            Transmission.PAYLOAD_HASH_FIELD);
    static final public IntegerColumn TRANSMISSION_COLUMN_PAYLOAD_TYPE = new IntegerColumn("PayloadType", true,
            Payload.PAYLOAD_TYPE_FIELD);
    static final public ForeignKeyColumn TRANSMISSION_COLUMN_CORRESPONDENT = new ForeignKeyColumn(
            CORRESPONDENT_SCHEMA, true);
    static final public IntegerColumn TRANSMISSION_COLUMN_NUMBER_OF_PARTS = new IntegerColumn("NumberOfParts",
            false, false, Integer.SIZE);
    static final public String TRANSMISSION_COLUMN_NAME_RESPONSE = "Response";
    static final public BooleanColumn TRANSMISSION_COLUMN_DELETED = new BooleanColumn("Deleted", false,
            Boolean.FALSE);
    static final public IntegerColumn TRANSMISSION_COLUMN_NUMBER_OF_RESEND_REQS_SENT = new IntegerColumn(
            "SentResendRequests", false, Integer.SIZE); // only used on receiving side
    static final public TimeStampColumn TRANSMISSION_COLUMN_LAST_RESEND_REQS_SENT_AT = TimeStampColumn
            .JavaMSTime("LastResendReqSentAt", true, false); // only used on receiving side
    //   Columns shared with Transmission Part schema:
    static final public TimeStampColumn COLUMN_SENT_AT = TimeStampColumn.JavaMSTime("SentAt", true, false);
    static final public TimeStampColumn COLUMN_RECEIVED_AT = TimeStampColumn.JavaMSTime("ReceivedAt", true, false);
    //   Add columns and index to Transmission schemas & seal them:
    static {
        // PKs:
        OUTGOING_TRANSMISSION_SCHEMA.addColumn(TRANSMISSION_COLUMN_ID);
        OUTGOING_TRANSMISSION_SCHEMA.setPrimaryKey(new AutoIncrementingPrimaryKey(
                OUTGOING_TRANSMISSION_SCHEMA.getName() + "_PK", TRANSMISSION_COLUMN_ID));
        INCOMING_TRANSMISSION_SCHEMA.addColumn(TRANSMISSION_COLUMN_ID);
        INCOMING_TRANSMISSION_SCHEMA.setPrimaryKey(new AutoIncrementingPrimaryKey(
                INCOMING_TRANSMISSION_SCHEMA.getName() + "_PK", TRANSMISSION_COLUMN_ID));
        // Other columns:
        for (Schema schema : new Schema[] { OUTGOING_TRANSMISSION_SCHEMA, INCOMING_TRANSMISSION_SCHEMA }) {
            schema.addColumn(TRANSMISSION_COLUMN_REMOTE_ID);
            schema.addColumn(TRANSMISSION_COLUMN_TYPE);
            schema.addColumn(TRANSMISSION_COLUMN_PAYLOAD_HASH);
            schema.addColumn(TRANSMISSION_COLUMN_PAYLOAD_TYPE);
            schema.addColumn(TRANSMISSION_COLUMN_CORRESPONDENT);
            schema.addColumn(TRANSMISSION_COLUMN_NUMBER_OF_PARTS);
            schema.addColumn(COLUMN_SENT_AT);
            schema.addColumn(COLUMN_RECEIVED_AT);
            // Response FK:
            schema.addColumn(new ForeignKeyColumn(TRANSMISSION_COLUMN_NAME_RESPONSE,
                    schema == OUTGOING_TRANSMISSION_SCHEMA ? INCOMING_TRANSMISSION_SCHEMA
                            : OUTGOING_TRANSMISSION_SCHEMA,
                    true));
            schema.addColumn(TRANSMISSION_COLUMN_DELETED);
            // Only for incoming transmissions:
            if (schema == INCOMING_TRANSMISSION_SCHEMA) {
                schema.addColumn(TRANSMISSION_COLUMN_NUMBER_OF_RESEND_REQS_SENT);
                schema.addColumn(TRANSMISSION_COLUMN_LAST_RESEND_REQS_SENT_AT);
            }
            schema.seal(); // !!!
        }
    }
    //   Transmission Part schemas:
    static final public Schema OUTGOING_TRANSMISSION_PART_SCHEMA = TransmissionClient
            .CreateSchemaWithSuffixedTableName(TRANSMISSION_MANAGEMENT_MODEL,
                    "Outgoing" + Transmission.class.getSimpleName() + "Part", "s");
    static final public Schema INCOMING_TRANSMISSION_PART_SCHEMA = TransmissionClient
            .CreateSchemaWithSuffixedTableName(TRANSMISSION_MANAGEMENT_MODEL,
                    "Incoming" + Transmission.class.getSimpleName() + "Part", "s");
    //   Transmission Part columns:
    static final public ForeignKeyColumn TRANSMISSION_PART_COLUMN_OUTGOING_TRANSMISSION = new ForeignKeyColumn(
            OUTGOING_TRANSMISSION_SCHEMA, false);
    static final public ForeignKeyColumn TRANSMISSION_PART_COLUMN_INCOMING_TRANSMISSION = new ForeignKeyColumn(
            INCOMING_TRANSMISSION_SCHEMA, false);
    static final public IntegerColumn TRANSMISSION_PART_COLUMN_NUMBER = new IntegerColumn("PartNumber", false,
            false, Integer.SIZE);
    static final public TimeStampColumn TRANSMISSION_PART_COLUMN_DELIVERED_AT = TimeStampColumn
            .JavaMSTime("DeliveredAt", true, false);
    static final public ByteArrayColumn TRANSMISSION_PART_COLUMN_BODY = new ByteArrayColumn("Body", false);
    static final public IntegerColumn TRANSMISSION_PART_COLUMN_BODY_BIT_LENGTH = new IntegerColumn("BodyBitLength",
            false, false, Integer.SIZE);
    //   Add columns to Transmission Part schemas & seal them:
    static {
        for (Schema schema : new Schema[] { OUTGOING_TRANSMISSION_PART_SCHEMA,
                INCOMING_TRANSMISSION_PART_SCHEMA }) {
            if (schema == OUTGOING_TRANSMISSION_PART_SCHEMA)
                schema.addColumn(TRANSMISSION_PART_COLUMN_OUTGOING_TRANSMISSION);
            else
                schema.addColumn(TRANSMISSION_PART_COLUMN_INCOMING_TRANSMISSION);
            schema.addColumn(TRANSMISSION_PART_COLUMN_NUMBER);
            schema.addColumn(COLUMN_SENT_AT);
            schema.addColumn(TRANSMISSION_PART_COLUMN_DELIVERED_AT);
            schema.addColumn(COLUMN_RECEIVED_AT);
            schema.addColumn(TRANSMISSION_PART_COLUMN_BODY);
            schema.addColumn(TRANSMISSION_PART_COLUMN_BODY_BIT_LENGTH);
            schema.setPrimaryKey(PrimaryKey.WithColumnNames(
                    (schema == OUTGOING_TRANSMISSION_PART_SCHEMA ? TRANSMISSION_PART_COLUMN_OUTGOING_TRANSMISSION
                            : TRANSMISSION_PART_COLUMN_INCOMING_TRANSMISSION),
                    TRANSMISSION_PART_COLUMN_NUMBER), true /*seal!*/);
        }
    }
    //   Transmittable Records schema:
    static final public Schema TRANSMITTABLE_RECORDS_SCHEMA = TransmissionClient.CreateSchemaWithSuffixedTableName(
            TRANSMISSION_MANAGEMENT_MODEL, "Transmittable" + Record.class.getSimpleName(), "s");
    //      Columns:
    static public final ForeignKeyColumn TRANSMITTABLE_RECORDS_COLUMN_RECEIVER = TRANSMITTABLE_RECORDS_SCHEMA
            .addColumn(new ForeignKeyColumn(CORRESPONDENT_SCHEMA, false));
    static public final ForeignKeyColumn TRANSMITTABLE_RECORDS_COLUMN_SCHEMA = TRANSMITTABLE_RECORDS_SCHEMA
            .addColumn(new ForeignKeyColumn(Model.SCHEMA_SCHEMA, false));
    static public final ByteArrayColumn TRANSMITTABLE_RECORDS_COLUMN_PK_VALUES = TRANSMITTABLE_RECORDS_SCHEMA
            .addColumn(new ByteArrayColumn("PKValueBytes", false));
    static public final ForeignKeyColumn TRANSMITTABLE_RECORDS_COLUMN_TRANSMISSION = TRANSMITTABLE_RECORDS_SCHEMA
            .addColumn(new ForeignKeyColumn(OUTGOING_TRANSMISSION_SCHEMA, true));
    static final public BooleanColumn TRANSMITTABLE_RECORDS_COLUMN_RECEIVED = TRANSMITTABLE_RECORDS_SCHEMA
            .addColumn(new BooleanColumn("Received", false, Boolean.FALSE));
    //      Set PK and seal:
    static {
        TRANSMITTABLE_RECORDS_SCHEMA.setPrimaryKey(
                PrimaryKey.WithColumnNames(TRANSMITTABLE_RECORDS_COLUMN_RECEIVER,
                        TRANSMITTABLE_RECORDS_COLUMN_SCHEMA, TRANSMITTABLE_RECORDS_COLUMN_PK_VALUES),
                true /*seal!*/);
    }
    //      ColumnPointers (helpers):
    static public final ColumnPointer<IntegerColumn> TRANSMITTABLE_RECORDS_CP_TRANSMISSION_ID = new ColumnPointer<IntegerColumn>(
            TRANSMITTABLE_RECORDS_SCHEMA, TRANSMISSION_COLUMN_ID);
    static public final ColumnPointer<IntegerColumn> TRANSMITTABLE_RECORDS_CP_SCHEMA_NUMBER = new ColumnPointer<IntegerColumn>(
            TRANSMITTABLE_RECORDS_SCHEMA, Model.SCHEMA_SCHEMA_NUMBER_COLUMN);
    static public final ColumnPointer<IntegerColumn> TRANSMITTABLE_RECORDS_CP_MODEL_ID = new ColumnPointer<IntegerColumn>(
            TRANSMITTABLE_RECORDS_SCHEMA, Model.MODEL_ID_COLUMN);
    //   Seal the model:
    static {
        TRANSMISSION_MANAGEMENT_MODEL.seal();
    }

    static private final int MAX_CACHE_SIZE = 8;

    static public TimeStamp retrieveTimeStamp(TimeStampColumn column, Record record) {
        return TimeStamp.setLocalTimeZone(column.retrieveValue(record));
    }

    // DYNAMICS--------------------------------------------
    private final Map<Integer, Transmission<?>> outCache;
    private final Map<Integer, Transmission<?>> inCache;

    private final TransmissionRecordGenerator generator = new TransmissionRecordGenerator();

    /**
     * @param client
     * @throws DBException
     */
    public TransmissionStore(TransmissionClient client) throws DBException {
        super(client);
        this.outCache = Collections.synchronizedMap(new LRUMap<Integer, Transmission<?>>(MAX_CACHE_SIZE));
        this.inCache = Collections.synchronizedMap(new LRUMap<Integer, Transmission<?>>(MAX_CACHE_SIZE));
    }

    protected Map<Integer, Transmission<?>> getCache(boolean incoming) {
        return incoming ? inCache : outCache;
    }

    public void store(Correspondent correspondent) throws DBException {
        // Start transaction
        recordStore.startTransaction();

        try {
            doStoreCorrespondent(correspondent);
        } catch (DBException e) {
            recordStore.rollbackTransactions();
            client.logError("Error upon storing correspondent", e);
            throw e;
        }

        // Commit transaction
        recordStore.commitTransaction();
    }

    /**
     * @param correspondent
     * @return
     */
    private Record getCorrespondentRecord(Correspondent correspondent) {
        return new CorrespondentRecordGenerator(correspondent).rec;
    }

    /**
     * 
     * 
     * @param correspondent
     * @return a RecordReference to the (now stored/updated) Correspondent Record
     * @throws DBException
     */
    private RecordReference doStoreCorrespondent(Correspondent correspondent) throws DBException {
        // Null check:
        if (correspondent == null)
            return null;

        Record cRec = getCorrespondentRecord(correspondent);

        // Store the correspondent record:
        recordStore.store(cRec);
        //   local ID should now be set in the record...

        // Check/set it on the object:
        if (correspondent.isLocalIDSet()) // if the object already had a local transmissionID...
        { // then it should match the ID on the record, so let's verify:
            if (correspondent.getLocalID() != CORRESPONDENT_COLUMN_ID.retrieveValue(cRec).intValue())
                throw new IllegalStateException("Non-matching correspodent ID"); // this should never happen
        } else
            // Set local transmissionID in object as on the record: 
            correspondent.setLocalID(CORRESPONDENT_COLUMN_ID.retrieveValue(cRec).intValue());

        //client.logInfo("Stored correspondent: " + correspondent.getName() + " (localid: " + (correspondent.isLocalIDSet() ? correspondent.getLocalID() : "null") + ")");

        return cRec.getReference();
    }

    /**
     * Note: this method is public because it is called from ProjectRecordStore
     * 
     * @param correspondent may be null (in which case this method just returns null as well)
     * @param storeIfNeeded whether to store the Correspondent if it isn't already
     * @param forceUpdate forces the Correspondent to be updated in the database
     * @return a RecordReference pointing to the Record representing the Correspondent in the database, or null if it has never been stored (or is null itself)
     * @throws DBException
     */
    public RecordReference getCorrespondentRecordReference(Correspondent correspondent, boolean storeIfNeeded,
            boolean forceUpdate) throws DBException {
        if (correspondent == null)
            return null;
        else if (correspondent.isLocalIDSet() /*already stored*/ && !forceUpdate)
            return CORRESPONDENT_SCHEMA.createRecordReference(correspondent.getLocalID());
        else if (storeIfNeeded || forceUpdate)
            return doStoreCorrespondent(correspondent);
        else
            return null;
    }

    @SuppressWarnings("unchecked")
    private <C extends Correspondent> C correspondentFromRecord(Record cRec) {
        // Null check:
        if (cRec == null)
            return null;

        int localID = CORRESPONDENT_COLUMN_ID.retrieveValue(cRec).intValue();
        String name = CORRESPONDENT_COLUMN_NAME.retrieveValue(cRec);
        String address = CORRESPONDENT_COLUMN_ADDRESS.retrieveValue(cRec);
        Transmission.Type ttype = Transmission.Type.values()[CORRESPONDENT_COLUMN_TRANSMISSION_TYPE
                .retrieveValue(cRec).intValue()];
        Correspondent corr;
        switch (ttype) {
        case BINARY_SMS:
            corr = new SMSCorrespondent(localID, name, address, true);
            break;
        case TEXTUAL_SMS:
            corr = new SMSCorrespondent(localID, name, address, false);
            break;
        case GeoKey:
            corr = new GeoKeyServer(localID, name, address);
            break;
        default:
            throw new IllegalStateException("Unsupported transmission type");
        }
        if (CORRESPONDENT_COLUMN_USER_DELETED.retrieveValue(cRec))
            corr.markAsUserDeleted();
        return (C) corr;
    }

    /**
     * Note: this method is public because it is called from ProjectRecordStore
     * 
     * @param recordQuery
     * @return
     */
    public Correspondent retrieveCorrespondentByQuery(SingleRecordQuery recordQuery) {
        // Query for record and convert to Correspondent object:
        return correspondentFromRecord(recordStore.retrieveRecord(recordQuery));
    }

    /**
     * @param includeUnknownSenders
     * @param includeUserDeleted
     * @return
     */
    public List<Correspondent> retrieveCorrespondents(boolean includeUnknownSenders, boolean includeUserDeleted) {
        RecordsQuery query = new RecordsQuery(Source.From(CORRESPONDENT_SCHEMA),
                (!includeUnknownSenders
                        ? new EqualityConstraint(CORRESPONDENT_COLUMN_NAME, Correspondent.UNKNOWN_SENDER_NAME,
                                false)
                        : null),
                (!includeUserDeleted ? new EqualityConstraint(CORRESPONDENT_COLUMN_USER_DELETED, Boolean.FALSE)
                        : null));
        List<Correspondent> correspondents = new ArrayList<Correspondent>();
        for (Record record : recordStore.retrieveRecords(query))
            CollectionUtils.addIgnoreNull(correspondents, correspondentFromRecord(record)); // convert to Correspondent objects
        return correspondents;
    }

    /**
     * Retrieves the SMSCorrespondent with the given phone number and binary/text mode
     * 
     * @param phoneNumber
     * @param binarySMS
     * @return the correspondent or null
     */
    public SMSCorrespondent retrieveSMSCorrespondent(PhoneNumber phoneNumber, boolean binarySMS) {
        return (SMSCorrespondent) retrieveCorrespondentByQuery(new FirstRecordQuery(CORRESPONDENT_SCHEMA,
                new EqualityConstraint(CORRESPONDENT_COLUMN_ADDRESS,
                        SMSCorrespondent.getAddressString(phoneNumber)),
                new RuleConstraint(CORRESPONDENT_COLUMN_TRANSMISSION_TYPE, Comparison.EQUAL,
                        (binarySMS ? Transmission.Type.BINARY_SMS : Transmission.Type.TEXTUAL_SMS).ordinal())));
    }

    /**
     * @param correspondent to delete
     */
    public void deleteCorrespondent(Correspondent correspondent) {
        if (!correspondent.isLocalIDSet())
            return; // the correspondent was never stored
        try {
            // Get record reference:
            RecordReference cRecRef = CORRESPONDENT_SCHEMA.createRecordReference(correspondent.getLocalID());

            // Delete transmission part records:
            recordStore.delete(cRecRef);
        } catch (Exception ignore) {
        }
    }

    /**
     * @param incoming if {@code true} we are dealing with transmissions that were received on the local device, if {@code false} we are dealing with transmissions created for sending from the local device to other ones
     * @return the schema to use to create/store/retrieve a Record representation of such Transmission(s)
     */
    private Schema getTransmissionSchema(boolean incoming) {
        return incoming ? INCOMING_TRANSMISSION_SCHEMA : OUTGOING_TRANSMISSION_SCHEMA;
    }

    /**
     * @param incoming if {@code true} we are dealing with transmissions that were received on the local device, if {@code false} we are dealing with transmissions created for sending from the local device to other ones
     * @return the schema to use to create/store/retrieve Record representations of parts of such Transmission(s)
     */
    private Schema getTransmissionPartSchema(boolean incoming) {
        return incoming ? INCOMING_TRANSMISSION_PART_SCHEMA : OUTGOING_TRANSMISSION_PART_SCHEMA;
    }

    /**
     * @param incoming if {@code true} we are dealing with transmissions that were received on the local device, if {@code false} we are dealing with transmissions created for sending from the local device to other ones
     * @return 
     */
    private ForeignKeyColumn getResponseColumn(boolean incoming) {
        return (ForeignKeyColumn) getTransmissionSchema(incoming).getColumn(TRANSMISSION_COLUMN_NAME_RESPONSE,
                false);
    }

    public synchronized void store(Transmission<?> transmission) throws DBException {
        // Start transaction
        recordStore.startTransaction();

        try {
            // Use TransmissionRecordGenerator to create a transmission record and part record(s):
            List<Record> records = generator.generate(transmission);
            if (records.size() < 2)
                throw new IllegalStateException("No transmission (part) record(s) generated!");
            Record tRec = records.get(0);

            // Set foreign key for Correspondent record (possibly first storing/updating it):
            TRANSMISSION_COLUMN_CORRESPONDENT.storeValue(tRec,
                    getCorrespondentRecordReference(transmission.getCorrespondent(), true, false));

            // Store the transmission record:
            recordStore.store(tRec);
            //   local ID should now be set in the record...

            // Check/set it on the object:
            if (transmission.isLocalIDSet()) // if the object already had a local transmissionID...
            { // then it should match the ID on the record, so let's verify:
                if (transmission.getLocalID() != TRANSMISSION_COLUMN_ID.retrieveValue(tRec).intValue())
                    throw new IllegalStateException("Non-matching transmission ID"); // this should never happen
            } else
                // Set local transmissionID in object as on the record: 
                transmission.setLocalID(TRANSMISSION_COLUMN_ID.retrieveValue(tRec).intValue());

            // Store part records:
            ForeignKeyColumn tFKCol = transmission.incoming ? TRANSMISSION_PART_COLUMN_INCOMING_TRANSMISSION
                    : TRANSMISSION_PART_COLUMN_OUTGOING_TRANSMISSION;
            RecordReference tRecRef = tRec.getReference();
            for (Record tPartRec : records.subList(1, records.size())) {
                tFKCol.storeValue(tPartRec, tRecRef); // set foreign key!
                recordStore.store(tPartRec);
            }

            // Put/update in cache:
            getCache(transmission.incoming).put(transmission.getLocalID(), transmission);
        } catch (Exception e) {
            recordStore.rollbackTransactions();
            if (e instanceof DBException)
                throw (DBException) e;
            throw new DBException(e);
        }

        // Commit transaction
        recordStore.commitTransaction();
    }

    /**
     * @param incoming if {@code true} the transmission was received on the local device, if {@code false} it was created for sending from the local device to another one
     * @param type
     * @param localID
     * @param remoteID
     * @param correspondent
     * @param payloadHash
     * @param numberOfParts
     * @return
     * @throws UnknownCorrespondentException 
     */
    private RecordsQuery getTransmissionsQuery(boolean incoming, Transmission.Type type, Integer localID,
            Integer remoteID, Correspondent correspondent, Integer payloadHash, Integer numberOfParts)
            throws UnknownCorrespondentException {
        if (correspondent != null && !correspondent.isLocalIDSet())
            throw new UnknownCorrespondentException(
                    "Correspondent (" + correspondent.toString() + ") is unknown in database.");
        return new RecordsQuery(
                // schema (sent/received):
                getTransmissionSchema(incoming),
                // localID:
                (localID != null
                        ? getTransmissionSchema(incoming).createRecordReference(localID).getRecordQueryConstraint()
                        : null),
                // remoteID:
                (remoteID != null ? new RuleConstraint(TRANSMISSION_COLUMN_REMOTE_ID, Comparison.EQUAL, remoteID)
                        : null),
                // type:
                (type != null ? new RuleConstraint(TRANSMISSION_COLUMN_TYPE, Comparison.EQUAL, type.ordinal())
                        : null),
                // correspondent:
                (correspondent != null
                        ? CORRESPONDENT_SCHEMA.createRecordReference(correspondent.getLocalID())
                                .getRecordQueryConstraint()
                        : null),
                // payload hash:
                (payloadHash != null
                        ? new RuleConstraint(TRANSMISSION_COLUMN_PAYLOAD_HASH, Comparison.EQUAL, payloadHash)
                        : null),
                // number of parts:
                (numberOfParts != null
                        ? new RuleConstraint(TRANSMISSION_COLUMN_NUMBER_OF_PARTS, Comparison.EQUAL, numberOfParts)
                        : null));
    }

    /**
     * @param multiRecordQuery
     * @return
     * @throws IllegalStateException when more than 1 matching Transmission is found (cannot happen for queries that check localID, which is unique)
     */
    protected Transmission<?> retrieveTransmissionByQuery(RecordsQuery multiRecordQuery, boolean knownLocalID)
            throws IllegalStateException {
        List<Transmission<?>> results = retrieveTransmissionsByQuery(multiRecordQuery);
        if (results.size() > 1) {
            if (!knownLocalID)
                throw new IllegalStateException("Found more than 1 matching transmission for query");
            else
                return null; // this should never happen
        }
        if (results.isEmpty())
            return null;
        else
            return results.get(0);
    }

    protected List<Transmission<?>> retrieveTransmissionsByQuery(RecordsQuery multiRecordQuery) {
        List<Transmission<?>> transmissions = new ArrayList<Transmission<?>>();
        for (Record record : recordStore.retrieveRecords(multiRecordQuery))
            CollectionUtils.addIgnoreNull(transmissions, transmissionFromRecord(record, false)); // convert to Transmission objects (skipping "hidden" deleted transmissions)
        return transmissions;
    }

    private Transmission<?> transmissionFromRecord(Record tRec, boolean includeDeleted) {
        // Null check:
        if (tRec == null)
            return null; // no such transmission found

        // Essential values:
        boolean incoming = tRec.getSchema().equals(INCOMING_TRANSMISSION_SCHEMA);
        int localID = TRANSMISSION_COLUMN_ID.retrieveValue(tRec).intValue();

        // Check cache:
        if (getCache(incoming).containsKey(localID))
            return getCache(incoming).get(localID);

        // Check if transmission is not deleted by hiding:
        if (TRANSMISSION_COLUMN_DELETED.retrieveValue(tRec) && !includeDeleted)
            return null;

        // Other values:
        Transmission.Type type = Transmission.Type.values()[TRANSMISSION_COLUMN_TYPE.retrieveValue(tRec)
                .intValue()];
        Integer remoteID = TRANSMISSION_COLUMN_REMOTE_ID.isValuePresent(tRec)
                ? TRANSMISSION_COLUMN_REMOTE_ID.retrieveValue(tRec).intValue()
                : null;
        Integer payloadType = TRANSMISSION_COLUMN_PAYLOAD_TYPE.isValuePresent(tRec)
                ? TRANSMISSION_COLUMN_PAYLOAD_TYPE.retrieveValue(tRec).intValue()
                : null;
        int payloadHash = TRANSMISSION_COLUMN_PAYLOAD_HASH.retrieveValue(tRec).intValue();
        TimeStamp sentAt = retrieveTimeStamp(COLUMN_SENT_AT, tRec);
        TimeStamp receivedAt = retrieveTimeStamp(COLUMN_RECEIVED_AT, tRec);
        int totalParts = TRANSMISSION_COLUMN_NUMBER_OF_PARTS.retrieveValue(tRec).intValue();
        //   Columns only occurring on receiving side:
        int numberOfSentResendRequests = incoming
                ? TRANSMISSION_COLUMN_NUMBER_OF_RESEND_REQS_SENT.retrieveValue(tRec).intValue()
                : 0;
        TimeStamp lastResendReqSentAt = incoming
                ? retrieveTimeStamp(TRANSMISSION_COLUMN_LAST_RESEND_REQS_SENT_AT, tRec)
                : null;
        RecordReference responseRecRef = getResponseColumn(incoming).retrieveValue(tRec);
        Transmission<?> response = responseRecRef != null
                ? retrieveTransmission(!incoming, TRANSMISSION_COLUMN_ID.retrieveValue(responseRecRef).intValue())
                : null;

        // Query for correspondent record:
        Record cRec = TRANSMISSION_COLUMN_CORRESPONDENT.isValuePresent(tRec)
                ? recordStore.retrieveRecord(TRANSMISSION_COLUMN_CORRESPONDENT.retrieveValue(tRec))
                : null;

        // Query for part records:      
        List<Record> tPartRecs = recordStore
                .retrieveRecords(new RecordsQuery(Source.From(getTransmissionPartSchema(incoming)),
                        Order.AscendingBy(TRANSMISSION_PART_COLUMN_NUMBER), tRec.getRecordQueryConstraint()));

        // Instantiate Transmissions & Messages:
        switch (type) {
        case BINARY_SMS:
            // create a new SMSTransmission object:
            BinarySMSTransmission binarySMST = new BinarySMSTransmission(client,
                    this.<SMSCorrespondent>correspondentFromRecord(cRec), incoming, localID, remoteID, payloadType,
                    payloadHash, sentAt, receivedAt, (BinarySMSTransmission) response, numberOfSentResendRequests,
                    lastResendReqSentAt);
            // add each part we got from the query:
            for (Record tPartRec : tPartRecs)
                binarySMST.addPart(new BinaryMessage(binarySMST,
                        TRANSMISSION_PART_COLUMN_NUMBER.retrieveValue(tPartRec).intValue(), totalParts,
                        retrieveTimeStamp(COLUMN_SENT_AT, tPartRec),
                        retrieveTimeStamp(TRANSMISSION_PART_COLUMN_DELIVERED_AT, tPartRec),
                        retrieveTimeStamp(COLUMN_RECEIVED_AT, tPartRec),
                        BitArray.FromBytes(TRANSMISSION_PART_COLUMN_BODY.retrieveValue(tPartRec),
                                TRANSMISSION_PART_COLUMN_BODY_BIT_LENGTH.retrieveValue(tPartRec).intValue())));
            return binarySMST;
        case TEXTUAL_SMS:
            // create a new SMSTransmission object:
            TextSMSTransmission textSMST = new TextSMSTransmission(client,
                    this.<SMSCorrespondent>correspondentFromRecord(cRec), incoming, localID, remoteID, payloadType,
                    payloadHash, sentAt, receivedAt, (TextSMSTransmission) response, numberOfSentResendRequests,
                    lastResendReqSentAt);
            // add each part we got from the query:
            for (Record tPartRec : tPartRecs)
                textSMST.addPart(new TextMessage(textSMST,
                        TRANSMISSION_PART_COLUMN_NUMBER.retrieveValue(tPartRec).intValue(), totalParts,
                        retrieveTimeStamp(COLUMN_SENT_AT, tPartRec),
                        retrieveTimeStamp(TRANSMISSION_PART_COLUMN_DELIVERED_AT, tPartRec),
                        retrieveTimeStamp(COLUMN_RECEIVED_AT, tPartRec),
                        BytesToString(TRANSMISSION_PART_COLUMN_BODY.retrieveValue(tPartRec))));
            return textSMST;
        case GeoKey:
            return new GeoKeyTransmission(client, this.<GeoKeyServer>correspondentFromRecord(cRec), incoming,
                    localID, remoteID, payloadType, payloadHash, lastResendReqSentAt, receivedAt,
                    (GeoKeyTransmission) response, TRANSMISSION_PART_COLUMN_BODY.retrieveValue(tPartRecs.get(0)));
        default:
            throw new IllegalStateException("Unsupported transmission type");
        }
    }

    /**
     * Retrieve an incoming or outgoing transmission by its local ID.
     * 
     * @param incoming if {@code true} the transmission was received on the local device, if {@code false} it was created for sending from the local device to another one
     * @param localID
     * 
     * @return the Transmission with the given {@code localID}, or {@code null} if no such transmission was found.
     */
    public synchronized Transmission<?> retrieveTransmission(boolean incoming, int localID) {
        return retrieveTransmission(incoming, localID, false);
    }

    /**
     * Retrieve an incoming or outgoing transmission by its local ID.
     * 
     * @param incoming if {@code true} the transmission was received on the local device, if {@code false} it was created for sending from the local device to another one
     * @param localID
     * @param findDeleted whether or not to retrieve transmission which are deleted by hiding
     * 
     * @return the Transmission with the given {@code localID}, or {@code null} if no such transmission was found.
     */
    public synchronized Transmission<?> retrieveTransmission(boolean incoming, int localID, boolean findDeleted) {
        try {
            // Check cache:
            if (getCache(incoming).containsKey(localID))
                return getCache(incoming).get(localID);
            //else:
            return transmissionFromRecord(
                    recordStore.retrieveRecord(getTransmissionSchema(incoming).createRecordReference(localID)),
                    findDeleted);
        } catch (Exception e) {
            client.logError("Error retrieving " + (incoming ? "received" : "sent")
                    + " transmission with local ID = " + localID + ".", e);
            return null;
        }
    }

    /**
     * Retrieve an incoming or outgoing transmission by a RecordReference pointing to a Transmission record.
     * 
     * @param transmissionRecordReference
     * @param findDeleted whether or not to retrieve transmission which are deleted by hiding
     * @return
     */
    protected synchronized Transmission<?> retrieveTransmission(RecordReference transmissionRecordReference,
            boolean findDeleted) {
        return retrieveTransmission(
                transmissionRecordReference.getReferencedSchema() == INCOMING_TRANSMISSION_SCHEMA,
                TRANSMISSION_COLUMN_ID.retrieveValue(transmissionRecordReference).intValue());
    }

    /**
     * @param incoming if {@code true} the transmission was received on the local device, if {@code false} it was created for sending from the local device to another one
     * @param localID
     * @param payloadHash
     * @return a matching Transmission, or {@code null} if no such transmission was found or an error occurred (check log output).
     */
    public synchronized Transmission<?> retrieveTransmission(boolean incoming, int localID, int payloadHash) {
        return retrieveTransmission(incoming, localID, payloadHash, null);
    }

    /**
     * @param incoming if {@code true} the transmission was received on the local device, if {@code false} it was created for sending from the local device to another one
     * @param localID
     * @param payloadHash
     * @param numberOfParts
     * @return a matching Transmission, or {@code null} if no such transmission was found or an error occurred (check log output).
     */
    public Transmission<?> retrieveTransmission(boolean incoming, int localID, int payloadHash,
            Integer numberOfParts) {
        return retrieveTransmissionByQuery(
                getTransmissionsQuery(incoming, null, localID, null, null, payloadHash, numberOfParts), true);
    }

    /**
     * Retrieve an incoming or outgoing transmission by its type (binary/textual), correspondent, remote(!) ID, payload hash, and number of parts.
     * 
     * @param incoming
     * @param type
     * @param correspondent
     * @param remoteID - not local!
     * @param payloadHash
     * @param numberOfParts
     * @return a matching Transmission, or {@code null} if no such transmission was found
     * @throws IllegalStateException when more than 1 matching Transmission is found
     * @throws UnknownCorrespondentException when the correspondent is unknown
     */
    public Transmission<?> retrieveTransmission(boolean incoming, Transmission.Type type,
            Correspondent correspondent, int remoteID, int payloadHash, int numberOfParts)
            throws IllegalStateException, UnknownCorrespondentException {
        return retrieveTransmissionByQuery(
                getTransmissionsQuery(incoming, type, null, remoteID, correspondent, payloadHash, numberOfParts),
                false);
    }

    /**
     * Retrieve an incoming or outgoing SMS transmission by its local ID, type (binary/textual) and number of parts.
     * 
     * @param incoming if {@code true} the transmission was received on the local device, if {@code false} it was created for sending from the local device to another one
     * @param localID
     * @param binary
     * @param numberOfParts
     * @return a matching Transmission, or {@code null} if no such transmission was found or an error occurred (check log output).
     */
    public SMSTransmission<?> retrieveSMSTransmission(boolean incoming, int localID, boolean binary,
            int numberOfParts) {
        return (SMSTransmission<?>) retrieveTransmissionByQuery(getTransmissionsQuery(incoming,
                binary ? Type.BINARY_SMS : Type.TEXTUAL_SMS, localID, null, null, null, numberOfParts), true);
    }

    /**
     * Returns a list of received but incomplete SMSTransmissions.
     * 
     * Note: this only deals with SMSTransmissions as an HTTPTransmission cannot (yet) be incomplete.
     * 
     * @return a list of incomplete SMSTransmissions
     */
    public List<SMSTransmission<?>> retrieveIncompleteSMSTransmissions() {
        List<SMSTransmission<?>> incompleteSMSTs = new ArrayList<SMSTransmission<?>>();

        // query DB for transmissions which are incomplete (have "null" as their receivedAt value):
        for (Transmission<?> t : retrieveTransmissionsByQuery(new RecordsQuery(
                Source.From(getTransmissionSchema(true)), EqualityConstraint.IsNull(COLUMN_RECEIVED_AT))))
            if (t instanceof SMSTransmission)
                incompleteSMSTs.add((SMSTransmission<?>) t); // cast these transmissions as SMSTransmissions

        return incompleteSMSTs;
    }

    /**
     * Returns a list of all incoming or outgoing transmissions from or to the given correspondent.
     * 
     * @param incoming
     * @param correspondent
     * @return
     */
    public List<Transmission<?>> retrieveTransmissions(boolean incoming, Correspondent correspondent) {
        try {
            return retrieveTransmissionsByQuery(
                    getTransmissionsQuery(incoming, null, null, null, correspondent, null, null));
        } catch (UnknownCorrespondentException uce) {
            return Collections.<Transmission<?>>emptyList();
        }
    }

    /**
     * @param transmission
     * @param byHiding if {@code true} the transmission will only be hidden (marked as deleted, but still in the db), if {@code false} it (and its parts) will be completely removed from the db
     */
    public void deleteTransmission(Transmission<?> transmission, boolean byHiding) {
        if (!transmission.isLocalIDSet())
            return; // the transmission was never stored
        try {
            recordStore.startTransaction();

            // Get record reference:
            RecordReference tRecRef = getTransmissionSchema(transmission.incoming)
                    .createRecordReference(transmission.getLocalID());

            if (byHiding) {
                Record tRec = generator.generate(transmission).get(0);
                TRANSMISSION_COLUMN_DELETED.storeValue(tRec, Boolean.TRUE);
                // Store the transmission record:
                recordStore.store(tRec);
            } else { // Really delete from db:
                     //   Delete transmission part records:
                recordStore.delete(new RecordsQuery(Source.From(getTransmissionPartSchema(transmission.incoming)),
                        tRecRef.getRecordQueryConstraint()));

                //   Delete transmission record:
                recordStore.delete(tRecRef);
            }

            recordStore.commitTransaction();

            // Delete from cache:
            getCache(transmission.incoming).remove(transmission.getLocalID());
        } catch (Exception e) {
            client.logError("Error upon deleting translission (local ID: " + transmission.getLocalID() + ")", e);
            try {
                recordStore.rollbackTransactions();
            } catch (Exception ignore) {
            }
        }
    }

    /**
     * Registers that a Record, indicated by the given RecordReference, is transmittable to the given Correspondent,
     * and optionally that a transmission (attempt) will take or has taken place using the given Transmission object. 
     * 
     * @param correspondent
     * @param recordReference a RecordReference pointing to the Record which we are told is transmittable
     * @param transmission may be null, but if it isn't it must have been stored before
     */
    public void storeTransmittableRecord(Correspondent correspondent, RecordReference recordReference,
            Transmission<?> transmission) {
        if (transmission != null && !transmission.isLocalIDSet())
            throw new IllegalArgumentException(
                    "Transmission must have been stored before being associated with records to need sending or have been sent.");
        try {
            recordStore.store(TRANSMITTABLE_RECORDS_SCHEMA.createRecord(
                    // Receiver column (first store/update the Correspondent if necessary):
                    getCorrespondentRecordReference(correspondent, true, false),
                    // Schema column (= Model ID + Schema#):
                    recordReference.getReferencedSchema().getMetaRecordReference(),
                    // PKValues column:
                    recordReference.toBytes(true),
                    // Transmission column:
                    transmission != null
                            ? getTransmissionSchema(false).createRecordReference(transmission.getLocalID())
                            : null,
                    // Received column:
                    Boolean.valueOf(transmission != null && transmission.isReceived())));
        } catch (Exception e) {
            client.logError("Error upon storing transmittable", e);
        }
    }

    /**
     * Removes all entries relating to the referenced Record from the TransmittableRecords table (possibly for multiple receivers). 
     * 
     * @param recordReference
     */
    public void deleteTransmittableRecord(RecordReference recordReference) {
        try {
            recordStore.delete(new RecordsQuery(TRANSMITTABLE_RECORDS_SCHEMA,
                    // Schema column (= Model ID + Schema#):
                    recordReference.getReferencedSchema().getMetaRecordReference().getRecordQueryConstraint(),
                    // PKValueBytes column:
                    new EqualityConstraint(TRANSMITTABLE_RECORDS_COLUMN_PK_VALUES, recordReference.toBytes(true))));
        } catch (Exception e) {
            client.logError("Error upon deleting transmittable(s)", e);
        }
    }

    /**
     * Retrieves all records, with Schemata from the given Model, that are marked for transmission
     * to the given Correspondent and which are not (yet) associated with a Transmission.
     * 
     * @param correspondent
     * @param model
     * @return
     */
    public List<Record> retrieveTransmittableRecordsWithoutTransmission(Correspondent correspondent, Model model) {
        return retrieveTransmittableUserRecords(correspondent, model,
                EqualityConstraint.IsNull(TRANSMITTABLE_RECORDS_COLUMN_TRANSMISSION));
    }

    /**
     * Retrieves all records, with Schemata from the given Model, that are marked for transmission
     * to the given Correspondent and which are (already) associated with a Transmission. 
     * 
     * @param correspondent
     * @param model
     * @return
     */
    public List<Record> retrieveTransmittableRecordsWithTransmission(Correspondent correspondent, Model model) {
        return retrieveTransmittableUserRecords(correspondent, model,
                EqualityConstraint.IsNotNull(TRANSMITTABLE_RECORDS_COLUMN_TRANSMISSION));
    }

    /**
     * Retrieves all records that are marked for transmission and which are associated with the given Transmission
     * (and therefore intended for its receiver).
     * Note that the result should be the (bar order) as getting the records from the Transmission's RecordsPayload.
     * 
     * @param correspondent
     * @param model
     * @param transmission
     * @return
     */
    public List<Record> retrieveTransmittableRecordsWithTransmission(Model model, Transmission<?> transmission) {
        return retrieveTransmittableUserRecords(transmission.getCorrespondent(), model, getTransmissionSchema(false)
                .createRecordReference(transmission.getLocalID()).getRecordQueryConstraint());
    }

    /**
     * @param receiver
     * @param model
     * @return
     */
    public synchronized List<Record> retrieveRecordsToTransmitNow(Correspondent receiver, Model model) {
        List<Record> recsToSend = new ArrayList<Record>();

        // Query for unsent (as in, not associated with a transmission) records for the given receiver & model:
        CollectionUtils.addAllIgnoreNull(recsToSend,
                retrieveTransmittableRecordsWithoutTransmission(receiver, model));

        //Also include transmittable records which have a transmission which was never sent or for which we haven't received a response since the timeout:
        CollectionUtils.addAllIgnoreNull(recsToSend, retrieveTransmittableRecordsForResending(receiver, model));

        return recsToSend;
    }

    /**
     * Gets all unreceived {@link #TRANSMITTABLE_RECORDS_SCHEMA} records with an assigned transmission, grouped by transmission.
     * 
     * @param correspondent
     * @param model
     * @return Map<RecordReference, List<Record>>: key = transmission record reference; value = list of associated {@link #TRANSMITTABLE_RECORDS_SCHEMA} records
     */
    private synchronized Map<RecordReference, List<Record>> retrieveUnreceivedTransmittablesWithTransmission(
            Correspondent correspondent, Model model) {
        List<Record> toSendRecs = retrieveTransmittableRecords(correspondent, model,
                Order.By(TRANSMITTABLE_RECORDS_COLUMN_TRANSMISSION), // order by the transmission
                new AndConstraint( // with transmission reference:
                        EqualityConstraint.IsNotNull(TRANSMITTABLE_RECORDS_COLUMN_TRANSMISSION),
                        // with Received=false:
                        new EqualityConstraint(TRANSMITTABLE_RECORDS_COLUMN_RECEIVED, Boolean.FALSE)));

        // Group by transmission record reference:
        Map<RecordReference, List<Record>> tRecRef2toSendRecs = new HashMap<RecordReference, List<Record>>();
        RecordReference prevTRecRef = null;
        for (Record toSendRec : toSendRecs) {
            RecordReference tRefRec = TRANSMITTABLE_RECORDS_COLUMN_TRANSMISSION.retrieveValue(toSendRec);
            if (!Objects.equals(prevTRecRef, tRefRec)) {
                tRecRef2toSendRecs.put(tRefRec, new ArrayList<Record>());
                prevTRecRef = tRefRec;
            }
            tRecRef2toSendRecs.get(tRefRec).add(toSendRec);
        }

        // Return map:
        return tRecRef2toSendRecs;
    }

    /**
     * @param correspondent
     * @param model
     * @param timeOutS
     * @return
     */
    public synchronized List<Record> retrieveTransmittableRecordsForResending(Correspondent correspondent,
            Model model) {
        // Get all unreceived transmittables with an assigned transmission:
        Map<RecordReference, List<Record>> tRecRef2toSendRecs = retrieveUnreceivedTransmittablesWithTransmission(
                correspondent, model);

        // Collection to return:
        List<Record> userRecs = new ArrayList<Record>();

        // Treat per transmission:
        for (Map.Entry<RecordReference, List<Record>> entry : tRecRef2toSendRecs.entrySet()) {
            // Get transmission object:
            Transmission<?> transmission = retrieveTransmission(entry.getKey(), false /*don't include deleted*/);

            if ( // unknown/deleted transmission:
            transmission == null ||
            // transmission is not received "says" it is appropriate to have its contents resent now:
                    (!transmission.isReceived() && transmission.isResendAppropriate())) {
                // Get user records for resending:
                for (Record toSendRec : entry.getValue())
                    CollectionUtils.addIgnoreNull(userRecs, getUserRecordFromTransmittable(toSendRec, model));
                // Delete transmission if there was one:
                if (transmission != null)
                    deleteTransmission(transmission, true /*deleting by hiding*/);
            } else if (transmission.isReceived()) { // transmission is received (i.e. ACKed):
                for (Record toSendRec : entry.getValue())
                    try { // Mark transmittable as received:
                        TRANSMITTABLE_RECORDS_COLUMN_RECEIVED.storeValue(toSendRec, Boolean.TRUE);
                        recordStore.store(toSendRec);
                    } catch (Exception e) {
                        client.logError("Error upon storing transmittable", e);
                    }
            }
        }

        return userRecs;
    }

    public synchronized void updateTransmittableReceivedState(Correspondent correspondent, Model model) {
        // Get all unreceived transmittables with an assigned transmission:
        Map<RecordReference, List<Record>> tRecRef2toSendRecs = retrieveUnreceivedTransmittablesWithTransmission(
                correspondent, model);

        // Treat per transmission:
        for (Map.Entry<RecordReference, List<Record>> entry : tRecRef2toSendRecs.entrySet()) {
            Transmission<?> transmission = retrieveTransmission(entry.getKey(), false /*don't include deleted*/);
            if (transmission != null && transmission.isReceived()) { // transmission is received (i.e. ACKed):
                for (Record toSendRec : entry.getValue())
                    try { // Mark transmittable as received:
                        TRANSMITTABLE_RECORDS_COLUMN_RECEIVED.storeValue(toSendRec, Boolean.TRUE);
                        recordStore.store(toSendRec);
                    } catch (Exception e) {
                        client.logError("Error upon storing transmittable", e);
                    }
            }
        }
    }

    /**
     * @param correspondent
     * @param model
     * @param contraint - may be null
     * @return a possibly empty list of {@link #TRANSMITTABLE_RECORDS_SCHEMA} records
     */
    private List<Record> retrieveTransmittableRecords(Correspondent correspondent, Model model, Order order,
            Constraint constraint) {
        RecordReference cRecRef = null;
        try {
            cRecRef = getCorrespondentRecordReference(correspondent, false, false);
        } catch (Exception ignore) {
        }
        if (cRecRef == null) // this means it has never been stored so there can also be no ToSend records for it
            return Collections.<Record>emptyList();

        // Query for ToSend records:
        List<Record> toSendRecs = recordStore.retrieveRecords(
                new RecordsQuery(TRANSMITTABLE_RECORDS_SCHEMA, order, cRecRef.getRecordQueryConstraint(),
                        model.getModelRecordReference().getRecordQueryConstraint(), constraint));

        // Return result:
        return toSendRecs;
    }

    /**
     * @param correspondent
     * @param model
     * @param contraint - may be null
     * @return
     */
    private List<Record> retrieveTransmittableUserRecords(Correspondent correspondent, Model model,
            Constraint contraint) {
        // Query for ToSend records:
        List<Record> toSendRecs = retrieveTransmittableRecords(correspondent, model,
                Order.By(TRANSMITTABLE_RECORDS_CP_SCHEMA_NUMBER), contraint);

        // Query for the actual records being referred to:
        List<Record> userRecs = new ArrayList<Record>(toSendRecs.size());
        for (Record toSendRec : toSendRecs)
            CollectionUtils.addIgnoreNull(userRecs, getUserRecordFromTransmittable(toSendRec, model));

        // Return result:
        return userRecs;
    }

    private Record getUserRecordFromTransmittable(Record toSendRecord, Model recycleModel) {
        try {
            // Get schema info:
            long modelID = ((Long) TRANSMITTABLE_RECORDS_CP_MODEL_ID.retrieveValue(toSendRecord)).longValue();
            int schemaNumber = ((Long) TRANSMITTABLE_RECORDS_CP_SCHEMA_NUMBER.retrieveValue(toSendRecord))
                    .intValue();
            // Get or recycle schema object:
            Schema schema = (recycleModel != null && recycleModel.id == modelID)
                    ? recycleModel.getSchema(schemaNumber)
                    : client.getSchema(modelID, schemaNumber);
            // Query for & return user record:
            return recordStore.retrieveRecord(schema
                    .createRecordReference(TRANSMITTABLE_RECORDS_COLUMN_PK_VALUES.retrieveValue(toSendRecord)));
        } catch (Exception e) {
            client.logError(
                    "Failed to retrieve user record for transmittable entry: " + toSendRecord.toString(false), e);
            return null;
        }
    }

    /**
     * Helper class to generate Records representing Correspondents
     * 
     * @author mstevens
     */
    private class CorrespondentRecordGenerator implements Correspondent.Handler {

        public final Record rec;

        public CorrespondentRecordGenerator(Correspondent correspondent) {
            rec = CORRESPONDENT_SCHEMA.createRecord();

            if (correspondent.isLocalIDSet())
                CORRESPONDENT_COLUMN_ID.storeValue(rec, correspondent.getLocalID());
            CORRESPONDENT_COLUMN_NAME.storeValue(rec, correspondent.getName());
            CORRESPONDENT_COLUMN_TRANSMISSION_TYPE.storeValue(rec, correspondent.getTransmissionType().ordinal());
            CORRESPONDENT_COLUMN_ADDRESS.storeValue(rec, correspondent.getAddress());
            CORRESPONDENT_COLUMN_USER_DELETED.storeValue(rec, correspondent.isUserDeleted());

            // Use double dispatch for subclass-specific work:
            correspondent.handle(this);
        }

        @Override
        public void handle(SMSCorrespondent smsCorrespondent) {
            // does nothing (for now)
        }

        @Override
        public void handle(GeoKeyServer geokeyAccount) {
            // does nothing (for now)
        }

    }

    /**
     * Helper class to generate Records representing Transmissions and their parts (Messages)
     * 
     * @author mstevens
     */
    private class TransmissionRecordGenerator implements Transmission.Handler, Message.Handler {

        private Record tRecord;
        private final List<Record> tPartRecords = new ArrayList<Record>();

        /**
         * @param transmission
         * @return a {@link List} of {@link Record}s, the first one of which is the tranmission record, the following ones are the transmission part records
         */
        public List<Record> generate(Transmission<?> transmission) {
            // Create new transmission record:
            tRecord = getTransmissionSchema(transmission.incoming).createRecord();

            // wipe part recs:
            tPartRecords.clear();

            // Set values of all columns will be set except for Correspondent & NumberOfParts:
            if (transmission.isLocalIDSet())
                TRANSMISSION_COLUMN_ID.storeValue(tRecord, transmission.getLocalID());
            if (transmission.isRemoteIDSet())
                TRANSMISSION_COLUMN_REMOTE_ID.storeValue(tRecord, transmission.getRemoteID());
            TRANSMISSION_COLUMN_TYPE.storeValue(tRecord, transmission.getType().ordinal());
            TRANSMISSION_COLUMN_PAYLOAD_HASH.storeValue(tRecord, transmission.getPayloadHash()); // payload hash should always be set before storage
            if (transmission.isPayloadTypeSet())
                TRANSMISSION_COLUMN_PAYLOAD_TYPE.storeValue(tRecord, transmission.getPayloadType());
            if (transmission.isSent())
                COLUMN_SENT_AT.storeValue(tRecord, transmission.getSentAt());
            if (transmission.isReceived())
                COLUMN_RECEIVED_AT.storeValue(tRecord, transmission.getReceivedAt());
            getResponseColumn(
                    transmission.incoming)
                            .storeValue(tRecord,
                                    transmission.hasResponse() && transmission.getResponse().isLocalIDSet()
                                            ? getTransmissionSchema(!transmission.incoming)
                                                    .createRecordReference(transmission.getResponse().getLocalID())
                                            : null);

            // Use double dispatch for type-specific work:
            transmission.handle(this);

            // Return list with tRecord and tPartRecords:
            List<Record> result = new ArrayList<Record>(tPartRecords.size() + 1);
            result.add(tRecord);
            result.addAll(tPartRecords);
            return result;
        }

        private Record newPartRecord(Transmission<?> transmission, int partNumber) {
            Record tPartRec = getTransmissionPartSchema(transmission.incoming).createRecord();
            tPartRecords.add(tPartRec);
            TRANSMISSION_PART_COLUMN_NUMBER.storeValue(tPartRec, partNumber);
            return tPartRec;
        }

        private void handleSMS(SMSTransmission<?> smsT) {
            // Set SMS-specific values:
            TRANSMISSION_COLUMN_NUMBER_OF_PARTS.storeValue(tRecord, smsT.getTotalNumberOfParts());
            if (smsT.incoming) { // columns only occuring on receiving side:
                TRANSMISSION_COLUMN_NUMBER_OF_RESEND_REQS_SENT.storeValue(tRecord,
                        smsT.getNumberOfSentResendRequests());
                TRANSMISSION_COLUMN_LAST_RESEND_REQS_SENT_AT.storeValue(tRecord, smsT.getLastResendRequestSentAt());
            }
            // Make records for the parts...
            for (Message<?, ?> msg : smsT.getParts()) {
                Record tPartRec = newPartRecord(smsT, msg.getPartNumber()); // adds to the tPartRecords list as well

                // Set columns (except for foreign key):
                COLUMN_SENT_AT.storeValue(tPartRec, msg.getSentAt());
                TRANSMISSION_PART_COLUMN_DELIVERED_AT.storeValue(tPartRec, msg.getDeliveredAt());
                COLUMN_RECEIVED_AT.storeValue(tPartRec, msg.getReceivedAt());
                msg.handle(this); // will set part body and body bit length
            }
        }

        @Override
        public void handle(BinarySMSTransmission binSMST) {
            handleSMS(binSMST);
        }

        @Override
        public void handle(TextSMSTransmission txtSMST) {
            handleSMS(txtSMST);
        }

        @Override
        public void handle(BinaryMessage binMsg) {
            BitArray bits = binMsg.getBody();
            setPartBody(bits.toByteArray(), bits.length());
        }

        @Override
        public void handle(TextMessage txtMsg) {
            setPartBody(StringToBytes(txtMsg.getBody()));
        }

        @Override
        public void handle(GeoKeyTransmission geoKeyT) {
            // Set number of parts (always = 1):
            TRANSMISSION_COLUMN_NUMBER_OF_PARTS.storeValue(tRecord, 1);
            if (geoKeyT.incoming) // columns only occuring on receiving side
            {
                // Set number of resend requests (always = 0):
                TRANSMISSION_COLUMN_NUMBER_OF_RESEND_REQS_SENT.storeValue(tRecord, 0);
                // TRANSMISSION_COLUMN_LAST_RESEND_REQS_SENT_AT remains null
            }

            // Create a single transmission part (only used to store the body):
            newPartRecord(geoKeyT, 1); // adds to the list as well
            setPartBody(geoKeyT.getBody()); // will set part body and body bit length
        }

        private void setPartBody(byte[] bodyBytes) {
            setPartBody(bodyBytes, bodyBytes.length * Byte.SIZE);
        }

        private void setPartBody(byte[] bodyBytes, int bitLength) {
            // Last tPartRec in the list:
            Record tPartRec = tPartRecords.get(tPartRecords.size() - 1);

            // Set body & body bit length columns:
            TRANSMISSION_PART_COLUMN_BODY.storeValue(tPartRec, bodyBytes);
            TRANSMISSION_PART_COLUMN_BODY_BIT_LENGTH.storeValue(tPartRec, bitLength);
        }

    }

}