/** * 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.net.SocketException; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.portlet.PortletRequest; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.DateTime; import net.fortuna.ical4j.model.PropertyList; import net.fortuna.ical4j.model.Recur; import net.fortuna.ical4j.model.TimeZone; import net.fortuna.ical4j.model.TimeZoneRegistry; import net.fortuna.ical4j.model.TimeZoneRegistryFactory; import net.fortuna.ical4j.model.WeekDay; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.property.CalScale; import net.fortuna.ical4j.model.property.Description; import net.fortuna.ical4j.model.property.DtEnd; import net.fortuna.ical4j.model.property.DtStamp; import net.fortuna.ical4j.model.property.DtStart; import net.fortuna.ical4j.model.property.Location; import net.fortuna.ical4j.model.property.ProdId; import net.fortuna.ical4j.model.property.RRule; import net.fortuna.ical4j.model.property.Summary; import net.fortuna.ical4j.model.property.Version; import net.fortuna.ical4j.util.UidGenerator; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jasig.portlet.calendar.CalendarConfiguration; import org.jasig.portlet.calendar.caching.ICacheKeyGenerator; import org.jasig.portlet.calendar.caching.RequestAttributeCacheKeyGeneratorImpl; import org.jasig.portlet.calendar.processor.ICalendarContentProcessorImpl; import org.jasig.portlet.calendar.processor.IContentProcessor; import org.jasig.portlet.courses.dao.ICoursesDao; import org.jasig.portlet.courses.model.xml.CourseMeeting; import org.jasig.portlet.courses.model.xml.Term; import org.jasig.portlet.courses.model.xml.TermList; import org.jasig.portlet.courses.model.xml.personal.Course; import org.jasig.portlet.courses.model.xml.personal.CoursesByTerm; import org.joda.time.Interval; /** * Implementation of {@link org.jasig.portlet.calendar.adapter.ICalendarAdapter} that creates a * single calendar in a {@link org.jasig.portlet.calendar.adapter.CalendarEventSet} using data from * a user's courses for the term. * * <p>The implementation expects that a term has a start and end date specified. * * @author James Wennmacher, jameswennmacher@gmail.com * @version $Id$ */ public class CoursesCalendarAdapter extends AbstractCalendarAdapter implements ICalendarAdapter { protected final Log log = LogFactory.getLog(this.getClass()); private Cache cache; private ICoursesDao courseDao; private ICacheKeyGenerator cacheKeyGenerator = new RequestAttributeCacheKeyGeneratorImpl(); private IContentProcessor contentProcessor = new ICalendarContentProcessorImpl(); private String cacheKeyPrefix = "courseDao"; private final UidGenerator uidGenerator; public CoursesCalendarAdapter() { try { this.uidGenerator = new UidGenerator("uidGen"); } catch (SocketException e) { throw new RuntimeException("Failed to create UidGenerator", e); } } /** Map dayCode strings from courses-portlet-api to iCal4j WeekDay objects. */ private enum DAYS { Su(WeekDay.SU), M(WeekDay.MO), T(WeekDay.TU), W(WeekDay.WE), Th(WeekDay.TH), F(WeekDay.FR), Sa(WeekDay.SA); private final WeekDay icalWeekDay; private DAYS(final WeekDay icalWeekDay) { this.icalWeekDay = icalWeekDay; } public WeekDay getIcalWeekDay() { return icalWeekDay; } } public void setCache(Cache cache) { this.cache = cache; } public void setCourseDao(ICoursesDao courseDao) { this.courseDao = courseDao; } public void setCacheKeyGenerator(ICacheKeyGenerator cacheKeyGenerator) { this.cacheKeyGenerator = cacheKeyGenerator; } public void setCacheKeyPrefix(String cacheKeyPrefix) { this.cacheKeyPrefix = cacheKeyPrefix; } public void setContentProcessor(IContentProcessor contentProcessor) { this.contentProcessor = contentProcessor; } public CalendarEventSet getEvents( CalendarConfiguration calendarConfiguration, Interval interval, PortletRequest request) throws CalendarException { String intervalCacheKey = cacheKeyGenerator.getKey( calendarConfiguration, interval, request, cacheKeyPrefix.concat(".") + interval.toString()); // Get the calendar event set for the set of terms from cache CalendarEventSet eventSet; Element cachedElement = this.cache.get(intervalCacheKey); if (cachedElement != null) { if (log.isDebugEnabled()) { log.debug("Retrieving calendar event set from cache, termCacheKey:" + intervalCacheKey); } return (CalendarEventSet) cachedElement.getValue(); } // Get the terms that overlap the requested interval. Current implementation // requires the terms to have the start date and end date present in the // term. TermList allTerms = courseDao.getTermList(request); Set<VEvent> calendarEventSet = new HashSet<VEvent>(); for (Term term : allTerms.getTerms()) { // todo determine if term ending Fri 10/31 (which means THROUGH 10/31 to 23:59:59) // and interval starting Fri 10/31 (meaning 10/31 12:00am) works as expected. // Determine if the interval overlaps any terms. if (interval.getStart().isBefore(term.getEnd().getTimeInMillis()) && interval.getEnd().isAfter(term.getStart().getTimeInMillis())) { Calendar calendar = retrieveCourseCalendar(request, interval, calendarConfiguration, term); Set<VEvent> events = contentProcessor.getEvents(interval, calendar); log.debug("contentProcessor found " + events.size() + " events"); calendarEventSet.addAll(events); } } // Save the calendar event set to the cache. eventSet = new CalendarEventSet(intervalCacheKey, calendarEventSet); cachedElement = new Element(intervalCacheKey, eventSet); if (log.isDebugEnabled()) { log.debug("Storing calendar event set to cache, key:" + intervalCacheKey); } cache.put(cachedElement); return eventSet; } /** * Return the full set of events (class schedule) for all the user's courses for the indicated * term. * * @param request portlet request * @param interval requested interval * @param calendarConfiguration calendar config * @param term term to get class schedule for * @return User's schedule of classes for the indicated term, represented as calendar events */ protected final Calendar retrieveCourseCalendar( PortletRequest request, Interval interval, CalendarConfiguration calendarConfiguration, Term term) { // Try to get the cached calendar for the specified term String termCacheKey = cacheKeyGenerator.getKey( calendarConfiguration, interval, request, cacheKeyPrefix.concat(".").concat(term.getCode())); Element cachedCalendar = this.cache.get(termCacheKey); if (cachedCalendar != null) { if (log.isDebugEnabled()) { log.debug("Retrieving calendar from cache, key:" + termCacheKey); } return (Calendar) cachedCalendar.getValue(); } 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); java.util.Calendar termStartDate = term.getStart(); java.util.Calendar termEndDate = term.getEnd(); CoursesByTerm coursesByTerm = courseDao.getCoursesByTerm(request, term.getCode()); if (coursesByTerm == null) { log.info( "User " + request.getRemoteUser() + " does not have any courses" + " for term " + term + " or invalid term code " + term); return calendar; } List<Course> courses = coursesByTerm.getCourses(); // For each course obtain the list of meeting schedule times and create // events for it. for (Course course : courses) { for (CourseMeeting meeting : course.getCourseMeetings()) { VEvent meetingEvent = createEvent(course, meeting, termStartDate, termEndDate); if (meetingEvent != null) { // Will be null if we can't parse it calendar.getComponents().add(meetingEvent); } } } // save the calendar to the cache cachedCalendar = new Element(termCacheKey, calendar); this.cache.put(cachedCalendar); if (log.isDebugEnabled()) { log.debug("Storing calendar cache, key:" + termCacheKey); } return calendar; } private VEvent createEvent( Course course, CourseMeeting courseMeeting, java.util.Calendar termStartDate, java.util.Calendar termEndDate) { // Treat meetings without a start date or end date as invalid. if (termStartDate == null && courseMeeting.getStartDate() == null) { log.error( "Course " + course.getCode() + " must have a term start date" + " or class meeting end date"); return null; } if (termEndDate == null && courseMeeting.getEndTime() == null) { log.error( "Course " + course.getCode() + " must have a term end date" + " or class meeting end date"); return null; } // Treat meetings without a start or end time as invalid if (courseMeeting.getStartTime() == null || courseMeeting.getEndTime() == null) { log.error("Course " + course.getCode() + " must have start time and end time specified"); return null; } TimeZone tz = selectTimeZone(courseMeeting, termStartDate); java.util.Calendar firstMeetingStartTime = calculateFirstMeetingStartTime(courseMeeting, termStartDate); java.util.Calendar firstMeetingEndTime = calculateFirstMeetingEndTime(firstMeetingStartTime, courseMeeting); java.util.Calendar recurrenceEndDate = calculateRecurrenceEndDate(courseMeeting, termEndDate); if (firstMeetingStartTime.after(firstMeetingEndTime)) { log.error("Course " + course.getCode() + " start time is after end time"); return null; } if (firstMeetingStartTime.after(recurrenceEndDate)) { log.error("Course " + course.getCode() + " start date is after end date"); return null; } // Currently assuming you always have at least one day of week specified; e.g. no course // meeting with start/end date and time specified but not day of week DateTime eventStart = new DateTime(); eventStart.setTime(firstMeetingStartTime.getTimeInMillis()); eventStart.setTimeZone(tz); DateTime eventEnd = new DateTime(); eventEnd.setTime(firstMeetingEndTime.getTimeInMillis()); eventStart.setTimeZone(tz); DateTime recurUntil = new DateTime(); recurUntil.setTime(recurrenceEndDate.getTimeInMillis()); eventStart.setTimeZone(tz); // create a property list representing the event PropertyList props = new PropertyList(); props.add(uidGenerator.generateUid()); props.add(new DtStamp()); props.add(new DtStart(eventStart)); props.add(new DtEnd(eventEnd)); props.add(new Summary(StringUtils.isNotBlank(course.getCode()) ? course.getCode() : "course")); if (StringUtils.isNotBlank(course.getTitle())) { props.add(new Description(course.getTitle())); } if (StringUtils.isNotBlank(courseMeeting.getLocation().getDisplayName())) { props.add(new Location(courseMeeting.getLocation().getDisplayName())); } List<String> courseDays = courseMeeting.getDayIds(); if (courseDays != null && courseDays.size() > 0) { Recur recur = new Recur(Recur.WEEKLY, recurUntil); for (String dayOfWeek : courseDays) { WeekDay day = DAYS.valueOf(dayOfWeek).getIcalWeekDay(); if (day != null) { recur.getDayList().add(day); } else { log.warn("Invalid course day of week string " + dayOfWeek); } } RRule rrule = new RRule(recur); props.add(rrule); } VEvent event = new VEvent(props); return event; } private TimeZone selectTimeZone(CourseMeeting courseMeeting, java.util.Calendar termStartDate) { java.util.TimeZone jdkTz = null; if (courseMeeting.getStartDate() != null) { jdkTz = courseMeeting.getStartDate().getTimeZone(); } else if (termStartDate != null) { jdkTz = termStartDate.getTimeZone(); } else { throw new IllegalArgumentException( "Arguments courseMeeting and termStartDate cannot both be null"); } TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry(); TimeZone rslt = registry.getTimeZone(jdkTz.getID()); return rslt; } private java.util.Calendar combineDateAndTime( java.util.Calendar datePart, java.util.Calendar timePart) { // Start with the first argument java.util.Calendar rslt = (java.util.Calendar) datePart.clone(); // Update time fields from the second rslt.set(java.util.Calendar.HOUR_OF_DAY, timePart.get(java.util.Calendar.HOUR_OF_DAY)); rslt.set(java.util.Calendar.MINUTE, timePart.get(java.util.Calendar.MINUTE)); rslt.set(java.util.Calendar.SECOND, timePart.get(java.util.Calendar.SECOND)); rslt.set(java.util.Calendar.MILLISECOND, timePart.get(java.util.Calendar.MILLISECOND)); return rslt; } private java.util.Calendar calculateFirstMeetingStartTime( CourseMeeting courseMeeting, java.util.Calendar termStartDate) { log.trace("Examining courseMeeting=" + courseMeeting); java.util.Calendar cmStartDate = courseMeeting.getStartDate(); log.trace("cmStartDate=" + cmStartDate); java.util.Calendar candidateStartDate = cmStartDate != null && !cmStartDate.getTime().before(termStartDate.getTime()) ? (java.util.Calendar) cmStartDate.clone() // Whether we use the term or meeting date, we must be : (java.util.Calendar) termStartDate.clone(); // careful to clone it b/c Calendar objects are mutable! log.trace("candidateStartDate=" + candidateStartDate); // We have to calculate the first time this course meets... List<String> courseDays = courseMeeting.getDayIds(); log.trace("courseDays=" + courseDays); java.util.Calendar actualStartDate = null; while (true) { // Must reach the break statement int calendarDayOfWeek = candidateStartDate.get(java.util.Calendar.DAY_OF_WEEK); log.trace("Considering calendarDayOfWeek=" + calendarDayOfWeek); for (String dayCode : courseDays) { WeekDay weekDay = DAYS.valueOf(dayCode).getIcalWeekDay(); if (calendarDayOfWeek == WeekDay.getCalendarDay(weekDay)) { // This course meets on this day; proceed... actualStartDate = (java.util.Calendar) candidateStartDate.clone(); log.trace("actualStartDate=" + actualStartDate); } } if (actualStartDate != null) { break; } else { // Advance & try again... candidateStartDate.add(java.util.Calendar.DAY_OF_YEAR, 1); } } java.util.Calendar meetingStartTime = courseMeeting.getStartTime().toGregorianCalendar(); java.util.Calendar rslt = combineDateAndTime(actualStartDate, meetingStartTime); return rslt; } // For recurring events end date date is the event end time on the event start date. private java.util.Calendar calculateFirstMeetingEndTime( java.util.Calendar firstMeetingStartTime, CourseMeeting courseMeeting) { java.util.Calendar meetingEndTime = courseMeeting.getEndTime().toGregorianCalendar(); return combineDateAndTime(firstMeetingStartTime, meetingEndTime); } // End date is the end date specified in the meeting or the term end date. private java.util.Calendar calculateRecurrenceEndDate( CourseMeeting courseMeeting, java.util.Calendar termDate) { java.util.Calendar meetingDate = courseMeeting.getEndDate(); java.util.Calendar meetingTime = courseMeeting.getEndTime().toGregorianCalendar(); java.util.Calendar endDate = meetingDate != null ? meetingDate : termDate; return combineDateAndTime(endDate, meetingTime); } }