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

Java tutorial

Introduction

Here is the source code for org.jasig.schedassist.model.AvailableBlockBuilder.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.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

/**
 * Factory for {@link AvailableBlock} objects.
 * 
 * @author Nicholas Blair, nblair@doit.wisc.edu
 * @version $Id: AvailableBlockBuilder.java 2525 2010-09-10 19:13:01Z npblair $
 */
public final class AvailableBlockBuilder {

    protected static final String TIME_REGEX = "(\\d{1,2})\\:([0-5]\\d{1}) ([AP]M)";
    protected static final Pattern TIME_PATTERN = Pattern.compile(TIME_REGEX, Pattern.CASE_INSENSITIVE);
    protected static final int MINIMUM_MINUTES = 5;

    private static Log LOG = LogFactory.getLog(AvailableBlockBuilder.class);

    /**
     * Construct a list of {@link AvailableBlock} from the following criteria:
     * 
     * <ul>
     * <li>startTimePhrase and endTimePhrase should look like HH:MM AM/PM.</li>
     * <li>daysOfWeekPhrase looks like "MWF" and uses the following characters:
     * <ul>
     * <li>N is Sunday</li>
     * <li>M is Monday</li>
     * <li>T is Tuesday</li>
     * <li>W is Wednesday</li>
     * <li>R is Thursday</li>
     * <li>F is Friday</li>
     * <li>S is Saturday</li>
     * </ul></li>
     * <li>startDate must exist before endDate on the calendar. Any non-midnight time value on either
     * of these dates is rolled back to 00:00:00 (midnight).</li>
     * </ul>
     * 
     * @param startTimePhrase
     * @param endTimePhrase
     * @param daysOfWeekPhrase
     * @param startDate
     * @param endDate
     * @return the {@link List} of {@link AvailableBlock}s that fall within the specified date/time criteria.
     * @throws InputFormatException if the values for startTimePhrase or endTimePhrase do not match the expected format
     */
    public static SortedSet<AvailableBlock> createBlocks(final String startTimePhrase, final String endTimePhrase,
            final String daysOfWeekPhrase, final Date startDate, final Date endDate) throws InputFormatException {
        return createBlocks(startTimePhrase, endTimePhrase, daysOfWeekPhrase, startDate, endDate, 1);
    }

    /**
     * Construct a list of {@link AvailableBlock} from the following criteria:
     * 
     * <ul>
     * <li>startTimePhrase and endTimePhrase should look like HH:MM AM/PM.</li>
     * <li>daysOfWeekPhrase looks like "MWF" and uses the following characters:
     * <ul>
     * <li>N is Sunday</li>
     * <li>M is Monday</li>
     * <li>T is Tuesday</li>
     * <li>W is Wednesday</li>
     * <li>R is Thursday</li>
     * <li>F is Friday</li>
     * <li>S is Saturday</li>
     * </ul></li>
     * <li>startDate must exist before endDate on the calendar. Any non-midnight time value on either
     * of these dates is rolled back to 00:00:00 (midnight).</li>
     * </ul>
     * 
     * @param startTimePhrase
     * @param endTimePhrase
     * @param daysOfWeekPhrase
     * @param startDate
     * @param endDate
     * @param visitorLimit
     * @return the {@link List} of {@link AvailableBlock}s that fall within the specified date/time criteria.
     * @throws InputFormatException if the values for startTimePhrase or endTimePhrase do not match the expected format
     */
    public static SortedSet<AvailableBlock> createBlocks(final String startTimePhrase, final String endTimePhrase,
            final String daysOfWeekPhrase, final Date startDate, final Date endDate, final int visitorLimit)
            throws InputFormatException {
        return createBlocks(startTimePhrase, endTimePhrase, daysOfWeekPhrase, startDate, endDate, visitorLimit,
                null);
    }

