Java tutorial
/* * 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; } }