001package biweekly.io.json; 002 003import static biweekly.util.IOUtils.utf8Reader; 004 005import java.io.File; 006import java.io.FileNotFoundException; 007import java.io.IOException; 008import java.io.InputStream; 009import java.io.Reader; 010import java.io.StringReader; 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.HashMap; 014import java.util.List; 015import java.util.Map; 016 017import biweekly.ICalDataType; 018import biweekly.ICalVersion; 019import biweekly.ICalendar; 020import biweekly.Warning; 021import biweekly.component.ICalComponent; 022import biweekly.io.CannotParseException; 023import biweekly.io.SkipMeException; 024import biweekly.io.StreamReader; 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.RawPropertyScribe; 031import biweekly.parameter.ICalParameters; 032import biweekly.property.ICalProperty; 033import biweekly.property.RawProperty; 034import biweekly.property.Version; 035 036import com.fasterxml.jackson.core.JsonParseException; 037 038/* 039 Copyright (c) 2013-2015, 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 * File file = new File("icals.json"); 072 * JCalReader reader = null; 073 * try { 074 * reader = new JCalReader(file); 075 * ICalendar ical; 076 * while ((ical = reader.readNext()) != null){ 077 * ... 078 * } 079 * } finally { 080 * if (reader != null) reader.close(); 081 * } 082 * </pre> 083 * 084 * </p> 085 * 086 * <p> 087 * <b>Getting timezone information:</b> 088 * 089 * <pre class="brush:java"> 090 * JCalReader reader = ... 091 * ICalendar ical = reader.readNext(); 092 * TimezoneInfo tzinfo = reader.getTimezoneInfo(); 093 * 094 * //get the VTIMEZONE components that were parsed 095 * //the VTIMEZONE components will NOT be in the ICalendar object 096 * Collection<VTimezone> vtimezones = tzinfo.getComponents(); 097 * 098 * //get the timezone that a property was originally formatted in 099 * DateStart dtstart = ical.getEvents().get(0).getDateStart(); 100 * TimeZone tz = tzinfo.getTimeZone(dtstart); 101 * </pre> 102 * 103 * </p> 104 * @author Michael Angstadt 105 * @see <a href="http://tools.ietf.org/html/rfc7265">RFC 7265</a> 106 */ 107public class JCalReader extends StreamReader { 108 private static final ICalendarScribe icalScribe = ScribeIndex.getICalendarScribe(); 109 private final JCalRawReader reader; 110 111 /** 112 * @param json the JSON string to read from 113 */ 114 public JCalReader(String json) { 115 this(new StringReader(json)); 116 } 117 118 /** 119 * @param in the input stream to read from 120 */ 121 public JCalReader(InputStream in) { 122 this(utf8Reader(in)); 123 } 124 125 /** 126 * @param file the file to read from 127 * @throws FileNotFoundException if the file doesn't exist 128 */ 129 public JCalReader(File file) throws FileNotFoundException { 130 this(utf8Reader(file)); 131 } 132 133 /** 134 * @param reader the reader to read from 135 */ 136 public JCalReader(Reader reader) { 137 this.reader = new JCalRawReader(reader); 138 } 139 140 /** 141 * Reads the next iCalendar object from the JSON data stream. 142 * @return the iCalendar object or null if there are no more 143 * @throws JCalParseException if the jCal syntax is incorrect (the JSON 144 * syntax may be valid, but it is not in the correct jCal format). 145 * @throws JsonParseException if the JSON syntax is incorrect 146 * @throws IOException if there is a problem reading from the data stream 147 */ 148 @Override 149 public ICalendar _readNext() throws IOException { 150 if (reader.eof()) { 151 return null; 152 } 153 154 context.setVersion(ICalVersion.V2_0); 155 156 JCalDataStreamListenerImpl listener = new JCalDataStreamListenerImpl(); 157 reader.readNext(listener); 158 159 return listener.getICalendar(); 160 } 161 162 //@Override 163 public void close() throws IOException { 164 reader.close(); 165 } 166 167 private class JCalDataStreamListenerImpl implements JCalDataStreamListener { 168 private final Map<List<String>, ICalComponent> components = new HashMap<List<String>, ICalComponent>(); 169 170 public void readProperty(List<String> componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) { 171 context.getWarnings().clear(); 172 173 //get the component that the property belongs to 174 ICalComponent parent = components.get(componentHierarchy); 175 176 //unmarshal the property 177 ICalPropertyScribe<? extends ICalProperty> scribe = index.getPropertyScribe(propertyName, ICalVersion.V2_0); 178 try { 179 ICalProperty property = scribe.parseJson(value, dataType, parameters, context); 180 for (Warning warning : context.getWarnings()) { 181 warnings.add(reader.getLineNum(), propertyName, warning); 182 } 183 184 //set "ICalendar.version" if the value of the VERSION property is recognized 185 //otherwise, unmarshal VERSION like a normal property 186 if (parent instanceof ICalendar && property instanceof Version) { 187 Version version = (Version) property; 188 ICalVersion icalVersion = version.toICalVersion(); 189 if (icalVersion != null) { 190 context.setVersion(icalVersion); 191 return; 192 } 193 } 194 195 parent.addProperty(property); 196 } catch (SkipMeException e) { 197 warnings.add(reader.getLineNum(), propertyName, 0, e.getMessage()); 198 } catch (CannotParseException e) { 199 RawProperty property = new RawPropertyScribe(propertyName).parseJson(value, dataType, parameters, context); 200 parent.addProperty(property); 201 202 String valueStr = property.getValue(); 203 warnings.add(reader.getLineNum(), propertyName, 1, valueStr, e.getMessage()); 204 } 205 } 206 207 public void readComponent(List<String> parentHierarchy, String componentName) { 208 ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(componentName, ICalVersion.V2_0); 209 ICalComponent component = scribe.emptyInstance(); 210 211 ICalComponent parent = components.get(parentHierarchy); 212 if (parent != null) { 213 parent.addComponent(component); 214 } 215 216 List<String> hierarchy = new ArrayList<String>(parentHierarchy); 217 hierarchy.add(componentName); 218 components.put(hierarchy, component); 219 } 220 221 public ICalendar getICalendar() { 222 if (components.isEmpty()) { 223 //EOF 224 return null; 225 } 226 227 ICalComponent component = components.get(Arrays.asList(icalScribe.getComponentName().toLowerCase())); 228 if (component == null) { 229 //should never happen because the parser always looks for a "vcalendar" component 230 return null; 231 } 232 233 if (component instanceof ICalendar) { 234 //should happen every time 235 return (ICalendar) component; 236 } 237 238 //this will only happen if the user decides to override the ICalendarScribe for some reason 239 ICalendar ical = icalScribe.emptyInstance(); 240 ical.addComponent(component); 241 return ical; 242 } 243 } 244}