uk.ac.cam.db538.cryptosms.storage.MessageData.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.cam.db538.cryptosms.storage.MessageData.java

Source

/*
 *   Copyright 2011 David Brazdil
 *
 *   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.cam.db538.cryptosms.storage;

import java.nio.ByteBuffer;
import java.util.ArrayList;

import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;

import uk.ac.cam.db538.cryptosms.crypto.Encryption;
import uk.ac.cam.db538.cryptosms.crypto.EncryptionInterface.EncryptionException;
import uk.ac.cam.db538.cryptosms.utils.Charset;
import uk.ac.cam.db538.cryptosms.utils.LowLevel;

/**
 * 
 * Class representing a message entry in the secure storage file.
 * 
 * @author David Brazdil
 *
 */
public class MessageData {
    // FILE FORMAT
    public static final int LENGTH_MESSAGE = 133;

    private static final int LENGTH_FLAGS = 1;
    private static final int LENGTH_TIMESTAMP = 29;
    private static final int LENGTH_MESSAGEBODYLEN = 2;
    private static final int LENGTH_MESSAGEBODY = LENGTH_MESSAGE;

    private static final int OFFSET_FLAGS = 0;
    private static final int OFFSET_TIMESTAMP = OFFSET_FLAGS + LENGTH_FLAGS;
    private static final int OFFSET_MESSAGEBODYLEN = OFFSET_TIMESTAMP + LENGTH_TIMESTAMP;
    private static final int OFFSET_MESSAGEBODY = OFFSET_MESSAGEBODYLEN + LENGTH_MESSAGEBODYLEN;

    private static final int OFFSET_RANDOMDATA = OFFSET_MESSAGEBODY + LENGTH_MESSAGEBODY;

    private static final int OFFSET_NEXTINDEX = Storage.ENCRYPTED_ENTRY_SIZE - 4;
    private static final int OFFSET_PREVINDEX = OFFSET_NEXTINDEX - 4;
    private static final int OFFSET_MSGSINDEX = OFFSET_PREVINDEX - 4;
    private static final int OFFSET_PARENTINDEX = OFFSET_MSGSINDEX - 4;

    private static final int LENGTH_RANDOMDATA = OFFSET_PARENTINDEX - OFFSET_RANDOMDATA;

    public enum MessageType {
        INCOMING, OUTGOING
    }

    // STATIC

    private static ArrayList<MessageData> cacheMessageData = new ArrayList<MessageData>();

    /**
     * Removes all instances from the list of cached objects.
     * Be sure you don't use the instances afterwards.
     */
    public static void forceClearCache() {
        synchronized (cacheMessageData) {
            cacheMessageData = new ArrayList<MessageData>();
        }
    }

    /**
     * Returns an instance of a new MessageData entry in the storage file.
     *
     * @param parent the parent
     * @return the message data
     * @throws StorageFileException the storage file exception
     */
    public static MessageData createMessageData(Conversation parent) throws StorageFileException {
        // create a new one
        MessageData msg = new MessageData(Empty.getEmptyIndex(), false);
        parent.attachMessageData(msg);
        return msg;
    }

    /**
     * Returns an instance of Empty class with given index in file.
     *
     * @param index    Index in file
     * @return the message data
     * @throws StorageFileException the storage file exception
     */
    static MessageData getMessageData(long index) throws StorageFileException {
        if (index <= 0L)
            return null;

        // try looking it up
        synchronized (cacheMessageData) {
            for (MessageData empty : cacheMessageData)
                if (empty.getEntryIndex() == index)
                    return empty;
        }

        // create a new one
        return new MessageData(index, true);
    }

    // INTERNAL FIELDS
    private long mEntryIndex; // READ ONLY
    private boolean mDeliveredPart;
    private boolean mDeliveredAll;
    private MessageType mMessageType;
    private boolean mUnread;
    private boolean mCompressed;
    private boolean mAscii;

