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 }