use of com.zimbra.common.calendar.ParsedDuration in project zm-mailbox by Zimbra.
the class CompleteTaskInstance method handle.
@Override
public Element handle(Element request, Map<String, Object> context) throws ServiceException {
ZimbraSoapContext zsc = getZimbraSoapContext(context);
Mailbox mbox = getRequestedMailbox(zsc);
OperationContext octxt = getOperationContext(zsc, context);
ItemId iid = new ItemId(request.getAttribute(MailConstants.A_ID), zsc);
Element exceptElem = request.getElement(MailConstants.E_CAL_EXCEPTION_ID);
mbox.lock.lock();
try {
CalendarItem calItem = mbox.getCalendarItemById(octxt, iid.getId());
if (calItem == null) {
throw MailServiceException.NO_SUCH_CALITEM(iid.toString(), "Could not find calendar item");
}
if (!(calItem instanceof Task)) {
throw MailServiceException.NO_SUCH_CALITEM(iid.toString(), "Calendar item is not a task");
}
Invite inv = calItem.getDefaultInviteOrNull();
if (inv == null) {
throw MailServiceException.NO_SUCH_CALITEM(iid.toString(), "No default invite found");
}
if (!inv.isRecurrence()) {
throw ServiceException.INVALID_REQUEST("Task is not a recurring task", null);
}
ParsedDateTime recurStart = inv.getStartTime();
if (recurStart == null) {
throw ServiceException.INVALID_REQUEST("Recurring task is missing start time", null);
}
// the instance being marked complete
TimeZoneMap tzmap = inv.getTimeZoneMap();
Element tzElem = request.getOptionalElement(MailConstants.E_CAL_TZ);
ICalTimeZone tz = null;
if (tzElem != null) {
tz = CalendarUtils.parseTzElement(tzElem);
tzmap.add(tz);
}
ParsedDateTime exceptDt = CalendarUtils.parseDateTime(exceptElem, tzmap);
if (exceptDt.getUtcTime() != recurStart.getUtcTime()) {
throw MailServiceException.INVITE_OUT_OF_DATE(iid.toString());
}
// Create a new single-instance task for completed date.
Invite completed = createCompletedInstanceInvite(inv, exceptDt);
mbox.addInvite(octxt, completed, calItem.getFolderId());
// Update recurrence's start date to the next instance start date.
long oldStart = recurStart.getUtcTime();
long newStart = -1;
Collection<Instance> instances = calItem.expandInstances(oldStart, Long.MAX_VALUE, false);
for (Instance inst : instances) {
if (inst.getStart() > oldStart) {
newStart = inst.getStart();
break;
}
}
if (newStart != -1) {
// Update DTSTART to newStart.
ParsedDateTime newStartDt = ParsedDateTime.fromUTCTime(newStart);
newStartDt.toTimeZone(inv.getStartTime().getTimeZone());
newStartDt.setHasTime(recurStart.hasTime());
// Update DUE.
ParsedDuration dur = inv.getEffectiveDuration();
if (dur != null) {
ParsedDateTime due = newStartDt.add(dur);
inv.setDtEnd(due);
}
inv.setDtStart(newStartDt);
inv.setSeqNo(inv.getSeqNo() + 1);
inv.setDtStamp(System.currentTimeMillis());
mbox.addInvite(octxt, inv, calItem.getFolderId());
} else {
// No more instance left. Delete the recurring task.
mbox.delete(octxt, calItem.getId(), calItem.getType());
}
} finally {
mbox.lock.release();
}
// response
Element response = getResponseElement(zsc);
return response;
}
use of com.zimbra.common.calendar.ParsedDuration in project zm-mailbox by Zimbra.
the class ToXML method encodeInviteComponent.
public static Element encodeInviteComponent(Element parent, ItemIdFormatter ifmt, OperationContext octxt, CalendarItem calItem, /* may be null */
ItemId calId, /* may be null */
Invite invite, int fields, boolean neuter) throws ServiceException {
boolean allFields = true;
if (fields != NOTIFY_FIELDS) {
allFields = false;
if (!needToOutput(fields, Change.INVITE)) {
return parent;
}
}
Element e = parent.addElement(MailConstants.E_INVITE_COMPONENT);
e.addAttribute(MailConstants.A_CAL_METHOD, invite.getMethod());
e.addAttribute(MailConstants.A_CAL_COMPONENT_NUM, invite.getComponentNum());
e.addAttribute(MailConstants.A_CAL_RSVP, invite.getRsvp());
boolean allowPrivateAccess = calItem != null ? allowPrivateAccess(octxt, calItem) : true;
if (allFields) {
if (invite.isPublic() || allowPrivateAccess) {
String priority = invite.getPriority();
if (priority != null) {
e.addAttribute(MailConstants.A_CAL_PRIORITY, priority);
}
e.addAttribute(MailConstants.A_NAME, invite.getName());
e.addAttribute(MailConstants.A_CAL_LOCATION, invite.getLocation());
List<String> categories = invite.getCategories();
if (categories != null) {
for (String cat : categories) {
e.addElement(MailConstants.E_CAL_CATEGORY).setText(cat);
}
}
List<String> comments = invite.getComments();
if (comments != null) {
for (String cmt : comments) {
e.addElement(MailConstants.E_CAL_COMMENT).setText(cmt);
}
}
List<String> contacts = invite.getContacts();
if (contacts != null) {
for (String contact : contacts) {
e.addElement(MailConstants.E_CAL_CONTACT).setText(contact);
}
}
Geo geo = invite.getGeo();
if (geo != null) {
geo.toXml(e);
}
// Percent Complete (VTODO)
if (invite.isTodo()) {
String pct = invite.getPercentComplete();
if (pct != null)
e.addAttribute(MailConstants.A_TASK_PERCENT_COMPLETE, pct);
long completed = invite.getCompleted();
if (completed != 0) {
ParsedDateTime c = ParsedDateTime.fromUTCTime(completed);
e.addAttribute(MailConstants.A_TASK_COMPLETED, c.getDateTimePartString());
}
}
// Attendee(s)
List<ZAttendee> attendees = invite.getAttendees();
for (ZAttendee at : attendees) {
at.toXml(e);
}
// Alarms
Iterator<Alarm> alarmsIter = invite.alarmsIterator();
while (alarmsIter.hasNext()) {
Alarm alarm = alarmsIter.next();
alarm.toXml(e);
}
// x-prop
encodeXProps(e, invite.xpropsIterator());
// fragment
String fragment = invite.getFragment();
if (!Strings.isNullOrEmpty(fragment)) {
e.addAttribute(MailConstants.E_FRAG, fragment, Element.Disposition.CONTENT);
}
if (!invite.hasBlobPart()) {
e.addAttribute(MailConstants.A_CAL_NO_BLOB, true);
}
// Description (plain and html)
String desc = invite.getDescription();
if (desc != null) {
Element descElem = e.addElement(MailConstants.E_CAL_DESCRIPTION);
descElem.setText(desc);
}
String descHtml = invite.getDescriptionHtml();
BrowserDefang defanger = DefangFactory.getDefanger(MimeConstants.CT_TEXT_HTML);
if (descHtml != null) {
try {
descHtml = StringUtil.stripControlCharacters(descHtml);
descHtml = defanger.defang(descHtml, neuter);
Element descHtmlElem = e.addElement(MailConstants.E_CAL_DESC_HTML);
descHtmlElem.setText(descHtml);
} catch (IOException ex) {
ZimbraLog.calendar.warn("Unable to defang HTML for SetAppointmentRequest", ex);
}
}
if (invite.isEvent()) {
if (calItem != null && calItem instanceof Appointment) {
Instance inst = Instance.fromInvite(calItem.getId(), invite);
Appointment appt = (Appointment) calItem;
e.addAttribute(MailConstants.A_APPT_FREEBUSY_ACTUAL, appt.getEffectiveFreeBusyActual(invite, inst));
}
e.addAttribute(MailConstants.A_APPT_FREEBUSY, invite.getFreeBusy());
e.addAttribute(MailConstants.A_APPT_TRANSPARENCY, invite.getTransparency());
}
// Organizer
if (invite.hasOrganizer()) {
ZOrganizer org = invite.getOrganizer();
org.toXml(e);
}
e.addAttribute(MailConstants.A_CAL_URL, invite.getUrl());
}
if (invite.isOrganizer()) {
e.addAttribute(MailConstants.A_CAL_ISORG, true);
}
boolean isRecurring = false;
e.addAttribute("x_uid", invite.getUid());
e.addAttribute(MailConstants.A_UID, invite.getUid());
e.addAttribute(MailConstants.A_CAL_SEQUENCE, invite.getSeqNo());
//zdsync
e.addAttribute(MailConstants.A_CAL_DATETIME, invite.getDTStamp());
String itemId = null;
if (calId != null) {
itemId = calId.toString(ifmt);
} else if (calItem != null) {
itemId = ifmt.formatItemId(calItem);
}
if ((itemId != null) && !("0".equals(itemId))) {
e.addAttribute(MailConstants.A_CAL_ID, /* calItemId */
itemId);
if (invite.isEvent()) {
// for backward compat
e.addAttribute(MailConstants.A_APPT_ID_DEPRECATE_ME, /* apptId */
itemId);
}
if (calItem != null) {
ItemId ciFolderId = new ItemId(calItem.getMailbox(), calItem.getFolderId());
e.addAttribute(MailConstants.A_CAL_ITEM_FOLDER, /* ciFolder */
ifmt.formatItemId(ciFolderId));
}
}
Recurrence.IRecurrence recur = invite.getRecurrence();
if (recur != null) {
isRecurring = true;
Element recurElt = e.addElement(MailConstants.E_CAL_RECUR);
recur.toXml(recurElt);
}
e.addAttribute(MailConstants.A_CAL_STATUS, invite.getStatus());
e.addAttribute(MailConstants.A_CAL_CLASS, invite.getClassProp());
boolean allDay = invite.isAllDayEvent();
boolean isException = invite.hasRecurId();
if (isException) {
e.addAttribute(MailConstants.A_CAL_IS_EXCEPTION, true);
RecurId rid = invite.getRecurId();
e.addAttribute(MailConstants.A_CAL_RECURRENCE_ID_Z, rid.getDtZ());
encodeRecurId(e, rid, allDay);
}
boolean forceUTC = DebugConfig.calendarForceUTC && !isRecurring && !isException && !allDay;
ParsedDateTime dtStart = invite.getStartTime();
if (dtStart != null) {
encodeDtStart(e, dtStart, allDay, forceUTC);
}
ParsedDateTime dtEnd = invite.getEndTime();
if (dtEnd != null) {
encodeDtEnd(e, dtEnd, allDay, invite.isTodo(), forceUTC);
}
ParsedDuration dur = invite.getDuration();
if (dur != null) {
dur.toXml(e);
}
if (allDay) {
e.addAttribute(MailConstants.A_CAL_ALLDAY, true);
}
if (invite.isDraft()) {
e.addAttribute(MailConstants.A_CAL_DRAFT, true);
}
if (invite.isNeverSent()) {
e.addAttribute(MailConstants.A_CAL_NEVER_SENT, true);
}
}
return e;
}
use of com.zimbra.common.calendar.ParsedDuration in project zm-mailbox by Zimbra.
the class Alarm method decodeMetadata.
/**
* Create an Alarm from Metadata. Return value may be null.
* @param meta
* @return
* @throws ServiceException
* @throws ParseException
*/
public static Alarm decodeMetadata(Metadata meta) throws ServiceException {
Action action = expandAction(meta.get(FN_ACTION));
if (!actionAllowed(action))
return null;
TriggerType tt = expandTriggerType(meta.get(FN_TRIGGER_TYPE));
TriggerRelated triggerRelated = null;
ParsedDuration triggerRelative = null;
ParsedDateTime triggerAbsolute = null;
if (TriggerType.ABSOLUTE.equals(tt)) {
try {
triggerAbsolute = ParsedDateTime.parseUtcOnly(meta.get(FN_TRIGGER_ABSOLUTE));
} catch (ParseException e) {
throw ServiceException.FAILURE("Error parsing metadata for alarm", e);
}
} else {
triggerRelative = ParsedDuration.parse(meta.get(FN_TRIGGER_RELATIVE));
triggerRelated = expandTriggerRelated(meta.get(FN_TRIGGER_RELATED, null));
}
ParsedDuration repeatDuration = null;
int repeatCount = 0;
String val = meta.get(FN_REPEAT_DURATION, null);
if (val != null) {
repeatDuration = ParsedDuration.parse(val);
repeatCount = (int) meta.getLong(FN_REPEAT_COUNT, 0);
}
String description = meta.get(FN_DESCRIPTION, null);
String summary = meta.get(FN_SUMMARY, null);
Attach attach = null;
Metadata metaAttach = meta.getMap(FN_ATTACH, true);
if (metaAttach != null)
attach = Util.decodeAttachFromMetadata(metaAttach);
int numAts = (int) meta.getLong(FN_NUM_ATTENDEES, 0);
List<ZAttendee> attendees = new ArrayList<ZAttendee>(numAts);
for (int i = 0; i < numAts; i++) {
try {
Metadata metaAttendee = meta.getMap(FN_ATTENDEE + i, true);
if (metaAttendee != null)
attendees.add(new ZAttendee(metaAttendee));
} catch (ServiceException e) {
ZimbraLog.calendar.warn("Problem decoding attendee " + i + " in ALARM ");
}
}
Alarm alarm = new Alarm(action, tt, triggerRelated, triggerRelative, triggerAbsolute, repeatDuration, repeatCount, description, summary, attach, attendees, Util.decodeXPropsFromMetadata(meta));
return alarm;
}
use of com.zimbra.common.calendar.ParsedDuration 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());
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);
}
}
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;
}
}
}
use of com.zimbra.common.calendar.ParsedDuration in project zm-mailbox by Zimbra.
the class FriendlyCalendaringDescription method addEventDetails.
private void addEventDetails(Invite invite) {
if (!invite.isEvent())
return;
int origPlainLen = mPlainText.length();
try {
String uid = invite.getUid();
String method = invite.getMethod();
List<ZAttendee> attendees = invite.getAttendees();
Mailbox mbox = MailboxManager.getInstance().getMailboxByAccount(mAccount);
// Would be nice to be able to discover what modifications were made by the CalDAV
// client. Currently, this information is not stored in the Calendar.
// Might be able to extract the information from messages in the Sent folder at
// some point in the future?
CalendarItem calItem = mbox.getCalendarItemByUid(null, uid);
if (calItem != null) {
Invite[] calInvites = calItem.getInvites();
if ((calInvites != null) && (calInvites.length > 1))
// At least 1 exception, so this is not a new item
mDefinitelyModified = true;
}
L10nUtil.MsgKey hdrKey = null;
if (method.equals("REQUEST")) {
if (invite.getRecurId() != null)
hdrKey = L10nUtil.MsgKey.zsApptInstanceModified;
else if (mDefinitelyModified)
hdrKey = L10nUtil.MsgKey.zsApptModified;
else
hdrKey = L10nUtil.MsgKey.zsApptNew;
} else if (method.equals("CANCEL")) {
if (invite.getRecurId() != null)
hdrKey = L10nUtil.MsgKey.calendarCancelAppointmentInstance;
else
hdrKey = L10nUtil.MsgKey.calendarCancelAppointment;
} else if (method.equals("REPLY")) {
if ((attendees != null) && !attendees.isEmpty()) {
ZAttendee replier = attendees.get(0);
String partStat = replier.getPartStat();
String replierName = replier.getFriendlyAddress().toString();
if (partStat.equals(IcalXmlStrMap.PARTSTAT_ACCEPTED)) {
hdrKey = L10nUtil.MsgKey.calendarDefaultReplyAccept;
} else if (partStat.equals(IcalXmlStrMap.PARTSTAT_TENTATIVE)) {
hdrKey = L10nUtil.MsgKey.calendarDefaultReplyAccept;
} else if (partStat.equals(IcalXmlStrMap.PARTSTAT_DECLINED)) {
hdrKey = L10nUtil.MsgKey.calendarDefaultReplyDecline;
}
if (hdrKey != null) {
mPlainText.append(L10nUtil.getMessage(hdrKey, mLc, replierName)).append("\n\n");
mHtml.append("<h3>").append(L10nUtil.getMessage(hdrKey, mLc, replierName)).append("</h3>");
hdrKey = null;
}
}
}
if (hdrKey != null) {
mPlainText.append(L10nUtil.getMessage(hdrKey, mLc)).append("\n\n");
mHtml.append("<h3>").append(L10nUtil.getMessage(hdrKey, mLc)).append("</h3>");
}
mHtml.append("\n\n<p>\n<table border=\"0\">\n");
addSimpleRow(L10nUtil.MsgKey.zsSubject, invite.getName());
if (invite.hasOrganizer())
addSimpleRow(L10nUtil.MsgKey.zsOrganizer, invite.getOrganizer().getFriendlyAddress().toString());
mHtml.append("</table>\n<p>\n<table border=\"0\">\n");
addSimpleRow(L10nUtil.MsgKey.zsLocation, invite.getLocation());
ParsedDateTime startTime = invite.getStartTime();
if (startTime != null) {
ParsedDateTime endTime = invite.getEndTime();
if (endTime == null) {
ParsedDuration dur = invite.getDuration();
if (dur == null) {
dur = ParsedDuration.parse(false, 0, 0, 1, /* hours */
0, /* mins */
0);
}
endTime = startTime.add(dur);
}
addSimpleRow(L10nUtil.MsgKey.zsTime, getTimeDisplayString(startTime, endTime, invite.isRecurrence(), invite.isAllDayEvent()));
ZRecur zr = getRecur(invite);
if (zr != null) {
addSimpleRow(L10nUtil.MsgKey.zsRecurrence, getRecurrenceDisplayString(zr, startTime.getCalendarCopy(), mLc));
}
}
if (!method.equals("REPLY") && (attendees != null) && !attendees.isEmpty()) {
mHtml.append("</table>\n<p>\n<table border=\"0\">\n");
StringBuilder attendeeList = new StringBuilder();
boolean firstAdded = false;
for (ZAttendee attendee : attendees) {
if (firstAdded) {
attendeeList.append(", ");
} else {
firstAdded = true;
}
attendeeList.append(attendee.getFriendlyAddress().toString());
}
addSimpleRow(L10nUtil.MsgKey.zsInvitees, attendeeList.toString());
}
mPlainText.append("*~*~*~*~*~*~*~*~*~*\n");
mHtml.append("</table>\n");
mHtml.append("<div>*~*~*~*~*~*~*~*~*~*</div><br>\n");
} catch (ServiceException e) {
sLog.debug("Resetting descriptions due to ServiceException", e);
mPlainText.setLength(origPlainLen);
mHtml.setLength(0);
}
}
Aggregations