001package biweekly.io;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.Date;
006import java.util.HashSet;
007import java.util.List;
008import java.util.Set;
009import java.util.TimeZone;
010
011import biweekly.component.DaylightSavingsTime;
012import biweekly.component.ICalComponent;
013import biweekly.component.Observance;
014import biweekly.component.StandardTime;
015import biweekly.component.VAlarm;
016import biweekly.component.VTimezone;
017import biweekly.io.ICalTimeZone.Boundary;
018import biweekly.parameter.Related;
019import biweekly.parameter.Role;
020import biweekly.property.Action;
021import biweekly.property.Attachment;
022import biweekly.property.Attendee;
023import biweekly.property.AudioAlarm;
024import biweekly.property.DateEnd;
025import biweekly.property.DateStart;
026import biweekly.property.Daylight;
027import biweekly.property.Description;
028import biweekly.property.DisplayAlarm;
029import biweekly.property.DurationProperty;
030import biweekly.property.EmailAlarm;
031import biweekly.property.Organizer;
032import biweekly.property.ProcedureAlarm;
033import biweekly.property.Repeat;
034import biweekly.property.Timezone;
035import biweekly.property.Trigger;
036import biweekly.property.UtcOffsetProperty;
037import biweekly.property.VCalAlarmProperty;
038import biweekly.util.DateTimeComponents;
039import biweekly.util.Duration;
040import biweekly.util.ICalDate;
041import biweekly.util.UtcOffset;
042
043import com.google.ical.values.DateTimeValue;
044
045/*
046 Copyright (c) 2013-2015, Michael Angstadt
047 All rights reserved.
048
049 Redistribution and use in source and binary forms, with or without
050 modification, are permitted provided that the following conditions are met: 
051
052 1. Redistributions of source code must retain the above copyright notice, this
053 list of conditions and the following disclaimer. 
054 2. Redistributions in binary form must reproduce the above copyright notice,
055 this list of conditions and the following disclaimer in the documentation
056 and/or other materials provided with the distribution. 
057
058 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
059 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
060 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
061 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
062 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
063 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
064 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
065 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
066 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
067 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
068 */
069
070/**
071 * Converts various properties/components into other properties/components for
072 * vCalendar-iCalendar compatibility.
073 * @author Michael Angstadt
074 */
075public class DataModelConverter {
076        /**
077         * Converts vCalendar timezone information to am iCalendar {@link VTimezone}
078         * component.
079         * @param daylights the DAYLIGHT properties
080         * @param tz the TZ property
081         * @return the VTIMEZONE component
082         */
083        public static VTimezone convert(List<Daylight> daylights, Timezone tz) {
084                UtcOffset tzOffset = (tz == null) ? null : tz.getValue();
085                if (daylights.isEmpty() && tzOffset == null) {
086                        return null;
087                }
088
089                VTimezone timezone = new VTimezone("TZ");
090                if (daylights.isEmpty() && tzOffset != null) {
091                        StandardTime st = new StandardTime();
092                        st.setTimezoneOffsetFrom(tzOffset);
093                        st.setTimezoneOffsetTo(tzOffset);
094                        timezone.addStandardTime(st);
095                        return timezone;
096                }
097
098                for (Daylight daylight : daylights) {
099                        if (!daylight.isDaylight()) {
100                                continue;
101                        }
102
103                        UtcOffset daylightOffset = daylight.getOffset();
104                        UtcOffset standardOffset = new UtcOffset(daylightOffset.getHour() - 1, daylightOffset.getMinute());
105
106                        DaylightSavingsTime dst = new DaylightSavingsTime();
107                        dst.setDateStart(daylight.getStart());
108                        dst.setTimezoneOffsetFrom(standardOffset);
109                        dst.setTimezoneOffsetTo(daylightOffset);
110                        dst.addTimezoneName(daylight.getDaylightName());
111                        timezone.addDaylightSavingsTime(dst);
112
113                        StandardTime st = new StandardTime();
114                        st.setDateStart(daylight.getEnd());
115                        st.setTimezoneOffsetFrom(daylightOffset);
116                        st.setTimezoneOffsetTo(standardOffset);
117                        st.addTimezoneName(daylight.getStandardName());
118                        timezone.addStandardTime(st);
119                }
120
121                return timezone.getComponents().isEmpty() ? null : timezone;
122        }
123
124        /**
125         * Converts an iCalendar {@link VTimezone} component into the appropriate
126         * vCalendar properties.
127         * @param timezone the TIMEZONE component
128         * @param dates the date values in the vCalendar object that are effected by
129         * the timezone.
130         * @return the vCalendar properties
131         */
132        public static VCalTimezoneProperties convert(VTimezone timezone, List<Date> dates) {
133                List<Daylight> daylights = new ArrayList<Daylight>();
134                Timezone tz = null;
135                if (dates.isEmpty()) {
136                        return new VCalTimezoneProperties(daylights, tz);
137                }
138
139                ICalTimeZone icalTz = new ICalTimeZone(timezone);
140                Collections.sort(dates);
141                Set<DateTimeValue> daylightStartDates = new HashSet<DateTimeValue>();
142                boolean zeroObservanceUsed = false;
143                for (Date date : dates) {
144                        Boundary boundary = icalTz.getObservanceBoundary(date);
145                        Observance observance = boundary.getObservanceIn();
146                        Observance observanceAfter = boundary.getObservanceAfter();
147                        if (observance == null && observanceAfter == null) {
148                                continue;
149                        }
150
151                        if (observance == null) {
152                                //the date comes before the earliest observance
153                                if (observanceAfter instanceof StandardTime && !zeroObservanceUsed) {
154                                        UtcOffset offset = getOffset(observanceAfter.getTimezoneOffsetFrom());
155                                        DateTimeValue start = null;
156                                        DateTimeValue end = boundary.getObservanceAfterStart();
157                                        String standardName = icalTz.getDisplayName(false, TimeZone.SHORT);
158                                        String daylightName = icalTz.getDisplayName(true, TimeZone.SHORT);
159
160                                        Daylight daylight = new Daylight(true, offset, convert(start), convert(end), standardName, daylightName);
161                                        daylights.add(daylight);
162                                        zeroObservanceUsed = true;
163                                }
164
165                                if (observanceAfter instanceof DaylightSavingsTime) {
166                                        UtcOffset offset = getOffset(observanceAfter.getTimezoneOffsetFrom());
167                                        if (offset != null) {
168                                                tz = new Timezone(offset);
169                                        }
170                                }
171
172                                continue;
173                        }
174
175                        if (observance instanceof StandardTime) {
176                                UtcOffset offset = getOffset(observance.getTimezoneOffsetTo());
177                                if (offset != null) {
178                                        tz = new Timezone(offset);
179                                }
180                                continue;
181                        }
182
183                        if (observance instanceof DaylightSavingsTime && !daylightStartDates.contains(boundary.getObservanceInStart())) {
184                                UtcOffset offset = getOffset(observance.getTimezoneOffsetTo());
185                                DateTimeValue start = boundary.getObservanceInStart();
186                                DateTimeValue end = null;
187                                if (observanceAfter != null) {
188                                        end = boundary.getObservanceAfterStart();
189                                }
190
191                                String standardName = icalTz.getDisplayName(false, TimeZone.SHORT);
192                                String daylightName = icalTz.getDisplayName(true, TimeZone.SHORT);
193
194                                Daylight daylight = new Daylight(true, offset, convert(start), convert(end), standardName, daylightName);
195                                daylights.add(daylight);
196                                daylightStartDates.add(start);
197                                continue;
198                        }
199                }
200
201                if (tz == null) {
202                        int rawOffset = icalTz.getRawOffset();
203                        UtcOffset offset = new UtcOffset(rawOffset);
204                        tz = new Timezone(offset);
205                }
206
207                if (daylights.isEmpty()) {
208                        Daylight daylight = new Daylight();
209                        daylight.setDaylight(false);
210                        daylights.add(daylight);
211                }
212
213                return new VCalTimezoneProperties(daylights, tz);
214        }
215
216        private static UtcOffset getOffset(UtcOffsetProperty property) {
217                return (property == null) ? null : property.getValue();
218        }
219
220        private static ICalDate convert(DateTimeValue value) {
221                if (value == null) {
222                        return null;
223                }
224
225                //@formatter:off
226                DateTimeComponents components = new DateTimeComponents(
227                        value.year(),
228                        value.month(),
229                        value.day(),
230                        value.hour(),
231                        value.minute(),
232                        value.second(),
233                        false
234                );
235                //@formatter:on
236
237                return new ICalDate(components, true);
238        }
239
240        /**
241         * Converts a {@link Attendee} property to a {@link Organizer} property.
242         * @param attendee the ATTENDEE property
243         * @return the ORGANIZER property
244         */
245        public static Organizer convert(Attendee attendee) {
246                Organizer organizer = new Organizer(attendee.getCommonName(), attendee.getEmail());
247                organizer.setUri(attendee.getUri());
248                organizer.setParameters(attendee.getParameters());
249                return organizer;
250        }
251
252        /**
253         * Converts a {@link Organizer} property to a {@link Attendee} property.
254         * @param organizer the ORGANIZER property
255         * @return the ATTENDEE property
256         */
257        public static Attendee convert(Organizer organizer) {
258                Attendee attendee = new Attendee(organizer.getCommonName(), organizer.getEmail());
259                attendee.setRole(Role.ORGANIZER);
260                attendee.setUri(organizer.getUri());
261                attendee.setParameters(organizer.getParameters());
262                return attendee;
263        }
264
265        /**
266         * Converts a {@link AudioAlarm} property to a {@link VAlarm} component.
267         * @param aalarm the AALARM property
268         * @return the VALARM component
269         */
270        public static VAlarm convert(AudioAlarm aalarm) {
271                Trigger trigger = new Trigger(aalarm.getStart());
272                VAlarm valarm = new VAlarm(Action.audio(), trigger);
273
274                valarm.addAttachment(buildAttachment(aalarm));
275                valarm.setDuration(aalarm.getSnooze());
276                valarm.setRepeat(aalarm.getRepeat());
277
278                return valarm;
279        }
280
281        /**
282         * Converts a {@link DisplayAlarm} property to a {@link VAlarm} component.
283         * @param dalarm the DALARM property
284         * @return the VALARM component
285         */
286        public static VAlarm convert(DisplayAlarm dalarm) {
287                Trigger trigger = new Trigger(dalarm.getStart());
288                VAlarm valarm = new VAlarm(Action.display(), trigger);
289
290                valarm.setDescription(dalarm.getText());
291                valarm.setDuration(dalarm.getSnooze());
292                valarm.setRepeat(dalarm.getRepeat());
293
294                return valarm;
295        }
296
297        /**
298         * Converts a {@link EmailAlarm} property to a {@link VAlarm} component.
299         * @param malarm the MALARM property
300         * @return the VALARM component
301         */
302        public static VAlarm convert(EmailAlarm malarm) {
303                Trigger trigger = new Trigger(malarm.getStart());
304                VAlarm valarm = new VAlarm(Action.email(), trigger);
305
306                String email = malarm.getEmail();
307                if (email != null) {
308                        valarm.addAttendee(new Attendee(null, email));
309                }
310                valarm.setDescription(malarm.getNote());
311                valarm.setDuration(malarm.getSnooze());
312                valarm.setRepeat(malarm.getRepeat());
313
314                return valarm;
315        }
316
317        /**
318         * Converts a {@link ProcedureAlarm} property to a {@link VAlarm} component.
319         * @param dalarm the PALARM property
320         * @return the VALARM component
321         */
322        public static VAlarm convert(ProcedureAlarm dalarm) {
323                Trigger trigger = new Trigger(dalarm.getStart());
324                VAlarm valarm = new VAlarm(Action.procedure(), trigger);
325
326                valarm.setDescription(dalarm.getPath());
327                valarm.setDuration(dalarm.getSnooze());
328                valarm.setRepeat(dalarm.getRepeat());
329
330                return valarm;
331        }
332
333        private static Attachment buildAttachment(AudioAlarm aalarm) {
334                String type = aalarm.getParameter("TYPE");
335                String contentType = (type == null) ? null : "audio/" + type.toLowerCase();
336                byte data[] = aalarm.getData();
337                if (data != null) {
338                        return new Attachment(contentType, data);
339                }
340
341                String contentId = aalarm.getContentId();
342                String uri = (contentId == null) ? aalarm.getUri() : "CID:" + contentId;
343                return new Attachment(contentType, uri);
344        }
345
346        /**
347         * Converts a {@link VAlarm} component to a vCal alarm property.
348         * @param valarm the VALARM component
349         * @param parent the component that holds the VALARM component
350         * @return the alarm property
351         */
352        public static VCalAlarmProperty convert(VAlarm valarm, ICalComponent parent) {
353                Action action = valarm.getAction();
354                if (action == null) {
355                        return null;
356                }
357
358                if (action.isAudio()) {
359                        AudioAlarm aalarm = new AudioAlarm();
360                        aalarm.setStart(determineStartDate(valarm, parent));
361
362                        List<Attachment> attaches = valarm.getAttachments();
363                        if (!attaches.isEmpty()) {
364                                Attachment attach = attaches.get(0);
365
366                                String formatType = attach.getFormatType();
367                                aalarm.setParameter("TYPE", formatType);
368
369                                byte[] data = attach.getData();
370                                if (data != null) {
371                                        aalarm.setData(data);
372                                }
373
374                                String uri = attach.getUri();
375                                if (uri != null) {
376                                        if (uri.toUpperCase().startsWith("CID:")) {
377                                                String contentId = uri.substring(4);
378                                                aalarm.setContentId(contentId);
379                                        } else {
380                                                aalarm.setUri(uri);
381                                        }
382                                }
383                        }
384
385                        DurationProperty duration = valarm.getDuration();
386                        if (duration != null) {
387                                aalarm.setSnooze(duration.getValue());
388                        }
389
390                        Repeat repeat = valarm.getRepeat();
391                        if (repeat != null) {
392                                aalarm.setRepeat(repeat.getValue());
393                        }
394
395                        return aalarm;
396                }
397
398                if (action.isDisplay()) {
399                        Description description = valarm.getDescription();
400                        String text = (description == null) ? null : description.getValue();
401                        return new DisplayAlarm(text);
402                }
403
404                if (action.isEmail()) {
405                        List<Attendee> attendees = valarm.getAttendees();
406                        String email = attendees.isEmpty() ? null : attendees.get(0).getEmail();
407                        EmailAlarm malarm = new EmailAlarm(email);
408
409                        Description description = valarm.getDescription();
410                        String note = (description == null) ? null : description.getValue();
411                        malarm.setNote(note);
412
413                        return malarm;
414                }
415
416                if (action.isProcedure()) {
417                        Description description = valarm.getDescription();
418                        String path = (description == null) ? null : description.getValue();
419                        return new ProcedureAlarm(path);
420                }
421
422                return null;
423        }
424
425        private static Date determineStartDate(VAlarm valarm, ICalComponent parent) {
426                Trigger trigger = valarm.getTrigger();
427                if (trigger == null) {
428                        return null;
429                }
430
431                Date start = trigger.getDate();
432                if (start != null) {
433                        return start;
434                }
435
436                Duration triggerDuration = trigger.getDuration();
437                if (triggerDuration == null) {
438                        return null;
439                }
440
441                Related related = trigger.getRelated();
442                if (related == Related.START) {
443                        DateStart parentDateStart = parent.getProperty(DateStart.class);
444                        if (parentDateStart == null) {
445                                return null;
446                        }
447
448                        Date date = parentDateStart.getValue();
449                        return (date == null) ? null : triggerDuration.add(date);
450                }
451
452                if (related == Related.END) {
453                        DateEnd parentDateEnd = parent.getProperty(DateEnd.class);
454                        if (parentDateEnd != null) {
455                                Date date = parentDateEnd.getValue();
456                                return (date == null) ? null : triggerDuration.add(date);
457                        }
458
459                        DateStart parentDateStart = parent.getProperty(DateStart.class);
460                        DurationProperty parentDuration = parent.getProperty(DurationProperty.class);
461                        if (parentDuration == null || parentDateStart == null) {
462                                return null;
463                        }
464
465                        Duration duration = parentDuration.getValue();
466                        Date date = parentDateStart.getValue();
467                        return (duration == null || date == null) ? null : duration.add(date);
468                }
469
470                return null;
471        }
472
473        public static class VCalTimezoneProperties {
474                private final List<Daylight> daylights;
475                private final Timezone tz;
476
477                public VCalTimezoneProperties(List<Daylight> daylights, Timezone tz) {
478                        this.daylights = daylights;
479                        this.tz = tz;
480                }
481
482                public List<Daylight> getDaylights() {
483                        return daylights;
484                }
485
486                public Timezone getTz() {
487                        return tz;
488                }
489
490        }
491}