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}