001 package biweekly.property.marshaller; 002 003 import java.util.Date; 004 import java.util.Iterator; 005 import java.util.List; 006 import java.util.Map; 007 import java.util.regex.Matcher; 008 import java.util.regex.Pattern; 009 010 import org.w3c.dom.Element; 011 012 import biweekly.ICalDataType; 013 import biweekly.io.json.JCalValue; 014 import biweekly.io.xml.XCalElement; 015 import biweekly.io.xml.XCalNamespaceContext; 016 import biweekly.parameter.ICalParameters; 017 import biweekly.property.RecurrenceProperty; 018 import biweekly.util.ICalDateFormatter; 019 import biweekly.util.ListMultimap; 020 import biweekly.util.Recurrence; 021 import biweekly.util.Recurrence.DayOfWeek; 022 import biweekly.util.Recurrence.Frequency; 023 import biweekly.util.XmlUtils; 024 025 /* 026 Copyright (c) 2013, Michael Angstadt 027 All rights reserved. 028 029 Redistribution and use in source and binary forms, with or without 030 modification, are permitted provided that the following conditions are met: 031 032 1. Redistributions of source code must retain the above copyright notice, this 033 list of conditions and the following disclaimer. 034 2. Redistributions in binary form must reproduce the above copyright notice, 035 this list of conditions and the following disclaimer in the documentation 036 and/or other materials provided with the distribution. 037 038 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 039 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 040 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 041 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 042 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 043 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 044 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 045 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 046 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 047 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 048 */ 049 050 /** 051 * Marshals properties whose values are {@link Recurrence}. 052 * @param <T> the property class 053 * @author Michael Angstadt 054 */ 055 public abstract class RecurrencePropertyMarshaller<T extends RecurrenceProperty> extends ICalPropertyMarshaller<T> { 056 public RecurrencePropertyMarshaller(Class<T> clazz, String propertyName) { 057 super(clazz, propertyName, ICalDataType.RECUR); 058 } 059 060 @Override 061 protected String _writeText(T property) { 062 Recurrence recur = property.getValue(); 063 if (recur == null) { 064 return ""; 065 } 066 067 ListMultimap<String, Object> components = buildComponents(recur, false); 068 return object(components.getMap()); 069 } 070 071 @Override 072 protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, List<String> warnings) { 073 Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); 074 ListMultimap<String, String> rules = object(value); 075 076 parseFreq(rules, builder, warnings); 077 parseUntil(rules, builder, warnings); 078 parseCount(rules, builder, warnings); 079 parseInterval(rules, builder, warnings); 080 parseBySecond(rules, builder, warnings); 081 parseByMinute(rules, builder, warnings); 082 parseByHour(rules, builder, warnings); 083 parseByDay(rules, builder, warnings); 084 parseByMonthDay(rules, builder, warnings); 085 parseByYearDay(rules, builder, warnings); 086 parseByWeekNo(rules, builder, warnings); 087 parseByMonth(rules, builder, warnings); 088 parseBySetPos(rules, builder, warnings); 089 parseWkst(rules, builder, warnings); 090 parseXRules(rules, builder, warnings); //must be called last 091 092 return newInstance(builder.build()); 093 } 094 095 @Override 096 protected void _writeXml(T property, XCalElement element) { 097 XCalElement recurElement = element.append(dataType(property)); 098 099 Recurrence recur = property.getValue(); 100 if (recur == null) { 101 return; 102 } 103 104 ListMultimap<String, Object> components = buildComponents(recur, true); 105 for (Map.Entry<String, List<Object>> component : components) { 106 String name = component.getKey().toLowerCase(); 107 for (Object value : component.getValue()) { 108 recurElement.append(name, value.toString()); 109 } 110 } 111 } 112 113 @Override 114 protected T _parseXml(XCalElement element, ICalParameters parameters, List<String> warnings) { 115 XCalElement value = element.child(defaultDataType); 116 if (value == null) { 117 throw missingXmlElements(defaultDataType); 118 } 119 120 ListMultimap<String, String> rules = new ListMultimap<String, String>(); 121 for (Element child : XmlUtils.toElementList(value.getElement().getChildNodes())) { 122 if (!XCalNamespaceContext.XCAL_NS.equals(child.getNamespaceURI())) { 123 continue; 124 } 125 126 String name = child.getLocalName().toUpperCase(); 127 String text = child.getTextContent(); 128 rules.put(name, text); 129 } 130 131 Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); 132 133 parseFreq(rules, builder, warnings); 134 parseUntil(rules, builder, warnings); 135 parseCount(rules, builder, warnings); 136 parseInterval(rules, builder, warnings); 137 parseBySecond(rules, builder, warnings); 138 parseByMinute(rules, builder, warnings); 139 parseByHour(rules, builder, warnings); 140 parseByDay(rules, builder, warnings); 141 parseByMonthDay(rules, builder, warnings); 142 parseByYearDay(rules, builder, warnings); 143 parseByWeekNo(rules, builder, warnings); 144 parseByMonth(rules, builder, warnings); 145 parseBySetPos(rules, builder, warnings); 146 parseWkst(rules, builder, warnings); 147 parseXRules(rules, builder, warnings); //must be called last 148 149 return newInstance(builder.build()); 150 } 151 152 @Override 153 protected JCalValue _writeJson(T property) { 154 Recurrence recur = property.getValue(); 155 if (recur == null) { 156 return JCalValue.object(new ListMultimap<String, Object>(0)); 157 } 158 159 ListMultimap<String, Object> components = buildComponents(recur, true); 160 161 //lower-case all the keys 162 ListMultimap<String, Object> object = new ListMultimap<String, Object>(components.keySet().size()); 163 for (Map.Entry<String, List<Object>> entry : components) { 164 String key = entry.getKey().toLowerCase(); 165 object.putAll(key, entry.getValue()); 166 } 167 168 return JCalValue.object(object); 169 } 170 171 @Override 172 protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, List<String> warnings) { 173 Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); 174 175 //upper-case the keys 176 ListMultimap<String, String> object = value.asObject(); 177 ListMultimap<String, String> rules = new ListMultimap<String, String>(object.keySet().size()); 178 for (Map.Entry<String, List<String>> entry : object) { 179 String key = entry.getKey().toUpperCase(); 180 rules.putAll(key, entry.getValue()); 181 } 182 183 parseFreq(rules, builder, warnings); 184 parseUntil(rules, builder, warnings); 185 parseCount(rules, builder, warnings); 186 parseInterval(rules, builder, warnings); 187 parseBySecond(rules, builder, warnings); 188 parseByMinute(rules, builder, warnings); 189 parseByHour(rules, builder, warnings); 190 parseByDay(rules, builder, warnings); 191 parseByMonthDay(rules, builder, warnings); 192 parseByYearDay(rules, builder, warnings); 193 parseByWeekNo(rules, builder, warnings); 194 parseByMonth(rules, builder, warnings); 195 parseBySetPos(rules, builder, warnings); 196 parseWkst(rules, builder, warnings); 197 parseXRules(rules, builder, warnings); //must be called last 198 199 return newInstance(builder.build()); 200 } 201 202 /** 203 * Creates a new instance of the recurrence property. 204 * @param recur the recurrence value 205 * @return the new instance 206 */ 207 protected abstract T newInstance(Recurrence recur); 208 209 private void parseFreq(ListMultimap<String, String> rules, Recurrence.Builder builder, List<String> warnings) { 210 List<String> values = rules.removeAll("FREQ"); 211 if (values.isEmpty()) { 212 return; 213 } 214 215 String value = values.get(0); 216 try { 217 builder.frequency(Frequency.valueOf(value.toUpperCase())); 218 } catch (IllegalArgumentException e) { 219 warnings.add("Unable to parse FREQ value: " + value); 220 } 221 } 222 223 private void parseUntil(ListMultimap<String, String> rules, Recurrence.Builder builder, List<String> warnings) { 224 List<String> values = rules.removeAll("UNTIL"); 225 if (values.isEmpty()) { 226 return; 227 } 228 229 String value = values.get(0); 230 try { 231 Date date = date(value).parse(); 232 boolean hasTime = ICalDateFormatter.dateHasTime(value); 233 builder.until(date, hasTime); 234 } catch (IllegalArgumentException e) { 235 warnings.add("Unable to parse UNTIL value: " + value); 236 } 237 } 238 239 private void parseCount(ListMultimap<String, String> rules, Recurrence.Builder builder, List<String> warnings) { 240 List<String> values = rules.removeAll("COUNT"); 241 if (values.isEmpty()) { 242 return; 243 } 244 245 String value = values.get(0); 246 try { 247 builder.count(Integer.valueOf(value)); 248 } catch (NumberFormatException e) { 249 warnings.add("Unable to parse COUNT value: " + value); 250 } 251 } 252 253 private void parseInterval(ListMultimap<String, String> rules, Recurrence.Builder builder, List<String> warnings) { 254 List<String> values = rules.removeAll("INTERVAL"); 255 if (values.isEmpty()) { 256 return; 257 } 258 259 String value = values.get(0); 260 try { 261 builder.interval(Integer.valueOf(value)); 262 } catch (NumberFormatException e) { 263 warnings.add("Unable to parse INTERVAL value: " + value); 264 } 265 } 266 267 private void parseBySecond(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<String> warnings) { 268 parseIntegerList("BYSECOND", rules.removeAll("BYSECOND"), warnings, new ListHandler() { 269 public void handle(Integer value) { 270 builder.bySecond(value); 271 } 272 }); 273 } 274 275 private void parseByMinute(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<String> warnings) { 276 parseIntegerList("BYMINUTE", rules.removeAll("BYMINUTE"), warnings, new ListHandler() { 277 public void handle(Integer value) { 278 builder.byMinute(value); 279 } 280 }); 281 } 282 283 private void parseByHour(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<String> warnings) { 284 parseIntegerList("BYHOUR", rules.removeAll("BYHOUR"), warnings, new ListHandler() { 285 public void handle(Integer value) { 286 builder.byHour(value); 287 } 288 }); 289 } 290 291 private void parseByDay(ListMultimap<String, String> rules, Recurrence.Builder builder, List<String> warnings) { 292 Pattern p = Pattern.compile("^([-+]?\\d+)?(.*)$"); 293 for (String value : rules.removeAll("BYDAY")) { 294 Matcher m = p.matcher(value); 295 if (!m.find()) { 296 //this should never happen 297 //the regex contains a "match-all" pattern and should never not find anything 298 warnings.add("Unable to parse BYDAY value (invalid format): " + value); 299 continue; 300 } 301 302 String dayStr = m.group(2); 303 DayOfWeek day = DayOfWeek.valueOfAbbr(dayStr); 304 if (day == null) { 305 warnings.add("Unable to parse BYDAY value (invalid day of the week): " + value); 306 continue; 307 } 308 309 String prefixStr = m.group(1); 310 Integer prefix = (prefixStr == null) ? null : Integer.valueOf(prefixStr); //no need to catch NumberFormatException because the regex guarantees that it will be a number 311 312 builder.byDay(prefix, day); 313 } 314 } 315 316 private void parseByMonthDay(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<String> warnings) { 317 parseIntegerList("BYMONTHDAY", rules.removeAll("BYMONTHDAY"), warnings, new ListHandler() { 318 public void handle(Integer value) { 319 builder.byMonthDay(value); 320 } 321 }); 322 } 323 324 private void parseByYearDay(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<String> warnings) { 325 parseIntegerList("BYYEARDAY", rules.removeAll("BYYEARDAY"), warnings, new ListHandler() { 326 public void handle(Integer value) { 327 builder.byYearDay(value); 328 } 329 }); 330 } 331 332 private void parseByWeekNo(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<String> warnings) { 333 parseIntegerList("BYWEEKNO", rules.removeAll("BYWEEKNO"), warnings, new ListHandler() { 334 public void handle(Integer value) { 335 builder.byWeekNo(value); 336 } 337 }); 338 } 339 340 private void parseByMonth(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<String> warnings) { 341 parseIntegerList("BYMONTH", rules.removeAll("BYMONTH"), warnings, new ListHandler() { 342 public void handle(Integer value) { 343 builder.byMonth(value); 344 } 345 }); 346 } 347 348 private void parseBySetPos(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<String> warnings) { 349 parseIntegerList("BYSETPOS", rules.removeAll("BYSETPOS"), warnings, new ListHandler() { 350 public void handle(Integer value) { 351 builder.bySetPos(value); 352 } 353 }); 354 } 355 356 private void parseWkst(ListMultimap<String, String> rules, Recurrence.Builder builder, List<String> warnings) { 357 List<String> values = rules.removeAll("WKST"); 358 if (values.isEmpty()) { 359 return; 360 } 361 362 String value = values.get(0); 363 DayOfWeek day = DayOfWeek.valueOfAbbr(value); 364 if (day == null) { 365 warnings.add("Unable to parse WKST (invalid day of the week): " + value); 366 return; 367 } 368 369 builder.workweekStarts(day); 370 } 371 372 private void parseXRules(ListMultimap<String, String> rules, Recurrence.Builder builder, List<String> warnings) { 373 for (Map.Entry<String, List<String>> rule : rules) { 374 String name = rule.getKey(); 375 for (String value : rule.getValue()) { 376 builder.xrule(name, value); 377 } 378 } 379 } 380 381 private ListMultimap<String, Object> buildComponents(Recurrence recur, boolean extended) { 382 ListMultimap<String, Object> components = new ListMultimap<String, Object>(); 383 384 //FREQ must come first 385 if (recur.getFrequency() != null) { 386 components.put("FREQ", recur.getFrequency().name()); 387 } 388 389 if (recur.getUntil() != null) { 390 String s = date(recur.getUntil()).time(recur.hasTimeUntilDate()).extended(extended).write(); 391 components.put("UNTIL", s); 392 } 393 394 if (recur.getCount() != null) { 395 components.put("COUNT", recur.getCount()); 396 } 397 398 if (recur.getInterval() != null) { 399 components.put("INTERVAL", recur.getInterval()); 400 } 401 402 addIntegerListComponent(components, "BYSECOND", recur.getBySecond()); 403 addIntegerListComponent(components, "BYMINUTE", recur.getByMinute()); 404 addIntegerListComponent(components, "BYHOUR", recur.getByHour()); 405 406 Iterator<Integer> prefixIt = recur.getByDayPrefixes().iterator(); 407 Iterator<DayOfWeek> dayIt = recur.getByDay().iterator(); 408 while (prefixIt.hasNext() && dayIt.hasNext()) { 409 Integer prefix = prefixIt.next(); 410 DayOfWeek day = dayIt.next(); 411 412 String value = day.getAbbr(); 413 if (prefix != null) { 414 value = prefix + value; 415 } 416 components.put("BYDAY", value); 417 } 418 419 addIntegerListComponent(components, "BYMONTHDAY", recur.getByMonthDay()); 420 addIntegerListComponent(components, "BYYEARDAY", recur.getByYearDay()); 421 addIntegerListComponent(components, "BYWEEKNO", recur.getByWeekNo()); 422 addIntegerListComponent(components, "BYMONTH", recur.getByMonth()); 423 addIntegerListComponent(components, "BYSETPOS", recur.getBySetPos()); 424 425 if (recur.getWorkweekStarts() != null) { 426 components.put("WKST", recur.getWorkweekStarts().getAbbr()); 427 } 428 429 for (Map.Entry<String, List<String>> entry : recur.getXRules().entrySet()) { 430 String name = entry.getKey(); 431 for (String value : entry.getValue()) { 432 components.put(name, value); 433 } 434 } 435 436 return components; 437 } 438 439 private void addIntegerListComponent(ListMultimap<String, Object> components, String name, List<Integer> values) { 440 for (Integer value : values) { 441 components.put(name, value); 442 } 443 } 444 445 private void parseIntegerList(String name, List<String> values, List<String> warnings, ListHandler handler) { 446 for (String value : values) { 447 try { 448 handler.handle(Integer.valueOf(value)); 449 } catch (NumberFormatException e) { 450 warnings.add("Ignoring non-numeric value found in " + name + ": " + value); 451 } 452 } 453 } 454 455 private static interface ListHandler { 456 void handle(Integer value); 457 } 458 }