org.jasig.schedassist.model.VisibleSchedule.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.schedassist.model.VisibleSchedule.java

Source

/**
 * 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.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;

import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.ComponentList;
import net.fortuna.ical4j.model.component.VEvent;

import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.schedassist.ICalendarDataDao;

/**
 * Object representation of the merged result of an {@link IScheduleOwner}'s
 * {@link AvailableSchedule} and their Calendar data (from a {@link ICalendarDataDao}).
 *  
 * @author Nicholas Blair, nblair@doit.wisc.edu
 * @version $Id: VisibleSchedule.java 2530 2010-09-10 20:21:16Z npblair $
 */
public class VisibleSchedule implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = -8774450894322731603L;
    private Log LOG = LogFactory.getLog(this.getClass());
    private SortedMap<AvailableBlock, AvailableStatus> blockMap = new TreeMap<AvailableBlock, AvailableStatus>();
    private final MeetingDurations meetingDurations;

    /**
     * Default constructor.
     */
    public VisibleSchedule(final MeetingDurations meetingDurations) {
        this.meetingDurations = meetingDurations;
    }

    /**
     * If the internal map already contains the target block as a key,
     * the existing key is removed and the new block is stored as the key.
     * Stores the value for this key as {@link AvailableStatus#FREE}.
     * 
     * @param block
     */
    public void addFreeBlock(final AvailableBlock block) {
        SortedSet<AvailableBlock> expanded = AvailableBlockBuilder.expand(block, meetingDurations.getMinLength());
        for (AvailableBlock small : expanded) {
            if (blockMap.containsKey(small)) {
                // remove any existing keys
                blockMap.remove(small);
            }
            this.blockMap.put(small, AvailableStatus.FREE);
        }
    }

    /**
     * If the internal map already contains the target block as a key,
     * remove the existing and store the argument in its place.
     * Otherwise does not store this block.
     * @param block
     */
    public void overwriteFreeBlockOnlyIfPresent(final AvailableBlock block) {
        if (this.blockMap.containsKey(block)) {
            // this only works because AvailableBlock's hashCode/equals doesn't take visitorLimit into account
            this.blockMap.remove(block);
            this.blockMap.put(block, AvailableStatus.FREE);
        }
    }

    /**
     * Invokes {@link #addFreeBlock(AvailableBlock)} on each {@link AvailableBlock}
     * in the {@link Collection}.
     * 
     * @param blocks
     */
    public void addFreeBlocks(final Collection<AvailableBlock> blocks) {
        for (AvailableBlock block : blocks) {
            addFreeBlock(block);
        }
    }

    /**
     * ONLY stores the block in the map if conflicting FREE blocks
     * are already stored in the block.
     * 
     * @param block
     */
    public void setBusyBlock(final AvailableBlock block) {
        if (this.blockMap.containsKey(block)) {
            this.blockMap.put(block, AvailableStatus.BUSY);
        } else {
            LOG.debug("setBusyBlock on non-matching block: " + block);
            Set<AvailableBlock> conflicting = locateConflicting(block);
            for (AvailableBlock conflict : conflicting) {
                this.blockMap.put(conflict, AvailableStatus.BUSY);
            }
        }

    }

    /**
     * 
     * @param blocks
     */
    public void setBusyBlocks(final Collection<AvailableBlock> blocks) {
        for (AvailableBlock block : blocks) {
            setBusyBlock(block);
        }
    }

    /**
     * ONLY stores the block in the map if conflicting FREE blocks
     * are already stored in the block.
     * 
     * @param block
     */
    public void setAttendingBlock(final AvailableBlock block) {
        if (this.blockMap.containsKey(block)) {
            this.blockMap.put(block, AvailableStatus.ATTENDING);
        } else {
            Set<AvailableBlock> conflicting = locateConflicting(block);
            if (conflicting.size() > 0) {
                // remove the conflicts
                for (AvailableBlock conflict : conflicting) {
                    this.blockMap.remove(conflict);
                }
                // store only the original
                this.blockMap.put(block, AvailableStatus.ATTENDING);
            }
        }
    }

    /**
     * 
     * @param blocks
     */
    public void setAttendingBlocks(final Collection<AvailableBlock> blocks) {
        for (AvailableBlock block : blocks) {
            setAttendingBlock(block);
        }
    }

    /**
     * @return a defensive copy of the whole map
     */
    public SortedMap<AvailableBlock, AvailableStatus> getBlockMap() {
        return new TreeMap<AvailableBlock, AvailableStatus>(blockMap);
    }

    /**
     * 
     * @return the number of events in this VisibleSchedule
     */
    public int getSize() {
        return this.blockMap.size();
    }

    /**
     * 
     * @return the number of free blocks in this instance
     */
    public int getFreeCount() {
        return getCountForStatus(AvailableStatus.FREE);
    }

    /**
     * 
     * @return the number of busy blocks in this instance
     */
    public int getBusyCount() {
        return getCountForStatus(AvailableStatus.BUSY);
    }

    /**
     * 
     * @return the number of attending blocks in this instance
     */
    public int getAttendingCount() {
        return getCountForStatus(AvailableStatus.ATTENDING);
    }

    /**
     * 
     * @return a {@link List} of {@link AvailableBlock} in this {@link VisibleSchedule} that are {@link AvailableStatus#FREE}
     */
    public List<AvailableBlock> getFreeList() {
        return getBlockListForStatus(AvailableStatus.FREE);
    }

    /**
     * 
     * @return a {@link List} of {@link AvailableBlock} in this {@link VisibleSchedule} that are {@link AvailableStatus#BUSY}
     */
    public List<AvailableBlock> getBusyList() {
        return getBlockListForStatus(AvailableStatus.BUSY);
    }

    /**
     * 
     * @return a {@link List} of {@link AvailableBlock} in this {@link VisibleSchedule} that are {@link AvailableStatus#ATTENDING}
     */
    public List<AvailableBlock> getAttendingList() {
        return getBlockListForStatus(AvailableStatus.ATTENDING);
    }

    /**
     * Return the startTime of the first {@link AvailableBlock} 
     * stored within this object, or null if this object
     * is empty.
     * 
     * @return the first start time of the first block in this instance
     */
    public Date getScheduleStart() {
        if (this.blockMap.isEmpty()) {
            return null;
        } else {
            AvailableBlock firstKey = this.blockMap.firstKey();
            return firstKey == null ? null : firstKey.getStartTime();
        }
    }

    /**
     * Return the endTime of the last {@link AvailableBlock} stored
     * within this object, or null if this object
     * is empty.
     * 
     * @return the very last end time of the last block in this instance
     */
    public Date getScheduleEnd() {
        if (this.blockMap.isEmpty()) {
            return null;
        } else {
            AvailableBlock lastKey = this.blockMap.lastKey();
            return lastKey == null ? null : lastKey.getEndTime();
        }
    }

    /**
     * Convenience method to generate an ical4j {@link Calendar} from
     * the {@link AvailableBlock}s stored in this instance.
     * 
     * The {@link VEvent}s are very rudimentary, simply defining
     * the start date, end date, and have an event title that matches
     * the status (and number of attendees if visitorLimit is > 1).
     * 
     * @return a {@link Calendar} of free/busy/attending events from this instance
     */
    public Calendar getCalendar() {
        ComponentList components = new ComponentList();
        for (Entry<AvailableBlock, AvailableStatus> mapEntry : blockMap.entrySet()) {
            AvailableBlock block = mapEntry.getKey();
            AvailableStatus status = mapEntry.getValue();
            StringBuilder eventTitle = new StringBuilder();
            if (block.getVisitorLimit() > 1 && AvailableStatus.FREE.equals(status)) {
                eventTitle.append("(");
                eventTitle.append(block.getVisitorLimit() - block.getVisitorsAttending());
                eventTitle.append("/");
                eventTitle.append(block.getVisitorLimit());
                eventTitle.append(") ");
            }
            eventTitle.append(status.getValue());
            VEvent event = new VEvent(new net.fortuna.ical4j.model.DateTime(block.getStartTime()),
                    new net.fortuna.ical4j.model.DateTime(block.getEndTime()), eventTitle.toString());
            components.add(event);
        }
        Calendar result = new Calendar(components);
        return result;
    }

    /**
     * This method returns a subset of this {@link VisibleSchedule} including 
     * only blocks between start and end, inclusive.
     * 
     * @param start
     * @param end
     * @return a subset of this instance between the dates, inclusive
     */
    public VisibleSchedule subset(final Date start, final Date end) {
        VisibleSchedule result = new VisibleSchedule(this.meetingDurations);
        // add the free blocks only first
        for (Entry<AvailableBlock, AvailableStatus> e : this.blockMap.entrySet()) {
            AvailableBlock block = e.getKey();
            AvailableStatus status = e.getValue();
            if (CommonDateOperations.equalsOrAfter(block.getStartTime(), start)
                    && CommonDateOperations.equalsOrBefore(block.getEndTime(), end)) {
                // have to register the block as free first as BUSY/ATTENDING setters only overwrite free blocks
                result.addFreeBlock(block);
                switch (status) {
                case FREE:
                    // do nothing, already added as free
                    break;
                case BUSY:
                    result.setBusyBlock(block);
                    break;
                case ATTENDING:
                    result.setAttendingBlock(block);
                    break;
                case UNAVAILABLE:
                    throw new IllegalStateException("unexpected status (" + status + ") for block " + block);
                }
            }
        }

        return result;
    }

    /**
     * Returns the set of {@link AvailableBlock} objects within this instance
     * that conflict with the argument.
     * 
     * A conflict is defined as any overlap of 1 minute or more.
     * 
     * @param conflict
     * @return a set of conflicting blocks within this instance that conflict with the block argument
     */
    protected Set<AvailableBlock> locateConflicting(final AvailableBlock conflict) {
        Set<AvailableBlock> conflictingKeys = new HashSet<AvailableBlock>();

        Date conflictDayStart = DateUtils.truncate(conflict.getStartTime(), java.util.Calendar.DATE);
        Date conflictDayEnd = DateUtils.addDays(DateUtils.truncate(conflict.getEndTime(), java.util.Calendar.DATE),
                1);
        conflictDayEnd = DateUtils.addMinutes(conflictDayEnd, -1);

        AvailableBlock rangeStart = AvailableBlockBuilder.createPreferredMinimumDurationBlock(conflictDayStart,
                meetingDurations);
        LOG.debug("rangeStart: " + rangeStart);
        AvailableBlock rangeEnd = AvailableBlockBuilder.createBlockEndsAt(conflictDayEnd,
                meetingDurations.getMinLength());
        LOG.debug("rangeEnd: " + rangeStart);

        SortedMap<AvailableBlock, AvailableStatus> subMap = blockMap.subMap(rangeStart, rangeEnd);
        LOG.debug("subset of blockMap size: " + subMap.size());

        for (AvailableBlock mapKey : subMap.keySet()) {
            // all the AvailableBlock keys in the map have start/endtimes truncated to the minute
            // shift the key slightly forward (10 seconds) so that conflicts that start or end on the
            // same minute as a key does don't result in false positives
            Date minuteWithinBlock = DateUtils.addSeconds(mapKey.getStartTime(), 10);
            boolean shortCircuit = true;
            while (shortCircuit && CommonDateOperations.equalsOrBefore(minuteWithinBlock, mapKey.getEndTime())) {
                if (minuteWithinBlock.before(conflict.getEndTime())
                        && minuteWithinBlock.after(conflict.getStartTime())) {
                    conflictingKeys.add(mapKey);
                    shortCircuit = false;
                }
                minuteWithinBlock = DateUtils.addMinutes(minuteWithinBlock, 1);
            }
        }

        return conflictingKeys;
    }

    /**
     * Iterate through the blockMap and return a count of
     * {@link AvailableBlock}s that match the target {@link AvailableStatus}.
     * @param targetStatus
     * @return a count of the number of blocks in this instance that match the status argument
     */
    protected int getCountForStatus(final AvailableStatus targetStatus) {
        int count = 0;
        for (AvailableStatus status : blockMap.values()) {
            if (targetStatus.equals(status)) {
                count++;
            }
        }
        return count;
    }

    /**
     * Iterate through the blockMap and return a {@link List} of
     * {@link AvailableBlock}s that match the target {@link AvailableStatus}.
     * @param targetStatus
     * @return the list of blocks within this instance that match the status
     */
    protected List<AvailableBlock> getBlockListForStatus(final AvailableStatus targetStatus) {
        List<AvailableBlock> results = new ArrayList<AvailableBlock>();
        for (Entry<AvailableBlock, AvailableStatus> mapEntry : blockMap.entrySet()) {
            AvailableStatus status = mapEntry.getValue();
            if (targetStatus.equals(status)) {
                AvailableBlock block = mapEntry.getKey();
                results.add(block);
            }
        }
        return results;
    }

}