All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.unitedinternet.cosmo.calendar.RecurrenceExpander Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2005-2007 Open Source Applications Foundation
 * 
 * Licensed 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.unitedinternet.cosmo.calendar;

import java.time.temporal.TemporalAmount;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateList;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.Parameter;
import net.fortuna.ical4j.model.Period;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.PropertyList;
import net.fortuna.ical4j.model.Recur;
import net.fortuna.ical4j.model.TemporalAmountAdapter;
import net.fortuna.ical4j.model.TimeZone;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.parameter.Value;
import net.fortuna.ical4j.model.property.DtEnd;
import net.fortuna.ical4j.model.property.DtStart;
import net.fortuna.ical4j.model.property.Duration;
import net.fortuna.ical4j.model.property.RDate;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.util.Dates;

/**
 * Utility class that contains apis that that involve
 * expanding recurring components.
 */
public class RecurrenceExpander {
    private static final Logger LOG = LoggerFactory.getLogger(RecurrenceExpander.class);
    private static Date maxExpandDate = null;
   
    static {
        // Expand out to 2030 for those recurrence rules
        // that have an end.  Recurring events with no end
        // will be indexed as infinite.
        java.util.Calendar c = java.util.Calendar.getInstance();
        
        c.set(java.util.Calendar.YEAR, 2030);
        c.set(java.util.Calendar.MONTH, 0);
        c.set(java.util.Calendar.DAY_OF_MONTH, 1);
        
        maxExpandDate = new Date(c.getTime());
    }
    
    /**
     * Constructor.
     */
    public RecurrenceExpander() {
        super();
    }
    
    /**
     * Return start and end Date that represent the start of the first 
     * occurrence of a recurring component and the end of the last
     * occurence.  If the recurring component has no end(infinite recurring event),
     * then no end date will be returned.
     * @param calendar Calendar containing master and modification components
     * @return array containing start (located at index 0) and end (index 1) of
     *         recurring component.
     */
    public Date[] calculateRecurrenceRange(Calendar calendar) {
        try{
            List vevents = calendar.getComponents().getComponents(Component.VEVENT);
            
            List exceptions = new ArrayList();
            Component masterComp = null;
            
            // get list of exceptions (VEVENT with RECURRENCEID)
            for (Iterator i = vevents.iterator(); i.hasNext();) {
                VEvent event = i.next();
                if (event.getRecurrenceId() != null) {
                    exceptions.add(event);
                }
                else {
                    masterComp = event;
                }
                
            }
            
            return calculateRecurrenceRange(masterComp, exceptions);
        } catch (Exception e){
            LOG.error("ERROR in calendar: " + calendar, e);
            throw e;
        }
    }

