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    }