package org.sakaiproject.component.app.messageforums.entity; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.api.app.messageforums.Attachment; import org.sakaiproject.api.app.messageforums.DiscussionForum; import org.sakaiproject.api.app.messageforums.DiscussionForumService; import org.sakaiproject.api.app.messageforums.DiscussionTopic; import org.sakaiproject.api.app.messageforums.Message; import org.sakaiproject.api.app.messageforums.MessageForumsMessageManager; import org.sakaiproject.api.app.messageforums.PrivateMessage; import org.sakaiproject.api.app.messageforums.PrivateMessageRecipient; import org.sakaiproject.api.app.messageforums.SynopticMsgcntrManager; import org.sakaiproject.api.app.messageforums.Topic; import org.sakaiproject.api.app.messageforums.entity.ForumMessageEntityProvider; import org.sakaiproject.api.app.messageforums.ui.DiscussionForumManager; import org.sakaiproject.api.app.messageforums.ui.PrivateMessageManager; import org.sakaiproject.api.app.messageforums.ui.UIPermissionsManager; import org.sakaiproject.authz.cover.SecurityService; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.entitybroker.EntityReference; import org.sakaiproject.entitybroker.EntityView; import org.sakaiproject.entitybroker.entityprovider.annotations.EntityCustomAction; import org.sakaiproject.entitybroker.entityprovider.capabilities.ActionsExecutable; import org.sakaiproject.entitybroker.entityprovider.capabilities.AutoRegisterEntityProvider; import org.sakaiproject.entitybroker.entityprovider.capabilities.PropertyProvideable; import org.sakaiproject.entitybroker.entityprovider.capabilities.RESTful; import org.sakaiproject.entitybroker.entityprovider.capabilities.RequestAware; import org.sakaiproject.entitybroker.entityprovider.capabilities.RequestStorable; import org.sakaiproject.entitybroker.entityprovider.extension.RequestGetter; import org.sakaiproject.entitybroker.entityprovider.extension.RequestStorage; import org.sakaiproject.entitybroker.entityprovider.search.Restriction; import org.sakaiproject.entitybroker.entityprovider.search.Search; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.user.api.UserNotDefinedException; import org.sakaiproject.user.cover.UserDirectoryService; import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException; public class ForumMessageEntityProviderImpl implements ForumMessageEntityProvider, AutoRegisterEntityProvider, PropertyProvideable, RESTful, RequestStorable, RequestAware, ActionsExecutable { private DiscussionForumManager forumManager; private PrivateMessageManager privateMessageManager; private UIPermissionsManager uiPermissionsManager; private MessageForumsMessageManager messageManager; private static final Log LOG = LogFactory.getLog(ForumMessageEntityProviderImpl.class); private RequestStorage requestStorage; public void setRequestStorage(RequestStorage requestStorage) { this.requestStorage = requestStorage; } private RequestGetter requestGetter; public void setRequestGetter(RequestGetter requestGetter){ this.requestGetter = requestGetter; } public String getEntityPrefix() { return ENTITY_PREFIX; } public boolean entityExists(String id) { Topic topic = null; try { topic = forumManager.getTopicById(Long.valueOf(id)); } catch (Exception e) { e.printStackTrace(); } return (topic != null); } public List<String> findEntityRefs(String[] prefixes, String[] name, String[] searchValue, boolean exactMatch) { List<String> rv = new ArrayList<String>(); String userId = null; String siteId = null; String topicId = null; if (ENTITY_PREFIX.equals(prefixes[0])) { for (int i = 0; i < name.length; i++) { if ("context".equalsIgnoreCase(name[i]) || "site".equalsIgnoreCase(name[i])) siteId = searchValue[i]; else if ("user".equalsIgnoreCase(name[i]) || "userId".equalsIgnoreCase(name[i])) userId = searchValue[i]; else if ("topic".equalsIgnoreCase(name[i]) || "topicId".equalsIgnoreCase(name[i])) topicId = searchValue[i]; else if ("parentReference".equalsIgnoreCase(name[i])) { String[] parts = searchValue[i].split("/"); topicId = parts[parts.length - 1]; } } String siteRef = siteId; if(siteRef != null && !siteRef.startsWith("/site/")){ siteRef = "/site/" + siteRef; } // TODO: support search by something other then topic id... if (topicId != null) { List<Message> messages = forumManager.getTopicByIdWithMessagesAndAttachments(Long.valueOf(topicId)).getMessages(); for (int i = 0; i < messages.size(); i++) { // TODO: authz is way too basic, someone more hip to message center please improve... //This should also allow people with read access to an item to link to it if (forumManager.isInstructor(userId, siteRef) || userId.equals(messages.get(i).getCreatedBy())) { rv.add("/" + ENTITY_PREFIX + "/" + messages.get(i).getId().toString()); } } } } return rv; } public Map<String, String> getProperties(String reference) { Map<String, String> props = new HashMap<String, String>(); Message message = forumManager.getMessageById(Long.valueOf(reference.substring(reference.lastIndexOf("/") + 1))); props.put("title", message.getTitle()); props.put("author", message.getCreatedBy()); if (message.getCreated() != null) props.put("date", DateFormat.getInstance().format(message.getCreated())); if (message.getModifiedBy() != null) { props.put("modified_by", message.getModifiedBy()); props.put("modified_date", DateFormat.getInstance().format(message.getModified())); } props.put("label", message.getLabel()); if (message.getDraft() != null) props.put("draft", message.getDraft().toString()); if (message.getApproved() != null) props.put("approved", message.getApproved().toString()); if (message.getGradeAssignmentName() != null) props.put("assignment_name", message.getGradeAssignmentName()); return props; } public String getPropertyValue(String reference, String name) { // TODO: don't be so lazy, just get what we need... Map<String, String> props = getProperties(reference); return props.get(name); } public void setPropertyValue(String reference, String name, String value) { // This does nothing for now... we could all the setting of many published assessment properties // here though... if you're feeling jumpy feel free. } public void setForumManager(DiscussionForumManager forumManager) { this.forumManager = forumManager; } public String createEntity(EntityReference ref, Object entity, Map<String, Object> params) { // TODO Auto-generated method stub return null; } public Object getSampleEntity() { // TODO Auto-generated method stub return null; } public void updateEntity(EntityReference ref, Object entity, Map<String, Object> params) { // TODO Auto-generated method stub } public Object getEntity(EntityReference ref) { // TODO Auto-generated method stub return null; } public void deleteEntity(EntityReference ref, Map<String, Object> params) { // TODO Auto-generated method stub } public List<DecoratedMessage> findReplies(List<Message> messages, Long messageId, Long topicId, Map msgIdReadStatusMap){ List<DecoratedMessage> replies = new ArrayList<DecoratedMessage>(); for (Message message : messages) { if(message.getInReplyTo() != null){ if(messageId.equals(message.getInReplyTo().getId())){ if(!message.getDeleted()){ List<String> attachments = new ArrayList<String>(); if(message.getHasAttachments()){ for(Attachment attachment : (List<Attachment>) message.getAttachments()){ attachments.add(attachment.getAttachmentName()); } } Boolean readStatus = (Boolean)msgIdReadStatusMap.get(message.getId()); if(readStatus == null) readStatus = Boolean.FALSE; DecoratedMessage dMessage = new DecoratedMessage(message .getId(), topicId, message.getTitle(), message.getBody(), "" + message.getModified().getTime(), attachments, findReplies(messages, message.getId(), topicId, msgIdReadStatusMap), message.getAuthor(), message.getInReplyTo() == null ? null : message.getInReplyTo().getId(), "" + message.getCreated().getTime(), readStatus.booleanValue(), "", ""); replies.add(dMessage); } } } } Collections.sort(replies, new Comparator<DecoratedMessage>(){ public int compare(DecoratedMessage arg0, DecoratedMessage arg1) { Long date1 = Long.parseLong(arg0.getCreatedOn()); Long date2 = Long.parseLong(arg1.getCreatedOn()); return date1.compareTo(date2); } }); return replies; } public List<DecoratedMessage> generateFlattenedMessagesListHelper(List<DecoratedMessage> messages, int indent){ List<DecoratedMessage> flattenedList = new ArrayList<DecoratedMessage>(); for (DecoratedMessage message : messages) { message.setIndentIndex(indent); List<DecoratedMessage> helperList = new ArrayList<DecoratedMessage>(); if(message.getReplies().size() > 0){ helperList = generateFlattenedMessagesListHelper(message.getReplies(), indent+1); } message.setReplies(null); flattenedList.add(message); flattenedList.addAll(helperList); } return flattenedList; } public List<?> getEntities(EntityReference ref, Search search) { String topicId = ""; String typeUuid = ""; String siteId = ""; String userId = UserDirectoryService.getCurrentUser().getId(); if (userId == null || "".equals(userId)){ return null; } if (! search.isEmpty()) { Restriction topicRes = search.getRestrictionByProperty("topicId"); if(topicRes != null){ topicId = topicRes.getStringValue(); } Restriction typeRes = search.getRestrictionByProperty("typeUuid"); if(typeRes != null){ typeUuid = typeRes.getStringValue(); } Restriction siteRes = search.getRestrictionByProperty("siteId"); if(siteRes != null){ siteId = siteRes.getStringValue(); } } List<DecoratedMessage> dMessages = new ArrayList<DecoratedMessage>(); if(topicId != null && !"".equals(topicId)){ List<Message> messages = forumManager.getTopicByIdWithMessagesAndAttachments(new Long(topicId)).getMessages(); DiscussionTopic dTopic = forumManager.getTopicById(Long.valueOf(topicId)); DiscussionForum dForum = forumManager.getForumById(dTopic.getBaseForum().getId()); siteId = forumManager.getContextForForumById(dForum.getId()); //make sure the user has access too this forum and topic and site: if(dForum.getDraft().equals(Boolean.FALSE) && dTopic.getDraft().equals(Boolean.FALSE) && SecurityService.unlock(userId, SiteService.SITE_VISIT, "/site/" + siteId)){ if (getUiPermissionsManager().isRead(dTopic.getId(), false, false, userId, siteId)) { messages = filterModeratedMessages(messages, dTopic, dForum, userId, siteId); List<Long> messageIds = new ArrayList<Long>(); for (Message message : messages) { if(message != null && !message.getDraft().booleanValue() && !message.getDeleted().booleanValue()) { messageIds.add(message.getId()); } } Map msgIdReadStatusMap = forumManager.getReadStatusForMessagesWithId(messageIds, userId); for (Message message : messages) { if(message.getInReplyTo() == null){ if(!message.getDeleted()){ //this is a top message, so now create the replies list (if any exists) List<String> attachments = new ArrayList<String>(); if(message.getHasAttachments()){ for(Attachment attachment : (List<Attachment>) message.getAttachments()){ attachments.add(attachment.getAttachmentName()); } } Boolean readStatus = (Boolean)msgIdReadStatusMap.get(message.getId()); if(readStatus == null) readStatus = Boolean.FALSE; DecoratedMessage dMessage = new DecoratedMessage(message .getId(), new Long(topicId), message.getTitle(), message.getBody(), "" + message.getModified().getTime(), attachments, findReplies(messages, message.getId(), new Long(topicId), msgIdReadStatusMap), message.getAuthor(), message.getInReplyTo() == null ? null : message.getInReplyTo().getId(), "" + message.getCreated().getTime(), readStatus.booleanValue(), "", ""); dMessages.add(dMessage); } } } } } //return a sorted list Collections.sort(dMessages, new Comparator<DecoratedMessage>(){ public int compare(DecoratedMessage arg0, DecoratedMessage arg1) { Long date1 = Long.parseLong(arg0.getCreatedOn()); Long date2 = Long.parseLong(arg1.getCreatedOn()); return date1.compareTo(date2); } }); //now that we have the hierarchy and ordered list, we need to flatten the returned list since there is a max depth //the entity broken can return (currently set at 8) List<DecoratedMessage> flattenedList = new ArrayList<DecoratedMessage>(); for (DecoratedMessage message : dMessages) { List<DecoratedMessage> helperList = new ArrayList<DecoratedMessage>(); if(message.getReplies().size() > 0){ helperList = generateFlattenedMessagesListHelper(message.getReplies(), 1); } //clear out the replies field since this can return bad data with the max depth issues. message.setReplies(null); flattenedList.add(message); flattenedList.addAll(helperList); } dMessages = flattenedList; }else if(typeUuid != null && !"".equals(typeUuid) && siteId != null && !"".equals(siteId)){ List decoratedPvtMsgs= getPrivateMessageManager().getMessagesByTypeByContext(typeUuid, siteId, userId, PrivateMessageManager.SORT_COLUMN_DATE, PrivateMessageManager.SORT_DESC); for (PrivateMessage pvtMessage : (List<PrivateMessage>) decoratedPvtMsgs) { PrivateMessage initPvtMessage = getPrivateMessageManager().initMessageWithAttachmentsAndRecipients(pvtMessage); //this is a top message, so now create the replies list (if any exists) List<String> attachments = new ArrayList<String>(); if(initPvtMessage.getHasAttachments()){ for(Attachment attachment : (List<Attachment>) initPvtMessage.getAttachments()){ attachments.add(attachment.getAttachmentName()); } } //getRecipients() is filtered for this particular user i.e. returned list of only one PrivateMessageRecipient object boolean read = false; for (Iterator iterator = pvtMessage.getRecipients().iterator(); iterator.hasNext();) { PrivateMessageRecipient el = (PrivateMessageRecipient) iterator.next(); if (el != null){ read = el.getRead().booleanValue(); } } DecoratedMessage dMessage = new DecoratedMessage(pvtMessage .getId(), null, pvtMessage.getTitle(), pvtMessage.getBody(), "" + pvtMessage.getModified().getTime(), attachments, null, pvtMessage.getAuthor(), pvtMessage.getInReplyTo() == null ? null : pvtMessage.getInReplyTo().getId(), "" + pvtMessage.getCreated().getTime(), read, pvtMessage.getRecipientsAsText(), pvtMessage.getLabel()); dMessages.add(dMessage); } } return dMessages; } public void markAsRead(String userId, String siteId, String readMessageId, int numOfAttempts) { try { Message msg = getMessageManager().getMessageById(new Long(readMessageId)); if(msg instanceof PrivateMessage){ String toolId = DiscussionForumService.MESSAGES_TOOL_ID; getPrivateMessageManager().markMessageAsReadForUser((PrivateMessage) msg, siteId, userId, toolId); }else{ String toolId = DiscussionForumService.FORUMS_TOOL_ID; String topicId = msg.getTopic().getId().toString(); messageManager.markMessageReadForUser(new Long(topicId), new Long(readMessageId), true, userId, siteId, toolId); } } catch (HibernateOptimisticLockingFailureException holfe) { // failed, so wait and try again try { Thread.sleep(SynopticMsgcntrManager.OPT_LOCK_WAIT); } catch (InterruptedException e) { e.printStackTrace(); } numOfAttempts--; if (numOfAttempts <= 0) { System.out .println("ForumMessageEntityProviderImpl: markAsRead: HibernateOptimisticLockingFailureException no more retries left"); holfe.printStackTrace(); } else { System.out .println("ForumMessageEntityProviderImpl: markAsRead: HibernateOptimisticLockingFailureException: attempts left: " + numOfAttempts); markAsRead(userId, siteId, readMessageId, numOfAttempts); } } catch (Exception e){ // failed, so wait and try again try { Thread.sleep(SynopticMsgcntrManager.OPT_LOCK_WAIT); } catch (InterruptedException ie) { ie.printStackTrace(); } numOfAttempts--; if (numOfAttempts <= 0) { System.out .println("ForumMessageEntityProviderImpl: markAsRead: no more retries left"); e.printStackTrace(); } else { System.out .println("ForumMessageEntityProviderImpl: markAsRead: attempts left: " + numOfAttempts); markAsRead(userId, siteId, readMessageId, numOfAttempts); } } } /** * Given a list of messages, will return all messages that meet at * least one of the following criteria: * 1) message is approved * */ private List filterModeratedMessages(List messages, DiscussionTopic topic, DiscussionForum forum, String userId, String siteId) { List viewableMsgs = new ArrayList(); if (messages != null && messages.size() > 0) { boolean hasModeratePerm = getUiPermissionsManager().isModeratePostings(topic, forum, userId, siteId); if (hasModeratePerm) return messages; Iterator msgIter = messages.iterator(); while (msgIter.hasNext()) { Message msg = (Message) msgIter.next(); if (msg.getApproved() != null && msg.getApproved()) viewableMsgs.add(msg); } } return viewableMsgs; } /** * markread/messageId/site/siteId * markread/messageId/site/siteId */ @EntityCustomAction(action="markread",viewKey=EntityView.VIEW_NEW) public boolean getForum(EntityView view, Map<String, Object> params) { String messageId = view.getPathSegment(2); String siteId = ""; if("site".equals(view.getPathSegment(3))){ siteId = view.getPathSegment(4); } String userId = UserDirectoryService.getCurrentUser().getId(); if (userId == null || "".equals(userId) || siteId == null || "".equals(siteId) || messageId == null || "".equals(messageId)) { return false; } markAsRead(userId, siteId, messageId, SynopticMsgcntrManager.NUM_OF_ATTEMPTS); return true; } /** * topic/topicId */ @EntityCustomAction(action="topic",viewKey=EntityView.VIEW_LIST) public List<?> getTopicMessagesInSite(EntityView view, Map<String, Object> params) { String topicId = view.getPathSegment(2); if (topicId == null) { topicId = (String) params.get("topicId"); if (topicId == null) { throw new IllegalArgumentException("topicId must be set in order to get the topic messages, set in params or in the URL /forum_message/topic/topicId"); } } List<?> l = getEntities(new EntityReference(ENTITY_PREFIX, ""), new Search("topicId", topicId)); return l; } /** * private/typeUuid/site/siteId */ @EntityCustomAction(action="private",viewKey=EntityView.VIEW_LIST) public List<?> getPrivateTopicMessagesInSite(EntityView view, Map<String, Object> params) { String topicId = view.getPathSegment(2); if (topicId == null) { topicId = (String) params.get("typeUuid"); if (topicId == null) { throw new IllegalArgumentException("typeUuid and siteId must be set in order to get the topic messages, set in params or in the URL /forum_message/private/typeUuid/site/siteId"); } } String siteId = ""; if("site".equals(view.getPathSegment(3))){ siteId = view.getPathSegment(4); } List<?> l = getEntities(new EntityReference(ENTITY_PREFIX, ""), new Search(new String[]{"typeUuid", "siteId"}, new String[]{topicId, siteId})); return l; } public String[] getHandledOutputFormats() { // TODO Auto-generated method stub return null; } public String[] getHandledInputFormats() { // TODO Auto-generated method stub return null; } public class DecoratedMessagesTopic{ private String title; private Long id; private int totalMessages = 0; private int totalUnreadMessages = 0; public DecoratedMessagesTopic(String title, Long id, int totalMessages, int totalUnreadMessages){ this.title = title; this.id = id; this.totalMessages = totalMessages; this.totalUnreadMessages = totalUnreadMessages; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public int getTotalMessages() { return totalMessages; } public void setTotalMessages(int totalMessages) { this.totalMessages = totalMessages; } public int getTotalUnreadMessages() { return totalUnreadMessages; } public void setTotalUnreadMessages(int totalUnreadMessages) { this.totalUnreadMessages = totalUnreadMessages; } } public class DecoratedMessage{ private Long messageId; private Long topicId; private String title; private String body; private String lastModified; private List<String> attachments; private List<DecoratedMessage> replies; private String authoredBy; private int indentIndex = 0; private Long replyTo; private String createdOn; private boolean read; private String recipients; private String label; public DecoratedMessage(Long messageId, Long topicId, String title, String body, String lastModified, List<String> attachments, List<DecoratedMessage> replies, String authoredBy, Long replyTo, String createdOn, boolean read, String recipients, String label){ this.messageId = messageId; this.topicId = topicId; this.title = title; this.body = body; this.attachments = attachments; this.replies = replies; this.lastModified = lastModified; this.authoredBy = authoredBy; this.replyTo = replyTo; this.createdOn = createdOn; this.read = read; this.recipients = recipients; this.label = label; } public Long getMessageId() { return messageId; } public void setMessageId(Long messageId) { this.messageId = messageId; } public Long getTopicId() { return topicId; } public void setTopicId(Long topicId) { this.topicId = topicId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public List<String> getAttachments() { return attachments; } public void setAttachments(List<String> attachments) { this.attachments = attachments; } public List<DecoratedMessage> getReplies() { return replies; } public void setReplies(List<DecoratedMessage> replies) { this.replies = replies; } public String getLastModified() { return lastModified; } public void setLastModified(String lastModified) { this.lastModified = lastModified; } public String getAuthoredBy() { return authoredBy; } public void setAuthoredBy(String authoredBy) { this.authoredBy = authoredBy; } public int getIndentIndex() { return indentIndex; } public void setIndentIndex(int indentIndex) { this.indentIndex = indentIndex; } public Long getReplyTo() { return replyTo; } public void setReplyTo(Long replyTo) { this.replyTo = replyTo; } public String getCreatedOn() { return createdOn; } public void setCreatedOn(String createdOn) { this.createdOn = createdOn; } public boolean isRead() { return read; } public void setRead(boolean read) { this.read = read; } public String getRecipients() { return recipients; } public void setRecipients(String recipients) { this.recipients = recipients; } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } } public PrivateMessageManager getPrivateMessageManager() { return privateMessageManager; } public void setPrivateMessageManager(PrivateMessageManager privateMessageManager) { this.privateMessageManager = privateMessageManager; } public UIPermissionsManager getUiPermissionsManager() { return uiPermissionsManager; } public void setUiPermissionsManager(UIPermissionsManager uiPermissionsManager) { this.uiPermissionsManager = uiPermissionsManager; } public MessageForumsMessageManager getMessageManager() { return messageManager; } public void setMessageManager(MessageForumsMessageManager messageManager) { this.messageManager = messageManager; } }