    /**
     * Return a start and end Date that represents the start of the first
     * occurence of a recurring component and the end of the last occurence.  If
     * the recurring component has no end(infinite recurring event),
     * then no end date will be returned.
     * 
     * @param comp Component to analyze
     * @return array containing start (located at index 0) and end (index 1) of
     *         recurring component.
     */
    public Date[] calculateRecurrenceRange(Component comp) {
        return calculateRecurrenceRange(comp, new ArrayList(0));
        
    }    
    /**
     * Return a start and end Date that represents the start of the first
     * occurence of a recurring component and the end of the last occurence.  If
     * the recurring component has no end(infinite recurring event),
     * then no end date will be returned.
     * 
     * @param comp Component to analyze
     * @param modifications modifications to component
     * @return array containing start (located at index 0) and end (index 1) of
     *         recurring component.
     */
    public Date[] calculateRecurrenceRange(Component comp, List modifications) {

        Date[] dateRange = new Date[2];
        Date start = getStartDate(comp);
        
        // must have start date
        if (start == null) {
            return new Date[]{};
        }
        
        TemporalAmount duration = null;
        Date end = getEndDate(comp);
        if (end == null) {
            if (start instanceof DateTime) {
                // Its an timed event with no duration
                duration = java.time.Duration.ZERO;
            } else {
                // Its an all day event so duration is one day
                duration = java.time.Duration.ofDays(1);
            }
            end = org.unitedinternet.cosmo.calendar.util.Dates.getInstance(new TemporalAmountAdapter(duration).getTime(start), start);
        } else {
            if(end instanceof DateTime) {
                // Handle case where dtend is before dtstart, in which the duration
                // will be 0, since it is a timed event
                if(end.before(start)) {
                    end = org.unitedinternet.cosmo.calendar.util.Dates.getInstance(
                            new TemporalAmountAdapter(java.time.Duration.ZERO).getTime(start), start);
                }
            } else {
                // Handle case where dtend is before dtstart, in which the duration
                // will be 1 day since its an all-day event
                if(end.before(start)) {
                    end = org.unitedinternet.cosmo.calendar.util.Dates.getInstance(
                            new TemporalAmountAdapter(java.time.Duration.ofDays(1)).getTime(start), start);
                }
            }
            duration = java.time.Duration.between(start.toInstant(), end.toInstant());
        }
        
        // Always add master's occurence
        dateRange[0] = start;
        dateRange[1] = end;
        
        // Now tweak range based on RDATE, RRULE, and component modifications
        // For now, ignore EXDATE and EXRULE because RDATE and RRULE will 
        // give us the broader range.
        
        // recurrence dates..
        PropertyList rDates = comp.getProperties().getProperties(Property.RDATE);
        for (RDate rdate : rDates) {
            // Both PERIOD and DATE/DATE-TIME values allowed
            if (Value.PERIOD.equals(rdate.getParameters().getParameter(
                    Parameter.VALUE))) {
                for (Period period : rdate.getPeriods()) {
                    if (period.getStart().before(dateRange[0])) {
                        dateRange[0] = period.getStart();
                    }
                    if (period.getEnd().after(dateRange[1])) {
                        dateRange[1] = period.getEnd();
                    }
                    
                }
            } else {
                for (Date startDate : rdate.getDates()) {
                    Date endDate = org.unitedinternet.cosmo.calendar.util.Dates.getInstance(new TemporalAmountAdapter(duration)
                            .getTime(startDate), startDate);
                    if (startDate.before(dateRange[0])) {
                        dateRange[0] = startDate;
                    }
                    if (endDate.after(dateRange[1])) {
                        dateRange[1] = endDate;
                    }
                }
            }
        }

        // recurrence rules..
        PropertyList rRules = comp.getProperties().getProperties(Property.RRULE);
        for (RRule rrule : rRules) {
            Recur recur = rrule.getRecur();
            
            // If this is an infinite recurring event, we are done processing
            // the rules
            if(recur.getCount()==-1 && recur.getUntil()==null) {
                dateRange[1] = null;
                break;
            }
            
            // DateList startDates = rrule.getRecur().getDates(start.getDate(),
            // adjustedRangeStart, rangeEnd, (Value)
            // start.getParameters().getParameter(Parameter.VALUE));
            DateList startDates = rrule.getRecur().getDates(start, start,
                    maxExpandDate,
                    start instanceof DateTime ? Value.DATE_TIME : Value.DATE);
            
            // Dates are sorted, so get the last occurence, and calculate the end
            // date and update dateRange if necessary
            if(startDates.size()>0) {
                Date lastStart = (Date) startDates.get(startDates.size()-1);
                Date endDate = org.unitedinternet.cosmo.calendar.util.Dates.getInstance(new TemporalAmountAdapter(duration).getTime(lastStart), start);
                
                if (endDate.after(dateRange[1])) {
                    dateRange[1] = endDate;
                }
            }
        }
        
        // event modifications....
        for(Component modComp : modifications) {
            Date startMod = getStartDate(modComp);
            Date endMod = getEndDate(modComp);
            if (startMod.before(dateRange[0])) {
                dateRange[0] = startMod;
            }
            if (dateRange[1] != null && endMod != null && endMod.after(dateRange[1])) {
                dateRange[1] = endMod;
            }
            
            // TODO: handle THISANDFUTURE/THISANDPRIOR edge cases
        }
        
        // make sure timezones are consistent with original timezone
        if(start instanceof DateTime) {
            
            boolean utc = ((DateTime) start).isUtc();
            TimeZone tz = ((DateTime) start).getTimeZone();
            
            if((dateRange[0] instanceof DateTime)){
                if(tz != null ){
                    ((DateTime) dateRange[0]).setTimeZone(tz);
                }else{
                    ((DateTime) dateRange[0]).setUtc(utc);
                }
            }
            if((dateRange[1] instanceof DateTime) &&
                dateRange[1]!=null && (dateRange[1] instanceof DateTime)) {
                    if(tz != null){
                        ((DateTime) dateRange[1]).setTimeZone(tz);
                    }else{
                        ((DateTime) dateRange[1]).setUtc(utc);
                    } 
            }
        }
        
        return dateRange;
    }
    
