001/**
002 * pdbconverter - Convert Palm PDB files into more common formats
003 *
004 * Copyright (C) 2009 Richard "Shred" Körber
005 *   http://pdbconverter.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU General Public License as published by
009 * the Free Software Foundation, either version 3 of the License, or
010 * (at your option) any later version.
011 *
012 * This program is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU General Public License
018 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019 */
020package org.shredzone.pdbconverter.export;
021
022import java.io.IOException;
023import java.io.OutputStream;
024import java.util.Calendar;
025
026import net.fortuna.ical4j.data.CalendarOutputter;
027import net.fortuna.ical4j.model.Date;
028import net.fortuna.ical4j.model.DateList;
029import net.fortuna.ical4j.model.DateTime;
030import net.fortuna.ical4j.model.Dur;
031import net.fortuna.ical4j.model.Recur;
032import net.fortuna.ical4j.model.TimeZoneRegistry;
033import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
034import net.fortuna.ical4j.model.ValidationException;
035import net.fortuna.ical4j.model.WeekDay;
036import net.fortuna.ical4j.model.component.VAlarm;
037import net.fortuna.ical4j.model.component.VEvent;
038import net.fortuna.ical4j.model.component.VTimeZone;
039import net.fortuna.ical4j.model.parameter.Value;
040import net.fortuna.ical4j.model.property.Action;
041import net.fortuna.ical4j.model.property.CalScale;
042import net.fortuna.ical4j.model.property.Categories;
043import net.fortuna.ical4j.model.property.Clazz;
044import net.fortuna.ical4j.model.property.Description;
045import net.fortuna.ical4j.model.property.DtEnd;
046import net.fortuna.ical4j.model.property.DtStart;
047import net.fortuna.ical4j.model.property.ExDate;
048import net.fortuna.ical4j.model.property.Location;
049import net.fortuna.ical4j.model.property.ProdId;
050import net.fortuna.ical4j.model.property.RRule;
051import net.fortuna.ical4j.model.property.Summary;
052import net.fortuna.ical4j.model.property.Version;
053import net.fortuna.ical4j.util.UidGenerator;
054
055import org.shredzone.pdbconverter.CalendarFactory;
056import org.shredzone.pdbconverter.pdb.PdbDatabase;
057import org.shredzone.pdbconverter.pdb.appinfo.CategoryAppInfo;
058import org.shredzone.pdbconverter.pdb.record.ScheduleRecord;
059import org.shredzone.pdbconverter.pdb.record.ScheduleRecord.Alarm;
060import org.shredzone.pdbconverter.pdb.record.ScheduleRecord.Repeat;
061import org.shredzone.pdbconverter.pdb.record.ScheduleRecord.ShortDate;
062import org.shredzone.pdbconverter.pdb.record.ScheduleRecord.ShortTime;
063
064/*
065 * NOTE TO THE READER:
066 *   This class uses ical4j for writing iCalendar output. ical4j uses classes that
067 *   are named like standard JDK classes, so take care when reading the source code.
068 *   For example, "Date" and "TimeZone" may not be the classes you expect.
069 */
070
071/**
072 * Writes a {@link ScheduleRecord} database as iCalender file.
073 *
074 * @author Richard "Shred" Körber
075 * @see <a href="http://wiki.modularity.net.au/ical4j/">ical4j</a>
076 */
077public class ScheduleExporter extends AbstractExporter<ScheduleRecord, CategoryAppInfo> {
078
079    private static final WeekDay[] WEEKDAYS = {
080        WeekDay.SU, WeekDay.MO, WeekDay.TU, WeekDay.WE, WeekDay.TH, WeekDay.FR, WeekDay.SA,
081    };
082
083    private CalendarFactory cf = CalendarFactory.getInstance();
084    private TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry();
085
086    /**
087     * Writes the {@link ScheduleRecord} database as iCalendar to the given
088     * {@link OutputStream}. iCalendar support is pretty good! It copes with the
089     * entire schedule database.
090     *
091     * @param database
092     *            {@link ScheduleRecord} {@link PdbDatabase} to write
093     * @param out
094     *            {@link OutputStream} to write to
095     */
096    @Override
097    public void export(PdbDatabase<ScheduleRecord, CategoryAppInfo> database, OutputStream out)
098    throws IOException {
099        UidGenerator uidGenerator = new UidGenerator("uidGen");
100
101        net.fortuna.ical4j.model.Calendar calendar = new net.fortuna.ical4j.model.Calendar();
102        calendar.getProperties().add(new ProdId("-//Shredzone.org/pdbconverter 1.0//EN"));
103        calendar.getProperties().add(Version.VERSION_2_0);
104        calendar.getProperties().add(CalScale.GREGORIAN);
105
106        VTimeZone vTimeZone = registry.getTimeZone(cf.getTimeZone().getID()).getVTimeZone();
107        calendar.getComponents().add(vTimeZone);
108
109        for (ScheduleRecord schedule : database.getRecords()) {
110            if (isAccepted(schedule)) {
111                VEvent event = createVEvent(schedule);
112                event.getProperties().add(uidGenerator.generateUid());
113                calendar.getComponents().add(event);
114            }
115        }
116
117        try {
118            CalendarOutputter co = new CalendarOutputter();
119            co.output(calendar, out);
120        } catch (ValidationException ex) {
121            throw new IOException("Validation error", ex);
122        }
123    }
124
125    /**
126     * Creates a new {@link VEvent} for a single {@link ScheduleRecord}.
127     *
128     * @param schedule
129     *            {@link ScheduleRecord} to be exported
130     * @return {@link VEvent} containing the {@link ScheduleRecord}
131     */
132    private VEvent createVEvent(ScheduleRecord schedule) {
133        VEvent result = new VEvent();
134
135        if (schedule.getStartTime() == null && schedule.getEndTime() == null) {
136            // all-day event
137            setAllDaySchedule(result, schedule);
138
139        } else {
140            // event with starting and ending time
141            setSchedule(result, schedule);
142        }
143
144        setAlarm(result, schedule);
145        setRepeat(result, schedule);
146        setDescription(result, schedule);
147        setLocation(result, schedule);
148        setNote(result, schedule);
149        setCategory(result, schedule);
150        setPrivacy(result, schedule);
151
152        return result;
153    }
154
155    /**
156     * Sets the schedule data for a standard calendar entry with definite starting and
157     * ending time.
158     *
159     * @param event
160     *            {@link VEvent} to write to
161     * @param schedule
162     *            {@link ScheduleRecord} to read from
163     */
164    private void setSchedule(VEvent event, ScheduleRecord schedule) {
165        Calendar startDate = convertDateTime(schedule.getSchedule(), schedule.getStartTime());
166        Calendar endDate   = convertDateTime(schedule.getSchedule(), schedule.getEndTime());
167
168        // If ending time is before starting time, add one day to make it end tomorrow
169        if (endDate.before(startDate)) {
170            endDate.add(Calendar.DATE, 1);
171        }
172
173        DateTime startDateTime = new DateTime(startDate.getTime());
174        startDateTime.setTimeZone(registry.getTimeZone(cf.getTimeZone().getID()));
175
176        DateTime endDateTime = new DateTime(endDate.getTime());
177        endDateTime.setTimeZone(registry.getTimeZone(cf.getTimeZone().getID()));
178
179        event.getProperties().add(new DtStart(startDateTime));
180        event.getProperties().add(new DtEnd(endDateTime));
181    }
182
183    /**
184     * Sets the schedule data for an all-day event.
185     *
186     * @param event
187     *            {@link VEvent} to write to
188     * @param schedule
189     *            {@link ScheduleRecord} to read from
190     */
191    private void setAllDaySchedule(VEvent event, ScheduleRecord schedule) {
192        Calendar startDate = convertDate(schedule.getSchedule());
193        event.getProperties().add(new DtStart(new Date(startDate.getTime())));
194        startDate.add(Calendar.DATE, 1);
195        event.getProperties().add(new DtEnd(new Date(startDate.getTime())));
196    }
197
198    /**
199     * Sets the alarm data, if given.
200     *
201     * @param event
202     *            {@link VEvent} to write to
203     * @param schedule
204     *            {@link ScheduleRecord} to read from
205     */
206    private void setAlarm(VEvent event, ScheduleRecord schedule) {
207        Alarm alarm = schedule.getAlarm();
208        if (alarm != null) {
209            int before = -alarm.getValue();
210
211            Dur dur = null;
212            switch (alarm.getUnit()) {
213                case MINUTES: dur = new Dur(0, 0, before, 0); break;
214                case HOURS:   dur = new Dur(0, before, 0, 0); break;
215                case DAYS:    dur = new Dur(before, 0, 0, 0); break;
216                default: throw new IllegalStateException("unknown alarm unit " + alarm.getUnit());
217            }
218
219            VAlarm valarm = new VAlarm(dur);
220            valarm.getProperties().add(Action.DISPLAY);
221            valarm.getProperties().add(new Description(schedule.getDescription()));
222            event.getAlarms().add(valarm);
223        }
224    }
225
226    /**
227     * Sets the repetition data, if given.
228     *
229     * @param event
230     *            {@link VEvent} to write to
231     * @param schedule
232     *            {@link ScheduleRecord} to read from
233     */
234    private void setRepeat(VEvent event, ScheduleRecord schedule) {
235        Repeat repeat = schedule.getRepeat();
236        if (repeat != null) {
237            Date until = null;
238            if (repeat.getUntil() != null) {
239                ShortTime endTime = schedule.getEndTime();
240                if (endTime != null) {
241                    DateTime untilTime = new DateTime(convertDateTime(repeat.getUntil(), endTime).getTime());
242                    untilTime.setUtc(true);
243                    until = untilTime;
244                } else {
245                    Calendar calUntil = convertDate(repeat.getUntil());
246                    calUntil.add(Calendar.DATE, 1);
247                    until = new Date(calUntil.getTime());
248                }
249            }
250
251            Recur recur = null;
252            switch (repeat.getMode()) {
253                case DAILY:
254                    recur = new Recur(Recur.DAILY, until);
255                    break;
256
257                case WEEKLY:
258                    recur = new Recur(Recur.WEEKLY, until);
259                    boolean[] repeatWeekDays = repeat.getWeeklyDays();
260                    for (int ix = 0; ix < repeatWeekDays.length; ix++) {
261                        if (repeatWeekDays[ix]) {
262                            recur.getDayList().add(WEEKDAYS[ix]);
263                        }
264                    }
265                    break;
266
267                case MONTHLY:
268                    recur = new Recur(Recur.MONTHLY, until);
269                    break;
270
271                case MONTHLY_BY_DAY:
272                    recur = new Recur(Recur.MONTHLY, until);
273                    WeekDay wd;
274                    int week = repeat.getMonthlyWeek();
275                    if (week == 4) {
276                        // Last week in month
277                        wd = new WeekDay(WEEKDAYS[repeat.getMonthlyDay()], -1);
278                    } else {
279                        // Any other week, starting from 1
280                        wd = new WeekDay(WEEKDAYS[repeat.getMonthlyDay()], week + 1);
281                    }
282                    recur.getDayList().add(wd);
283                    break;
284
285                case YEARLY:
286                    recur = new Recur(Recur.YEARLY, until);
287                    break;
288
289                default:
290                    throw new IllegalStateException("unknown repeat mode " + repeat.getMode());
291            }
292
293            if (repeat.getFrequency() > 1) {
294                recur.setInterval(repeat.getFrequency());
295            }
296
297            event.getProperties().add(new RRule(recur));
298            setExceptions(event, schedule);
299        }
300    }
301
302    /**
303     * Sets exceptions to a repeating event.
304     *
305     * @param event
306     *            {@link VEvent} to write to
307     * @param schedule
308     *            {@link ScheduleRecord} to read from
309     */
310    private void setExceptions(VEvent event, ScheduleRecord schedule) {
311        for (ShortDate exception : schedule.getExceptions()) {
312            DateList datelist = new DateList(Value.DATE);
313            datelist.add(new Date(convertDate(exception).getTime()));
314            event.getProperties().add(new ExDate(datelist));
315        }
316    }
317
318    /**
319     * Sets the description, if given.
320     *
321     * @param event
322     *            {@link VEvent} to write to
323     * @param schedule
324     *            {@link ScheduleRecord} to read from
325     */
326    private void setDescription(VEvent event, ScheduleRecord schedule) {
327        String summary = schedule.getDescription();
328        if (summary != null) {
329            event.getProperties().add(new Summary(summary));
330        }
331    }
332
333    /**
334     * Sets the location, if given.
335     *
336     * @param event
337     *            {@link VEvent} to write to
338     * @param schedule
339     *            {@link ScheduleRecord} to read from
340     */
341    private void setLocation(VEvent event, ScheduleRecord schedule) {
342        String location = schedule.getLocation();
343        if (location != null) {
344            event.getProperties().add(new Location(location));
345        }
346    }
347
348    /**
349     * Sets the note, if given.
350     *
351     * @param event
352     *            {@link VEvent} to write to
353     * @param schedule
354     *            {@link ScheduleRecord} to read from
355     */
356    private void setNote(VEvent event, ScheduleRecord schedule) {
357        String note = schedule.getNote();
358        if (note != null) {
359            event.getProperties().add(new Description(note));
360        }
361    }
362
363    /**
364     * Sets the category, if given.
365     *
366     * @param event
367     *            {@link VEvent} to write to
368     * @param schedule
369     *            {@link ScheduleRecord} to read from
370     */
371    private void setCategory(VEvent event, ScheduleRecord schedule) {
372        String cat = schedule.getCategory();
373        if (cat != null) {
374            event.getProperties().add(new Categories(cat));
375        }
376    }
377
378    /**
379     * Sets the classification to PRIVATE if the schedule is secret.
380     *
381     * @param event
382     *            {@link VEvent} to write to
383     * @param schedule
384     *            {@link ScheduleRecord} to read from
385     */
386    private void setPrivacy(VEvent event, ScheduleRecord schedule) {
387        if (schedule.isSecret()) {
388            event.getProperties().add(Clazz.PRIVATE);
389        }
390    }
391
392    /**
393     * Converts a {@link ShortDate} to a {@link Calendar} object. Time is set to midnight.
394     *
395     * @param date
396     *            {@link ShortDate} to be converted
397     * @return {@link Calendar} containing the date only
398     */
399    private Calendar convertDate(ShortDate date) {
400        Calendar cal = cf.create();
401        cal.clear();
402        cal.set(date.getYear(), date.getMonth() - 1, date.getDay());
403        return cal;
404    }
405
406    /**
407     * Converts a {@link ShortDate} and {@link ShortTime} to a {@link Calendar} object.
408     *
409     * @param date
410     *            {@link ShortDate} to be converted
411     * @param time
412     *            {@link ShortTime} to be converted
413     * @return {@link Calendar} containing the date and time
414     */
415    private Calendar convertDateTime(ShortDate date, ShortTime time) {
416        Calendar cal = cf.create();
417        cal.clear();
418        cal.set(date.getYear(), date.getMonth() - 1, date.getDay(), time.getHour(), time.getMinute());
419        return cal;
420    }
421
422}