    private DateTime mTimeStamp;
    private byte[] mMessageBody;
    private long mIndexParent;
    private long mIndexMessageParts;
    private long mIndexPrev;
    private long mIndexNext;

    // CONSTRUCTORS

    /**
     * Constructor
     * @param index         Which chunk of data should occupy in file
     * @param readFromFile   Does this entry already exist in the file?
     * @throws StorageFileException
     */
    private MessageData(long index, boolean readFromFile) throws StorageFileException {
        mEntryIndex = index;

        if (readFromFile) {
            byte[] dataEncrypted = Storage.getStorage().getEntry(index);
            byte[] dataPlain;
            try {
                dataPlain = Encryption.getEncryption().decryptSymmetricWithMasterKey(dataEncrypted);
            } catch (EncryptionException e) {
                throw new StorageFileException(e);
            }

            byte flags = dataPlain[OFFSET_FLAGS];
            boolean deliveredPart = ((flags & (1 << 7)) == 0) ? false : true;
            boolean deliveredAll = ((flags & (1 << 6)) == 0) ? false : true;
            boolean messageOutgoing = ((flags & (1 << 5)) == 0) ? false : true;
            boolean unread = ((flags & (1 << 4)) == 0) ? false : true;
            boolean compressed = ((flags & (1 << 3)) == 0) ? false : true;
            boolean ascii = ((flags & (1 << 2)) == 0) ? false : true;

            String timeStamp = Charset.fromAscii8(dataPlain, OFFSET_TIMESTAMP, LENGTH_TIMESTAMP);

            setDeliveredPart(deliveredPart);
            setDeliveredAll(deliveredAll);
            setMessageType((messageOutgoing) ? MessageType.OUTGOING : MessageType.INCOMING);
            setUnread(unread);
            setCompressed(compressed);
            setAscii(ascii);
            setTimeStamp(ISODateTimeFormat.dateTimeParser().parseDateTime(timeStamp));
            int messageBodyLength = Math.min(LENGTH_MESSAGEBODY,
                    LowLevel.getUnsignedShort(dataPlain, OFFSET_MESSAGEBODYLEN));
            setMessageBody(LowLevel.cutData(dataPlain, OFFSET_MESSAGEBODY, messageBodyLength));
            setIndexParent(LowLevel.getUnsignedInt(dataPlain, OFFSET_PARENTINDEX));
            setIndexMessageParts(LowLevel.getUnsignedInt(dataPlain, OFFSET_MSGSINDEX));
            setIndexPrev(LowLevel.getUnsignedInt(dataPlain, OFFSET_PREVINDEX));
            setIndexNext(LowLevel.getUnsignedInt(dataPlain, OFFSET_NEXTINDEX));
        } else {
            // default values
            setDeliveredPart(false);
            setDeliveredAll(false);
            setMessageType(MessageType.OUTGOING);
            setUnread(false);
            setCompressed(false);
            setAscii(true);
            setTimeStamp(new DateTime());
            setMessageBody(new byte[0]);
            setIndexParent(0L);
            setIndexMessageParts(0L);
            setIndexPrev(0L);
            setIndexNext(0L);

            saveToFile();
        }

        synchronized (cacheMessageData) {
            cacheMessageData.add(this);
        }
    }

    // FUNCTIONS

