Search in sources :

Example 1 with ZRecur

use of com.zimbra.cs.mailbox.calendar.ZRecur in project zm-mailbox by Zimbra.

the class CalendarUtils method parseRecur.

static Recurrence.IRecurrence parseRecur(Element recurElt, TimeZoneMap invTzMap, ParsedDateTime dtStart, ParsedDateTime dtEnd, ParsedDuration dur, RecurId recurId) throws ServiceException {
    if (dur == null && dtStart != null && dtEnd != null)
        dur = dtEnd.difference(dtStart);
    ArrayList<IRecurrence> addRules = new ArrayList<IRecurrence>();
    ArrayList<IRecurrence> subRules = new ArrayList<IRecurrence>();
    for (Iterator iter = recurElt.elementIterator(); iter.hasNext(); ) {
        Element e = (Element) iter.next();
        boolean exclude = false;
        if (e.getName().equals(MailConstants.E_CAL_EXCLUDE)) {
            exclude = true;
        } else {
            if (!e.getName().equals(MailConstants.E_CAL_ADD)) {
                continue;
            }
        }
        for (Iterator intIter = e.elementIterator(); intIter.hasNext(); ) {
            Element intElt = (Element) intIter.next();
            if (intElt.getName().equals(MailConstants.E_CAL_DATES)) {
                // handle RDATE or EXDATE
                String tzid = intElt.getAttribute(MailConstants.A_CAL_TIMEZONE, null);
                ICalTimeZone tz = tzid != null ? invTzMap.lookupAndAdd(tzid) : null;
                RdateExdate rexdate = new RdateExdate(exclude ? ICalTok.EXDATE : ICalTok.RDATE, tz);
                ICalTok valueType = null;
                for (Iterator<Element> dtvalIter = intElt.elementIterator(MailConstants.E_CAL_DATE_VAL); dtvalIter.hasNext(); ) {
                    ICalTok dtvalValueType = null;
                    Element dtvalElem = dtvalIter.next();
                    Element dtvalStartElem = dtvalElem.getElement(MailConstants.E_CAL_START_TIME);
                    String dtvalStartDateStr = dtvalStartElem.getAttribute(MailConstants.A_CAL_DATETIME);
                    ParsedDateTime dtvalStart = parseDateTime(dtvalElem.getName(), dtvalStartDateStr, tzid, invTzMap);
                    Element dtvalEndElem = dtvalElem.getOptionalElement(MailConstants.E_CAL_END_TIME);
                    Element dtvalDurElem = dtvalElem.getOptionalElement(MailConstants.E_CAL_DURATION);
                    if (dtvalEndElem == null && dtvalDurElem == null) {
                        if (dtvalStart.hasTime())
                            dtvalValueType = ICalTok.DATE_TIME;
                        else
                            dtvalValueType = ICalTok.DATE;
                        rexdate.addValue(dtvalStart);
                    } else {
                        dtvalValueType = ICalTok.PERIOD;
                        if (dtvalEndElem != null) {
                            String dtvalEndDateStr = dtvalEndElem.getAttribute(MailConstants.A_CAL_DATETIME);
                            ParsedDateTime dtvalEnd = parseDateTime(dtvalElem.getName(), dtvalEndDateStr, tzid, invTzMap);
                            Period p = new Period(dtvalStart, dtvalEnd);
                            rexdate.addValue(p);
                        } else {
                            ParsedDuration d = ParsedDuration.parse(dtvalDurElem);
                            Period p = new Period(dtvalStart, d);
                            rexdate.addValue(p);
                        }
                    }
                    if (valueType == null) {
                        valueType = dtvalValueType;
                        rexdate.setValueType(valueType);
                    } else if (valueType != dtvalValueType)
                        throw ServiceException.INVALID_REQUEST("Cannot mix different value types in a single <" + intElt.getName() + "> element", null);
                }
                Recurrence.SingleDates sd = new Recurrence.SingleDates(rexdate, dur);
                if (exclude)
                    subRules.add(sd);
                else
                    addRules.add(sd);
            } else if (intElt.getName().equals(MailConstants.E_CAL_RULE)) {
                // handle RRULE or EXRULE
                // Turn XML into iCal RECUR string, which will then be
                // parsed by ical4j Recur object.
                StringBuilder recurBuf = new StringBuilder(100);
                String freq = IcalXmlStrMap.sFreqMap.toIcal(intElt.getAttribute(MailConstants.A_CAL_RULE_FREQ));
                recurBuf.append("FREQ=").append(freq);
                for (Iterator ruleIter = intElt.elementIterator(); ruleIter.hasNext(); ) {
                    Element ruleElt = (Element) ruleIter.next();
                    String ruleEltName = ruleElt.getName();
                    if (ruleEltName.equals(MailConstants.E_CAL_RULE_UNTIL)) {
                        recurBuf.append(";UNTIL=");
                        String d = ruleElt.getAttribute(MailConstants.A_CAL_DATETIME);
                        recurBuf.append(d);
                        // (RFC2445 Section 4.3.10 Recurrence Rule)
                        if (d.indexOf("T") >= 0)
                            if (d.indexOf("Z") < 0)
                                recurBuf.append('Z');
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_COUNT)) {
                        int num = (int) ruleElt.getAttributeLong(MailConstants.A_CAL_RULE_COUNT_NUM, -1);
                        if (num > 0) {
                            recurBuf.append(";COUNT=").append(num);
                        } else {
                            throw ServiceException.INVALID_REQUEST("Expected positive num attribute in <recur> <rule> <count>", null);
                        }
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_INTERVAL)) {
                        String ival = ruleElt.getAttribute(MailConstants.A_CAL_RULE_INTERVAL_IVAL);
                        recurBuf.append(";INTERVAL=").append(ival);
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_BYSECOND)) {
                        String list = ruleElt.getAttribute(MailConstants.A_CAL_RULE_BYSECOND_SECLIST);
                        recurBuf.append(";BYSECOND=").append(list);
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_BYMINUTE)) {
                        String list = ruleElt.getAttribute(MailConstants.A_CAL_RULE_BYMINUTE_MINLIST);
                        recurBuf.append(";BYMINUTE=").append(list);
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_BYHOUR)) {
                        String list = ruleElt.getAttribute(MailConstants.A_CAL_RULE_BYHOUR_HRLIST);
                        recurBuf.append(";BYHOUR=").append(list);
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_BYDAY)) {
                        recurBuf.append(";BYDAY=");
                        int pos = 0;
                        for (Iterator bydayIter = ruleElt.elementIterator(MailConstants.E_CAL_RULE_BYDAY_WKDAY); bydayIter.hasNext(); pos++) {
                            Element wkdayElt = (Element) bydayIter.next();
                            if (pos > 0)
                                recurBuf.append(",");
                            String ordwk = wkdayElt.getAttribute(MailConstants.A_CAL_RULE_BYDAY_WKDAY_ORDWK, null);
                            if (ordwk != null)
                                recurBuf.append(ordwk);
                            String day = wkdayElt.getAttribute(MailConstants.A_CAL_RULE_DAY);
                            if (day == null || day.length() == 0)
                                throw ServiceException.INVALID_REQUEST("Missing " + MailConstants.A_CAL_RULE_DAY + " in <" + ruleEltName + ">", null);
                            recurBuf.append(day);
                        }
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_BYMONTHDAY)) {
                        String list = ruleElt.getAttribute(MailConstants.A_CAL_RULE_BYMONTHDAY_MODAYLIST);
                        recurBuf.append(";BYMONTHDAY=").append(list);
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_BYYEARDAY)) {
                        String list = ruleElt.getAttribute(MailConstants.A_CAL_RULE_BYYEARDAY_YRDAYLIST);
                        recurBuf.append(";BYYEARDAY=").append(list);
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_BYWEEKNO)) {
                        String list = ruleElt.getAttribute(MailConstants.A_CAL_RULE_BYWEEKNO_WKLIST);
                        recurBuf.append(";BYWEEKNO=").append(list);
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_BYMONTH)) {
                        String list = ruleElt.getAttribute(MailConstants.A_CAL_RULE_BYMONTH_MOLIST);
                        recurBuf.append(";BYMONTH=").append(list);
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_BYSETPOS)) {
                        String list = ruleElt.getAttribute(MailConstants.A_CAL_RULE_BYSETPOS_POSLIST);
                        recurBuf.append(";BYSETPOS=").append(list);
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_WKST)) {
                        String day = ruleElt.getAttribute(MailConstants.A_CAL_RULE_DAY);
                        recurBuf.append(";WKST=").append(day);
                    } else if (ruleEltName.equals(MailConstants.E_CAL_RULE_XNAME)) {
                        String name = ruleElt.getAttribute(MailConstants.A_CAL_RULE_XNAME_NAME, null);
                        if (name != null) {
                            String value = ruleElt.getAttribute(MailConstants.A_CAL_RULE_XNAME_VALUE, "");
                            // TODO: Escape/unescape value according to
                            // "text" rule.
                            recurBuf.append(";").append(name).append("=").append(value);
                        }
                    }
                }
                try {
                    ZRecur recur = new ZRecur(recurBuf.toString(), invTzMap);
                    if (exclude) {
                        subRules.add(new Recurrence.SimpleRepeatingRule(dtStart, dur, recur, null));
                    } else {
                        addRules.add(new Recurrence.SimpleRepeatingRule(dtStart, dur, recur, null));
                    }
                } catch (ServiceException ex) {
                    throw ServiceException.INVALID_REQUEST("Exception parsing <recur> <rule>", ex);
                }
            } else {
                throw ServiceException.INVALID_REQUEST("Expected <date> or <rule> inside of " + e.getName() + ", got " + intElt.getName(), null);
            }
        }
    // iterate inside <add> or <exclude>
    }
    if (recurId != null) {
        return new Recurrence.ExceptionRule(recurId, dtStart, dur, null, addRules, subRules);
    } else {
        return new Recurrence.RecurrenceRule(dtStart, dur, null, addRules, subRules);
    }
}
Also used : IRecurrence(com.zimbra.cs.mailbox.calendar.Recurrence.IRecurrence) Recurrence(com.zimbra.cs.mailbox.calendar.Recurrence) RdateExdate(com.zimbra.cs.mailbox.calendar.RdateExdate) ParsedDuration(com.zimbra.common.calendar.ParsedDuration) Element(com.zimbra.common.soap.Element) ArrayList(java.util.ArrayList) Period(com.zimbra.cs.mailbox.calendar.Period) ICalTok(com.zimbra.common.calendar.ZCalendar.ICalTok) IRecurrence(com.zimbra.cs.mailbox.calendar.Recurrence.IRecurrence) ZRecur(com.zimbra.cs.mailbox.calendar.ZRecur) ServiceException(com.zimbra.common.service.ServiceException) Iterator(java.util.Iterator) ParsedDateTime(com.zimbra.common.calendar.ParsedDateTime) ICalTimeZone(com.zimbra.common.calendar.ICalTimeZone)

