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