001 package biweekly.io.json; 002 003 import static biweekly.util.IOUtils.utf8Reader; 004 import static biweekly.util.StringUtils.NEWLINE; 005 006 import java.io.Closeable; 007 import java.io.File; 008 import java.io.FileNotFoundException; 009 import java.io.IOException; 010 import java.io.InputStream; 011 import java.io.Reader; 012 import java.io.StringReader; 013 import java.util.ArrayList; 014 import java.util.Arrays; 015 import java.util.HashMap; 016 import java.util.List; 017 import java.util.Map; 018 019 import biweekly.ICalDataType; 020 import biweekly.ICalendar; 021 import biweekly.component.ICalComponent; 022 import biweekly.component.marshaller.ICalComponentMarshaller; 023 import biweekly.component.marshaller.ICalendarMarshaller; 024 import biweekly.io.CannotParseException; 025 import biweekly.io.ICalMarshallerRegistrar; 026 import biweekly.io.SkipMeException; 027 import biweekly.io.json.JCalRawReader.JCalDataStreamListener; 028 import biweekly.parameter.ICalParameters; 029 import biweekly.property.ICalProperty; 030 import biweekly.property.RawProperty; 031 import biweekly.property.marshaller.ICalPropertyMarshaller; 032 import biweekly.property.marshaller.ICalPropertyMarshaller.Result; 033 import biweekly.property.marshaller.RawPropertyMarshaller; 034 035 import com.fasterxml.jackson.core.JsonParseException; 036 037 /* 038 Copyright (c) 2013, Michael Angstadt 039 All rights reserved. 040 041 Redistribution and use in source and binary forms, with or without 042 modification, are permitted provided that the following conditions are met: 043 044 1. Redistributions of source code must retain the above copyright notice, this 045 list of conditions and the following disclaimer. 046 2. Redistributions in binary form must reproduce the above copyright notice, 047 this list of conditions and the following disclaimer in the documentation 048 and/or other materials provided with the distribution. 049 050 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 051 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 052 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 053 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 054 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 055 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 056 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 057 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 058 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 059 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 060 */ 061 062 /** 063 * <p> 064 * Parses {@link ICalendar} objects from a jCal data stream (JSON). 065 * </p> 066 * <p> 067 * <b>Example:</b> 068 * 069 * <pre class="brush:java"> 070 * InputStream in = ... 071 * JCalReader jcalReader = new JCalReader(in); 072 * ICalendar ical; 073 * while ((ical = jcalReader.readNext()) != null){ 074 * ... 075 * } 076 * jcalReader.close(); 077 * </pre> 078 * 079 * </p> 080 * @author Michael Angstadt 081 * @see <a href="http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-05">jCal 082 * draft</a> 083 */ 084 public class JCalReader implements Closeable { 085 private static final ICalendarMarshaller icalMarshaller = ICalMarshallerRegistrar.getICalendarMarshaller(); 086 private ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar(); 087 private final JCalRawReader reader; 088 private final List<String> warnings = new ArrayList<String>(); 089 090 /** 091 * Creates a jCard reader. 092 * @param json the JSON string 093 */ 094 public JCalReader(String json) { 095 this(new StringReader(json)); 096 } 097 098 /** 099 * Creates a jCard reader. 100 * @param in the input stream to read the vCards from 101 */ 102 public JCalReader(InputStream in) { 103 this(utf8Reader(in)); 104 } 105 106 /** 107 * Creates a jCard reader. 108 * @param file the file to read the vCards from 109 * @throws FileNotFoundException if the file doesn't exist 110 */ 111 public JCalReader(File file) throws FileNotFoundException { 112 this(utf8Reader(file)); 113 } 114 115 /** 116 * Creates a jCard reader. 117 * @param reader the reader to read the vCards from 118 */ 119 public JCalReader(Reader reader) { 120 this.reader = new JCalRawReader(reader); 121 } 122 123 /** 124 * Gets the warnings from the last iCalendar object that was unmarshalled. 125 * This list is reset every time a new iCalendar object is read. 126 * @return the warnings or empty list if there were no warnings 127 */ 128 public List<String> getWarnings() { 129 return new ArrayList<String>(warnings); 130 } 131 132 /** 133 * <p> 134 * Registers an experimental property marshaller. Can also be used to 135 * override the marshaller of a standard property (such as DTSTART). Calling 136 * this method is the same as calling: 137 * </p> 138 * <p> 139 * {@code getRegistrar().register(marshaller)}. 140 * </p> 141 * @param marshaller the marshaller to register 142 */ 143 public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) { 144 registrar.register(marshaller); 145 } 146 147 /** 148 * <p> 149 * Registers an experimental component marshaller. Can also be used to 150 * override the marshaller of a standard component (such as VEVENT). Calling 151 * this method is the same as calling: 152 * </p> 153 * <p> 154 * {@code getRegistrar().register(marshaller)}. 155 * </p> 156 * @param marshaller the marshaller to register 157 */ 158 public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) { 159 registrar.register(marshaller); 160 } 161 162 /** 163 * Gets the object that manages the component/property marshaller objects. 164 * @return the marshaller registrar 165 */ 166 public ICalMarshallerRegistrar getRegistrar() { 167 return registrar; 168 } 169 170 /** 171 * Sets the object that manages the component/property marshaller objects. 172 * @param registrar the marshaller registrar 173 */ 174 public void setRegistrar(ICalMarshallerRegistrar registrar) { 175 this.registrar = registrar; 176 } 177 178 /** 179 * Reads the next iCalendar object from the JSON data stream. 180 * @return the iCalendar object or null if there are no more 181 * @throws JCalParseException if the jCal syntax is incorrect (the JSON 182 * syntax may be valid, but it is not in the correct jCal format). 183 * @throws JsonParseException if the JSON syntax is incorrect 184 * @throws IOException if there is a problem reading from the data stream 185 */ 186 public ICalendar readNext() throws IOException { 187 if (reader.eof()) { 188 return null; 189 } 190 191 warnings.clear(); 192 193 JCalDataStreamListenerImpl listener = new JCalDataStreamListenerImpl(); 194 reader.readNext(listener); 195 return listener.getICalendar(); 196 } 197 198 private void addWarning(String message, String propertyName) { 199 StringBuilder sb = new StringBuilder(); 200 sb.append("Line ").append(reader.getLineNum()); 201 if (propertyName != null) { 202 sb.append(" (").append(propertyName).append(" property)"); 203 } 204 sb.append(": ").append(message); 205 206 warnings.add(sb.toString()); 207 } 208 209 //@Override 210 public void close() throws IOException { 211 reader.close(); 212 } 213 214 private class JCalDataStreamListenerImpl implements JCalDataStreamListener { 215 private final Map<List<String>, ICalComponent> components = new HashMap<List<String>, ICalComponent>(); 216 217 public void readProperty(List<String> componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) { 218 //get the component that the property belongs to 219 ICalComponent parent = components.get(componentHierarchy); 220 221 //unmarshal the property 222 ICalPropertyMarshaller<? extends ICalProperty> m = registrar.getPropertyMarshaller(propertyName); 223 ICalProperty property = null; 224 try { 225 Result<? extends ICalProperty> result = m.parseJson(value, dataType, parameters); 226 227 for (String warning : result.getWarnings()) { 228 addWarning(warning, propertyName); 229 } 230 231 property = result.getProperty(); 232 } catch (SkipMeException e) { 233 if (e.getMessage() == null) { 234 addWarning("Property has requested that it be skipped.", propertyName); 235 } else { 236 addWarning("Property has requested that it be skipped: " + e.getMessage(), propertyName); 237 } 238 } catch (CannotParseException e) { 239 Result<? extends ICalProperty> result = new RawPropertyMarshaller(propertyName).parseJson(value, dataType, parameters); 240 for (String warning : result.getWarnings()) { 241 addWarning(warning, propertyName); 242 } 243 property = result.getProperty(); 244 245 String valueStr = ((RawProperty) property).getValue(); 246 if (e.getMessage() == null) { 247 addWarning("Property value could not be unmarshalled: " + valueStr, propertyName); 248 } else { 249 addWarning("Property value could not be unmarshalled." + NEWLINE + " Value: " + valueStr + NEWLINE + " Reason: " + e.getMessage(), propertyName); 250 } 251 } 252 253 if (property != null) { 254 parent.addProperty(property); 255 } 256 } 257 258 public void readComponent(List<String> parentHierarchy, String componentName) { 259 ICalComponentMarshaller<? extends ICalComponent> m = registrar.getComponentMarshaller(componentName); 260 ICalComponent component = m.emptyInstance(); 261 262 ICalComponent parent = components.get(parentHierarchy); 263 if (parent != null) { 264 parent.addComponent(component); 265 } 266 267 List<String> hierarchy = new ArrayList<String>(parentHierarchy); 268 hierarchy.add(componentName); 269 components.put(hierarchy, component); 270 } 271 272 public ICalendar getICalendar() { 273 if (components.isEmpty()) { 274 //EOF 275 return null; 276 } 277 278 ICalComponent component = components.get(Arrays.asList(icalMarshaller.getComponentName().toLowerCase())); 279 if (component == null) { 280 //should never happen because the parser always looks for a "vcalendar" component 281 return null; 282 } 283 284 if (component instanceof ICalendar) { 285 //should happen every time 286 return (ICalendar) component; 287 } 288 289 //this will only happen if the user decides to override the ICalendarMarshaller for some reason 290 ICalendar ical = icalMarshaller.emptyInstance(); 291 ical.addComponent(component); 292 return ical; 293 } 294 } 295 }