Package plugins.Freetalk

Source Code of plugins.Freetalk.MessageManager

/* 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.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;

import plugins.Freetalk.IdentityManager.IdentityDeletedCallback;
import plugins.Freetalk.IdentityManager.NewOwnIdentityCallback;
import plugins.Freetalk.Message.Attachment;
import plugins.Freetalk.MessageList.MessageFetchFailedMarker;
import plugins.Freetalk.MessageList.MessageListFetchFailedMarker;
import plugins.Freetalk.MessageList.MessageListID;
import plugins.Freetalk.MessageList.MessageReference;
import plugins.Freetalk.exceptions.DuplicateBoardException;
import plugins.Freetalk.exceptions.DuplicateElementException;
import plugins.Freetalk.exceptions.DuplicateFetchFailedMarkerException;
import plugins.Freetalk.exceptions.DuplicateMessageException;
import plugins.Freetalk.exceptions.DuplicateMessageListException;
import plugins.Freetalk.exceptions.InvalidParameterException;
import plugins.Freetalk.exceptions.NoSuchBoardException;
import plugins.Freetalk.exceptions.NoSuchFetchFailedMarkerException;
import plugins.Freetalk.exceptions.NoSuchIdentityException;
import plugins.Freetalk.exceptions.NoSuchMessageException;
import plugins.Freetalk.exceptions.NoSuchMessageListException;
import plugins.Freetalk.exceptions.NoSuchMessageRatingException;
import plugins.Freetalk.exceptions.NoSuchObjectException;
import plugins.Freetalk.tasks.NewBoardTask;
import plugins.Freetalk.tasks.PersistentTaskManager;
import plugins.Freetalk.tasks.SubscribeToAllBoardsTask;

import com.db4o.ObjectSet;
import com.db4o.ext.ExtObjectContainer;
import com.db4o.query.Query;

import freenet.keys.FreenetURI;
import freenet.node.PrioRunnable;
import freenet.pluginmanager.PluginRespirator;
import freenet.support.CurrentTimeUTC;
import freenet.support.Logger;
import freenet.support.TrivialTicker;
import freenet.support.io.NativeThread;

/**
* The MessageManager is the core connection between the UI and the backend of the plugin:
* It is the entry point for posting messages, obtaining messages, obtaining boards, etc.
*
*
* @author xor (xor@freenetproject.org)
*/
public abstract class MessageManager implements PrioRunnable, NewOwnIdentityCallback, IdentityDeletedCallback {

  protected final IdentityManager mIdentityManager;
 
  protected final Freetalk mFreetalk;
 
  protected final ExtObjectContainer db;
 
  protected final PluginRespirator mPluginRespirator;
 
  private static final int STARTUP_DELAY = Freetalk.FAST_DEBUG_MODE ? (1 * 60 * 1000) : (3 * 60 * 1000);
  private static final int THREAD_PERIOD = Freetalk.FAST_DEBUG_MODE ? (15 * 60 * 1000) : (15 * 60 * 1000);
 
  private static final int PROCESS_NEW_MESSAGES_DELAY = 1 * 60 * 1000;
 
  /**
   * When a {@link Message} fetch fails (DNF for example) the message is marked as fetch failed and the fetch will be retried after a growing amount of time.
   * This is the minimal delay.
   */
  public static final long MINIMAL_MESSAGE_FETCH_RETRY_DELAY = Freetalk.FAST_DEBUG_MODE ? (10 * 60 * 1000) (4 * 60 * 60 * 1000); // TODO: Make configurable.
 
  /**
   * When a {@link Message} fetch fails (DNF for example) the message is marked as fetch failed and the fetch will be retried after a growing amount of time.
   * This is the maximal delay.
   */
  public static final long MAXIMAL_MESSAGE_FETCH_RETRY_DELAY = Freetalk.FAST_DEBUG_MODE ? (30 * 60 * 1000) : (7 * 24 * 60 *60 * 1000); // TODO: Make configurable
 
  public static final int MAXIMAL_MESSAGE_FETCH_RETRY_DELAY_AT_RETRY_COUNT = (int)(Math.log(MAXIMAL_MESSAGE_FETCH_RETRY_DELAY / MINIMAL_MESSAGE_FETCH_RETRY_DELAY) / Math.log(2));
   
  /**
   * When a {@link MessageList} fetch fails (DNF for example) the {@link MessageList} is marked as fetch failed and the fetch will be retried after a
   * growing amount of time. This is the minimal delay.
   * Notice that this only applies to "old" message lists - that is message lists with an edition number lower than the latest successfully fetched edition.
   */
  public static final long MINIMAL_MESSAGELIST_FETCH_RETRY_DELAY = Freetalk.FAST_DEBUG_MODE ? (10 * 60 * 1000) : (4 * 60 * 60 * 1000); // TODO: Make configurable.
 
  /**
   * When a {@link MessageList} fetch fails (DNF for example) the {@link MessageList}  is marked as fetch failed and the fetch will be retried after a
   * growing amount of time. This is the maximal delay.
   * Notice that this only applies to "old" message lists - that is message lists with an edition number lower than the latest successfully fetched edition.
   */
  public static final long MAXIMAL_MESSAGELIST_FETCH_RETRY_DELAY = Freetalk.FAST_DEBUG_MODE ? (30 * 60 * 1000) : (7 * 24 * 60 * 60 * 1000)// TODO: Make configurable.
 
  public static final int MAXIMAL_MESSAGELIST_FETCH_RETRY_DELAY_AT_RETRY_COUNT = (int)(Math.log(MAXIMAL_MESSAGELIST_FETCH_RETRY_DELAY / MINIMAL_MESSAGELIST_FETCH_RETRY_DELAY) / Math.log(2));
 
  private final TrivialTicker mTicker;
  private final Random mRandom;
 
  /* These booleans are used for preventing the construction of log-strings if logging is disabled (for saving some cpu cycles) */
 
  private static transient volatile boolean logDEBUG = false;
  private static transient volatile boolean logMINOR = false;
 
  static {
    Logger.registerClass(MessageManager.class);
  }
 

  public MessageManager(ExtObjectContainer myDB, IdentityManager myIdentityManager, Freetalk myFreetalk, PluginRespirator myPluginRespirator) {
    assert(myDB != null);
    assert(myIdentityManager != null);
    assert(myFreetalk != null);
    assert(myPluginRespirator != null);   
   
    db = myDB;
    mIdentityManager = myIdentityManager;
    mFreetalk = myFreetalk;
    mPluginRespirator = myPluginRespirator;
   
    mTicker = new TrivialTicker(mFreetalk.getPluginRespirator().getNode().executor);
    mRandom = mPluginRespirator.getNode().fastWeakRandom;
   
    mIdentityManager.registerNewOwnIdentityCallback(this);
    mIdentityManager.registerIdentityDeletedCallback(this, true);
  }
 
  /**
   * For being used in JUnit tests to run without a node.
   */
  protected MessageManager(Freetalk myFreetalk) {
    mFreetalk = myFreetalk;
    db = mFreetalk.getDatabase();
    mIdentityManager = mFreetalk.getIdentityManager();
    mPluginRespirator = null;
    mTicker = null;
    mRandom = null;
  }
 
  public int getPriority() {
    return NativeThread.MIN_PRIORITY;
  }
 
