001    package biweekly.property.marshaller;
002    
003    import java.util.ArrayList;
004    import java.util.Arrays;
005    import java.util.Date;
006    import java.util.List;
007    import java.util.Map;
008    import java.util.regex.Matcher;
009    import java.util.regex.Pattern;
010    
011    import biweekly.io.xml.XCalElement;
012    import biweekly.parameter.ICalParameters;
013    import biweekly.parameter.Value;
014    import biweekly.property.RecurrenceRule;
015    import biweekly.property.RecurrenceRule.DayOfWeek;
016    import biweekly.property.RecurrenceRule.Frequency;
017    import biweekly.util.ListMultimap;
018    import biweekly.util.StringUtils;
019    import biweekly.util.StringUtils.JoinMapCallback;
020    
021    /*
022     Copyright (c) 2013, Michael Angstadt
023     All rights reserved.
024    
025     Redistribution and use in source and binary forms, with or without
026     modification, are permitted provided that the following conditions are met: 
027    
028     1. Redistributions of source code must retain the above copyright notice, this
029     list of conditions and the following disclaimer. 
030     2. Redistributions in binary form must reproduce the above copyright notice,
031     this list of conditions and the following disclaimer in the documentation
032     and/or other materials provided with the distribution. 
033    
034     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
035     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
036     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
037     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
038     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
039     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
040     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
042     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
043     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
044     */
045    
046    /**
047     * Marshals {@link RecurrenceRule} properties.
048     * @author Michael Angstadt
049     */
050    public class RecurrenceRuleMarshaller extends ICalPropertyMarshaller<RecurrenceRule> {
051            public RecurrenceRuleMarshaller() {
052                    super(RecurrenceRule.class, "RRULE");
053            }
054    
055            @Override
056            protected String _writeText(RecurrenceRule property) {
057                    ListMultimap<String, String> components = buildComponents(property, false);
058    
059                    return StringUtils.join(components.getMap(), ";", new JoinMapCallback<String, List<String>>() {
060                            public void handle(StringBuilder sb, String key, List<String> values) {
061                                    sb.append(key).append('=').append(StringUtils.join(values, ","));
062                            }
063                    });
064            }
065    
066            @Override
067            protected RecurrenceRule _parseText(String value, ICalParameters parameters, List<String> warnings) {
068                    ListMultimap<String, String> components = new ListMultimap<String, String>();
069                    for (String component : value.split(";")) {
070                            String split[] = component.split("=");
071                            if (split.length < 2) {
072                                    warnings.add("Skipping invalid recurrence rule component: " + component);
073                                    continue;
074                            }
075                            String name = split[0];
076                            String values[] = split[1].split(",");
077                            components.putAll(name.toUpperCase(), Arrays.asList(values));
078                    }
079    
080                    RecurrenceRule property = new RecurrenceRule(null);
081    
082                    parseFreq(components.first("FREQ"), property, warnings);
083                    parseUntil(components.first("UNTIL"), property, warnings);
084                    parseCount(components.first("COUNT"), property, warnings);
085                    parseInterval(components.first("INTERVAL"), property, warnings);
086                    parseBySecond(components.get("BYSECOND"), property, warnings);
087                    parseByMinute(components.get("BYMINUTE"), property, warnings);
088                    parseByHour(components.get("BYHOUR"), property, warnings);
089                    parseByDay(components.get("BYDAY"), property, warnings);
090                    parseByMonthDay(components.get("BYMONTHDAY"), property, warnings);
091                    parseByYearDay(components.get("BYYEARDAY"), property, warnings);
092                    parseByWeekNo(components.get("BYWEEKNO"), property, warnings);
093                    parseByMonth(components.get("BYMONTH"), property, warnings);
094                    parseBySetPos(components.get("BYSETPOS"), property, warnings);
095                    parseWkst(components.first("WKST"), property, warnings);
096    
097                    return property;
098            }
099    
100            @Override
101            protected void _writeXml(RecurrenceRule property, XCalElement element) {
102                    ListMultimap<String, String> components = buildComponents(property, true);
103    
104                    XCalElement recur = element.append(Value.RECUR);
105                    for (Map.Entry<String, List<String>> component : components) {
106                            String name = component.getKey().toLowerCase();
107                            for (String value : component.getValue()) {
108                                    recur.append(name, value);
109                            }
110                    }
111            }
112    
113            @Override
114            protected RecurrenceRule _parseXml(XCalElement element, ICalParameters parameters, List<String> warnings) {
115                    RecurrenceRule property = new RecurrenceRule(null);
116    
117                    XCalElement recur = element.child(Value.RECUR);
118                    if (recur == null) {
119                            return property;
120                    }
121    
122                    parseFreq(recur.first("freq"), property, warnings);
123                    parseUntil(recur.first("until"), property, warnings);
124                    parseCount(recur.first("count"), property, warnings);
125                    parseInterval(recur.first("interval"), property, warnings);
126                    parseBySecond(recur.all("bysecond"), property, warnings);
127                    parseByMinute(recur.all("byminute"), property, warnings);
128                    parseByHour(recur.all("byhour"), property, warnings);
129                    parseByDay(recur.all("byday"), property, warnings);
130                    parseByMonthDay(recur.all("bymonthday"), property, warnings);
131                    parseByYearDay(recur.all("byyearday"), property, warnings);
132                    parseByWeekNo(recur.all("byweekno"), property, warnings);
133                    parseByMonth(recur.all("bymonth"), property, warnings);
134                    parseBySetPos(recur.all("bysetpos"), property, warnings);
135                    parseWkst(recur.first("wkst"), property, warnings);
136    
137                    return property;
138            }
139    
140            private void parseFreq(String value, RecurrenceRule property, List<String> warnings) {
141                    if (value != null) {
142                            try {
143                                    property.setFrequency(Frequency.valueOf(value.toUpperCase()));
144                            } catch (IllegalArgumentException e) {
145                                    warnings.add("Invalid frequency value: " + value);
146                            }
147                    }
148            }
149    
150            private void parseUntil(String value, RecurrenceRule property, List<String> warnings) {
151                    if (value != null) {
152                            try {
153                                    Date date = date(value).parse();
154                                    boolean hasTime = value.contains("T");
155                                    property.setUntil(date, hasTime);
156                            } catch (IllegalArgumentException e) {
157                                    warnings.add("Could not parse UNTIL date: " + value);
158                            }
159                    }
160            }
161    
162            private void parseCount(String value, RecurrenceRule property, List<String> warnings) {
163                    if (value != null) {
164                            try {
165                                    property.setCount(Integer.valueOf(value));
166                            } catch (NumberFormatException e) {
167                                    warnings.add("Could not parse COUNT integer: " + value);
168                            }
169                    }
170            }
171    
172            private void parseInterval(String value, RecurrenceRule property, List<String> warnings) {
173                    if (value != null) {
174                            try {
175                                    property.setInterval(Integer.valueOf(value));
176                            } catch (NumberFormatException e) {
177                                    warnings.add("Could not parse INTERVAL integer: " + value);
178                            }
179                    }
180            }
181    
182            private void parseBySecond(List<String> values, RecurrenceRule property, List<String> warnings) {
183                    if (!values.isEmpty()) {
184                            property.setBySecond(toIntegerList("BYSECOND", values, warnings));
185                    }
186            }
187    
188            private void parseByMinute(List<String> values, RecurrenceRule property, List<String> warnings) {
189                    if (!values.isEmpty()) {
190                            property.setByMinute(toIntegerList("BYMINUTE", values, warnings));
191                    }
192            }
193    
194            private void parseByHour(List<String> values, RecurrenceRule property, List<String> warnings) {
195                    if (!values.isEmpty()) {
196                            property.setByHour(toIntegerList("BYHOUR", values, warnings));
197                    }
198    
199            }
200    
201            private void parseByDay(List<String> values, RecurrenceRule property, List<String> warnings) {
202                    if (!values.isEmpty()) {
203                            Pattern p = Pattern.compile("^([-+]?\\d+)?(.*)$");
204                            for (String v : values) {
205                                    Matcher m = p.matcher(v);
206                                    if (m.find()) {
207                                            String prefixStr = m.group(1);
208                                            String dayStr = m.group(2);
209    
210                                            DayOfWeek day = DayOfWeek.valueOfAbbr(dayStr);
211                                            if (day == null) {
212                                                    warnings.add("Ignoring invalid day string: " + dayStr);
213                                                    continue;
214                                            }
215    
216                                            Integer prefix = (prefixStr == null) ? null : Integer.valueOf(prefixStr);
217                                            property.addByDay(prefix, day);
218                                    } else {
219                                            //should never reach here due to nature of regular expression
220                                            warnings.add("Problem parsing BYDAY value: " + v);
221                                    }
222                            }
223                    }
224            }
225    
226            private void parseByMonthDay(List<String> values, RecurrenceRule property, List<String> warnings) {
227                    if (!values.isEmpty()) {
228                            property.setByMonthDay(toIntegerList("BYMONTHDAY", values, warnings));
229                    }
230            }
231    
232            private void parseByYearDay(List<String> values, RecurrenceRule property, List<String> warnings) {
233                    if (!values.isEmpty()) {
234                            property.setByYearDay(toIntegerList("BYYEARDAY", values, warnings));
235                    }
236            }
237    
238            private void parseByWeekNo(List<String> values, RecurrenceRule property, List<String> warnings) {
239                    if (!values.isEmpty()) {
240                            property.setByWeekNo(toIntegerList("BYWEEKNO", values, warnings));
241                    }
242            }
243    
244            private void parseByMonth(List<String> values, RecurrenceRule property, List<String> warnings) {
245                    if (!values.isEmpty()) {
246                            property.setByMonth(toIntegerList("BYMONTH", values, warnings));
247                    }
248            }
249    
250            private void parseBySetPos(List<String> values, RecurrenceRule property, List<String> warnings) {
251                    if (!values.isEmpty()) {
252                            property.setBySetPos(toIntegerList("BYSETPOS", values, warnings));
253                    }
254            }
255    
256            private void parseWkst(String value, RecurrenceRule property, List<String> warnings) {
257                    if (value != null) {
258                            DayOfWeek day = DayOfWeek.valueOfAbbr(value);
259                            if (day == null) {
260                                    warnings.add("Invalid day string: " + value);
261                            } else {
262                                    property.setWorkweekStarts(day);
263                            }
264                    }
265            }
266    
267            private ListMultimap<String, String> buildComponents(RecurrenceRule property, boolean extended) {
268                    ListMultimap<String, String> components = new ListMultimap<String, String>();
269    
270                    if (property.getFrequency() != null) {
271                            components.put("FREQ", property.getFrequency().name());
272                    }
273    
274                    if (property.getUntil() != null) {
275                            String s = date(property.getUntil()).time(property.hasTimeUntilDate()).extended(extended).write();
276                            components.put("UNTIL", s);
277                    }
278    
279                    if (property.getCount() != null) {
280                            components.put("COUNT", property.getCount().toString());
281                    }
282    
283                    if (property.getInterval() != null) {
284                            components.put("INTERVAL", property.getInterval().toString());
285                    }
286    
287                    addIntegerListComponent(components, "BYSECOND", property.getBySecond());
288                    addIntegerListComponent(components, "BYMINUTE", property.getByMinute());
289                    addIntegerListComponent(components, "BYHOUR", property.getByHour());
290    
291                    for (int i = 0; i < property.getByDay().size(); i++) {
292                            DayOfWeek day = property.getByDay().get(i);
293                            Integer prefix = property.getByDayPrefixes().get(i);
294    
295                            String value = day.getAbbr();
296                            if (prefix != null) {
297                                    value = prefix + value;
298                            }
299                            components.put("BYDAY", value);
300                    }
301    
302                    addIntegerListComponent(components, "BYMONTHDAY", property.getByMonthDay());
303                    addIntegerListComponent(components, "BYYEARDAY", property.getByYearDay());
304                    addIntegerListComponent(components, "BYWEEKNO", property.getByWeekNo());
305                    addIntegerListComponent(components, "BYMONTH", property.getByMonth());
306                    addIntegerListComponent(components, "BYSETPOS", property.getBySetPos());
307    
308                    if (property.getWorkweekStarts() != null) {
309                            components.put("WKST", property.getWorkweekStarts().getAbbr());
310                    }
311    
312                    return components;
313            }
314    
315            private List<Integer> toIntegerList(String name, List<String> values, List<String> warnings) {
316                    List<Integer> list = new ArrayList<Integer>(values.size());
317    
318                    for (String value : values) {
319                            try {
320                                    list.add(Integer.valueOf(value));
321                            } catch (NumberFormatException e) {
322                                    warnings.add("Ignoring non-numeric value found in " + name + ": " + value);
323                            }
324                    }
325    
326                    return list;
327            }
328    
329            private void addIntegerListComponent(ListMultimap<String, String> components, String name, List<Integer> values) {
330                    if (values == null) {
331                            return;
332                    }
333                    for (Integer value : values) {
334                            components.put(name, value.toString());
335                    }
336            }
337    }