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