/*
* 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;
}
}