Package net.pterodactylus.sone.database.memory

Source Code of net.pterodactylus.sone.database.memory.MemoryDatabase

/*
* Sone - MemoryDatabase.java - Copyright © 2013 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package net.pterodactylus.sone.database.memory;

import static com.google.common.base.Optional.fromNullable;
import static com.google.common.base.Preconditions.checkNotNull;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import net.pterodactylus.sone.data.Album;
import net.pterodactylus.sone.data.Image;
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.PostReply;
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.data.impl.AlbumBuilderImpl;
import net.pterodactylus.sone.data.impl.ImageBuilderImpl;
import net.pterodactylus.sone.database.AlbumBuilder;
import net.pterodactylus.sone.database.Database;
import net.pterodactylus.sone.database.DatabaseException;
import net.pterodactylus.sone.database.ImageBuilder;
import net.pterodactylus.sone.database.PostBuilder;
import net.pterodactylus.sone.database.PostDatabase;
import net.pterodactylus.sone.database.PostReplyBuilder;
import net.pterodactylus.sone.database.SoneProvider;
import net.pterodactylus.util.config.Configuration;
import net.pterodactylus.util.config.ConfigurationException;

import com.google.common.base.Optional;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import com.google.common.util.concurrent.AbstractService;
import com.google.inject.Inject;

/**
* Memory-based {@link PostDatabase} implementation.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
public class MemoryDatabase extends AbstractService implements Database {

  /** The lock. */
  private final ReadWriteLock lock = new ReentrantReadWriteLock();

  /** The Sone provider. */
  private final SoneProvider soneProvider;

  /** The configuration. */
  private final Configuration configuration;

  /** All posts by their ID. */
  private final Map<String, Post> allPosts = new HashMap<String, Post>();

  /** All posts by their Sones. */
  private final Map<String, Collection<Post>> sonePosts = new HashMap<String, Collection<Post>>();

  /** All posts by their recipient. */
  private final Map<String, Collection<Post>> recipientPosts = new HashMap<String, Collection<Post>>();

  /** Whether posts are known. */
  private final Set<String> knownPosts = new HashSet<String>();

  /** All post replies by their ID. */
  private final Map<String, PostReply> allPostReplies = new HashMap<String, PostReply>();

  /** Replies sorted by Sone. */
  private final SortedSetMultimap<String, PostReply> sonePostReplies = TreeMultimap.create(new Comparator<String>() {

    @Override
    public int compare(String leftString, String rightString) {
      return leftString.compareTo(rightString);
    }
  }, PostReply.TIME_COMPARATOR);

  /** Replies by post. */
  private final Map<String, SortedSet<PostReply>> postReplies = new HashMap<String, SortedSet<PostReply>>();

  /** Whether post replies are known. */
  private final Set<String> knownPostReplies = new HashSet<String>();

  private final Map<String, Album> allAlbums = new HashMap<String, Album>();

  private final Map<String, Image> allImages = new HashMap<String, Image>();

  /**
   * Creates a new memory database.
   *
   * @param soneProvider
   *     The Sone provider
   * @param configuration
   *     The configuration for loading and saving elements
   */
  @Inject
  public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) {
    this.soneProvider = soneProvider;
    this.configuration = configuration;
  }

  //
  // DATABASE METHODS
  //

  /**
   * Saves the database.
   *
   * @throws DatabaseException
   *     if an error occurs while saving
   */
  @Override
  public void save() throws DatabaseException {
    saveKnownPosts();
    saveKnownPostReplies();
  }

  //
  // SERVICE METHODS
  //

  /** {@inheritDocs} */
  @Override
  protected void doStart() {
    loadKnownPosts();
    loadKnownPostReplies();
    notifyStarted();
  }

  /** {@inheritDocs} */
  @Override
  protected void doStop() {
    try {
      save();
      notifyStopped();
    } catch (DatabaseException de1) {
      notifyFailed(de1);
    }
  }

  //
  // POSTPROVIDER METHODS
  //

  /** {@inheritDocs} */
  @Override
  public Optional<Post> getPost(String postId) {
    lock.readLock().lock();
    try {
      return fromNullable(allPosts.get(postId));
    } finally {
      lock.readLock().unlock();
    }
  }

  /** {@inheritDocs} */
  @Override
  public Collection<Post> getPosts(String soneId) {
    return new HashSet<Post>(getPostsFrom(soneId));
  }

  /** {@inheritDocs} */
  @Override
  public Collection<Post> getDirectedPosts(String recipientId) {
    lock.readLock().lock();
    try {
      Collection<Post> posts = recipientPosts.get(recipientId);
      return (posts == null) ? Collections.<Post>emptySet() : new HashSet<Post>(posts);
    } finally {
      lock.readLock().unlock();
    }
  }

  //
  // POSTBUILDERFACTORY METHODS
  //

  /** {@inheritDocs} */
  @Override
  public PostBuilder newPostBuilder() {
    return new MemoryPostBuilder(this, soneProvider);
  }

  //
  // POSTSTORE METHODS
  //

  /** {@inheritDocs} */
  @Override
  public void storePost(Post post) {
    checkNotNull(post, "post must not be null");
    lock.writeLock().lock();
    try {
      allPosts.put(post.getId(), post);
      getPostsFrom(post.getSone().getId()).add(post);
      if (post.getRecipientId().isPresent()) {
        getPostsTo(post.getRecipientId().get()).add(post);
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  /** {@inheritDocs} */
  @Override
  public void removePost(Post post) {
    checkNotNull(post, "post must not be null");
    lock.writeLock().lock();
    try {
      allPosts.remove(post.getId());
      getPostsFrom(post.getSone().getId()).remove(post);
      if (post.getRecipientId().isPresent()) {
        getPostsTo(post.getRecipientId().get()).remove(post);
      }
      post.getSone().removePost(post);
    } finally {
      lock.writeLock().unlock();
    }
  }

  /** {@inheritDocs} */
  @Override
  public void storePosts(Sone sone, Collection<Post> posts) throws IllegalArgumentException {
    checkNotNull(sone, "sone must not be null");
    /* verify that all posts are from the same Sone. */
    for (Post post : posts) {
      if (!sone.equals(post.getSone())) {
        throw new IllegalArgumentException(String.format("Post from different Sone found: %s", post));
      }
    }

    lock.writeLock().lock();
    try {
      /* remove all posts by the Sone. */
      getPostsFrom(sone.getId()).clear();
      for (Post post : posts) {
        allPosts.remove(post.getId());
        if (post.getRecipientId().isPresent()) {
          getPostsTo(post.getRecipientId().get()).remove(post);
        }
      }

      /* add new posts. */
      getPostsFrom(sone.getId()).addAll(posts);
      for (Post post : posts) {
        allPosts.put(post.getId(), post);
        if (post.getRecipientId().isPresent()) {
          getPostsTo(post.getRecipientId().get()).add(post);
        }
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  /** {@inheritDocs} */
  @Override
  public void removePosts(Sone sone) {
    checkNotNull(sone, "sone must not be null");
    lock.writeLock().lock();
    try {
      /* remove all posts by the Sone. */
      getPostsFrom(sone.getId()).clear();
      for (Post post : sone.getPosts()) {
        allPosts.remove(post.getId());
        if (post.getRecipientId().isPresent()) {
          getPostsTo(post.getRecipientId().get()).remove(post);
        }
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  //
  // POSTREPLYPROVIDER METHODS
  //

  /** {@inheritDocs} */
  @Override
  public Optional<PostReply> getPostReply(String id) {
    lock.readLock().lock();
    try {
      return fromNullable(allPostReplies.get(id));
    } finally {
      lock.readLock().unlock();
    }
  }

  /** {@inheritDocs} */
  @Override
  public List<PostReply> getReplies(String postId) {
    lock.readLock().lock();
    try {
      if (!postReplies.containsKey(postId)) {
        return Collections.emptyList();
      }
      return new ArrayList<PostReply>(postReplies.get(postId));
    } finally {
      lock.readLock().unlock();
    }
  }

  //
  // POSTREPLYBUILDERFACTORY METHODS
  //

  /** {@inheritDocs} */
  @Override
  public PostReplyBuilder newPostReplyBuilder() {
    return new MemoryPostReplyBuilder(this, soneProvider);
  }

  //
  // POSTREPLYSTORE METHODS
  //

  /** {@inheritDocs} */
  @Override
  public void storePostReply(PostReply postReply) {
    lock.writeLock().lock();
    try {
      allPostReplies.put(postReply.getId(), postReply);
      if (postReplies.containsKey(postReply.getPostId())) {
        postReplies.get(postReply.getPostId()).add(postReply);
      } else {
        TreeSet<PostReply> replies = new TreeSet<PostReply>(Reply.TIME_COMPARATOR);
        replies.add(postReply);
        postReplies.put(postReply.getPostId(), replies);
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  /** {@inheritDocs} */
  @Override
  public void storePostReplies(Sone sone, Collection<PostReply> postReplies) {
    checkNotNull(sone, "sone must not be null");
    /* verify that all posts are from the same Sone. */
    for (PostReply postReply : postReplies) {
      if (!sone.equals(postReply.getSone())) {
        throw new IllegalArgumentException(String.format("PostReply from different Sone found: %s", postReply));
      }
    }

    lock.writeLock().lock();
    try {
      /* remove all post replies of the Sone. */
      for (PostReply postReply : getRepliesFrom(sone.getId())) {
        removePostReply(postReply);
      }
      for (PostReply postReply : postReplies) {
        allPostReplies.put(postReply.getId(), postReply);
        sonePostReplies.put(postReply.getSone().getId(), postReply);
        if (this.postReplies.containsKey(postReply.getPostId())) {
          this.postReplies.get(postReply.getPostId()).add(postReply);
        } else {
          TreeSet<PostReply> replies = new TreeSet<PostReply>(Reply.TIME_COMPARATOR);
          replies.add(postReply);
          this.postReplies.put(postReply.getPostId(), replies);
        }
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  /** {@inheritDocs} */
  @Override
  public void removePostReply(PostReply postReply) {
    lock.writeLock().lock();
    try {
      allPostReplies.remove(postReply.getId());
      if (postReplies.containsKey(postReply.getPostId())) {
        postReplies.get(postReply.getPostId()).remove(postReply);
        if (postReplies.get(postReply.getPostId()).isEmpty()) {
          postReplies.remove(postReply.getPostId());
        }
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  /** {@inheritDocs} */
  @Override
  public void removePostReplies(Sone sone) {
    checkNotNull(sone, "sone must not be null");

    lock.writeLock().lock();
    try {
      for (PostReply postReply : sone.getReplies()) {
        removePostReply(postReply);
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  //
  // ALBUMPROVDER METHODS
  //

  @Override
  public Optional<Album> getAlbum(String albumId) {
    lock.readLock().lock();
    try {
      return fromNullable(allAlbums.get(albumId));
    } finally {
      lock.readLock().unlock();
    }
  }

  //
  // ALBUMBUILDERFACTORY METHODS
  //

  @Override
  public AlbumBuilder newAlbumBuilder() {
    return new AlbumBuilderImpl();
  }

  //
  // ALBUMSTORE METHODS
  //

  @Override
  public void storeAlbum(Album album) {
    lock.writeLock().lock();
    try {
      allAlbums.put(album.getId(), album);
    } finally {
      lock.writeLock().unlock();
    }
  }

  @Override
  public void removeAlbum(Album album) {
    lock.writeLock().lock();
    try {
      allAlbums.remove(album.getId());
    } finally {
      lock.writeLock().unlock();
    }
  }

  //
  // IMAGEPROVIDER METHODS
  //

  @Override
  public Optional<Image> getImage(String imageId) {
    lock.readLock().lock();
    try {
      return fromNullable(allImages.get(imageId));
    } finally {
      lock.readLock().unlock();
    }
  }

  //
  // IMAGEBUILDERFACTORY METHODS
  //

  @Override
  public ImageBuilder newImageBuilder() {
    return new ImageBuilderImpl();
  }

  //
  // IMAGESTORE METHODS
  //

  @Override
  public void storeImage(Image image) {
    lock.writeLock().lock();
    try {
      allImages.put(image.getId(), image);
    } finally {
      lock.writeLock().unlock();
    }
  }

  @Override
  public void removeImage(Image image) {
    lock.writeLock().lock();
    try {
      allImages.remove(image.getId());
    } finally {
      lock.writeLock().unlock();
    }
  }

  //
  // PACKAGE-PRIVATE METHODS
  //

  /**
   * Returns whether the given post is known.
   *
   * @param post
   *     The post
   * @return {@code true} if the post is known, {@code false} otherwise
   */
  boolean isPostKnown(Post post) {
    lock.readLock().lock();
    try {
      return knownPosts.contains(post.getId());
    } finally {
      lock.readLock().unlock();
    }
  }

  /**
   * Sets whether the given post is known.
   *
   * @param post
   *     The post
   * @param known
   *     {@code true} if the post is known, {@code false} otherwise
   */
  void setPostKnown(Post post, boolean known) {
    lock.writeLock().lock();
    try {
      if (known) {
        knownPosts.add(post.getId());
      } else {
        knownPosts.remove(post.getId());
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  /**
   * Returns whether the given post reply is known.
   *
   * @param postReply
   *     The post reply
   * @return {@code true} if the given post reply is known, {@code false}
   *         otherwise
   */
  boolean isPostReplyKnown(PostReply postReply) {
    lock.readLock().lock();
    try {
      return knownPostReplies.contains(postReply.getId());
    } finally {
      lock.readLock().unlock();
    }
  }

  /**
   * Sets whether the given post reply is known.
   *
   * @param postReply
   *     The post reply
   * @param known
   *     {@code true} if the post reply is known, {@code false} otherwise
   */
  void setPostReplyKnown(PostReply postReply, boolean known) {
    lock.writeLock().lock();
    try {
      if (known) {
        knownPostReplies.add(postReply.getId());
      } else {
        knownPostReplies.remove(postReply.getId());
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  //
  // PRIVATE METHODS
  //

  /**
   * Gets all posts for the given Sone, creating a new collection if there is
   * none yet.
   *
   * @param soneId
   *     The ID of the Sone to get the posts for
   * @return All posts
   */
  private Collection<Post> getPostsFrom(String soneId) {
    Collection<Post> posts = null;
    lock.readLock().lock();
    try {
      posts = sonePosts.get(soneId);
    } finally {
      lock.readLock().unlock();
    }
    if (posts != null) {
      return posts;
    }

    posts = new HashSet<Post>();
    lock.writeLock().lock();
    try {
      sonePosts.put(soneId, posts);
    } finally {
      lock.writeLock().unlock();
    }

    return posts;
  }

  /**
   * Gets all posts that are directed the given Sone, creating a new collection
   * if there is none yet.
   *
   * @param recipientId
   *     The ID of the Sone to get the posts for
   * @return All posts
   */
  private Collection<Post> getPostsTo(String recipientId) {
    Collection<Post> posts = null;
    lock.readLock().lock();
    try {
      posts = recipientPosts.get(recipientId);
    } finally {
      lock.readLock().unlock();
    }
    if (posts != null) {
      return posts;
    }

    posts = new HashSet<Post>();
    lock.writeLock().lock();
    try {
      recipientPosts.put(recipientId, posts);
    } finally {
      lock.writeLock().unlock();
    }

    return posts;
  }

  /** Loads the known posts. */
  private void loadKnownPosts() {
    lock.writeLock().lock();
    try {
      int postCounter = 0;
      while (true) {
        String knownPostId = configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").getValue(null);
        if (knownPostId == null) {
          break;
        }
        knownPosts.add(knownPostId);
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  /**
   * Saves the known posts to the configuration.
   *
   * @throws DatabaseException
   *     if a configuration error occurs
   */
  private void saveKnownPosts() throws DatabaseException {
    lock.readLock().lock();
    try {
      int postCounter = 0;
      for (String knownPostId : knownPosts) {
        configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
      }
      configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
    } catch (ConfigurationException ce1) {
      throw new DatabaseException("Could not save database.", ce1);
    } finally {
      lock.readLock().unlock();
    }
  }

  /**
   * Returns all replies by the given Sone.
   *
   * @param id
   *     The ID of the Sone
   * @return The post replies of the Sone, sorted by time (newest first)
   */
  private Collection<PostReply> getRepliesFrom(String id) {
    lock.readLock().lock();
    try {
      if (sonePostReplies.containsKey(id)) {
        return Collections.unmodifiableCollection(sonePostReplies.get(id));
      }
      return Collections.emptySet();
    } finally {
      lock.readLock().unlock();
    }
  }

  /** Loads the known post replies. */
  private void loadKnownPostReplies() {
    lock.writeLock().lock();
    try {
      int replyCounter = 0;
      while (true) {
        String knownReplyId = configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").getValue(null);
        if (knownReplyId == null) {
          break;
        }
        knownPostReplies.add(knownReplyId);
      }
    } finally {
      lock.writeLock().unlock();
    }
  }

  /**
   * Saves the known post replies to the configuration.
   *
   * @throws DatabaseException
   *     if a configuration error occurs
   */
  private void saveKnownPostReplies() throws DatabaseException {
    lock.readLock().lock();
    try {
      int replyCounter = 0;
      for (String knownReplyId : knownPostReplies) {
        configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
      }
      configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
    } catch (ConfigurationException ce1) {
      throw new DatabaseException("Could not save database.", ce1);
    } finally {
      lock.readLock().unlock();
    }
  }

}
TOP

Related Classes of net.pterodactylus.sone.database.memory.MemoryDatabase

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.