Example 2 with ZRecur

use of com.zimbra.cs.mailbox.calendar.ZRecur in project zm-mailbox by Zimbra.

the class WorkingHours method getRecurrenceForDay.

private static IRecurrence getRecurrenceForDay(int dayOfWeek, StartSpec startSpec, TimeRange timeRange, ICalTimeZone tz, TimeZoneMap tzmap) throws ServiceException {
    // DTSTART
    String dateStr = startSpec.getDateString(dayOfWeek);
    String dtStartStr;
    if (tz.sameAsUTC())
        dtStartStr = String.format("%sT%02d%02d00Z", dateStr, timeRange.start.hour, timeRange.start.minute);
    else
        dtStartStr = String.format("TZID=\"%s\":%sT%02d%02d00", tz.getID(), dateStr, timeRange.start.hour, timeRange.start.minute);
    ParsedDateTime dtStart;
    try {
        dtStart = ParsedDateTime.parse(dtStartStr, tzmap);
    } catch (ParseException e) {
        throw ServiceException.INVALID_REQUEST("Bad date/time value \"" + dtStartStr + "\"", e);
    }
    // DURATION
    ParsedDuration dur = timeRange.getDuration();
    // RRULE
    String dayName = DayOfWeekName.lookup(dayOfWeek);
    String ruleStr = String.format("FREQ=WEEKLY;INTERVAL=1;BYDAY=%s", dayName);
    return new Recurrence.SimpleRepeatingRule(dtStart, dur, new ZRecur(ruleStr, tzmap), null);
}
Also used : ZRecur(com.zimbra.cs.mailbox.calendar.ZRecur) ParsedDuration(com.zimbra.common.calendar.ParsedDuration) ParsedDateTime(com.zimbra.common.calendar.ParsedDateTime) ParseException(java.text.ParseException)

