Java tutorial
/** * 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; } }