Java tutorial
/* ***** BEGIN LICENSE BLOCK ***** * * Copyright (C) 2011-2014 Linagora * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version, provided you comply * with the Additional Terms applicable for OBM connector by Linagora * pursuant to Section 7 of the GNU Affero General Public License, * subsections (b), (c), and (e), pursuant to which you must notably (i) retain * the Message sent thanks to OBM, Free Communication by Linagora? * signature notice appended to any and all outbound messages * (notably e-mail and meeting requests), (ii) retain all hypertext links between * OBM and obm.org, as well as between Linagora and linagora.com, and (iii) refrain * from infringing Linagora intellectual property rights over its trademarks * and commercial brands. Other Additional Terms apply, * see <http://www.linagora.com/licenses/> for more details. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License * for more details. * * You should have received a copy of the GNU Affero General Public License * and its applicable Additional Terms for OBM along with this program. If not, * see <http://www.gnu.org/licenses/> for the GNU Affero General Public License version 3 * and <http://www.linagora.com/licenses/> for the Additional Terms applicable to * OBM connectors. * * ***** END LICENSE BLOCK ***** */ package org.obm.icalendar; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.net.URISyntaxException; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.EnumSet; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import net.fortuna.ical4j.data.CalendarBuilder; import net.fortuna.ical4j.data.CalendarOutputter; import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.data.UnfoldingReader; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.ComponentList; import net.fortuna.ical4j.model.DateList; import net.fortuna.ical4j.model.DateTime; import net.fortuna.ical4j.model.Dur; import net.fortuna.ical4j.model.Parameter; import net.fortuna.ical4j.model.Period; import net.fortuna.ical4j.model.Property; import net.fortuna.ical4j.model.PropertyFactoryImpl; import net.fortuna.ical4j.model.PropertyList; import net.fortuna.ical4j.model.Recur; import net.fortuna.ical4j.model.TimeZoneRegistry; import net.fortuna.ical4j.model.TimeZoneRegistryFactory; import net.fortuna.ical4j.model.ValidationException; import net.fortuna.ical4j.model.WeekDay; import net.fortuna.ical4j.model.WeekDayList; import net.fortuna.ical4j.model.component.CalendarComponent; import net.fortuna.ical4j.model.component.VAlarm; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.component.VFreeBusy; import net.fortuna.ical4j.model.component.VToDo; import net.fortuna.ical4j.model.parameter.Cn; import net.fortuna.ical4j.model.parameter.CuType; import net.fortuna.ical4j.model.parameter.FbType; import net.fortuna.ical4j.model.parameter.PartStat; import net.fortuna.ical4j.model.parameter.Role; import net.fortuna.ical4j.model.parameter.Rsvp; import net.fortuna.ical4j.model.parameter.Value; import net.fortuna.ical4j.model.property.Action; import net.fortuna.ical4j.model.property.CalScale; import net.fortuna.ical4j.model.property.Categories; import net.fortuna.ical4j.model.property.Clazz; import net.fortuna.ical4j.model.property.Comment; import net.fortuna.ical4j.model.property.Created; import net.fortuna.ical4j.model.property.DateProperty; import net.fortuna.ical4j.model.property.Description; import net.fortuna.ical4j.model.property.DtEnd; import net.fortuna.ical4j.model.property.DtStart; import net.fortuna.ical4j.model.property.Due; import net.fortuna.ical4j.model.property.Duration; import net.fortuna.ical4j.model.property.ExDate; import net.fortuna.ical4j.model.property.LastModified; import net.fortuna.ical4j.model.property.Location; import net.fortuna.ical4j.model.property.Method; import net.fortuna.ical4j.model.property.Organizer; import net.fortuna.ical4j.model.property.PercentComplete; import net.fortuna.ical4j.model.property.Priority; import net.fortuna.ical4j.model.property.ProdId; import net.fortuna.ical4j.model.property.RRule; import net.fortuna.ical4j.model.property.RecurrenceId; import net.fortuna.ical4j.model.property.Repeat; import net.fortuna.ical4j.model.property.Sequence; import net.fortuna.ical4j.model.property.Status; import net.fortuna.ical4j.model.property.Summary; import net.fortuna.ical4j.model.property.Transp; import net.fortuna.ical4j.model.property.Trigger; import net.fortuna.ical4j.model.property.Uid; import net.fortuna.ical4j.model.property.Version; import net.fortuna.ical4j.model.property.XProperty; import net.fortuna.ical4j.util.TimeZones; import org.apache.commons.lang.StringUtils; import org.obm.icalendar.ical4jwrapper.EventDate; import org.obm.sync.auth.AccessToken; import org.obm.sync.calendar.Attendee; import org.obm.sync.calendar.CalendarUserType; import org.obm.sync.calendar.Event; import org.obm.sync.calendar.EventExtId; import org.obm.sync.calendar.EventExtId.Factory; import org.obm.sync.calendar.EventOpacity; import org.obm.sync.calendar.EventPrivacy; import org.obm.sync.calendar.EventRecurrence; import org.obm.sync.calendar.EventType; import org.obm.sync.calendar.FreeBusy; import org.obm.sync.calendar.FreeBusyInterval; import org.obm.sync.calendar.FreeBusyRequest; import org.obm.sync.calendar.Participation; import org.obm.sync.calendar.ParticipationRole; import org.obm.sync.calendar.RecurrenceDay; import org.obm.sync.calendar.RecurrenceDays; import org.obm.sync.calendar.RecurrenceKind; import org.obm.sync.date.DateProvider; import org.obm.sync.exception.IllegalRecurrenceKindException; import org.obm.sync.services.AttendeeService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.BiMap; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.inject.Inject; import com.google.inject.Singleton; import fr.aliacom.obm.common.domain.ObmDomain; import fr.aliacom.obm.common.domain.ObmDomainUuid; @Singleton public class Ical4jHelper implements Ical4jRecurrenceHelper { private static final String MAILTO = "mailto:"; private static final int MAX_FOLD_LENGTH = 74; private static final String X_OBM_DOMAIN = "X-OBM-DOMAIN"; private static final String X_OBM_DOMAIN_UUID = "X-OBM-DOMAIN-UUID"; private static final String XOBMORIGIN = "X-OBM-ORIGIN"; private static final boolean DTSTART_WITHOUT_TIMEZONE = false; private static final boolean DTSTART_WITH_TIMEZONE = true; private static Logger logger = LoggerFactory.getLogger(Ical4jHelper.class); private static final BiMap<RecurrenceDay, WeekDay> RECURRENCE_DAY_TO_WEEK_DAY = new ImmutableBiMap.Builder<RecurrenceDay, WeekDay>() .put(RecurrenceDay.Sunday, WeekDay.SU).put(RecurrenceDay.Monday, WeekDay.MO) .put(RecurrenceDay.Tuesday, WeekDay.TU).put(RecurrenceDay.Wednesday, WeekDay.WE) .put(RecurrenceDay.Thursday, WeekDay.TH).put(RecurrenceDay.Friday, WeekDay.FR) .put(RecurrenceDay.Saturday, WeekDay.SA).build(); private static final BiMap<WeekDay, RecurrenceDay> WEEK_DAY_TO_RECURRENCE_DAY = RECURRENCE_DAY_TO_WEEK_DAY .inverse(); private static final Map<RecurrenceKind, String> RECURRENCEKIND_TO_RECUR = new ImmutableMap.Builder<RecurrenceKind, String>() .put(RecurrenceKind.daily, Recur.DAILY).put(RecurrenceKind.weekly, Recur.WEEKLY) .put(RecurrenceKind.monthlybydate, Recur.MONTHLY).put(RecurrenceKind.monthlybyday, Recur.MONTHLY) .put(RecurrenceKind.yearly, Recur.YEARLY).put(RecurrenceKind.yearlybyday, Recur.YEARLY).build(); private static final BiMap<EventPrivacy, Clazz> PRIVACY_TO_CLASSIFICATION = new ImmutableBiMap.Builder<EventPrivacy, Clazz>() .put(EventPrivacy.PUBLIC, Clazz.PUBLIC).put(EventPrivacy.PRIVATE, Clazz.PRIVATE) .put(EventPrivacy.CONFIDENTIAL, Clazz.CONFIDENTIAL).build(); private static final BiMap<Clazz, EventPrivacy> CLASSIFICATION_TO_PRIVACY = PRIVACY_TO_CLASSIFICATION.inverse(); private static final ImmutableSet<String> UTC_TZID_STRINGS = ImmutableSet.of(TimeZones.GMT_ID, // "Etc/GMT" TimeZones.UTC_ID, // "Etc/UTC" TimeZones.IBM_UTC_ID // "GMT" ); private final DateProvider dateProvider; private final Factory eventExtIdFactory; private final AttendeeService attendeeService; private final TimeZoneRegistry tzRegistry; @Inject @VisibleForTesting public Ical4jHelper(DateProvider obmHelper, EventExtId.Factory eventExtIdFactory, AttendeeService attendeeService) { this.dateProvider = obmHelper; this.eventExtIdFactory = eventExtIdFactory; this.attendeeService = attendeeService; this.tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry(); } public String buildIcsInvitationRequest(Ical4jUser iCal4jUser, Event event, AccessToken token) { Calendar calendar = initCalendar(); VEvent vEvent = buildIcsInvitationVEvent(iCal4jUser, event, token, DTSTART_WITHOUT_TIMEZONE); calendar.getComponents().add(vEvent); if (event.isRecurrent()) { for (Event ee : event.getRecurrence().getEventExceptions()) { VEvent eventExt = buildIcsInvitationVEventException(ee, DTSTART_WITHOUT_TIMEZONE); appendUidToICS(eventExt.getProperties(), ee, event.getExtId()); calendar.getComponents().add(eventExt); } } calendar.getProperties().add(Method.REQUEST); return foldingWriterToString(calendar); } private String foldingWriterToString(final Calendar calendar) { Writer writer = new StringWriter(); CalendarOutputter calendarOutputter = new CalendarOutputter(true, MAX_FOLD_LENGTH); try { calendarOutputter.output(calendar, writer); return writer.toString(); } catch (IOException e) { logger.error(e.getMessage(), e); } catch (ValidationException e) { logger.error(e.getMessage(), e); } return null; } public String buildIcsInvitationReply(final Event event, final Ical4jUser replyICal4jUser, AccessToken token) { Method method = Method.REPLY; final Attendee replyAttendee = event.findAttendeeFromEmail(replyICal4jUser.getEmail()); final Calendar calendar = buildVEvent(replyICal4jUser, event, replyAttendee, method, token, DTSTART_WITHOUT_TIMEZONE); calendar.getProperties().add(method); return foldingWriterToString(calendar); } public String buildIcsInvitationCancel(Ical4jUser iCal4jUser, Event event, AccessToken token) { Method method = Method.CANCEL; Calendar calendar = buildVEvent(iCal4jUser, event, null, method, token, DTSTART_WITHOUT_TIMEZONE); calendar.getProperties().add(method); return foldingWriterToString(calendar); } public String buildIcs(Ical4jUser iCal4jUser, Collection<Event> events, AccessToken token) { Calendar calendar = this.buildVEvents(iCal4jUser, events, null, null, token, DTSTART_WITHOUT_TIMEZONE); return foldingWriterToString(calendar); } public String buildIcsWithTimeZoneOnDtStart(Ical4jUser iCal4jUser, Collection<Event> events, AccessToken token) { Calendar calendar = this.buildVEvents(iCal4jUser, events, null, null, token, DTSTART_WITH_TIMEZONE); return foldingWriterToString(calendar); } private VEvent buildIcsInvitationVEventDefaultValue(Event event, boolean dtStartWithTimeZone) { VEvent vEvent = new VEvent(); PropertyList prop = vEvent.getProperties(); appendDtstamp(event, vEvent); appendCreated(prop, event); appendLastModified(prop, event); appendSequence(prop, event); appendAttendeesToICS(prop, event.getAttendees()); appendCategoryToICS(prop, event); appendEventDates(prop, event, dtStartWithTimeZone); appendDescriptionToICS(prop, event); appendLocationToICS(prop, event); appendTranspToICS(prop, event); appendOrganizerToICS(prop, event); appendPriorityToICS(prop, event); appendPrivacyToICS(prop, event); appendSummaryToICS(prop, event); appendRRuleToICS(prop, event); appendExDateToICS(prop, event); appendVAlarmToICS(vEvent.getAlarms(), event); appendRecurenceIdToICS(prop, event); appendXMozLastAck(prop); return vEvent; } private VEvent buildIcsInvitationVEventException(Event event, boolean dtStartWithTimeZone) { return buildIcsInvitationVEventDefaultValue(event, dtStartWithTimeZone); } private VEvent buildIcsInvitationVEvent(Ical4jUser iCal4jUser, Event event, AccessToken token, boolean dtStartWithTimeZone) { VEvent vEvent = buildIcsInvitationVEventDefaultValue(event, dtStartWithTimeZone); PropertyList prop = vEvent.getProperties(); appendUidToICS(prop, event, null); appendXObmDomainProperties(iCal4jUser, prop); appendXObmOrigin(prop, token); return vEvent; } public FreeBusyRequest parseICSFreeBusy(String ics, ObmDomain domain, Integer ownerId) throws IOException, ParserException { CalendarBuilder builder = new CalendarBuilder(); Calendar calendar = builder.build(new StringReader(ics)); FreeBusyRequest freeBusy = new FreeBusyRequest(); if (calendar != null) { ComponentList comps = getComponents(calendar, Component.VFREEBUSY); if (comps.size() > 0) { VFreeBusy vFreeBusy = (VFreeBusy) comps.get(0); freeBusy = getFreeBusy(vFreeBusy, domain, ownerId); } } return freeBusy; } public List<Event> parseICS(String ics, Ical4jUser ical4jUser, Integer ownerId) throws IOException, ParserException { Calendar calendar = buildCalendar(ics); Cache<String, Optional<Attendee>> cache = newAttendeeCache(); if (calendar != null) { return ImmutableList.copyOf(Iterables.concat(getEvents(calendar, ical4jUser, ownerId, cache), getTodos(ical4jUser, calendar, ownerId, cache))); } return ImmutableList.<Event>of(); } private Calendar buildCalendar(String ics) throws IOException, ParserException { CalendarBuilder builder = new CalendarBuilder(); Calendar calendar = builder.build(new UnfoldingReader(new StringReader(ics), true)); return calendar; } public List<Event> parseICSEvent(String ics, Ical4jUser ical4jUser, Integer ownerId) throws IOException, ParserException { Calendar calendar = buildCalendar(ics); Cache<String, Optional<Attendee>> cache = newAttendeeCache(); if (calendar != null) { return ImmutableList.copyOf(getEvents(calendar, ical4jUser, ownerId, cache)); } return ImmutableList.<Event>of(); } private Cache<String, Optional<Attendee>> newAttendeeCache() { return CacheBuilder.newBuilder().build(); } private Collection<Event> getTodos(Ical4jUser ical4jUser, Calendar calendar, Integer ownerId, Cache<String, Optional<Attendee>> cache) { List<Event> todos = Lists.newArrayList(); ComponentList comps = getComponents(calendar, Component.VTODO); for (Object obj : comps) { VToDo vTodo = (VToDo) obj; Event event = convertVTodoToEvent(ical4jUser, vTodo, ownerId, cache); todos.add(event); } return todos; } private Collection<Event> getEvents(Calendar calendar, Ical4jUser ical4jUser, Integer ownerId, Cache<String, Optional<Attendee>> cache) { Map<EventExtId, Event> mapEvents = Maps.newHashMap(); Multimap<EventExtId, Event> mapExceptionEvents = HashMultimap.create(); ComponentList comps = getComponents(calendar, Component.VEVENT); for (Object obj : comps) { VEvent vEvent = (VEvent) obj; Event event = convertVEventToEvent(ical4jUser, vEvent, ownerId, cache); if (event.getRecurrenceId() == null) { mapEvents.put(event.getExtId(), event); } else { mapExceptionEvents.put(event.getExtId(), event); } } return addEventExceptionToDefinedParentEvent(mapEvents, mapExceptionEvents); } private Collection<Event> addEventExceptionToDefinedParentEvent(Map<EventExtId, Event> mapEvents, Multimap<EventExtId, Event> mapExceptionEvents) { Collection<Entry<EventExtId, Collection<Event>>> mapExceptionEventsEntries = mapExceptionEvents.asMap() .entrySet(); for (Entry<EventExtId, Collection<Event>> entry : mapExceptionEventsEntries) { Event parentEvent = mapEvents.get(entry.getKey()); Collection<Event> eventsException = entry.getValue(); if (parentEvent != null) { addOrReplaceExceptions(parentEvent.getRecurrence(), eventsException); } else { logger.warn( "Drop following events exception while parsing ICS file because parent was not defined: {}", eventsException); } } return mapEvents.values(); } private void addOrReplaceExceptions(EventRecurrence recurrenceTarget, Collection<Event> eventsToAdd) { for (Event eventToAdd : eventsToAdd) { recurrenceTarget.getExceptions().remove(eventToAdd.getRecurrenceId()); } recurrenceTarget.getEventExceptions().addAll(eventsToAdd); } /* package */ Event convertVEventToEvent(Ical4jUser ical4jUser, VEvent vEvent, Integer ownerId, Cache<String, Optional<Attendee>> cache) { Event event = new Event(); event.setType(EventType.VEVENT); appendSummary(event, vEvent.getSummary()); appendDescription(event, vEvent.getDescription()); appendUid(event, vEvent.getUid()); appendPrivacy(event, vEvent.getClassification()); appendOwner(event, vEvent.getOrganizer()); appendCategory(event, vEvent.getProperty(Property.CATEGORIES)); appendLocation(event, vEvent.getLocation()); appendSequence(event, vEvent.getSequence()); appendDate(event, vEvent.getStartDate()); appendDuration(event, vEvent.getStartDate(), vEvent.getEndDate()); appendAllDay(event, vEvent); appendPriority(event, vEvent.getPriority()); appendRecurrenceId(event, vEvent.getRecurrenceId()); appendAttendees(event, vEvent, ical4jUser.getObmDomain(), ownerId, cache); appendRecurence(event, vEvent); appendAlert(event, vEvent.getAlarms()); appendOpacity(event, vEvent.getTransparency(), event.isAllday()); appendIsInternal(ical4jUser, event, vEvent.getProperty(X_OBM_DOMAIN_UUID)); appendCreated(event, vEvent.getCreated()); appendLastModified(event, vEvent.getLastModified()); return event; } /* package */ Event convertVTodoToEvent(Ical4jUser ical4jUser, VToDo vTodo, Integer ownerId, Cache<String, Optional<Attendee>> cache) { Event event = new Event(); event.setType(EventType.VTODO); appendSummary(event, vTodo.getSummary()); appendDescription(event, vTodo.getDescription()); appendUid(event, vTodo.getUid()); appendPrivacy(event, vTodo.getClassification()); appendOwner(event, vTodo.getOrganizer()); appendCategory(event, vTodo.getProperty(Property.CATEGORIES)); appendLocation(event, vTodo.getLocation()); appendDate(event, vTodo.getStartDate()); appendDuration(event, vTodo.getStartDate(), vTodo.getDue()); appendAllDay(event, vTodo.getStartDate(), vTodo.getDue()); appendPriority(event, vTodo.getPriority()); appendRecurrenceId(event, vTodo.getRecurrenceId()); appendAttendees(event, vTodo, ical4jUser.getObmDomain(), ownerId, cache); appendRecurence(event, vTodo); appendAlert(event, vTodo.getAlarms()); appendPercent(event, vTodo.getPercentComplete(), ical4jUser.getEmail()); appendStatus(event, vTodo.getStatus(), ical4jUser.getEmail()); appendOpacity(event, (Transp) vTodo.getProperties().getProperty(Property.TRANSP), event.isAllday()); appendIsInternal(ical4jUser, event, vTodo.getProperty(X_OBM_DOMAIN_UUID)); appendCreated(event, vTodo.getCreated()); appendLastModified(event, vTodo.getLastModified()); return event; } private void appendLastModified(Event event, LastModified lastModified) { if (lastModified != null) { event.setTimeUpdate(lastModified.getDate()); } } private void appendCreated(Event event, Created created) { if (created != null) { event.setTimeCreate(created.getDate()); } } private void appendIsInternal(Ical4jUser ical4jUser, Event event, Property obmDomain) { boolean eventIsInternal = false; if (obmDomain != null) { eventIsInternal = ical4jUser.getObmDomain().getUuid().equals(ObmDomainUuid.of(obmDomain.getValue())); } event.setInternalEvent(eventIsInternal); } private void appendOpacity(Event event, Transp transp, boolean isAllDay) { if (Transp.TRANSPARENT.equals(transp)) { event.setOpacity(EventOpacity.TRANSPARENT); } else if (Transp.OPAQUE.equals(transp)) { event.setOpacity(EventOpacity.OPAQUE); } else if (isAllDay) { event.setOpacity(EventOpacity.TRANSPARENT); } else { event.setOpacity(EventOpacity.OPAQUE); } } private void appendRecurrenceId(Event event, RecurrenceId recurrenceId) { if (recurrenceId != null) { event.setRecurrenceId(recurrenceId.getDate()); } } private void appendStatus(Event event, Status status, String email) { if (status != null) { for (Attendee att : event.getAttendees()) { if (att.getEmail().equals(email)) { if (Status.VTODO_NEEDS_ACTION.equals(status)) { att.setParticipation(Participation.needsAction()); } else if (Status.VTODO_IN_PROCESS.equals(status)) { att.setParticipation(Participation.inProgress()); } else if (Status.VTODO_COMPLETED.equals(status)) { att.setParticipation(Participation.completed()); } else if (Status.VTODO_CANCELLED.equals(status)) { att.setParticipation(Participation.declined()); } else { att.setParticipation(null); } } } } } private void appendPercent(Event event, PercentComplete percentComplete, String email) { if (percentComplete != null) { for (Attendee att : event.getAttendees()) { if (att.getEmail().equals(email)) { att.setPercent(percentComplete.getPercentage()); } } } } private void appendPriority(Event event, Priority priority) { int value = 2; if (priority != null) { if (priority.getLevel() >= 6) { value = 1; } else if (priority.getLevel() >= 3 && priority.getLevel() < 6) { value = 2; } else if (priority.getLevel() > 0 && priority.getLevel() < 3) { value = 3; } event.setPriority(value); } } @VisibleForTesting void appendAllDay(Event event, DtStart startDate, DateProperty endDate) { if (endDate != null && startDate != null && startDate.getDate() != null && endDate.getDate() != null) { if (startDate.getDate() instanceof DateTime || endDate.getDate() instanceof DateTime) { event.setAllday(false); return; } } event.setAllday(true); } @VisibleForTesting void appendAllDay(Event event, VEvent vevent) { event.setAllday(isVeventAllDay(vevent)); } private boolean isVeventAllDay(VEvent vevent) { DtStart startDate = vevent.getStartDate(); if (startDate != null) { Parameter dtStartValueParameter = startDate.getParameter(Parameter.VALUE); return Value.DATE.equals(dtStartValueParameter); } Duration duration = vevent.getDuration(); if (duration == null) { return false; } Dur dur = duration.getDuration(); boolean isAllDay = dur.getDays() > 0 && dur.getWeeks() == 0 && dur.getHours() == 0 && dur.getMinutes() == 0 && dur.getSeconds() == 0; return isAllDay; } private void appendDuration(Event event, DtStart startDate, DateProperty due) { int duration = getDuration(startDate, due); event.setDuration(duration); } private void appendDate(Event event, DtStart startDate) { if (startDate != null) { event.setStartDate(startDate.getDate()); event.setTimezoneName("Etc/GMT"); } } private void appendLocation(Event event, Location location) { if (location != null) { event.setLocation(location.getValue()); } } private void appendSequence(Event event, Sequence sequence) { if (sequence != null) { event.setSequence(sequence.getSequenceNo()); } } private void appendCategory(Event event, Property category) { if (category != null) { event.setCategory(category.getValue()); } } private void appendOwner(Event event, Organizer organizer) { if (organizer != null) { Parameter cn = organizer.getParameter(Parameter.CN); String cnOrganizer = ""; if (cn != null) { cnOrganizer = cn.getValue(); } if (cnOrganizer != null && !"".equals(cnOrganizer)) { event.setOwner(cnOrganizer); } int mailToIndex = organizer.getValue().toLowerCase().indexOf(MAILTO); if (mailToIndex != -1) { event.setOwnerEmail(organizer.getValue().substring(mailToIndex + MAILTO.length())); if (StringUtils.isEmpty(event.getOwner())) { event.setOwner(organizer.getValue().substring(mailToIndex + MAILTO.length())); } } } } private void appendPrivacy(Event event, Clazz classification) { EventPrivacy eventPrivacy = CLASSIFICATION_TO_PRIVACY.get(classification); event.setPrivacy(Objects.firstNonNull(eventPrivacy, EventPrivacy.PUBLIC)); } private void appendUid(Event event, Uid uid) { if (uid != null && !Strings.isNullOrEmpty(uid.getValue())) { event.setExtId(new EventExtId(uid.getValue())); } else { event.setExtId(eventExtIdFactory.generate()); } } private void appendDescription(Event event, Description description) { if (description != null) { event.setDescription(description.getValue()); } } private void appendSummary(Event event, Summary summary) { if (summary != null) { event.setTitle(summary.getValue()); } } public String parseEvents(Ical4jUser iCal4jUser, Collection<Event> listEvent, AccessToken token) { Calendar calendar = initCalendar(); for (Event event : listEvent) { VEvent vEvent = getVEvent(iCal4jUser, event, null, null, token, DTSTART_WITHOUT_TIMEZONE); calendar.getComponents().add(vEvent); } return calendar.toString(); } public String parseEvent(Event event, Ical4jUser iCal4jUser, AccessToken token) { if (EventType.VEVENT.equals(event.getType())) { Calendar c = buildVEvent(iCal4jUser, event, null, null, token, DTSTART_WITHOUT_TIMEZONE); return c.toString(); } else if (EventType.VTODO.equals(event.getType())) { Calendar c = buildVTodo(event, iCal4jUser); return c.toString(); } return null; } private Calendar buildVTodo(Event event, Ical4jUser iCal4jUser) { Calendar calendar = initCalendar(); VToDo vTodo = getVToDo(event, iCal4jUser); calendar.getComponents().add(vTodo); if (event.isRecurrent()) { for (Event ee : event.getRecurrence().getEventExceptions()) { VToDo todoExt = getVTodo(ee, event.getExtId(), iCal4jUser, event); calendar.getComponents().add(todoExt); } } return calendar; } private Calendar buildVEvent(Ical4jUser iCal4jUser, Event event, Attendee replyAttendee, Method method, AccessToken token, boolean dtStartWithTimeZone) { return buildVEvents(iCal4jUser, Arrays.asList(event), replyAttendee, method, token, dtStartWithTimeZone); } private Calendar buildVEvents(Ical4jUser iCal4jUser, Collection<Event> events, Attendee replyAttendee, Method method, AccessToken token, boolean dtStartWithTimeZone) { Calendar calendar = initCalendar(); for (Event event : events) { VEvent vEvent = getVEvent(iCal4jUser, event, replyAttendee, method, token, dtStartWithTimeZone); calendar.getComponents().add(vEvent); if (event.isRecurrent()) { for (Event ee : event.getRecurrence().getEventExceptions()) { VEvent eventExt = getVEvent(null, ee, event.getExtId(), event, replyAttendee, method, token, dtStartWithTimeZone); calendar.getComponents().add(eventExt); } } } return calendar; } private VEvent getVEvent(Ical4jUser iCal4jUser, Event event, Attendee replyAttendee, Method method, AccessToken token, boolean dtStartWithTimeZone) { return getVEvent(iCal4jUser, event, null, null, replyAttendee, method, token, dtStartWithTimeZone); } private VEvent getVEvent(Ical4jUser iCal4jUser, Event event, EventExtId parentExtID, Event parent, Attendee replyAttendee, Method method, AccessToken token, boolean dtStartWithTimeZone) { VEvent vEvent = new VEvent(); PropertyList prop = vEvent.getProperties(); if (Method.REPLY.equals(method)) { appendDtstamp(dateProvider.getDate(), vEvent); } else { appendDtstamp(event, vEvent); } appendUidToICS(prop, event, parentExtID); appendCreated(prop, event); appendLastModified(prop, event); appendSequence(prop, event); if (replyAttendee == null) { appendAttendeesToICS(prop, event.getAttendees()); } else { appendAttendeesToICS(prop, ImmutableList.of(replyAttendee)); appendReplyCommentToICS(prop, replyAttendee); } appendCategoryToICS(prop, event); appendEventDates(prop, event, dtStartWithTimeZone); appendDescriptionToICS(prop, event); appendLocationToICS(prop, event); appendTranspToICS(prop, event); if (parent != null) { appendOrganizerToICS(prop, parent); } else { appendOrganizerToICS(prop, event); } appendPriorityToICS(prop, event); appendPrivacyToICS(prop, event); appendSummaryToICS(prop, event); appendRRuleToICS(prop, event); appendExDateToICS(prop, event); if (canAddVAlarmToICS(method)) { appendVAlarmToICS(vEvent.getAlarms(), event); } appendRecurenceIdToICS(prop, event); appendXMozLastAck(prop); if (token != null) { appendXObmOrigin(prop, token); } if (iCal4jUser != null) { appendXObmDomainProperties(iCal4jUser, prop); } return vEvent; } private void appendDtstamp(Event event, VEvent vEvent) { Date eventTimeUpdate = event.getTimeUpdate(); Date eventTimeCreate = event.getTimeCreate(); if (eventTimeUpdate == null && eventTimeCreate == null) { return; } appendDtstamp(Objects.firstNonNull(eventTimeUpdate, eventTimeCreate), vEvent); } private void appendDtstamp(Date time, VEvent vEvent) { vEvent.getDateStamp().setDateTime(new DateTime(time)); } private boolean canAddVAlarmToICS(Method method) { if (method == null || Method.ADD.equals(method) || Method.COUNTER.equals(method) || Method.PUBLISH.equals(method) || Method.REQUEST.equals(method)) { return true; } else { return false; } } private VToDo getVToDo(Event event, Ical4jUser iCal4jUser) { return getVTodo(event, null, iCal4jUser, null); } private VToDo getVTodo(Event event, EventExtId parentExtID, Ical4jUser iCal4jUser, Event pere) { VToDo vTodo = new VToDo(); PropertyList prop = vTodo.getProperties(); appendUidToICS(prop, event, parentExtID); appendCreated(prop, event); appendLastModified(prop, event); appendAttendeesToICS(prop, event.getAttendees()); appendCategoryToICS(prop, event); appendDtStartToICS(prop, event); appendDuedToICS(prop, event); appendDescriptionToICS(prop, event); appendLocationToICS(prop, event); appendTranspToICS(prop, event); if (pere != null) { appendOrganizerToICS(prop, pere); } else { appendOrganizerToICS(prop, event); } appendPriorityToICS(prop, event); appendPrivacyToICS(prop, event); appendSummaryToICS(prop, event); appendRRuleToICS(prop, event); appendExDateToICS(prop, event); appendVAlarmToICS(vTodo.getAlarms(), event); appendRecurenceIdToICS(prop, event); appendPercentCompleteToICS(prop, event, iCal4jUser); appendStatusToICS(prop, event, iCal4jUser); appendXMozLastAck(prop); return vTodo; } private void appendDtStartToICS(PropertyList prop, Event event) { prop.add(getDtStart(event.getStartDate())); } private void appendXMozLastAck(PropertyList prop) { java.util.Calendar cal = java.util.Calendar.getInstance(TimeZone.getTimeZone("GMT")); cal.setTimeInMillis(System.currentTimeMillis()); DateProperty p = new DateProperty("X-MO-LASTACK", PropertyFactoryImpl.getInstance()) { private static final long serialVersionUID = -2237202839701797737L; }; p.setDate(new DateTime(cal.getTime())); prop.add(p); } private void appendXObmDomainProperties(Ical4jUser iCal4jUser, PropertyList prop) { ObmDomain obmDomain = iCal4jUser.getObmDomain(); XProperty domainProp = new XProperty(X_OBM_DOMAIN, obmDomain.getName()); XProperty uuidDomainProp = new XProperty(X_OBM_DOMAIN_UUID, obmDomain.getUuid().get()); prop.add(domainProp); prop.add(uuidDomainProp); } private void appendXObmOrigin(PropertyList prop, AccessToken token) { XProperty p = new XProperty(XOBMORIGIN, token.getOrigin()); prop.add(p); } private void appendStatusToICS(PropertyList prop, Event event, Ical4jUser iCal4jUser) { for (Attendee att : event.getAttendees()) { if (att.getEmail().equals(iCal4jUser.getEmail())) { if (Participation.needsAction().equals(att.getParticipation())) { prop.add(Status.VTODO_NEEDS_ACTION); } else if (Participation.inProgress().equals(att.getParticipation())) { prop.add(Status.VTODO_IN_PROCESS); } else if (Participation.completed().equals(att.getParticipation())) { prop.add(Status.VTODO_COMPLETED); } else if (Participation.declined().equals(att.getParticipation())) { prop.add(Status.VTODO_CANCELLED); } else { prop.add(new Status("")); } } } } private void appendPercentCompleteToICS(PropertyList prop, Event event, Ical4jUser iCal4jUser) { for (Attendee att : event.getAttendees()) { if (att.getEmail().equals(iCal4jUser.getEmail())) { prop.add(new PercentComplete(att.getPercent())); } } } private void appendDuedToICS(PropertyList prop, Event event) { if (event.getDuration() != 0) { Due dtEnd = getDue(event.getStartDate(), event.getDuration()); if (dtEnd != null) { prop.add(dtEnd); } } } private void appendRecurenceIdToICS(PropertyList prop, Event event) { if (event.getRecurrenceId() != null) { prop.add(getRecurrenceId(event)); } } private void appendVAlarmToICS(ComponentList prop, Event event) { VAlarm vAlarm = getVAlarm(event.getAlert()); if (vAlarm != null) { prop.add(vAlarm); } } private void appendExDateToICS(PropertyList prop, Event event) { ExDate exDate = getExDate(event); if (exDate != null) { prop.add(exDate); } } private void appendRRuleToICS(PropertyList prop, Event event) { if (event.getRecurrenceId() == null) { RRule rrule = getRRule(event); if (rrule != null) { prop.add(rrule); } } } private void appendSummaryToICS(PropertyList prop, Event event) { prop.add(new Summary(event.getTitle())); } private void appendReplyCommentToICS(PropertyList prop, Attendee attendee) { Participation status = attendee.getParticipation(); if (status.hasDefinedComment()) { org.obm.sync.calendar.Comment comment = status.getComment(); prop.add(new Comment(comment.serializeToString())); } } private void appendPrivacyToICS(PropertyList prop, Event event) { prop.add(getClazz(event.getPrivacy())); } private void appendPriorityToICS(PropertyList prop, Event event) { int priority = 5; if (event.getPriority() != null) { if (event.getPriority() == 1) { priority = 9; } else if (event.getPriority() == 2) { priority = 5; } else if (event.getPriority() == 3) { priority = 1; } } prop.add(new Priority(priority)); } private void appendOrganizerToICS(PropertyList prop, Event event) { final Attendee organizer = event.findOrganizer(); if (organizer != null) { prop.add(getOrganizer(organizer.getDisplayName(), organizer.getEmail())); } else { prop.add(getOrganizer(event.getOwnerDisplayName(), event.getOwnerEmail())); } } private void appendTranspToICS(PropertyList prop, Event event) { prop.add(getTransp(event.getOpacity())); } private void appendLocationToICS(PropertyList prop, Event event) { if (!isEmpty(event.getLocation())) { prop.add(new Location(event.getLocation())); } } private void appendDescriptionToICS(PropertyList prop, Event event) { if (!isEmpty(event.getDescription())) { prop.add(new Description(event.getDescription())); } } private void appendEventDates(PropertyList prop, Event event, boolean dtStartWithTimeZone) { if (event.isAllday()) { appendDtStartAsDateToICS(prop, event); appendDtEndAsDateToICS(prop, event); } else { if (dtStartWithTimeZone) { appendDtStartAsDateTimeToICSWithTimeZone(prop, event); } else { appendDtStartAsDateTimeToICS(prop, event); } appendDurationToICS(prop, event); } } private void appendDtEndAsDateToICS(PropertyList prop, Event event) { prop.add(new DtEnd(new EventDate(event.getEndDate(), TimeZone.getTimeZone(event.getTimezoneName())), true)); } private void appendDtStartAsDateToICS(PropertyList prop, Event event) { prop.add(new DtStart(new EventDate(event.getStartDate(), TimeZone.getTimeZone(event.getTimezoneName())), true)); } private void appendDtStartAsDateTimeToICSWithTimeZone(PropertyList prop, Event event) { String timezoneName = Objects.firstNonNull(event.getTimezoneName(), TimeZones.GMT_ID); net.fortuna.ical4j.model.TimeZone zone = tzRegistry.getTimeZone(timezoneName); DateTime icalDate = new DateTime(); icalDate.setTime(event.getStartDate().getTime()); if (UTC_TZID_STRINGS.contains(timezoneName)) { icalDate.setUtc(true); } else { icalDate.setTimeZone(zone); } DtStart dts = new DtStart(zone); dts.setDate(icalDate); prop.add(dts); } private void appendDtStartAsDateTimeToICS(PropertyList prop, Event event) { prop.add(new DtStart(new DateTime(event.getStartDate()), true)); } private void appendLastModified(PropertyList prop, Event event) { if (event.getTimeUpdate() != null) { prop.add(new LastModified(new DateTime(event.getTimeUpdate().getTime()))); } } private void appendSequence(PropertyList prop, Event event) { prop.add(new Sequence(event.getSequence())); } private void appendCreated(PropertyList prop, Event event) { if (event.getTimeCreate() != null) { prop.add(new Created(new DateTime(event.getTimeCreate().getTime()))); } } private void appendDurationToICS(PropertyList prop, Event event) { prop.add(new Duration(new Dur(event.getStartDate(), event.getEndDate()))); } private void appendCategoryToICS(PropertyList prop, Event event) { if (!isEmpty(event.getCategory())) { prop.add(new Categories(event.getCategory())); } } private void appendAttendeesToICS(PropertyList prop, List<Attendee> attendees) { for (final Attendee attendee : attendees) { prop.add(getAttendee(attendee)); } } @VisibleForTesting CuType calendarUserTypeToCuType(CalendarUserType type) { return new CuType(type.name()); } private net.fortuna.ical4j.model.property.Attendee getAttendee(Attendee attendee) { net.fortuna.ical4j.model.property.Attendee att = new net.fortuna.ical4j.model.property.Attendee(); att.getParameters().add(calendarUserTypeToCuType(attendee.getCalendarUserType())); PartStat ps = getPartStat(attendee); att.getParameters().add(ps); att.getParameters().add(Rsvp.TRUE); Cn cn = getCn(attendee); att.getParameters().add(cn); Role role = getRole(attendee); att.getParameters().add(role); try { att.setValue(MAILTO + attendee.getEmail()); } catch (URISyntaxException e) { logger.error(e.getMessage(), e); } return att; } private void appendUidToICS(PropertyList prop, Event event, EventExtId parentExtId) { if (parentExtId != null && parentExtId.getExtId() != null) { prop.add(new Uid(parentExtId.serializeToString())); } else if (event.getExtId() != null && event.getExtId().getExtId() != null) { prop.add(new Uid(event.getExtId().serializeToString())); } else { throw new InvalidParameterException(); } } @Override public Date isInIntervalDate(Event event, Date start, Date end, Set<Date> dateExce) { return isInIntervalDate(event.getRecurrence(), event.getStartDate(), start, end, dateExce); } @Override public Date isInIntervalDate(EventRecurrence recurrence, Date eventDate, Date start, Date end, Set<Date> dateExce) { List<Date> dates = dateInInterval(recurrence, eventDate, start, end, dateExce); for (Date date : dates) { if ((date.after(start) || date.equals(start)) && (end == null || ((date.before(end) || date.equals(end))))) { return date; } } return null; } @Override public List<Date> dateInInterval(EventRecurrence recurrence, Date eventDate, Date start, Date end, Set<Date> dateExce) { List<Date> ret = new LinkedList<Date>(); Recur recur = getRecur(recurrence, eventDate); if (recur == null) { ret.add(eventDate); return ret; } if (end == null) { if (start.before(eventDate)) { ret.add(eventDate); return ret; } return ImmutableList.of(); } DateList dl = recur.getDates(new DateTime(eventDate), new DateTime(start), new DateTime(end), Value.DATE_TIME); for (Iterator<?> it = dl.iterator(); it.hasNext();) { Date evD = (Date) it.next(); GregorianCalendar cal = new GregorianCalendar(); cal.setTime(evD); cal.set(GregorianCalendar.MILLISECOND, 0); if (!dateExce.contains(cal.getTime())) { ret.add(evD); } } return ret; } @VisibleForTesting Recur getRecur(EventRecurrence eventRecurrence, Date eventStartDate) { Recur recur = null; if (eventRecurrence.isRecurrent()) { RecurrenceKind recurrenceKind = eventRecurrence.getKind(); if (!RECURRENCEKIND_TO_RECUR.containsKey(recurrenceKind)) { throw new IllegalRecurrenceKindException(recurrenceKind); } String recurFrequency = RECURRENCEKIND_TO_RECUR.get(recurrenceKind); recur = getRecurFrom(eventRecurrence, recurFrequency); if (RecurrenceKind.monthlybyday.equals(recurrenceKind)) { addMonthlyOffsetToRecurDayList(eventStartDate, recur); } recur.setInterval(eventRecurrence.getFrequence()); setRecurDayList(eventRecurrence, recur); } return recur; } private void addMonthlyOffsetToRecurDayList(Date eventStartDate, Recur recur) { GregorianCalendar cal = new GregorianCalendar(); cal.setTime(eventStartDate); recur.getDayList().add(WeekDay.getMonthlyOffset(cal)); } private void setRecurDayList(EventRecurrence eventRecurrence, Recur recur) { Set<WeekDay> listDay = getListDay(eventRecurrence); for (WeekDay weekDay : listDay) { recur.getDayList().add(weekDay); } } private Recur getRecurFrom(EventRecurrence eventRecurrence, String frequency) { if (eventRecurrence.getEnd() == null) { return new Recur(frequency, null); } else { GregorianCalendar cal = new GregorianCalendar(); cal.setTime(eventRecurrence.getEnd()); cal.set(GregorianCalendar.SECOND, 0); return new Recur(frequency, new DateTime(cal.getTime())); } } @VisibleForTesting Set<WeekDay> getListDay(EventRecurrence eventRecurrence) { Set<WeekDay> listDay = new HashSet<WeekDay>(); RecurrenceDays recurrenceDays = eventRecurrence.getDays(); for (RecurrenceDay recurrenceDay : recurrenceDays) { WeekDay weekDay = RECURRENCE_DAY_TO_WEEK_DAY.get(recurrenceDay); if (weekDay == null) { throw new IllegalArgumentException("Unknown recurrence day " + recurrenceDay); } listDay.add(weekDay); } return listDay; } private int getDuration(DtStart startDate, DateProperty endDate) { if (startDate != null && endDate != null) { long start = startDate.getDate().getTime(); long end = endDate.getDate().getTime(); return (int) ((end - start) / 1000); } return 0; } private void appendAlert(Event event, ComponentList cl) { if (cl.size() > 0) { final VAlarm valarm = (VAlarm) cl.get(0); if (valarm != null) { if ((isVAlarmRepeat(valarm) && valarm.getDuration() != null) || (!isVAlarmRepeat(valarm) && valarm.getDuration() == null)) { final Trigger trigger = valarm.getTrigger(); Dur dur = trigger.getDuration(); Dur durZero = new Dur(0, 0, 0, 0); if (dur == null || dur.equals(durZero)) { event.setAlert(null); return; } else if (dur.isNegative()) { dur = dur.negate(); } int day = (dur.getWeeks() * 7) + dur.getDays(); int hours = (day * 24) + dur.getHours(); int min = (hours * 60) + dur.getMinutes(); int sec = min * 60 + dur.getSeconds(); event.setAlert(sec); return; } } } else { event.setAlert(null); } } private boolean isVAlarmRepeat(final VAlarm valarm) { final Repeat repeat = valarm.getRepeat(); if (repeat != null) { return true; } return false; } private void appendRecurence(Event event, CalendarComponent component) { EventRecurrence er = new EventRecurrence(); RRule rrule = (RRule) component.getProperty(Property.RRULE); EnumSet<RecurrenceDay> recurrenceDays = EnumSet.noneOf(RecurrenceDay.class); if (rrule != null) { Recur recur = rrule.getRecur(); String frequency = recur.getFrequency(); if (Recur.WEEKLY.equals(frequency) || Recur.DAILY.equals(frequency)) { for (Object ob : recur.getDayList()) { recurrenceDays.add(weekDayToRecurrenceDay((WeekDay) ob)); } if (Recur.WEEKLY.equals(frequency) && recurrenceDays.isEmpty()) { GregorianCalendar cal = getEventStartCalendar(event); WeekDay eventStartWeekDay = WeekDay.getDay(cal.get(GregorianCalendar.DAY_OF_WEEK)); recurrenceDays.add(WEEK_DAY_TO_RECURRENCE_DAY.get(eventStartWeekDay)); } } er.setDays(new RecurrenceDays(recurrenceDays)); er.setEnd(recur.getUntil()); er.setFrequence(Math.max(recur.getInterval(), 1)); // getInterval() returns -1 if no interval is defined if (er.getDays().isEmpty()) { if (Recur.DAILY.equals(frequency)) { er.setKind(RecurrenceKind.daily); } else if (Recur.WEEKLY.equals(frequency)) { er.setKind(RecurrenceKind.weekly); } else if (Recur.MONTHLY.equals(frequency)) { WeekDayList wdl = recur.getDayList(); if (wdl.size() > 0) { WeekDay day = (WeekDay) wdl.get(0); GregorianCalendar cal = getEventStartCalendar(event); er.setKind(RecurrenceKind.monthlybyday); cal.set(GregorianCalendar.DAY_OF_WEEK, WeekDay.getCalendarDay(day)); cal.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, day.getOffset()); event.setStartDate(cal.getTime()); } else { er.setKind(RecurrenceKind.monthlybydate); } } else if (Recur.YEARLY.equals(frequency)) { er.setKind(RecurrenceKind.yearly); } } else { er.setKind(RecurrenceKind.weekly); } } event.setRecurrence(er); appendNegativeExceptions(event, component.getProperties(Property.EXDATE)); } private void appendNegativeExceptions(Event event, PropertyList exdates) { for (Object ob : exdates) { for (Object date : ((ExDate) ob).getDates()) { event.getRecurrence().addException((Date) date); } } } private RecurrenceDay weekDayToRecurrenceDay(WeekDay weekDay) { RecurrenceDay recurrenceDay = WEEK_DAY_TO_RECURRENCE_DAY.get(weekDay); if (recurrenceDay == null) { throw new IllegalArgumentException("Unknown week day " + weekDay); } return recurrenceDay; } private GregorianCalendar getEventStartCalendar(Event event) { GregorianCalendar cal = new GregorianCalendar(); cal.setTime(event.getStartDate()); return cal; } private void appendAttendees(Event event, Component vEvent, ObmDomain domain, Integer ownerId, Cache<String, Optional<Attendee>> cache) { Map<String, Attendee> emails = new HashMap<String, Attendee>(); for (Property prop : getProperties(vEvent, Property.ATTENDEE)) { Attendee att = convertAttendeePropertyToAttendee(prop, domain, ownerId, cache); if (att == null) { logger.warn("Couldn't find an attendee matching {}, skipping.", prop); continue; } if (att.getEmail() != null && !attendeeAlreadyExist(emails, att)) { emails.put(att.getEmail(), att); } } appendOrganizer(emails, vEvent, domain, ownerId, cache); event.addAttendees(new ArrayList<Attendee>(emails.values())); } private boolean attendeeAlreadyExist(Map<String, Attendee> emails, Attendee att) { return emails.containsKey(att.getEmail()); } private void appendOrganizer(Map<String, Attendee> emails, Component vEvent, ObmDomain domain, Integer ownerId, Cache<String, Optional<Attendee>> cache) { Property prop = vEvent.getProperty(Property.ORGANIZER); if (prop != null) { Organizer orga = (Organizer) prop; if (orga.getValue() != null) { String email = removeMailto(orga); Attendee organizer = emails.get(email); if (organizer != null) { organizer.setOrganizer(true); } else { organizer = convertAttendeePropertyToAttendee(prop, domain, ownerId, cache); organizer.setParticipationRole(ParticipationRole.REQ); organizer.setParticipation(Participation.accepted()); organizer.setOrganizer(true); emails.put(organizer.getEmail(), organizer); } } } } private String removeMailto(Property prop) { String email = extractEmail(prop); return Objects.firstNonNull(email, prop.getValue()); } private String extractEmail(Property prop) { String value = prop.getValue(); int mailIndex = value.toLowerCase().indexOf(MAILTO); if (mailIndex != -1) { return value.substring(mailIndex + MAILTO.length()); } return null; } private Calendar initCalendar() { Calendar calendar = new Calendar(); calendar.getProperties().add(new ProdId("-//Aliasource Groupe LINAGORA//OBM Calendar //FR")); calendar.getProperties().add(Version.VERSION_2_0); calendar.getProperties().add(CalScale.GREGORIAN); return calendar; } @VisibleForTesting ExDate getExDate(Event event) { if (eventHasExceptions(event)) { return buildExDate(event.getRecurrence()); } else { return null; } } private boolean eventHasExceptions(Event event) { if (event.isRecurrent() && event.getRecurrence().hasException() || event.getRecurrence().hasEventException()) { return true; } return false; } private ExDate buildExDate(EventRecurrence eventRecurrence) { DateList exceptionDates = new DateList(); exceptionDates.setUtc(true); for (Date exceptionDeleted : eventRecurrence.getExceptions()) { exceptionDates.add(new DateTime(exceptionDeleted)); } return new ExDate(exceptionDates); } /* package */ VAlarm getVAlarm(Integer alert) { if (alert != null && !alert.equals(0)) { Dur dur = new Dur(0, 0, 0, -alert); VAlarm va = new VAlarm(dur); va.getProperties().add(Action.DISPLAY); va.getProperties().add(new Description("Default Obm Description")); Trigger ti = va.getTrigger(); ti.getParameters().add(new Value("DURATION")); return va; } return null; } /* package */ Clazz getClazz(EventPrivacy privacy) { return Objects.firstNonNull(PRIVACY_TO_CLASSIFICATION.get(privacy), Clazz.PUBLIC); } /* package */ Organizer getOrganizer(String owner, String ownerEmail) { Organizer orga = new Organizer(); try { if (owner != null && !"".equals(owner)) { orga.getParameters().add(new Cn(owner)); } if (ownerEmail != null && !"".equals(ownerEmail)) { orga.setValue(MAILTO + ownerEmail); } } catch (URISyntaxException e) { logger.error(e.getMessage(), e); } return orga; } /* package */ Transp getTransp(EventOpacity eo) { Transp transp = Transp.OPAQUE; if (EventOpacity.OPAQUE.equals(eo)) { transp = Transp.OPAQUE; } else if (EventOpacity.TRANSPARENT.equals(eo)) { transp = Transp.TRANSPARENT; } return transp; } private Due getDue(Date start, int duration) { if (start != null && duration >= 0) { int durationInMS = duration * 1000; DateTime dateTimeEnd = new DateTime(start.getTime() + durationInMS); return new Due(dateTimeEnd); } return null; } /* package */ DtEnd getDtEnd(Date start, int duration) { if (start != null && duration >= 0) { net.fortuna.ical4j.model.Date dateTimeEnd = new DateTime(start.getTime() + duration * 1000); return new DtEnd(dateTimeEnd, true); } return null; } @VisibleForTesting Duration getDuration(Date startDate, Date endDate) { return new Duration(startDate, endDate); } private RecurrenceId getRecurrenceId(Event event) { net.fortuna.ical4j.model.Date dt = null; dt = new DateTime(event.getRecurrenceId()); return new RecurrenceId(dt); } /* package */ Role getRole(Attendee attendee) { Role role = Role.OPT_PARTICIPANT; if (ParticipationRole.CHAIR.equals(attendee.getParticipationRole())) { role = Role.CHAIR; } else if (ParticipationRole.NON.equals(attendee.getParticipationRole())) { role = Role.NON_PARTICIPANT; } else if (ParticipationRole.OPT.equals(attendee.getParticipationRole())) { role = Role.OPT_PARTICIPANT; } else if (ParticipationRole.REQ.equals(attendee.getParticipationRole())) { role = Role.REQ_PARTICIPANT; } return role; } /* package */ Cn getCn(Attendee attendee) { if (isEmpty(attendee.getDisplayName())) { return new Cn(attendee.getEmail()); } return new Cn(attendee.getDisplayName()); } /* package */ PartStat getPartStat(Attendee attendee) { PartStat partStat = PartStat.NEEDS_ACTION; if (Participation.accepted().equals(attendee.getParticipation())) { partStat = PartStat.ACCEPTED; } else if (Participation.completed().equals(attendee.getParticipation())) { partStat = PartStat.COMPLETED; } else if (Participation.declined().equals(attendee.getParticipation())) { partStat = PartStat.DECLINED; } else if (Participation.delegated().equals(attendee.getParticipation())) { partStat = PartStat.DELEGATED; } else if (Participation.inProgress().equals(attendee.getParticipation())) { partStat = PartStat.IN_PROCESS; } else if (Participation.needsAction().equals(attendee.getParticipation())) { partStat = PartStat.NEEDS_ACTION; } else if (Participation.tentative().equals(attendee.getParticipation())) { partStat = PartStat.TENTATIVE; } return partStat; } /* package */ RRule getRRule(Event event) { RRule rrule = null; Recur recur = getRecur(event.getRecurrence(), event.getStartDate()); if (recur != null) { rrule = new RRule(recur); } return rrule; } /* package */ ComponentList getComponents(Calendar calendar, String component) { return calendar.getComponents(component); } private List<Property> getProperties(Component comp, String property) { List<Property> propsSet = new ArrayList<Property>(); PropertyList propList = comp.getProperties(property); for (Iterator<Property> it = propList.iterator(); it.hasNext();) { Property prop = it.next(); propsSet.add(prop); } return propsSet; } private boolean isEmpty(String st) { return st == null || "".equals(st); } private FreeBusyRequest getFreeBusy(VFreeBusy vFreeBusy, ObmDomain domain, Integer ownerId) { FreeBusyRequest fb = new FreeBusyRequest(); appendOwner(fb, vFreeBusy.getOrganizer()); fb.setUid(vFreeBusy.getUid().getValue()); if (vFreeBusy.getStartDate() != null) { fb.setStart(vFreeBusy.getStartDate().getDate()); } if (vFreeBusy.getEndDate() != null) { fb.setEnd(vFreeBusy.getEndDate().getDate()); } appendAttendees(fb, vFreeBusy, domain, ownerId, CacheBuilder.newBuilder().<String, Optional<Attendee>>build()); return fb; } private String getParameterValue(Parameter parameter) { if (parameter == null) { return null; } return parameter.getValue(); } @VisibleForTesting Attendee findAttendeeUsingCuType(String name, String email, String cuType, ObmDomain domain, Integer ownerId) { Attendee attendee = null; if (cuType == null) { attendee = attendeeService.findAttendee(name, email, true, domain, ownerId); } else { CalendarUserType type = CalendarUserType.valueOf(cuType); switch (type) { case GROUP: break; case INDIVIDUAL: attendee = attendeeService.findUserAttendee(name, email, domain); if (attendee == null) { attendee = attendeeService.findContactAttendee(name, email, true, domain, ownerId); } break; case ROOM: case RESOURCE: attendee = attendeeService.findResourceAttendee(name, email, domain, ownerId); break; case UNKNOWN: attendee = attendeeService.findAttendee(name, email, true, domain, ownerId); } } return attendee; } private Attendee findAttendee(final String email, final String cn, final String cuType, final ObmDomain domain, final Integer ownerId, Cache<String, Optional<Attendee>> cache) { try { return cache.get(email, new Callable<Optional<Attendee>>() { @Override public Optional<Attendee> call() throws Exception { return Optional.fromNullable(findAttendeeUsingCuType(cn, email, cuType, domain, ownerId)); } }).transform(new Function<Attendee, Attendee>() { @Override public Attendee apply(Attendee input) { return input.clone(); } }).orNull(); } catch (ExecutionException e) { throw Throwables.propagate(e); } } private Attendee convertAttendeePropertyToAttendee(Property prop, ObmDomain domain, Integer ownerId, Cache<String, Optional<Attendee>> cache) { String email = removeMailto(prop); String cn = getParameterValue(prop.getParameter(Parameter.CN)); String cuType = getParameterValue(prop.getParameter(Parameter.CUTYPE)); Attendee attendee = findAttendee(email, cn, cuType, domain, ownerId, cache); // Couldn't find a resource matching the attendee... if (attendee == null) { return null; } String role = getParameterValue(prop.getParameter(Parameter.ROLE)); String partStat = getParameterValue(prop.getParameter(Parameter.PARTSTAT)); if (cn != null) { attendee.setDisplayName(cn); } if (role != null) { int dashIndex = role.indexOf("-"); if (dashIndex != -1) { attendee.setParticipationRole(ParticipationRole.valueOf(role.substring(0, dashIndex))); } } if (partStat != null) { if (partStat.equals(PartStat.IN_PROCESS.getValue())) { attendee.setParticipation(Participation.inProgress()); } else { attendee.setParticipation(Participation.getValueOf(partStat)); } } else { //rfc5545 : 3.2.12, if PART-STAT is missing, default is NEEDS-ACTION attendee.setParticipation(Participation.needsAction()); } return attendee; } private void appendAttendees(FreeBusyRequest fb, VFreeBusy vFreeBusy, ObmDomain domain, Integer ownerId, Cache<String, Optional<Attendee>> cache) { List<Property> props = getProperties(vFreeBusy, Property.ATTENDEE); for (Property prop : props) { fb.addAttendee(convertAttendeePropertyToAttendee(prop, domain, ownerId, cache)); } } private void appendOwner(FreeBusyRequest fb, Organizer organizer) { if (organizer != null) { Parameter cn = organizer.getParameter(Parameter.CN); String cnOrganizer = ""; if (cn != null) { cnOrganizer = cn.getValue(); } if (cnOrganizer != null && !"".equals(cnOrganizer)) { fb.setOwner(cnOrganizer); } else { int mailToIndex = organizer.getValue().toLowerCase().indexOf(MAILTO); if (mailToIndex != -1) { fb.setOwner(organizer.getValue().substring(mailToIndex + MAILTO.length())); } } } } public String parseFreeBusy(FreeBusy fb) { Calendar calendar = initCalendar(); calendar.getProperties().add(Method.REPLY); VFreeBusy vFreeBusy = getVFreeBusy(fb, fb.getAtt(), fb.getFreeBusyIntervals()); calendar.getComponents().add(vFreeBusy); return calendar.toString(); } private VFreeBusy getVFreeBusy(FreeBusy fb, Attendee att, Set<FreeBusyInterval> fbls) { VFreeBusy vfb = new VFreeBusy(); Organizer orga = getOrganizer("", fb.getOwner()); vfb.getProperties().add(orga); DtStart st = getDtStart(fb.getStart()); vfb.getProperties().add(st); DtEnd en = getDtEnd(fb.getEnd()); vfb.getProperties().add(en); net.fortuna.ical4j.model.property.Attendee at = getAttendee(att); vfb.getProperties().add(at); if (fb.getUid() != null && !"".equals(fb.getUid())) { vfb.getProperties().add(new Uid(fb.getUid())); } for (FreeBusyInterval line : fbls) { DtStart start = getDtStart(line.getStart()); DtEnd end = getDtEnd(line.getStart(), line.getDuration()); if (start != null && end != null) { net.fortuna.ical4j.model.property.FreeBusy fbics = new net.fortuna.ical4j.model.property.FreeBusy(); Period p = new Period(new DateTime(start.getDate()), new DateTime(end.getDate())); fbics.getPeriods().add(p); fbics.getParameters().add(FbType.BUSY); vfb.getProperties().add(fbics); } } return vfb; } private DtEnd getDtEnd(Date end) { return new DtEnd(new DateTime(end), true); } @VisibleForTesting DtStart getDtStart(Date start) { return new DtStart(new DateTime(start), true); } }