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