Package plugins.Freetalk

Source Code of plugins.Freetalk.Board$DownloadedMessageLink

/* 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.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import plugins.Freetalk.Persistent.IndexedClass;
import plugins.Freetalk.exceptions.DuplicateMessageException;
import plugins.Freetalk.exceptions.InvalidParameterException;
import plugins.Freetalk.exceptions.NoSuchMessageException;

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

import freenet.l10n.ISO639_3;
import freenet.support.Logger;
import freenet.support.StringValidityChecker;
import freenet.support.codeshortification.IfNull;

/**
* Represents a forum / newsgroups / discussion board in Freetalk. Boards are created by the <code>MessageManager</code> on demand, you do
* not need to manually create them. The <code>MessageManager</code> takes care of anything related to boards, to someone who just wants to
* write a user interface this class can be considered as read-only.
*
* @author xor (xor@freenetproject.org)
*/
@IndexedClass // TODO: Check whether we need the index
public class Board extends Persistent implements Comparable<Board> {

    /* Constants */

    private static transient final Map<String, ISO639_3.LanguageCode> ALLOWED_LANGUAGES = Collections.unmodifiableMap(loadAllowedLanguages());

    // Characters not allowed in board names:
    //  ! , ? * [ \ ] (space)  not allowed by NNTP
    //  / : < > | "            not allowed in filenames on certain platforms
    //                         (a problem for some newsreaders)
    private static transient final String DISALLOWED_NAME_CHARACTERS = "!,?*[\\] /:<>|\"";

    public static transient final int MAX_BOARDNAME_TEXT_LENGTH = 256;


    /* Attributes, stored in the database */
   
    @IndexedField
    private final String mID;

    @IndexedField
    private final String mName;
   
    private String mDescription;
   
    /** True if at least one {@link SubscribedBoard} for this Board exists, i.e. if we should download messages of this board. */
    private boolean mHasSubscriptions;
   
    private int mNextFreeMessageIndex = 1;

    private static final Map<String, ISO639_3.LanguageCode> loadAllowedLanguages() {
      final ISO639_3 iso639_3 = new ISO639_3();
     
      // Get all real (non-symbolic) and living languages
      final HashMap<String, ISO639_3.LanguageCode> languages = new HashMap<String, ISO639_3.LanguageCode>(
            iso639_3.getLanguagesByScopeAndType(ISO639_3.LanguageCode.Scope.Individual, ISO639_3.LanguageCode.Type.Living)
          ); // Convert from Hashtable to HashMap, we do not need synchronization.
     
      // Add the special code for multiple languages
      final ISO639_3.LanguageCode multilingual = iso639_3.getMultilingualCode();
      languages.put(multilingual.id, multilingual);
     
      // Latin is still being taught in schools. Type == Ancient, therefore not in result of getLanguagesByScopeAndType
      final ISO639_3.LanguageCode lat = iso639_3.getLanguages().get("lat");
      IfNull.thenThrow(lat);
      languages.put("lat", lat);
     
      // Esperanto, added by request. Scope == "Constructed", therefore not in result of getLanguagesByScopeAndType
      final ISO639_3.LanguageCode epo = iso639_3.getLanguages().get("epo");
      IfNull.thenThrow(epo);
      languages.put("epo", epo);
     
      // Klingon, easter-egg for nerds. Scope == "Constructed", therefore not in result of getLanguagesByScopeAndType
      final ISO639_3.LanguageCode tlh = iso639_3.getLanguages().get("tlh");
      IfNull.thenThrow(tlh);
      languages.put("tlh", tlh);
     
        return languages;
    }
   
    public static final Map<String, ISO639_3.LanguageCode> getAllowedLanguages() {
      return ALLOWED_LANGUAGES;
    }