    /**
     * Expand recurring event for given time-range.
     * @param calendar calendar containing recurring event and modifications
     * @param rangeStart expand start
     * @param rangeEnd expand end
     * @param timezone Optional timezone to use for floating dates.  If null, the
     *        system default is used.
     * @return InstanceList containing all occurences of recurring event during
     *         time range
     */
    public InstanceList getOcurrences(Calendar calendar, Date rangeStart, Date rangeEnd, TimeZone timezone) {
        List vevents = calendar.getComponents().getComponents(Component.VEVENT);
        
        List exceptions = new ArrayList();
        Component masterComp = null;
        
        // get list of exceptions (VEVENT with RECURRENCEID)
        for (Iterator i = vevents.iterator(); i.hasNext();) {
            VEvent event = i.next();
            if (event.getRecurrenceId() != null) {
                exceptions.add(event);
            }
            else {
                masterComp = event; 
            }
            
        }
        
        return getOcurrences(masterComp, exceptions, rangeStart, rangeEnd, timezone);
    }
    
    /**
     * Expand recurring compnent for given time-range.
     * @param component recurring component to expand
     * @param rangeStart expand start date
     * @param rangeEnd expand end date
     * @param timezone Optional timezone to use for floating dates.  If null, the
     *        system default is used.
     * @return InstanceList containing all occurences of recurring event during
     *         time range
     */
    public InstanceList getOcurrences(Component component, Date rangeStart, Date rangeEnd, TimeZone timezone) {
        return getOcurrences(component, new ArrayList(0), rangeStart, rangeEnd, timezone);
    }
    
    /**
     * Expand recurring compnent for given time-range.
     * @param component recurring component to expand
     * @param modifications modifications to recurring component
     * @param rangeStart expand start date
     * @param rangeEnd expand end date
     * @param timezone Optional timezone to use for floating dates.  If null, the
     *        system default is used.
     * @return InstanceList containing all occurences of recurring event during
     *         time range 
     */
    public InstanceList getOcurrences(Component component, List modifications, Date rangeStart, 
                                        Date rangeEnd, TimeZone timezone) {
        InstanceList instances = new InstanceList();
        instances.setTimezone(timezone);
        instances.addMaster(component, rangeStart, rangeEnd);
        for(Component mod: modifications) {
            instances.addOverride(mod, rangeStart, rangeEnd);
        }
        
        return instances;
    }
    
    
    /**
     * Determine if date is a valid occurence in recurring calendar component
     * @param calendar recurring calendar component
     * @param occurrence occurrence date
     * @return true if the occurrence date is a valid occurrence, otherwise false
     */
    public boolean isOccurrence(Calendar calendar, Date occurrence) {
        java.util.Calendar cal = Dates.getCalendarInstance(occurrence);
        cal.setTime(occurrence);
       
        // Add a second or day (one unit forward) so we can set a range for
        // finding instances.  This is required because ical4j's Recur apis
        // only calculate recurring dates up until but not including the 
        // end date of the range.
        if(occurrence instanceof DateTime) {
            cal.add(java.util.Calendar.SECOND, 1);
        }
        else {
            cal.add(java.util.Calendar.DAY_OF_WEEK, 1);
        }
        
        Date rangeEnd = 
            org.unitedinternet.cosmo.calendar.util.Dates.getInstance(cal.getTime(), occurrence);
        
        TimeZone tz = null;
        
        for(Object obj : calendar.getComponents(Component.VEVENT)){
        	VEvent evt = (VEvent)obj;
        	if(evt.getRecurrenceId() == null && evt.getStartDate() != null){
        		tz = evt.getStartDate().getTimeZone();
        	}
        }
        InstanceList instances = getOcurrences(calendar, occurrence, rangeEnd, tz);
        
        for(Iterator it = instances.values().iterator(); it.hasNext();) {
            Instance instance = it.next();
            if(instance.getRid().getTime()==occurrence.getTime()) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Gets start date.
     * @param comp The component.
     * @return The date.
     */
    private Date getStartDate(Component comp) {
        DtStart prop = (DtStart) comp.getProperties().getProperty(
                Property.DTSTART);
        return (prop != null) ? prop.getDate() : null;
    }

    /**
     * Gets end date.
     * @param comp The component.
     * @return The date.
     */
    private Date getEndDate(Component comp) {
        DtEnd dtEnd = (DtEnd) comp.getProperties().getProperty(Property.DTEND);
        // No DTEND? No problem, we'll use the DURATION if present.
        if (dtEnd == null) {
            Date dtStart = getStartDate(comp);
            Duration duration = (Duration) comp.getProperties().getProperty(
                    Property.DURATION);
            if (duration != null) {
                dtEnd = new DtEnd(org.unitedinternet.cosmo.calendar.util.Dates.getInstance(new TemporalAmountAdapter(duration.getDuration())
                        .getTime(dtStart), dtStart));
            }
        }
        return (dtEnd != null) ? dtEnd.getDate() : null;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy