/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/announcement/trunk/announcement-tool/tool/src/java/org/sakaiproject/announcement/entityprovider/AnnouncementEntityProviderImpl.java $ * $Id: AnnouncementEntityProviderImpl.java 87813 2011-01-28 13:42:17Z savithap@umich.edu $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009 The Sakai Foundation * * Licensed under the Educational Community 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.osedu.org/licenses/ECL-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.sakaiproject.announcement.entityprovider; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.announcement.api.AnnouncementMessage; import org.sakaiproject.announcement.api.AnnouncementService; import org.sakaiproject.authz.api.SecurityService; import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entitybroker.EntityReference; import org.sakaiproject.entitybroker.EntityView; import org.sakaiproject.entitybroker.entityprovider.CoreEntityProvider; 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.RESTful; import org.sakaiproject.entitybroker.entityprovider.extension.Formats; import org.sakaiproject.entitybroker.entityprovider.search.Search; import org.sakaiproject.entitybroker.exception.EntityNotFoundException; import org.sakaiproject.entitybroker.util.AbstractEntityProvider; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SiteService; import org.sakaiproject.site.api.ToolConfiguration; import org.sakaiproject.time.api.Time; import org.sakaiproject.time.api.TimeService; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.tool.api.ToolManager; import org.sakaiproject.user.api.UserDirectoryService; import org.sakaiproject.util.MergedList; import org.sakaiproject.util.ResourceLoader; public class AnnouncementEntityProviderImpl extends AbstractEntityProvider implements CoreEntityProvider, AutoRegisterEntityProvider, RESTful, ActionsExecutable{ public final static String ENTITY_PREFIX = "announcement"; private static final String PORTLET_CONFIG_PARAM_MERGED_CHANNELS = "mergedAnnouncementChannels"; private static final String MOTD_SITEID = "!site"; private static final String ADMIN_SITEID = "!admin"; private static final String MOTD_CHANNEL_SUFFIX = "motd"; public static int DEFAULT_NUM_ANNOUNCEMENTS = 3; public static int DEFAULT_DAYS_IN_PAST = 10; private static final Log log = LogFactory.getLog(AnnouncementEntityProviderImpl.class); private static ResourceLoader rb = new ResourceLoader("announcement"); /** * Prefix for this provider */ public String getEntityPrefix() { return ENTITY_PREFIX; } /** * Get the list of announcements for a site (or user site, or !site for motd) * * @param siteId - siteId requested, or user site, or !site for motd. * @param params - the raw URL params that were sent, for processing. * @param onlyPublic - only show public announcements * @return */ public List<?> getAnnouncements(String siteId, Map<String,Object> params, boolean onlyPublic) { //check if we are loading the MOTD boolean motdView = false; if(StringUtils.equals(siteId, MOTD_SITEID)) { motdView = true; } //get number of announcements and days in the past to show from the URL params, validate and set to 0 if not set or conversion fails. //we use this zero value to determine if we need to look up from the tool config, or use the defaults if still not set. int numberOfAnnouncements = NumberUtils.toInt((String)params.get("n"), 0); int numberOfDaysInThePast = NumberUtils.toInt((String)params.get("d"), 0); //get currentUserId for permissions checks, although unused for motdView and onlyPublic String currentUserId = sessionManager.getCurrentSessionUserId(); log.debug("motdView: " + motdView); log.debug("siteId: " + siteId); log.debug("currentUserId: " + currentUserId); log.debug("onlyPublic: " + onlyPublic); //check current user has annc.read permissions for this site, not for public or motd though if(!onlyPublic && !motdView) { if(!securityService.unlock(currentUserId, AnnouncementService.SECURE_ANNC_READ, siteService.siteReference(siteId))) { throw new SecurityException("You do not have access to site: " + siteId); } } // get the channels List<String> channels = getChannels(siteId); if(channels.size() == 0){ throw new EntityNotFoundException("No announcement channels found for site: " + siteId, siteId); } log.debug("channels: " + channels.toString()); log.debug("num channels: " + channels.size()); Site site = null; String siteTitle = null; ToolConfiguration synopticTc = null; if(!motdView) { //get site try { site = siteService.getSite(siteId); } catch (IdUnusedException e) { throw new IllegalArgumentException("No site found for the siteid:" + siteId + " : "+e.getMessage()); } //get properties for synoptic tool in this site synopticTc = site.getToolForCommonId("sakai.synoptic.announcement"); } if(synopticTc != null){ Properties props = synopticTc.getPlacementConfig(); if(props.isEmpty()) { props = synopticTc.getConfig(); } if(props != null){ //only get these from the synoptic tool config if not already set in the URL params if (numberOfAnnouncements == 0 && props.get("items") != null) { numberOfAnnouncements = getIntegerParameter(props, "items", DEFAULT_NUM_ANNOUNCEMENTS); } if (numberOfDaysInThePast == 0 && props.get("days") != null) { numberOfDaysInThePast = getIntegerParameter(props, "days", DEFAULT_DAYS_IN_PAST); } } } //get site title if(!motdView) { siteTitle = site.getTitle(); } else { siteTitle = rb.getString("motd.title"); } //if numbers are still zero, use the defaults if(numberOfAnnouncements == 0) { numberOfAnnouncements = DEFAULT_NUM_ANNOUNCEMENTS; } if(numberOfDaysInThePast == 0) { numberOfDaysInThePast = DEFAULT_DAYS_IN_PAST; } log.debug("numberOfAnnouncements: " + numberOfAnnouncements); log.debug("numberOfDaysInThePast: " + numberOfDaysInThePast); //get the Sakai Time for the given java Date Time t = timeService.newTime(getTimeForDaysInPast(numberOfDaysInThePast).getTime()); //get the announcements for each channel List<AnnouncementMessage> announcements = new ArrayList<AnnouncementMessage>(); //for each channel for(String channel: channels) { try { announcements.addAll(announcementService.getMessages(channel, t, numberOfAnnouncements, true, false, onlyPublic)); } catch (PermissionException e) { log.warn("User: " + currentUserId + " does not have access to view the announcement channel: " + channel + ". Skipping..."); } } log.debug("announcements.size(): " + announcements.size()); //convert raw announcements into decorated announcements List<DecoratedAnnouncement> decoratedAnnouncements = new ArrayList<DecoratedAnnouncement>(); for (AnnouncementMessage a : announcements) { try { DecoratedAnnouncement da = new DecoratedAnnouncement(); da.setId(a.getId()); da.setTitle(a.getAnnouncementHeader().getSubject()); da.setBody(a.getBody()); da.setCreatedByDisplayName(a.getHeader().getFrom().getDisplayName()); da.setCreatedOn(new Date(a.getHeader().getDate().getTime())); da.setSiteId(siteId); da.setSiteTitle(siteTitle); //get attachments List<String> attachments = new ArrayList<String>(); for (Reference attachment : (List<Reference>) a.getHeader().getAttachments()) { attachments.add(attachment.getProperties().getPropertyFormatted(attachment.getProperties().getNamePropDisplayName())); } da.setAttachments(attachments); decoratedAnnouncements.add(da); } catch (Exception e) { //this can throw an exception if we are not logged in, ie public, this is fine so just deal with it and continue log.info("Exception caught processing announcement: " + a.getId() + " for user: " + currentUserId + ". Skipping..."); } } //sort Collections.sort(decoratedAnnouncements); //reverse so it is date descending. This could be dependent on a parameter that specifies the sort order Collections.reverse(decoratedAnnouncements); //trim to final number, within bounds of list size. if(numberOfAnnouncements > announcements.size()) { numberOfAnnouncements = announcements.size(); } decoratedAnnouncements = decoratedAnnouncements.subList(0, numberOfAnnouncements); return decoratedAnnouncements; } /** * Utility routine used to get an integer named value from a map or supply a default value if none is found. */ private int getIntegerParameter(Map<?,?> params, String paramName, int defaultValue) { String intValString = (String) params.get(paramName); if (StringUtils.trimToNull(intValString) != null) { return Integer.parseInt(intValString); } else { return defaultValue; } } /** * Utility to get the date for n days ago * @param n number of days in the past * @return */ private Date getTimeForDaysInPast(int n) { Calendar cal = Calendar.getInstance(); cal.add(Calendar.DATE, -n); return cal.getTime(); } /** * Helper to get the channels for a site. * <p> * If user site and not superuser, returns all available channels for this user.<br /> * If user site and superuser, return all merged channels.<br /> * If normal site, returns all merged channels.<br /> * If motd site, returns the motd channel. * * @param siteId * @return */ private List<String> getChannels(String siteId) { List<String> channels = new ArrayList<String>(); //if motd if(StringUtils.equals(siteId, MOTD_SITEID)) { log.debug("is motd site, returning motd channel"); channels = Collections.singletonList(announcementService.channelReference(siteId, MOTD_CHANNEL_SUFFIX)); return channels; } //if user site if(siteService.isUserSite(siteId)) { //if not super user, get all channels this user has access to if(!securityService.isSuperUser()){ log.debug("is user site and not super user, returning all permitted channels"); channels = Arrays.asList(new MergedList().getAllPermittedChannels(new AnnouncementChannelReferenceMaker())); return channels; } } //this is either a normal site, or we are a super user //so get the merged announcements for this site Site site = null; try { site = siteService.getSite(siteId); } catch (IdUnusedException e) { //this should have been caught and dealt with already so just return empty list return channels; } if(site != null) { ToolConfiguration toolConfig = site.getToolForCommonId("sakai.announcements"); if(toolConfig != null){ Properties props = toolConfig.getPlacementConfig(); if(props.isEmpty()) { props = toolConfig.getConfig(); } if(props != null){ String mergeProp = (String)props.get(PORTLET_CONFIG_PARAM_MERGED_CHANNELS); if(StringUtils.isNotBlank(mergeProp)) { log.debug("is normal site or super user, returning all merged channels in this site"); log.debug("mergeProp: " + mergeProp); channels = Arrays.asList(new MergedList().getChannelReferenceArrayFromDelimitedString(new AnnouncementChannelReferenceMaker().makeReference(siteId), mergeProp)); } else { log.debug("is normal site or super user but no merged channels, using original siteId channel"); channels = Collections.singletonList(announcementService.channelReference(siteId, SiteService.MAIN_CONTAINER)); } } } } return channels; } /* * Callback class so that we can form references in a generic way. */ private final class AnnouncementChannelReferenceMaker implements MergedList.ChannelReferenceMaker { public String makeReference(String siteId){ return announcementService.channelReference(siteId, SiteService.MAIN_CONTAINER); } } /** * site/siteId */ @EntityCustomAction(action="site",viewKey=EntityView.VIEW_LIST) public List<?> getAnnouncementsForSite(EntityView view, Map<String, Object> params) { //get siteId String siteId = view.getPathSegment(2); //check siteId supplied if (StringUtils.isBlank(siteId)) { throw new IllegalArgumentException("siteId must be set in order to get the announcements for a site, via the URL /announcement/site/siteId"); } boolean onlyPublic = false; //check if logged in String currentUserId = sessionManager.getCurrentSessionUserId(); if (StringUtils.isBlank(currentUserId)) { //not logged in so set flag to just return any public announcements for the site onlyPublic = true; } //check this is a valid site if(!siteService.siteExists(siteId)) { throw new EntityNotFoundException("Invalid siteId: " + siteId, siteId); } List<?> l = getAnnouncements(siteId, params, onlyPublic); return l; } /** * user */ @EntityCustomAction(action="user",viewKey=EntityView.VIEW_LIST) public List<?> getAnnouncementsForUser(EntityView view, Map<String, Object> params) { String userId = sessionManager.getCurrentSessionUserId(); if (StringUtils.isBlank(userId)) { //throw new SecurityException("You must be logged in to get your announcements."); return getMessagesOfTheDay(view, params); } //we still need a siteId since Announcements keys it's data on a channel reference created from a siteId. //in the case of a user, this is the My Workspace siteId for that user (as an internal user id) String siteId = siteService.getUserSiteId(userId); if(StringUtils.isBlank(siteId)) { throw new IllegalArgumentException("No siteId was found for userId: " + userId); } //if admin user, siteID is the admin workspace if(StringUtils.equals(userId, userDirectoryService.ADMIN_EID)){ siteId = ADMIN_SITEID; } List<?> l = getAnnouncements(siteId, params, false); return l; } /** * motd */ @EntityCustomAction(action="motd",viewKey=EntityView.VIEW_LIST) public List<?> getMessagesOfTheDay(EntityView view, Map<String, Object> params) { //MOTD announcements are published to a special site List<?> l = getAnnouncements(MOTD_SITEID, params, false); return l; } public boolean entityExists(String id) { return false; } public Object getSampleEntity() { return new DecoratedAnnouncement(); } public Object getEntity(EntityReference ref) { return null; } /** * Unimplemented EntityBroker methods */ public List<String> findEntityRefs(String[] arg0, String[] arg1, String[] arg2, boolean arg3) { // TODO Auto-generated method stub return null; } public String createEntity(EntityReference ref, Object entity, Map<String, Object> params) { // TODO Auto-generated method stub return null; } public void updateEntity(EntityReference ref, Object entity, Map<String, Object> params) { // TODO Auto-generated method stub } public void deleteEntity(EntityReference ref, Map<String, Object> params) { // TODO Auto-generated method stub } public String[] getHandledOutputFormats() { return new String[] { Formats.XML, Formats.JSON }; } public String[] getHandledInputFormats() { // TODO Auto-generated method stub return null; } public List<?> getEntities(EntityReference ref, Search search) { return null; } /** * Class to hold only the fields that we want to return */ public class DecoratedAnnouncement implements Comparable<Object>{ private String id; private String title; private String body; private String createdByDisplayName; private Date createdOn; private List<String> attachments; private String siteId; private String siteTitle; public DecoratedAnnouncement(){ } public String getId() { return id; } public void setId(String id) { this.id = id; } 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 String getCreatedByDisplayName() { return createdByDisplayName; } public void setCreatedByDisplayName(String createdByDisplayName) { this.createdByDisplayName = createdByDisplayName; } public Date getCreatedOn() { return createdOn; } public void setCreatedOn(Date createdOn) { this.createdOn = createdOn; } public List<String> getAttachments() { return attachments; } public void setAttachments(List<String> attachments) { this.attachments = attachments; } public String getSiteId() { return siteId; } public void setSiteId(String siteId) { this.siteId = siteId; } public String getSiteTitle() { return siteTitle; } public void setSiteTitle(String siteTitle) { this.siteTitle = siteTitle; } //default sort by date ascending public int compareTo(Object o) { Date field = ((DecoratedAnnouncement)o).getCreatedOn(); int lastCmp = createdOn.compareTo(field); return (lastCmp != 0 ? lastCmp : createdOn.compareTo(field)); } } private SecurityService securityService; public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } private SessionManager sessionManager; public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } private SiteService siteService; public void setSiteService(SiteService siteService) { this.siteService = siteService; } private AnnouncementService announcementService; public void setAnnouncementService(AnnouncementService announcementService) { this.announcementService = announcementService; } private UserDirectoryService userDirectoryService; public void setUserDirectoryService(UserDirectoryService userDirectoryService) { this.userDirectoryService = userDirectoryService; } private TimeService timeService; public void setTimeService(TimeService timeService) { this.timeService = timeService; } private ToolManager toolManager; public void setToolManager(ToolManager toolManager) { this.toolManager = toolManager; } }