    /**
     * Create a board. You have to store() it yourself after creation.
     * Only for being used directly by unit tests. The client interface for creating boards is {@link MessageManager.getOrCreateBoard}.
     *
     * @param newName The name of the board. For restrictions, see <code>isNameValid()</code>
     * @throws InvalidParameterException If none or an invalid name is given.
     */
    public Board(String newName, String description, boolean hasSubscriptions) throws InvalidParameterException {
        if(newName==null || newName.length() == 0)
            throw new IllegalArgumentException("Empty board name.");
        if(!isNameValid(newName))
            throw new InvalidParameterException("Invalid board name."); // TODO: Explain what is invalid

        mID = UUID.randomUUID().toString();
        mName = newName.toLowerCase();
        mDescription = description != null ? description : "";
        mHasSubscriptions = hasSubscriptions;
    }

  @Override
  public void databaseIntegrityTest() throws Exception {
    checkedActivate(1); // String, int, boolean are db4o primitive types, depth 1 is enough
   
      if(mID == null)
        throw new NullPointerException("mID==null");
     
      try {
        UUID.fromString(mID);
      } catch(Exception e) {
        throw new IllegalStateException("mID is invalid UUID: " + mID);
      }
     
      if(mName == null)
        throw new NullPointerException();
     
      if(!isNameValid(mName))
        throw new IllegalStateException("mName is invalid: " + mName);
     
      if(mHasSubscriptions != (mFreetalk.getMessageManager().subscribedBoardIterator(mName).size() != 0))
        throw new IllegalStateException("mHasSubscriptions is wrong: " + mHasSubscriptions);
     
      if(mNextFreeMessageIndex < 1)
        throw new IllegalStateException("mNextFreeMessageIndex is illegal: " + mNextFreeMessageIndex);
     
      if(getDownloadedMessagesAfterIndex(mNextFreeMessageIndex-1).size() != 0)
        throw new IllegalStateException("mNextFreetMessageIndex is wrong: " + mNextFreeMessageIndex);
  }

    /**
     * Store this object in the database. You have to initializeTransient() before.
     *
     * Does not provide synchronization, you have to lock the MessageManager, this Board and then the database before calling this function.
     */
    protected void storeWithoutCommit() {
      super.storeWithoutCommit(1); // String, int, boolean are db4o primitive types, depth 1 is enough
    }


    /**
     * Check if a board name is valid.
     *
     * Board names are required to begin with a known language code,
     * and may not contain any blacklisted characters.  Formatting
     * characters must be properly paired within each part of the name
     * (special formatting characters may be needed, e.g. for some
     * Arabic or Hebrew group names to be displayed properly.)
     */
    public static final boolean isNameValid(String name) {
        // paranoia checks

        if (name == null || name.length() == 0) {
            return false;
        }

        // check maximum length

        if (name.length() > MAX_BOARDNAME_TEXT_LENGTH) {
            return false;
        }

        // check for illegal characters

        if (!StringValidityChecker.containsNoLinebreaks(name)
                || !StringValidityChecker.containsNoInvalidCharacters(name)
                || !StringValidityChecker.containsNoControlCharacters(name)
                || !StringValidityChecker.containsNoIDNBlacklistCharacters(name))
            return false;

        for (Character c : name.toCharArray()) {
            if (DISALLOWED_NAME_CHARACTERS.indexOf(c) != -1)
                return false;
        }

        // check for invalid formatting characters (each dot-separated
        // part of the input string must be valid on its own)

        String[] parts = name.split("\\.", -1); // The 1-argument version will not return empty parts!
        if (parts.length < 2)
            return false;

        for (int i = 0; i < parts.length; i++) {
            if (parts[i].length() == 0 || !StringValidityChecker.containsNoInvalidFormatting(parts[i]))
                return false;
        }

        // first part of name must be a recognized language code

        return (ALLOWED_LANGUAGES.containsKey(parts[0]));
    }
   
    /**
     * Get the ID of this board.
     *
     * It is a local (to this Freetalk database), random, unique UUID of this board. If a board is created, deleted and then re-created with the same name,
     * the ID will still be different.
     * Needed for synchronization with client apps: They use the per-board unique message index numbers to check whether they have all messages stored in their
     * caching database. If the user deleted a board in Freetalk and then the board was re-created due to new messages, the message indexes will start at 0 again.
     * If we used only the board name as identification, the client apps would not download the new messages because they already have stored messages with index
     * 0, 1, 2 and so on.
     */
    public final String getID() {
    checkedActivate(1); // String is a db4o primitive type so 1 is enough
      return mID;
    }
   