Example 3 with ZRecur

use of com.zimbra.cs.mailbox.calendar.ZRecur in project zm-mailbox by Zimbra.

the class CalendarItem method processNewInviteRequestOrCancel.

/**
 * @param pm
 * @param newInvite
 * @param folderId
 * @param nextAlarm
 * @param preserveAlarms
 * @param discardExistingInvites
 * @param batch - if true this call will not update the recurrence and may not persist to the data.
 *                The caller needs to persist the data by calling setContent().
 * @return
 * @throws ServiceException
 */
private boolean processNewInviteRequestOrCancel(ParsedMessage pm, Invite newInvite, int folderId, long nextAlarm, boolean preserveAlarms, boolean discardExistingInvites, boolean batch) throws ServiceException {
    // trace logging
    if (!newInvite.hasRecurId())
        ZimbraLog.calendar.info("Modifying CalendarItem: id=%d, folderId=%d, method=%s, subject=\"%s\", UID=%s", mId, getFolderId(), newInvite.getMethod(), newInvite.isPublic() ? newInvite.getName() : "(private)", mUid);
    else
        ZimbraLog.calendar.info("Modifying CalendarItem: id=%d, folderId=%d, method=%s, subject=\"%s\", UID=%s, recurId=%s", mId, getFolderId(), newInvite.getMethod(), newInvite.isPublic() ? newInvite.getName() : "(private)", mUid, newInvite.getRecurId().getDtZ());
    newInvite.sanitize(false);
    OperationContext octxt = getMailbox().getOperationContext();
    Account authAccount = octxt != null ? octxt.getAuthenticatedUser() : null;
    boolean asAdmin = octxt != null ? octxt.isUsingAdminPrivileges() : false;
    boolean isCancel = newInvite.isCancel();
    boolean requirePrivateCheck = requirePrivateCheck(newInvite);
    short rightsNeeded = isCancel ? (short) (ACL.RIGHT_DELETE | ACL.RIGHT_WRITE) : ACL.RIGHT_WRITE;
    if (!canAccess(rightsNeeded, authAccount, asAdmin, requirePrivateCheck))
        throw ServiceException.PERM_DENIED("you do not have sufficient permissions on this calendar item");
    // Don't allow moving a private appointment on behalf of another user,
    // unless that other user is a calendar resource.
    boolean isCalendarResource = getMailbox().getAccount() instanceof CalendarResource;
    boolean denyPrivateAccess = requirePrivateCheck ? !allowPrivateAccess(authAccount, asAdmin) : false;
    if (!newInvite.isPublic() || !isPublic()) {
        if (folderId != getFolderId()) {
            Folder folder = getMailbox().getFolderById(folderId);
            if (!allowPrivateAccess(folder, authAccount, asAdmin)) {
                denyPrivateAccess = true;
                if (!isCalendarResource)
                    throw ServiceException.PERM_DENIED("you do not have permission to update/cancel private calendar item in target folder");
            }
        }
    }
    // Do not allow organizer to be changed. (bug 74400)
    boolean organizerChanged = organizerChangeCheck(newInvite, true);
    ZOrganizer newOrganizer = newInvite.getOrganizer();
    // of the invite.
    if (isCancel) {
        boolean cancelAll;
        boolean outdated;
        if (!newInvite.hasRecurId()) {
            cancelAll = true;
            // Canceling series.  Check the sequencing requirement to make sure the invite isn't outdated.
            Invite series = getInvite((RecurId) null);
            // If series invite is not found, assume cancel is not outdated.
            outdated = series != null && !newInvite.isSameOrNewerVersion(series);
        } else {
            // Canceling an instance.  It's a total cancel only if mInvites has one invite and it matches
            // the recurrence id.  (subject to sequencing requirements)
            cancelAll = false;
            outdated = false;
            Invite curr = getInvite(newInvite.getRecurId());
            if (curr != null) {
                if (newInvite.isSameOrNewerVersion(curr)) {
                    cancelAll = true;
                    // See if there any non-cancel invites besides the one being canceled.
                    for (Invite inv : mInvites) {
                        if (!inv.equals(curr) && !inv.isCancel()) {
                            cancelAll = false;
                            break;
                        }
                    }
                } else {
                    // There is already a newer invite.  Ignore the cancel.
                    outdated = true;
                }
            }
        }
        if (outdated) {
            ZimbraLog.calendar.info("Ignoring outdated cancel request");
            return false;
        }
        if (cancelAll) {
            Folder trash = mMailbox.getFolderById(Mailbox.ID_FOLDER_TRASH);
            move(trash);
            // If we have revisions enabled we need to force metadata write to db because version field changed.
            if (getMaxRevisions() != 1)
                saveMetadata();
            return true;
        }
    }
    // Clear all replies if replacing appointment in trash folder with a new invite.  All existing invites are
    // being discarded, and so all existing replies must be discarded as well.
    Folder folder = getMailbox().getFolderById(folderId);
    if (!isCancel && discardExistingInvites && inTrash() && !folder.inTrash()) {
        mReplyList.mReplies.clear();
    }
    // Handle change to the series that involves time and/or recurrence.  In Exchange compatibility mode,
    // time/recurrence change blows away all exception instances.  In non-compat mode (old ZCS behavior),
    // look for change in the start time and shift the time part of exceptions' RECURRENCE-ID by the same delta.
    boolean needRecurrenceIdUpdate = false;
    ParsedDateTime oldDtStart = null;
    ParsedDuration dtStartMovedBy = null;
    ArrayList<Invite> toUpdate = new ArrayList<Invite>();
    if (!discardExistingInvites && !isCancel && newInvite.isRecurrence()) {
        Invite defInv = getDefaultInviteOrNull();
        if (defInv != null && defInv.isRecurrence()) {
            if (!getAccount().isCalendarKeepExceptionsOnSeriesTimeChange()) {
                // Exchange compatibility mode
                InviteChanges ic = new InviteChanges(defInv, newInvite);
                if (ic.isExceptionRemovingChange()) {
                    discardExistingInvites = true;
                }
            } else {
                // old ZCS behavior
                // Be careful.  If invites got delivered out of order, we may have defInv that's not
                // a series.  Imagine 1st invite received was an exception and 2nd was the series.
                // In that situation we simply skip the DTSTART shift calculation.
                oldDtStart = defInv.getStartTime();
                ParsedDateTime newDtStart = newInvite.getStartTime();
                // if (newDtStart != null && oldDtStart != null && !newDtStart.sameTime(oldDtStart))
                if (newDtStart != null && oldDtStart != null && !newDtStart.equals(oldDtStart)) {
                    // Find the series frequency.
                    Frequency freq = null;
                    IRecurrence recurrence = newInvite.getRecurrence();
                    if (recurrence != null) {
                        Iterator rulesIter = recurrence.addRulesIterator();
                        if (rulesIter != null) {
                            for (; rulesIter.hasNext(); ) {
                                Object ruleObj = rulesIter.next();
                                if (ruleObj instanceof SimpleRepeatingRule) {
                                    SimpleRepeatingRule series = (SimpleRepeatingRule) ruleObj;
                                    ZRecur recur = series.getRule();
                                    freq = recur.getFrequency();
                                    break;
                                }
                            }
                        }
                    }
                    // Maximum allowed delta depends on the frequency.
                    ParsedDuration deltaLimit = null;
                    if (freq != null) {
                        switch(freq) {
                            case DAILY:
                                deltaLimit = ParsedDuration.ONE_DAY;
                                break;
                            case WEEKLY:
                            case MONTHLY:
                            case YEARLY:
                                // Do the RECURRENCE-ID adjustment only when DTSTART moved by 7 days or less.
                                // If it moved by more, it gets too complicated to figure out what the old RECURRENCE-ID
                                // should be in the new series.  Just blow away all exceptions.
                                deltaLimit = ParsedDuration.ONE_WEEK;
                                break;
                            default:
                                // Secondly/minutely/hourly rules are too frequent to allow recurrence id shifting.
                                break;
                        }
                    }
                    if (deltaLimit != null) {
                        ParsedDuration delta = newDtStart.difference(oldDtStart);
                        if (delta.abs().compareTo(deltaLimit) < 0) {
                            needRecurrenceIdUpdate = true;
                            dtStartMovedBy = delta;
                        }
                    }
                }
            }
        }
    }
    // found, inherit from the series invite.
    if (!discardExistingInvites && preserveAlarms) {
        Invite localSeries = null;
        Invite alarmSourceInv = null;
        for (Invite inv : mInvites) {
            if (recurrenceIdsMatch(inv, newInvite)) {
                alarmSourceInv = inv;
                break;
            }
            if (!inv.hasRecurId())
                localSeries = inv;
        }
        if (alarmSourceInv == null)
            alarmSourceInv = localSeries;
        if (alarmSourceInv != null) {
            newInvite.clearAlarms();
            for (Iterator<Alarm> alarmIter = alarmSourceInv.alarmsIterator(); alarmIter.hasNext(); ) {
                newInvite.addAlarm(alarmIter.next());
            }
        }
    }
    // Is this a series update invite from ZCO?  If so, we have to treat all exceptions as local-only
    // and make them snap to series.
    boolean zcoSeriesUpdate = false;
    ZProperty xzDiscardExcepts = newInvite.getXProperty(ICalTok.X_ZIMBRA_DISCARD_EXCEPTIONS.toString());
    if (xzDiscardExcepts != null)
        zcoSeriesUpdate = xzDiscardExcepts.getBoolValue();
    // Is this an update to the series with UNTIL in the rule?  If so, we need to remove exceptions
    // whose RECURRENCE-ID come later than UNTIL. (bug 11870)
    long seriesUntil = Long.MAX_VALUE;
    if (!isCancel && !newInvite.hasRecurId()) {
        ParsedDateTime dtStart = newInvite.getStartTime();
        IRecurrence recur = newInvite.getRecurrence();
        if (recur != null && dtStart != null) {
            ICalTimeZone tz = dtStart.getTimeZone();
            // Find the repeating rule.
            Iterator<?> iter = recur.addRulesIterator();
            if (iter != null) {
                for (; iter.hasNext(); ) {
                    IRecurrence cur = (IRecurrence) iter.next();
                    if (cur.getType() == Recurrence.TYPE_REPEATING) {
                        ZRecur rrule = ((Recurrence.SimpleRepeatingRule) cur).getRule();
                        ParsedDateTime until = rrule.getUntil();
                        if (until != null)
                            seriesUntil = Math.min(until.getDateForRecurUntil(tz).getTime(), seriesUntil);
                    }
                }
            }
        }
    }
    // Check if exception instances are made obsolete by updated recurrence rule.  (bug 47061)
    Set<String> obsoletedRecurIdZs = new HashSet<String>();
    if (!isCancel && newInvite.isRecurrence()) {
        Invite seriesInv = null;
        // Find the range of existing exception instances.
        long rangeStart = Long.MAX_VALUE;
        long rangeEnd = Long.MIN_VALUE;
        for (Invite inv : mInvites) {
            if (inv.hasRecurId()) {
                RecurId rid = inv.getRecurId();
                ParsedDateTime ridDt = rid.getDt();
                if (ridDt != null) {
                    // Turn Outlook-style all-day RecurId to standard-style.
                    if (inv.isAllDayEvent() && ridDt.hasTime() && ridDt.hasZeroTime()) {
                        ParsedDateTime ridDtFixed = (ParsedDateTime) ridDt.clone();
                        ridDtFixed.setHasTime(false);
                        rid = new RecurId(ridDtFixed, rid.getRange());
                        ridDt = rid.getDt();
                    }
                    // Adjust start time if necessary.
                    RecurId adjustedRid;
                    long adjustedT;
                    if (dtStartMovedBy != null) {
                        ParsedDateTime dt = ridDt.add(dtStartMovedBy);
                        adjustedRid = new RecurId(dt, rid.getRange());
                        adjustedT = dt.getUtcTime();
                    } else {
                        adjustedRid = rid;
                        adjustedT = ridDt.getUtcTime();
                    }
                    rangeStart = Math.min(rangeStart, adjustedT);
                    rangeEnd = Math.max(rangeEnd, adjustedT);
                    // initially all instances considered obsolete
                    obsoletedRecurIdZs.add(adjustedRid.getDtZ());
                }
            } else {
                seriesInv = inv;
            }
        }
        // Extend the range by a day on both ends to compensate for all-day appointments.
        // 25 hours to accommodate DST onset dates
        long millisIn25Hours = 25 * 60 * 60 * 1000;
        if (rangeStart != Long.MAX_VALUE)
            rangeStart -= millisIn25Hours;
        if (rangeEnd != Long.MIN_VALUE)
            rangeEnd += millisIn25Hours;
        if (rangeStart != Long.MAX_VALUE && rangeEnd != Long.MIN_VALUE && rangeStart <= rangeEnd) {
            // so the final instance is included in the range
            ++rangeEnd;
            IRecurrence recur = newInvite.getRecurrence();
            if (recur instanceof RecurrenceRule) {
                RecurrenceRule rrule = (RecurrenceRule) recur;
                List<Instance> instances = rrule.expandInstances(getId(), rangeStart, rangeEnd);
                if (instances != null) {
                    for (Instance inst : instances) {
                        Invite refInv = seriesInv != null ? seriesInv : newInvite;
                        RecurId rid = inst.makeRecurId(refInv);
                        // Turn Outlook-style all-day RecurId to standard-style.
                        if (refInv.isAllDayEvent() && rid.getDt() != null) {
                            ParsedDateTime ridDtFixed = (ParsedDateTime) rid.getDt().clone();
                            ridDtFixed.setHasTime(false);
                            rid = new RecurId(ridDtFixed, rid.getRange());
                        }
                        // "Un-obsolete" the surviving recurrence ids.
                        obsoletedRecurIdZs.remove(rid.getDtZ());
                    }
                }
            } else if (recur != null) {
                // This shouldn't happen.
                ZimbraLog.calendar.warn("Expected RecurrenceRule object, but got " + recur.getClass().getName());
            }
        }
    }
    boolean addNewOne = true;
    boolean replaceExceptionBodyWithSeriesBody = false;
    boolean modifiedCalItem = false;
    // the invite which has been made obsolete by the new one coming in
    Invite prev = null;
    // Invites to remove from our blob store
    ArrayList<Invite> toRemove = new ArrayList<Invite>();
    // indexes to remove from mInvites
    ArrayList<Integer> idxsToRemove = new ArrayList<Integer>();
    // get current size because we may add to the list in the loop
    int numInvitesCurrent = mInvites.size();
    for (int i = 0; i < numInvitesCurrent; i++) {
        Invite cur = mInvites.get(i);
        // If request is a cancellation of entire appointment, simply add each invite to removal list.
        if (isCancel && !newInvite.hasRecurId()) {
            addNewOne = false;
            modifiedCalItem = true;
            toRemove.add(cur);
            idxsToRemove.add(0, i);
            continue;
        }
        // Use DTSTART for comparison rather than RECURRENCE-ID.
        if (!isCancel && cur.hasRecurId()) {
            ParsedDateTime instDtStart = cur.getStartTime();
            if (instDtStart != null && instDtStart.getUtcTime() > seriesUntil) {
                modifiedCalItem = true;
                toRemove.add(cur);
                idxsToRemove.add(0, i);
                continue;
            }
        }
        // Remove exceptions obsoleted by changed RRULE.  (bug 47061)
        if (cur.hasRecurId() && !obsoletedRecurIdZs.isEmpty()) {
            RecurId rid = cur.getRecurId();
            if (rid != null && rid.getDt() != null) {
                // Turn Outlook-style all-day RecurId to standard-style.
                ParsedDateTime ridDt = rid.getDt();
                if (cur.isAllDayEvent() && ridDt.hasTime() && ridDt.hasZeroTime()) {
                    ParsedDateTime ridDtFixed = (ParsedDateTime) ridDt.clone();
                    ridDtFixed.setHasTime(false);
                    rid = new RecurId(ridDtFixed, rid.getRange());
                }
                // Adjust start time if necessary.
                RecurId adjustedRid;
                if (dtStartMovedBy != null) {
                    ParsedDateTime dt = rid.getDt().add(dtStartMovedBy);
                    adjustedRid = new RecurId(dt, rid.getRange());
                } else {
                    adjustedRid = rid;
                }
                if (obsoletedRecurIdZs.contains(adjustedRid.getDtZ())) {
                    modifiedCalItem = true;
                    toRemove.add(cur);
                    idxsToRemove.add(0, i);
                    continue;
                }
            }
        }
        boolean matchingRecurId = recurrenceIdsMatch(cur, newInvite);
        if (discardExistingInvites || matchingRecurId) {
            if (discardExistingInvites || newInvite.isSameOrNewerVersion(cur)) {
                // Invite is local-only only if both old and new are local-only.
                newInvite.setLocalOnly(cur.isLocalOnly() && newInvite.isLocalOnly());
                toRemove.add(cur);
                // add to FRONT of list, so when we iterate for the removals we go from HIGHER TO LOWER
                // that way the numbers all match up as the list contracts!
                idxsToRemove.add(0, Integer.valueOf(i));
                boolean invalidateReplies = false;
                if (!discardExistingInvites) {
                    InviteChanges invChg = new InviteChanges(cur, newInvite);
                    invalidateReplies = invChg.isReplyInvalidatingChange();
                }
                if (discardExistingInvites || invalidateReplies) {
                    // clean up any old REPLYs that have been made obsolete by this new invite
                    mReplyList.removeObsoleteEntries(newInvite.getRecurId(), newInvite.getSeqNo(), newInvite.getDTStamp());
                } else {
                    // If the change is minor, don't discard earlier replies.  Organizer may have incremented the
                    // sequence unnecessarily, and we have to cope with this by bumping up the sequence in the
                    // replies accordingly.
                    mReplyList.upgradeEntriesToNewSeq(newInvite.getRecurId(), newInvite.getSeqNo(), newInvite.getDTStamp());
                }
                prev = cur;
                modifiedCalItem = true;
                if (isCancel && !newInvite.hasRecurId()) {
                    // can't CANCEL just the recurId=null entry -- we must delete the whole appointment
                    addNewOne = false;
                }
            } else {
                // perhaps delivered out of order.  Ignore it.
                return false;
            }
        } else if (!isCancel) {
            modifiedCalItem = true;
            boolean addToUpdateList = false;
            if (organizerChanged) {
                // If organizer is changing on any invite, change it on all invites.
                cur.setOrganizer(newOrganizer);
                addToUpdateList = true;
            }
            if (needRecurrenceIdUpdate) {
                // Adjust RECURRENCE-ID by the delta in series DTSTART, if recurrence id value has the
                // same time of day as old DTSTART.
                RecurId rid = cur.getRecurId();
                if (rid != null && rid.getDt() != null && oldDtStart != null) {
                    ParsedDateTime ridDt = rid.getDt();
                    if (ridDt.sameTime(oldDtStart)) {
                        ParsedDateTime dt = rid.getDt().add(dtStartMovedBy);
                        RecurId newRid = new RecurId(dt, rid.getRange());
                        cur.setRecurId(newRid);
                        // used in RECURRENCE-ID and adjust DTEND accordingly.
                        if (cur.isCancel()) {
                            cur.setDtStart(dt);
                            ParsedDateTime dtEnd = cur.getEndTime();
                            if (dtEnd != null) {
                                ParsedDateTime dtEndMoved = dtEnd.add(dtStartMovedBy);
                                cur.setDtEnd(dtEndMoved);
                            }
                        }
                        addToUpdateList = true;
                    }
                }
            }
            // organizer) are left alone.
            if (!newInvite.hasRecurId() && cur.hasRecurId() && (zcoSeriesUpdate || cur.isLocalOnly())) {
                if (cur.isCancel()) {
                    // Local-only cancellations are undone by update to the series.
                    toRemove.add(cur);
                    // add to FRONT of list, so when we iterate for the removals we go from HIGHER TO LOWER
                    // that way the numbers all match up as the list contracts!
                    idxsToRemove.add(0, Integer.valueOf(i));
                    // clean up any old REPLYs that have been made obsolete by this new invite
                    mReplyList.removeObsoleteEntries(newInvite.getRecurId(), newInvite.getSeqNo(), newInvite.getDTStamp());
                    addToUpdateList = false;
                } else {
                    replaceExceptionBodyWithSeriesBody = true;
                    // Recreate invite with data from newInvite, but preserve alarm info.
                    Invite copy = newInvite.newCopy();
                    // It's still local-only.
                    copy.setLocalOnly(true);
                    copy.setMailItemId(cur.getMailItemId());
                    copy.setComponentNum(cur.getComponentNum());
                    copy.setSeqNo(cur.getSeqNo());
                    copy.setDtStamp(cur.getDTStamp());
                    copy.setRecurId(cur.getRecurId());
                    // because we're only dealing with exceptions
                    copy.setRecurrence(null);
                    ParsedDateTime start = cur.getRecurId().getDt();
                    if (start != null) {
                        // snap back to series start time
                        copy.setDtStart(start);
                        ParsedDuration dur = cur.getDuration();
                        if (dur != null) {
                            copy.setDtEnd(null);
                            copy.setDuration(dur);
                        } else {
                            copy.setDuration(null);
                            dur = cur.getEffectiveDuration();
                            ParsedDateTime end = null;
                            if (dur != null)
                                end = start.add(dur);
                            copy.setDtEnd(end);
                        }
                    } else {
                        copy.setDtStart(null);
                        copy.setDtEnd(cur.getEndTime());
                        copy.setDuration(null);
                    }
                    copy.clearAlarms();
                    for (Iterator<Alarm> iter = cur.alarmsIterator(); iter.hasNext(); ) {
                        copy.addAlarm(iter.next());
                    }
                    // Series was updated, so change this exception's partstat to NEEDS-ACTION.
                    ZAttendee me = copy.getMatchingAttendee(getAccount());
                    if (me != null)
                        me.setPartStat(IcalXmlStrMap.PARTSTAT_NEEDS_ACTION);
                    mInvites.set(i, copy);
                    addToUpdateList = true;
                }
            }
            if (addToUpdateList)
                toUpdate.add(cur);
        }
    }
    boolean callProcessPartStat = false;
    if (addNewOne) {
        newInvite.setCalendarItem(this);
        // unless that other user is a calendar resource.
        if (denyPrivateAccess && prev != null && !prev.isPublic() && !isCalendarResource)
            throw ServiceException.PERM_DENIED("you do not have sufficient permissions on this calendar item");
        if (prev != null && !newInvite.isOrganizer() && newInvite.sentByMe()) {
            // A non-organizer attendee is modifying data on his/her
            // appointment/task.  Any information that is tracked in
            // metadata rather than in the iCal MIME part must be
            // carried over from the last invite to the new one.
            newInvite.setPartStat(prev.getPartStat());
            if (prev.hasRsvp()) {
                newInvite.setRsvp(prev.getRsvp());
            }
            newInvite.getCalendarItem().saveMetadata();
        // No need to mark invite as modified item in mailbox as
        // it has already been marked as a created item.
        } else {
            callProcessPartStat = true;
        }
        newInvite.setClassPropSetByMe(newInvite.sentByMe());
        // retain the value and therefore don't allow the organizer to override it.
        if (prev != null && !newInvite.isOrganizer() && !newInvite.sentByMe()) {
            if (!prev.isPublic() && prev.classPropSetByMe()) {
                newInvite.setClassProp(prev.getClassProp());
                newInvite.setClassPropSetByMe(true);
            }
            if (!newInvite.hasRsvp()) {
                newInvite.setRsvp(prev.getRsvp());
            }
        }
        mInvites.add(newInvite);
        // the appointment/task stores an uber-tzmap, for its uber-recurrence
        // this might give us problems if we had two invites with conflicting TZ
        // defs....should be very unlikely
        mTzMap.add(newInvite.getTimeZoneMap());
        // TIM: don't write the blob until the end of the function (so we only do one write for the update)
        // modifyBlob(toRemove, replaceExistingInvites, toUpdate, pm, newInvite, locator, isCancel, !denyPrivateAccess);
        modifiedCalItem = true;
    } else {
    // TIM: don't write the blob until the end of the function (so we only do one write for the update)
    // modifyBlob(toRemove, replaceExistingInvites, toUpdate, null, null, locator, isCancel, !denyPrivateAccess);
    }
    // now remove the inviteid's from our list
    for (Iterator<Integer> iter = idxsToRemove.iterator(); iter.hasNext(); ) {
        assert (modifiedCalItem);
        Integer i = iter.next();
        mInvites.remove(i.intValue());
    }
    // Check if there are any surviving non-cancel invites after applying the update.
    // Also check for changes in flags.
    int oldFlags = mData.getFlags();
    int newFlags = mData.getFlags() & ~(Flag.BITMASK_ATTACHED | Flag.BITMASK_DRAFT | Flag.BITMASK_HIGH_PRIORITY | Flag.BITMASK_LOW_PRIORITY);
    boolean hasSurvivingRequests = false;
    for (Invite cur : mInvites) {
        String method = cur.getMethod();
        if (method.equals(ICalTok.REQUEST.toString()) || method.equals(ICalTok.PUBLISH.toString())) {
            hasSurvivingRequests = true;
            if (cur.hasAttachment())
                newFlags |= Flag.BITMASK_ATTACHED;
            if (cur.isDraft())
                newFlags |= Flag.BITMASK_DRAFT;
            if (cur.isHighPriority())
                newFlags |= Flag.BITMASK_HIGH_PRIORITY;
            if (cur.isLowPriority())
                newFlags |= Flag.BITMASK_LOW_PRIORITY;
        }
    }
    if (newFlags != oldFlags) {
        mData.setFlags(newFlags);
        modifiedCalItem = true;
    }
    if (!hasSurvivingRequests) {
        if (!isCancel)
            ZimbraLog.calendar.warn("Invalid state: deleting calendar item " + getId() + " in mailbox " + getMailboxId() + " while processing a non-cancel request");
        else
            ZimbraLog.calendar.warn("Invalid state: deleting calendar item " + getId() + " in mailbox " + getMailboxId() + " because it has no invite after applying cancel invite");
        // delete this appointment/task from the table,
        delete();
        // it doesn't have anymore REQUESTs!
        return false;
    } else {
        if (nextAlarm > 0 && mAlarmData != null && mAlarmData.getNextAtBase() != nextAlarm)
            modifiedCalItem = true;
        if (modifiedCalItem) {
            if (!batch && !updateRecurrence(nextAlarm)) {
                // no default invite!  This appointment/task no longer valid
                ZimbraLog.calendar.warn("Invalid state: deleting calendar item " + getId() + " in mailbox " + getMailboxId() + " because it has no invite");
                delete();
                return false;
            } else {
                if (callProcessPartStat) {
                    // processPartStat() must be called after
                    // updateRecurrence() has been called.  (bug 8072)
                    processPartStat(newInvite, pm != null ? pm.getMimeMessage() : null, false, newInvite.getPartStat());
                }
                if (getFolderId() != folderId) {
                    // Move appointment/task to a different folder.
                    move(folder);
                }
                // Did the appointment have a blob before the change?
                boolean hadBlobPart = false;
                Invite[] oldInvs = getInvites();
                if (oldInvs != null) {
                    for (Invite oldInv : oldInvs) {
                        if (oldInv.hasBlobPart()) {
                            hadBlobPart = true;
                            break;
                        }
                    }
                }
                // Update blob if adding a new ParsedMessage or if there is already a blob, in which
                // case we may have to delete a section from it.
                boolean newInvHasBlobPart = newInvite.hasBlobPart();
                if (hadBlobPart || newInvHasBlobPart) {
                    if (addNewOne) {
                        modifyBlob(toRemove, discardExistingInvites, toUpdate, pm, newInvite, isCancel, !denyPrivateAccess, true, replaceExceptionBodyWithSeriesBody);
                    } else {
                        if (!newInvHasBlobPart)
                            // force existing MIME part to be removed
                            toRemove.add(newInvite);
                        modifyBlob(toRemove, discardExistingInvites, toUpdate, null, null, isCancel, !denyPrivateAccess, true, replaceExceptionBodyWithSeriesBody);
                    }
                // TIM: modifyBlob will save the metadata for us as a side-effect
                // saveMetadata();
                } else {
                    markItemModified(Change.INVITE);
                    try {
                        if (batch) {
                            persistBatchedChanges = true;
                        } else {
                            // call setContent here so that MOD_CONTENT is updated...this is required
                            // for the index entry to be correctly updated (bug 39463)
                            setContent(null, null);
                        }
                    } catch (IOException e) {
                        throw ServiceException.FAILURE("IOException", e);
                    }
                }
                // remove the item if all the instances are canceled.
                Invite defInvite = getDefaultInviteOrNull();
                if (defInvite != null) {
                    Collection<Instance> instances = expandInstances(0, Long.MAX_VALUE, false);
                    if (instances.isEmpty()) {
                        ZimbraLog.calendar.warn("Deleting calendar item " + getId() + " in mailbox " + getMailboxId() + " because it has no invite after applying request/cancel invite");
                        delete();
                        return true;
                    }
                }
                Callback cb = getCallback();
                if (cb != null)
                    cb.modified(this);
                return true;
            }
        } else {
            if (getFolderId() != folderId) {
                // Move appointment/task to a different folder.
                move(folder);
            }
            return false;
        }
    }
}
Also used : Account(com.zimbra.cs.account.Account) InviteChanges(com.zimbra.cs.mailbox.calendar.InviteChanges) ParsedDuration(com.zimbra.common.calendar.ParsedDuration) ArrayList(java.util.ArrayList) RecurId(com.zimbra.cs.mailbox.calendar.RecurId) IRecurrence(com.zimbra.cs.mailbox.calendar.Recurrence.IRecurrence) Iterator(java.util.Iterator) ParsedDateTime(com.zimbra.common.calendar.ParsedDateTime) SimpleRepeatingRule(com.zimbra.cs.mailbox.calendar.Recurrence.SimpleRepeatingRule) CalendarResource(com.zimbra.cs.account.CalendarResource) HashSet(java.util.HashSet) RecurrenceRule(com.zimbra.cs.mailbox.calendar.Recurrence.RecurrenceRule) ZOrganizer(com.zimbra.cs.mailbox.calendar.ZOrganizer) IOException(java.io.IOException) ZRecur(com.zimbra.cs.mailbox.calendar.ZRecur) Alarm(com.zimbra.cs.mailbox.calendar.Alarm) ZAttendee(com.zimbra.cs.mailbox.calendar.ZAttendee) ZProperty(com.zimbra.common.calendar.ZCalendar.ZProperty) Frequency(com.zimbra.cs.mailbox.calendar.ZRecur.Frequency) Invite(com.zimbra.cs.mailbox.calendar.Invite) ICalTimeZone(com.zimbra.common.calendar.ICalTimeZone)

