001package biweekly.io.xml;
002
003import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS;
004import static biweekly.io.xml.XCalQNames.COMPONENTS;
005import static biweekly.io.xml.XCalQNames.ICALENDAR;
006import static biweekly.io.xml.XCalQNames.PARAMETERS;
007import static biweekly.io.xml.XCalQNames.PROPERTIES;
008import static biweekly.io.xml.XCalQNames.VCALENDAR;
009
010import java.io.Closeable;
011import java.io.File;
012import java.io.FileInputStream;
013import java.io.FileNotFoundException;
014import java.io.IOException;
015import java.io.InputStream;
016import java.io.Reader;
017import java.io.StringReader;
018import java.util.ArrayList;
019import java.util.LinkedList;
020import java.util.List;
021import java.util.concurrent.ArrayBlockingQueue;
022import java.util.concurrent.BlockingQueue;
023
024import javax.xml.namespace.QName;
025import javax.xml.transform.ErrorListener;
026import javax.xml.transform.Source;
027import javax.xml.transform.Transformer;
028import javax.xml.transform.TransformerConfigurationException;
029import javax.xml.transform.TransformerException;
030import javax.xml.transform.TransformerFactory;
031import javax.xml.transform.dom.DOMSource;
032import javax.xml.transform.sax.SAXResult;
033import javax.xml.transform.stream.StreamSource;
034
035import org.w3c.dom.Document;
036import org.w3c.dom.Element;
037import org.w3c.dom.Node;
038import org.xml.sax.Attributes;
039import org.xml.sax.SAXException;
040import org.xml.sax.helpers.DefaultHandler;
041
042import biweekly.ICalVersion;
043import biweekly.ICalendar;
044import biweekly.Warning;
045import biweekly.component.ICalComponent;
046import biweekly.io.CannotParseException;
047import biweekly.io.ParseContext;
048import biweekly.io.SkipMeException;
049import biweekly.io.StreamReader;
050import biweekly.io.TimezoneInfo;
051import biweekly.io.scribe.component.ICalComponentScribe;
052import biweekly.io.scribe.property.ICalPropertyScribe;
053import biweekly.parameter.ICalParameters;
054import biweekly.property.ICalProperty;
055import biweekly.property.Version;
056import biweekly.property.Xml;
057import biweekly.util.XmlUtils;
058
059/*
060 Copyright (c) 2013-2015, Michael Angstadt
061 All rights reserved.
062
063 Redistribution and use in source and binary forms, with or without
064 modification, are permitted provided that the following conditions are met: 
065
066 1. Redistributions of source code must retain the above copyright notice, this
067 list of conditions and the following disclaimer. 
068 2. Redistributions in binary form must reproduce the above copyright notice,
069 this list of conditions and the following disclaimer in the documentation
070 and/or other materials provided with the distribution. 
071
072 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
073 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
074 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
075 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
076 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
077 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
078 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
079 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
080 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
081 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
082
083 The views and conclusions contained in the software and documentation are those
084 of the authors and should not be interpreted as representing official policies, 
085 either expressed or implied, of the FreeBSD Project.
086 */
087
088/**
089 * <p>
090 * Reads xCals (XML-encoded vCards) in a streaming fashion.
091 * </p>
092 * <p>
093 * <b>Example:</b>
094 * 
095 * <pre class="brush:java">
096 * File file = new File(&quot;xcals.xml&quot;);
097 * List&lt;ICalendar&gt; icals = new ArrayList&lt;ICalendar&gt;();
098 * XCalReader xcalReader = new XCalReader(file);
099 * ICalendar ical;
100 * while ((ical = xcalReader.readNext()) != null) {
101 *      icals.add(ical);
102 * }
103 * </pre>
104 * 
105 * </p>
106 * 
107 * <p>
108 * <b>Getting timezone information:</b>
109 * 
110 * <pre class="brush:java">
111 * XCalReader reader = ...
112 * ICalendar ical = reader.readNext();
113 * TimezoneInfo tzinfo = reader.getTimezoneInfo();
114 * 
115 * //get the VTIMEZONE components that were parsed
116 * //the VTIMEZONE components will NOT be in the ICalendar object
117 * Collection&ltVTimezone&gt; vtimezones = tzinfo.getComponents();
118 * 
119 * //get the timezone that a property was originally formatted in
120 * DateStart dtstart = ical.getEvents().get(0).getDateStart();
121 * TimeZone tz = tzinfo.getTimeZone(dtstart);
122 * </pre>
123 * 
124 * </p>
125 * @author Michael Angstadt
126 * @see <a href="http://tools.ietf.org/html/rfc6321">RFC 6321</a>
127 */
128public class XCalReader extends StreamReader {
129        private final Source source;
130        private final Closeable stream;
131
132        private volatile ICalendar readICal;
133        private volatile TransformerException thrown;
134
135        private final ReadThread thread = new ReadThread();
136        private final Object lock = new Object();
137        private final BlockingQueue<Object> readerBlock = new ArrayBlockingQueue<Object>(1);
138        private final BlockingQueue<Object> threadBlock = new ArrayBlockingQueue<Object>(1);
139
140        /**
141         * Creates an xCal reader.
142         * @param str the string to read the xCals from
143         */
144        public XCalReader(String str) {
145                this(new StringReader(str));
146        }
147
148        /**
149         * Creates an xCal reader.
150         * @param in the input stream to read the xCals from
151         */
152        public XCalReader(InputStream in) {
153                source = new StreamSource(in);
154                stream = in;
155        }
156
157        /**
158         * Creates an xCal reader.
159         * @param file the file to read the xCals from
160         * @throws FileNotFoundException if the file doesn't exist
161         */
162        public XCalReader(File file) throws FileNotFoundException {
163                this(new FileInputStream(file));
164        }
165
166        /**
167         * Creates an xCal reader.
168         * @param reader the reader to read from
169         */
170        public XCalReader(Reader reader) {
171                source = new StreamSource(reader);
172                stream = reader;
173        }
174
175        /**
176         * Creates an xCal reader.
177         * @param node the DOM node to read from
178         */
179        public XCalReader(Node node) {
180                source = new DOMSource(node);
181                stream = null;
182        }
183
184        @Override
185        protected ICalendar _readNext() throws IOException {
186                readICal = null;
187                warnings.clear();
188                context = new ParseContext();
189                tzinfo = new TimezoneInfo();
190                thrown = null;
191
192                if (!thread.started) {
193                        thread.start();
194                } else {
195                        if (thread.finished || thread.closed) {
196                                return null;
197                        }
198
199                        try {
200                                threadBlock.put(lock);
201                        } catch (InterruptedException e) {
202                                return null;
203                        }
204                }
205
206                //wait until thread reads xCard
207                try {
208                        readerBlock.take();
209                } catch (InterruptedException e) {
210                        return null;
211                }
212
213                if (thrown != null) {
214                        throw new IOException(thrown);
215                }
216
217                return readICal;
218        }
219
220        private class ReadThread extends Thread {
221                private final SAXResult result;
222                private final Transformer transformer;
223                private volatile boolean finished = false, started = false, closed = false;
224
225                public ReadThread() {
226                        setName(getClass().getSimpleName());
227
228                        //create the transformer
229                        try {
230                                transformer = TransformerFactory.newInstance().newTransformer();
231                        } catch (TransformerConfigurationException e) {
232                                //no complex configurations
233                                throw new RuntimeException(e);
234                        }
235
236                        //prevent error messages from being printed to stderr
237                        transformer.setErrorListener(new ErrorListener() {
238                                public void error(TransformerException e) {
239                                        //empty
240                                }
241
242                                public void fatalError(TransformerException e) {
243                                        //empty
244                                }
245
246                                public void warning(TransformerException e) {
247                                        //empty
248                                }
249                        });
250
251                        result = new SAXResult(new ContentHandlerImpl());
252                }
253
254                @Override
255                public void run() {
256                        started = true;
257
258                        try {
259                                transformer.transform(source, result);
260                        } catch (TransformerException e) {
261                                if (!thread.closed) {
262                                        thrown = e;
263                                }
264                        } finally {
265                                finished = true;
266                                try {
267                                        readerBlock.put(lock);
268                                } catch (InterruptedException e) {
269                                        //ignore
270                                }
271                        }
272                }
273        }
274
275        private class ContentHandlerImpl extends DefaultHandler {
276                private final Document DOC = XmlUtils.createDocument();
277                private final XCalStructure structure = new XCalStructure();
278                private final StringBuilder characterBuffer = new StringBuilder();
279                private final LinkedList<ICalComponent> componentStack = new LinkedList<ICalComponent>();
280
281                private Element propertyElement, parent;
282                private QName paramName;
283                private ICalComponent curComponent;
284                private ICalParameters parameters;
285
286                @Override
287                public void characters(char[] buffer, int start, int length) throws SAXException {
288                        characterBuffer.append(buffer, start, length);
289                }
290
291                @Override
292                public void startElement(String namespace, String localName, String qName, Attributes attributes) throws SAXException {
293                        QName qname = new QName(namespace, localName);
294                        String textContent = characterBuffer.toString();
295                        characterBuffer.setLength(0);
296
297                        if (structure.isEmpty()) {
298                                //<icalendar>
299                                if (ICALENDAR.equals(qname)) {
300                                        structure.push(ElementType.icalendar);
301                                }
302                                return;
303                        }
304
305                        ElementType parentType = structure.peek();
306                        ElementType typeToPush = null;
307                        //System.out.println(structure.stack + " current: " + localName);
308                        if (parentType != null) {
309                                switch (parentType) {
310
311                                case icalendar:
312                                        //<vcalendar>
313                                        if (VCALENDAR.equals(qname)) {
314                                                ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(localName, ICalVersion.V2_0);
315                                                ICalComponent component = scribe.emptyInstance();
316
317                                                curComponent = component;
318                                                readICal = (ICalendar) component;
319                                                typeToPush = ElementType.component;
320                                        }
321                                        break;
322
323                                case component:
324                                        if (PROPERTIES.equals(qname)) {
325                                                //<properties>
326                                                typeToPush = ElementType.properties;
327                                        } else if (COMPONENTS.equals(qname)) {
328                                                //<components>
329                                                componentStack.add(curComponent);
330                                                curComponent = null;
331
332                                                typeToPush = ElementType.components;
333                                        }
334                                        break;
335
336                                case components:
337                                        //start component element
338                                        if (XCAL_NS.equals(namespace)) {
339                                                ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(localName, ICalVersion.V2_0);
340                                                curComponent = scribe.emptyInstance();
341
342                                                ICalComponent parent = componentStack.getLast();
343                                                parent.addComponent(curComponent);
344
345                                                typeToPush = ElementType.component;
346                                        }
347                                        break;
348
349                                case properties:
350                                        //start property element
351                                        propertyElement = createElement(namespace, localName, attributes);
352                                        parameters = new ICalParameters();
353                                        parent = propertyElement;
354                                        typeToPush = ElementType.property;
355                                        break;
356
357                                case property:
358                                        //<parameters>
359                                        if (PARAMETERS.equals(qname)) {
360                                                typeToPush = ElementType.parameters;
361                                        }
362                                        break;
363
364                                case parameters:
365                                        //inside of <parameters>
366                                        if (XCAL_NS.equals(namespace)) {
367                                                paramName = qname;
368                                                typeToPush = ElementType.parameter;
369                                        }
370                                        break;
371
372                                case parameter:
373                                        //inside of a parameter element
374                                        if (XCAL_NS.equals(namespace)) {
375                                                typeToPush = ElementType.parameterValue;
376                                        }
377                                        break;
378                                case parameterValue:
379                                        //should never have child elements
380                                        break;
381                                }
382                        }
383
384                        //append element to property element
385                        if (propertyElement != null && typeToPush != ElementType.property && typeToPush != ElementType.parameters && !structure.isUnderParameters()) {
386                                if (textContent.length() > 0) {
387                                        parent.appendChild(DOC.createTextNode(textContent));
388                                }
389
390                                Element element = createElement(namespace, localName, attributes);
391                                parent.appendChild(element);
392                                parent = element;
393                        }
394
395                        structure.push(typeToPush);
396                }
397
398                @Override
399                public void endElement(String namespace, String localName, String qName) throws SAXException {
400                        String textContent = characterBuffer.toString();
401                        characterBuffer.setLength(0);
402
403                        if (structure.isEmpty()) {
404                                //no <icalendar> elements were read yet
405                                return;
406                        }
407
408                        ElementType type = structure.pop();
409                        if (type == null && (propertyElement == null || structure.isUnderParameters())) {
410                                //it's a non-xCal element
411                                return;
412                        }
413
414                        //System.out.println(structure.stack + " ending: " + localName);
415                        if (type != null) {
416                                switch (type) {
417                                case parameterValue:
418                                        parameters.put(paramName.getLocalPart(), textContent);
419                                        break;
420
421                                case parameter:
422                                        //do nothing
423                                        break;
424
425                                case parameters:
426                                        //do nothing
427                                        break;
428
429                                case property:
430                                        context.getWarnings().clear();
431
432                                        propertyElement.appendChild(DOC.createTextNode(textContent));
433
434                                        //unmarshal property and add to parent component
435                                        QName propertyQName = new QName(propertyElement.getNamespaceURI(), propertyElement.getLocalName());
436                                        String propertyName = localName;
437                                        ICalPropertyScribe<? extends ICalProperty> scribe = index.getPropertyScribe(propertyQName);
438                                        try {
439                                                ICalProperty property = scribe.parseXml(propertyElement, parameters, context);
440                                                if (property instanceof Version && curComponent instanceof ICalendar) {
441                                                        Version versionProp = (Version) property;
442                                                        ICalVersion version = versionProp.toICalVersion();
443                                                        if (version != null) {
444                                                                ICalendar ical = (ICalendar) curComponent;
445                                                                ical.setVersion(version);
446                                                                context.setVersion(version);
447
448                                                                propertyElement = null;
449                                                                break;
450                                                        }
451                                                }
452
453                                                curComponent.addProperty(property);
454                                                for (Warning warning : context.getWarnings()) {
455                                                        warnings.add(null, propertyName, warning);
456                                                }
457                                        } catch (SkipMeException e) {
458                                                warnings.add(null, propertyName, 22, e.getMessage());
459                                        } catch (CannotParseException e) {
460                                                String xml = XmlUtils.toString(propertyElement);
461                                                warnings.add(null, propertyName, 33, xml, e.getMessage());
462
463                                                scribe = index.getPropertyScribe(Xml.class);
464                                                ICalProperty property = scribe.parseXml(propertyElement, parameters, context);
465                                                curComponent.addProperty(property);
466                                        }
467
468                                        propertyElement = null;
469                                        break;
470
471                                case component:
472                                        curComponent = null;
473
474                                        //</vcalendar>
475                                        if (VCALENDAR.getNamespaceURI().equals(namespace) && VCALENDAR.getLocalPart().equals(localName)) {
476                                                //wait for readNext() to be called again
477                                                try {
478                                                        readerBlock.put(lock);
479                                                        threadBlock.take();
480                                                } catch (InterruptedException e) {
481                                                        throw new SAXException(e);
482                                                }
483                                                return;
484                                        }
485                                        break;
486
487                                case properties:
488                                        break;
489
490                                case components:
491                                        curComponent = componentStack.removeLast();
492                                        break;
493
494                                case icalendar:
495                                        break;
496                                }
497                        }
498
499                        //append element to property element
500                        if (propertyElement != null && type != ElementType.property && type != ElementType.parameters && !structure.isUnderParameters()) {
501                                if (textContent.length() > 0) {
502                                        parent.appendChild(DOC.createTextNode(textContent));
503                                }
504                                parent = (Element) parent.getParentNode();
505                        }
506                }
507
508                private Element createElement(String namespace, String localName, Attributes attributes) {
509                        Element element = DOC.createElementNS(namespace, localName);
510
511                        //copy the attributes
512                        for (int i = 0; i < attributes.getLength(); i++) {
513                                String qname = attributes.getQName(i);
514                                if (qname.startsWith("xmlns:")) {
515                                        continue;
516                                }
517
518                                String name = attributes.getLocalName(i);
519                                String value = attributes.getValue(i);
520                                element.setAttribute(name, value);
521                        }
522
523                        return element;
524                }
525        }
526
527        private enum ElementType {
528                //a value is missing for "vcalendar" because it is treated as a "component"
529                //enum values are lower-case so they won't get confused with the "XCalQNames" variable names
530                icalendar, components, properties, component, property, parameters, parameter, parameterValue;
531        }
532
533        /**
534         * <p>
535         * Keeps track of the structure of an xCal XML document.
536         * </p>
537         * 
538         * <p>
539         * Note that this class is here because you can't just do QName comparisons
540         * on a one-by-one basis. The location of an XML element within the XML
541         * document is important too. It's possible for two elements to have the
542         * same QName, but be treated differently depending on their location (e.g.
543         * the {@code <duration>} property has a {@code <duration>} data type)
544         * </p>
545         */
546        private static class XCalStructure {
547                private final List<ElementType> stack = new ArrayList<ElementType>();
548
549                /**
550                 * Pops the top element type off the stack.
551                 * @return the element type or null if the stack is empty
552                 */
553                public ElementType pop() {
554                        return stack.isEmpty() ? null : stack.remove(stack.size() - 1);
555                }
556
557                /**
558                 * Looks at the top element type.
559                 * @return the top element type or null if the stack is empty
560                 */
561                public ElementType peek() {
562                        return stack.isEmpty() ? null : stack.get(stack.size() - 1);
563                }
564
565                /**
566                 * Adds an element type to the stack.
567                 * @param type the type to add or null if the XML element is not an xCal
568                 * element
569                 */
570                public void push(ElementType type) {
571                        stack.add(type);
572                }
573
574                /**
575                 * Determines if the leaf node is under a {@code <parameters>} element.
576                 * @return true if it is, false if not
577                 */
578                public boolean isUnderParameters() {
579                        //get the first non-null type
580                        ElementType nonNull = null;
581                        for (int i = stack.size() - 1; i >= 0; i--) {
582                                ElementType type = stack.get(i);
583                                if (type != null) {
584                                        nonNull = type;
585                                        break;
586                                }
587                        }
588
589                        return nonNull == ElementType.parameters || nonNull == ElementType.parameter || nonNull == ElementType.parameterValue;
590                }
591
592                /**
593                 * Determines if the stack is empty
594                 * @return true if the stack is empty, false if not
595                 */
596                public boolean isEmpty() {
597                        return stack.isEmpty();
598                }
599        }
600
601        /**
602         * Closes the underlying input stream.
603         */
604        public void close() throws IOException {
605                if (thread.isAlive()) {
606                        thread.closed = true;
607                        thread.interrupt();
608                }
609
610                if (stream != null) {
611                        stream.close();
612                }
613        }
614}