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.IOException;
011import java.io.InputStream;
012import java.io.Reader;
013import java.io.StringReader;
014import java.nio.file.Files;
015import java.nio.file.Path;
016import java.util.ArrayList;
017import java.util.List;
018import java.util.concurrent.ArrayBlockingQueue;
019import java.util.concurrent.BlockingQueue;
020import java.util.stream.IntStream;
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-2026, 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 * Path file = Paths.get("vcards.xml");
093 * try (XCardReader reader = new XCardReader(file)) {
094 *   VCard vcard;
095 *   while ((vcard = reader.readNext()) != null) {
096 *     //...
097 *   }
098 * }
099 * </pre>
100 * 
101 * @author Michael Angstadt
102 * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a>
103 */
104public class XCardReader extends StreamReader {
105        private final VCardVersion version = VCardVersion.V4_0;
106        private final String NS = version.getXmlNamespace();
107
108        private final Source source;
109        private final Closeable stream;
110
111        private volatile VCard readVCard;
112        private volatile TransformerException thrown;
113
114        private final ReadThread thread = new ReadThread();
115        private final Object lock = new Object();
116        private final BlockingQueue<Object> readerBlock = new ArrayBlockingQueue<>(1);
117        private final BlockingQueue<Object> threadBlock = new ArrayBlockingQueue<>(1);
118
119        /**
120         * @param xml the XML to read from
121         */
122        public XCardReader(String xml) {
123                this(new StringReader(xml));
124        }
125
126        /**
127         * @param in the input stream to read from
128         */
129        public XCardReader(InputStream in) {
130                source = new StreamSource(in);
131                stream = in;
132        }
133
134        /**
135         * @param file the file to read from
136         * @throws IOException if there is a problem opening the file
137         */
138        public XCardReader(Path file) throws IOException {
139                this(new BufferedInputStream(Files.newInputStream(file)));
140        }
141
142        /**
143         * @param reader the reader to read from
144         */
145        public XCardReader(Reader reader) {
146                source = new StreamSource(reader);
147                stream = reader;
148        }
149
150        /**
151         * @param node the DOM node to read from
152         */
153        public XCardReader(Node node) {
154                source = new DOMSource(node);
155                stream = null;
156        }
157
158        @Override
159        protected VCard _readNext() throws IOException {
160                readVCard = null;
161                thrown = null;
162                context.setVersion(version);
163
164                if (!thread.started) {
165                        thread.start();
166                } else {
167                        if (thread.finished || thread.closed) {
168                                return null;
169                        }
170
171                        try {
172                                threadBlock.put(lock);
173                        } catch (InterruptedException e) {
174                                Thread.currentThread().interrupt();
175                                return null;
176                        }
177                }
178
179                //wait until thread reads xCard
180                try {
181                        readerBlock.take();
182                } catch (InterruptedException e) {
183                        Thread.currentThread().interrupt();
184                        return null;
185                }
186
187                if (thrown != null) {
188                        throw new IOException(thrown);
189                }
190
191                return readVCard;
192        }
193
194        private class ReadThread extends Thread {
195                private final SAXResult result;
196                private final Transformer transformer;
197                private volatile boolean finished = false;
198                private volatile boolean started = false;
199                private volatile boolean 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                                        Thread.currentThread().interrupt();
237                                        //ignore
238                                }
239                        }
240                }
241        }
242
243        private class ContentHandlerImpl extends DefaultHandler {
244                private final Document DOC = XmlUtils.createDocument();
245                private final XCardStructure structure = new XCardStructure();
246                private final ClearableStringBuilder characterBuffer = new ClearableStringBuilder();
247
248                private String group;
249                private Element propertyElement;
250                private Element parent;
251                private QName paramName;
252                private VCardParameters parameters;
253
254                @Override
255                public void characters(char[] buffer, int start, int length) throws SAXException {
256                        /*
257                         * Ignore all text nodes that are outside of a property element. All
258                         * valid text nodes will be inside of property elements (parameter
259                         * values and property values)
260                         */
261                        if (propertyElement == null) {
262                                return;
263                        }
264
265                        characterBuffer.append(buffer, start, length);
266                }
267
268                @Override
269                public void startElement(String namespace, String localName, String qName, Attributes attributes) throws SAXException {
270                        QName qname = new QName(namespace, localName);
271                        String textContent = characterBuffer.getAndClear();
272
273                        //ignore all XML until a <vcards> elements is read
274                        if (!isInsideVCardsElement(qname)) {
275                                return;
276                        }
277
278                        ElementType parentType = structure.peek();
279                        ElementType typeToPush = (parentType == null) ? null : processStartElement(parentType, qname, attributes);
280
281                        if (shouldElementBeAppendedToPropertyElement(typeToPush)) {
282                                appendNonEmptyTextContentToParent(textContent);
283
284                                Element element = createElement(namespace, localName, attributes);
285                                parent.appendChild(element);
286                                parent = element;
287                        }
288
289                        structure.push(typeToPush);
290                }
291
292                private boolean isInsideVCardsElement(QName qname) {
293                        if (!structure.isEmpty()) {
294                                return true;
295                        }
296
297                        if (VCARDS.equals(qname)) {
298                                structure.push(ElementType.vcards);
299                        }
300
301                        return false;
302                }
303
304                private ElementType processStartElement(ElementType parentType, QName qname, Attributes attributes) {
305                        switch (parentType) {
306                        case vcards:
307                                //<vcard>
308                                if (VCARD.equals(qname)) {
309                                        readVCard = new VCard();
310                                        readVCard.setVersion(version);
311                                        return ElementType.vcard;
312                                }
313                                break;
314
315                        case vcard:
316                                //<group>
317                                if (GROUP.equals(qname)) {
318                                        group = attributes.getValue("name");
319                                        return ElementType.group;
320                                } else {
321                                        propertyElement = createElement(qname.getNamespaceURI(), qname.getLocalPart(), attributes);
322                                        parameters = new VCardParameters();
323                                        parent = propertyElement;
324                                        return ElementType.property;
325                                }
326
327                        case group:
328                                propertyElement = createElement(qname.getNamespaceURI(), qname.getLocalPart(), attributes);
329                                parameters = new VCardParameters();
330                                parent = propertyElement;
331                                return ElementType.property;
332
333                        case property:
334                                //<parameters>
335                                if (PARAMETERS.equals(qname)) {
336                                        return ElementType.parameters;
337                                }
338                                break;
339
340                        case parameters:
341                                //inside of <parameters>
342                                if (NS.equals(qname.getNamespaceURI())) {
343                                        paramName = qname;
344                                        return ElementType.parameter;
345                                }
346                                break;
347
348                        case parameter:
349                                if (NS.equals(qname.getNamespaceURI())) {
350                                        return ElementType.parameterValue;
351                                }
352                                break;
353
354                        case parameterValue:
355                                //should never have child elements
356                                break;
357                        }
358
359                        return null;
360                }
361
362                @Override
363                public void endElement(String namespace, String localName, String qName) throws SAXException {
364                        String textContent = characterBuffer.getAndClear();
365
366                        //ignore all XML until a <vcards> elements is read
367                        boolean isInsideVCardsElement = !structure.isEmpty();
368                        if (!isInsideVCardsElement) {
369                                return;
370                        }
371
372                        ElementType type = structure.pop();
373                        boolean isNotAnXCardElement = (type == null && (propertyElement == null || structure.isUnderParameters()));
374                        if (isNotAnXCardElement) {
375                                return;
376                        }
377
378                        if (type != null) {
379                                processEndElement(type, localName, textContent);
380                        }
381
382                        if (shouldElementBeAppendedToPropertyElement(type)) {
383                                appendNonEmptyTextContentToParent(textContent);
384                                parent = (Element) parent.getParentNode();
385                        }
386                }
387
388                private boolean shouldElementBeAppendedToPropertyElement(ElementType type) {
389                        return propertyElement != null && type != ElementType.property && type != ElementType.parameters && !structure.isUnderParameters();
390                }
391
392                private void appendNonEmptyTextContentToParent(String text) {
393                        if (!text.isEmpty()) {
394                                parent.appendChild(DOC.createTextNode(text));
395                        }
396                }
397
398                private void processEndElement(ElementType type, String localName, String textContent) throws SAXException {
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                                propertyElement.appendChild(DOC.createTextNode(textContent));
414
415                                String propertyName = localName;
416                                VCardProperty property;
417                                QName propertyQName = new QName(propertyElement.getNamespaceURI(), propertyElement.getLocalName());
418                                VCardPropertyScribe<? extends VCardProperty> scribe = index.getPropertyScribe(propertyQName);
419
420                                context.getWarnings().clear();
421                                context.setPropertyName(propertyName);
422                                try {
423                                        property = scribe.parseXml(propertyElement, parameters, context);
424                                        property.setGroup(group);
425                                        readVCard.addProperty(property);
426                                        warnings.addAll(context.getWarnings());
427                                } catch (SkipMeException e) {
428                                        //@formatter:off
429                                        warnings.add(new ParseWarning.Builder(context)
430                                                .message(22, e.getMessage())
431                                                .build()
432                                        );
433                                        //@formatter:on
434                                } catch (CannotParseException e) {
435                                        //@formatter:off
436                                        warnings.add(new ParseWarning.Builder(context)
437                                                .message(e)
438                                                .build()
439                                        );
440                                        //@formatter:on
441
442                                        scribe = index.getPropertyScribe(Xml.class);
443                                        property = scribe.parseXml(propertyElement, parameters, context);
444                                        property.setGroup(group);
445                                        readVCard.addProperty(property);
446                                } catch (EmbeddedVCardException e) {
447                                        //@formatter:off
448                                        warnings.add(new ParseWarning.Builder(context)
449                                                .message(34)
450                                                .build()
451                                        );
452                                        //@formatter:on
453                                }
454
455                                propertyElement = null;
456                                break;
457
458                        case group:
459                                group = null;
460                                break;
461
462                        case vcard:
463                                //wait for readNext() to be called again
464                                try {
465                                        readerBlock.put(lock);
466                                        threadBlock.take();
467                                } catch (InterruptedException e) {
468                                        Thread.currentThread().interrupt();
469                                        throw new SAXException(e);
470                                }
471                                break;
472
473                        case vcards:
474                                //do nothing
475                                break;
476                        }
477                }
478
479                private Element createElement(String namespace, String localName, Attributes attributes) {
480                        Element element = DOC.createElementNS(namespace, localName);
481                        applyAttributes(element, attributes);
482                        return element;
483                }
484
485                private void applyAttributes(Element element, Attributes attributes) {
486                        //@formatter:off
487                        IntStream.range(0, attributes.getLength())
488                                .filter(i -> !attributes.getQName(i).startsWith("xmlns:"))
489                        .forEach(i -> {
490                                String name = attributes.getLocalName(i);
491                                String value = attributes.getValue(i);
492                                element.setAttribute(name, value);
493                        });
494                        //@formatter:on
495                }
496        }
497
498        private enum ElementType {
499                //enum values are lower-case so they won't get confused with the "XCardQNames" variable names
500                vcards, vcard, group, property, parameters, parameter, parameterValue
501        }
502
503        /**
504         * <p>
505         * Keeps track of the structure of an xCard XML document.
506         * </p>
507         * 
508         * <p>
509         * Note that this class is here because you can't just do QName comparisons
510         * on a one-by-one basis. The location of an XML element within the XML
511         * document is important too. It's possible for two elements to have the
512         * same QName, but be treated differently depending on their location (e.g.
513         * a parameter named "parameters")
514         * </p>
515         */
516        private static class XCardStructure {
517                private final List<ElementType> stack = new ArrayList<>();
518
519                /**
520                 * Pops the top element type off the stack.
521                 * @return the element type or null if the stack is empty
522                 */
523                public ElementType pop() {
524                        return isEmpty() ? null : stack.remove(stack.size() - 1);
525                }
526
527                /**
528                 * Looks at the top element type.
529                 * @return the top element type or null if the stack is empty
530                 */
531                public ElementType peek() {
532                        return isEmpty() ? null : stack.get(stack.size() - 1);
533                }
534
535                /**
536                 * Adds an element type to the stack.
537                 * @param type the type to add or null if the XML element is not an
538                 * xCard element
539                 */
540                public void push(ElementType type) {
541                        stack.add(type);
542                }
543
544                /**
545                 * Determines if the leaf node is under a {@code <parameters>} element.
546                 * @return true if it is, false if not
547                 */
548                public boolean isUnderParameters() {
549                        //get the first non-null type
550                        ElementType nonNull = null;
551                        for (int i = stack.size() - 1; i >= 0; i--) {
552                                ElementType type = stack.get(i);
553                                if (type != null) {
554                                        nonNull = type;
555                                        break;
556                                }
557                        }
558
559                        //@formatter:off
560                        return
561                        nonNull == ElementType.parameters ||
562                        nonNull == ElementType.parameter ||
563                        nonNull == ElementType.parameterValue;
564                        //@formatter:on
565                }
566
567                /**
568                 * Determines if the stack is empty
569                 * @return true if the stack is empty, false if not
570                 */
571                public boolean isEmpty() {
572                        return stack.isEmpty();
573                }
574        }
575
576        /**
577         * An implementation of {@link ErrorListener} that doesn't do anything.
578         */
579        private static class NoOpErrorListener implements ErrorListener {
580                public void error(TransformerException e) {
581                        //do nothing
582                }
583
584                public void fatalError(TransformerException e) {
585                        //do nothing
586                }
587
588                public void warning(TransformerException e) {
589                        //do nothing
590                }
591        }
592
593        /**
594         * Closes the underlying input stream.
595         */
596        public void close() throws IOException {
597                if (thread.isAlive()) {
598                        thread.closed = true;
599                        thread.interrupt();
600                }
601
602                if (stream != null) {
603                        stream.close();
604                }
605        }
606}