    /**
     * Save the contents of this class to its place in the storage file.
     *
     * @throws StorageFileException the storage file exception
     */
    public void saveToFile() throws StorageFileException {
        ByteBuffer msgBuffer = ByteBuffer.allocate(Storage.ENCRYPTED_ENTRY_SIZE);

        // flags
        byte flags = 0;
        if (this.mDeliveredPart)
            flags |= (byte) ((1 << 7) & 0xFF);
        if (this.mDeliveredAll)
            flags |= (byte) ((1 << 6) & 0xFF);
        if (this.mMessageType == MessageType.OUTGOING)
            flags |= (byte) ((1 << 5) & 0xFF);
        if (this.mUnread)
            flags |= (byte) ((1 << 4) & 0xFF);
        if (this.mCompressed)
            flags |= (byte) ((1 << 3) & 0xFF);
        if (this.mAscii)
            flags |= (byte) ((1 << 2) & 0xFF);
        msgBuffer.put(flags);

        // time stamp
        String timeStamp = ISODateTimeFormat.dateTime().print(this.mTimeStamp);
        msgBuffer.put(Charset.toAscii8(timeStamp, LENGTH_TIMESTAMP));

        // message body
        msgBuffer.put(LowLevel.getBytesUnsignedShort(this.mMessageBody.length));
        msgBuffer.put(LowLevel.wrapData(mMessageBody, LENGTH_MESSAGEBODY));

        // random data
        msgBuffer.put(Encryption.getEncryption().generateRandomData(LENGTH_RANDOMDATA));

        // indices
        msgBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexParent));
        msgBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexMessageParts));
        msgBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexPrev));
        msgBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexNext));

        byte[] dataEncrypted = null;
        try {
            dataEncrypted = Encryption.getEncryption().encryptSymmetricWithMasterKey(msgBuffer.array());
        } catch (EncryptionException e) {
            throw new StorageFileException(e);
        }
        Storage.getStorage().setEntry(mEntryIndex, dataEncrypted);
    }

    /**
     * Returns an instance of the Conversation class that is the parent of this Message in the data structure
     * @return
     * @throws StorageFileException
     */
    public Conversation getParent() throws StorageFileException {
        if (mIndexParent == 0)
            return null;
        return Conversation.getConversation(mIndexParent);
    }

    /**
     * Returns an instance of the Message that's predecessor of this one in the linked list of Messages of this Conversation
     * @return
     * @throws StorageFileException
     */
    public MessageData getPreviousMessageData() throws StorageFileException {
        if (mIndexPrev == 0)
            return null;
        return MessageData.getMessageData(mIndexPrev);
    }

    /**
     * Returns an instance of the Message that's successor of this one in the linked list of Messages of this Conversation
     * @return
     * @throws StorageFileException
     */
    public MessageData getNextMessageData() throws StorageFileException {
        if (mIndexNext == 0)
            return null;
        return MessageData.getMessageData(mIndexNext);
    }

    /**
     * Returns first message part in the linked list of MessageParts.
     * Should not be public - Message has API for making this seamlessly
     * @return
     * @throws StorageFileException
     */
    MessageDataPart getFirstMessageDataPart() throws StorageFileException {
        if (mIndexMessageParts == 0)
            return null;
        return MessageDataPart.getMessageDataPart(mIndexMessageParts);
    }

    /**
     * Replaces assigned message parts with given list.
     *
     * @param list the list
     * @throws StorageFileException the storage file exception
     */
    void assignMessageDataParts(ArrayList<MessageDataPart> list) throws StorageFileException {
        // delete all previous message parts
        long indexFirstInStack = getIndexMessageParts();
        while (indexFirstInStack != 0) {
            MessageDataPart msgPart = MessageDataPart.getMessageDataPart(indexFirstInStack);
            indexFirstInStack = msgPart.getIndexNext();
            msgPart.delete();
        }

        // add new ones
        for (int i = 0; i < list.size(); ++i) {
            MessageDataPart msgPart = list.get(i);

            // parent
            msgPart.setIndexParent(this.mEntryIndex);

            // previous pointer
            if (i > 0)
                msgPart.setIndexPrev(list.get(i - 1).getEntryIndex());
            else
                msgPart.setIndexPrev(0L);

            // next pointer
            if (i < list.size() - 1)
                msgPart.setIndexNext(list.get(i + 1).getEntryIndex());
            else
                msgPart.setIndexNext(0L);

            msgPart.saveToFile();
        }

        // update pointer in the conversation 
        if (list.size() > 0)
            this.setIndexMessageParts(list.get(0).getEntryIndex());
        else
            this.setIndexMessageParts(0L);
        this.saveToFile();
    }

    /**
     * Delete Message and all the MessageParts it controls.
     *
     * @throws StorageFileException the storage file exception
     */
    public void delete() throws StorageFileException {
        MessageData prev = this.getPreviousMessageData();
        MessageData next = this.getNextMessageData();

        if (prev != null) {
            // this is not the first message in the list
            // update the previous one
            prev.setIndexNext(this.getIndexNext());
            prev.saveToFile();
        } else {
            // this IS the first message in the list
            // update parent
            Conversation parent = this.getParent();
            parent.setIndexMessages(this.getIndexNext());
            parent.saveToFile();
        }

        // update next one
        if (next != null) {
            next.setIndexPrev(this.getIndexPrev());
            next.saveToFile();
        }

        // delete all of the MessageParts
        MessageDataPart part = getFirstMessageDataPart();
        while (part != null) {
            part.delete();
            part = getFirstMessageDataPart();
        }

        // delete this message
        Empty.replaceWithEmpty(mEntryIndex);

        // remove from cache
        synchronized (cacheMessageData) {
            cacheMessageData.remove(this);
        }

        // make this instance invalid
        this.mEntryIndex = -1L;
    }

    // MESSAGE HIGH LEVEL

    /**
     * Returns the data assigned to message part at given index.
     *
     * @param index the index
     * @return the part data
     * @throws StorageFileException the storage file exception
     */
    public byte[] getPartData(int index) throws StorageFileException {
        if (index == 0)
            return this.getMessageBody();
        else {
            --index;
            MessageDataPart part = getFirstMessageDataPart();
            while (part != null) {
                if (index-- == 0)
                    return part.getMessageBody();
                part = part.getNextMessageDataPart();
            }
        }
        throw new IndexOutOfBoundsException();
    }

    /**
     * Adds/removes message parts so that there is exactly given number of them
     * (There is always at least one)
     * @param count
     * @throws StorageFileException
     */
    public void setNumberOfParts(int count) throws StorageFileException {
        --count; // count the first part

        MessageDataPart temp = null, part = getFirstMessageDataPart();
        while (count > 0 && part != null) {
            part.setMessageBody(new byte[0]);
            part.setDeliveredPart(false);
            part.saveToFile();

            --count;
            temp = part;
            part = part.getNextMessageDataPart();
        }

        if (count > 0 && part == null) {
            // we need to add more
            while (count-- > 0) {
                part = MessageDataPart.createMessageDataPart();
                // parent
                part.setIndexParent(this.mEntryIndex);
                // pointers
                if (temp == null) {
                    // this is the first one in list
                    part.setIndexPrev(0L);
                    this.setIndexMessageParts(part.getEntryIndex());
                    this.saveToFile();
                } else {
                    part.setIndexPrev(temp.getEntryIndex());
                    temp.setIndexNext(part.getEntryIndex());
                    temp.saveToFile();
                }
                part.setIndexNext(0);
                // save and move to next
                if (count <= 0) // otherwise will be saved in the next run
                    part.saveToFile();
                temp = part;
            }

        } else if (count <= 0 && part != null) {
            // we need to remove some
            while (part != null) {
                temp = part.getNextMessageDataPart();
                part.delete();
                part = temp;
            }
        }
    }

    /**
     * Returns message part of given index (only for indices > 0)
     * @param index
     * @return
     * @throws StorageFileException
     */
    private MessageDataPart getMessageDataPart(int index) throws StorageFileException {
        if (index <= 0)
            throw new IndexOutOfBoundsException();
        else {
            --index; // for the first part
            MessageDataPart part = this.getFirstMessageDataPart();
            while (part != null && index > 0) {
                part = part.getNextMessageDataPart();
                index--;
            }

            if (part != null)
                return part;
            else
                throw new IndexOutOfBoundsException();
        }
    }

    /**
     * Sets data to given message part.
     *
     * @param index the index
     * @param data the data
     * @throws StorageFileException the storage file exception
     */
    public void setPartData(int index, byte[] data) throws StorageFileException {
        // if it's too long, just cut it
        if (data.length > LENGTH_MESSAGEBODY)
            data = LowLevel.cutData(data, 0, LENGTH_MESSAGEBODY);

        if (index == 0) {
            this.setMessageBody(data);
            this.saveToFile();
        } else {
            MessageDataPart part = getMessageDataPart(index);
            part.setMessageBody(data);
            part.saveToFile();
        }
    }

    /**
     * Returns whether given message part was delivered.
     *
     * @param index the index
     * @return the part delivered
     * @throws StorageFileException the storage file exception
     */
    public boolean getPartDelivered(int index) throws StorageFileException {
        if (index == 0)
            return this.getDeliveredPart();
        else
            return getMessageDataPart(index).getDeliveredPart();
    }

    /**
     * Sets whether given message part was delivered.
     *
     * @param index the index
     * @param delivered the delivered
     * @throws StorageFileException the storage file exception
     */
    public void setPartDelivered(int index, boolean delivered) throws StorageFileException {
        if (index == 0) {
            this.setDeliveredPart(delivered);
            this.saveToFile();
        } else {
            MessageDataPart part = getMessageDataPart(index);
            part.setDeliveredPart(delivered);
            part.saveToFile();
        }
    }

    // GETTERS / SETTERS

    long getEntryIndex() {
        return mEntryIndex;
    }

    void setDeliveredPart(boolean deliveredPart) {
        this.mDeliveredPart = deliveredPart;
    }

    boolean getDeliveredPart() {
        return mDeliveredPart;
    }

    public void setDeliveredAll(boolean deliveredAll) {
        this.mDeliveredAll = deliveredAll;
    }

    public boolean getDeliveredAll() {
        return mDeliveredAll;
    }

    public void setMessageType(MessageType messageType) {
        this.mMessageType = messageType;
    }

    public MessageType getMessageType() {
        return mMessageType;
    }

    public void setUnread(boolean unread) {
        this.mUnread = unread;
    }

    public boolean getUnread() {
        return mUnread;
    }

    public void setCompressed(boolean compressed) {
        this.mCompressed = compressed;
    }

    public boolean getCompressed() {
        return mCompressed;
    }

    public void setAscii(boolean ascii) {
        this.mAscii = ascii;
    }

    public boolean getAscii() {
        return mAscii;
    }

    void setMessageBody(byte[] messageBody) {
        this.mMessageBody = messageBody;
    }

    byte[] getMessageBody() {
        return mMessageBody;
    }

    public void setTimeStamp(DateTime timeStamp) {
        this.mTimeStamp = timeStamp;
    }

    public DateTime getTimeStamp() {
        return mTimeStamp;
    }

    long getIndexMessageParts() {
        return mIndexMessageParts;
    }

    void setIndexMessageParts(long indexMessageParts) {
        if (indexMessageParts > 0xFFFFFFFFL || indexMessageParts < 0L)
            throw new IndexOutOfBoundsException();

        this.mIndexMessageParts = indexMessageParts;
    }

    long getIndexPrev() {
        return mIndexPrev;
    }

    void setIndexPrev(long indexPrev) {
        if (indexPrev > 0xFFFFFFFFL || indexPrev < 0L)
            throw new IndexOutOfBoundsException();

        this.mIndexPrev = indexPrev;
    }

    long getIndexNext() {
        return mIndexNext;
    }

    void setIndexNext(long indexNext) {
        if (indexNext > 0xFFFFFFFFL || indexNext < 0L)
            throw new IndexOutOfBoundsException();

        this.mIndexNext = indexNext;
    }

    void setIndexParent(long indexParent) {
        this.mIndexParent = indexParent;
    }

    long getIndexParent() {
        return mIndexParent;
    }
}