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 iCalendar objects) in a streaming fashion.
091 * </p>
092 * <p>
093 * <b>Example:</b>
094 * 
095 * <pre class="brush:java">
096 * File file = new File("icals.xml");
097 * XCalReader reader = null;
098 * try {
099 *   reader = new XCalReader(file);
100 *   ICalendar ical;
101 *   while ((ical = reader.readNext()) != null){
102 *         ...
103 *   }
104 * } finally {
105 *   if (reader != null) reader.close();
106 * }
107 * </pre>
108 * 
109 * </p>
110 * 
111 * <p>
112 * <b>Getting timezone information:</b>
113 * 
114 * <pre class="brush:java">
115 * XCalReader reader = ...
116 * ICalendar ical = reader.readNext();
117 * TimezoneInfo tzinfo = reader.getTimezoneInfo();
118 * 
119 * //get the VTIMEZONE components that were parsed
120 * //the VTIMEZONE components will NOT be in the ICalendar object
121 * Collection&ltVTimezone&gt; vtimezones = tzinfo.getComponents();
122 * 
123 * //get the timezone that a property was originally formatted in
124 * DateStart dtstart = ical.getEvents().get(0).getDateStart();
125 * TimeZone tz = tzinfo.getTimeZone(dtstart);
126 * </pre>
127 * 
128 * </p>
129 * @author Michael Angstadt
130 * @see <a href="http://tools.ietf.org/html/rfc6321">RFC 6321</a>
131 */
132public class XCalReader extends StreamReader {
133        private final Source source;
134        private final Closeable stream;
135
136        private volatile ICalendar readICal;
137        private volatile TransformerException thrown;
138
139        private final ReadThread thread = new ReadThread();
140        private final Object lock = new Object();
141        private final BlockingQueue<Object> readerBlock = new ArrayBlockingQueue<Object>(1);
142        private final BlockingQueue<Object> threadBlock = new ArrayBlockingQueue<Object>(1);
143
144        /**
145         * @param str the string to read from
146         */
147        public XCalReader(String str) {
148                this(new StringReader(str));
149        }
150
151        /**
152         * @param in the input stream to read from
153         */
154        public XCalReader(InputStream in) {
155                source = new StreamSource(in);
156                stream = in;
157        }
158
159        /**
160         * @param file the file to read from
161         * @throws FileNotFoundException if the file doesn't exist
162         */
163        public XCalReader(File file) throws FileNotFoundException {
164                this(new FileInputStream(file));
165        }
166
167        /**
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         * @param node the DOM node to read from
177         */
178        public XCalReader(Node node) {
179                source = new DOMSource(node);
180                stream = null;
181        }
182
183        @Override
184        protected ICalendar _readNext() throws IOException {
185                readICal = null;
186                warnings.clear();
187                context = new ParseContext();
188                tzinfo = new TimezoneInfo();
189                thrown = null;
190
191                if (!thread.started) {
192                        thread.start();
193                } else {
194                        if (thread.finished || thread.closed) {
195                                return null;
196                        }
197
198                        try {
199                                threadBlock.put(lock);
200                        } catch (InterruptedException e) {
201                                return null;
202                        }
203                }
204
205                //wait until thread reads xCard
206                try {
207                        readerBlock.take();
208                } catch (InterruptedException e) {
209                        return null;
210                }
211
212                if (thrown != null) {
213                        throw new IOException(thrown);
214                }
215
216                return readICal;
217        }
218
219        private class ReadThread extends Thread {
220                private final SAXResult result;
221                private final Transformer transformer;
222                private volatile boolean finished = false, started = false, closed = false;
223
224                public ReadThread() {
225                        setName(getClass().getSimpleName());
226
227                        //create the transformer
228                        try {
229                                transformer = TransformerFactory.newInstance().newTransformer();
230                        } catch (TransformerConfigurationException e) {
231                                //shouldn't be thrown because it's a simple configuration
232                                throw new RuntimeException(e);
233                        }
234
235                        //prevent error messages from being printed to stderr
236                        transformer.setErrorListener(new NoOpErrorListener());
237
238                        result = new SAXResult(new ContentHandlerImpl());
239                }
240
241                @Override
242                public void run() {
243                        started = true;
244
245                        try {
246                                transformer.transform(source, result);
247                        } catch (TransformerException e) {
248                                if (!thread.closed) {
249                                        thrown = e;
250                                }
251                        } finally {
252                                finished = true;
253                                try {
254                                        readerBlock.put(lock);
255                                } catch (InterruptedException e) {
256                                        //ignore
257                                }
258                        }
259                }
260        }
261
262        private class ContentHandlerImpl extends DefaultHandler {
263                private final Document DOC = XmlUtils.createDocument();
264                private final XCalStructure structure = new XCalStructure();
265                private final StringBuilder characterBuffer = new StringBuilder();
266                private final LinkedList<ICalComponent> componentStack = new LinkedList<ICalComponent>();
267
268                private Element propertyElement, parent;
269                private QName paramName;
270                private ICalComponent curComponent;
271                private ICalParameters parameters;
272
273                @Override
274                public void characters(char[] buffer, int start, int length) throws SAXException {
275                        characterBuffer.append(buffer, start, length);
276                }
277
278                @Override
279                public void startElement(String namespace, String localName, String qName, Attributes attributes) throws SAXException {
280                        QName qname = new QName(namespace, localName);
281                        String textContent = emptyCharacterBuffer();
282
283                        if (structure.isEmpty()) {
284                                //<icalendar>
285                                if (ICALENDAR.equals(qname)) {
286                                        structure.push(ElementType.icalendar);
287                                }
288                                return;
289                        }
290
291                        ElementType parentType = structure.peek();
292                        ElementType typeToPush = null;
293                        if (parentType != null) {
294                                switch (parentType) {
295
296                                case icalendar:
297                                        //<vcalendar>
298                                        if (VCALENDAR.equals(qname)) {
299                                                ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(localName, ICalVersion.V2_0);
300                                                ICalComponent component = scribe.emptyInstance();
301
302                                                curComponent = component;
303                                                readICal = (ICalendar) component;
304                                                typeToPush = ElementType.component;
305                                        }
306                                        break;
307
308                                case component:
309                                        if (PROPERTIES.equals(qname)) {
310                                                //<properties>
311                                                typeToPush = ElementType.properties;
312                                        } else if (COMPONENTS.equals(qname)) {
313                                                //<components>
314                                                componentStack.add(curComponent);
315                                                curComponent = null;
316
317                                                typeToPush = ElementType.components;
318                                        }
319                                        break;
320
321                                case components:
322                                        //start component element
323                                        if (XCAL_NS.equals(namespace)) {
324                                                ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(localName, ICalVersion.V2_0);
325                                                curComponent = scribe.emptyInstance();
326
327                                                ICalComponent parent = componentStack.getLast();
328                                                parent.addComponent(curComponent);
329
330                                                typeToPush = ElementType.component;
331                                        }
332                                        break;
333
334                                case properties:
335                                        //start property element
336                                        propertyElement = createElement(namespace, localName, attributes);
337                                        parameters = new ICalParameters();
338                                        parent = propertyElement;
339                                        typeToPush = ElementType.property;
340                                        break;
341
342                                case property:
343                                        //<parameters>
344                                        if (PARAMETERS.equals(qname)) {
345                                                typeToPush = ElementType.parameters;
346                                        }
347                                        break;
348
349                                case parameters:
350                                        //inside of <parameters>
351                                        if (XCAL_NS.equals(namespace)) {
352                                                paramName = qname;
353                                                typeToPush = ElementType.parameter;
354                                        }
355                                        break;
356
357                                case parameter:
358                                        //inside of a parameter element
359                                        if (XCAL_NS.equals(namespace)) {
360                                                typeToPush = ElementType.parameterValue;
361                                        }
362                                        break;
363                                case parameterValue:
364                                        //should never have child elements
365                                        break;
366                                }
367                        }
368
369                        //append element to property element
370                        if (propertyElement != null && typeToPush != ElementType.property && typeToPush != ElementType.parameters && !structure.isUnderParameters()) {
371                                if (textContent.length() > 0) {
372                                        parent.appendChild(DOC.createTextNode(textContent));
373                                }
374
375                                Element element = createElement(namespace, localName, attributes);
376                                parent.appendChild(element);
377                                parent = element;
378                        }
379
380                        structure.push(typeToPush);
381                }
382
383                @Override
384                public void endElement(String namespace, String localName, String qName) throws SAXException {
385                        String textContent = emptyCharacterBuffer();
386
387                        if (structure.isEmpty()) {
388                                //no <icalendar> elements were read yet
389                                return;
390                        }
391
392                        ElementType type = structure.pop();
393                        if (type == null && (propertyElement == null || structure.isUnderParameters())) {
394                                //it's a non-xCal element
395                                return;
396                        }
397
398                        if (type != null) {
399                                switch (type) {
400                                case parameterValue:
401                                        parameters.put(paramName.getLocalPart(), textContent);
402                                        break;
403
404                                case parameter:
405                                        //do nothing
406                                        break;
407
408                                case parameters:
409                                        //do nothing
410                                        break;
411
412                                case property:
413                                        context.getWarnings().clear();
414
415                                        propertyElement.appendChild(DOC.createTextNode(textContent));
416
417                                        //unmarshal property and add to parent component
418                                        QName propertyQName = new QName(propertyElement.getNamespaceURI(), propertyElement.getLocalName());
419                                        String propertyName = localName;
420                                        ICalPropertyScribe<? extends ICalProperty> scribe = index.getPropertyScribe(propertyQName);
421                                        try {
422                                                ICalProperty property = scribe.parseXml(propertyElement, parameters, context);
423                                                if (property instanceof Version && curComponent instanceof ICalendar) {
424                                                        Version versionProp = (Version) property;
425                                                        ICalVersion version = versionProp.toICalVersion();
426                                                        if (version != null) {
427                                                                ICalendar ical = (ICalendar) curComponent;
428                                                                ical.setVersion(version);
429                                                                context.setVersion(version);
430
431                                                                propertyElement = null;
432                                                                break;
433                                                        }
434                                                }
435
436                                                curComponent.addProperty(property);
437                                                for (Warning warning : context.getWarnings()) {
438                                                        warnings.add(null, propertyName, warning);
439                                                }
440                                        } catch (SkipMeException e) {
441                                                warnings.add(null, propertyName, 22, e.getMessage());
442                                        } catch (CannotParseException e) {
443                                                String xml = XmlUtils.toString(propertyElement);
444                                                warnings.add(null, propertyName, 33, xml, e.getMessage());
445
446                                                scribe = index.getPropertyScribe(Xml.class);
447                                                ICalProperty property = scribe.parseXml(propertyElement, parameters, context);
448                                                curComponent.addProperty(property);
449                                        }
450
451                                        propertyElement = null;
452                                        break;
453
454                                case component:
455                                        curComponent = null;
456
457                                        //</vcalendar>
458                                        if (VCALENDAR.getNamespaceURI().equals(namespace) && VCALENDAR.getLocalPart().equals(localName)) {
459                                                //wait for readNext() to be called again
460                                                try {
461                                                        readerBlock.put(lock);
462                                                        threadBlock.take();
463                                                } catch (InterruptedException e) {
464                                                        throw new SAXException(e);
465                                                }
466                                                return;
467                                        }
468                                        break;
469
470                                case properties:
471                                        break;
472
473                                case components:
474                                        curComponent = componentStack.removeLast();
475                                        break;
476
477                                case icalendar:
478                                        break;
479                                }
480                        }
481
482                        //append element to property element
483                        if (propertyElement != null && type != ElementType.property && type != ElementType.parameters && !structure.isUnderParameters()) {
484                                if (textContent.length() > 0) {
485                                        parent.appendChild(DOC.createTextNode(textContent));
486                                }
487                                parent = (Element) parent.getParentNode();
488                        }
489                }
490
491                private String emptyCharacterBuffer() {
492                        String textContent = characterBuffer.toString();
493                        characterBuffer.setLength(0);
494                        return textContent;
495                }
496
497                private Element createElement(String namespace, String localName, Attributes attributes) {
498                        Element element = DOC.createElementNS(namespace, localName);
499                        applyAttributesTo(element, attributes);
500                        return element;
501                }
502
503                private void applyAttributesTo(Element element, Attributes attributes) {
504                        for (int i = 0; i < attributes.getLength(); i++) {
505                                String qname = attributes.getQName(i);
506                                if (qname.startsWith("xmlns:")) {
507                                        continue;
508                                }
509
510                                String name = attributes.getLocalName(i);
511                                String value = attributes.getValue(i);
512                                element.setAttribute(name, value);
513                        }
514                }
515        }
516
517        private enum ElementType {
518                //a value is missing for "vcalendar" because it is treated as a "component"
519                //enum values are lower-case so they won't get confused with the "XCalQNames" variable names
520                icalendar, components, properties, component, property, parameters, parameter, parameterValue;
521        }
522
523        /**
524         * <p>
525         * Keeps track of the structure of an xCal XML document.
526         * </p>
527         * 
528         * <p>
529         * Note that this class is here because you can't just do QName comparisons
530         * on a one-by-one basis. The location of an XML element within the XML
531         * document is important too. It's possible for two elements to have the
532         * same QName, but be treated differently depending on their location (e.g.
533         * the {@code <duration>} property has a {@code <duration>} data type)
534         * </p>
535         */
536        private static class XCalStructure {
537                private final List<ElementType> stack = new ArrayList<ElementType>();
538
539                /**
540                 * Pops the top element type off the stack.
541                 * @return the element type or null if the stack is empty
542                 */
543                public ElementType pop() {
544                        return isEmpty() ? null : stack.remove(stack.size() - 1);
545                }
546
547                /**
548                 * Looks at the top element type.
549                 * @return the top element type or null if the stack is empty
550                 */
551                public ElementType peek() {
552                        return isEmpty() ? null : stack.get(stack.size() - 1);
553                }
554
555                /**
556                 * Adds an element type to the stack.
557                 * @param type the type to add or null if the XML element is not an xCal
558                 * element
559                 */
560                public void push(ElementType type) {
561                        stack.add(type);
562                }
563
564                /**
565                 * Determines if the leaf node is under a {@code <parameters>} element.
566                 * @return true if it is, false if not
567                 */
568                public boolean isUnderParameters() {
569                        //get the first non-null type
570                        ElementType nonNull = null;
571                        for (int i = stack.size() - 1; i >= 0; i--) {
572                                ElementType type = stack.get(i);
573                                if (type != null) {
574                                        nonNull = type;
575                                        break;
576                                }
577                        }
578
579                        //@formatter:off
580                        return
581                        nonNull == ElementType.parameters ||
582                        nonNull == ElementType.parameter ||
583                        nonNull == ElementType.parameterValue;
584                        //@formatter:on
585                }
586
587                /**
588                 * Determines if the stack is empty
589                 * @return true if the stack is empty, false if not
590                 */
591                public boolean isEmpty() {
592                        return stack.isEmpty();
593                }
594        }
595
596        /**
597         * An implementation of {@link ErrorListener} that doesn't do anything.
598         */
599        private static class NoOpErrorListener implements ErrorListener {
600                public void error(TransformerException e) {
601                        //do nothing
602                }
603
604                public void fatalError(TransformerException e) {
605                        //do nothing
606                }
607
608                public void warning(TransformerException e) {
609                        //do nothing
610                }
611        }
612
613        /**
614         * Closes the underlying input stream.
615         */
616        public void close() throws IOException {
617                if (thread.isAlive()) {
618                        thread.closed = true;
619                        thread.interrupt();
620                }
621
622                if (stream != null) {
623                        stream.close();
624                }
625        }
626}