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    }