    /**
     * 
     * @param startTimePhrase
     * @param endTimePhrase
     * @param daysOfWeekPhrase
     * @param startDate
     * @param endDate
     * @param visitorLimit
     * @param meetingLocation
     * @return
     * @throws InputFormatException
     */
    public static SortedSet<AvailableBlock> createBlocks(final String startTimePhrase, final String endTimePhrase,
            final String daysOfWeekPhrase, final Date startDate, final Date endDate, final int visitorLimit,
            final String meetingLocation) throws InputFormatException {
        SortedSet<AvailableBlock> blocks = new TreeSet<AvailableBlock>();
        // set time of startDate to 00:00:00
        Date realStartDate = DateUtils.truncate(startDate, Calendar.DATE);
        // set time of endDate to 23:59:59
        Date dayAfterEndDate = DateUtils.truncate(DateUtils.addDays(endDate, 1), Calendar.DATE);
        Date realEndDate = DateUtils.addSeconds(dayAfterEndDate, -1);

        if (LOG.isDebugEnabled()) {
            LOG.debug("createBlocks calculated realStartDate: " + realStartDate + ", realEndDate: " + realEndDate);
        }

        List<Date> matchingDays = matchingDays(daysOfWeekPhrase, realStartDate, realEndDate);
        for (Date matchingDate : matchingDays) {
            Calendar startCalendar = Calendar.getInstance();
            startCalendar.setTime(matchingDate);
            Calendar endCalendar = (Calendar) startCalendar.clone();

            interpretAndUpdateTime(startTimePhrase, startCalendar);
            interpretAndUpdateTime(endTimePhrase, endCalendar);

            Date blockStartTime = startCalendar.getTime();
            Date blockEndTime = endCalendar.getTime();
            if (!blockEndTime.after(blockStartTime)) {
                throw new InputFormatException("Start time must occur before end time");
            }
            if (CommonDateOperations.equalsOrAfter(blockStartTime, realStartDate)
                    && CommonDateOperations.equalsOrBefore(blockEndTime, realEndDate)) {
                AvailableBlock block = new AvailableBlock(blockStartTime, blockEndTime, visitorLimit,
                        meetingLocation);
                blocks.add(block);
            }

        }
        return blocks;
    }

    /**
     * Create a single {@link AvailableBlock} with a visitorLimit of 1.
     * 
     * @param startDate
     * @param endDate
     * @return the new block
     */
    public static AvailableBlock createBlock(final Date startDate, final Date endDate) {
        return createBlock(startDate, endDate, 1);
    }

    /**
     * Create a single {@link AvailableBlock}.
     * 
     * @param startDate
     * @param endDate
     * @param visitorLimit
     * @return the new block
     */
    public static AvailableBlock createBlock(final Date startDate, final Date endDate, final int visitorLimit) {
        return createBlock(startDate, endDate, visitorLimit, null);
    }

    /**
     * Create a single {@link AvailableBlock}.
     * 
     * @param startDate
     * @param endDate
     * @param visitorLimit
     * @param meetingLocation
     * @return the new block
     */
    public static AvailableBlock createBlock(final Date startDate, final Date endDate, final int visitorLimit,
            final String meetingLocation) {
        return new AvailableBlock(startDate, endDate, visitorLimit, meetingLocation);
    }

    /**
     * Create a single {@link AvailableBlock} with a visitorLimit of 1 and using this application's common time format ("yyyyMMdd-HHmm")
     * for the start and end datetimes.
     * 
     * @see CommonDateOperations#parseDateTimePhrase(String)
     * @param startTimePhrase
     * @param endTimePhrase
     * @return the new block
     * @throws InputFormatException
     */
    public static AvailableBlock createBlock(final String startTimePhrase, final String endTimePhrase)
            throws InputFormatException {
        return createBlock(startTimePhrase, endTimePhrase, 1);
    }

    /**
     * Create a single {@link AvailableBlock} using this applications common time format ("yyyyMMdd-HHmm").
     * 
     * @see CommonDateOperations#parseDateTimePhrase(String)
     * @param startTimePhrase
     * @param endTimePhrase
     * @param visitorLimit
     * @return the new block
     * @throws InputFormatException
     */
    public static AvailableBlock createBlock(final String startTimePhrase, final String endTimePhrase,
            final int visitorLimit) throws InputFormatException {
        return createBlock(startTimePhrase, endTimePhrase, visitorLimit, null);
    }

    /**
     * Create a single {@link AvailableBlock} using this applications common time format ("yyyyMMdd-HHmm").
     * 
     * @see CommonDateOperations#parseDateTimePhrase(String)
     * @param startTimePhrase
     * @param endTimePhrase
     * @param visitorLimit
     * @return the new block
     * @throws InputFormatException
     */
    public static AvailableBlock createBlock(final String startTimePhrase, final String endTimePhrase,
            final int visitorLimit, final String meetingLocation) throws InputFormatException {
        Date startTime = CommonDateOperations.parseDateTimePhrase(startTimePhrase);
        Date endTime = CommonDateOperations.parseDateTimePhrase(endTimePhrase);
        return createBlock(startTime, endTime, visitorLimit, meetingLocation);
    }

