001package biweekly.io.scribe.property;
002
003import java.util.ArrayList;
004import java.util.Arrays;
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.ICalVersion;
014import biweekly.Warning;
015import biweekly.io.CannotParseException;
016import biweekly.io.ParseContext;
017import biweekly.io.WriteContext;
018import biweekly.io.json.JCalValue;
019import biweekly.io.xml.XCalElement;
020import biweekly.io.xml.XCalNamespaceContext;
021import biweekly.parameter.ICalParameters;
022import biweekly.property.RecurrenceProperty;
023import biweekly.util.ICalDate;
024import biweekly.util.ListMultimap;
025import biweekly.util.Recurrence;
026import biweekly.util.Recurrence.ByDay;
027import biweekly.util.Recurrence.DayOfWeek;
028import biweekly.util.Recurrence.Frequency;
029import biweekly.util.XmlUtils;
030
031/*
032 Copyright (c) 2013-2015, Michael Angstadt
033 All rights reserved.
034
035 Redistribution and use in source and binary forms, with or without
036 modification, are permitted provided that the following conditions are met: 
037
038 1. Redistributions of source code must retain the above copyright notice, this
039 list of conditions and the following disclaimer. 
040 2. Redistributions in binary form must reproduce the above copyright notice,
041 this list of conditions and the following disclaimer in the documentation
042 and/or other materials provided with the distribution. 
043
044 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
045 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
046 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
047 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
048 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
049 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
050 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
051 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
052 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
053 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
054 */
055
056/**
057 * Marshals properties whose values are {@link Recurrence}.
058 * @param <T> the property class
059 * @author Michael Angstadt
060 */
061public abstract class RecurrencePropertyScribe<T extends RecurrenceProperty> extends ICalPropertyScribe<T> {
062        private static final String FREQ = "FREQ";
063        private static final String UNTIL = "UNTIL";
064        private static final String COUNT = "COUNT";
065        private static final String INTERVAL = "INTERVAL";
066        private static final String BYSECOND = "BYSECOND";
067        private static final String BYMINUTE = "BYMINUTE";
068        private static final String BYHOUR = "BYHOUR";
069        private static final String BYDAY = "BYDAY";
070        private static final String BYMONTHDAY = "BYMONTHDAY";
071        private static final String BYYEARDAY = "BYYEARDAY";
072        private static final String BYWEEKNO = "BYWEEKNO";
073        private static final String BYMONTH = "BYMONTH";
074        private static final String BYSETPOS = "BYSETPOS";
075        private static final String WKST = "WKST";
076
077        public RecurrencePropertyScribe(Class<T> clazz, String propertyName) {
078                super(clazz, propertyName);
079        }
080
081        @Override
082        protected ICalDataType _defaultDataType(ICalVersion version) {
083                return ICalDataType.RECUR;
084        }
085
086        @Override
087        protected ICalParameters _prepareParameters(T property, WriteContext context) {
088                if (isInObservance(context)) {
089                        return property.getParameters();
090                }
091
092                Recurrence recur = property.getValue();
093                if (recur == null || recur.getUntil() == null) {
094                        return property.getParameters();
095                }
096
097                return handleTzidParameter(property, recur.getUntil().hasTime(), context);
098        }
099
100        @Override
101        protected String _writeText(T property, WriteContext context) {
102                //null value
103                Recurrence recur = property.getValue();
104                if (recur == null) {
105                        return "";
106                }
107
108                //iCal 2.0
109                if (context.getVersion() != ICalVersion.V1_0) {
110                        ListMultimap<String, Object> components = buildComponents(property, context, false);
111                        return object(components.getMap());
112                }
113
114                //vCal 1.0
115                Frequency frequency = recur.getFrequency();
116                if (frequency == null) {
117                        return "";
118                }
119
120                StringBuilder sb = new StringBuilder();
121
122                Integer interval = recur.getInterval();
123                if (interval == null) {
124                        interval = 1;
125                }
126
127                switch (frequency) {
128                case YEARLY:
129                        if (!recur.getByMonth().isEmpty()) {
130                                sb.append("YM").append(interval);
131                                for (Integer month : recur.getByMonth()) {
132                                        sb.append(' ').append(month);
133                                }
134                        } else {
135                                sb.append("YD").append(interval);
136                                for (Integer day : recur.getByYearDay()) {
137                                        sb.append(' ').append(day);
138                                }
139                        }
140                        break;
141
142                case MONTHLY:
143                        if (!recur.getByMonthDay().isEmpty()) {
144                                sb.append("MD").append(interval);
145                                for (Integer day : recur.getByMonthDay()) {
146                                        sb.append(' ').append(writeVCalInt(day));
147                                }
148                        } else {
149                                sb.append("MP").append(interval);
150                                for (ByDay byDay : recur.getByDay()) {
151                                        DayOfWeek day = byDay.getDay();
152                                        Integer prefix = byDay.getNum();
153                                        if (prefix == null) {
154                                                prefix = 1;
155                                        }
156
157                                        sb.append(' ').append(writeVCalInt(prefix)).append(' ').append(day.getAbbr());
158                                }
159                        }
160                        break;
161
162                case WEEKLY:
163                        sb.append("W").append(interval);
164                        for (ByDay byDay : recur.getByDay()) {
165                                sb.append(' ').append(byDay.getDay().getAbbr());
166                        }
167                        break;
168
169                case DAILY:
170                        sb.append("D").append(interval);
171                        break;
172
173                case HOURLY:
174                        sb.append("M").append(interval * 60);
175                        break;
176
177                case MINUTELY:
178                        sb.append("M").append(interval);
179                        break;
180
181                default:
182                        return "";
183                }
184
185                Integer count = recur.getCount();
186                ICalDate until = recur.getUntil();
187                sb.append(' ');
188
189                if (count != null) {
190                        sb.append('#').append(count);
191                } else if (until != null) {
192                        String dateStr = date(until, property, context).extended(false).write();
193                        sb.append(dateStr);
194                } else {
195                        sb.append("#0");
196                }
197
198                return sb.toString();
199        }
200
201        @Override
202        protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) {
203                final Recurrence.Builder builder = new Recurrence.Builder((Frequency) null);
204
205                if (value.length() == 0) {
206                        return newInstance(builder.build());
207                }
208
209                if (context.getVersion() != ICalVersion.V1_0) {
210                        ListMultimap<String, String> rules = object(value);
211
212                        List<Warning> warnings = context.getWarnings();
213                        parseFreq(rules, builder, warnings);
214                        parseUntil(rules, builder, warnings);
215                        parseCount(rules, builder, warnings);
216                        parseInterval(rules, builder, warnings);
217                        parseBySecond(rules, builder, warnings);
218                        parseByMinute(rules, builder, warnings);
219                        parseByHour(rules, builder, warnings);
220                        parseByDay(rules, builder, warnings);
221                        parseByMonthDay(rules, builder, warnings);
222                        parseByYearDay(rules, builder, warnings);
223                        parseByWeekNo(rules, builder, warnings);
224                        parseByMonth(rules, builder, warnings);
225                        parseBySetPos(rules, builder, warnings);
226                        parseWkst(rules, builder, warnings);
227                        parseXRules(rules, builder, warnings); //must be called last
228
229                        T property = newInstance(builder.build());
230
231                        ICalDate until = property.getValue().getUntil();
232                        if (until != null) {
233                                context.addDate(until, property, parameters);
234                        }
235
236                        return property;
237                }
238
239                String splitValues[] = value.toUpperCase().split("\\s+");
240
241                //parse the frequency and interval from the first token (e.g. "W2")
242                String frequencyStr;
243                Integer interval;
244                {
245                        String firstToken = splitValues[0];
246                        Pattern p = Pattern.compile("^([A-Z]+)(\\d+)$");
247                        Matcher m = p.matcher(firstToken);
248                        if (!m.find()) {
249                                throw new CannotParseException("Invalid token: " + firstToken);
250                        }
251
252                        frequencyStr = m.group(1);
253                        interval = Integer.valueOf(m.group(2));
254                        splitValues = Arrays.copyOfRange(splitValues, 1, splitValues.length);
255                }
256                builder.interval(interval);
257
258                Integer count = null;
259                ICalDate until = null;
260                if (splitValues.length == 0) {
261                        count = 2;
262                } else {
263                        String lastToken = splitValues[splitValues.length - 1];
264                        if (lastToken.startsWith("#")) {
265                                String countStr = lastToken.substring(1, lastToken.length());
266                                count = Integer.valueOf(countStr);
267                                if (count == 0) {
268                                        //infinite
269                                        count = null;
270                                }
271
272                                splitValues = Arrays.copyOfRange(splitValues, 0, splitValues.length - 1);
273                        } else {
274                                try {
275                                        //see if the value is an "until" date
276                                        until = date(lastToken).parse();
277                                        splitValues = Arrays.copyOfRange(splitValues, 0, splitValues.length - 1);
278                                } catch (IllegalArgumentException e) {
279                                        //last token is a regular value
280                                        count = 2;
281                                }
282                        }
283                }
284                builder.count(count);
285                builder.until(until);
286
287                //determine what frequency enum to use and how to treat each tokenized value
288                Frequency frequency = null;
289                Handler<String> handler = null;
290                if ("YD".equals(frequencyStr)) {
291                        frequency = Frequency.YEARLY;
292                        handler = new Handler<String>() {
293                                public void handle(String value) {
294                                        if (value == null) {
295                                                return;
296                                        }
297
298                                        Integer dayOfYear = Integer.valueOf(value);
299                                        builder.byYearDay(dayOfYear);
300                                }
301                        };
302                } else if ("YM".equals(frequencyStr)) {
303                        frequency = Frequency.YEARLY;
304                        handler = new Handler<String>() {
305                                public void handle(String value) {
306                                        if (value == null) {
307                                                return;
308                                        }
309
310                                        Integer month = Integer.valueOf(value);
311                                        builder.byMonth(month);
312                                }
313                        };
314                } else if ("MD".equals(frequencyStr)) {
315                        frequency = Frequency.MONTHLY;
316                        handler = new Handler<String>() {
317                                public void handle(String value) {
318                                        if (value == null) {
319                                                return;
320                                        }
321
322                                        Integer date = "LD".equals(value) ? -1 : parseVCalInt(value);
323                                        builder.byMonthDay(date);
324                                }
325                        };
326                } else if ("MP".equals(frequencyStr)) {
327                        frequency = Frequency.MONTHLY;
328                        handler = new Handler<String>() {
329                                private final List<Integer> nums = new ArrayList<Integer>();
330                                private final List<DayOfWeek> days = new ArrayList<DayOfWeek>();
331                                private boolean readNum = false;
332
333                                public void handle(String value) {
334                                        if (value == null) {
335                                                //end of list
336                                                for (Integer num : nums) {
337                                                        for (DayOfWeek day : days) {
338                                                                builder.byDay(num, day);
339                                                        }
340                                                }
341                                                return;
342                                        }
343
344                                        if (value.matches("\\d{4}")) {
345                                                readNum = false;
346
347                                                Integer hour = Integer.valueOf(value.substring(0, 2));
348                                                builder.byHour(hour);
349
350                                                Integer minute = Integer.valueOf(value.substring(2, 4));
351                                                builder.byMinute(minute);
352                                                return;
353                                        }
354
355                                        try {
356                                                Integer curNum = parseVCalInt(value);
357
358                                                if (!readNum) {
359                                                        //reset lists, new segment
360                                                        for (Integer num : nums) {
361                                                                for (DayOfWeek day : days) {
362                                                                        builder.byDay(num, day);
363                                                                }
364                                                        }
365                                                        nums.clear();
366                                                        days.clear();
367
368                                                        readNum = true;
369                                                }
370
371                                                nums.add(curNum);
372                                        } catch (NumberFormatException e) {
373                                                readNum = false;
374
375                                                DayOfWeek day = parseDay(value);
376                                                days.add(day);
377                                        }
378                                }
379                        };
380                } else if ("W".equals(frequencyStr)) {
381                        frequency = Frequency.WEEKLY;
382                        handler = new Handler<String>() {
383                                public void handle(String value) {
384                                        if (value == null) {
385                                                return;
386                                        }
387
388                                        DayOfWeek day = parseDay(value);
389                                        builder.byDay(day);
390                                }
391                        };
392                } else if ("D".equals(frequencyStr)) {
393                        frequency = Frequency.DAILY;
394                        handler = new Handler<String>() {
395                                public void handle(String value) {
396                                        if (value == null) {
397                                                return;
398                                        }
399
400                                        Integer hour = Integer.valueOf(value.substring(0, 2));
401                                        builder.byHour(hour);
402
403                                        Integer minute = Integer.valueOf(value.substring(2, 4));
404                                        builder.byMinute(minute);
405                                }
406                        };
407                } else if ("M".equals(frequencyStr)) {
408                        frequency = Frequency.MINUTELY;
409                        handler = new Handler<String>() {
410                                public void handle(String value) {
411                                        //TODO can this ever have values?
412                                }
413                        };
414                } else {
415                        throw new CannotParseException("Unrecognized frequency: " + frequencyStr);
416                }
417
418                builder.frequency(frequency);
419
420                //parse the rest of the tokens
421                for (String splitValue : splitValues) {
422                        //TODO not sure how to handle the "$" symbol, ignore it
423                        if (splitValue.endsWith("$")) {
424                                context.addWarning(36, splitValue);
425                                splitValue = splitValue.substring(0, splitValue.length() - 1);
426                        }
427
428                        handler.handle(splitValue);
429                }
430                handler.handle(null);
431
432                T property = newInstance(builder.build());
433                if (until != null) {
434                        context.addDate(until, property, parameters);
435                }
436
437                return property;
438        }
439
440        private int parseVCalInt(String value) {
441                int negate = 1;
442                if (value.endsWith("+")) {
443                        value = value.substring(0, value.length() - 1);
444                } else if (value.endsWith("-")) {
445                        value = value.substring(0, value.length() - 1);
446                        negate = -1;
447                }
448
449                return Integer.parseInt(value) * negate;
450        }
451
452        private String writeVCalInt(Integer value) {
453                if (value > 0) {
454                        return value + "+";
455                }
456
457                if (value < 0) {
458                        return Math.abs(value) + "-";
459                }
460
461                return value + "";
462        }
463
464        private DayOfWeek parseDay(String value) {
465                DayOfWeek day = DayOfWeek.valueOfAbbr(value);
466                if (day == null) {
467                        throw new CannotParseException("Invalid day: " + value);
468                }
469
470                return day;
471        }
472
473        @Override
474        protected void _writeXml(T property, XCalElement element, WriteContext context) {
475                XCalElement recurElement = element.append(dataType(property, null));
476
477                Recurrence recur = property.getValue();
478                if (recur == null) {
479                        return;
480                }
481
482                ListMultimap<String, Object> components = buildComponents(property, context, true);
483                for (Map.Entry<String, List<Object>> component : components) {
484                        String name = component.getKey().toLowerCase();
485                        for (Object value : component.getValue()) {
486                                recurElement.append(name, value.toString());
487                        }
488                }
489        }
490
491        @Override
492        protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) {
493                ICalDataType dataType = defaultDataType(context.getVersion());
494                XCalElement value = element.child(dataType);
495                if (value == null) {
496                        throw missingXmlElements(dataType);
497                }
498
499                ListMultimap<String, String> rules = new ListMultimap<String, String>();
500                for (Element child : XmlUtils.toElementList(value.getElement().getChildNodes())) {
501                        if (!XCalNamespaceContext.XCAL_NS.equals(child.getNamespaceURI())) {
502                                continue;
503                        }
504
505                        String name = child.getLocalName().toUpperCase();
506                        String text = child.getTextContent();
507                        rules.put(name, text);
508                }
509
510                Recurrence.Builder builder = new Recurrence.Builder((Frequency) null);
511
512                List<Warning> warnings = context.getWarnings();
513                parseFreq(rules, builder, warnings);
514                parseUntil(rules, builder, warnings);
515                parseCount(rules, builder, warnings);
516                parseInterval(rules, builder, warnings);
517                parseBySecond(rules, builder, warnings);
518                parseByMinute(rules, builder, warnings);
519                parseByHour(rules, builder, warnings);
520                parseByDay(rules, builder, warnings);
521                parseByMonthDay(rules, builder, warnings);
522                parseByYearDay(rules, builder, warnings);
523                parseByWeekNo(rules, builder, warnings);
524                parseByMonth(rules, builder, warnings);
525                parseBySetPos(rules, builder, warnings);
526                parseWkst(rules, builder, warnings);
527                parseXRules(rules, builder, warnings); //must be called last
528
529                T property = newInstance(builder.build());
530
531                ICalDate until = property.getValue().getUntil();
532                if (until != null) {
533                        context.addDate(until, property, parameters);
534                }
535
536                return property;
537        }
538
539        @Override
540        protected JCalValue _writeJson(T property, WriteContext context) {
541                Recurrence recur = property.getValue();
542                if (recur == null) {
543                        return JCalValue.object(new ListMultimap<String, Object>(0));
544                }
545
546                ListMultimap<String, Object> components = buildComponents(property, context, true);
547
548                //lower-case all the keys
549                ListMultimap<String, Object> object = new ListMultimap<String, Object>(components.keySet().size());
550                for (Map.Entry<String, List<Object>> entry : components) {
551                        String key = entry.getKey().toLowerCase();
552                        object.putAll(key, entry.getValue());
553                }
554
555                return JCalValue.object(object);
556        }
557
558        @Override
559        protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) {
560                Recurrence.Builder builder = new Recurrence.Builder((Frequency) null);
561
562                //upper-case the keys
563                ListMultimap<String, String> object = value.asObject();
564                ListMultimap<String, String> rules = new ListMultimap<String, String>(object.keySet().size());
565                for (Map.Entry<String, List<String>> entry : object) {
566                        String key = entry.getKey().toUpperCase();
567                        rules.putAll(key, entry.getValue());
568                }
569
570                List<Warning> warnings = context.getWarnings();
571                parseFreq(rules, builder, warnings);
572                parseUntil(rules, builder, warnings);
573                parseCount(rules, builder, warnings);
574                parseInterval(rules, builder, warnings);
575                parseBySecond(rules, builder, warnings);
576                parseByMinute(rules, builder, warnings);
577                parseByHour(rules, builder, warnings);
578                parseByDay(rules, builder, warnings);
579                parseByMonthDay(rules, builder, warnings);
580                parseByYearDay(rules, builder, warnings);
581                parseByWeekNo(rules, builder, warnings);
582                parseByMonth(rules, builder, warnings);
583                parseBySetPos(rules, builder, warnings);
584                parseWkst(rules, builder, warnings);
585                parseXRules(rules, builder, warnings); //must be called last
586
587                T property = newInstance(builder.build());
588
589                ICalDate until = property.getValue().getUntil();
590                if (until != null) {
591                        context.addDate(until, property, parameters);
592                }
593
594                return property;
595        }
596
597        /**
598         * Creates a new instance of the recurrence property.
599         * @param recur the recurrence value
600         * @return the new instance
601         */
602        protected abstract T newInstance(Recurrence recur);
603
604        private void parseFreq(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) {
605                parseFirst(rules, FREQ, new Handler<String>() {
606                        public void handle(String value) {
607                                value = value.toUpperCase();
608                                try {
609                                        Frequency frequency = Frequency.valueOf(value);
610                                        builder.frequency(frequency);
611                                } catch (IllegalArgumentException e) {
612                                        warnings.add(Warning.parse(7, FREQ, value));
613                                }
614                        }
615                });
616        }
617
618        private void parseUntil(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) {
619                parseFirst(rules, UNTIL, new Handler<String>() {
620                        public void handle(String value) {
621                                try {
622                                        ICalDate date = date(value).parse();
623                                        builder.until(date);
624                                } catch (IllegalArgumentException e) {
625                                        warnings.add(Warning.parse(7, UNTIL, value));
626                                }
627                        }
628                });
629        }
630
631        private void parseCount(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) {
632                parseFirst(rules, COUNT, new Handler<String>() {
633                        public void handle(String value) {
634                                try {
635                                        builder.count(Integer.valueOf(value));
636                                } catch (NumberFormatException e) {
637                                        warnings.add(Warning.parse(7, COUNT, value));
638                                }
639                        }
640                });
641        }
642
643        private void parseInterval(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) {
644                parseFirst(rules, INTERVAL, new Handler<String>() {
645                        public void handle(String value) {
646                                try {
647                                        builder.interval(Integer.valueOf(value));
648                                } catch (NumberFormatException e) {
649                                        warnings.add(Warning.parse(7, INTERVAL, value));
650                                }
651                        }
652                });
653        }
654
655        private void parseBySecond(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
656                parseIntegerList(BYSECOND, rules, warnings, new Handler<Integer>() {
657                        public void handle(Integer value) {
658                                builder.bySecond(value);
659                        }
660                });
661        }
662
663        private void parseByMinute(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
664                parseIntegerList(BYMINUTE, rules, warnings, new Handler<Integer>() {
665                        public void handle(Integer value) {
666                                builder.byMinute(value);
667                        }
668                });
669        }
670
671        private void parseByHour(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
672                parseIntegerList(BYHOUR, rules, warnings, new Handler<Integer>() {
673                        public void handle(Integer value) {
674                                builder.byHour(value);
675                        }
676                });
677        }
678
679        private void parseByDay(ListMultimap<String, String> rules, Recurrence.Builder builder, List<Warning> warnings) {
680                Pattern p = Pattern.compile("^([-+]?\\d+)?(.*)$");
681                for (String value : rules.removeAll(BYDAY)) {
682                        Matcher m = p.matcher(value);
683                        if (!m.find()) {
684                                //this should never happen
685                                //the regex contains a "match-all" pattern and should never not find anything
686                                warnings.add(Warning.parse(7, BYDAY, value));
687                                continue;
688                        }
689
690                        String dayStr = m.group(2);
691                        DayOfWeek day = DayOfWeek.valueOfAbbr(dayStr);
692                        if (day == null) {
693                                warnings.add(Warning.parse(7, BYDAY, value));
694                                continue;
695                        }
696
697                        String prefixStr = m.group(1);
698                        Integer prefix = (prefixStr == null) ? null : Integer.valueOf(prefixStr); //no need to catch NumberFormatException because the regex guarantees that it will be a number
699
700                        builder.byDay(prefix, day);
701                }
702        }
703
704        private void parseByMonthDay(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
705                parseIntegerList(BYMONTHDAY, rules, warnings, new Handler<Integer>() {
706                        public void handle(Integer value) {
707                                builder.byMonthDay(value);
708                        }
709                });
710        }
711
712        private void parseByYearDay(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
713                parseIntegerList(BYYEARDAY, rules, warnings, new Handler<Integer>() {
714                        public void handle(Integer value) {
715                                builder.byYearDay(value);
716                        }
717                });
718        }
719
720        private void parseByWeekNo(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
721                parseIntegerList(BYWEEKNO, rules, warnings, new Handler<Integer>() {
722                        public void handle(Integer value) {
723                                builder.byWeekNo(value);
724                        }
725                });
726        }
727
728        private void parseByMonth(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
729                parseIntegerList(BYMONTH, rules, warnings, new Handler<Integer>() {
730                        public void handle(Integer value) {
731                                builder.byMonth(value);
732                        }
733                });
734        }
735
736        private void parseBySetPos(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) {
737                parseIntegerList(BYSETPOS, rules, warnings, new Handler<Integer>() {
738                        public void handle(Integer value) {
739                                builder.bySetPos(value);
740                        }
741                });
742        }
743
744        private void parseWkst(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) {
745                parseFirst(rules, WKST, new Handler<String>() {
746                        public void handle(String value) {
747                                DayOfWeek day = DayOfWeek.valueOfAbbr(value);
748                                if (day == null) {
749                                        warnings.add(Warning.parse(7, WKST, value));
750                                        return;
751                                }
752
753                                builder.workweekStarts(day);
754                        }
755                });
756        }
757
758        private void parseXRules(ListMultimap<String, String> rules, Recurrence.Builder builder, List<Warning> warnings) {
759                for (Map.Entry<String, List<String>> rule : rules) {
760                        String name = rule.getKey();
761                        for (String value : rule.getValue()) {
762                                builder.xrule(name, value);
763                        }
764                }
765        }
766
767        private ListMultimap<String, Object> buildComponents(T property, WriteContext context, boolean extended) {
768                ListMultimap<String, Object> components = new ListMultimap<String, Object>();
769                Recurrence recur = property.getValue();
770
771                //FREQ must come first
772                if (recur.getFrequency() != null) {
773                        components.put(FREQ, recur.getFrequency().name());
774                }
775
776                ICalDate until = recur.getUntil();
777                if (until != null) {
778                        String dateStr;
779                        if (isInObservance(context)) {
780                                dateStr = date(until).observance(true).extended(extended).write();
781                        } else {
782                                dateStr = date(until, property, context).extended(extended).write();
783                        }
784                        components.put(UNTIL, dateStr);
785                }
786
787                if (recur.getCount() != null) {
788                        components.put(COUNT, recur.getCount());
789                }
790
791                if (recur.getInterval() != null) {
792                        components.put(INTERVAL, recur.getInterval());
793                }
794
795                addIntegerListComponent(components, BYSECOND, recur.getBySecond());
796                addIntegerListComponent(components, BYMINUTE, recur.getByMinute());
797                addIntegerListComponent(components, BYHOUR, recur.getByHour());
798
799                for (ByDay byDay : recur.getByDay()) {
800                        Integer prefix = byDay.getNum();
801                        DayOfWeek day = byDay.getDay();
802
803                        String value = day.getAbbr();
804                        if (prefix != null) {
805                                value = prefix + value;
806                        }
807                        components.put(BYDAY, value);
808                }
809
810                addIntegerListComponent(components, BYMONTHDAY, recur.getByMonthDay());
811                addIntegerListComponent(components, BYYEARDAY, recur.getByYearDay());
812                addIntegerListComponent(components, BYWEEKNO, recur.getByWeekNo());
813                addIntegerListComponent(components, BYMONTH, recur.getByMonth());
814                addIntegerListComponent(components, BYSETPOS, recur.getBySetPos());
815
816                if (recur.getWorkweekStarts() != null) {
817                        components.put(WKST, recur.getWorkweekStarts().getAbbr());
818                }
819
820                for (Map.Entry<String, List<String>> entry : recur.getXRules().entrySet()) {
821                        String name = entry.getKey();
822                        for (String value : entry.getValue()) {
823                                components.put(name, value);
824                        }
825                }
826
827                return components;
828        }
829
830        private void addIntegerListComponent(ListMultimap<String, Object> components, String name, List<Integer> values) {
831                for (Integer value : values) {
832                        components.put(name, value);
833                }
834        }
835
836        private void parseFirst(ListMultimap<String, String> rules, String name, Handler<String> handler) {
837                List<String> values = rules.removeAll(name);
838                if (values.isEmpty()) {
839                        return;
840                }
841
842                String value = values.get(0);
843                handler.handle(value);
844        }
845
846        private void parseIntegerList(String name, ListMultimap<String, String> rules, List<Warning> warnings, Handler<Integer> handler) {
847                List<String> values = rules.removeAll(name);
848                for (String value : values) {
849                        try {
850                                handler.handle(Integer.valueOf(value));
851                        } catch (NumberFormatException e) {
852                                warnings.add(Warning.parse(8, name, value));
853                        }
854                }
855        }
856
857        private interface Handler<T> {
858                void handle(T value);
859        }
860}