    /**
     * @return Returns the language code of the board. It is the token before the first '.' in the name of the board.
     */
    public final ISO639_3.LanguageCode getLanguage() {
    checkedActivate(1); // String is a db4o primitive type so 1 is enough
      return ALLOWED_LANGUAGES.get(mName.substring(0, mName.indexOf('.')));
    }

    /**
     * @return The name of this board. Only one board with a given name can exist at once. The name is case-insensitive.
     */
    public final String getName() {
    checkedActivate(1); // String is a db4o primitive type so 1 is enough
        return mName;
    }
   
    /**
     * @see getName()
     */
    public final String getNameWithoutLanguagePrefix() {
    checkedActivate(1); // String is a db4o primitive type so 1 is enough
        return mName.substring(mName.indexOf('.')+1);
    }
   
    /**
     * In the current implementation, returns the hardcoded description if there is one.
     * No user specified descriptions are supported yet, we just hardcode descriptions of the default boards.
     *
     * TODO: The future implementation should do that:
     * Returns the description of this board from the view of the given {@link OwnIdentity}.
     * This shall be voted by the community and the description with the most votes shall be returned.
     * "From the view of the given OwnIdentity" means that votes are only counted when the own identity trusts the voter.
     */
    public String getDescription(OwnIdentity viewer) {
    checkedActivate(1); // String is a db4o primitive type so 1 is enough
    return mDescription;
    }
   
    protected boolean setDescription(String newDescription) {
    checkedActivate(1); // String is a db4o primitive type so 1 is enough
     
    boolean result = false;
    if(mDescription == null || !mDescription.equals(newDescription))
      result = true;
   
    mDescription = newDescription;
   
    return result;
    }

    public final Date getFirstSeenDate() {
    checkedActivate(1); // Date is a db4o primitive type so 1 is enough
        return mCreationDate;
    }
   
    /**
     * @return Returns true if at least one {@link SubscribedBoard} for this board exists, i.e. if we should download messages for this board.
     */
    public final boolean hasSubscriptions() {
    checkedActivate(1); // boolean is a db4o primitive type so 1 is enough
      return mHasSubscriptions;
    }
   
    /**
     * Set the "has subscriptions" flag (see {@link hasSubscriptions}) of this board.
     *
     * This function must be called by the {@link MessageManager} when the amount of {@link SubscribedBoard} objects for this board changes from zero to positive
     * or positive to zero because the "has subscriptions" flag is a cached boolean and therefore is NOT auto-updated by the database when you delete the last
     * {@link SubscribedBoard} object or create the first one.
     */
  protected final void setHasSubscriptions(boolean hasSubscriptions) {
    checkedActivate(1); // boolean is a db4o primitive type so 1 is enough
    mHasSubscriptions = hasSubscriptions;
  }

    /**
     * Compare boards by comparing their names; provided so we can sort an array of boards.
     */
    public int compareTo(Board b) {
        return getName().compareTo(b.getName());
    }
   
    /**
     * Returns true if the given {@link Message} has this board's name listed in it's target board names.
     * Does not check whether the {@link Board} object referenced by the message is equal to this {@link Board} object!
     */
    public final boolean contains(Message message) {
    checkedActivate(1); // String is a db4o primitive type so 1 is enough
     
      for(Board board : message.getBoards()) {
        if(mName.equals(board.getName()))
          return true;
      }
     
      return false;
    }
   
    /**
     * A DownloadedMessageLink links an actually downloaded message to a board - in opposite to {@link MessageList.MessageReference} which mark messages which might be downloaded or not. 
     * These DownloadedMessageLink objects are used for querying the database for messages which belong to a certain board.
     * - Since a message can be posted to multiple boards, we need these helper objects for being able to do fast queries on the message lists of boards.
     */
    // @IndexedClass // I can't think of any query which would need to get all DownloadedMessageLink objects.
    public static final class DownloadedMessageLink extends Persistent {
     
      @IndexedField
      private final Board mBoard;
     
