/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * licenses this file to you 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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.jasig.portlet.calendar.adapter; import java.io.IOException; import java.net.URISyntaxException; import java.text.ParseException; import java.util.Date; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.portlet.PortletRequest; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.property.CalScale; import net.fortuna.ical4j.model.property.ProdId; import net.fortuna.ical4j.model.property.Version; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jasig.portlet.calendar.CalendarConfiguration; import org.jasig.portlet.calendar.mvc.CalendarDisplayEvent; import org.joda.time.DateMidnight; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.Interval; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.DateTimeFormatterBuilder; import org.springframework.beans.factory.annotation.Required; import org.springframework.context.MessageSource; /** * @author Jen Bourey, jennifer.bourey@gmail.com * @version $Revision$ */ public class CalendarEventsDao { protected final Log log = LogFactory.getLog(getClass()); private Cache cache; /** @param cache the cache to set */ @Required public void setCache(Cache cache) { this.cache = cache; } private Map<String, DateTimeFormatter> dateFormatters = new ConcurrentHashMap<String, DateTimeFormatter>(); private Map<String, DateTimeFormatter> timeFormatters = new ConcurrentHashMap<String, DateTimeFormatter>(); private MessageSource messageSource; /** * Setter of attribute messageSource. * * @param messageSource the attribute messageSource to set */ @Required public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } public Calendar getCalendar( ICalendarAdapter adapter, CalendarConfiguration calendarConfig, Interval interval, PortletRequest request) { // get the set of pre-timezone-corrected events for the requested period // Rely on the adapter to do caching final CalendarEventSet eventSet = adapter.getEvents(calendarConfig, interval, request); // Create a calendar from the events Calendar calendar = new Calendar(); calendar.getProperties().add(new ProdId("-//Ben Fortuna//iCal4j 1.0//EN")); calendar.getProperties().add(Version.VERSION_2_0); calendar.getProperties().add(CalScale.GREGORIAN); for (VEvent event : eventSet.getEvents()) { calendar.getComponents().add(event); } return calendar; } /** * Obtains the calendar events from the adapter and returns timezone-adjusted events within the * requested interval. * * @param adapter Adapter to invoke to obtain the calendar events * @param calendar Per-user Calendar configuration * @param interval Interval to return events for * @param request Portlet request * @param usersConfiguredDateTimeZone Timezone to adjust the calendar events to (typically the * user's timezone) * @return Set of calendar events meeting the requested criteria */ public Set<CalendarDisplayEvent> getEvents( ICalendarAdapter adapter, CalendarConfiguration calendar, Interval interval, PortletRequest request, DateTimeZone usersConfiguredDateTimeZone) { // Get the set of calendar events for the requested period. // We invoke the adapter before checking cache because we expect the adapter // to do the first-level caching of the events. final CalendarEventSet eventSet = adapter.getEvents(calendar, interval, request); // The calendar events from the adapter will reflect the timezone of the calendar // server in the event times. The events need to be corrected to reflect the // requested timezone (typically the user's timezone). Adjusting the // event's timezone is an expensive operation so the JSON of the // timezone-adjusted events is cached per timezone. // Append the requested time zone id to the retrieve event set's cache // key to generate a timezone-aware cache key final String tzKey = eventSet.getKey().concat(usersConfiguredDateTimeZone.getID()); // attempt to retrieve the timezone-aware event set from cache Element cachedElement = this.cache.get(tzKey); if (cachedElement != null) { if (log.isDebugEnabled()) { log.debug("Retrieving JSON timezone-aware event set from cache, key:" + tzKey); } @SuppressWarnings("unchecked") final Set<CalendarDisplayEvent> jsonEvents = (Set<CalendarDisplayEvent>) cachedElement.getValue(); return jsonEvents; } // if the timezone-corrected event set is not available in the cache // generate a new set and cache it else { // for each event in the event set, generate a set of timezone-corrected // event occurrences and add it to the new set final Set<CalendarDisplayEvent> displayEvents = new HashSet<CalendarDisplayEvent>(); for (VEvent event : eventSet.getEvents()) { try { displayEvents.addAll( getDisplayEvents(event, interval, request.getLocale(), usersConfiguredDateTimeZone)); } catch (ParseException e) { log.error("Exception parsing event", e); } catch (IOException e) { log.error("Exception parsing event", e); } catch (URISyntaxException e) { log.error("Exception parsing event", e); } catch (IllegalArgumentException e) { // todo fix the root problem. Just masking for the moment because no time to fix. log.info("Likely invalid event returned from exchangeAdapter; see CAP-159"); } } // Cache and return the resulting event list. If the event set // was cached, set the event list to expire at about the same time so it // doesn't live in cache beyond the time the data it is derived // from is considered up to date. Time to live is relative to the // time the item is put into cache so the resulting event list will typically // expire from 1 second before the event set to afterward by the amount // of execution time from the adapter to displayEvents computation // completing which should not be a big delta. cachedElement = new Element(tzKey, displayEvents); long currentTime = System.currentTimeMillis(); if (eventSet.getExpirationTime() > currentTime) { long timeToLiveInMilliseconds = eventSet.getExpirationTime() - currentTime; int timeToLiveInSeconds = (int) timeToLiveInMilliseconds / 1000; cachedElement.setTimeToLive(timeToLiveInSeconds); if (log.isDebugEnabled()) { log.debug( "Storing JSON timezone-aware event set to cache, key:" + tzKey + " with expiration in " + timeToLiveInSeconds + " seconds to" + " coincide with adapter's cache expiration time"); } } this.cache.put(cachedElement); return displayEvents; } } /** * Get a JSON-appropriate representation of each recurrence of an event within the specified time * period. * * @param e * @param interval * @param usersConfiguredDateTimeZone * @return * @throws IOException * @throws URISyntaxException * @throws ParseException */ protected Set<CalendarDisplayEvent> getDisplayEvents( VEvent e, Interval interval, Locale locale, DateTimeZone usersConfiguredDateTimeZone) throws IOException, URISyntaxException, ParseException { final VEvent event = (VEvent) e.copy(); DateTime eventStart; DateTime eventEnd = null; if (event.getStartDate().getTimeZone() == null && !event.getStartDate().isUtc()) { if (log.isDebugEnabled()) { log.debug("Identified event " + event.getSummary() + " as a floating event"); } int offset = usersConfiguredDateTimeZone.getOffset(event.getStartDate().getDate().getTime()); eventStart = new DateTime( event.getStartDate().getDate().getTime() - offset, usersConfiguredDateTimeZone); if (event.getEndDate() != null) { eventEnd = new DateTime( event.getEndDate().getDate().getTime() - offset, usersConfiguredDateTimeZone); } } else { eventStart = new DateTime(event.getStartDate().getDate(), usersConfiguredDateTimeZone); if (event.getEndDate() != null) { eventEnd = new DateTime(event.getEndDate().getDate(), usersConfiguredDateTimeZone); } } if (eventEnd == null) { eventEnd = eventStart; } // Multi-day events may begin in the past; make sure to choose a date in range for the first pass... final Date firstDayToProcess = interval.contains(event.getStartDate().getDate().getTime()) ? event.getStartDate().getDate() : interval.getStart().toDate(); DateMidnight startOfTheSpecificDay = new DateMidnight(firstDayToProcess, usersConfiguredDateTimeZone); DateMidnight endOfTheSpecificDay = startOfTheSpecificDay.plusDays(1); final DateTimeFormatter df = getDateFormatter(locale, usersConfiguredDateTimeZone); final DateTimeFormatter tf = getTimeFormatter(locale, usersConfiguredDateTimeZone); final Set<CalendarDisplayEvent> events = new HashSet<CalendarDisplayEvent>(); final Interval eventInterval = new Interval(eventStart, eventEnd); do { final Interval theSpecificDay = new Interval( startOfTheSpecificDay.getMillis(), endOfTheSpecificDay.getMillis(), usersConfiguredDateTimeZone); /* * Test if the event interval abuts the start of the day or is within the day. * This start time check is needed for the corner case where a zero duration interval * is set for midnight. * The start times are tested directly as opposed to using abuts() because that method * also returns true if the intervals abut at the end of the day. We want to associate * instant events that start at midnight with the starting day, not the ending day. */ if (theSpecificDay.getStart().isEqual(eventStart) || theSpecificDay.overlaps(eventInterval)) { final CalendarDisplayEvent json = new CalendarDisplayEvent(event, eventInterval, theSpecificDay, df, tf); events.add(json); } startOfTheSpecificDay = startOfTheSpecificDay.plusDays(1); endOfTheSpecificDay = endOfTheSpecificDay.plusDays(1); } while (!startOfTheSpecificDay.isAfter(eventEnd) && interval.contains(startOfTheSpecificDay)); return events; } protected DateTimeFormatter getDateFormatter(Locale locale, DateTimeZone timezone) { if (this.dateFormatters.containsKey(timezone.getID())) { return this.dateFormatters.get(timezone.getID()); } final String displayPattern = this.messageSource.getMessage("date.formatter.display", null, "EEE MMM d", locale); DateTimeFormatter df = new DateTimeFormatterBuilder() .appendPattern(displayPattern) .toFormatter() .withZone(timezone); this.dateFormatters.put(timezone.getID(), df); return df; } protected DateTimeFormatter getTimeFormatter(Locale locale, DateTimeZone timezone) { if (this.timeFormatters.containsKey(timezone.getID())) { return this.timeFormatters.get(timezone.getID()); } final String displayPattern = this.messageSource.getMessage("time.formatter.display", null, "h:mm a", locale); DateTimeFormatter tf = new DateTimeFormatterBuilder() .appendPattern(displayPattern) .toFormatter() .withZone(timezone); this.timeFormatters.put(timezone.getID(), tf); return tf; } }