/********************************************************************************** * $URL:$ * $Id:$ *********************************************************************************** * * Copyright (c) 2008 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.calendar.impl; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.Stack; import java.util.Vector; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.authz.cover.SecurityService; import org.sakaiproject.calendar.api.Calendar; import org.sakaiproject.calendar.api.CalendarEvent; import org.sakaiproject.calendar.api.CalendarEventEdit; import org.sakaiproject.calendar.api.CalendarImporterService; import org.sakaiproject.calendar.api.CalendarService; import org.sakaiproject.calendar.api.ExternalCalendarSubscriptionService; import org.sakaiproject.calendar.api.ExternalSubscription; import org.sakaiproject.calendar.api.RecurrenceRule; import org.sakaiproject.calendar.api.CalendarEvent.EventAccess; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.entity.api.Entity; import org.sakaiproject.entity.api.EntityManager; import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.entity.api.ResourcePropertiesEdit; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.IdUsedException; import org.sakaiproject.exception.ImportException; import org.sakaiproject.exception.InUseException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.id.api.IdManager; import org.sakaiproject.javax.Filter; 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.TimeRange; import org.sakaiproject.time.cover.TimeService; import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.tool.cover.ToolManager; import org.sakaiproject.util.BaseResourcePropertiesEdit; import org.sakaiproject.util.FormattedText; import org.sakaiproject.util.commonscodec.CommonsCodecBase64; import org.w3c.dom.Document; import org.w3c.dom.Element; public class BaseExternalCalendarSubscriptionService implements ExternalCalendarSubscriptionService { /** Logging */ private static Log m_log = LogFactory.getLog(BaseExternalCalendarSubscriptionService.class); /** Schedule tool ID */ private final static String SCHEDULE_TOOL_ID = "sakai.schedule"; /** Default context for institutional subscriptions */ private final static String INSTITUTIONAL_CONTEXT = "!worksite"; /** Default context for user-provided subscriptions */ private final static String USER_CONTEXT = "!user"; /** Default connect timeout when retrieving external subscriptions */ private final static int TIMEOUT = 30000; /** Default max cached external subscription entries (institutional) */ private final static int DEFAULT_MAX_INST_CACHED_ENTRIES = 16; /** Default max cached external subscription entries (user) */ private final static int DEFAULT_MAX_USER_CACHED_ENTRIES = 16; /** Default max cached external subscription time in minutes (institutional) */ private final static int DEFAULT_MAX_INST_CACHED_TIME = 2 * 60; // 2h /** Default max cached external subscription time in minutes (user) */ private final static int DEFAULT_MAX_USER_CACHED_TIME = 2 * 60; // 2h /** iCal external subscription enable flag */ private boolean enabled = false; /** merge iCal external subscriptions from other sites into My Workspace? */ private boolean mergeIntoMyworkspace = true; /** Column map for iCal processing */ private Map columnMap = null; /** Cache map of Institutional Calendars: <String url, Calendar cal> */ private Map<String, ExternalSubscription> institutionalSubscriptions = null; /** Cache map of user Calendars: <String url, Calendar cal> */ private Map<String, ExternalSubscription> userSubscriptions = null; // ###################################################### // Spring services // ###################################################### /** Dependency: CalendarService. */ protected CalendarService m_calendarService = null; public void setCalendarService(CalendarService service) { this.m_calendarService = service; } /** Dependency: ServerConfigurationService. */ protected ServerConfigurationService m_configurationService = null; public void setServerConfigurationService(ServerConfigurationService service) { this.m_configurationService = service; } /** Dependency: CalendarImporterService. */ protected CalendarImporterService m_importerService = null; public void setCalendarImporterService(CalendarImporterService service) { this.m_importerService = service; } /** Dependency: EntityManager. */ protected EntityManager m_entityManager = null; public void setEntityManager(EntityManager service) { this.m_entityManager = service; } /** Dependency: SiteService. */ protected SiteService m_siteService = null; public void setSiteService(SiteService service) { this.m_siteService = service; } /** Dependency: IdManager (COVER). */ protected IdManager m_idManager = org.sakaiproject.id.cover.IdManager.getInstance(); public void init() { // external calendar subscriptions: enable? enabled = m_configurationService.getBoolean(SAK_PROP_EXTSUBSCRIPTIONS_ENABLED, false); mergeIntoMyworkspace = m_configurationService.getBoolean(SAK_PROP_EXTSUBSCRIPTIONS_MERGEINTOMYWORKSPACE, true); m_log.info("init(): enabled: " + enabled + ", merge from other sites into My Workspace? "+mergeIntoMyworkspace); if (enabled) { // iCal column map try { columnMap = m_importerService .getDefaultColumnMap(CalendarImporterService.ICALENDAR_IMPORT); } catch (ImportException e1) { m_log .error("Unable to get column map for ICal import. External subscriptions will be disabled."); enabled = false; return; } // subscription cache config // Institutional subscription defaults: max 16 entries, max 2 hours int institutionalMaxSize = m_configurationService.getInt(SAK_PROP_EXTSUBSCRIPTIONS_URL+".count", DEFAULT_MAX_INST_CACHED_ENTRIES ); int institutionalMaxTime = m_configurationService.getInt( SAK_PROP_EXTSUBSCRIPTIONS_INST_CACHETIME, DEFAULT_MAX_INST_CACHED_TIME); m_log.info("init(): " + institutionalMaxSize + " institutional subscriptions in memory, re-loading every " + institutionalMaxTime + " min"); institutionalSubscriptions = new SubscriptionCacheMap(institutionalMaxSize, institutionalMaxTime * 60 * 1000 ); // User subscription defaults: max 32 entries, max 2 hours int userMaxSize = m_configurationService.getInt( SAK_PROP_EXTSUBSCRIPTIONS_USER_CACHEENTRIES, DEFAULT_MAX_USER_CACHED_ENTRIES); int userMaxTime = m_configurationService.getInt( SAK_PROP_EXTSUBSCRIPTIONS_USER_CACHETIME, DEFAULT_MAX_USER_CACHED_TIME); m_log.info("init(): max " + userMaxSize + " user subscriptions in memory, re-loading every " + userMaxTime + " min"); userSubscriptions = new SubscriptionCacheMap(userMaxSize, userMaxTime * 60 * 1000); // add reload-on-expire listener SubscriptionExpiredListener listener = new SubscriptionReloadOnExpiredListener(); ((SubscriptionCacheMap) institutionalSubscriptions) .setSubscriptionExpiredListener(listener); ((SubscriptionCacheMap) userSubscriptions) .setSubscriptionExpiredListener(listener); // load institutional calendar subscriptions loadInstitutionalSubscriptions(); } } public void destroy() { m_log.info("destroy()"); try { if (institutionalSubscriptions != null) { ((SubscriptionCacheMap) institutionalSubscriptions) .stopCleanerThread(); } if (userSubscriptions != null) { ((SubscriptionCacheMap) userSubscriptions).stopCleanerThread(); } } catch (Throwable e) { e.printStackTrace(); } } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } // ###################################################### // PUBLIC methods // ###################################################### /* * (non-Javadoc) * * @see org.sakaiproject.calendar.api.ExternalCalendarSubscriptionService#calendarSubscriptionReference(java.lang.String, * java.lang.String) */ public String calendarSubscriptionReference(String context, String id) { return CalendarService.REFERENCE_ROOT + Entity.SEPARATOR + CalendarService.REF_TYPE_CALENDAR_SUBSCRIPTION + Entity.SEPARATOR + context + Entity.SEPARATOR + id; } /* * (non-Javadoc) * * @see org.sakaiproject.calendar.impl.ExternalCalendarSubscriptionService#getCalendarSubscription(java.lang.String) */ public Calendar getCalendarSubscription(String reference) { if (!isEnabled() || reference == null) return null; // Get Reference and Subscription URL Reference _ref = m_entityManager.newReference(reference); String subscriptionUrl = getSubscriptionUrlFromId(_ref.getId()); if (subscriptionUrl == null || subscriptionUrl.equals("null")) return null; m_log.debug("ExternalCalendarSubscriptionService.getCalendarSubscription(" + reference + ")"); m_log.debug(" |-> subscriptionUrl: " + subscriptionUrl); ExternalSubscription subscription = null; // 1. Is a institutional subscription (cached)? if (institutionalSubscriptions.containsKey(subscriptionUrl)) { m_log.debug(" |-> Is a institutional subscription"); subscription = institutionalSubscriptions.get(subscriptionUrl); // may not have this one loaded yet... if (subscription == null || subscription.getCalendar() == null) { m_log.debug(" |-> Not cached yet..."); reloadInstitutionalSubscription(subscriptionUrl, INSTITUTIONAL_CONTEXT); subscription = institutionalSubscriptions.get(subscriptionUrl); } if (subscription != null) subscription.setContext(_ref.getContext()); } // 2. Is the user subscription cached? else if (userSubscriptions.containsKey(subscriptionUrl)) { m_log.debug(" |-> Is a user subscription"); subscription = userSubscriptions.get(subscriptionUrl); } // 3. Is a user subscription but is not cached. if (!institutionalSubscriptions.containsKey(subscriptionUrl) && (subscription == null || subscription.getCalendar() == null)) { m_log.debug(" |-> Not cached yet..."); subscription = loadCalendarSubscriptionFromUrl(subscriptionUrl, _ref .getContext()); if (subscription != null) { userSubscriptions.put(subscriptionUrl, subscription); subscription.setContext(_ref.getContext()); } } m_log.debug(" |-> Subscription is " + subscription); if (subscription != null) { m_log.debug(" |-> Calendar is " + subscription.getCalendar()); return subscription.getCalendar(); } else { m_log.debug(" |-> Calendar is NULL"); return null; } } public Set<String> getCalendarSubscriptionChannelsForChannels( String primaryCalendarReference, Collection<Object> channels) { Set<String> subscriptionChannels = new HashSet<String>(); Set<String> subscriptionUrlsAdded = new HashSet<String>(); if(isOnWorkspaceTab() && (!mergeIntoMyworkspace || SecurityService.isSuperUser())) { channels = new ArrayList<Object>(); channels.add(primaryCalendarReference); } for (Object channel : channels) { Set<String> channelSubscriptions = getCalendarSubscriptionChannelsForChannel((String) channel); for (String channelSub : channelSubscriptions) { Reference ref = m_entityManager.newReference(channelSub); if (!subscriptionUrlsAdded.contains(ref.getId())) { subscriptionChannels.add(channelSub); subscriptionUrlsAdded.add(ref.getId()); } } } return subscriptionChannels; } /* * (non-Javadoc) * * @see org.sakaiproject.calendar.impl.ExternalCalendarSubscriptionService#getCalendarSubscriptionChannelsForSite() */ public Set<String> getCalendarSubscriptionChannelsForChannel(String reference) { Set<String> channels = new HashSet<String>(); if (!isEnabled() || reference == null) return channels; // get externally subscribed urls from tool config Reference ref = m_entityManager.newReference(reference); Site site = null; try { site = m_siteService.getSite(ref.getContext()); } catch (IdUnusedException e) { m_log .error("ExternalCalendarSubscriptionService.getCalendarSubscriptionChannelsForChannel(): IdUnusedException for context in reference: " + reference); return channels; } ToolConfiguration tc = site.getToolForCommonId(SCHEDULE_TOOL_ID); Properties config = tc == null? null : tc.getConfig(); if (tc != null && config != null) { String prop = config.getProperty(TC_PROP_SUBCRIPTIONS); if (prop != null) { String[] chsPair = prop.split(SUBS_REF_DELIMITER); for (int i = 0; i < chsPair.length; i++) { String[] pair = chsPair[i].split(SUBS_NAME_DELIMITER); channels.add(pair[0]); } } } return channels; } public Set<ExternalSubscription> getAvailableInstitutionalSubscriptionsForChannel( String reference) { Set<ExternalSubscription> subscriptions = new HashSet<ExternalSubscription>(); if (!isEnabled() || reference == null) return subscriptions; Reference ref = m_entityManager.newReference(reference); for (ExternalSubscription subscription : institutionalSubscriptions.values()) { subscription.setContext(ref.getContext()); subscriptions.add(subscription); } return subscriptions; } public Set<ExternalSubscription> getSubscriptionsForChannel(String reference, boolean loadCalendar) { Set<ExternalSubscription> subscriptions = new HashSet<ExternalSubscription>(); if (!isEnabled() || reference == null) return subscriptions; // get externally subscribed urls from tool config Reference ref = m_entityManager.newReference(reference); Site site = null; try { site = m_siteService.getSite(ref.getContext()); } catch (IdUnusedException e) { m_log .error("ExternalCalendarSubscriptionService.getSubscriptionsForChannel(): IdUnusedException for context in reference: " + reference); return subscriptions; } ToolConfiguration tc = site.getToolForCommonId(SCHEDULE_TOOL_ID); Properties config = tc == null? null : tc.getConfig(); if (tc != null && config != null) { String prop = config.getProperty(TC_PROP_SUBCRIPTIONS); if (prop != null) { String[] chsPair = prop.split(SUBS_REF_DELIMITER); for (int i = 0; i < chsPair.length; i++) { String[] pair = chsPair[i].split(SUBS_NAME_DELIMITER); String r = pair[0]; Reference r1 = m_entityManager.newReference(r); String url = getSubscriptionUrlFromId(r1.getId()); String name = null; if (pair.length == 2) name = pair[1]; else { try { name = institutionalSubscriptions.get(url) .getSubscriptionName(); } catch (Exception e) { name = url; } } ExternalSubscription subscription = new BaseExternalSubscription( name, url, ref.getContext(), loadCalendar ? getCalendarSubscription(r) : null, isInstitutionalCalendar(r)); subscriptions.add(subscription); } } } return subscriptions; } /* * (non-Javadoc) * * @see org.sakaiproject.calendar.impl.ExternalCalendarSubscriptionService#setSubscriptionsForChannel(String, * Collection<ExternalSubscription>) */ public void setSubscriptionsForChannel(String reference, Collection<ExternalSubscription> subscriptions) { if (!isEnabled() || reference == null) return; // set externally subscriptions in tool config Reference ref = m_entityManager.newReference(reference); Site site = null; try { site = m_siteService.getSite(ref.getContext()); } catch (IdUnusedException e) { m_log .error("ExternalCalendarSubscriptionService.setSubscriptionsForChannel(): IdUnusedException for context in reference: " + reference); return; } ToolConfiguration tc = site.getToolForCommonId(SCHEDULE_TOOL_ID); if (tc != null) { boolean first = true; StringBuffer tmpStr = new StringBuffer(); for (ExternalSubscription subscription : subscriptions) { if (!first) tmpStr.append(SUBS_REF_DELIMITER); first = false; tmpStr.append(subscription.getReference()); if (!subscription.isInstitutional()) tmpStr.append(SUBS_NAME_DELIMITER + subscription.getSubscriptionName()); } Properties config = tc.getConfig(); config.setProperty(TC_PROP_SUBCRIPTIONS, tmpStr.toString()); tc.save(); } } public boolean isInstitutionalCalendar(String reference) { // Get Reference and Subscription URL Reference _ref = m_entityManager.newReference(reference); String subscriptionUrl = getSubscriptionUrlFromId(_ref.getId()); if (subscriptionUrl == null || subscriptionUrl.equals("null")) return false; // Is a institutional subscription? return institutionalSubscriptions.containsKey(subscriptionUrl); } public String getIdFromSubscriptionUrl(String url) { // use Base64 byte[] encoded = CommonsCodecBase64.encodeBase64(url.getBytes()); // '/' cannot be used in Reference => use '.' instead (not part of // Base64 alphabet) String encStr = new String(encoded).replaceAll("/", "\\."); return encStr; } public String getSubscriptionUrlFromId(String id) { // use Base64 byte[] decoded = CommonsCodecBase64.decodeBase64(id.replaceAll("\\.", "/") .getBytes()); return new String(decoded); } // ###################################################### // PRIVATE methods // ###################################################### private void loadInstitutionalSubscriptions() { String[] subscriptionURLs = m_configurationService .getStrings(SAK_PROP_EXTSUBSCRIPTIONS_URL); String[] subscriptionNames = m_configurationService .getStrings(SAK_PROP_EXTSUBSCRIPTIONS_NAME); String[] subscriptionEventTypes = m_configurationService .getStrings(SAK_PROP_EXTSUBSCRIPTIONS_EVENTTYPE); if (subscriptionURLs != null) { for (int i = 0; i < subscriptionURLs.length; i++) { if (!institutionalSubscriptions.containsKey(subscriptionURLs[i]) || institutionalSubscriptions.get(subscriptionURLs[i]) == null || institutionalSubscriptions.get(subscriptionURLs[i]) .getCalendar() == null) { String calendarName = subscriptionURLs[i]; if (subscriptionNames != null && subscriptionNames.length > i) calendarName = subscriptionNames[i]; String forcedEventType = null; if (subscriptionEventTypes != null && subscriptionEventTypes.length > i) forcedEventType = subscriptionEventTypes[i]; ExternalSubscription subscription = loadCalendarSubscriptionFromUrl( subscriptionURLs[i], INSTITUTIONAL_CONTEXT, calendarName, forcedEventType); institutionalSubscriptions.put(subscriptionURLs[i], subscription); } } } } private void reloadInstitutionalSubscription(String subscriptionUrl, String context) { String[] subscriptionURLs = m_configurationService .getStrings(SAK_PROP_EXTSUBSCRIPTIONS_URL); String[] subscriptionNames = m_configurationService .getStrings(SAK_PROP_EXTSUBSCRIPTIONS_NAME); String[] subscriptionEventTypes = m_configurationService .getStrings(SAK_PROP_EXTSUBSCRIPTIONS_EVENTTYPE); if (subscriptionURLs != null) { for (int i = 0; i < subscriptionURLs.length; i++) { if (subscriptionURLs[i].equals(subscriptionUrl)) { String calendarName = null; if (subscriptionNames != null && subscriptionNames.length > i) calendarName = subscriptionNames[i]; String forcedEventType = null; if (subscriptionEventTypes != null && subscriptionEventTypes.length > i) forcedEventType = subscriptionEventTypes[i]; ExternalSubscription subscription = loadCalendarSubscriptionFromUrl( subscriptionURLs[i], context, calendarName, forcedEventType); subscription.setInstitutional(true); institutionalSubscriptions.put(subscriptionURLs[i], subscription); break; } } } } private ExternalSubscription loadCalendarSubscriptionFromUrl(String url, String context) { return loadCalendarSubscriptionFromUrl(url, context, null, null); } private ExternalSubscription loadCalendarSubscriptionFromUrl(String url, String context, String calendarName, String forcedEventType) { ExternalSubscription subscription = new BaseExternalSubscription(calendarName, url, context, null, INSTITUTIONAL_CONTEXT.equals(context)); ExternalCalendarSubscription calendar = null; List<CalendarEvent> events = null; try { URL _url = new URL(url); if (calendarName == null) calendarName = _url.getFile(); // connect URLConnection conn = _url.openConnection(); conn.setConnectTimeout(TIMEOUT); conn.setReadTimeout(TIMEOUT); InputStream stream = conn.getInputStream(); BufferedInputStream buffStream = new BufferedInputStream(stream); // import events = m_importerService.doImport(CalendarImporterService.ICALENDAR_IMPORT, buffStream, columnMap, null); String subscriptionId = getIdFromSubscriptionUrl(url); String reference = calendarSubscriptionReference(context, subscriptionId); calendar = new ExternalCalendarSubscription(reference); for (CalendarEvent event : events) { String eventType = event.getType(); if (forcedEventType != null) eventType = forcedEventType; calendar.addEvent(event.getRange(), event.getDisplayName(), event .getDescription(), eventType, event.getLocation(), event .getRecurrenceRule(), null); } calendar.setName(calendarName); subscription.setCalendar(calendar); m_log.info("Loaded calendar subscription: " + subscription.toString()); buffStream.close(); stream.close(); } catch (ImportException e) { m_log.error("Error loading calendar subscription '" + calendarName + "' (will NOT retry again): " + url, e); String subscriptionId = getIdFromSubscriptionUrl(url); String reference = calendarSubscriptionReference(context, subscriptionId); calendar = new ExternalCalendarSubscription(reference); calendar.setName(calendarName); subscription.setCalendar(calendar); } catch (PermissionException e) { // This will never be called (for now) e.printStackTrace(); } catch (MalformedURLException e) { m_log.error("Mal-formed URL in calendar subscription '" + calendarName + "': " + url, e); } catch (IOException e) { m_log.error("Unable to read calendar subscription '" + calendarName + "' from URL (I/O Error): " + url, e); } catch (Exception e) { m_log.error("Unknown error occurred while reading calendar subscription '" + calendarName + "' from URL: " + url, e); } return subscription; } /** * See if the current tab is the workspace tab (i.e. user site) * @return true if we are currently on the "My Workspace" tab. */ private boolean isOnWorkspaceTab() { return m_siteService.isUserSite(ToolManager.getCurrentPlacement().getContext()); } // ###################################################### // Support classes // ###################################################### public class BaseExternalSubscription implements ExternalSubscription { private String subscriptionName; private String subscriptionUrl; private String reference; private String context; private Calendar calendar; private boolean isInstitutional; public BaseExternalSubscription() { } public BaseExternalSubscription(String subscriptionName, String subscriptionUrl, String context, Calendar calendar, boolean isInstitutional) { setSubscriptionName(subscriptionName); setSubscriptionUrl(subscriptionUrl); setCalendar(calendar); setContext(context); setInstitutional(isInstitutional); if (calendar != null) setReference(calendar.getReference()); } public String getSubscriptionName() { return subscriptionName; } public void setSubscriptionName(String subscriptionName) { this.subscriptionName = subscriptionName; } public String getSubscriptionUrl() { return subscriptionUrl; } public void setSubscriptionUrl(String subscriptionUrl) { this.subscriptionUrl = subscriptionUrl; } public String getContext() { return context; } public void setContext(String context) { this.context = context; if (calendar != null) ((ExternalCalendarSubscription) calendar).setContext(context); } public void setReference(String reference) { this.reference = reference; } public String getReference() { if (calendar != null) return calendar.getReference(); else return calendarSubscriptionReference(context, getIdFromSubscriptionUrl(subscriptionUrl)); } public Calendar getCalendar() { return calendar; } public void setCalendar(Calendar calendar) { this.calendar = calendar; } public boolean isInstitutional() { return isInstitutional; } public void setInstitutional(boolean isInstitutional) { this.isInstitutional = isInstitutional; } @Override public boolean equals(Object o) { if (o instanceof BaseExternalSubscription) return getReference().equals( ((BaseExternalSubscription) o).getReference()); return false; } @Override public int hashCode() { int hashCode = super.hashCode(); if (getReference() != null) { hashCode += getReference().hashCode(); }; return hashCode; } @Override public String toString() { StringBuilder buff = new StringBuilder(); buff.append(getSubscriptionName() != null ? getSubscriptionName() : ""); buff.append('|'); buff.append(getSubscriptionUrl()); buff.append('|'); buff.append(getReference()); return buff.toString(); } } public class ExternalCalendarSubscription implements Calendar { /** Memory storage */ protected Map<String, CalendarEvent> m_storage = new HashMap<String, CalendarEvent>(); /** The context in which this calendar exists. */ protected String m_context = null; /** Store the unique-in-context calendar id. */ protected String m_id = null; /** Store the calendar name. */ protected String m_name = null; /** The properties. */ protected ResourcePropertiesEdit m_properties = null; protected String modifiedDateStr = null; public ExternalCalendarSubscription(String ref) { // set the ids Reference r = m_entityManager.newReference(ref); m_context = r.getContext(); m_id = r.getId(); // setup for properties m_properties = new BaseResourcePropertiesEdit(); } public CalendarEvent addEvent(TimeRange range, String displayName, String description, String type, String location, EventAccess access, Collection groups, List attachments) throws PermissionException { return addEvent(range, displayName, description, type, location, attachments); } public CalendarEvent addEvent(TimeRange range, String displayName, String description, String type, String location, List attachments) throws PermissionException { return addEvent(range, displayName, description, type, location, null, attachments); } public CalendarEvent addEvent(TimeRange range, String displayName, String description, String type, String location, RecurrenceRule rrule, List attachments) throws PermissionException { // allocate a new unique event id // String id = getUniqueId(); String id = getUniqueIdBasedOnFields(displayName, description, type, location); // create event ExternalCalendarEvent edit = new ExternalCalendarEvent(m_context, m_id, id); // set it up edit.setRange(range); edit.setDisplayName(displayName); edit.setDescription(description); edit.setType(type); edit.setLocation(location); edit.setCreator(); if (rrule != null) edit.setRecurrenceRule(rrule); // put in storage m_storage.put(id, edit); return edit; } public CalendarEventEdit addEvent() throws PermissionException { // allocate a new unique event id // String id = getUniqueId(); // create event // CalendarEventEdit event = new ExternalCalendarEvent(this, id); // put in storage // m_storage.put(id, event); return null; } public CalendarEvent addEvent(CalendarEvent event) { // allocate a new unique event id String id = event.getId(); // put in storage m_storage.put(id, event); return event; } public Collection<CalendarEvent> getAllEvents() { return m_storage.values(); } public boolean allowAddCalendarEvent() { return false; } public boolean allowAddEvent() { return false; } public boolean allowEditEvent(String eventId) { return false; } public boolean allowGetEvent(String eventId) { return true; } public boolean allowGetEvents() { return true; } public boolean allowRemoveEvent(CalendarEvent event) { return false; } public void cancelEvent(CalendarEventEdit edit) { } public void commitEvent(CalendarEventEdit edit, int intention) { } public void commitEvent(CalendarEventEdit edit) { } public String getContext() { return m_context; } public CalendarEventEdit getEditEvent(String eventId, String editType) throws IdUnusedException, PermissionException, InUseException { return null; } public CalendarEvent getEvent(String eventId) throws IdUnusedException, PermissionException { return m_storage.get(eventId); } public String getEventFields() { return m_properties .getPropertyFormatted(ResourceProperties.PROP_CALENDAR_EVENT_FIELDS); } public List getEvents(TimeRange range, Filter filter) throws PermissionException { return filterEvents(new ArrayList<CalendarEvent>(m_storage.values()), range); } public boolean getExportEnabled() { return false; } public Collection getGroupsAllowAddEvent() { return new ArrayList(); } public Collection getGroupsAllowGetEvent() { return new ArrayList(); } public Collection getGroupsAllowRemoveEvent(boolean own) { return new ArrayList(); } public Time getModified() { return TimeService.newTimeGmt(modifiedDateStr); } public CalendarEventEdit mergeEvent(Element el) throws PermissionException, IdUsedException { // TODO Implement mergeEvent() return null; } public void removeEvent(CalendarEventEdit edit, int intention) throws PermissionException { } public void removeEvent(CalendarEventEdit edit) throws PermissionException { } public void setExportEnabled(boolean enable) { } public void setModified() { } public String getId() { return m_id; } public ResourceProperties getProperties() { return m_properties; } public String getReference() { return m_calendarService.calendarSubscriptionReference(m_context, m_id); } protected void setContext(String context) { // set the ids m_context = context; for (CalendarEvent e : m_storage.values()) { // ((ExternalCalendarEvent) e).setCalendar(this); ((ExternalCalendarEvent) e).setCalendarContext(m_context); ((ExternalCalendarEvent) e).setCalendarId(m_id); } } public String getReference(String rootProperty) { return rootProperty + getReference(); } public String getUrl() { // TODO Auto-generated method stub return null; } public String getUrl(String rootProperty) { // TODO Auto-generated method stub return null; } public Element toXml(Document doc, Stack stack) { // TODO Auto-generated method stub return null; } public String getName() { return m_name; } public void setName(String calendarName) { this.m_name = calendarName; } /** * Access the id generating service and return a unique id. * * @return a unique id. */ protected String getUniqueId() { return m_idManager.createUuid(); } protected String getUniqueIdBasedOnFields(String displayName, String description, String type, String location) { StringBuffer key = new StringBuffer(); key.append(displayName + description + type + location); String id = null; int n = 0; boolean unique = false; while (!unique) { byte[] bytes = key.toString().getBytes(); try{ MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.update(bytes); bytes = digest.digest(); id = getHexStringFromBytes(bytes); }catch(NoSuchAlgorithmException e){ // fall back to Base64 byte[] encoded = CommonsCodecBase64.encodeBase64(bytes); id = new String(encoded); } if (!m_storage.containsKey(id)) unique = true; else key.append(n++); } return id; } protected String getHexStringFromBytes(byte[] raw) { final String HEXES = "0123456789ABCDEF"; if(raw == null) { return null; } final StringBuilder hex = new StringBuilder(2 * raw.length); for(final byte b : raw) { hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))); } return hex.toString(); } /** * Filter the events to only those in the time range. * * @param events * The full list of events. * @param range * The time range. * @return A list of events from the incoming list that overlap the * given time range. */ protected List<CalendarEvent> filterEvents(List<CalendarEvent> events, TimeRange range) { List<CalendarEvent> filtered = new ArrayList<CalendarEvent>(); for (int i = 0; i < events.size(); i++) { CalendarEvent event = events.get(i); // resolve the event to the list of events in this range // TODO Support for recurring events List<CalendarEvent> resolved = ((ExternalCalendarEvent) event) .resolve(range); filtered.addAll(resolved); } return filtered; } } public class ExternalCalendarEvent implements CalendarEvent { // protected Calendar m_calendar = null; protected String m_calendar_context = null; protected String m_calendar_id = null; protected ResourcePropertiesEdit m_properties = null; protected String m_id = null; protected String calendarReference = null; protected TimeRange m_range = null; protected TimeRange m_baseRange = null; protected RecurrenceRule m_singleRule = null; protected RecurrenceRule m_exclusionRule = null; public ExternalCalendarEvent(String calendarContext, String calendarId, String id) { this(calendarContext, calendarId, id, null); } public ExternalCalendarEvent(String calendarContext, String calendarId, String id, String eventType) { m_id = id; // m_calendar = calendar; m_calendar_context = calendarContext; m_calendar_id = calendarId; m_properties = new BaseResourcePropertiesEdit(); if (eventType != null) m_properties .addProperty(ResourceProperties.PROP_CALENDAR_TYPE, eventType); } public ExternalCalendarEvent(CalendarEvent other, RecurrenceInstance ri) { // m_calendar = ((ExternalCalendarEvent) other).m_calendar; m_calendar_context = ((ExternalCalendarEvent) other).m_calendar_context; m_calendar_id = ((ExternalCalendarEvent) other).m_calendar_id; // encode the instance and the other's id into my id m_id = '!' + ri.getRange().toString() + '!' + ri.getSequence() + '!' + ((ExternalCalendarEvent) other).m_id; // use the new range m_range = (TimeRange) ri.getRange().clone(); m_baseRange = ((ExternalCalendarEvent) other).m_range; // point at the properties m_properties = ((ExternalCalendarEvent) other).m_properties; // point at the rules m_singleRule = ((ExternalCalendarEvent) other).m_singleRule; m_exclusionRule = ((ExternalCalendarEvent) other).m_exclusionRule; } public EventAccess getAccess() { return CalendarEvent.EventAccess.SITE; } public String getCalendarReference() { // return m_calendar.getReference(); return m_calendarService.calendarSubscriptionReference(m_calendar_context, m_calendar_id); } // protected Calendar getCalendar(){ // return m_calendar; // } // protected void setCalendar(Calendar calendar) { // m_calendar = calendar; // } protected void setCalendarContext(String calendarContext) { m_calendar_context = calendarContext; } protected void setCalendarId(String calendarId) { m_calendar_id = calendarId; } public String getCreator() { return m_properties.getProperty(ResourceProperties.PROP_CREATOR); } public String getDescription() { return FormattedText .convertFormattedTextToPlaintext(getDescriptionFormatted()); } public String getDescriptionFormatted() { // %%% JANDERSE the calendar event description can now be formatted // text // first try to use the formatted text description; if that isn't // found, use the plaintext description String desc = m_properties .getPropertyFormatted(ResourceProperties.PROP_DESCRIPTION + "-html"); if (desc != null && desc.length() > 0) return desc; desc = m_properties.getPropertyFormatted(ResourceProperties.PROP_DESCRIPTION + "-formatted"); desc = FormattedText.convertOldFormattedText(desc); if (desc != null && desc.length() > 0) return desc; desc = FormattedText.convertPlaintextToFormattedText(m_properties .getPropertyFormatted(ResourceProperties.PROP_DESCRIPTION)); return desc; } public String getDisplayName() { return m_properties .getPropertyFormatted(ResourceProperties.PROP_DISPLAY_NAME); } public String getField(String name) { // names are prefixed to form a namespace name = ResourceProperties.PROP_CALENDAR_EVENT_FIELDS + "." + name; return m_properties.getPropertyFormatted(name); } public Collection getGroupObjects() { return new ArrayList(); } public String getGroupRangeForDisplay(Calendar calendar) { return ""; } public Collection getGroups() { return new ArrayList(); } public String getLocation() { return m_properties .getPropertyFormatted(ResourceProperties.PROP_CALENDAR_LOCATION); } public String getModifiedBy() { return m_properties.getPropertyFormatted(ResourceProperties.PROP_MODIFIED_BY); } public TimeRange getRange() { // range might be null in the creation process, before the fields // are set in an edit, but // after the storage has registered the event and it's id. if (m_range == null) { return TimeService.newTimeRange(TimeService.newTime(0)); } // return (TimeRange) m_range.clone(); return m_range; } public RecurrenceRule getRecurrenceRule() { return m_singleRule; } public RecurrenceRule getExclusionRule() { if (m_exclusionRule == null) m_exclusionRule = new ExclusionSeqRecurrenceRule(); return m_exclusionRule; } protected List resolve(TimeRange range) { List rv = new Vector(); // for no rules, use the event if it's in range if (m_singleRule == null) { // the actual event if (range.overlaps(getRange())) { rv.add(this); } } // for rules... else { List instances = m_singleRule.generateInstances(this.getRange(), range, TimeService.getLocalTimeZone()); // remove any excluded getExclusionRule().excludeInstances(instances); for (Iterator iRanges = instances.iterator(); iRanges.hasNext();) { RecurrenceInstance ri = (RecurrenceInstance) iRanges.next(); // generate an event object that is exactly like me but with // this range and no rules CalendarEvent clone = new ExternalCalendarEvent(this, ri); rv.add(clone); } } return rv; } public void setRecurrenceRule(RecurrenceRule rule) { m_singleRule = rule; } public void setExclusionRule(RecurrenceRule rule) { m_exclusionRule = rule; } public String getType() { return m_properties .getPropertyFormatted(ResourceProperties.PROP_CALENDAR_TYPE); } public boolean isUserOwner() { return false; } public String getId() { return m_id; } protected void setId(String id) { m_id = id; } public ResourceProperties getProperties() { return m_properties; } public String getReference() { // return m_calendar.getReference() + Entity.SEPARATOR + m_id; return m_calendarService.eventSubscriptionReference(m_calendar_context, m_calendar_id, m_id); } public String getReference(String rootProperty) { return rootProperty + getReference(); } public String getUrl() { return null;// m_calendar.getUrl() + getId(); } public String getUrl(String rootProperty) { return rootProperty + getUrl(); } public Element toXml(Document doc, Stack stack) { // TODO Auto-generated method stub return null; } public int compareTo(Object o) { if (!(o instanceof CalendarEvent)) throw new ClassCastException(); Time mine = getRange().firstTime(); Time other = ((CalendarEvent) o).getRange().firstTime(); if (mine.before(other)) return -1; if (mine.after(other)) return +1; return 0; } public List getAttachments() { // TODO Auto-generated method stub return null; } public void setCreator() { String currentUser = SessionManager.getCurrentSessionUserId(); String now = TimeService.newTime().toString(); m_properties.addProperty(ResourceProperties.PROP_CREATOR, currentUser); m_properties.addProperty(ResourceProperties.PROP_CREATION_DATE, now); } public void setLocation(String location) { m_properties.addProperty(ResourceProperties.PROP_CALENDAR_LOCATION, location); } public void setType(String type) { m_properties.addProperty(ResourceProperties.PROP_CALENDAR_TYPE, type); } public void setDescription(String description) { setDescriptionFormatted(FormattedText .convertPlaintextToFormattedText(description)); } public void setDescriptionFormatted(String description) { // %%% JANDERSE the calendar event description can now be formatted // text // save both a formatted and a plaintext version of the description m_properties.addProperty(ResourceProperties.PROP_DESCRIPTION + "-html", description); m_properties.addProperty(ResourceProperties.PROP_DESCRIPTION, FormattedText .convertFormattedTextToPlaintext(description)); } public void setDisplayName(String displayName) { m_properties.addProperty(ResourceProperties.PROP_DISPLAY_NAME, displayName); } public void setRange(TimeRange range) { m_range = (TimeRange) range.clone(); } /** * Gets a site name for this calendar event */ public String getSiteName() { String calendarName = ""; if (m_calendar_context != null) { try { Site site = m_siteService.getSite(m_calendar_context); if (site != null) calendarName = site.getTitle(); } catch (IdUnusedException e) { m_log.warn(".getSiteName(): " + e); } } return calendarName; } } /** * Hash table and linked list implementation of the Map interface, * access-ordered. Older entries will be removed if map exceeds the maximum * capacity specified. * * @author nfernandes */ class SubscriptionCacheMap extends LinkedHashMap<String, ExternalSubscription> implements Runnable { private static final long serialVersionUID = 1L; private final static float DEFAULT_LOAD_FACTOR = 0.75f; private int maxCachedEntries; private int maxCachedTime; private Thread threadCleaner; private boolean threadCleanerRunning = false; private Object threadCleanerRunningSemaphore = new Object(); private Map<String, Long> cacheTime; private SubscriptionExpiredListener listener; private Object listenerLock = new Object(); public SubscriptionCacheMap() { this(DEFAULT_MAX_USER_CACHED_ENTRIES, DEFAULT_MAX_USER_CACHED_TIME); } /** * LinkedHashMap implementation that removes least accessed entry and * (optionally) removes entries with more that maxCachedTime. * * @param maxCachedEntries * Maximum number of entries to keep cached. * @param maxCachedTime * If > 0, entries will be removed after being 'maxCachedTime' in * cache. */ public SubscriptionCacheMap(int maxCachedEntries, int maxCachedTime) { super(maxCachedEntries, DEFAULT_LOAD_FACTOR, true); this.maxCachedEntries = maxCachedEntries; this.maxCachedTime = maxCachedTime; if (maxCachedTime > 0) { cacheTime = new ConcurrentHashMap<String, Long>(); startCleanerThread(); } } public void setSubscriptionExpiredListener(SubscriptionExpiredListener listener) { synchronized(listenerLock) { this.listener = listener; } } public void removeSubscriptionExpiredListener() { synchronized(listenerLock) { this.listener = null; } } @Override public ExternalSubscription get(Object arg0) { ExternalSubscription e = super.get(arg0); return e; } @Override public ExternalSubscription put(String key, ExternalSubscription value) { if (maxCachedTime > 0 && key != null) { cacheTime.put(key, System.currentTimeMillis()); } return super.put(key, value); } @Override public void putAll(Map<? extends String, ? extends ExternalSubscription> map) { if (maxCachedTime > 0 && map != null) { for (String key : map.keySet()) { cacheTime.put(key, System.currentTimeMillis()); } } if ( map != null ) super.putAll(map); } @Override public void clear() { if (maxCachedTime > 0) { cacheTime.clear(); } super.clear(); } @Override public ExternalSubscription remove(Object key) { if (maxCachedTime > 0 && key != null) { if (cacheTime.containsKey(key)) cacheTime.remove(key); } return super.remove(key); } public void setMaxCachedEntries(int maxCachedEntries) { this.maxCachedEntries = maxCachedEntries; } @Override protected boolean removeEldestEntry(Entry<String, ExternalSubscription> arg0) { return size() > maxCachedEntries; } public void run() { try { while (threadCleanerRunning) { // clean expired entries List<String> toClear = new ArrayList<String>(); for (String key : this.keySet()) { long cachedFor = System.currentTimeMillis() - cacheTime.get(key); if (cachedFor > maxCachedTime) { toClear.add(key); } } // cleaning is not object removal but, Calendar removal from // value (ExternalSubscription) for (String key : toClear) { synchronized (listenerLock) { ExternalSubscription e = this.get(key); if (e != null) { e.setCalendar(null); this.put(key, e); m_log.debug("Cleared cache for expired Calendar Subscription: " + key); if (listener != null) { listener.subscriptionExpired(key, e); } } } } // sleep if no work to do if (!threadCleanerRunning) break; try { synchronized (threadCleanerRunningSemaphore) { threadCleanerRunningSemaphore.wait(maxCachedTime); } } catch (InterruptedException e) { m_log.warn("Failed to sleep SmallCacheMap entry cleaner thread", e); } } } catch (Throwable t) { m_log.debug("Failed to execute SmallCacheMap entry cleaner thread", t); } finally { if (threadCleanerRunning) { // thread was stopped by an unknown error: restart m_log .debug("SmallCacheMap entry cleaner thread was stoped by an unknown error: restarting..."); startCleanerThread(); } else m_log.debug("Finished SmallCacheMap entry cleaner thread"); } } /** Start the update thread */ private void startCleanerThread() { threadCleanerRunning = true; threadCleaner = null; threadCleaner = new Thread(this, this.getClass().getName()); threadCleaner.start(); } /** Stop the update thread */ private void stopCleanerThread() { threadCleanerRunning = false; synchronized (threadCleanerRunningSemaphore) { threadCleanerRunningSemaphore.notifyAll(); } } } interface SubscriptionExpiredListener { public void subscriptionExpired(String subscriptionUrl, ExternalSubscription subscription); } class SubscriptionReloadOnExpiredListener implements SubscriptionExpiredListener { public void subscriptionExpired(String subscriptionUrl, ExternalSubscription subscription) { if (institutionalSubscriptions.containsKey(subscriptionUrl)) { // if is a Institutional calendar, re-load expired m_log.debug("Re-loading institutional calendar: " + subscriptionUrl); reloadInstitutionalSubscription(subscriptionUrl, subscription .getContext()); } else { // if is a User-specified calendar, re-load expired m_log.debug("Re-loading user-specified calendar: " + subscriptionUrl); if (subscription != null) { ExternalSubscription s = loadCalendarSubscriptionFromUrl(subscriptionUrl, subscription.getContext()); //userSubscriptions.put(subscriptionUrl, subscription); userSubscriptions.put(subscriptionUrl, s); } } } } }