001package biweekly.io.json; 002 003import static biweekly.util.IOUtils.utf8Reader; 004 005import java.io.Closeable; 006import java.io.File; 007import java.io.FileNotFoundException; 008import java.io.IOException; 009import java.io.InputStream; 010import java.io.Reader; 011import java.io.StringReader; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.HashMap; 015import java.util.List; 016import java.util.Map; 017 018import biweekly.ICalDataType; 019import biweekly.ICalendar; 020import biweekly.Warning; 021import biweekly.component.ICalComponent; 022import biweekly.io.CannotParseException; 023import biweekly.io.ParseWarnings; 024import biweekly.io.SkipMeException; 025import biweekly.io.json.JCalRawReader.JCalDataStreamListener; 026import biweekly.io.scribe.ScribeIndex; 027import biweekly.io.scribe.component.ICalComponentScribe; 028import biweekly.io.scribe.component.ICalendarScribe; 029import biweekly.io.scribe.property.ICalPropertyScribe; 030import biweekly.io.scribe.property.ICalPropertyScribe.Result; 031import biweekly.io.scribe.property.RawPropertyScribe; 032import biweekly.parameter.ICalParameters; 033import biweekly.property.ICalProperty; 034import biweekly.property.RawProperty; 035 036import com.fasterxml.jackson.core.JsonParseException; 037 038/* 039 Copyright (c) 2013, Michael Angstadt 040 All rights reserved. 041 042 Redistribution and use in source and binary forms, with or without 043 modification, are permitted provided that the following conditions are met: 044 045 1. Redistributions of source code must retain the above copyright notice, this 046 list of conditions and the following disclaimer. 047 2. Redistributions in binary form must reproduce the above copyright notice, 048 this list of conditions and the following disclaimer in the documentation 049 and/or other materials provided with the distribution. 050 051 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 052 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 053 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 054 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 055 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 056 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 057 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 058 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 059 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 060 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 061 */ 062 063/** 064 * <p> 065 * Parses {@link ICalendar} objects from a jCal data stream (JSON). 066 * </p> 067 * <p> 068 * <b>Example:</b> 069 * 070 * <pre class="brush:java"> 071 * InputStream in = ... 072 * JCalReader jcalReader = new JCalReader(in); 073 * ICalendar ical; 074 * while ((ical = jcalReader.readNext()) != null){ 075 * ... 076 * } 077 * jcalReader.close(); 078 * </pre> 079 * 080 * </p> 081 * @author Michael Angstadt 082 * @see <a href="http://tools.ietf.org/html/rfc7265">RFC 7265</a> 083 */ 084public class JCalReader implements Closeable { 085 private static final ICalendarScribe icalScribe = ScribeIndex.getICalendarScribe(); 086 private ScribeIndex index = new ScribeIndex(); 087 private final JCalRawReader reader; 088 private final ParseWarnings warnings = new ParseWarnings(); 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 warnings.copy(); 130 } 131 132 /** 133 * <p> 134 * Registers an experimental property scribe. Can also be used to override 135 * the scribe of a standard property (such as DTSTART). Calling this method 136 * is the same as calling: 137 * </p> 138 * <p> 139 * {@code getScribeIndex().register(scribe)}. 140 * </p> 141 * @param scribe the scribe to register 142 */ 143 public void registerScribe(ICalPropertyScribe<? extends ICalProperty> scribe) { 144 index.register(scribe); 145 } 146 147 /** 148 * <p> 149 * Registers an experimental component scribe. Can also be used to override 150 * the scribe of a standard component (such as VEVENT). Calling this method 151 * is the same as calling: 152 * </p> 153 * <p> 154 * {@code getScribeIndex().register(scribe)}. 155 * </p> 156 * @param scribe the scribe to register 157 */ 158 public void registerScribe(ICalComponentScribe<? extends ICalComponent> scribe) { 159 index.register(scribe); 160 } 161 162 /** 163 * Gets the object that manages the component/property scribes. 164 * @return the scribe index 165 */ 166 public ScribeIndex getScribeIndex() { 167 return index; 168 } 169 170 /** 171 * Sets the object that manages the component/property scribes. 172 * @param index the scribe index 173 */ 174 public void setScribeIndex(ScribeIndex index) { 175 this.index = index; 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 //@Override 199 public void close() throws IOException { 200 reader.close(); 201 } 202 203 private class JCalDataStreamListenerImpl implements JCalDataStreamListener { 204 private final Map<List<String>, ICalComponent> components = new HashMap<List<String>, ICalComponent>(); 205 206 public void readProperty(List<String> componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) { 207 //get the component that the property belongs to 208 ICalComponent parent = components.get(componentHierarchy); 209 210 //unmarshal the property 211 ICalPropertyScribe<? extends ICalProperty> scribe = index.getPropertyScribe(propertyName); 212 try { 213 Result<? extends ICalProperty> result = scribe.parseJson(value, dataType, parameters); 214 for (Warning warning : result.getWarnings()) { 215 warnings.add(reader.getLineNum(), propertyName, warning); 216 } 217 ICalProperty property = result.getProperty(); 218 parent.addProperty(property); 219 } catch (SkipMeException e) { 220 warnings.add(reader.getLineNum(), propertyName, 0, e.getMessage()); 221 } catch (CannotParseException e) { 222 Result<? extends ICalProperty> result = new RawPropertyScribe(propertyName).parseJson(value, dataType, parameters); 223 ICalProperty property = result.getProperty(); 224 parent.addProperty(property); 225 226 String valueStr = ((RawProperty) property).getValue(); 227 warnings.add(reader.getLineNum(), propertyName, 1, valueStr, e.getMessage()); 228 } 229 } 230 231 public void readComponent(List<String> parentHierarchy, String componentName) { 232 ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(componentName); 233 ICalComponent component = scribe.emptyInstance(); 234 235 ICalComponent parent = components.get(parentHierarchy); 236 if (parent != null) { 237 parent.addComponent(component); 238 } 239 240 List<String> hierarchy = new ArrayList<String>(parentHierarchy); 241 hierarchy.add(componentName); 242 components.put(hierarchy, component); 243 } 244 245 public ICalendar getICalendar() { 246 if (components.isEmpty()) { 247 //EOF 248 return null; 249 } 250 251 ICalComponent component = components.get(Arrays.asList(icalScribe.getComponentName().toLowerCase())); 252 if (component == null) { 253 //should never happen because the parser always looks for a "vcalendar" component 254 return null; 255 } 256 257 if (component instanceof ICalendar) { 258 //should happen every time 259 return (ICalendar) component; 260 } 261 262 //this will only happen if the user decides to override the ICalendarScribe for some reason 263 ICalendar ical = icalScribe.emptyInstance(); 264 ical.addComponent(component); 265 return ical; 266 } 267 } 268}