    /**
     * Create a single {@link AvailableBlock} that starts at the startTime Phrase (uses 
     * {@link CommonDateOperations#parseDateTimePhrase(String)} format) and ends duration minutes later.
     * 
     * @param startTimePhrase
     * @param duration
     * @return the new block
     * @throws InputFormatException
     */
    public static AvailableBlock createBlock(final String startTimePhrase, final int duration)
            throws InputFormatException {
        Date startTime = CommonDateOperations.parseDateTimePhrase(startTimePhrase);
        Date endTime = DateUtils.addMinutes(startTime, duration);
        return createBlock(startTime, endTime);
    }

    /**
     * Create a single {@link AvailableBlock} that ENDS at the endDate argument and starts duration minutes prior.
     * 
     * @param endDate end time
     * @param duration how many minutes prior for the start date
     * @return the new block
     */
    public static AvailableBlock createBlockEndsAt(final Date endDate, final int duration) {
        Date startDate = DateUtils.addMinutes(endDate, -duration);
        return createBlock(startDate, endDate);
    }

    /**
     * Create an {@link AvailableBlock} from the specified startTime to an endTime interpreted from {@link MeetingDurations#getMinLength()}.
     * visitorLimit for the returned {@link AvailableBlock} will be set to 1.
     * 
     * @param startTime
     * @param preferredMeetingDurations
     * @return the new block
     */
    public static AvailableBlock createPreferredMinimumDurationBlock(final Date startTime,
            final MeetingDurations preferredMeetingDurations) {
        return createPreferredMinimumDurationBlock(startTime, preferredMeetingDurations, 1);
    }

    /**
     * Create an {@link AvailableBlock} from the specified startTime to an endTime interpreted from {@link MeetingDurations#getMinLength()}.
     * 
     * @param startTime
     * @param preferredMeetingDurations
     * @param visitorLimit
     * @return the new block
     */
    public static AvailableBlock createPreferredMinimumDurationBlock(final Date startTime,
            final MeetingDurations preferredMeetingDurations, final int visitorLimit) {
        Date endTime = DateUtils.addMinutes(startTime, preferredMeetingDurations.getMinLength());
        return createBlock(startTime, endTime, visitorLimit);
    }

    /**
     * Creates a minimum size {@link AvailableBlock} by adding MINIMUM_MINUTES minutes to startTime as the endTime
     * and visitorLimit of 1.
     * 
     * @param startTimePhrase
     * @return the new block
     * @throws InputFormatException
     */
    public static AvailableBlock createSmallestAllowedBlock(final String startTimePhrase)
            throws InputFormatException {
        return createSmallestAllowedBlock(startTimePhrase, 1);
    }

    /**
     * Creates a minimum size {@link AvailableBlock} by adding MINIMUM_MINUTES minutes to startTime as the endTime.
     * 
     * @see #createSmallestAllowedBlock(Date, int)
     * @param startTimePhrase
     * @param visitorLimit
     * @return the new block
     * @throws InputFormatException
     */
    public static AvailableBlock createSmallestAllowedBlock(final String startTimePhrase, final int visitorLimit)
            throws InputFormatException {
        Date startTime = CommonDateOperations.parseDateTimePhrase(startTimePhrase);
        return createSmallestAllowedBlock(startTime, visitorLimit);
    }

    /**
     * Creates a minimum size {@link AvailableBlock} by adding MINIMUM_MINUTES minutes to startTime as the endTime
     * and visitorLimit of 1.
     * 
     * @see #createSmallestAllowedBlock(Date, int)
     * @param startTime
     * @return the new block
     */
    public static AvailableBlock createSmallestAllowedBlock(final Date startTime) {
        return createSmallestAllowedBlock(startTime, 1);
    }

    /**
     * Creates a minimum size {@link AvailableBlock} by adding MINIMUM_MINUTES minutes to startTime as the endTime.
     * 
     * @param startTime
     * @param visitorLimit
     * @return the new block
     */
    public static AvailableBlock createSmallestAllowedBlock(final Date startTime, final int visitorLimit) {
        Date endTime = DateUtils.addMinutes(startTime, MINIMUM_MINUTES);
        return createBlock(startTime, endTime, visitorLimit);
    }

