/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package plugins.Freetalk;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import plugins.Freetalk.Message.Attachment;
import plugins.Freetalk.MessageList.MessageReference;
import plugins.Freetalk.exceptions.DuplicateBoardException;
import plugins.Freetalk.exceptions.DuplicateMessageException;
import plugins.Freetalk.exceptions.DuplicateMessageListException;
import plugins.Freetalk.exceptions.InvalidParameterException;
import plugins.Freetalk.exceptions.NoSuchBoardException;
import plugins.Freetalk.exceptions.NoSuchMessageException;
import plugins.Freetalk.exceptions.NoSuchMessageListException;
import com.db4o.ObjectSet;
import com.db4o.ext.ExtObjectContainer;
import com.db4o.query.Query;
import freenet.keys.FreenetURI;
import freenet.support.Executor;
import freenet.support.Logger;
/**
* @author xor
*
*/
public abstract class MessageManager implements Runnable {
protected final MessageManager self = this;
protected ExtObjectContainer db;
protected Executor mExecutor;
protected IdentityManager mIdentityManager;
public MessageManager(ExtObjectContainer myDB, Executor myExecutor, IdentityManager myIdentityManager) {
assert(myDB != null);
assert(myIdentityManager != null);
db = myDB;
mExecutor = myExecutor;
mIdentityManager = myIdentityManager;
}
/**
* For being used in JUnit tests to run without a node.
*/
public MessageManager(ExtObjectContainer myDB, IdentityManager myIdentityManager) {
db = myDB;
mExecutor = null;
mIdentityManager = myIdentityManager;
}
/**
* This is the primary function for posting messages.
*
* @param myParentMessage The message to which the new message is a reply. Null if the message should be a thread.
* @param myBoards The boards to which the new message should be posted. Has to contain at least one board.
* @param myReplyToBoard The board to which replies to this message should be sent. This is just a recommendation. Notice that it should be contained in myBoards. Can be null.
* @param myAuthor The author of the new message. Cannot be null.
* @param myTitle The subject of the new message. Cannot be null or empty.
* @param myDate The UTC time of the message. Null to use the current time.
* @param myText The body of the new message. Cannot be null.
* @param myAttachments The Attachments of the new Message. See Message.Attachment. Set to null if the message has none.
* @return The new message.
* @throws InvalidParameterException Invalid board names, invalid title, invalid body.
* @throws Exception
*/
public abstract OwnMessage postMessage(Message myParentMessage, Set myBoards, Board myReplyToBoard, FTOwnIdentity myAuthor,
String myTitle, Date myDate, String myText, List myAttachments) throws InvalidParameterException, Exception;
public synchronized OwnMessage postMessage(Message myParentMessage, Set myBoards, String myReplyToBoard, FTOwnIdentity myAuthor,
String myTitle, Date myDate, String myText, List myAttachments) throws Exception {
/* FIXME: Instead of always creating the boards, notify the user that they do not exist and ask if he made a typo */
HashSet boardSet = new HashSet();
for (Iterator i = myBoards.iterator(); i.hasNext(); ) {
String boardName = i.next();
Board board = getOrCreateBoard(boardName);
boardSet.add(board);
}
Board replyToBoard = null;
if (myReplyToBoard != null) {
replyToBoard = getOrCreateBoard(myReplyToBoard);
}
return postMessage(myParentMessage, boardSet, replyToBoard, myAuthor, myTitle, myDate, myText, myAttachments);
}
@SuppressWarnings("unchecked")
public synchronized int countUnsentMessages() {
/* FIXME: This is not fully synchronized: MessageInserter calls OwnMessage.wasInserted() to mark a message as inserted and that
* function synchronizes on the OwnMessage object. */
Query q = db.query();
q.constrain(OwnMessage.class);
q.descend("mRealURI").constrain(null).identity();
int unsentCount = q.execute().size();
q = db.query();
q.constrain(OwnMessageList.class);
q.descend("iWasInserted").constrain(false);
ObjectSet notInsertedLists = q.execute();
for(OwnMessageList list : notInsertedLists)
unsentCount += list.getMessageCount();
return unsentCount;
}
public synchronized void onMessageReceived(Message message) {
try {
get(message.getID());
Logger.debug(this, "Downloaded a message which we already have: " + message.getURI());
}
catch(NoSuchMessageException e) {
try {
message.initializeTransient(db, this);
message.store();
for(Board board : message.getBoards())
board.addMessage(message);
for(MessageReference ref : getAllReferencesToMessage(message.getID()))
ref.setMessageWasDownloadedFlag();
}
catch(Exception ex) {
/* FIXME: Delete the message if this happens. */
Logger.error(this, "Exception while storing a downloaded message", ex);
}
}
}
public synchronized void onMessageListReceived(MessageList list) {
try {
getMessageList(list.getID());
Logger.debug(this, "Downloaded a MessageList which we already have: " + list.getURI());
}
catch(NoSuchMessageListException e) {
list.initializeTransient(db, this);
list.store();
}
}
/**
* Abstract because we need to store an object of a child class of MessageList which is chosen dependent on which implementation of the
* messging system we are using.
*/
public abstract void onMessageListFetchFailed(FTIdentity author, FreenetURI uri, MessageList.MessageListFetchFailedReference.Reason reason);
public synchronized void onMessageFetchFailed(MessageReference messageReference, MessageList.MessageFetchFailedReference.Reason reason) {
if(reason == MessageList.MessageFetchFailedReference.Reason.DataNotFound) {
/* TODO: Handle DNF in some reasonable way. Mark the Messages as unavailable after a certain amount of retries maybe */
return;
}
try {
get(messageReference.getMessageID());
Logger.debug(this, "Trying to mark a message as 'downlod failed' which we actually have: " + messageReference.getURI());
}
catch(NoSuchMessageException e) {
try {
MessageList.MessageFetchFailedReference failedMarker = new MessageList.MessageFetchFailedReference(messageReference, reason);
failedMarker.initializeTransient(db);
failedMarker.store();
for(MessageReference r : getAllReferencesToMessage(messageReference.getMessageID()))
r.setMessageWasDownloadedFlag();
Logger.debug(this, "Marked message as download failed with reason " + reason + ": " + messageReference.getURI());
}
catch(Exception ex) {
Logger.error(this, "Exception while marking a not-downloadable messge", ex);
}
}
}
/**
* Get a list of all MessageReference objects to the given message ID. References to OwnMessage are not returned.
* Used to mark the references to a message which was downloaded as downloaded.
*/
private Iterable getAllReferencesToMessage(final String id) {
return new Iterable() {
@SuppressWarnings("unchecked")
public Iterator iterator() {
return new Iterator() {
private Iterator iter;
{
Query query = db.query();
query.constrain(MessageList.MessageReference.class);
query.constrain(OwnMessageList.OwnMessageReference.class).not();
query.descend("mMessageID").constrain(id);
/* FIXME: This function should only return MessageReferences which are for a board which an OwnIdentity wants to read and from
* as specified above. This has to be implemented yet. */
/* FIXME: Order the message references randomly with some trick. */
iter = query.execute().iterator();
}
public boolean hasNext() {
return iter.hasNext();
}
public MessageList.MessageReference next() {
MessageList.MessageReference next = iter.next();
next.initializeTransient(db);
return next;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
};
}
/**
* Get a message by its URI. The transient fields of the returned message will be initialized already.
* This will NOT return OwnMessage objects. Your own messages will be returned by this function as soon as they have been downloaded.
* @throws NoSuchMessageException
*/
public Message get(FreenetURI uri) throws NoSuchMessageException {
/* return get(Message.getIDFromURI(uri)); */
throw new UnsupportedOperationException("Getting a message by it's URI is inefficient compared to getting by ID. Please only repair this function if absolutely unavoidable.");
}
/**
* Get a message by its ID. The transient fields of the returned message will be initialized already.
* This will NOT return OwnMessage objects. Your own messages will be returned by this function as soon as they have been downloaded as
* if they were normal messages of someone else.
* @throws NoSuchMessageException
*/
@SuppressWarnings("unchecked")
public synchronized Message get(String id) throws NoSuchMessageException {
Query query = db.query();
query.constrain(Message.class);
query.constrain(OwnMessage.class).not();
query.descend("mID").constrain(id);
ObjectSet result = query.execute();
if(result.size() > 1)
throw new DuplicateMessageException();
if(result.size() == 0)
throw new NoSuchMessageException(id);
Message m = result.next();
m.initializeTransient(db, this);
return m;
}
/**
* Get a MessageList by its ID. The transient fields of the returned MessageList will be initialized already.
* This will NOT return OwnMessageList objects. Your own message lists will be returned by this function as soon as they have
* been downloaded as if they were normal message lists of someone else.
* @throws NoSuchMessageListException
*/
@SuppressWarnings("unchecked")
public synchronized MessageList getMessageList(String id) throws NoSuchMessageListException {
Query query = db.query();
query.constrain(MessageList.class);
query.constrain(OwnMessageList.class).not();
query.descend("mID").constrain(id);
ObjectSet result = query.execute();
if(result.size() > 1)
throw new DuplicateMessageListException();
if(result.size() == 0)
throw new NoSuchMessageListException(id);
MessageList list = result.next();
list.initializeTransient(db, this);
return list;
}
@SuppressWarnings("unchecked")
public synchronized OwnMessageList getOwnMessageList(String id) throws NoSuchMessageListException {
Query query = db.query();
query.constrain(OwnMessageList.class);
query.descend("mID").constrain(id);
ObjectSet result = query.execute();
if(result.size() > 1)
throw new DuplicateMessageListException();
if(result.size() == 0)
throw new NoSuchMessageListException(id);
OwnMessageList list = result.next();
list.initializeTransient(db, this);
return list;
}
public OwnMessage getOwnMessage(FreenetURI uri) throws NoSuchMessageException {
/* return getOwnMessage(Message.getIDFromURI(uri)); */
throw new UnsupportedOperationException("Getting a message by it's URI is inefficient compared to getting by ID. Please only repair this function if absolutely unavoidable.");
}
@SuppressWarnings("unchecked")
public synchronized OwnMessage getOwnMessage(String id) throws NoSuchMessageException {
Query query = db.query();
query.constrain(OwnMessage.class);
query.descend("mID").constrain(id);
ObjectSet result = query.execute();
if(result.size() > 1)
throw new DuplicateMessageException();
if(result.size() == 0)
throw new NoSuchMessageException(id);
OwnMessage m = result.next();
m.initializeTransient(db, this);
return m;
}
/**
* Get a board by its name. The transient fields of the returned board will be initialized already.
* @throws NoSuchBoardException
*/
@SuppressWarnings("unchecked")
public synchronized Board getBoardByName(String name) throws NoSuchBoardException {
name = name.toLowerCase();
Query query = db.query();
query.constrain(Board.class);
query.descend("mName").constrain(name);
ObjectSet result = query.execute();
if(result.size() > 1)
throw new DuplicateBoardException();
if(result.size() == 0)
throw new NoSuchBoardException(name);
Board b = result.next();
b.initializeTransient(db, this);
return b;
}
public synchronized Board getOrCreateBoard(String name) throws InvalidParameterException {
name = name.toLowerCase();
Board board;
try {
board = getBoardByName(name);
}
catch(NoSuchBoardException e) {
board = new Board(name);
board.initializeTransient(db, this);
board.store();
}
return board;
}
/**
* For a database Query of result type ObjectSet\, this function provides an iterator. The iterator of the ObjectSet
* cannot be used instead because it will not call initializeTransient() on the boards. The iterator which is returned by this function
* takes care of that.
* Please synchronize on the MessageManager when using this function, it is not synchronized itself.
*/
@SuppressWarnings("unchecked")
protected Iterator generalBoardIterator(final Query q) {
return new Iterator() {
private Iterator iter = q.execute().iterator();
public boolean hasNext() {
return iter.hasNext();
}
public Board next() {
Board next = iter.next();
next.initializeTransient(db, self);
return next;
}
public void remove() {
throw new UnsupportedOperationException("Boards cannot be deleted yet.");
}
};
}
/**
* Get an iterator of all boards. The transient fields of the returned boards will be initialized already.
*/
public synchronized Iterator boardIterator() {
/* FIXME: Accelerate this query. db4o should be configured to keep an alphabetic index of boards */
Query query = db.query();
query.constrain(Board.class);
query.descend("mName").orderDescending();
return generalBoardIterator(query);
}
/**
* Get an iterator of boards which were first seen after the given Date, sorted ascending by the date they were first seen at.
*/
public synchronized Iterator boardIteratorSortedByDate(final Date seenAfter) {
Query query = db.query();
query.constrain(Board.class);
query.descend("mFirstSeenDate").constrain(seenAfter).greater();
query.descend("mFirstSeenDate").orderAscending();
return generalBoardIterator(query);
}
/**
* Get the next index of which a message from the selected identity is not stored.
*/
// public int getUnavailableMessageIndex(FTIdentity messageAuthor) {
// Query q = db.query();
// q.constrain(Message.class);
// q.constrain(OwnMessage.class).not(); /* We also download our own message. This helps the user to spot problems: If he does not see his own messages we can hope that he reports a bug */
// q.descend("mAuthor").constrain(messageAuthor);
// q.descend("mIndex").orderDescending(); /* FIXME: Write a native db4o query which just looks for the maximum! */
// ObjectSet result = q.execute();
//
// return result.size() > 0 ? result.next().getIndex()+1 : 0;
// }
@SuppressWarnings("unchecked")
public synchronized Iterator notInsertedMessageIterator() {
return new Iterator() {
private Iterator iter;
{
Query query = db.query();
query.constrain(OwnMessage.class);
query.descend("mRealURI").constrain(null).identity();
iter = query.execute().iterator();
}
public boolean hasNext() {
return iter.hasNext();
}
public OwnMessage next() {
OwnMessage next = iter.next();
next.initializeTransient(db, self);
return next;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Get a list of not downloaded messages. This function only returns messages which are posted to a board which an OwnIdentity wants to
* receive messages from. However, it might also return messages which are from an author which nobody wants to receive messages from.
* Filtering out unwanted authors is done at MessageList-level: MessageLists are only downloaded from identities which we want to read
* messages from.
*/
@SuppressWarnings("unchecked")
public synchronized Iterator notDownloadedMessageIterator() {
return new Iterator() {
private Iterator iter;
{
Query query = db.query();
query.constrain(MessageList.MessageReference.class);
query.constrain(OwnMessageList.OwnMessageReference.class).not();
query.descend("iWasDownloaded").constrain(false);
/* FIXME: This function should only return MessageReferences which are for a board which an OwnIdentity wants to read and from
* as specified above. This has to be implemented yet. */
/* FIXME: Order the message references randomly with some trick. */
iter = query.execute().iterator();
}
public boolean hasNext() {
return iter.hasNext();
}
public MessageList.MessageReference next() {
MessageList.MessageReference next = iter.next();
next.initializeTransient(db);
return next;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Returns true if the message was not downloaded yet and any of the FTOwnIdentity wants the message.
*/
// protected synchronized boolean shouldDownloadMessage(FreenetURI uri, FTIdentity author) {
// try {
// get(uri);
// return false;
// }
// catch(NoSuchMessageException e) {
// return mIdentityManager.anyOwnIdentityWantsMessagesFrom(author);
// }
// }
public abstract void terminate();
public IdentityManager getIdentityManager() {
return mIdentityManager;
}
}