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 }