      @IndexedField
      private final int mIndex;
     
      @IndexedField
      private final Message mMessage;
     
      private final Identity mAuthor;
     
      private DownloadedMessageLink(Board myBoard, Message myMessage, int myIndex) {
        if(myBoard == null) throw new NullPointerException();
        if(myMessage == null) throw new NullPointerException();
        if(myIndex <= 0) throw new IllegalArgumentException();
       
        mBoard = myBoard;
        mMessage = myMessage;
        mIndex = myIndex;
        mAuthor = myMessage.getAuthor();
       
        assert(mIndex == (mBoard.mNextFreeMessageIndex-1));
      }


    @Override
    public void databaseIntegrityTest() throws Exception {
      checkedActivate(2); // One higher than necessary, we call getters on the member objects anyway
     
      if(mBoard == null)
        throw new NullPointerException("mBoard==null");
       
      if(mIndex < 1)
        throw new IllegalStateException("mIndex is illegal: " + mIndex);
     
      final Board board = getBoard();
     
      // The primary reason for calling it is to ensure that the index is only taken once:
      // It should throw a DuplicateMessageException if there are multiple...
      if(board.getDownloadedMessageByIndex(mIndex) != this)
        throw new IllegalStateException("getMessageByIndex is broken");
       
      if(mMessage == null)
        throw new NullPointerException("mMessage==null");
     
      final Message message = getMessage();
     
      if(!board.contains(message))
        throw new IllegalStateException("mMessage does not belong in this Board: mBoard==" + mBoard
            + "; mMessage.getBoards()==" + mMessage.getBoards()
            + "; mMessage==" + mMessage);
     
      // The primary reason for calling it is to ensure that the message is only linked once:
      // It should throw a DuplicateMessageException if there are multiple...
      if(board.getDownloadedMessageLink(message) != this)
        throw new IllegalStateException("getMessageLink is broken");
     
      if(mAuthor == null)
        throw new NullPointerException("mAuthor==null");
     
      if(message.getAuthor() != mAuthor)
        throw new IllegalStateException("mAuthor is wrong: mAuthor==" + mAuthor
            + "; mMessage.getAuthor()==" + message.getAuthor()
            + "; mMessage==" + mMessage);
     
    }

      public Board getBoard() {
        checkedActivate(1);
        mBoard.initializeTransient(mFreetalk);
        return mBoard;
      }
     
      public Message getMessage() {
        checkedActivate(1);
        mMessage.initializeTransient(mBoard.mFreetalk);
        return mMessage;
      }
     
      public int getMessageIndex() {
        checkedActivate(1); // int is a db4o primitive type so 1 is enough
        return mIndex;
      }
     
      public Identity getAuthor() {
        checkedActivate(1);
        if(mAuthor instanceof Persistent)
          ((Persistent)mAuthor).initializeTransient(mFreetalk);
        return mAuthor;
      }
     

        /**
         * Does not provide synchronization, you have to lock the MessageManager, this Board and then the database before calling this function.
         */
        protected void storeWithoutCommit() {
          try {
            checkedActivate(1);
            throwIfNotStored(mBoard);
            throwIfNotStored(mMessage);
            throwIfNotStored(mAuthor);
            checkedStore();
          }
          catch(RuntimeException e) {
            checkedRollbackAndThrow(e);
          }
        }
       
      protected void deleteWithoutCommit() {
        deleteWithoutCommit(2);
    }

    }
   
  protected final DownloadedMessageLink getDownloadedMessageLink(Message message) throws NoSuchMessageException {
      Query q = mDB.query();
      q.constrain(DownloadedMessageLink.class);
      q.descend("mMessage").constrain(message).identity();
      q.descend("mBoard").constrain(this).identity();
      ObjectSet<DownloadedMessageLink> messageLinks = new Persistent.InitializingObjectSet<Board.DownloadedMessageLink>(mFreetalk, q);
     
      switch(messageLinks.size()) {
        case 0: throw new NoSuchMessageException(message.getID());
        case 1: return messageLinks.next();
        default: throw new DuplicateMessageException(message.getID());
      }
    }
   
   
    /**
     * @return Returns the next free message index and increments the internal free message index counter - therefore, the message index will be taken even if
     *   you do not store any message with it. This ensures that deleting the head message cannot cause it's index to be associated with a new, different message.
     */
  protected final synchronized int takeFreeMessageIndexWithoutCommit() {
    checkedActivate(1); // int is a db4o primitive type so 1 is enough
    int result = mNextFreeMessageIndex++;
    storeWithoutCommit();
    return result;
    }
   
