001package biweekly.io.scribe.property;
002
003import java.util.Date;
004import java.util.Iterator;
005import java.util.List;
006import java.util.Map;
007import java.util.regex.Matcher;
008import java.util.regex.Pattern;
009
010import org.w3c.dom.Element;
011
012import biweekly.ICalDataType;
013import biweekly.Warning;
014import biweekly.io.json.JCalValue;
015import biweekly.io.xml.XCalElement;
016import biweekly.io.xml.XCalNamespaceContext;
017import biweekly.parameter.ICalParameters;
018import biweekly.property.RecurrenceProperty;
019import biweekly.util.ICalDateFormat;
020import biweekly.util.ListMultimap;
021import biweekly.util.Recurrence;
022import biweekly.util.Recurrence.DayOfWeek;
023import biweekly.util.Recurrence.Frequency;
024import 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 */
056public abstract class RecurrencePropertyScribe<T extends RecurrenceProperty> extends ICalPropertyScribe<T> {
057        private static final String FREQ = "FREQ";
058        private static final String UNTIL = "UNTIL";
059        private static final String COUNT = "COUNT";
060        private static final String INTERVAL = "INTERVAL";
061        private static final String BYSECOND = "BYSECOND";
062        private static final String BYMINUTE = "BYMINUTE";
063        private static final String BYHOUR = "BYHOUR";
064        private static final String BYDAY = "BYDAY";
065        private static final String BYMONTHDAY = "BYMONTHDAY";
066        private static final String BYYEARDAY = "BYYEARDAY";
067        private static final String BYWEEKNO = "BYWEEKNO";
068        private static final String BYMONTH = "BYMONTH";
069        private static final String BYSETPOS = "BYSETPOS";
070        private static final String WKST = "WKST";
071
072        public RecurrencePropertyScribe(Class<T> clazz, String propertyName) {
073                super(clazz, propertyName, ICalDataType.RECUR);
074        }
075
076        @Override
077        protected String _writeText(T property) {
078                Recurrence recur = property.getValue();
079                if (recur == null) {
080                        return "";
081                }
082
083                ListMultimap<String, Object> components = buildComponents(recur, false);
084                return object(components.getMap());
085        }
086
087        @Override
088        protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, List<Warning> warnings) {
089                Recurrence.Builder builder = new Recurrence.Builder((Frequency) null);
090                ListMultimap<String, String> rules = object(value);
091
092                parseFreq(rules, builder, warnings);
093                parseUntil(rules, builder, warnings);
094                parseCount(rules, builder, warnings);
095                parseInterval(rules, builder, warnings);
096                parseBySecond(rules, builder, warnings);
097                parseByMinute(rules, builder, warnings);
098                parseByHour(rules, builder, warnings);
099                parseByDay(rules, builder, warnings);
100                parseByMonthDay(rules, builder, warnings);
101                parseByYearDay(rules, builder, warnings);
102                parseByWeekNo(rules, builder, warnings);
103                parseByMonth(rules, builder, warnings);
104                parseBySetPos(rules, builder, warnings);
105                parseWkst(rules, builder, warnings);
106                parseXRules(rules, builder, warnings); //must be called last
107
108                return newInstance(builder.build());
109        }
110
111        @Override
112        protected void _writeXml(T property, XCalElement element) {
113                XCalElement recurElement = element.append(dataType(property));
114
115                Recurrence recur = property.getValue();
116                if (recur == null) {
117                        return;
118                }
119
120                ListMultimap<String, Object> components = buildComponents(recur, true);
121                for (Map.Entry<String, List<Object>> component : components) {
122                        String name = component.getKey().toLowerCase();
123                        for (Object value : component.getValue()) {
124                                recurElement.append(name, value.toString());
125                        }
126                }
127        }
128
129        @Override
130        protected T _parseXml(XCalElement element, ICalParameters parameters, List<Warning> warnings) {
131                XCalElement value = element.child(defaultDataType);
132                if (value == null) {
133                        throw missingXmlElements(defaultDataType);
134                }
135
136                ListMultimap<String, String> rules = new ListMultimap<String, String>();
137                for (Element child : XmlUtils.toElementList(value.getElement().getChildNodes())) {
138                        if (!XCalNamespaceContext.XCAL_NS.equals(child.getNamespaceURI())) {
139                                continue;
140                        }
141
142                        String name = child.getLocalName().toUpperCase();
143                        String text = child.getTextContent();
144                        rules.put(name, text);
145                }
146
147                Recurrence.Builder builder = new Recurrence.Builder((Frequency) null);
148
149                parseFreq(rules, builder, warnings);
150                parseUntil(rules, builder, warnings);
151                parseCount(rules, builder, warnings);
152                parseInterval(rules, builder, warnings);
153                parseBySecond(rules, builder, warnings);
154                parseByMinute(rules, builder, warnings);
155                parseByHour(rules, builder, warnings);
156                parseByDay(rules, builder, warnings);
157                parseByMonthDay(rules, builder, warnings);
158                parseByYearDay(rules, builder, warnings);
159                parseByWeekNo(rules, builder, warnings);
160                parseByMonth(rules, builder, warnings);
161                parseBySetPos(rules, builder, warnings);
162                parseWkst(rules, builder, warnings);
163                parseXRules(rules, builder, warnings); //must be called last
164
165                return newInstance(builder.build());
166        }
167
168        @Override
169        protected JCalValue _writeJson(T property) {
170                Recurrence recur = property.getValue();
171                if (recur == null) {
172                        return JCalValue.object(new ListMultimap<String, Object>(0));
173                }
174
175                ListMultimap<String, Object> components = buildComponents(recur, true);
176
177                //lower-case all the keys
178                ListMultimap<String, Object> object = new ListMultimap<String, Object>(components.keySet().size());
179                for (Map.Entry<String, List<Object>> entry : components) {
180                        String key = entry.getKey().toLowerCase();
181                        object.putAll(key, entry.getValue());
182                }
183
184                return JCalValue.object(object);
185        }
186
187        @Override
188        protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, List<Warning> warnings) {
189                Recurrence.Builder builder = new Recurrence.Builder((Frequency) null);
190
191                //upper-case the keys
192                ListMultimap<String, String> object = value.asObject();
193                ListMultimap<String, String> rules = new ListMultimap<String, String>(object.keySet().size());
194                for (Map.Entry<String, List<String>> entry : object) {
195                        String key = entry.getKey().toUpperCase();
196                        rules.putAll(key, entry.getValue());
197                }
198
199                parseFreq(rules, builder, warnings);
200                parseUntil(rules, builder, warnings);
201                parseCount(rules, builder, warnings);
202                parseInterval(rules, builder, warnings);
203                parseBySecond(rules, builder, warnings);
204                parseByMinute(rules, builder, warnings);
205                parseByHour(rules, builder, warnings);
206                parseByDay(rules, builder, warnings);
207                parseByMonthDay(rules, builder, warnings);
208                parseByYearDay(rules, builder, warnings);
209                parseByWeekNo(rules, builder, warnings);
210                parseByMonth(rules, builder, warnings);
211                parseBySetPos(rules, builder, warnings);
212                parseWkst(rules, builder, warnings);
213                parseXRules(rules, builder, warnings); //must be called last
214
215                return newInstance(builder.build());
216        }
217
218        /**
219         * Creates a new instance of the recurrence property.
220         * @param recur the recurrence value
221         * @return the new instance
222         */
223        protected abstract T newInstance(Recurrence recur);
224
225        private void parseFreq(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) {
226                parseFirst(rules, FREQ, new Handler<String>() {
227                        public void handle(String value) {
228                                value = value.toUpperCase();
229                                try {
230                                        Frequency frequency = Frequency.valueOf(value);
231                                        builder.frequency(frequency);
232                                } catch (IllegalArgumentException e) {
233                                        warnings.add(Warning.parse(7, FREQ, value));
234                                }
235                        }
236                });
237        }
238
239        private void parseUntil(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) {
240                parseFirst(rules, UNTIL, new Handler<String>() {
241                        public void handle(String value) {
242                                try {
243                                        Date date = date(value).parse();
244                                        boolean hasTime = ICalDateFormat.dateHasTime(value);
245                                        builder.until(date, hasTime);
246                                } catch (IllegalArgumentException e) {
247                                        warnings.add(Warning.parse(7, UNTIL, value));
248                                }
249                        }
250                });
251        }
252
253        private void parseCount(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) {
254                parseFirst(rules, COUNT, new Handler<String>() {
255                        public void handle(String value) {
256                                try {
257                                        builder.count(Integer.valueOf(value));
258                                } catch (NumberFormatException e) {
259                                        warnings.add(Warning.parse(7, COUNT, value));
260                                }
261                        }
262                });
263        }
264
265        private void parseInterval(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) {
266                parseFirst(rules, INTERVAL, new Handler<String>() {
267                        public void handle(String value) {
268                                try {
269                                        builder.interval(Integer.valueOf(value));
270                                } catch (NumberFormatException e) {
271                                        warnings.add(Warning.parse(7, INTERVAL, value));
272                                }
273                        }
274                });
275        }
276
277        private void parseBySecond(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
278                parseIntegerList(BYSECOND, rules, warnings, new Handler<Integer>() {
279                        public void handle(Integer value) {
280                                builder.bySecond(value);
281                        }
282                });
283        }
284
285        private void parseByMinute(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
286                parseIntegerList(BYMINUTE, rules, warnings, new Handler<Integer>() {
287                        public void handle(Integer value) {
288                                builder.byMinute(value);
289                        }
290                });
291        }
292
293        private void parseByHour(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
294                parseIntegerList(BYHOUR, rules, warnings, new Handler<Integer>() {
295                        public void handle(Integer value) {
296                                builder.byHour(value);
297                        }
298                });
299        }
300
301        private void parseByDay(ListMultimap<String, String> rules, Recurrence.Builder builder, List<Warning> warnings) {
302                Pattern p = Pattern.compile("^([-+]?\\d+)?(.*)$");
303                for (String value : rules.removeAll(BYDAY)) {
304                        Matcher m = p.matcher(value);
305                        if (!m.find()) {
306                                //this should never happen
307                                //the regex contains a "match-all" pattern and should never not find anything
308                                warnings.add(Warning.parse(7, BYDAY, value));
309                                continue;
310                        }
311
312                        String dayStr = m.group(2);
313                        DayOfWeek day = DayOfWeek.valueOfAbbr(dayStr);
314                        if (day == null) {
315                                warnings.add(Warning.parse(7, BYDAY, value));
316                                continue;
317                        }
318
319                        String prefixStr = m.group(1);
320                        Integer prefix = (prefixStr == null) ? null : Integer.valueOf(prefixStr); //no need to catch NumberFormatException because the regex guarantees that it will be a number
321
322                        builder.byDay(prefix, day);
323                }
324        }
325
326        private void parseByMonthDay(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
327                parseIntegerList(BYMONTHDAY, rules, warnings, new Handler<Integer>() {
328                        public void handle(Integer value) {
329                                builder.byMonthDay(value);
330                        }
331                });
332        }
333
334        private void parseByYearDay(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
335                parseIntegerList(BYYEARDAY, rules, warnings, new Handler<Integer>() {
336                        public void handle(Integer value) {
337                                builder.byYearDay(value);
338                        }
339                });
340        }
341
342        private void parseByWeekNo(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
343                parseIntegerList(BYWEEKNO, rules, warnings, new Handler<Integer>() {
344                        public void handle(Integer value) {
345                                builder.byWeekNo(value);
346                        }
347                });
348        }
349
350        private void parseByMonth(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
351                parseIntegerList(BYMONTH, rules, warnings, new Handler<Integer>() {
352                        public void handle(Integer value) {
353                                builder.byMonth(value);
354                        }
355                });
356        }
357
358        private void parseBySetPos(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
359                parseIntegerList(BYSETPOS, rules, warnings, new Handler<Integer>() {
360                        public void handle(Integer value) {
361                                builder.bySetPos(value);
362                        }
363                });
364        }
365
366        private void parseWkst(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) {
367                parseFirst(rules, WKST, new Handler<String>() {
368                        public void handle(String value) {
369                                DayOfWeek day = DayOfWeek.valueOfAbbr(value);
370                                if (day == null) {
371                                        warnings.add(Warning.parse(7, WKST, value));
372                                        return;
373                                }
374
375                                builder.workweekStarts(day);
376                        }
377                });
378        }
379
380        private void parseXRules(ListMultimap<String, String> rules, Recurrence.Builder builder, List<Warning> warnings) {
381                for (Map.Entry<String, List<String>> rule : rules) {
382                        String name = rule.getKey();
383                        for (String value : rule.getValue()) {
384                                builder.xrule(name, value);
385                        }
386                }
387        }
388
389        private ListMultimap<String, Object> buildComponents(Recurrence recur, boolean extended) {
390                ListMultimap<String, Object> components = new ListMultimap<String, Object>();
391
392                //FREQ must come first
393                if (recur.getFrequency() != null) {
394                        components.put(FREQ, recur.getFrequency().name());
395                }
396
397                if (recur.getUntil() != null) {
398                        String s = date(recur.getUntil()).time(recur.hasTimeUntilDate()).extended(extended).write();
399                        components.put(UNTIL, s);
400                }
401
402                if (recur.getCount() != null) {
403                        components.put(COUNT, recur.getCount());
404                }
405
406                if (recur.getInterval() != null) {
407                        components.put(INTERVAL, recur.getInterval());
408                }
409
410                addIntegerListComponent(components, BYSECOND, recur.getBySecond());
411                addIntegerListComponent(components, BYMINUTE, recur.getByMinute());
412                addIntegerListComponent(components, BYHOUR, recur.getByHour());
413
414                Iterator<Integer> prefixIt = recur.getByDayPrefixes().iterator();
415                Iterator<DayOfWeek> dayIt = recur.getByDay().iterator();
416                while (prefixIt.hasNext() && dayIt.hasNext()) {
417                        Integer prefix = prefixIt.next();
418                        DayOfWeek day = dayIt.next();
419
420                        String value = day.getAbbr();
421                        if (prefix != null) {
422                                value = prefix + value;
423                        }
424                        components.put(BYDAY, value);
425                }
426
427                addIntegerListComponent(components, BYMONTHDAY, recur.getByMonthDay());
428                addIntegerListComponent(components, BYYEARDAY, recur.getByYearDay());
429                addIntegerListComponent(components, BYWEEKNO, recur.getByWeekNo());
430                addIntegerListComponent(components, BYMONTH, recur.getByMonth());
431                addIntegerListComponent(components, BYSETPOS, recur.getBySetPos());
432
433                if (recur.getWorkweekStarts() != null) {
434                        components.put(WKST, recur.getWorkweekStarts().getAbbr());
435                }
436
437                for (Map.Entry<String, List<String>> entry : recur.getXRules().entrySet()) {
438                        String name = entry.getKey();
439                        for (String value : entry.getValue()) {
440                                components.put(name, value);
441                        }
442                }
443
444                return components;
445        }
446
447        private void addIntegerListComponent(ListMultimap<String, Object> components, String name, List<Integer> values) {
448                for (Integer value : values) {
449                        components.put(name, value);
450                }
451        }
452
453        private void parseFirst(ListMultimap<String, String> rules, String name, Handler<String> handler) {
454                List<String> values = rules.removeAll(name);
455                if (values.isEmpty()) {
456                        return;
457                }
458
459                String value = values.get(0);
460                handler.handle(value);
461        }
462
463        private void parseIntegerList(String name, ListMultimap<String, String> rules, List<Warning> warnings, Handler<Integer> handler) {
464                List<String> values = rules.removeAll(name);
465                for (String value : values) {
466                        try {
467                                handler.handle(Integer.valueOf(value));
468                        } catch (NumberFormatException e) {
469                                warnings.add(Warning.parse(8, name, value));
470                        }
471                }
472        }
473
474        private interface Handler<T> {
475                void handle(T value);
476        }
477}