  public void run() {
    if(logDEBUG) Logger.debug(this, "Main loop running...");
   
    try {
      // Must be called periodically because they are not called on demand.
      clearExpiredFetchFailedMarkers();
      recheckUnwantedMessages();
      recheckWantedMessages();
    finally {
      long sleepTime = THREAD_PERIOD/2 + mRandom.nextInt(THREAD_PERIOD);
      if(logDEBUG) Logger.debug(this, "Sleeping for " + sleepTime/(60*1000) + " minutes.");
      mTicker.queueTimedJob(this, "Freetalk " + this.getClass().getSimpleName(), sleepTime, false, true);
    }
   
    if(logDEBUG) Logger.debug(this, "Main loop finished.");
  }
 
  private final Runnable mNewMessageProcessor = new PrioRunnable() {
    public int getPriority() {
      return NativeThread.NORM_PRIORITY;
    }
   
    public void run() {
      if(logDEBUG) Logger.debug(MessageManager.this, "Processing new messages...");
     
      boolean success1 = addMessagesToBoards(); // Normally does not fail
     
      // CAN fail because SubscribedBoard.addeMessage() tries to query the Score of the author from WoT and this can
      // fail due to connectivity issues (and currently most likely due to bugs in PluginTalker and especially BlockingPluginTalker!)
      boolean success2 = synchronizeSubscribedBoards();
     
      // If it didn't work we re-schedule it... but not in unit tests, they would infinite loop..
      if(mTicker != null && (!success1 || !success2))
        scheduleNewMessageProcessing();
    }
  };
 
  private void scheduleNewMessageProcessing() {
    if(logDEBUG) Logger.debug(this, "Scheduling new message processing to be run in " + PROCESS_NEW_MESSAGES_DELAY / (60*1000) + " minutes...");
    if(mTicker != null)
      mTicker.queueTimedJob(mNewMessageProcessor, "Freetalk " + this.getClass().getSimpleName(), PROCESS_NEW_MESSAGES_DELAY, false, true);
    else { // For unit tests
      mNewMessageProcessor.run();
    }
  }
 
 
  public void start() {
    if(logDEBUG) Logger.debug(this, "Starting...");
   
    createDefaultBoards();
   
    long startupDelay = STARTUP_DELAY/2 + mRandom.nextInt(STARTUP_DELAY);
    if(logDEBUG) Logger.debug(this, "Main loop will run in " + startupDelay/(60*1000) + " minutes.");
    mTicker.queueTimedJob(this, "Freetalk " + this.getClass().getSimpleName(), startupDelay, false, true);
   
    // It might happen that Freetalk is shutdown after a message has been downloaded and before addMessagesToBoards was called:
    // Then the message will still be stored but not visible in the boards because storing a message and adding it to boards are separate transactions.
    // Therefore, we must call addMessagesToBoards (and synchronizeSubscribedBoards) during startup.
    scheduleNewMessageProcessing();
   
    if(logDEBUG) Logger.debug(this, "Started.");
  }

  public void terminate() {
    if(logDEBUG) Logger.debug(this, "Stopping ...");
    mTicker.shutdown();
    if(logDEBUG) Logger.debug(this, "Stopped.");
  }
 
  /**
   * This is the primary function for posting messages.
   * TODO: Optimization: This probably does not require any synchronization when calling since the storeWithoutComit of Message throws if the referenced objects do not exist anymore.
   *
   * @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 <code>Message.Attachment</code>. 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(MessageURI myParentThreadURI, Message myParentMessage, Set<Board> myBoards, Board myReplyToBoard, OwnIdentity myAuthor,
      String myTitle, Date myDate, String myText, List<Attachment> myAttachments) throws InvalidParameterException, Exception;

  /**
   * TODO: Optimization: This probably does not require any synchronization when calling since the storeWithoutComit of Message throws if the referenced objects do not exist anymore.
   */
  public OwnMessage postMessage(MessageURI myParentThreadURI, Message myParentMessage, Set<String> myBoards, String myReplyToBoard,
      OwnIdentity myAuthor, String myTitle, Date myDate, String myText, List<Attachment> myAttachments) throws Exception {

    HashSet<Board> boardSet = new HashSet<Board>();
    for (Iterator<String> 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(myParentThreadURI, myParentMessage, boardSet, replyToBoard, myAuthor, myTitle, myDate, myText, myAttachments);
  }
 
  @SuppressWarnings("unchecked")
  public synchronized int countUnsentMessages() {
    Query q = db.query();
    q.constrain(OwnMessage.class);
    q.descend("mFreenetURI").constrain(null).identity();
    int unsentCount = 0;
   
    for(OwnMessage m : new Persistent.InitializingObjectSet<OwnMessage>(mFreetalk, q)) {
      // TODO: Remove this workaround for the db4o bug as soon as we are sure that it does not happen anymore.
      if(!m.testFreenetURIisNull()) // Logs an error for us
        continue;
   
      ++unsentCount;
    }
   
    q = db.query();
    q.constrain(OwnMessageList.class);
    q.descend("mWasInserted").constrain(false);
    ObjectSet<OwnMessageList> notInsertedLists = new Persistent.InitializingObjectSet<OwnMessageList>(mFreetalk, q);
    for(OwnMessageList list : notInsertedLists)
      unsentCount += list.getMessageCount();
   
    return unsentCount;
  }
 
  public synchronized int countMessages() {
    final Query q = db.query();
    q.constrain(Message.class);
    // This should use indexes and be O(1) therefore, which I'm not sure about with using "q.constrain(OwnMessage.class).not();"
    return q.execute().size() - countOwnMessages();
  }
 
  public synchronized int countOwnMessages() {
    final Query q = db.query();
    q.constrain(OwnMessage.class);
    return q.execute().size();
  }
 
  public void deleteUnsentMessage(String messageID) throws NoSuchMessageException {
    synchronized(mFreetalk.getMessageInserter()) {
    synchronized(this) {
      final OwnMessage message = getOwnMessage(messageID);
     
      if(message.wasInserted())
        throw new UnsupportedOperationException("The message was inserted already");
     
      mFreetalk.getMessageInserter().abortMessageInsert(message.getID());
     
      deleteMessage(message);
    }
    }
  }

  private synchronized void deleteMessage(Message message) {
    if(!(message instanceof OwnMessage)) { // OwnMessages cannot be rated / added to boards.
   
    for(MessageRating rating : getAllMessageRatings(message)) {
      // We must not undo the effect because we do not want message deletion due to distrust of the author result in the distrust to be undone.
      deleteMessageRatingWithoutRevertingEffect(rating); // This call does a full transaction.
    }
   
    for(Board board : message.getBoards()) {
      synchronized(board) {
      synchronized(message) { // TODO: Check whether we actually need to lock messages. I don't think so.
      synchronized(Persistent.transactionLock(db)) {
      try {
        board.deleteMessage(message);
        message.setLinkedIn(false);
        message.storeAndCommit();
      } catch (NoSuchMessageException e) {
        // The message was not added to the board yet, this is normal
      } catch(RuntimeException e) {
        Persistent.checkedRollbackAndThrow(db, this, e);
      }
      }
      }
      }
     
      for(SubscribedBoard subscribedBoard : subscribedBoardIterator(board.getName())) {
        synchronized(subscribedBoard) {
        synchronized(message) { // TODO: Check whether we actually need to lock messages. I don't think so.
        synchronized(Persistent.transactionLock(db)) {
        try {
          subscribedBoard.deleteMessage(message);
          subscribedBoard.checkedCommit(this);
        } catch (NoSuchMessageException e) {
          // The message was not added to the board yet, this is normal
        } catch(RuntimeException e) {
          Persistent.checkedRollbackAndThrow(db, this, e);
        }
        }
        }
        }
      }
     
    }
   
    }

    synchronized(message) { // TODO: Check whether we actually need to lock messages. I don't think so.
    synchronized(Persistent.transactionLock(db)) { 
      try {
        if(message instanceof OwnMessage) {
          final OwnMessage ownMessage = (OwnMessage)message;
         
          for(OwnMessageList.OwnMessageReference ref : getAllOwnReferencesToMessage(ownMessage.getID())) {
            ((OwnMessageList)ref.getMessageList()).removeMessageWithoutCommit(ownMessage);
          }
         
          // clearParent / clearThread is not neccessary for OwnMessages.
        } else {
        // Clear the "message was downloaded" flags of the references to this message.
        // This is necessary because the following transaction (deletion of the message lists of the identity) might fail and we should
        ///re-download the message if the identity is not deleted.
        for(MessageReference ref : getAllReferencesToMessage(message.getID())) {
          ref.clearMessageWasDownloadedFlag();
          ref.storeWithoutCommit();
        }
       
        for(Message reply : getAllRepliesToMessage(message)) {
          reply.clearParent();
        }
       
        for(Message threadReply : getAllThreadRepliesToMessage(message)) {
          threadReply.clearThread();
        }
        }
       
        message.deleteWithoutCommit();
       
        message.checkedCommit(this);
      }
      catch(RuntimeException e) {
        Persistent.checkedRollbackAndThrow(db, this, e);
      }
    }
    }
  }
 
  /**
   * Deletes all boards which are empty.
   * A board is considered empty if:
   * - There are no subscriptions to it
   * - There are no fetched messages or message lists for it
   * - There are no own messages or own message lists in it
   * @return The list of the deleted boards' names.
   */
  public synchronized ArrayList<String> deleteEmptyBoards() {
    Logger.normal(this, "Attempting to delete empty boards...");
   
    // TODO: Optimization: This might speed things up... or slow them down.
    // addMessagesToBoards();
   
    final ArrayList<String> deletedBoards = new ArrayList<String>();
    final ObjectSet<OwnMessage> notInsertedOwnMessages = notInsertedMessageIterator();
   
    for(final Board board: boardIteratorSortedByName()) { // TODO: Optimization: Implement & use a non-sorting function.
      if(board.hasSubscriptions()) {
        Logger.normal(this, "Not deleting board because it has subscriptions: " + board);
        continue;
      }
     
      // TODO: This is debug code, remove it when we are sure that it does not happen.
      if(subscribedBoardIterator(board.getName()).size() != 0) {
        Logger.error(this, "Board.hasSubscriptions()==false but subscribed boards exist, not deleting: " + board);
        continue;
      }
     
      if(board.getDownloadedMessageCount() != 0) {
        Logger.normal(this, "Not deleting board because getDownloadedMessageCount()!=0: " + board);
        continue;
      }
     
      if(getOwnMessages(board).size() > 0) {
        // TODO: We should provide functionality for deleting them.
        // I did not implement this yet because the goal of this function is to provide the ability to delete large amounts of
        // boards which were created as spam, the user is not very likely to have posted in them. Yet we WILL need provide the
        // ability to delete boards which only contain own messages.
        Logger.warning(this, "Cannot delete board because there are own messages in it: " + board);
        continue;
      }

      { // Check for not inserted own messages...
        boolean containsNotInsertedOwnMessage = false;
       
        for(final OwnMessage notInserted : notInsertedOwnMessages) {
          if(board.contains(notInserted)) {
            containsNotInsertedOwnMessage = true;
            break;
          }
        }
       
        if(containsNotInsertedOwnMessage) {
          Logger.warning(this, "Cannot delete board because there are own messages in it: " + board);
          continue;
        }
      }
     
      if(getDownloadableMessageCount(board) > 0) {
        Logger.normal(this, "Not deleting board because it is referenced in message lists: " + board);
        continue;
      }
     
      synchronized(Persistent.transactionLock(db)) {
        try {
          Logger.normal(this, "Deleting empty board " + board);
          board.deleteWithoutCommit();
          board.checkedCommit(this);
          deletedBoards.add(board.getName());
        } catch(RuntimeException e) {
          Persistent.checkedRollback(db, this, e);
        }
      }
    }
   
    return deletedBoards;
  }
 
  /**
   * Called by the IdentityManager after a new Identity has been stored to the database and before committing the transaction.
   * The IdentityManager and PersistentTaskManager are locked when this function is called.
   *
   * Creates a SubscribeToAllBoardsTask for the identity if auto-subscription to boards is enabled.
   */
  public void onNewOwnIdentityAdded(OwnIdentity identity) {
    if(identity.wantsAutoSubscribeToNewBoards()) {
      // We cannot subscribe to the boards here because we lack the lock to the MessageManager
      mFreetalk.getTaskManager().storeTaskWithoutCommit(new SubscribeToAllBoardsTask(identity));
      mFreetalk.getTaskManager().processTasksSoon();
    }
  }
 
  /**
   * Called by the {@link IdentityManager} before an identity is deleted from the database.
   *
   * Deletes any messages and message lists referencing to it and commits the transaction.
   */
  public synchronized void beforeIdentityDeletion(Identity identity) {
    if(logDEBUG) Logger.debug(this, "Deleting all objects of identity " + identity);
    // We use multiple transactions here: We cannot acquire the Persistent.transactionLock(db) before deleteMessageRatting, board.deleteWithoutCommit and
    // deleteMessage. Each of them synchronize on message ratings / boards, therefore we must acquire the db.lock after synchronizing on each object.
    // TODO: Check whether this can result in bad side effects. IMHO it cannot.
    // If it can, add some mechanism similar to addMessagesToBoards()/synchronizeSubscribedBoards which handles half-deleted identities.
   
    if(identity instanceof OwnIdentity) {
      final OwnIdentity ownId = (OwnIdentity)identity;
      for(final MessageRating messageRating : getAllMessageRatingsBy(ownId)) {
        // We must not undo the effect because we do not want message deletion due to distrust of the author result in the distrust to be undone.
        deleteMessageRatingWithoutRevertingEffect(messageRating); // This does a full transaction and commits it.
      }
           
      for(SubscribedBoard board : subscribedBoardIteratorSortedByName((OwnIdentity)identity)) { // TODO: Optimization: Use a non-sorting function.
        unsubscribeFromBoard(ownId, board); // This does a full transaction and commits it.
      }
    }

    for(Message message : getMessagesBy(identity)) {
      deleteMessage(message); // This does a full transaction and commits it.
    }

    synchronized(Persistent.transactionLock(db)) {
      try {
        for(MessageList messageList : getMessageListsBy(identity)) {
          messageList.deleteWithoutCommit();
          // We do not call onMessageListDeleted for the IdentityStatistics since the statistics object will be deleted within
          // this transaction anyway.
        }

        if(identity instanceof OwnIdentity) {
          final OwnIdentity ownId = (OwnIdentity)identity;
         
          // TODO: Make sure that deleteMessage also works well for OwnMessages and use it.
         
          for(final OwnMessage message : getOwnMessagesBy(ownId)) {
            message.deleteWithoutCommit();
           
            // We don't need to delete it from the boards because own messages are not being added to boards
            // We don't need to set parent/thread pointers to this message to null because parent/thread pointers are never set to an OwnMessage
          }

          for(final OwnMessageList messageList : getOwnMessageListsBy(ownId)) {
            messageList.deleteWithoutCommit();
          }
        }
       
        try {
          getIdentityStatistics(identity).deleteWithoutCommit();
        } catch(NoSuchObjectException e) {}

        if(logDEBUG) Logger.debug(this, "beforeIdentityDeletion finished for " + identity);
        Persistent.checkedCommit(db, this);
      }
      catch(RuntimeException e) {
        Persistent.checkedRollbackAndThrow(db, this, e);
      }
    }
  }
 
  /**
   * Called by the {@link MessageListInserter} implementation when the insertion of an {@link OwnMessageList} is to be started.
   * Has to be called before any data is pulled from the {@link OwnMessageList}: It locks the list so no further messages can be added.
   * Further, you have to acquire the lock on this MessageManager before calling this function and while taking data from the {@link OwnMessageList} since
   * the lock of the message list could be cleared and further messages could be added if you do not.
   *
   * @param uri The URI of the {@link OwnMessageList}.
   * @throws NoSuchMessageListException If there is no such {@link OwnMessageList}.
   */
  public synchronized void onMessageListInsertStarted(OwnMessageList list) {
    synchronized(Persistent.transactionLock(db)) {
      try {
        list.throwIfNotStored();
        list.beginOfInsert();
        Persistent.checkedCommit(db, this);
      }
      catch(RuntimeException e) {
        // This function MUST NOT succeed if the list was not marked as being inserted: Otherwise messages could be added to the list while it is
        // being inserted already, resulting in the messages being marked as successfully inserted but not being visible to anyone!
        Persistent.checkedRollbackAndThrow(db, this, e);
      }
    }
  }
 
  /**
   * Called by the {@link MessageListInserter} implementation when the insertion of an {@link OwnMessageList} succeeded. Marks the list as inserted.
   *
   * @param uri The URI of the {@link OwnMessageList}.
   * @throws NoSuchMessageListException If there is no such {@link OwnMessageList}.
   */
  public synchronized void onMessageListInsertSucceeded(FreenetURI uri) throws NoSuchMessageListException {
    synchronized(Persistent.transactionLock(db)) {
      try {
        OwnMessageList list = getOwnMessageList(MessageListID.construct(uri).toString());
        list.markAsInserted();
        list.checkedCommit(this);
      }
      catch(RuntimeException e) {
        Persistent.checkedRollbackAndThrow(db, this, e);
      }
    }
  }
 
  /**
   * Called by the {@link MessageListInserter} implementation when the insertion of an {@link OwnMessageList} fails.
   * Clears the "being inserted"-flag of the given message list.
   *
   * @param uri The URI of the {@link OwnMessageList}.
   * @param collision Whether the index of the {@link OwnMessageList} was already taken. If true, the index of the message list is incremented.
   * @throws NoSuchMessageListException If there is no such {@link OwnMessageList}.
   */
  public abstract void onMessageListInsertFailed(FreenetURI uri, boolean collision) throws NoSuchMessageListException;
 
  public synchronized void onMessageReceived(Message message) {
    message.initializeTransient(mFreetalk);
   
    boolean wasDownloadedAlready;
    try {
      message = get(message.getID());
      wasDownloadedAlready = true;
      Logger.error(this, "Downloaded a message which we already have: " + message);
    }
    catch(NoSuchMessageException e) {
      wasDownloadedAlready = false;
    }
   
    synchronized(Persistent.transactionLock(db)) {
      try {
        if(!wasDownloadedAlready) {
          message.storeWithoutCommit();
        }
       
        // We also try to mark the message as downloaded if it was fetched already to ensure that its not being fetched over and over again.

        for(MessageReference ref : getAllReferencesToMessage(message.getID())) {
          try {
            getMessageFetchFailedMarker(ref).deleteWithoutCommit();
            Logger.normal(this, "Deleted a FetchFailedMarker for the message.");
          } catch(NoSuchFetchFailedMarkerException e1) { }
         
          ref.setMessageWasDownloadedFlag();
          ref.storeWithoutCommit();
        }
       
        if(wasDownloadedAlready) {
          Persistent.checkedCommit(db, this);
          return;
        }
       
        try {
          message.setThread(get(message.getThreadID())); // Calls storeWithoutCommit
        } catch(NoSuchMessageException e) {
          // The message has no thread ID or the parent thread was not downloaded yet
        }
       
        try {
          message.setParent(get(message.getParentID()))// Calls storeWithoutCommit
        } catch(NoSuchMessageException e) {
          // The message has no parent ID or the parent message was not downloaded yet
        }
       
        for(Message reply : getAllRepliesToMessage(message.getID())) {
          reply.setParent(message); // Calls storeWithoutCommit
        }
       
        for(Message threadReply : getAllThreadRepliesToMessage(message.getID())) {
          threadReply.setThread(message); // Calls storeWithoutCommit
        }

        Persistent.checkedCommit(db, this);
      }
      catch(Exception ex) {
        Persistent.checkedRollback(db, this, ex);
      }
    }
   
    scheduleNewMessageProcessing();
  }
 
  /**
   * @return True If adding new messages succeeded, false if not.
   */
  private synchronized boolean addMessagesToBoards() {
    Logger.normal(this, "Adding messages to boards...");
   
    Query q = db.query();
    q.constrain(Message.class);
    q.descend("mWasLinkedIn").constrain(false);
    q.constrain(OwnMessage.class).not();
    ObjectSet<Message> invisibleMessages = new Persistent.InitializingObjectSet<Message>(mFreetalk, q);
   
    boolean allSuccessful = true;
   
    for(Message message : invisibleMessages) {
      boolean messageSuccessful = true;
     
      for(Board board : message.getBoards()) {
        synchronized(board) {
        synchronized(message) {
        synchronized(Persistent.transactionLock(db)) {
          try {
            if(logDEBUG) Logger.debug(this, "Adding message to board: " + message);
            board.addMessage(message);
            board.checkedCommit(this);
          }
          catch(Exception e) {
            messageSuccessful = false;
            Persistent.checkedRollback(db, this, e);     
          }
        }
        }
        }
      }
     
      if(messageSuccessful) {
        synchronized(message) {
        synchronized(Persistent.transactionLock(db)) {
          message.setLinkedIn(true);
          message.storeAndCommit();
        }
        }
      } else {
        allSuccessful = false;
      }
    }
   
    Logger.normal(this, "Finished adding messages to boards.");
   
    return allSuccessful;
  }
 
  private synchronized boolean synchronizeSubscribedBoards() {
    Logger.normal(this, "Synchronizing subscribed boards...");

    boolean success = true;
   
    for(SubscribedBoard board : subscribedBoardIterator()) {
      // No need to lock the parent board because we do not modify it and we've locked the MessageManager which prevents writes to the parent board.
      // synchronized(board.getParentBoard()) {
      synchronized(board) {
      synchronized(Persistent.transactionLock(db)) {
        try {
          board.synchronizeWithoutCommit();
          board.checkedCommit(this);
        }
        catch(Exception e) {
          success = false;
          Persistent.checkedRollback(db, this, e);
        }
      }
      }
      // }
     
      Thread.yield();
    }
   
    Logger.normal(this, "Finished synchronizing subscribed boards.");
    return success;
  }
 
  public synchronized void onMessageListReceived(MessageList list) {
    list.initializeTransient(mFreetalk);
   
    // It's not possible to keep the synchronization order of message lists to synchronize BEFORE synchronizing on Persistent.transactionLock(db) some places so we
    // do not synchronize here.
    // And in this function we don't need to synchronize on it anyway because it is not known to anything which might modify it anyway.
    // In general, due to those issues the functions which modify message lists just use the message manager as synchronization object.
    //synchronized(list) {
    MessageListFetchFailedMarker marker;
    MessageList ghostList;

    try {
      marker = getMessageListFetchFailedMarker(list.getID());
    }
    catch(NoSuchFetchFailedMarkerException e) {
      marker = null;
    }
   
    try {
      ghostList = getMessageList(list.getID());
     
      if(marker == null) {
        if(logDEBUG) Logger.debug(this, "Downloaded a MessageList which we already have: " + list);
        return;
      }

    } catch(NoSuchMessageListException e) {
      ghostList = null;
    }

    synchronized(Persistent.transactionLock(db)) {
        try {
          if(marker != null) {
            // TODO: This is usually an error, but it is no error if the fetched list is the only list which there was a ghost list for (i.e. edition 0)
            // Re-think about the conditions when this is an error and log an error then, not only a warning.
            Logger.warning(this, "MessageList was fetched even though a FetchFailedMarker existed for it! Deleting the marker: " + marker);
            marker.deleteWithoutCommit();
            marker = null;
          }
           
          if(ghostList != null) { // We do not nest it with the above if() for readability / robustness.
            Logger.warning(this, "MessageList was fetched even though a ghost list existed for it! Deleting the ghost list: " + ghostList);
            ghostList.deleteWithoutCommit();
            ghostList = null;
            // We don't call onMessageListDeleted on the IdentityStatistics since we will call onMessageListFetched for the
            // list with the same ID in this transaction anyway.
          }
         
          // TODO: Optimization: This is debug code which was added on 2011-02-13 for tracking down 0004739, it can be removed after some months if this does not happen.
          // and/or if the bug tracker entry is marked as fixed
          try {
            getMessageList(list.getID());
            throw new RuntimeException("Duplicate list would have been created, ghostList.deleteWithoutCommit() did not work!");
          } catch(NoSuchMessageListException e) {}
         
          list.storeWithoutCommit();
         
          // Mark existing messages as fetched... Can happen if a message is listed in multiple lists.
          for(MessageReference ref : list) {
            try {
              get(ref.getMessageID());
              ref.setMessageWasDownloadedFlag();
              ref.storeWithoutCommit();
            } catch(NoSuchMessageException e) {}
          }
         
          final IdentityStatistics stats = getOrCreateIdentityStatistics(list.getAuthor());
          stats.onMessageListFetched(list);
          stats.storeWithoutCommit();
         
          list.checkedCommit(this);
        }
        catch(RuntimeException ex) {
          Persistent.checkedRollback(db, this, ex);
        }
    }
    //}
  }
 
  /**
   * Abstract because we need to store an object of a child class of MessageList which is chosen dependent on which implementation of the
   * messaging system we are using.
   */
  public abstract void onMessageListFetchFailed(Identity author, FreenetURI uri, FetchFailedMarker.Reason reason);
 
  public synchronized void onMessageFetchFailed(MessageReference messageReference, FetchFailedMarker.Reason reason) {
    try {
      get(messageReference.getMessageID());
      if(logDEBUG) Logger.debug(this, "Trying to mark a message as 'download failed' which we actually have: " + messageReference.getURI());
    }
    catch(NoSuchMessageException e) {
      synchronized(Persistent.transactionLock(db)) {
      try {       
        Date date = CurrentTimeUTC.get();
       
        for(MessageReference ref : getAllReferencesToMessage(messageReference.getMessageID())) {
          MessageList.MessageFetchFailedMarker failedMarker;
         
          try {
            failedMarker = getMessageFetchFailedMarker(ref);
            failedMarker.setReason(reason);
            failedMarker.incrementNumberOfRetries();
            Date dateOfNextRetry = calculateDateOfNextMessageFetchRetry(failedMarker.getReason(), date, failedMarker.getNumberOfRetries());
            failedMarker.setDate(date);
            failedMarker.setDateOfNextRetry(dateOfNextRetry);
          } catch(NoSuchFetchFailedMarkerException e1) {
            Date dateOfNextRetry = calculateDateOfNextMessageFetchRetry(reason, date, 0);
            failedMarker = new MessageList.MessageFetchFailedMarker(ref, reason, date, dateOfNextRetry);
            failedMarker.initializeTransient(mFreetalk);
          }
         
          ref.setMessageWasDownloadedFlag();
          ref.storeWithoutCommit();
          // failedMarker.setAllowRetryNow(false); // setDateOfNextRetry does this for us
          failedMarker.storeWithoutCommit();
       
         
          Logger.normal(this, "Marked message as download failed with reason " + reason + " (next retry is at " + failedMarker.getDateOfNextRetry()
              + ", number of retries: " + failedMarker.getNumberOfRetries() + "): "
              +  messageReference.getURI());
        }
       
       
        Persistent.checkedCommit(db, this);
      }
      catch(RuntimeException ex) {
        Persistent.checkedRollback(db, this, ex);
      }
      }
    }
  }
 
  protected Date calculateDateOfNextMessageFetchRetry(FetchFailedMarker.Reason reason, Date now, int numberOfRetries) {
    switch(reason) {
      case DataNotFound:
      // TODO: Optimization: Once we are sure that parsing failures do not happen randomly, set retry to a very high delay or limit to 3 retries or so....
      case ParsingFailed:
        // We need this check to prevent overflow causing negative Dates :)
        if(numberOfRetries >= MAXIMAL_MESSAGE_FETCH_RETRY_DELAY_AT_RETRY_COUNT)
          return new Date(now.getTime() + MAXIMAL_MESSAGE_FETCH_RETRY_DELAY);
       
        // Math.min() is just a double check
        return new Date(now.getTime() + Math.min(MINIMAL_MESSAGE_FETCH_RETRY_DELAY * (1<<numberOfRetries), MAXIMAL_MESSAGE_FETCH_RETRY_DELAY));
      default:
        return new Date(now.getTime()  + MINIMAL_MESSAGE_FETCH_RETRY_DELAY);
    }
  }
 
  protected Date calculateDateOfNextMessageListFetchRetry(FetchFailedMarker.Reason reason, Date now, int numberOfRetries) {
    switch(reason) {
      case DataNotFound:
      // TODO: Optimization: Once we are sure that parsing failures do not happen randomly, set retry to a very high delay or limit to 3 retries or so....
      case ParsingFailed:
        // We need this check to prevent overflow causing negative Dates :)
        if(numberOfRetries >= MAXIMAL_MESSAGELIST_FETCH_RETRY_DELAY_AT_RETRY_COUNT)
          return new Date(now.getTime() + MAXIMAL_MESSAGELIST_FETCH_RETRY_DELAY);
       
        // Math.min() is just a double check
        return new Date(now.getTime()  + Math.min(MINIMAL_MESSAGELIST_FETCH_RETRY_DELAY * (1<<numberOfRetries), MAXIMAL_MESSAGELIST_FETCH_RETRY_DELAY));
      default:
        return new Date(now.getTime()  + MINIMAL_MESSAGELIST_FETCH_RETRY_DELAY);
    }   
  }

  private ObjectSet<FetchFailedMarker> getExpiredFetchFailedMarkers(final Date now) {
    final Query query = db.query();
    query.constrain(FetchFailedMarker.class);
    query.descend("mDateOfNextRetry").constrain(now).greater().not();
    query.descend("mRetryAllowedNow").constrain(false);
    return new Persistent.InitializingObjectSet<FetchFailedMarker>(mFreetalk, query);
  }
 
  public MessageFetchFailedMarker getMessageFetchFailedMarker(final MessageReference ref) throws NoSuchFetchFailedMarkerException {
    final Query q = db.query();
    q.constrain(MessageFetchFailedMarker.class);
    q.descend("mMessageReference").constrain(ref).identity();
    final ObjectSet<MessageFetchFailedMarker> markers = new Persistent.InitializingObjectSet<MessageList.MessageFetchFailedMarker>(mFreetalk, q);
   
    switch(markers.size()) {
      case 1:
        return markers.next();
      case 0:
        throw new NoSuchFetchFailedMarkerException(ref.toString());
      default:
        throw new DuplicateFetchFailedMarkerException(ref.toString());
    }
  }
 
  protected MessageListFetchFailedMarker getMessageListFetchFailedMarker(final String messageListID) throws NoSuchFetchFailedMarkerException {
    final Query q = db.query();
    q.constrain(MessageListFetchFailedMarker.class);
    q.descend("mMessageListID").constrain(messageListID);
    @SuppressWarnings("unchecked")
    final ObjectSet<MessageListFetchFailedMarker> markers = q.execute();
   
    switch(markers.size()) {
      case 1:
        final MessageListFetchFailedMarker result = markers.next();
        result.initializeTransient(mFreetalk);
        return result;
      case 0:
        throw new NoSuchFetchFailedMarkerException(messageListID);
      default:
        throw new DuplicateFetchFailedMarkerException(messageListID);
    }
  }
 
  /**
   * Only for being used by the MessageManager itself and by unit tests.
   */
  protected synchronized void clearExpiredFetchFailedMarkers() {
    Logger.normal(this, "Clearing expired FetchFailedMarkers...");
 
    Date now = CurrentTimeUTC.get();
   
    int amount = 0;
   
    for(FetchFailedMarker marker : getExpiredFetchFailedMarkers(now)) {
      synchronized(Persistent.transactionLock(db)) {
        try {
          if(marker instanceof MessageFetchFailedMarker) {
            MessageFetchFailedMarker m = (MessageFetchFailedMarker)marker;
            MessageReference ref = m.getMessageReference();
            ref.clearMessageWasDownloadedFlag();
            ref.storeWithoutCommit();
          } else if(marker instanceof MessageListFetchFailedMarker) {
            MessageListFetchFailedMarker m = (MessageListFetchFailedMarker)marker;
            try {
              MessageList list = getMessageList(m.getMessageListID());
              list.deleteWithoutCommit();
             
              final IdentityStatistics stats = getOrCreateIdentityStatistics(list.getAuthor());
              stats.onMessageListDeleted(list);
              stats.storeWithoutCommit();
             
              m.storeWithoutCommit(); // MessageList.deleteWithoutCommit deletes it.
            }
            catch(NoSuchMessageListException e) {
              // The marker was already processed.
            }
          } else
            Logger.error(this, "Unknown FetchFailedMarker type: " + marker);
         
          ++amount;
         
          if(logDEBUG) Logger.debug(this, "Cleared marker " + marker);
          marker.setAllowRetryNow(true);
          marker.checkedCommit(this);
        }
        catch(RuntimeException e) {
          Persistent.checkedRollback(db, this, e);
        }
      }
    }
   
    Logger.normal(this, "Finished clearing " + amount + " expired FetchFailedMarkers.");

    assert(validateMessageFetchFailedMarkers(now));
  }
 
  private boolean validateMessageFetchFailedMarkers(Date now) {
    boolean valid = true;
   
    Query q = db.query();
    q.constrain(MessageFetchFailedMarker.class);
    q.descend("mDateOfNextRetry").constrain(now).greater();
    ObjectSet<MessageFetchFailedMarker> messageMarkers = new Persistent.InitializingObjectSet<MessageFetchFailedMarker>(mFreetalk, q);
   
    for(MessageFetchFailedMarker marker : messageMarkers) {
      if(marker.isRetryAllowedNow()) {
        assert(false);
        Logger.error(this, "Invalid MessageFetchFailedMarker: Date of next retry is in future but isRetryAllowedNow==true: " + marker);
        valid = false;
      }
       
      if(!marker.getMessageReference().wasMessageDownloaded()) {
        assert(false);
        valid = false;
        Logger.error(this, "Invalid MessageFetchFailedMarker: Date of next retry is in future but message is marked as not fetched: " + marker);
      }
    }
   
    Logger.normal(this, "Number of non-expired MessageFetchFailedMarker: " + messageMarkers.size());
   

    q = db.query();
    q.constrain(MessageListFetchFailedMarker.class);
    q.descend("mDateOfNextRetry").constrain(now).greater();
    ObjectSet<MessageListFetchFailedMarker> listMarkers = new Persistent.InitializingObjectSet<MessageListFetchFailedMarker>(mFreetalk, q);
   
    for(MessageListFetchFailedMarker marker : listMarkers) {
      if(marker.isRetryAllowedNow()) {
        assert(false);
        Logger.error(this, "Invalid MessageListFetchFailedMarker: Date of next retry is in future but isRetryAllowedNow==true: " + marker);
        valid = false;
      }
     
      try {
        getMessageList(marker.getMessageListID());
      } catch(NoSuchMessageListException e) {
        assert(false);
        valid = false;
        Logger.error(this, "Invalid MessageListFetchFailedMarker: Date of next retry is in future but there is no ghost message list for it: " + marker);
      }
    }
   
    Logger.normal(this, "Number of non-expired MessageListFetchFailedMarker: " + listMarkers.size());
   
    return valid;
  }
 
  /**
   * Only for being used by the MessageManager itself and by unit tests.
   *
   * Checks whether there are any messages in subscribed boards which the subscriber did not want to read (because he does not like the author) and now
   * wants to read ... they must be added to the boards then.
   */
  protected synchronized void recheckUnwantedMessages() {
    Logger.normal(this, "Rechecking unwanted messages...");
   
    final Date now = CurrentTimeUTC.get();
   
    for(SubscribedBoard board : subscribedBoardIterator()) {
      board.retryAllUnwantedMessages(now);
    }
   
    Logger.normal(this, "Finished rechecking unwanted message");
  }
 
  /**
   * Only for being used by the MessageManager itself and by unit tests.
   *
   * Checks whether there are any messages in subscribed boards which the subscriber did want to read and now does not want to read anymore.
   */
  protected synchronized void recheckWantedMessages() {
    Logger.normal(this, "Rechecking wanted messages...");
   
    final Date now = CurrentTimeUTC.get();
   
    for(SubscribedBoard board : subscribedBoardIterator()) {
      board.validateAllWantedMessages(now);
    }
   
    Logger.normal(this, "Finished rechecking wanted message");
  }
 
  /**
   * 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 ObjectSet<MessageList.MessageReference> getAllReferencesToMessage(final String id) {
    final Query query = db.query();
    query.constrain(MessageList.MessageReference.class);
    query.constrain(OwnMessageList.OwnMessageReference.class).not();
    query.descend("mMessageID").constrain(id);
    return new Persistent.InitializingObjectSet<MessageList.MessageReference>(mFreetalk, query);
  }
 
  private ObjectSet<OwnMessageList.OwnMessageReference> getAllOwnReferencesToMessage(final String id) {
    final Query query = db.query();
    query.constrain(OwnMessageList.OwnMessageReference.class);
    query.descend("mMessageID").constrain(id);
    return new Persistent.InitializingObjectSet<OwnMessageList.OwnMessageReference>(mFreetalk, query);
  }
 
  private ObjectSet<Message> getAllRepliesToMessage(String messageID) {
    final Query query = db.query();
    query.constrain(Message.class);
    query.constrain(OwnMessage.class).not();
    query.descend("mParentID").constrain(messageID);
    return new Persistent.InitializingObjectSet<Message>(mFreetalk, query);
  }
 
  private ObjectSet<Message> getAllRepliesToMessage(Message message) {
    final Query query = db.query();
    query.constrain(Message.class);
    query.constrain(OwnMessage.class).not();
    query.descend("mParent").constrain(message).identity();
    return new Persistent.InitializingObjectSet<Message>(mFreetalk, query);
  }
 
  private ObjectSet<Message> getAllThreadRepliesToMessage(String threadID) {
    final Query query = db.query();
    query.constrain(Message.class);
    query.constrain(OwnMessage.class).not();
    query.descend("mThreadID").constrain(threadID);
    return new Persistent.InitializingObjectSet<Message>(mFreetalk, query);
  }
 
  private ObjectSet<Message> getAllThreadRepliesToMessage(Message message) {
    final Query query = db.query();
    query.constrain(Message.class);
    query.constrain(OwnMessage.class).not();
    query.descend("mThread").constrain(message).identity();
    return new Persistent.InitializingObjectSet<Message>(mFreetalk, query);
  }

  /**
   * 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(final String id) throws NoSuchMessageException {
    final Query query = db.query();
    query.constrain(Message.class);
    query.constrain(OwnMessage.class).not();
    query.descend("mID").constrain(id);
    final ObjectSet<Message> result = query.execute();

    switch(result.size()) {
      case 1:
        final Message m = result.next();
        m.initializeTransient(mFreetalk);
        return m;
      case 0:
        throw new NoSuchMessageException(id);
      default:
        throw new DuplicateMessageException(id);
    }
  }
 
  /**
   * Get a <code>MessageList</code> by its ID. The transient fields of the returned <code>MessageList</code>  will be initialized already.
   * This will NOT return <code>OwnMessageList</code> 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(final String id) throws NoSuchMessageListException {
    final Query query = db.query();
    query.constrain(MessageList.class);
    query.constrain(OwnMessageList.class).not();
    query.descend("mID").constrain(id);
    final ObjectSet<MessageList> result = query.execute();

    switch(result.size()) {
      case 1:
        final MessageList list = result.next();
        list.initializeTransient(mFreetalk);
        return list;
      case 0:
        throw new NoSuchMessageListException(id);
      default:
        throw new DuplicateMessageListException(id);
    }
  }
 
  @SuppressWarnings("unchecked")
  public synchronized OwnMessageList getOwnMessageList(final String id) throws NoSuchMessageListException {
    final Query query = db.query();
    query.constrain(OwnMessageList.class);
    query.descend("mID").constrain(id);
    final ObjectSet<OwnMessageList> result = query.execute();

    switch(result.size()) {
      case 1:
        final OwnMessageList list = result.next();
        list.initializeTransient(mFreetalk);
        return list;
      case 0:
        throw new NoSuchMessageListException(id);
      default:
        throw new DuplicateMessageListException(id);
    }
  }
 
  public OwnMessage getOwnMessage(final 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(final String id) throws NoSuchMessageException {
    final Query query = db.query();
    query.constrain(OwnMessage.class);
    query.descend("mID").constrain(id);
    final ObjectSet<OwnMessage> result = query.execute();

    switch(result.size()) {
      case 1:
        final OwnMessage m = result.next();
        m.initializeTransient(mFreetalk);
        return m;
      case 0:
        throw new NoSuchMessageException(id);
      default:
        throw new DuplicateMessageException(id);
    }
  }

  /**
   * 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();
   
    final Query query = db.query();
    query.constrain(Board.class);
    query.constrain(SubscribedBoard.class).not();
    query.descend("mName").constrain(name);
    final ObjectSet<Board> result = query.execute();

    switch(result.size()) {
      case 1:
        final Board b = result.next();
        b.initializeTransient(mFreetalk);
        return b;
      case 0:
        throw new NoSuchBoardException(name);
      default:
        throw new DuplicateBoardException(name);
    }
  }

  /**
   * Gets the board with the given name. If it does not exist, it is created and stored, the transaction is commited.
   * @param The name of the desired board
   * @throws InvalidParameterException If the name is invalid.
   */
  public Board getOrCreateBoard(String name) throws InvalidParameterException {
    return getOrCreateBoard(name, null);
  }

  private synchronized Board getOrCreateBoard(String name, String description) throws InvalidParameterException {
    name = name.toLowerCase();
   
    Board board;

    try {   
      board = getBoardByName(name);
    }
    catch(NoSuchBoardException e) {
      PersistentTaskManager tm = mFreetalk.getTaskManager();
      synchronized(tm) {
      synchronized(Persistent.transactionLock(db)) {
      try {
        board = new Board(name, description, false);
        board.initializeTransient(mFreetalk);
        board.storeWithoutCommit();
        if(logDEBUG) Logger.debug(this, "Created board " + name);
        board.checkedCommit(this);
       
        tm.storeTaskWithoutCommit(new NewBoardTask(board));
      }
      catch(RuntimeException ex) {
        Persistent.checkedRollbackAndThrow(db, this, ex);
        throw ex; // Satisfy the compiler
      }
      }
      }
    }
   
    return board;
  }
 

  /**
   * Get an iterator of all boards. The list is sorted ascending by name.
   *
   * You have to synchronize on this MessageManager before calling this function and while processing the returned list.
   * The transient fields of the returned boards will be initialized already.
   */
  public ObjectSet<Board> boardIteratorSortedByName() {
    final Query query = db.query();
    query.constrain(Board.class);
    query.constrain(SubscribedBoard.class).not();
    query.descend("mName").orderAscending();
    return new Persistent.InitializingObjectSet<Board>(mFreetalk, query);
  }
 
  /**
   * Get all boards which are being subscribed to by at least one {@link OwnIdentity}, i.e. the boards from which we should download messages.
   */
  public synchronized ObjectSet<Board> boardWithSubscriptionsIterator() {
    final Query query = db.query();
    query.constrain(Board.class);
    query.descend("mHasSubscriptions").constrain(true);
    return new Persistent.InitializingObjectSet<Board>(mFreetalk, query);
  }

  public synchronized ObjectSet<SubscribedBoard> subscribedBoardIterator() {
    final Query query = db.query();
    query.constrain(SubscribedBoard.class);
    return new Persistent.InitializingObjectSet<SubscribedBoard>(mFreetalk, 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 ObjectSet<SubscribedBoard> subscribedBoardIteratorSortedByDate(final OwnIdentity subscriber, final Date seenAfter) {
    final Query query = db.query();
    query.constrain(SubscribedBoard.class);
    query.descend("mFirstSeenDate").constrain(seenAfter).greater();
    query.descend("mFirstSeenDate").orderAscending();
    return new Persistent.InitializingObjectSet<SubscribedBoard>(mFreetalk, query);
  }
 
  /**
   * Get a list of all subscribed boards of the given identity. The list is sorted ascending by name.
   *
   * You have to synchronize on this MessageManager before calling this function and while processing the returned list.
   *
   * The transient fields of the returned objects will be initialized already.
   */
  public ObjectSet<SubscribedBoard> subscribedBoardIteratorSortedByName(final OwnIdentity subscriber) {
    final Query query = db.query();
    query.constrain(SubscribedBoard.class);
    query.descend("mSubscriber").constrain(subscriber).identity();
    query.descend("mName").orderAscending();
    return new Persistent.InitializingObjectSet<SubscribedBoard>(mFreetalk, query);
  }
 
  protected ObjectSet<SubscribedBoard> subscribedBoardIterator(String boardName) {
    boardName = boardName.toLowerCase();
   
      final Query q = db.query();
      q.constrain(SubscribedBoard.class);
      q.descend("mName").constrain(boardName);
      return new Persistent.InitializingObjectSet<SubscribedBoard>(mFreetalk, q);
    }
 
    @SuppressWarnings("unchecked")
  public synchronized SubscribedBoard getSubscription(final OwnIdentity subscriber, String boardName) throws NoSuchBoardException {
      boardName = boardName.toLowerCase();
     
      final Query q = db.query();
      q.constrain(SubscribedBoard.class);
      q.descend("mName").constrain(boardName);
      q.descend("mSubscriber").constrain(subscriber).identity();
      final ObjectSet<SubscribedBoard> result = q.execute();
     
      switch(result.size()) {
        case 1:
          final SubscribedBoard board = result.next();
          board.initializeTransient(mFreetalk);
          return board;
        case 0: throw new NoSuchBoardException(boardName);
        default: throw new DuplicateBoardException(boardName);
      }
    }
   
  /**
   * You do NOT need to synchronize on the IdentityManager when calling this function.
   */
  public SubscribedBoard subscribeToBoard(OwnIdentity subscriber, String boardName) throws InvalidParameterException, NoSuchIdentityException, NoSuchBoardException {
    boardName = boardName.toLowerCase();
   
    synchronized(mIdentityManager) {
      subscriber = mIdentityManager.getOwnIdentity(subscriber.getID()); // Ensure that the identity still exists so the caller does not have to synchronize.

      synchronized(this) {
        Board board = getBoardByName(boardName);

        try {
          return getSubscription(subscriber, boardName);
        }
        catch(NoSuchBoardException e) {
          synchronized(Persistent.transactionLock(db)) {
            try {
              SubscribedBoard subscribedBoard = new SubscribedBoard(board, subscriber);
              subscribedBoard.initializeTransient(mFreetalk);
              subscribedBoard.storeWithoutCommit();
              subscribedBoard.synchronizeWithoutCommit();
             
              if(board.hasSubscriptions() == false) {
                if(logDEBUG) Logger.debug(this, "First subscription received for board " + board + ", setting it's HasSubscriptions flag.");
                board.setHasSubscriptions(true);
                board.storeWithoutCommit();
              }
             
              subscribedBoard.checkedCommit(this);

              return subscribedBoard;
            }
            catch(InvalidParameterException error) {
              Persistent.checkedRollbackAndThrow(db, this, new RuntimeException(error));
              throw error; // Satisfy the compiler
            }
            catch(Exception error) {
              Persistent.checkedRollbackAndThrow(db, this, new RuntimeException(error));
              throw new RuntimeException(error); // Satisfy the compiler
            }
          }
        }
      }
    }
  }
 
  protected synchronized void unsubscribeFromBoard(OwnIdentity subscriber, SubscribedBoard subscribedBoard) {
    synchronized(subscribedBoard) {
      synchronized(Persistent.transactionLock(db)) {
        try {
          subscribedBoard.deleteWithoutCommit();
         
          if(subscribedBoardIterator(subscribedBoard.getName()).isEmpty()) {
            try {
              Board board = getBoardByName(subscribedBoard.getName());
              if(logDEBUG) Logger.debug(this, "Last subscription to board " + board + " removed, clearing it's HasSubscriptions flag.");
              board.setHasSubscriptions(false);
              board.storeWithoutCommit();
            } catch (NoSuchBoardException e) {
              throw new RuntimeException(e); // Should not happen.
            }
          }
         
          subscribedBoard.checkedCommit(this);
        }
        catch(RuntimeException e) {
          Persistent.checkedRollbackAndThrow(db, this, e);
        }
      }
    }
  }
 
  /**
   * You do NOT need to synchronize on the IdentityManager when calling this function.
   */
  public void unsubscribeFromBoard(OwnIdentity subscriber, String boardName) throws NoSuchBoardException, NoSuchIdentityException {
    synchronized(mIdentityManager) {
    subscriber = mIdentityManager.getOwnIdentity(subscriber.getID()); // Ensure that the identity still exists so the caller does not have to synchronize.
     
    synchronized(this) {   
      SubscribedBoard subscribedBoard = getSubscription(subscriber, boardName);
      unsubscribeFromBoard(subscriber, subscribedBoard);
    }
    }
  }

  public synchronized ObjectSet<OwnMessage> notInsertedMessageIterator() {
    final Query query = db.query();
    query.constrain(OwnMessage.class);
    query.descend("mFreenetURI").constrain(null).identity();
    // TODO: Sort ascending by date if db4o is intelligent enough to evaluate the mFreenetURI constrain before sorting...
    return new Persistent.InitializingObjectSet<OwnMessage>(mFreetalk, query);

  }
 
  /**
   * 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.
   */
  public synchronized ObjectSet<MessageList.MessageReference> notDownloadedMessageIterator() {
    // TODO: This query is very slow!
    final Query query = db.query();
    query.constrain(MessageList.MessageReference.class);
    query.constrain(OwnMessageList.OwnMessageReference.class).not();
    query.descend("mWasDownloaded").constrain(false);
    query.descend("mDate").orderDescending();
    query.descend("mBoard").descend("mHasSubscriptions").constrain(true);
   
    // TODO: The date only contains day, month and year (the XML does not contain more). We have some randomization by sorting by date but we might
    // want even more maybe - are there any security issues with not downloading messages in perfectly random order? Probably not?

    return new Persistent.InitializingObjectSet<MessageList.MessageReference>(mFreetalk, query);   
  }
 
  public synchronized int getDownloadableMessageCount(final Board board) {
    final Query query = db.query();
    query.constrain(MessageList.MessageReference.class);
    query.constrain(OwnMessageList.OwnMessageReference.class).not();
    query.descend("mBoard").constrain(board).identity();
    return query.execute().size();
  }

  /**
   * Gets all downloadable messages for the given board and sorts them descending by date.
   * The date here is NOT the date specified by the author but the date when we got to know about the message.
   */
  public ObjectSet<MessageList.MessageReference> getDownloadableMessagesSortedByDate(final Board board) {
    final Query query = db.query();
    query.constrain(MessageList.MessageReference.class);
    query.constrain(OwnMessageList.OwnMessageReference.class).not();
    query.descend("mBoard").constrain(board).identity();
    query.descend("mCreationDate").orderDescending();
    return new Persistent.InitializingObjectSet<MessageList.MessageReference>(mFreetalk, query);
  }
 
  /**
   * Gets the amount of all downloadable messages. This are the messages which where referenced in any non-own message list.
   * Messages which are posted to multiple boards are counted multiple times, once for each board.
   */
  public final synchronized int countNonOwnMessageListMessageReferences() {
    final Query query = db.query();
    query.constrain(MessageList.MessageReference.class);
    query.constrain(OwnMessageList.OwnMessageReference.class).not();
    return query.execute().size();
  }

  public final synchronized int countNonOwnMessageLists() {
    final Query query = db.query();
    query.constrain(MessageList.class);
    query.constrain(OwnMessageList.class).not();
    return query.execute().size();
  }
 
  /**
   * Get a list of all message lists from the given identity.
   * If the identity is an {@link OwnIdentity}, it's own message lists are only returned if they have been downloaded as normal message lists.
   * Technically, this means that no objects of class {@link OwnMessageList} are returned.
   *
   * The purpose of this behavior is to ensure that own messages are only displayed to the user if they have been successfully inserted.
   *
   * @param author An identity or own identity.
   * @return All message lists of the given identity except those of class OwnMessageList.
   */
  protected synchronized ObjectSet<MessageList> getMessageListsBy(final Identity author) {
    final Query query = db.query();
    query.constrain(MessageList.class);
    query.constrain(OwnMessageList.class).not();
    query.descend("mAuthor").constrain(author).identity();
    return new Persistent.InitializingObjectSet<MessageList>(mFreetalk, query);
  }
 
  /**
   * Get a list of locally stored own message lists of the given identity.
   * Locally stored means that only message lists of class {@link OwnMessageList} are returned.
   *
   * This means that there is no guarantee that the returned message lists have actually been inserted to Freenet.
   * - The message lists returned by this function can be considered as the outbox of the given identity.
   *
   * If you want a list of message lists  which is actually downloadable from Freenet, see {@link getMessageListsBy}.
   *
   * @param author The author of the message lists.
   * @return All own message lists of the given own identity.
   */
  protected synchronized ObjectSet<OwnMessageList> getOwnMessageListsBy(final OwnIdentity author) {
    final Query query = db.query();
    query.constrain(OwnMessageList.class);
    query.descend("mAuthor").constrain(author).identity();
    return new Persistent.InitializingObjectSet<OwnMessageList>(mFreetalk, query);
  }
 
  /**
   * Get a list of all messages from the given identity.
   * If the identity is an {@link OwnIdentity}, it's own messages are only returned if they have been downloaded as normal messages.
   * Technically, this means that no objects of class {@link OwnMessage} are returned.
   *
   * The purpose of this behavior is to ensure that own messages are only displayed to the user if they have been successfully inserted.
   *
   * Does not lock the MessageManager, you have to do this while calling the function and parsing the returned list.
   *
   * @param author An identity or own identity.
   * @return All messages of the given identity except those of class OwnMessage.
   */
  public ObjectSet<Message> getMessagesBy(final Identity author) {
    final Query query = db.query();
    query.constrain(Message.class);
    query.constrain(OwnMessage.class).not();
    query.descend("mAuthor").constrain(author).identity();
    return new Persistent.InitializingObjectSet<Message>(mFreetalk, query);
  }
 
  /**
   * Get a list of locally stored own messages of the given identity.
   * Locally stored means that only messages of class {@link OwnMessage} are returned.
   *
   * This means that there is no guarantee that the returned messages have actually been inserted to Freenet.
   * - The messages returned by this function can be considered as the outbox of the given identity.
   *
   * If you want a list of messages which is actually downloadable from Freenet, see {@link getMessagesBy}.
   *
   * @param author The author of the messages.
   * @return All own messages of the given own identity.
   */
  public synchronized ObjectSet<OwnMessage> getOwnMessagesBy(final OwnIdentity author) {
    final Query query = db.query();
    query.constrain(OwnMessage.class);
    query.descend("mAuthor").constrain(author).identity();
    return new Persistent.InitializingObjectSet<OwnMessage>(mFreetalk, query);
  }
 
  private ObjectSet<OwnMessageList.OwnMessageReference> getOwnMessages(final Board board) {
    final Query query = db.query();
    query.constrain(OwnMessageList.OwnMessageReference.class);
    query.descend("mBoard").constrain(board).identity();
    return new Persistent.InitializingObjectSet<OwnMessageList.OwnMessageReference>(mFreetalk, query);
  }

  public IdentityManager getIdentityManager() {
    return mIdentityManager;
  }
 
 
  public abstract MessageRating getMessageRating(OwnIdentity rater, Message message) throws NoSuchMessageRatingException;
 
  public abstract ObjectSet<? extends MessageRating> getAllMessageRatings(Message message);
 
  public abstract ObjectSet<? extends MessageRating> getAllMessageRatingsBy(OwnIdentity rater);

  /**
   * Deletes the given rating. Does not undo the effect of the rating (trust value changes, etc).
   * For being used in automatic rating deletion - this usually happens when an identity is deleted due to distrust - we do not want the distrust
   * which caused the identity deletion to be undone.
   */
  public abstract void deleteMessageRatingWithoutRevertingEffect(final MessageRating rating);

  /**
   * Deletes the given rating and reverts its effect (trust value change, etc.)
   * For being used in the UI - it can fail to revert the effect and throw an exception (due to being disconnected to the web of trust plugin,
   * due to the trust value of the affected identity having been changed by the user, etc.).
   */
  public abstract void deleteMessageRatingAndRevertEffect(final MessageRating rating);

  protected final synchronized IdentityStatistics getIdentityStatistics(final Identity identity) throws NoSuchObjectException {
    final Query query = db.query();
    query.constrain(IdentityStatistics.class);
    query.descend("mIdentity").constrain(identity).identity();
    final ObjectSet<IdentityStatistics> result = new Persistent.InitializingObjectSet<IdentityStatistics>(mFreetalk, query);
   
    switch(result.size()) {
      case 1: return result.next();
      case 0: throw new NoSuchObjectException();
      default: throw new DuplicateElementException("Duplicate IdentityStatistics for " + identity);
    }
  }
 
  public final ObjectSet<IdentityStatistics> getAllIdentityStatistics() {
    final Query query = db.query();
    query.constrain(IdentityStatistics.class);
    return new Persistent.InitializingObjectSet<IdentityStatistics>(mFreetalk, query);
  }
 
  /**
   * Not synchronized because it is typically being used in a transaction anyway - we need to store the created object.
   */
  protected final IdentityStatistics getOrCreateIdentityStatistics(final Identity identity)  {
    try {
      return getIdentityStatistics(identity);
    } catch(NoSuchObjectException e) {
      IdentityStatistics stats = new IdentityStatistics(identity);
      stats.initializeTransient(mFreetalk);
      return stats;
    }
  }
 
  /**
   * A list of boards which is automatically created in new Freetalk databases.
   * The goal of this list is:
   * - to be useful from the perspective of a user: Names an categories shall be chosen in a way that they make sense to a user, even if a better naming for developers exists
   * - to prevent the board list from becoming a mess: It has been shown on Frost/FMS that it is really difficult to get users to migrate from existing boards to new boards
   *     which serve the same purpose but have a different name. We try to provide proper categories for the most purposes which have been observed on Frost/FMS.
   * - to encourage categorization: This is part of the above, but I want to stress it again nevertheless: FMS/Frost has also shown that users tend to not use any categories
   *     ( = the parts between the '.') when creating boards. Categories are good for filtering, usability, blah blah. We should encourage their usage.
   *
   * I have tried to take all boards which can be seen on Frost and FMS into consideration for designing this clean list
   */
  public static final String[][] DEFAULT_BOARDS = {
    new String[] { "deu.diskussion", "Offene Diskussion über alle Themen, für die es kein spezielles Forum gibt" },
   
    new String[] { "deu.downloads", "Downloads aller Art, für die es kein spezielles Forum gibt. Bitte respektieren Sie die Gesetze!" },
    new String[] { "deu.downloads.anfragen", "Anfragen für Downloads aller Art, für die es kein spezielles Forum gibt. Bitte respektieren Sie die Gesetze!" },
   
    new String[] { "deu.freenet", "Allgemeine Diskussion rund um Freenet" },
    new String[] { "deu.freenet.freetalk", "Diskussion über Freetalk - das ist dieses Forumsystem"},
    new String[] { "deu.freenet.jsite", "Diskussion über JSite - das Werkzeug zum Hochladen von Freesites" },
    new String[] { "deu.freenet.sone", "Diskussion über Sone - dem sozialen Netzwerk für Freenet" },
    new String[] { "deu.freenet.weboftrust", "Diskussion über Web Of Trust - dem Kern der meisten Freenet-Community-Plugins" },
    new String[] { "deu.freenet.seiten", "Diskussion über Freesites - die Websites in Freenet. Hier kann man seine Freesite der Öffentlichkeit und insbesondere Index-Verwaltern bekannt machen." },
   
    new String[] { "deu.nachrichten.international", "Internationale Nachrichten"},
    new String[] { "deu.gesellschaft.politik.international", "Diskussion über internationale Politik"},
   
    new String[] { "eng.boards", "Discussion about the board list and announcement of new boards" },
   
    new String[] { "eng.computers", "General discussion about computer-related topics"},
    new String[] { "eng.computers.help.hardware", "Questions about computer hardware"},
    new String[] { "eng.computers.help.software", "Questions about computer software"},
    new String[] { "eng.computers.programming", "General discussion about software programming"},
   
    new String[] { "eng.discussion", "General discussion about all topics for which there is no special board" },
   
    new String[] { "eng.freenet", "General discussion about Freenet" },
    new String[] { "eng.freenet.fms", "Discussion about FMS - the standalone (non-plugin) forum system for Freenet"},
    new String[] { "eng.freenet.freemail", "Discussion about Freemail - the E-Mail implementation for Freenet"},
    new String[] { "eng.freenet.freetalk", "Discussion about Freetalk - which is this forum system"},
    new String[] { "eng.freenet.fuqid", "Discussion about Fuqid - a file-transfer management tool for Freenet"},
    new String[] { "eng.freenet.jsite", "Discussion about JSite - the tool for uploading Freesites"},
    new String[] { "eng.freenet.sone", "Discussion about Sone - the social messagging tool for Freenet"},
    new String[] { "eng.freenet.thaw", "Discussion about Thaw - a file-transfer management tool for Freenet"},
    new String[] { "eng.freenet.translation", "Discussion about translating Freenet or the existing translations"},
    new String[] { "eng.freenet.weboftrust", "Discussion about Web Of Trust - the core of most community plugins for Freenet"},
    new String[] { "eng.freenet.sites", "Discussion about Freesites - the websites of Freenet. You can announce Freesites to the public and especially index-maintainers here."},
   
    new String[] { "eng.internet", "Discussion about the 'normal' (non-Freenet) Internet" },
    new String[] { "eng.internet.sites", "A board about interesting links of all kinds on the 'normal' (non-Freenet) internet" },
   
    new String[] { "eng.news.international", "International news"},
   
    new String[] { "eng.market", "Trading of various goods happens here. Please obey the law."},
   
    new String[] { "eng.media.tv", "Discussion about television" },
   
    new String[] { "eng.science", "Discussion about science"},
    new String[] { "eng.science.mathematics", "Discussion about mathematics"},
    new String[] { "eng.society.censorship", "Discussion and revelations of censorship"},
    new String[] { "eng.society.politics.international", "Discussion of international politics"},
    new String[] { "eng.society.privacy", "Discussion about privacy"},
    new String[] { "eng.society.religion", "Discussion about religion"},
   
    new String[] { "eng.trustvalues", "Discussion about the trust values which the Web Of Trust community has assigned to identities"},
   
    new String[] { "fra.freenet", "Discussions générales à propos de Freenet"},
   
    new String[] { "mul.downloads", "All kinds of downloads for which there is no special board. Please obey the law!"},
    new String[] { "mul.downloads.books", "Downloads of written books. Please obey the law!"},
    new String[] { "mul.downloads.books.audio", "Downloads of audible books. Please obey the law!"},
    new String[] { "mul.downloads.books.comics", "Downloads of comic books. Please obey the law!"},
    new String[] { "mul.downloads.games", "Downloads of games. Please obey the law!"},
    new String[] { "mul.downloads.movies", "Downloads of movies. Please obey the law!"},
    new String[] { "mul.downloads.music", "Downloads of music. Please obey the law!"},
    new String[] { "mul.downloads.music.videos", "Downloads of music with video. Please obey the law!"},
    new String[] { "mul.downloads.pictures", "Downloads of pictures. Please obey the law!"},
    new String[] { "mul.downloads.videos", "Video-downloads which do not fit in any category. Please obey the law!"},
    new String[] { "mul.downloads.videos.series", "Downloads of series of videos. Please obey the law!"},
    new String[] { "mul.downloads.requests", "You can ask for download links here. To ensure a large audience, all categories of downloads are allowed here. Please obey the law!"},
    new String[] { "mul.downloads.requests.reinserts", "If a download does not succeed anymore, you can request someone to upload it again here. Please obey the law!"},

    new String[] { "mul.random", "All content is allowed in this 'playground' board. Please try to not give negative ratings for its messages whenever possible by your law and ethics." },

    new String[] { "mul.test", "Board for sending test messages to. Readers shall try to ensure that each message gets a reply." },
  };
 
    public synchronized void createDefaultBoards() {
      Logger.normal(this, "Creating the default boards...");
   
      for(String[] boardInfo : DEFAULT_BOARDS) {
        try {
          try {
            final Board existingBoard = getBoardByName(boardInfo[0]);
            synchronized(Persistent.transactionLock(db)) {
              try {
                if(existingBoard.setDescription(boardInfo[1])) {
                  if(logDEBUG) Logger.debug(this, "Updated description for " + existingBoard);
                  existingBoard.storeWithoutCommit();
                  Persistent.checkedCommit(db, this);
                }
              } catch(RuntimeException e) {
                Persistent.checkedRollback(db, this, e);
              }
            }
          } catch(NoSuchBoardException e) {
            getOrCreateBoard(boardInfo[0], boardInfo[1]);
          }
        } catch(Exception e) {
          Logger.error(this, "Creating a board failed", e);
        }
      }
     
      Logger.normal(this, "Finished creating the default boards.");
    }

}
TOP

Related Classes of plugins.Freetalk.MessageManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.