    /**
     * Creates a minimum size {@link AvailableBlock} using the argument endTime as the end and MINIMUM_MINUTES minutes
     * prior to endTime as the start.
     * 
     * @param endTime
     * @return the new block
     */
    public static AvailableBlock createMinimumEndBlock(final Date endTime) {
        Date startTime = DateUtils.addMinutes(endTime, -MINIMUM_MINUTES);
        return createBlock(startTime, endTime);
    }

    /**
     * Expand one {@link AvailableBlock} into a {@link SortedSet} of {@link AvailableBlock}s with a duration equal
     * to the meetingLengthMinutes argument in minutes.
     * 
     * @param largeBlock
     * @return the new set
     */
    public static SortedSet<AvailableBlock> expand(final AvailableBlock largeBlock,
            final int meetingLengthMinutes) {
        SortedSet<AvailableBlock> smallBlocks = new TreeSet<AvailableBlock>();

        long meetingLengthInMsec = convertMinutesToMsec(meetingLengthMinutes);
        Date currentStart = largeBlock.getStartTime();
        while (largeBlock.getEndTime().getTime() - currentStart.getTime() >= meetingLengthInMsec) {
            Date newEndTime = new Date(currentStart.getTime() + meetingLengthInMsec);
            AvailableBlock smallBlock = createBlock(currentStart, newEndTime, largeBlock.getVisitorLimit(),
                    largeBlock.getMeetingLocation());
            smallBlock.setVisitorsAttending(largeBlock.getVisitorsAttending());
            smallBlocks.add(smallBlock);
            currentStart = newEndTime;
        }
        return smallBlocks;
    }

    /**
     * Expand a {@link Set} of {@link AvailableBlock} into a {@link SortedSet} of {@link AvailableBlock}s with a duration equal
     * to the meetingLengthMinutes argument in minutes.
     * 
     * @param largeBlocks
     * @return the new set
     */
    public static SortedSet<AvailableBlock> expand(Set<AvailableBlock> largeBlocks,
            final int meetingLengthMinutes) {
        SortedSet<AvailableBlock> smallBlocks = new TreeSet<AvailableBlock>();
        for (AvailableBlock sourceBlock : largeBlocks) {
            smallBlocks.addAll(expand(sourceBlock, meetingLengthMinutes));
        }
        return smallBlocks;
    }

    /**
     * Combine adjacent {@link AvailableBlock}s in the argument {@link Set}.
     * 
     * @param smallBlocks
     * @return the new set
     */
    public static SortedSet<AvailableBlock> combine(SortedSet<AvailableBlock> smallBlocks) {
        SortedSet<AvailableBlock> largeBlocks = new TreeSet<AvailableBlock>();

        Iterator<AvailableBlock> smallBlockIterator = smallBlocks.iterator();

        if (smallBlockIterator.hasNext()) {
            AvailableBlock current = smallBlockIterator.next();
            while (smallBlockIterator.hasNext()) {
                AvailableBlock next = smallBlockIterator.next();
                if (combinable(current, next)) {
                    // current and next are adjacent AND have the same visitorLimit AND have same meetinglocation
                    // update current to have current.startTime and next.endTime
                    try {
                        current = new AvailableBlock(current.getStartTime(), next.getEndTime(),
                                current.getVisitorLimit(), current.getMeetingLocation());
                    } catch (IllegalArgumentException e) {
                        // could not create a block, what to do?
                        LOG.error("failed to create an AvailableBlock from " + current.getStartTime() + " and "
                                + next.getEndTime(), e);
                    }
                } else {
                    // current and next are either not adjacent or have a different visitorLimit
                    // current should be pushed into largeBlocks
                    largeBlocks.add(current);
                    // current should now point to next
                    current = next;
                }
            }
            // guarantee that current gets pushed into largeBlocks
            largeBlocks.add(current);
        }

        return largeBlocks;
    }

    /**
     * 2 blocks are combinable if and onlyl if:
     * <ol>
     * <li>the end time of the left equals the start time of the right</li>
     * <li>the visitor limits are equivalent</li>
     * <li>the meeting locations are equivalent</li>
     * </ol>
     * 
     * @see #safeMeetingLocationEquals(AvailableBlock, AvailableBlock)
     * @param left
     * @param right
     * @return true if the 2 blocks can be combined
     */
    static boolean combinable(AvailableBlock left, AvailableBlock right) {
        if (left == null || right == null) {
            return false;
        }
        return left.getEndTime().equals(right.getStartTime()) && left.getVisitorLimit() == right.getVisitorLimit()
                && safeMeetingLocationEquals(left, right);
    }

