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 }