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