/*
* Copyright 2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.vaadin.tori.data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import org.vaadin.tori.Configuration;
import org.vaadin.tori.data.entity.Attachment;
import org.vaadin.tori.data.entity.AttachmentData;
import org.vaadin.tori.data.entity.Category;
import org.vaadin.tori.data.entity.DiscussionThread;
import org.vaadin.tori.data.entity.Post;
import org.vaadin.tori.data.entity.PostVote;
import org.vaadin.tori.data.entity.User;
import org.vaadin.tori.data.util.PersistenceUtil;
import org.vaadin.tori.exception.DataSourceException;
import org.vaadin.tori.exception.NoSuchCategoryException;
import org.vaadin.tori.service.post.PostReport.Reason;
public class TestDataSource implements DataSource {
private static final String CONTEXT = "/webapp";
private static final String ATTACHMENT_PREFIX = CONTEXT + "/attachments/";
public static Long currentUserId;
public TestDataSource() throws DataSourceException {
if (isEmptyDatabase()) {
TestDataGenerator.generateTestData();
}
}
private boolean isEmptyDatabase() throws DataSourceException {
return executeWithEntityManager(new Command<Boolean>() {
@Override
public final Boolean execute(final EntityManager em) {
boolean isEmpty = false;
try {
final TypedQuery<Long> q = em.createQuery(
"select count(c) from Category c", Long.class);
final Long categoryCount = q.getSingleResult();
isEmpty = categoryCount == 0;
} catch (Exception e) {
isEmpty = true;
}
return isEmpty;
}
});
}
private User getUser(final long userId) throws DataSourceException {
return executeWithEntityManager(new Command<User>() {
@Override
public final User execute(final EntityManager em) {
return em.find(User.class, userId);
}
});
}
@Override
public List<Category> getSubCategories(final Long categoryId)
throws DataSourceException {
Category category = null;
try {
category = getCategory(categoryId != null ? categoryId : 0);
} catch (NoSuchCategoryException e) {
}
return _getSubCategories(category);
}
private List<Category> _getSubCategories(final Category category)
throws DataSourceException {
return executeWithEntityManager(new Command<List<Category>>() {
@Override
@SuppressWarnings("unchecked")
public List<Category> execute(final EntityManager em) {
final Query q = em
.createQuery("select c from Category c where c.parentCategory "
+ (category != null ? "= :parent" : "is null"));
if (category != null) {
q.setParameter("parent", category);
}
return q.getResultList();
}
});
}
@Override
public List<DiscussionThread> getThreads(final Long categoryId,
final int startIndex, final int endIndex)
throws DataSourceException {
final Category category = getCategory(categoryId);
return executeWithEntityManager(new Command<List<DiscussionThread>>() {
@Override
public final List<DiscussionThread> execute(final EntityManager em) {
final TypedQuery<DiscussionThread> threadQuery = em
.createQuery(
"select t from DiscussionThread t "
+ "where t.category "
+ (category != null ? "= :category"
: "is null")
+ " order by t.sticky desc, t.id desc",
DiscussionThread.class);
if (startIndex >= 0 && endIndex >= 0) {
threadQuery.setFirstResult(startIndex);
threadQuery.setMaxResults(endIndex - startIndex + 1);
System.out.println("Querying threads from " + startIndex
+ " to " + endIndex + ", max results "
+ (endIndex - startIndex + 1) + ".");
}
if (category != null) {
threadQuery.setParameter("category", category);
}
return threadQuery.getResultList();
}
});
}
@Override
public List<DiscussionThread> getThreads(final Category category)
throws DataSourceException {
return getThreads(category.getId(), -1, -1);
}
@Override
public Category getCategory(final Long categoryId)
throws DataSourceException {
Category category = null;
if (categoryId != null) {
category = executeWithEntityManager(new Command<Category>() {
@Override
public final Category execute(final EntityManager em) {
return em.find(Category.class, categoryId);
}
});
}
if (categoryId != null && category == null) {
throw new NoSuchCategoryException(categoryId, null);
}
return category;
}
@Override
public int getThreadCountRecursively(final Long categoryId)
throws DataSourceException {
final Category category = getCategory(categoryId);
final long threadCount = getThreadCount(categoryId);
return executeWithEntityManager(new Command<Integer>() {
@Override
public Integer execute(final EntityManager em) {
final TypedQuery<Long> query = em
.createQuery(
"select count(t) from DiscussionThread t where t.category = :category",
Long.class);
query.setParameter("category", category);
// recursively add thread count of all sub categories
Long theThreadCount = threadCount;
try {
final List<Category> subCategories = getSubCategories(category != null ? category
.getId() : null);
for (final Category subCategory : subCategories) {
theThreadCount += getThreadCountRecursively(subCategory
.getId());
}
} catch (final DataSourceException e) {
throw new RuntimeException(e);
}
return new Long(theThreadCount).intValue();
}
});
}
@Override
public int getThreadCount(final Long categoryId) throws DataSourceException {
Category category = null;
if (categoryId != null) {
category = getCategory(categoryId);
}
final Category theCategory = category;
long result = executeWithEntityManager(new Command<Long>() {
@Override
public Long execute(final EntityManager em) {
StringBuilder sb = new StringBuilder(
"select count(t) from DiscussionThread t where t.category");
if (theCategory == null) {
sb.append(" IS NULL");
} else {
sb.append(" = :category");
}
TypedQuery<Long> q = em.createQuery(sb.toString(), Long.class);
if (theCategory != null) {
q.setParameter("category", theCategory);
}
return q.getSingleResult();
}
});
return new Long(result).intValue();
}
@Override
public DiscussionThread getThread(final long threadId)
throws DataSourceException {
return executeWithEntityManager(new Command<DiscussionThread>() {
@Override
public final DiscussionThread execute(final EntityManager em) {
return em.find(DiscussionThread.class, threadId);
}
});
}
@Override
public List<Post> getPosts(final long threadId) throws DataSourceException {
final DiscussionThread thread = getThread(threadId);
return executeWithEntityManager(new Command<List<Post>>() {
@Override
public List<Post> execute(final EntityManager em) {
final TypedQuery<Post> query = em.createQuery(
"select p from Post p where p.thread = :thread "
+ "order by p.time asc", Post.class);
query.setParameter("thread", thread);
return query.getResultList();
}
});
}
/**
* Convenience method to execute a {@link Command} with an EntityManager
* instance that will always be closed after the execution.
*
* @param command
* @return
*/
private static <T> T executeWithEntityManager(final Command<T> command)
throws DataSourceException {
final EntityManager em = PersistenceUtil.createEntityManager();
try {
return command.execute(em);
} catch (final Throwable e) {
throw new DataSourceException(e);
} finally {
em.close();
}
}
private interface Command<T> {
T execute(EntityManager em) throws DataSourceException;
}
@Override
public void deleteCategory(final long categoryId)
throws DataSourceException {
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em) {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
// must merge detached entity before removal
em.remove(em.find(Category.class, categoryId));
transaction.commit();
} catch (final Exception e) {
e.printStackTrace();
if (transaction.isActive()) {
transaction.rollback();
}
}
return null;
}
});
}
@Override
public int getUnreadThreadCount(final long categoryId) {
return 0;
}
@Override
public void savePost(final long postId, final String rawBody)
throws DataSourceException {
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em) {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
Post post = em.find(Post.class, postId);
post.setBodyRaw(rawBody);
transaction.commit();
return null;
}
});
}
@Override
public void banUser(final long userId) throws DataSourceException {
User user = getUser(userId);
user.setBanned(true);
save(user);
}
@Override
public void unbanUser(final long userId) throws DataSourceException {
User user = getUser(userId);
user.setBanned(false);
save(user);
}
private void save(final User user) throws DataSourceException {
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em) {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.merge(user);
transaction.commit();
return null;
}
});
}
@Override
@SuppressWarnings("deprecation")
public void followThread(final long threadId) throws DataSourceException {
DiscussionThread thread = getThread(threadId);
if (!isFollowingThread(threadId)) {
final org.vaadin.tori.data.entity.Following following = new org.vaadin.tori.data.entity.Following();
following.setFollower(getCurrentUser());
following.setThread(thread);
save(following);
}
}
@SuppressWarnings("deprecation")
private void save(final org.vaadin.tori.data.entity.Following following)
throws DataSourceException {
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em) {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.merge(following);
transaction.commit();
return null;
}
});
}
@Override
public void unfollowThread(final long threadId) throws DataSourceException {
final DiscussionThread thread = getThread(threadId);
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em) {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
final Query query = em
.createQuery("delete from Following f where f.thread = :thread and f.follower = :follower");
query.setParameter("thread", thread);
query.setParameter("follower", getCurrentUser());
query.executeUpdate();
} finally {
transaction.commit();
}
return null;
}
});
}
@Override
public boolean isFollowingThread(final long threadId) {
try {
final DiscussionThread thread = getThread(threadId);
return executeWithEntityManager(new Command<Boolean>() {
@Override
public Boolean execute(final EntityManager em) {
final TypedQuery<Long> query = em
.createQuery(
"select count(f) from Following f where f.follower = :follower AND f.thread = :thread",
Long.class);
query.setParameter("follower", getCurrentUser());
query.setParameter("thread", thread);
final Long result = query.getSingleResult();
if (result != null) {
return result > 0;
} else {
return false;
}
}
});
} catch (DataSourceException e) {
return false;
}
}
@Override
public void deletePost(final long postId) throws DataSourceException {
final Post post = getPost(postId);
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em)
throws DataSourceException {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
final DiscussionThread thread = post.getThread();
thread.getPosts().remove(post);
// must merge detached entity before removal
final DiscussionThread mergedThread = em.merge(thread);
em.merge(mergedThread);
transaction.commit();
} catch (final Exception e) {
e.printStackTrace();
if (transaction.isActive()) {
transaction.rollback();
}
throw new DataSourceException(e);
}
return null;
}
});
}
@Override
public Boolean getPostVote(final long postId) throws DataSourceException {
Boolean result = null;
PostVote vote = getPostVoteInternal(postId);
if (vote.isDownvote()) {
result = false;
} else if (vote.isUpvote()) {
result = true;
}
return result;
}
private PostVote getPostVoteInternal(final long postId)
throws DataSourceException {
final Post post = getPost(postId);
return executeWithEntityManager(new Command<PostVote>() {
@Override
public PostVote execute(final EntityManager em) {
try {
final TypedQuery<PostVote> query = em.createQuery(
"select v from PostVote v where v.voter = :voter "
+ "and v.post = :post", PostVote.class);
query.setParameter("voter", getCurrentUser());
query.setParameter("post", post);
return query.getSingleResult();
} catch (final NoResultException e) {
final PostVote vote = new PostVote();
vote.setPost(post);
vote.setVoter(getCurrentUser());
return vote;
}
}
});
}
@Override
public void upvote(final long postId) throws DataSourceException {
final PostVote vote = getPostVoteInternal(postId);
vote.setUpvote();
save(vote);
}
@Override
public void downvote(final long postId) throws DataSourceException {
final PostVote vote = getPostVoteInternal(postId);
vote.setDownvote();
save(vote);
}
public void save(final PostVote vote) throws DataSourceException {
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em) {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.merge(vote);
Post post = em.find(Post.class, vote.getPost().getId());
List<PostVote> postVotes = post.getPostVotes();
if (postVotes == null) {
post.setPostVotes(new ArrayList<PostVote>());
postVotes = post.getPostVotes();
}
postVotes.add(vote);
transaction.commit();
return null;
}
});
}
@Override
public void removeUserVote(final long postId) throws DataSourceException {
delete(getPostVoteInternal(postId));
}
private void delete(final PostVote postVote) throws DataSourceException {
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em) {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
// must merge detached entity before removal
final PostVote mergedVote = em.merge(postVote);
em.remove(mergedVote);
transaction.commit();
} catch (final Exception e) {
e.printStackTrace();
if (transaction.isActive()) {
transaction.rollback();
}
}
return null;
}
});
}
@Override
public long getPostScore(final long postId) throws DataSourceException {
final Post post = getPost(postId);
return executeWithEntityManager(new Command<Long>() {
@Override
public Long execute(final EntityManager em) {
final TypedQuery<Long> query = em
.createQuery(
"select SUM(v.vote) from PostVote v where v.post = :post",
Long.class);
query.setParameter("post", post);
final Long singleResult = query.getSingleResult();
if (singleResult != null) {
return singleResult;
} else {
return 0L;
}
}
});
}
@Override
public Post saveReply(final String rawBody,
final Map<String, byte[]> attachments, final long threadId)
throws DataSourceException {
return executeWithEntityManager(new Command<Post>() {
@Override
public Post execute(final EntityManager em) {
em.getTransaction().begin();
final Post post = new Post();
post.setBodyRaw(rawBody);
post.setAuthor(getCurrentUser());
post.setTime(new Date());
post.setFormatBBCode(true);
final DiscussionThread thread = em.find(DiscussionThread.class,
threadId);
thread.getPosts().add(post);
em.persist(post);
post.setThread(thread);
persistPostAttachments(post, attachments, em);
em.getTransaction().commit();
return post;
}
});
}
private void persistPostAttachments(final Post post,
final Map<String, byte[]> files, final EntityManager em) {
if (files == null) {
return;
}
if (!em.getTransaction().isActive()) {
throw new IllegalArgumentException("Transaction inactive");
}
if (!em.contains(post)) {
throw new IllegalArgumentException("Post instance not managed");
}
if (post.getAttachments() == null) {
post.setAttachments(new ArrayList<Attachment>());
}
for (final Entry<String, byte[]> entry : files.entrySet()) {
final Attachment attachment = new Attachment();
attachment.setFilename(entry.getKey());
attachment.setPost(post);
attachment.setFileSize(entry.getValue().length);
em.persist(attachment);
post.getAttachments().add(attachment);
final AttachmentData attachmentData = new AttachmentData();
attachmentData.setAttachment(attachment);
attachmentData.setData(entry.getValue());
em.persist(attachmentData);
em.flush();
attachment.setDownloadUrl(ATTACHMENT_PREFIX
+ attachmentData.getId() + "/" + attachment.getFilename());
}
}
@Override
public void moveThread(final long threadId, final Long destinationCategoryId)
throws DataSourceException {
final DiscussionThread thread = getThread(threadId);
final Category destinationCategory = getCategory(destinationCategoryId);
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em) {
thread.setCategory(destinationCategory);
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.merge(thread);
transaction.commit();
return null;
}
});
}
@Override
public void stickyThread(final long threadId) throws DataSourceException {
DiscussionThread thread = getThread(threadId);
thread.setSticky(true);
save(thread);
}
@Override
public void unstickyThread(final long threadId) throws DataSourceException {
DiscussionThread thread = getThread(threadId);
thread.setSticky(false);
save(thread);
}
@Override
public void lockThread(final long threadId) throws DataSourceException {
DiscussionThread thread = getThread(threadId);
thread.setLocked(true);
save(thread);
}
@Override
public void unlockThread(final long threadId) throws DataSourceException {
DiscussionThread thread = getThread(threadId);
thread.setLocked(false);
save(thread);
}
private static DiscussionThread save(final DiscussionThread thread)
throws DataSourceException {
return executeWithEntityManager(new Command<DiscussionThread>() {
@Override
public DiscussionThread execute(final EntityManager em) {
final EntityTransaction t = em.getTransaction();
t.begin();
final DiscussionThread mergedThread = em.merge(thread);
t.commit();
return mergedThread;
}
});
}
@Override
public void deleteThread(final long threadId) throws DataSourceException {
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em) {
final EntityTransaction t = em.getTransaction();
t.begin();
final DiscussionThread thread = em.find(DiscussionThread.class,
threadId);
// remove all Following references
final Query followDelete = em
.createQuery("delete from Following f where f.thread = :thread");
followDelete.setParameter("thread", thread);
followDelete.executeUpdate();
try {
// remove all votes for posts inside thread.
for (final Post post : getPosts(thread.getId())) {
// "in" is not supported :(
final Query postDelete = em
.createQuery("delete from PostVote where post = :post");
postDelete.setParameter("post", post);
postDelete.executeUpdate();
}
} catch (final DataSourceException e) {
throw new RuntimeException(e);
}
em.remove(thread);
t.commit();
return null;
}
});
}
@Override
public Post saveNewThread(final String topic, final String rawBody,
final Map<String, byte[]> attachments, final Long categoryId)
throws DataSourceException {
final Category category = categoryId == null ? null
: getCategory(categoryId);
return executeWithEntityManager(new Command<Post>() {
@Override
public Post execute(final EntityManager em) {
DiscussionThread newThread = new DiscussionThread();
newThread.setCategory(category);
newThread.setTopic(topic);
Post firstPost = new Post();
firstPost.setFormatBBCode(true);
firstPost.setAuthor(getCurrentUser());
firstPost.setBodyRaw(rawBody);
firstPost.setTime(new Date());
newThread.setPosts(Arrays.asList(firstPost));
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
final DiscussionThread mergedThread = em.merge(newThread);
firstPost.setThread(mergedThread);
final Post post = em.merge(firstPost);
persistPostAttachments(post, attachments, em);
transaction.commit();
return post;
}
});
}
@Override
public void incrementViewCount(final DiscussionThread thread)
throws DataSourceException {
executeWithEntityManager(new Command<Void>() {
@Override
public Void execute(final EntityManager em) {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
final Query q = em
.createQuery("update DiscussionThread t set t.viewCount = t.viewCount + 1 where t = :thread");
q.setParameter("thread", thread);
q.executeUpdate();
transaction.commit();
return null;
}
});
}
@Override
public boolean isThreadRead(final long threadId) {
return new Random().nextBoolean();
}
@Override
public void markThreadRead(final long threadId) throws DataSourceException {
System.out.println(String.format(
"Marking thread %d as read. Not actually implemented in "
+ getClass().getSimpleName() + ".", threadId));
}
@Override
public List<DiscussionThread> getRecentPosts(final int from, final int to)
throws DataSourceException {
final List<Post> posts = executeWithEntityManager(new Command<List<Post>>() {
@Override
public List<Post> execute(final EntityManager em) {
final EntityTransaction tx = em.getTransaction();
tx.begin();
final TypedQuery<Post> q = em
.createQuery(
"select p from Post p order by p.time desc",
Post.class);
q.setFirstResult(from);
q.setMaxResults(to - from + 1);
final List<Post> result = q.getResultList();
tx.commit();
return result;
}
});
final List<DiscussionThread> threads = new ArrayList<DiscussionThread>();
for (final Post post : posts) {
threads.add(post.getThread());
}
return threads;
}
@Override
public int getRecentPostsCount() throws DataSourceException {
final Number number = executeWithEntityManager(new Command<Number>() {
@Override
public Number execute(final EntityManager em) {
final EntityTransaction tx = em.getTransaction();
tx.begin();
final TypedQuery<Number> q = em.createQuery(
"select count(p) from Post p", Number.class);
final Number result = q.getSingleResult();
tx.commit();
return result;
}
});
return number.intValue();
}
@Override
public List<DiscussionThread> getMyPostThreads(final int from, final int to)
throws DataSourceException {
System.out.println("TestDataSource.getMyPostThreads(): "
+ "getMyPostThreads not implemented in "
+ getClass().getSimpleName() + ".");
return Collections.emptyList();
}
@Override
public int getMyPostThreadsCount() throws DataSourceException {
System.out.println("TestDataSource.getMyPostThreadsCount(): "
+ "getMyPostThreads not implemented in "
+ getClass().getSimpleName() + ".");
return 0;
}
@Override
public int getAttachmentMaxFileSize() {
return 307200;
}
@Override
public boolean isLoggedInUser() {
return true;
}
@Override
public void save(final Configuration conf) {
}
@Override
@Deprecated
public String getPathRoot() {
return null;
}
@Override
public User getToriUser(final long userId) {
User user = null;
if (userId > 0) {
try {
user = getUser(userId);
} catch (DataSourceException e) {
e.printStackTrace();
}
}
return user;
}
@Override
public Post getPost(final long postId) throws DataSourceException {
return executeWithEntityManager(new Command<Post>() {
@Override
public final Post execute(final EntityManager em) {
return em.find(Post.class, postId);
}
});
}
@Override
public void saveNewCategory(final Long parentCategoryId, final String name,
final String description) throws DataSourceException {
executeWithEntityManager(new Command<Category>() {
@Override
public Category execute(final EntityManager em) {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
Category category = new Category();
category.setName(name);
category.setDescription(description);
if (parentCategoryId != null) {
category.setParentCategory(getCategory(parentCategoryId));
}
em.persist(category);
transaction.commit();
} catch (final Exception e) {
e.printStackTrace();
if (transaction.isActive()) {
transaction.rollback();
}
}
return null;
}
});
}
@Override
public void updateCategory(final long categoryId, final String name,
final String description) throws DataSourceException {
executeWithEntityManager(new Command<Category>() {
@Override
public Category execute(final EntityManager em) {
final EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
Category category = em.find(Category.class, categoryId);
category.setName(name);
category.setDescription(description);
transaction.commit();
} catch (final Exception e) {
e.printStackTrace();
if (transaction.isActive()) {
transaction.rollback();
}
}
return null;
}
});
}
@Override
public void reportPost(final long postId, final Reason reason,
final String additionalInfo, final String postUrl) {
System.out.println("TestDataSource.reportPost()");
System.out.println("Post: " + postId);
System.out.println("Reason: " + reason);
System.out.println("Info: " + additionalInfo);
}
@Override
public User getCurrentUser() {
User result = null;
try {
result = getUser(currentUserId);
} catch (DataSourceException e) {
// Ignore
}
return result;
}
@Override
public void markThreadUnRead(final long threadId)
throws DataSourceException {
// Ignore
}
@Override
public Configuration getConfiguration() {
Configuration configuration = new Configuration();
configuration.setUseToriMailService(true);
configuration.setShowThreadsOnDashboard(true);
configuration.setMayNotReplyNote("Please log in to reply");
configuration.setUpdatePageTitle(true);
configuration.setPageTitlePrefix("Tori");
configuration.setReplacements(new HashMap<String, String>());
configuration.setReplaceMessageBoardsLinks(false);
configuration.setGoogleAnalyticsTrackerId(null);
return configuration;
}
}