/**
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig 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:
*
* http://www.apache.org/licenses/LICENSE-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.jasig.schedassist.model;
import java.util.Date;
import java.util.SortedSet;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.ComponentList;
import net.fortuna.ical4j.model.Period;
import net.fortuna.ical4j.model.PeriodList;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.component.VEvent;
import org.apache.commons.lang.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.schedassist.NullAffiliationSourceImpl;
/**
* This class implements the mechanism of merging the {@link IScheduleOwner}'s {@link AvailableSchedule}
* and the {@link IScheduleOwner}'s {@link Calendar} for an {@link IScheduleVisitor}.
*
* @author Nicholas Blair, nblair@doit.wisc.edu
* @version $Id: VisibleScheduleBuilder.java 2530 2010-09-10 20:21:16Z npblair $
*/
public class VisibleScheduleBuilder implements IVisibleScheduleBuilder {
private static Log LOG = LogFactory.getLog(VisibleScheduleBuilder.class);
public static final String FREE = "free";
public static final String BUSY = "busy";
public static final String ATTENDING = "attending";
private IEventUtils eventUtils = new DefaultEventUtilsImpl(new NullAffiliationSourceImpl());
/**
* Default Constructor, will set the eventUtils field to {@link DefaultEventUtilsImpl}.
*/
public VisibleScheduleBuilder() {
}
/**
* @param eventUtils
*/
public VisibleScheduleBuilder(IEventUtils eventUtils) {
this.eventUtils = eventUtils;
}
/**
* @param eventUtils the eventUtils to set
*/
public void setEventUtils(IEventUtils eventUtils) {
this.eventUtils = eventUtils;
}
/*
* (non-Javadoc)
* @see org.jasig.schedassist.model.IVisibleScheduleBuilder#calculateVisibleSchedule(java.util.Date, java.util.Date, net.fortuna.ical4j.model.Calendar, org.jasig.schedassist.model.AvailableSchedule, org.jasig.schedassist.model.IScheduleOwner)
*/
@Override
public VisibleSchedule calculateVisibleSchedule(final Date startTime, final Date endTime,
final Calendar calendar, final AvailableSchedule schedule, final IScheduleOwner owner) {
return calculateVisibleScheduleNoAttendingCheck(startTime, endTime, calendar, schedule, owner.getPreferredMeetingDurations(), owner.getCalendarAccount());
}
/*
* (non-Javadoc)
* @see org.jasig.schedassist.model.IVisibleScheduleBuilder#calculateVisitorConflicts(java.util.Date, java.util.Date, net.fortuna.ical4j.model.Calendar, org.jasig.schedassist.model.AvailableSchedule, org.jasig.schedassist.model.MeetingDurations, org.jasig.schedassist.model.IScheduleVisitor)
*/
@Override
public VisibleSchedule calculateVisitorConflicts(Date startTime,
Date endTime, Calendar calendar, AvailableSchedule schedule,
MeetingDurations meetingDurations, IScheduleVisitor visitor) {
return calculateVisibleScheduleNoAttendingCheck(startTime, endTime, calendar, schedule, meetingDurations, visitor.getCalendarAccount());
}
/*
* (non-Javadoc)
* @see org.jasig.schedassist.model.IVisibleScheduleBuilder#calculateVisibleSchedule(java.util.Date, java.util.Date, net.fortuna.ical4j.model.Calendar, org.jasig.schedassist.model.AvailableSchedule, org.jasig.schedassist.model.IScheduleOwner, org.jasig.schedassist.model.IScheduleVisitor)
*/
@Override
public VisibleSchedule calculateVisibleSchedule(final Date startTime, final Date endTime,
final Calendar calendar, final AvailableSchedule schedule, final IScheduleOwner owner, final IScheduleVisitor visitor) {
Validate.notNull(startTime, "startTime cannot be null");
Validate.notNull(endTime, "endTime cannot be null");
Validate.notNull(calendar, "calendar cannot be null");
Validate.notNull(schedule, "available schedule cannot be null");
Validate.notNull(owner, "owner cannot be null");
ICalendarAccount visitorCalendarAccount = null;
if(visitor != null) {
visitorCalendarAccount = visitor.getCalendarAccount();
}
if(endTime.before(startTime)) {
throw new IllegalArgumentException("cannot pass end time (" + endTime +") that is before start time (" + startTime + ")");
}
LOG.debug("startTime: " + startTime + "; endTime: " + endTime);
final MeetingDurations durations = owner.getPreferredMeetingDurations();
// expand the passed in schedule's availableBlocks
SortedSet<AvailableBlock> availableBlocks = AvailableBlockBuilder.expand(schedule.getAvailableBlocks(), durations.getMinLength());
// create endpoints for the subset of availableBlocks
AvailableBlock availabilityStartBlock = AvailableBlockBuilder.createPreferredMinimumDurationBlock(startTime, durations);
AvailableBlock availabilityEndBlock = AvailableBlockBuilder.createPreferredMinimumDurationBlock(endTime, durations);
// trim the availableBlocks set to within startTime/endTime
availableBlocks = availableBlocks.subSet(availabilityStartBlock, availabilityEndBlock);
// construct our return value
VisibleSchedule visibleSchedule = new VisibleSchedule(durations);
// add the trimmed availableSchedule to the visibleSchedule as "FREE" blocks
visibleSchedule.addFreeBlocks(availableBlocks);
// now iterate through the schedule and construct blocks to overwrite in the visibleSchedul
ComponentList events = calendar.getComponents(Component.VEVENT);
for(Object component : events) {
VEvent event = (VEvent) component;
boolean causesConflict = this.eventUtils.willEventCauseConflict(owner.getCalendarAccount(), event);
if(!causesConflict) {
if(LOG.isDebugEnabled()) {
LOG.debug("event will not cause conflict, skipping: " + event);
}
continue;
}
// if we reach this point, this event is not skippable,
// it's going to be either BUSY, FREE with visitors, or ATTENDING
if(eventUtils.isEventRecurring(event)) {
// expand the recurrence rules
PeriodList recurrenceList = this.eventUtils.calculateRecurrence(event, startTime, endTime);
for(Object o : recurrenceList) {
Period period = (Period) o;
mutateAppropriateBlockInVisibleSchedule(visibleSchedule, event, owner.getCalendarAccount(), visitorCalendarAccount, period.getStart(), period.getEnd(), true);
}
} else {
// event is not recurring, just check block on start/end
Date startDate = event.getStartDate().getDate();
Date endDate = event.getEndDate(true).getDate();
mutateAppropriateBlockInVisibleSchedule(visibleSchedule, event, owner.getCalendarAccount(), visitorCalendarAccount, startDate, endDate, true);
}
}
return visibleSchedule;
}
/**
*
* @param startTime
* @param endTime
* @param calendar
* @param schedule
* @param meetingDurations
* @param calendarAccount
* @return an appropriate {@link VisibleSchedule}
*/
protected VisibleSchedule calculateVisibleScheduleNoAttendingCheck(Date startTime,
Date endTime, Calendar calendar, AvailableSchedule schedule, MeetingDurations meetingDurations, ICalendarAccount calendarAccount) {
Validate.notNull(startTime, "startTime cannot be null");
Validate.notNull(endTime, "endTime cannot be null");
Validate.notNull(calendar, "calendar cannot be null");
Validate.notNull(meetingDurations, "MeetingDurations argument cannot be null");
Validate.notNull(schedule, "AvailableSchedule argument cannot be null");
Validate.notNull(calendarAccount, "calendarAccount cannot be null");
if(endTime.before(startTime)) {
throw new IllegalArgumentException("cannot pass end time (" + endTime +") that is before start time (" + startTime + ")");
}
LOG.debug("startTime: " + startTime + "; endTime: " + endTime);
// expand the passed in schedule's availableBlocks
SortedSet<AvailableBlock> availableBlocks = AvailableBlockBuilder.expand(schedule.getAvailableBlocks(), meetingDurations.getMinLength());
// create endpoints for the subset of availableBlocks
AvailableBlock availabilityStartBlock = AvailableBlockBuilder.createPreferredMinimumDurationBlock(startTime, meetingDurations);
AvailableBlock availabilityEndBlock = AvailableBlockBuilder.createPreferredMinimumDurationBlock(endTime, meetingDurations);
// trim the availableBlocks set to within startTime/endTime
availableBlocks = availableBlocks.subSet(availabilityStartBlock, availabilityEndBlock);
// construct our return value
VisibleSchedule visibleSchedule = new VisibleSchedule(meetingDurations);
// add the trimmed availableSchedule to the visibleSchedule as "FREE" blocks
visibleSchedule.addFreeBlocks(availableBlocks);
// now iterate through the schedule and construct blocks to overwrite in the visibleSchedul
ComponentList events = calendar.getComponents(Component.VEVENT);
for(Object component : events) {
VEvent event = (VEvent) component;
boolean causesConflict = this.eventUtils.willEventCauseConflict(calendarAccount, event);
if(!causesConflict) {
if(LOG.isDebugEnabled()) {
LOG.debug("event will not cause conflict, skipping: " + event);
}
continue;
}
// if we reach this point, this event is not skippable,
// it's going to be either BUSY, FREE with visitors, or ATTENDING
// whether event is recurring or not, check block on start/end
Date startDate = event.getStartDate().getDate();
Date endDate = event.getEndDate(true).getDate();
mutateAppropriateBlockInVisibleSchedule(visibleSchedule, event, calendarAccount, null, startDate, endDate, false);
if(eventUtils.isEventRecurring(event)) {
// expand the recurrence rules
PeriodList recurrenceList = this.eventUtils.calculateRecurrence(event, startTime, endTime);
for(Object o : recurrenceList) {
Period period = (Period) o;
mutateAppropriateBlockInVisibleSchedule(visibleSchedule, event, calendarAccount, null, period.getStart(), period.getEnd(), false);
}
}
}
return visibleSchedule;
}
/**
* Mutative method to alter the {@link VisibleSchedule} in an appropriate fashion according to the {@link VEvent}'s properties.
*
* @param visibleSchedule
* @param event
* @param owner
* @param visitor
* @param eventInstanceStartDate
* @param eventInstanceEndDate
* @param performAttendingCheck
*/
void mutateAppropriateBlockInVisibleSchedule(VisibleSchedule visibleSchedule, VEvent event,
ICalendarAccount owner, ICalendarAccount visitor, Date eventInstanceStartDate, Date eventInstanceEndDate, boolean performAttendingCheck) {
int visitorLimit = safeVisitorLimit(event);
final AvailableBlock eventBlock = AvailableBlockBuilder.createBlock(eventInstanceStartDate, eventInstanceEndDate, visitorLimit);
// test to see if this appointment is an available appointment
Property availableEventMarker = event.getProperty(SchedulingAssistantAppointment.AVAILABLE_APPOINTMENT);
if(null == availableEventMarker || !SchedulingAssistantAppointment.TRUE.equals(availableEventMarker)) {
// non available appointments will ALWAYS simply be busy
visibleSchedule.setBusyBlock(eventBlock);
} else {
// the event is an available appointment
// first test if it's an ATTENDING match
if(performAttendingCheck && null != visitor && this.eventUtils.isAttendingAsOwner(event, owner) && this.eventUtils.isAttendingAsVisitor(event, visitor)) {
visibleSchedule.setAttendingBlock(eventBlock);
} else if (this.eventUtils.isAttendingAsOwner(event, owner)) {
// not an attending match, check visitorLimit exceeded
int availableVisitorCount = this.eventUtils.getScheduleVisitorCount(event);
if(availableVisitorCount >= visitorLimit) {
// busy
visibleSchedule.setBusyBlock(eventBlock);
} else {
// visitor count is less than limit - this is still free
// amend the block to represent current visitor count
eventBlock.setVisitorsAttending(availableVisitorCount);
visibleSchedule.overwriteFreeBlockOnlyIfPresent(eventBlock);
}
} else {
// the event is an available appointment, but does not match attending criteria and should
// be considered busy
visibleSchedule.setBusyBlock(eventBlock);
}
}
}
/**
* Safely return the value of the {@link VisitorLimit} of the event.
* If it's not set, this returns 1.
*
* @param event
* @return the value of the {@link VisitorLimit}, or 1 if not set.
*/
int safeVisitorLimit(VEvent event) {
Integer visitorLimit = eventUtils.getEventVisitorLimit(event);
if(visitorLimit == null) {
return 1;
}
return visitorLimit;
}
}