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 * 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 * 082 * <p> 083 * <b>Getting timezone information:</b> 084 * 085 * <pre class="brush:java"> 086 * JCalReader reader = ... 087 * ICalendar ical = reader.readNext(); 088 * TimezoneInfo tzinfo = reader.getTimezoneInfo(); 089 * 090 * //get the VTIMEZONE components that were parsed 091 * //the VTIMEZONE components will NOT be in the ICalendar object 092 * Collection<VTimezone> vtimezones = tzinfo.getComponents(); 093 * 094 * //get the timezone that a property was originally formatted in 095 * DateStart dtstart = ical.getEvents().get(0).getDateStart(); 096 * TimeZone tz = tzinfo.getTimeZone(dtstart); 097 * </pre> 098 * 099 * </p> 100 * @author Michael Angstadt 101 * @see <a href="http://tools.ietf.org/html/rfc7265">RFC 7265</a> 102 */ 103public class JCalReader extends StreamReader { 104 private static final ICalendarScribe icalScribe = ScribeIndex.getICalendarScribe(); 105 private final JCalRawReader reader; 106 107 /** 108 * Creates a jCard reader. 109 * @param json the JSON string 110 */ 111 public JCalReader(String json) { 112 this(new StringReader(json)); 113 } 114 115 /** 116 * Creates a jCard reader. 117 * @param in the input stream to read the vCards from 118 */ 119 public JCalReader(InputStream in) { 120 this(utf8Reader(in)); 121 } 122 123 /** 124 * Creates a jCard reader. 125 * @param file the file to read the vCards from 126 * @throws FileNotFoundException if the file doesn't exist 127 */ 128 public JCalReader(File file) throws FileNotFoundException { 129 this(utf8Reader(file)); 130 } 131 132 /** 133 * Creates a jCard reader. 134 * @param reader the reader to read the vCards 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}