    protected final ObjectSet<DownloadedMessageLink> getDownloadedMessagesAfterIndex(int index) {
        Query q = mDB.query();
        q.constrain(DownloadedMessageLink.class);
        q.descend("mBoard").constrain(this).identity();
        q.descend("mIndex").constrain(index).greater();
        return new Persistent.InitializingObjectSet<DownloadedMessageLink>(mFreetalk, q.execute());
    }
   
    private final DownloadedMessageLink getDownloadedMessageByIndex(int index) throws NoSuchMessageException {
        final Query q = mDB.query();
        q.constrain(DownloadedMessageLink.class);
        q.descend("mBoard").constrain(this).identity();
        q.descend("mIndex").constrain(index);
      final ObjectSet<DownloadedMessageLink> messageLinks = new Persistent.InitializingObjectSet<Board.DownloadedMessageLink>(mFreetalk, q);
     
      switch(messageLinks.size()) {
        case 0: throw new NoSuchMessageException("index: " + index);
        case 1: return messageLinks.next();
        default: throw new DuplicateMessageException("index: " + index);
      }
    }
   
    public final synchronized int getDownloadedMessageCount() {
      final Query query = mDB.query();
      query.constrain(DownloadedMessageLink.class);
      query.descend("mBoard").constrain(this).identity();
      return query.execute().size();
    }
   
    /**
     * Sanity check function for checking whether the given message really belongs to this board.
     * @throws IllegalArgumentException If the message does not belong to this board according to its board list or if it is an OwnMessage.
     */
    protected final void throwIfNotAllowedInThisBoard(Message newMessage) {
      if(newMessage instanceof OwnMessage) {
        /* We do not add the message to the boards it is posted to because the user should only see the message if it has been downloaded
         * successfully. This helps the user to spot problems: If he does not see his own messages we can hope that he reports a bug */
        throw new IllegalArgumentException("Adding OwnMessages to a board is not allowed.");
      }
     
      if(contains(newMessage) == false)
        throw new IllegalArgumentException("addMessage called with a message which was not posted to this board (" + getName() + "): " +
            newMessage);
    }
   
    /**
     * Called by the {@link MessageManager} when a new message was fetched.
     * Stores any messages in this board, does not check whether any {@link OwnIdentity} actually wants the messages.
     *
     * The purpose of this is:
     * - that the message manager can fill a new {@link SubscribedBoard} with already downloaded messages from it's parent {@link Board}
     * - that the message manager can tell existing subscribed boards to pull new messages from their parent boards.
     *
     * @throws IllegalArgumentException When trying to add a message which does not belong to this board or trying to add an OwnMessage.
     * @throws RuntimeException If storing this Board to the database fails.
     */
    protected synchronized void addMessage(Message newMessage) throws Exception {
      throwIfNotAllowedInThisBoard(newMessage);
     
      try {
        getDownloadedMessageLink(newMessage);
        Logger.error(this, "addMessage() called for already existing message: " + newMessage);
      }
      catch(NoSuchMessageException e) {
        final DownloadedMessageLink link = new DownloadedMessageLink(this, newMessage, takeFreeMessageIndexWithoutCommit());
        link.initializeTransient(mFreetalk);
        link.storeWithoutCommit();
      }
    }
   
    protected synchronized void deleteMessage(Message message) throws NoSuchMessageException {
      getDownloadedMessageLink(message).deleteWithoutCommit();
    }

    @Override
    public String toString() {
    checkedActivate(1); // String is a db4o primitive type so 1 is enough
      return super.toString() + " with mName: " + mName;
    }
}
TOP

Related Classes of plugins.Freetalk.Board$DownloadedMessageLink

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.