    /**
     * Null safe equality test for {@link AvailableBlock#getMeetingLocation()
     * @param left
     * @param right
     * @return true if the meetingLocation fields are equivalent
     */
    static boolean safeMeetingLocationEquals(AvailableBlock left, AvailableBlock right) {
        final String leftLocation = left.getMeetingLocation();
        final String rightLocation = right.getMeetingLocation();
        if (leftLocation == null && rightLocation == null) {
            return true;
        }
        if (leftLocation != null) {
            return leftLocation.equals(rightLocation);
        }
        if (rightLocation != null) {
            return rightLocation.equals(leftLocation);
        }
        // not reachable?
        return false;
    }

    /**
     * Returns a {@link List} of {@link Date} objects that fall between startDate and endDate and
     * exist on the days specified by daysOfWeekPhrase.
     * 
     * For instance, passing "MWF", a start Date of June 30 2008, and an end Date of July 04 2008, this 
     * method will return a list of 3 Date objects (one for Monday June 30, one for Wednesday July 2, and
     * one for Friday July 4).
     * 
     * The time values for returned {@link Date}s will always be 00:00:00 (in the JVM's default timezone).
     * 
     * @param daysOfWeekPhrase
     * @param startDate
     * @param endDate
     * @return a {@link List} of {@link Date} objects that fall between startDate and endDate and
     * exist on the days specified by daysOfWeekPhrase.
     */
    protected static List<Date> matchingDays(final String daysOfWeekPhrase, final Date startDate,
            final Date endDate) {
        List<Date> matchingDays = new ArrayList<Date>();

        Set<Integer> daysOfWeek = new HashSet<Integer>();
        for (char character : daysOfWeekPhrase.toUpperCase().toCharArray()) {
            switch (character) {
            case 'N':
                daysOfWeek.add(Calendar.SUNDAY);
                break;
            case 'M':
                daysOfWeek.add(Calendar.MONDAY);
                break;
            case 'T':
                daysOfWeek.add(Calendar.TUESDAY);
                break;
            case 'W':
                daysOfWeek.add(Calendar.WEDNESDAY);
                break;
            case 'R':
                daysOfWeek.add(Calendar.THURSDAY);
                break;
            case 'F':
                daysOfWeek.add(Calendar.FRIDAY);
                break;
            case 'S':
                daysOfWeek.add(Calendar.SATURDAY);
                break;
            }
        }

        Calendar current = Calendar.getInstance();
        current.setTime(startDate);
        // set the time to 00:00:00 to insure the time doesn't affect our comparison)
        // (because there may be a valid time window on endDate)
        current = CommonDateOperations.zeroOutTimeFields(current);

        while (current.getTime().compareTo(endDate) < 0) {
            if (daysOfWeek.contains(current.get(Calendar.DAY_OF_WEEK))) {
                matchingDays.add(current.getTime());
            }
            // increment currentDate +1 day 
            current.add(Calendar.DATE, 1);
        }
        return matchingDays;
    }

    /**
     * Parses the timePhrase (e.g. 9:30 AM) and updates the HOUR_OF_DAY and MINUTE fields on the toModify Calendar.
     * 
     * Mutates the toModify Calendar argument.
     * 
     * @param timePhrase
     * @param toModify
     * @throws InputFormatException
     */
    protected static void interpretAndUpdateTime(final String timePhrase, final Calendar toModify)
            throws InputFormatException {
        Matcher matcher = TIME_PATTERN.matcher(timePhrase);
        if (matcher.matches()) {
            int endHour = Integer.parseInt(matcher.group(1));
            int endMinutes = Integer.parseInt(matcher.group(2));
            if (endHour == 12 && matcher.group(3).equalsIgnoreCase("am")) {
                endHour = 0;
            }
            if (matcher.group(3).equalsIgnoreCase("pm") && endHour != 12) {
                endHour += 12;
            }
            toModify.set(Calendar.HOUR_OF_DAY, endHour);
            toModify.set(Calendar.MINUTE, endMinutes);
        } else {
            throw new InputFormatException(timePhrase + " does not match expected format of HH:MM AM/PM");
        }
    }

    /**
     * Convert integer minutes to milliseconds (as long).
     * 
     * @param minutes
     * @return the number of milliseconds in the minutes argument
     */
    protected static long convertMinutesToMsec(final int minutes) {
        final long msecPerSecond = 1000;
        final long secondsPerMinute = 60;
        long result = ((long) minutes) * secondsPerMinute * msecPerSecond;
        return result;
    }
}