org.vaadin.tori.data.LiferayDataSource.java Source code

Java tutorial

Introduction

Here is the source code for org.vaadin.tori.data.LiferayDataSource.java

Source

/*
 * 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.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.portlet.PortletPreferences;
import javax.portlet.PortletRequest;
import javax.portlet.ReadOnlyException;
import javax.portlet.ValidatorException;

import org.apache.log4j.Logger;
import org.vaadin.tori.Configuration;
import org.vaadin.tori.PortletRequestAware;
import org.vaadin.tori.data.entity.Attachment;
import org.vaadin.tori.data.entity.Category;
import org.vaadin.tori.data.entity.DiscussionThread;
import org.vaadin.tori.data.entity.LiferayEntityFactoryUtil;
import org.vaadin.tori.data.entity.Post;
import org.vaadin.tori.data.entity.User;
import org.vaadin.tori.exception.DataSourceException;
import org.vaadin.tori.service.post.PostReport.Reason;

import com.liferay.portal.NoSuchUserException;
import com.liferay.portal.kernel.dao.orm.DynamicQuery;
import com.liferay.portal.kernel.dao.orm.DynamicQueryFactoryUtil;
import com.liferay.portal.kernel.dao.orm.OrderFactoryUtil;
import com.liferay.portal.kernel.dao.orm.ProjectionFactoryUtil;
import com.liferay.portal.kernel.dao.orm.PropertyFactoryUtil;
import com.liferay.portal.kernel.exception.NestableException;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.repository.model.FileEntry;
import com.liferay.portal.kernel.util.ObjectValuePair;
import com.liferay.portal.kernel.util.PortalClassLoaderUtil;
import com.liferay.portal.kernel.util.PrefsPropsUtil;
import com.liferay.portal.kernel.util.PropsKeys;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.WebKeys;
import com.liferay.portal.kernel.workflow.WorkflowConstants;
import com.liferay.portal.portletfilerepository.PortletFileRepositoryUtil;
import com.liferay.portal.service.ServiceContext;
import com.liferay.portal.service.ServiceContextFactory;
import com.liferay.portal.service.SubscriptionLocalServiceUtil;
import com.liferay.portal.service.UserLocalServiceUtil;
import com.liferay.portal.theme.ThemeDisplay;
import com.liferay.portal.util.PortalUtil;
import com.liferay.portlet.PortletPreferencesFactoryUtil;
import com.liferay.portlet.flags.service.FlagsEntryServiceUtil;
import com.liferay.portlet.messageboards.NoSuchCategoryException;
import com.liferay.portlet.messageboards.NoSuchThreadException;
import com.liferay.portlet.messageboards.model.MBBan;
import com.liferay.portlet.messageboards.model.MBCategory;
import com.liferay.portlet.messageboards.model.MBMessage;
import com.liferay.portlet.messageboards.model.MBMessageConstants;
import com.liferay.portlet.messageboards.model.MBThread;
import com.liferay.portlet.messageboards.model.MBThreadConstants;
import com.liferay.portlet.messageboards.model.MBThreadFlag;
import com.liferay.portlet.messageboards.service.MBBanLocalServiceUtil;
import com.liferay.portlet.messageboards.service.MBBanServiceUtil;
import com.liferay.portlet.messageboards.service.MBCategoryLocalServiceUtil;
import com.liferay.portlet.messageboards.service.MBCategoryServiceUtil;
import com.liferay.portlet.messageboards.service.MBMessageLocalServiceUtil;
import com.liferay.portlet.messageboards.service.MBMessageServiceUtil;
import com.liferay.portlet.messageboards.service.MBThreadFlagLocalServiceUtil;
import com.liferay.portlet.messageboards.service.MBThreadLocalServiceUtil;
import com.liferay.portlet.messageboards.service.MBThreadServiceUtil;
import com.liferay.portlet.messageboards.util.comparator.MessageCreateDateComparator;
import com.liferay.portlet.ratings.NoSuchEntryException;
import com.liferay.portlet.ratings.model.RatingsEntry;
import com.liferay.portlet.ratings.model.RatingsStats;
import com.liferay.portlet.ratings.service.RatingsEntryLocalServiceUtil;
import com.liferay.portlet.ratings.service.RatingsEntryServiceUtil;
import com.liferay.portlet.ratings.service.RatingsStatsLocalServiceUtil;

public class LiferayDataSource implements DataSource, PortletRequestAware {

    private static final Logger LOG = Logger.getLogger(LiferayDataSource.class);
    private static final boolean INCLUDE_SUBSCRIBED = false;
    private static final boolean INCLUDE_ANONYMOUS = false;

    private static final long ROOT_CATEGORY_ID = 0;
    protected static final int QUERY_ALL = com.liferay.portal.kernel.dao.orm.QueryUtil.ALL_POS;
    // TODO this should be dynamic as it can be customized in liferay
    private static final double STICKY_PRIORITY = 2.0d;

    protected long scopeGroupId = -1;
    protected long currentUserId;
    private String imagePath;

    private ServiceContext mbBanServiceContext;
    protected ServiceContext flagsServiceContext;
    protected ServiceContext mbCategoryServiceContext;
    protected ServiceContext mbMessageServiceContext;

    protected ThemeDisplay themeDisplay;
    private PortletRequest request;

    private static final String PREFS_ANALYTICS_ID = "analytics";
    private static final String PREFS_REPLACE_MESSAGE_BOARDS_LINKS = "toriReplaceMessageBoardsLinks";
    private static final String PREFS_UPDATE_PAGE_TITLE = "toriUpdatePageTitle";
    private static final String PREFS_PAGE_TITLE_PREFIX = "toriPageTitlePrefix";
    private static final String PREFS_MAY_NOT_REPLY_NOTE = "mayNotReplyNote";
    private static final String PREFS_SHOW_THREADS_ON_DASHBOARD = "showThreadsOnDashboard";
    private static final String PREFS_USE_TORI_MAIL_SERVICE = "useToriMailService";
    public static final String PREFS_EMAIL_HEADER_IMAGE_URL = "emailHeaderImageUrl";
    public static final String PREFS_EMAIL_FROM_ADDRESS = "emailFromAddress";
    public static final String PREFS_EMAIL_FROM_NAME = "emailFromName";
    public static final String PREFS_EMAIL_REPLY_TO_ADDRESS = "emailReplyToAddress";

    private static final String PREFS_REPLACEMENTS_KEY = "toriPostReplacements";
    private static final String REPLACEMENT_SEPARATOR = "<TORI-REPLACEMENT>";

    @Override
    public List<Category> getSubCategories(final Long categoryId) throws DataSourceException {
        final long parentCategoryId = normalizeCategoryId(categoryId);

        try {
            List<MBCategory> categories = MBCategoryLocalServiceUtil.getCategories(scopeGroupId, parentCategoryId,
                    QUERY_ALL, QUERY_ALL);
            if (LOG.isDebugEnabled()) {
                LOG.debug(String.format("Found %d categories.", categories.size()));
            }
            return LiferayEntityFactoryUtil.createCategories(categories, this);
        } catch (final SystemException e) {
            LOG.error(String.format("Couldn't get subcategories for parent category %d.", parentCategoryId), e);
            throw new DataSourceException(e);
        }
    }

    public static long getRootMessageId(final long threadId) throws DataSourceException {
        try {
            final MBThread liferayThread = MBThreadLocalServiceUtil.getMBThread(threadId);
            return liferayThread.getRootMessageId();
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't get root message id for thread %d.", threadId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public List<DiscussionThread> getThreads(final Long categoryId, final int startIndex, int endIndex)
            throws DataSourceException {
        try {
            if (endIndex != QUERY_ALL) {
                // adjust the endIndex to be inclusive
                endIndex += 1;
            }
            final List<MBThread> liferayThreads = getLiferayThreadsForCategory(normalizeCategoryId(categoryId),
                    startIndex, endIndex);

            final Category category = getCategory(categoryId);

            // collection for the final result
            final List<DiscussionThread> result = new ArrayList<DiscussionThread>(liferayThreads.size());
            for (final MBThread liferayThread : liferayThreads) {
                final DiscussionThread thread = wrapLiferayThread(liferayThread, category);
                result.add(thread);
            }
            return result;
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't get threads for category %d.", categoryId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public List<DiscussionThread> getThreads(final Category category) throws DataSourceException {
        final int startIndex = QUERY_ALL; // use QUERY_ALL to get all
        final int endIndex = QUERY_ALL; // use QUERY_ALL get all
        return getThreads(category.getId(), startIndex, endIndex);
    }

    @Override
    public int getMyPostThreadsCount() throws DataSourceException {
        // Not an optimal solution (performance-wise), but currently
        // MBThreadServiceUtil.getGroupThreadsCount doesn't _always_ give the
        // same count for my threads as getMyPostThreads does.
        final int groupThreadsCount = getMyPostThreads(QUERY_ALL, QUERY_ALL).size();
        LOG.debug("LiferayDataSource.getMyPostThreadsCount(): " + groupThreadsCount);
        return groupThreadsCount;
    }

    @Override
    public List<DiscussionThread> getMyPostThreads(final int from, final int to) throws DataSourceException {
        if (isLoggedInUser()) {
            try {
                final List<MBThread> liferayThreads = MBThreadServiceUtil.getGroupThreads(scopeGroupId,
                        currentUserId, WorkflowConstants.STATUS_ANY, from, to);
                final List<DiscussionThread> result = new ArrayList<DiscussionThread>(liferayThreads.size());
                for (final MBThread liferayThread : liferayThreads) {
                    final DiscussionThread thread = wrapLiferayThread(liferayThread, null);
                    result.add(thread);
                }

                return result;
            } catch (Exception e) {
                // getGroupThreads() failed, handle with getGroupMessages
                return getMyPostThreadsFromMessages(from, to);
            }
        } else {
            return Collections.emptyList();
        }
    }

    @Override
    public int getRecentPostsCount() throws DataSourceException {
        try {
            return MBThreadServiceUtil.getGroupThreadsCount(scopeGroupId, 0, WorkflowConstants.STATUS_APPROVED,
                    INCLUDE_ANONYMOUS, INCLUDE_SUBSCRIBED);
        } catch (final SystemException e) {
            LOG.error("Couldn't get amount of recent threads.", e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public List<DiscussionThread> getRecentPosts(final int from, final int to) throws DataSourceException {
        final List<DiscussionThread> result = new ArrayList<DiscussionThread>();

        Collection categoryIdsRecursively = getCategoryIdsRecursively(ROOT_CATEGORY_ID);

        DynamicQuery dynamicQuery = DynamicQueryFactoryUtil
                .forClass(MBThread.class, PortalClassLoaderUtil.getClassLoader())
                .add(PropertyFactoryUtil.forName("groupId").eq(scopeGroupId))
                .add(PropertyFactoryUtil.forName("status").eq(WorkflowConstants.STATUS_APPROVED))
                .add(PropertyFactoryUtil.forName("categoryId").in(categoryIdsRecursively))
                .addOrder(OrderFactoryUtil.desc("priority")).addOrder(OrderFactoryUtil.desc("lastPostDate"));

        try {
            List<?> liferayThreads = MBThreadLocalServiceUtil.dynamicQuery(dynamicQuery, from, to);

            for (final Object object : liferayThreads) {
                try {
                    if (object instanceof MBThread) {
                        final DiscussionThread thread = wrapLiferayThread((MBThread) object, null);
                        result.add(thread);
                    }
                } catch (NestableException e1) {
                    LOG.info("Mapping of an MBThread failed", e1);
                }
            }
        } catch (SystemException e1) {
            LOG.info("Dynamic query for recent threads failed", e1);
            throw new DataSourceException(e1);
        }

        return result;
    }

    private List<DiscussionThread> getMyPostThreadsFromMessages(final int from, final int to)
            throws DataSourceException {
        try {
            // collection for the final result
            final List<DiscussionThread> threads = new ArrayList<DiscussionThread>();
            final Map<Long, Date> myLastPostDates = new HashMap<Long, Date>();
            final Set<Long> processedThreads = new HashSet<Long>();
            for (final MBMessage liferayMessage : MBMessageLocalServiceUtil.getGroupMessages(scopeGroupId,
                    currentUserId, WorkflowConstants.STATUS_ANY, QUERY_ALL, QUERY_ALL)) {
                if (processedThreads.add(liferayMessage.getThreadId())) {
                    try {
                        MBThread liferayThread = liferayMessage.getThread();
                        myLastPostDates.put(liferayMessage.getThreadId(), liferayThread.getLastPostDate());
                        final DiscussionThread thread = wrapLiferayThread(liferayThread, null);
                        threads.add(thread);
                    } catch (NoSuchThreadException e) {
                        // Ignore and continue
                    }
                }

            }

            Collections.sort(threads, new Comparator<DiscussionThread>() {
                @Override
                public int compare(final DiscussionThread t1, final DiscussionThread t2) {
                    return myLastPostDates.get(t2.getId()).compareTo(myLastPostDates.get(t1.getId()));

                }
            });

            int toIndex = to == -1 ? threads.size() - 1 : to;

            if (toIndex > threads.size() - 1) {
                toIndex = threads.size() - 1;
            }

            if (toIndex < 0) {
                toIndex = 0;
            }

            return threads.subList(Math.max(0, from), toIndex);
        } catch (final NestableException e) {
            LOG.error("Couldn't get my posts.", e);
            throw new DataSourceException(e);
        }
    }

    protected DiscussionThread wrapLiferayThread(final MBThread liferayThread, Category category)
            throws PortalException, SystemException, DataSourceException {
        // get the root message of the thread
        final MBMessage rootMessage = MBMessageLocalServiceUtil.getMessage(liferayThread.getRootMessageId());
        // get the author of the root message
        final User threadAuthor = getUser(rootMessage.getUserId());
        // get the author of the last post
        final User lastPostAuthor = getUser(liferayThread.getLastPostByUserId());

        if (category == null) {
            // fetch the category
            category = getCategory(liferayThread.getCategoryId());
        }

        return LiferayEntityFactoryUtil.createDiscussionThread(category, liferayThread, rootMessage, threadAuthor,
                lastPostAuthor, liferayThread.getPriority() > 0, this);
    }

    private User getUser(final long userId) throws PortalException, SystemException {
        if (userId == 0) {
            return LiferayEntityFactoryUtil.createAnonymousUser(imagePath);
        } else {
            try {
                final com.liferay.portal.model.User liferayUser = UserLocalServiceUtil.getUser(userId);
                if (liferayUser.isDefaultUser()) {
                    return LiferayEntityFactoryUtil.createAnonymousUser(imagePath);
                } else {
                    final boolean isBanned = MBBanLocalServiceUtil.hasBan(scopeGroupId, liferayUser.getUserId());

                    String userLink = null;
                    if (liferayUser.getGroup() != null && liferayUser.getPublicLayoutsPageCount() > 0) {
                        userLink = liferayUser.getDisplayURL(themeDisplay);
                    }

                    return LiferayEntityFactoryUtil.createUser(liferayUser, imagePath, userLink,
                            liferayUser.isFemale(), isBanned);
                }
            } catch (NoSuchUserException e) {
                return LiferayEntityFactoryUtil.createAnonymousUser(imagePath);
            }
        }
    }

    private List<MBThread> getLiferayThreadsForCategory(final long categoryId, final int start, final int end)
            throws SystemException {
        final List<MBThread> liferayThreads = MBThreadLocalServiceUtil.getThreads(scopeGroupId, categoryId,
                WorkflowConstants.STATUS_APPROVED, start, end);
        if (LOG.isDebugEnabled()) {
            LOG.debug(
                    String.format("Found %d threads for category with id %d.", liferayThreads.size(), categoryId));
        }
        return liferayThreads;
    }

    @Override
    public Category getCategory(final Long categoryId) throws DataSourceException {
        try {
            return LiferayEntityFactoryUtil
                    .createCategory(MBCategoryLocalServiceUtil.getCategory(normalizeCategoryId(categoryId)), this);
        } catch (final NoSuchCategoryException e) {
            throw new org.vaadin.tori.exception.NoSuchCategoryException(categoryId, e);
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't get category for id %d.", categoryId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public int getThreadCountRecursively(final Long categoryId) throws DataSourceException {
        try {
            int count = MBThreadLocalServiceUtil.getCategoryThreadsCount(scopeGroupId,
                    normalizeCategoryId(categoryId), WorkflowConstants.STATUS_APPROVED);

            // recursively add thread count of all sub categories
            List<MBCategory> subCategories = MBCategoryLocalServiceUtil.getCategories(scopeGroupId,
                    normalizeCategoryId(categoryId), QUERY_ALL, QUERY_ALL);
            for (final MBCategory subCategory : subCategories) {
                count += getThreadCountRecursively(subCategory.getCategoryId());
            }
            return count;
        } catch (final SystemException e) {
            LOG.error(String.format("Couldn't get recursive thread count for category %d.", categoryId), e);
            throw new DataSourceException(e);
        }
    }

    protected Collection<Long> getCategoryIdsRecursively(final Long rootCategoryId) throws DataSourceException {
        Collection<Long> categories = new ArrayList<Long>();
        categories.add(rootCategoryId);
        try {
            List<MBCategory> subCategories = MBCategoryLocalServiceUtil.getCategories(scopeGroupId, rootCategoryId,
                    QUERY_ALL, QUERY_ALL);
            for (final MBCategory subCategory : subCategories) {
                categories.addAll(getCategoryIdsRecursively(subCategory.getCategoryId()));
            }
            return categories;
        } catch (final SystemException e) {
            throw new DataSourceException(e);
        }
    }

    @Override
    public int getThreadCount(final Long categoryId) throws DataSourceException {
        try {
            return MBThreadLocalServiceUtil.getCategoryThreadsCount(scopeGroupId, normalizeCategoryId(categoryId),
                    WorkflowConstants.STATUS_APPROVED);
        } catch (final SystemException e) {
            LOG.error(String.format("Couldn't get thread count for category %d.", categoryId), e);
            throw new DataSourceException(e);
        }
    }

    public static long normalizeCategoryId(final Long categoryId) {
        return categoryId == null ? ROOT_CATEGORY_ID : categoryId;
    }

    @Override
    public DiscussionThread getThread(final long threadId) throws DataSourceException {
        try {
            final MBThread thread = MBThreadLocalServiceUtil.getMBThread(threadId);
            final Category category = LiferayEntityFactoryUtil
                    .createCategory(MBCategoryLocalServiceUtil.getCategory(thread.getCategoryId()), this);
            return wrapLiferayThread(thread, category);
        } catch (final NoSuchThreadException e) {
            throw new org.vaadin.tori.exception.NoSuchThreadException(threadId, e);
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't get thread for id %d.", threadId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public void incrementViewCount(final DiscussionThread thread) throws DataSourceException {
        try {
            // Reload the thread to get the latest view count.
            // Here we have a race condition, but this is the same way Liferay
            // handles the view count incrementation.
            final MBThread liferayThread = MBThreadLocalServiceUtil.getThread(thread.getId());
            MBThreadLocalServiceUtil.updateThread(liferayThread.getThreadId(), liferayThread.getViewCount() + 1);
        } catch (final PortalException e) {
            LOG.error(String.format("Couldn't increment view count for thread %d.", thread.getId()), e);
            throw new DataSourceException(e);
        } catch (final SystemException e) {
            LOG.error(String.format("Couldn't increment view count for thread %d.", thread.getId()), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public List<Post> getPosts(final long threadId) throws DataSourceException {
        try {
            final List<MBMessage> messages = getLiferayPostsForThread(threadId);
            final List<Post> result = new ArrayList<Post>(messages.size());
            final DiscussionThread thread = getThread(threadId);
            for (final MBMessage message : messages) {
                result.add(internalGetPost(message, thread));
            }
            return result;
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't get posts for thread %d.", threadId), e);
            throw new DataSourceException(e);
        }
    }

    public List<MBMessage> getLiferayPostsForThread(final long threadId) throws SystemException {
        @SuppressWarnings("unchecked")
        final Comparator<MBMessage> comparator = new MessageCreateDateComparator(true);
        return MBMessageLocalServiceUtil.getThreadMessages(threadId, WorkflowConstants.STATUS_APPROVED, comparator);
    }

    @Override
    public void updateCategory(final long categoryId, final String name, final String description)
            throws DataSourceException {
        try {
            LOG.debug("Updating existing category: " + categoryId);
            final MBCategory category = MBCategoryLocalServiceUtil.getCategory(categoryId);
            category.setName(name);
            category.setDescription(description);
            MBCategoryLocalServiceUtil.updateMBCategory(category);
        } catch (NestableException e) {
            LOG.error(String.format("Cannot save category %d", categoryId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public void deleteCategory(final long categoryId) throws DataSourceException {
        try {
            MBCategoryServiceUtil.deleteCategory(scopeGroupId, categoryId);
        } catch (final NestableException e) {
            LOG.error(String.format("Cannot delete category %d", categoryId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public void reportPost(final long postId, final Reason reason, final String additionalInfo,
            final String postUrl) {
        String reporterEmailAddress = "";
        try {
            reporterEmailAddress = UserLocalServiceUtil.getUser(currentUserId).getEmailAddress();
        } catch (final NestableException e) {
            LOG.error("Couldn't get the email address of current user.", e);
        }

        try {
            Post post = getPost(postId);
            final long reportedUserId = post.getAuthor().getId();
            final String contentTitle = post.getThread().getTopic();
            final String contentURL = postUrl;
            String reasonString = reason.toString();
            if (additionalInfo != null && !additionalInfo.isEmpty()) {
                reasonString += ": " + additionalInfo;
            }

            FlagsEntryServiceUtil.addEntry(MBMessage.class.getName(), postId, reporterEmailAddress, reportedUserId,
                    contentTitle, contentURL, reasonString, flagsServiceContext);
        } catch (DataSourceException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void savePost(final long postId, final String bodyRaw) {
        try {
            // Currently only editing of message body allowed
            MBMessageLocalServiceUtil.updateMessage(postId, bodyRaw);
        } catch (final Exception e) {
            LOG.error("Editing message failed", e);
        }
    }

    @Override
    public void banUser(final long userId) throws DataSourceException {
        try {
            MBBanServiceUtil.addBan(userId, mbBanServiceContext);
        } catch (NestableException e) {
            LOG.error(String.format("Cannot ban user %d", userId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public void unbanUser(final long userId) throws DataSourceException {
        try {
            MBBanServiceUtil.deleteBan(userId, mbBanServiceContext);
        } catch (final NestableException e) {
            LOG.error(String.format("Cannot unban user %d", userId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public void unfollowThread(final long threadId) throws DataSourceException {
        try {
            SubscriptionLocalServiceUtil.deleteSubscription(currentUserId, MBThread.class.getName(), threadId);
        } catch (final NestableException e) {
            LOG.error(String.format("Cannot unfollow thread %d", threadId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public boolean isFollowingThread(final long threadId) {
        boolean result = false;
        if (isLoggedInUser()) {
            try {
                final com.liferay.portal.model.User user = UserLocalServiceUtil.getUser(currentUserId);
                result = SubscriptionLocalServiceUtil.isSubscribed(user.getCompanyId(), user.getUserId(),
                        MBThread.class.getName(), threadId);
            } catch (final NestableException e) {
                LOG.error(String.format("Cannot check if user is following thread %d", threadId), e);
            }
        }
        return result;
    }

    @Override
    public void deletePost(final long postId) throws DataSourceException {
        try {
            MBMessageServiceUtil.deleteMessage(postId);
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't delete post %d.", postId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public Boolean getPostVote(final long postId) throws DataSourceException {
        Boolean result = null;
        try {
            RatingsEntry entry = RatingsEntryLocalServiceUtil.getEntry(currentUserId, MBMessage.class.getName(),
                    postId);
            if (entry != null) {
                result = entry.getScore() > 0;
            }
        } catch (final NoSuchEntryException e) {
            // Ignore
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't get post vote for post %d.", postId), e);
            throw new DataSourceException(e);
        }
        return result;
    }

    @Override
    public void upvote(final long postId) throws DataSourceException {
        ratePost(postId, 1);
    }

    @Override
    public void downvote(final long postId) throws DataSourceException {
        ratePost(postId, -1);
    }

    private void ratePost(final long postId, final int score) throws DataSourceException {
        try {
            RatingsEntryServiceUtil.updateEntry(MBMessage.class.getName(), postId, score);
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't rate post %d.", postId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public void removeUserVote(final long postId) throws DataSourceException {
        try {
            RatingsEntryServiceUtil.deleteEntry(MBMessage.class.getName(), postId);
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't remove user vote for post %d.", postId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public long getPostScore(final long postId) throws DataSourceException {
        try {
            final RatingsStats ratingsStats = RatingsStatsLocalServiceUtil.getStats(MBMessage.class.getName(),
                    postId);
            return (long) (ratingsStats.getAverageScore() * ratingsStats.getTotalEntries());
        } catch (final SystemException e) {
            LOG.error(String.format("Couldn't get score for post %d.", postId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public Post saveReply(final String rawBody, final Map<String, byte[]> attachments, final long threadId)
            throws DataSourceException {
        try {
            mbMessageServiceContext.setAddCommunityPermissions(true);
            mbMessageServiceContext.setAddGuestPermissions(true);
            final MBMessage newPost = internalSaveAsCurrentUser(rawBody, attachments, getThread(threadId),
                    getRootMessageId(threadId));
            markThreadRead(threadId);
            return getPost(newPost.getMessageId());
        } catch (final NestableException e) {
            LOG.error("Couldn't save post.", e);
            if ("FileNameException".equals(e.getClass().getSimpleName())) {
                throw new org.vaadin.tori.exception.FileNameException(e);
            } else {
                throw new DataSourceException(e);
            }
        }
    }

    @Override
    public void moveThread(final long threadId, final Long destinationCategoryId) throws DataSourceException {
        try {
            MBThreadLocalServiceUtil.moveThread(scopeGroupId, destinationCategoryId, threadId);
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't move thread %d.", threadId), e);
            throw new DataSourceException(e);
        }

    }

    @Override
    public void stickyThread(final long threadId) throws DataSourceException {
        updateThreadPriority(threadId, STICKY_PRIORITY);
    }

    @Override
    public void unstickyThread(final long threadId) throws DataSourceException {
        updateThreadPriority(threadId, 0);
    }

    private void updateThreadPriority(final long threadId, final double newPriority) throws DataSourceException {
        try {
            final MBThread liferayThread = MBThreadLocalServiceUtil.getThread(threadId);
            liferayThread.setPriority(newPriority);
            MBThreadLocalServiceUtil.updateMBThread(liferayThread);
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't change priority for thread %d.", threadId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public void lockThread(final long threadId) throws DataSourceException {
        try {
            MBThreadServiceUtil.lockThread(threadId);
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't lock thread %d.", threadId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public void unlockThread(final long threadId) throws DataSourceException {
        try {
            MBThreadServiceUtil.unlockThread(threadId);
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't unlock thread %d.", threadId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public void deleteThread(final long threadId) throws DataSourceException {
        try {
            MBThreadLocalServiceUtil.deleteMBThread(threadId);
        } catch (final NestableException e) {
            LOG.error(String.format("Couldn't delete thread %d.", threadId), e);
            throw new DataSourceException(e);
        }
    }

    @Override
    public void setRequest(final PortletRequest request) {
        this.request = request;
        themeDisplay = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY);

        if (themeDisplay != null) {
            if (scopeGroupId < 0) {
                // scope not defined yet -> get if from the theme display
                scopeGroupId = themeDisplay.getScopeGroupId();
                LOG.debug("Using groupId " + scopeGroupId + " as the scope.");
            }
            long remoteUser = 0;
            if (request.getRemoteUser() != null) {
                remoteUser = Long.valueOf(request.getRemoteUser());
            }
            if (currentUserId != remoteUser) {
                // current user is changed
                currentUserId = remoteUser;
            }
            if (imagePath == null) {
                imagePath = themeDisplay.getPathImage();
            }
        }

        try {
            mbBanServiceContext = ServiceContextFactory.getInstance(MBBan.class.getName(), request);
            flagsServiceContext = ServiceContextFactory.getInstance("com.liferay.portlet.flags.model.FlagsEntry",
                    request);
            mbCategoryServiceContext = ServiceContextFactory.getInstance(MBCategory.class.getName(), request);
            mbMessageServiceContext = ServiceContextFactory.getInstance(MBMessage.class.getName(), request);
        } catch (final NestableException e) {
            LOG.error("Couldn't create ServiceContext.", e);
        }

        if (toriConfiguration == null) {
            toriConfiguration = mapConfiguration(request);
        }
    }

    private static final int DEFAULT_MAX_FILE_SIZE = 307200;

    private Configuration toriConfiguration;

    @Override
    public Post saveNewThread(final String topic, final String rawBody, final Map<String, byte[]> attachments,
            final Long categoryId) throws DataSourceException {

        try {
            final DiscussionThread thread = new DiscussionThread(topic);
            if (categoryId != null) {
                thread.setCategory(getCategory(categoryId));
            }

            mbMessageServiceContext.setAddCommunityPermissions(true);
            mbMessageServiceContext.setAddGuestPermissions(true);
            final MBMessage savedRootMessage = internalSaveAsCurrentUser(rawBody, attachments, thread,
                    MBMessageConstants.DEFAULT_PARENT_MESSAGE_ID);
            if (savedRootMessage != null) {
                return getPost(savedRootMessage.getMessageId());
            }
        } catch (final NestableException e) {
            LOG.error("Couldn't save new thread.", e);
            if ("FileNameException".equals(e.getClass().getSimpleName())) {
                throw new org.vaadin.tori.exception.FileNameException(e);
            } else {
                throw new DataSourceException(e);
            }
        }
        // if we get this far, saving has failed -> throw exception
        throw new DataSourceException();
    }

    @Override
    public int getAttachmentMaxFileSize() {
        try {
            return Integer.parseInt(PrefsPropsUtil.getString(PropsKeys.DL_FILE_MAX_SIZE));
        } catch (final Exception e) {
            LOG.error("Couldn't get max file size");
            return DEFAULT_MAX_FILE_SIZE;
        }
    }

    @Override
    public boolean isLoggedInUser() {
        return currentUserId != 0;
    }

    @Override
    public final void save(final Configuration config) throws DataSourceException {

        final Map<String, String> postReplacements = config.getReplacements();
        final String[] values = new String[postReplacements.size()];
        int index = 0;
        for (final Entry<String, String> entry : postReplacements.entrySet()) {
            values[index++] = entry.getKey() + REPLACEMENT_SEPARATOR + entry.getValue();
        }
        try {
            PortletPreferences portletPreferences = PortletPreferencesFactoryUtil.getPortletSetup(request);
            portletPreferences.setValues(PREFS_REPLACEMENTS_KEY, values);

            portletPreferences.setValue(PREFS_REPLACE_MESSAGE_BOARDS_LINKS,
                    Boolean.toString(config.isReplaceMessageBoardsLinks()));

            portletPreferences.setValue(PREFS_SHOW_THREADS_ON_DASHBOARD,
                    Boolean.toString(config.isShowThreadsOnDashboard()));

            portletPreferences.setValue(PREFS_ANALYTICS_ID, config.getGoogleAnalyticsTrackerId());
            portletPreferences.setValue(PREFS_UPDATE_PAGE_TITLE, Boolean.toString(config.isUpdatePageTitle()));
            portletPreferences.setValue(PREFS_PAGE_TITLE_PREFIX, config.getPageTitlePrefix());
            portletPreferences.setValue(PREFS_MAY_NOT_REPLY_NOTE, config.getMayNotReplyNote());

            portletPreferences.setValue(PREFS_USE_TORI_MAIL_SERVICE,
                    Boolean.toString(config.isUseToriMailService()));
            portletPreferences.setValue(PREFS_EMAIL_FROM_ADDRESS, config.getEmailFromAddress());
            portletPreferences.setValue(PREFS_EMAIL_FROM_NAME, config.getEmailFromName());
            portletPreferences.setValue(PREFS_EMAIL_REPLY_TO_ADDRESS, config.getEmailReplyToAddress());
            portletPreferences.setValue(PREFS_EMAIL_HEADER_IMAGE_URL, config.getEmailHeaderImageUrl());

            portletPreferences.store();
        } catch (final Exception e) {
            LOG.error("Unable to store portlet preferences", e);
            throw new DataSourceException(e);
        }
    }

    @Override
    @Deprecated
    public String getPathRoot() {
        String pathRoot = "";
        try {
            String layoutFriendlyURL = PortalUtil.getLayoutFriendlyURL(themeDisplay.getLayout(), themeDisplay);
            URI uri = new URI(layoutFriendlyURL);
            pathRoot = getLocaleAdjustedURI(uri.getPath());
        } catch (NestableException e) {
            LOG.warn("Unable to determine root path!", e);
        } catch (URISyntaxException e) {
            LOG.warn("Unable to determine root path!", e);
        }
        return pathRoot;
    }

    /**
     * <p>
     * Remove the Locale setting parameter in the Liferay URI.
     * <p>
     * Take an URL <code>vaadin.com/foo</code>. Liferay accepts an url
     * <code>vaadin.com/en_GB/foo</code>, and produces the same page. Some
     * Liferay features are able to take use of the locale definition in the
     * URL, and translate things. Since Tori doesn't support that currently, we
     * need to ignore that bit in the URI.
     */
    private static String getLocaleAdjustedURI(final String path) {

        // remove the prefixes locale string from the input -->
        // /fi/foo/bar -> /foo/bar
        // /en_GB/foo/bar -> /foo/bar

        final Pattern pathShortener = Pattern.compile("^/(?:[a-z]{2}(?:_[A-Z]{2})?/)?(.+)$");
        /*-
         * ^/              # the string needs to start with a forward slash
         * (
         *   ?:[a-z]{2}    # non-capturing group that matches two lower case letters
         *   (
         *     ?:_[A-Z]{2} # non-capturing group that, if the previous was matched, this matches the following underscore, and two upper case letters
         *   )?            # the previous group is optional (so, it's fine to match the lower case letters only
         *   /             # if the two lower case letters were found, no matter if the second group is found, a forward slash is required
         * )?              # the entire group is optional
         * (.+)$           # capture the string that comes after these groups.
         */

        final Matcher matcher = pathShortener.matcher(path);
        if (matcher.matches()) {
            return "/" + matcher.group(1);
        } else {
            return path;
        }
    }

    @Override
    public User getToriUser(final long userId) throws DataSourceException {
        User user = null;
        if (userId > 0) {
            try {
                user = getUser(userId);
            } catch (NestableException e) {
                throw new DataSourceException(e);
            }
        }
        return user;
    }

    @Override
    public Post getPost(final long postId) throws DataSourceException {
        Post result = null;
        try {
            MBMessage message = MBMessageLocalServiceUtil.getMBMessage(postId);
            DiscussionThread thread = getThread(message.getThreadId());
            result = internalGetPost(message, thread);
        } catch (NestableException e) {
            throw new DataSourceException(e);
        }
        return result;
    }

    private Post internalGetPost(final MBMessage message, final DiscussionThread thread) throws NestableException {
        final User author = getUser(message.getUserId());
        final List<Attachment> attachments = getAttachments(message);
        final boolean formatBBCode = message.isFormatBBCode();
        String bodyRaw = message.getBody(false);
        return LiferayEntityFactoryUtil.createPost(message, bodyRaw, formatBBCode, author, thread, attachments);
    }

    @Override
    public User getCurrentUser() {
        try {
            return getUser(currentUserId);
        } catch (NestableException e) {
            return LiferayEntityFactoryUtil.createAnonymousUser(imagePath);
        }
    }

    @Override
    public Configuration getConfiguration() {
        return toriConfiguration;
    }

    private Configuration mapConfiguration(final PortletRequest request) {

        Configuration configuration = new Configuration();

        try {
            PortletPreferences portletPreferences = PortletPreferencesFactoryUtil.getPortletSetup(request);

            // Post body replacements
            configuration.setReplacements(new HashMap<String, String>());
            final String[] values = portletPreferences.getValues(PREFS_REPLACEMENTS_KEY, new String[0]);
            if (values != null) {
                for (final String value : values) {
                    final String[] split = value.split(REPLACEMENT_SEPARATOR);
                    if (split.length == 2) {
                        configuration.getReplacements().put(split[0], split[1]);
                    }
                }
            }
            // Replace message boards links
            Boolean replace = Boolean.valueOf(
                    portletPreferences.getValue(PREFS_REPLACE_MESSAGE_BOARDS_LINKS, Boolean.toString(true)));
            configuration.setReplaceMessageBoardsLinks(replace);

            // Update page title
            Boolean updatePageTitle = Boolean
                    .valueOf(portletPreferences.getValue(PREFS_UPDATE_PAGE_TITLE, Boolean.toString(true)));
            configuration.setUpdatePageTitle(updatePageTitle);

            // Page title
            configuration.setPageTitlePrefix(portletPreferences.getValue(PREFS_PAGE_TITLE_PREFIX, null));

            // Use Tori mail service
            Boolean useToriMailService = Boolean
                    .valueOf(portletPreferences.getValue(PREFS_USE_TORI_MAIL_SERVICE, Boolean.toString(true)));
            configuration.setUseToriMailService(useToriMailService);

            // Email from address
            configuration.setEmailFromAddress(portletPreferences.getValue(PREFS_EMAIL_FROM_ADDRESS, null));

            // Email from name
            configuration.setEmailFromName(portletPreferences.getValue(PREFS_EMAIL_FROM_NAME, null));

            // Email reply-to address
            configuration.setEmailReplyToAddress(portletPreferences.getValue(PREFS_EMAIL_REPLY_TO_ADDRESS, null));

            // Email content header image url
            configuration.setEmailHeaderImageUrl(portletPreferences.getValue(PREFS_EMAIL_HEADER_IMAGE_URL, null));

            // May not reply note
            configuration.setMayNotReplyNote(portletPreferences.getValue(PREFS_MAY_NOT_REPLY_NOTE, null));

            // GA tracker id
            configuration.setGoogleAnalyticsTrackerId(portletPreferences.getValue(PREFS_ANALYTICS_ID, null));

            // Show threads on dashboard
            Boolean showThreadsOnDashboard = Boolean
                    .valueOf(portletPreferences.getValue(PREFS_SHOW_THREADS_ON_DASHBOARD, Boolean.toString(true)));
            configuration.setShowThreadsOnDashboard(showThreadsOnDashboard);

            String defaultEmailsEnabled = Boolean.toString(!useToriMailService);
            portletPreferences.setValue("email-message-added-enabled", defaultEmailsEnabled);
            portletPreferences.setValue("email-message-updated-enabled", defaultEmailsEnabled);
            portletPreferences.setValue("emailMessageAddedEnabled", defaultEmailsEnabled);
            portletPreferences.setValue("emailMessageUpdatedEnabled", defaultEmailsEnabled);
            portletPreferences.store();
        } catch (final NestableException e) {
            LOG.error("Couldn't load PortletPreferences.", e);
        } catch (final ReadOnlyException e) {
            LOG.error("Couldn't update PortletPreferences.", e);
        } catch (final ValidatorException e) {
            LOG.error("Couldn't update PortletPreferences.", e);
        } catch (final IOException e) {
            LOG.error("Couldn't update PortletPreferences.", e);
        }

        return configuration;
    }

    @Override
    public void followThread(final long threadId) throws DataSourceException {
        if (isLoggedInUser()) {
            try {
                SubscriptionLocalServiceUtil.addSubscription(currentUserId,
                        UserLocalServiceUtil.getUser(currentUserId).getGroupId(), MBThread.class.getName(),
                        threadId);
            } catch (final NestableException e) {
                LOG.error(String.format("Cannot follow thread %d", threadId), e);
                throw new DataSourceException(e);
            } catch (final NullPointerException e) {
                LOG.error(String.format("Cannot follow thread %d", threadId), e);
            }
        }
    }

    @Override
    public int getUnreadThreadCount(final long categoryId) throws DataSourceException {

        int result = 0;
        if (isLoggedInUser()) {
            // 0. All the category ids (recursively) including the parameter
            @SuppressWarnings("rawtypes")
            Collection categoryIds = getCategoryIdsRecursively(categoryId);

            // 1. Ids of all the threads user has read
            DynamicQuery readThreadIds = DynamicQueryFactoryUtil
                    .forClass(MBThreadFlag.class, PortalClassLoaderUtil.getClassLoader())
                    .setProjection(ProjectionFactoryUtil.property("threadId"))
                    .add(PropertyFactoryUtil.forName("userId").eq(currentUserId));

            // 2. Query the threads that are in one of the categories
            // from 0. AND are not read 1. AND are approved by status.
            DynamicQuery resultQuery = DynamicQueryFactoryUtil
                    .forClass(MBThread.class, PortalClassLoaderUtil.getClassLoader())
                    .add(PropertyFactoryUtil.forName("categoryId").in(categoryIds))
                    .add(PropertyFactoryUtil.forName("threadId").notIn(readThreadIds))
                    .add(PropertyFactoryUtil.forName("status").eq(WorkflowConstants.STATUS_APPROVED));

            try {
                result = new Long(MBThreadLocalServiceUtil.dynamicQueryCount(resultQuery)).intValue();
            } catch (SystemException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    @Override
    public boolean isThreadRead(final long threadId) {
        boolean result = true;
        if (isLoggedInUser()) {
            try {
                result = MBThreadFlagLocalServiceUtil.hasThreadFlag(currentUserId,
                        MBThreadLocalServiceUtil.getThread(threadId));
            } catch (final NestableException e) {
                LOG.error(String.format("Couldn't check for read flag on thread %d.", threadId), e);
            }
        }
        // default to read in case of an anonymous user
        return result;
    }

    @Override
    public void markThreadRead(final long threadId) throws DataSourceException {
        if (isLoggedInUser()) {
            try {
                MBThreadFlagLocalServiceUtil.addThreadFlag(currentUserId,
                        MBThreadLocalServiceUtil.getThread(threadId), flagsServiceContext);
            } catch (final NestableException e) {
                LOG.error(String.format("Couldn't mark thread %d as read.", threadId), e);
                throw new DataSourceException(e);
            }
        }
    }

    @Override
    public void markThreadUnRead(final long threadId) throws DataSourceException {
        if (isLoggedInUser()) {
            try {
                MBThreadFlag threadFlag = MBThreadFlagLocalServiceUtil.getThreadFlag(currentUserId,
                        MBThreadLocalServiceUtil.getThread(threadId));
                MBThreadFlagLocalServiceUtil.deleteMBThreadFlag(threadFlag);
            } catch (final NestableException e) {
                LOG.error(String.format("Couldn't mark thread %d as read.", threadId), e);
                throw new DataSourceException(e);
            }
        }
    }

    @Override
    public void saveNewCategory(final Long parentCategoryId, final String name, final String description)
            throws DataSourceException {
        try {
            LOG.debug("Adding new category: " + name);
            final long parentId = normalizeCategoryId(parentCategoryId);

            final String displayStyle = "default";

            mbCategoryServiceContext.setAddGroupPermissions(true);
            mbCategoryServiceContext.setAddGuestPermissions(true);
            MBCategoryServiceUtil.addCategory(parentId, name, description, displayStyle, null, null, null, 0, false,
                    null, null, 0, null, false, null, 0, false, null, null, false, false, mbCategoryServiceContext);
        } catch (final NestableException e) {
            LOG.error("Cannot persist category", e);
            throw new DataSourceException(e);
        }
    }

    private List<Attachment> getAttachments(final MBMessage message) throws NestableException {
        if (message.getAttachmentsFileEntriesCount() > 0) {
            final List<FileEntry> filenames = message.getAttachmentsFileEntries();
            final List<Attachment> attachments = new ArrayList<Attachment>(filenames.size());
            for (final FileEntry fileEntry : filenames) {
                String downloadUrl = PortletFileRepositoryUtil.getPortletFileEntryURL(themeDisplay, fileEntry,
                        StringPool.BLANK);

                final String shortFilename = fileEntry.getTitle();
                final long fileSize = fileEntry.getSize();

                final Attachment attachment = new Attachment(shortFilename, fileSize);
                attachment.setDownloadUrl(downloadUrl);
                attachments.add(attachment);
            }
            return attachments;
        }
        return Collections.emptyList();
    }

    protected MBMessage internalSaveAsCurrentUser(final String rawBody, final Map<String, byte[]> files,
            final DiscussionThread thread, final long parentMessageId) throws PortalException, SystemException {
        final long groupId = scopeGroupId;
        final long categoryId = thread.getCategory() != null ? thread.getCategory().getId()
                : normalizeCategoryId(null);

        // trim because liferay seems to bug out otherwise
        String subject = thread.getTopic().trim();
        final String body = rawBody.trim();
        final List<ObjectValuePair<String, InputStream>> attachments = new ArrayList<ObjectValuePair<String, InputStream>>();

        if (files != null) {
            for (final Entry<String, byte[]> file : files.entrySet()) {
                final String fileName = file.getKey();
                final byte[] bytes = file.getValue();

                if ((bytes != null) && (bytes.length > 0)) {
                    final ObjectValuePair<String, InputStream> ovp = new ObjectValuePair<String, InputStream>(
                            fileName, new ByteArrayInputStream(bytes));

                    attachments.add(ovp);
                }
            }
        }

        final boolean anonymous = false;
        final double priority = MBThreadConstants.PRIORITY_NOT_GIVEN;
        final boolean allowPingbacks = false;
        final String format = "bbcode";

        MBMessage message = null;

        if (parentMessageId == MBMessageConstants.DEFAULT_PARENT_MESSAGE_ID) {
            // Post new thread
            message = MBMessageServiceUtil.addMessage(groupId, categoryId, subject, body, format, attachments,
                    anonymous, priority, allowPingbacks, mbMessageServiceContext);
        } else {
            // Post reply
            message = MBMessageServiceUtil.addMessage(parentMessageId, "RE: " + subject, body, format, attachments,
                    anonymous, priority, allowPingbacks, mbMessageServiceContext);
        }
        return message;
    }

}