Aggregations

ParsedDateTime (com.zimbra.common.calendar.ParsedDateTime)3 ParsedDuration (com.zimbra.common.calendar.ParsedDuration)3 ZRecur (com.zimbra.cs.mailbox.calendar.ZRecur)3 ICalTimeZone (com.zimbra.common.calendar.ICalTimeZone)2 IRecurrence (com.zimbra.cs.mailbox.calendar.Recurrence.IRecurrence)2 ArrayList (java.util.ArrayList)2 Iterator (java.util.Iterator)2 ICalTok (com.zimbra.common.calendar.ZCalendar.ICalTok)1 ZProperty (com.zimbra.common.calendar.ZCalendar.ZProperty)1 ServiceException (com.zimbra.common.service.ServiceException)1 Element (com.zimbra.common.soap.Element)1 Account (com.zimbra.cs.account.Account)1 CalendarResource (com.zimbra.cs.account.CalendarResource)1 Alarm (com.zimbra.cs.mailbox.calendar.Alarm)1 Invite (com.zimbra.cs.mailbox.calendar.Invite)1 InviteChanges (com.zimbra.cs.mailbox.calendar.InviteChanges)1 Period (com.zimbra.cs.mailbox.calendar.Period)1 RdateExdate (com.zimbra.cs.mailbox.calendar.RdateExdate)1 RecurId (com.zimbra.cs.mailbox.calendar.RecurId)1 Recurrence (com.zimbra.cs.mailbox.calendar.Recurrence)1