XMLReader.java
001 /*
002  * Java GPX Library (jpx-3.1.0).
003  * Copyright (c) 2016-2023 Franz Wilhelmstötter
004  *
005  * Licensed under the Apache License, Version 2.0 (the "License");
006  * you may not use this file except in compliance with the License.
007  * You may obtain a copy of the License at
008  *
009  *      http://www.apache.org/licenses/LICENSE-2.0
010  *
011  * Unless required by applicable law or agreed to in writing, software
012  * distributed under the License is distributed on an "AS IS" BASIS,
013  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014  * See the License for the specific language governing permissions and
015  * limitations under the License.
016  *
017  * Author:
018  *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
019  */
020 package io.jenetics.jpx;
021 
022 import static java.lang.String.format;
023 import static java.util.Arrays.asList;
024 import static java.util.Objects.requireNonNull;
025 import static javax.xml.stream.XMLStreamConstants.CDATA;
026 import static javax.xml.stream.XMLStreamConstants.CHARACTERS;
027 import static javax.xml.stream.XMLStreamConstants.COMMENT;
028 import static javax.xml.stream.XMLStreamConstants.END_DOCUMENT;
029 import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
030 import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
031 import static io.jenetics.jpx.Lists.copyOf;
032 
033 import java.util.ArrayList;
034 import java.util.HashMap;
035 import java.util.List;
036 import java.util.Map;
037 import java.util.Objects;
038 import java.util.function.Function;
039 import java.util.stream.IntStream;
040 import java.util.stream.Stream;
041 
042 import javax.xml.stream.XMLStreamException;
043 import javax.xml.stream.XMLStreamReader;
044 
045 import org.w3c.dom.Document;
046 
047 import io.jenetics.jpx.XMLReader.Type;
048 
049 /**
050  * Simplifies the usage of the {@link XMLStreamReader}.
051  *
052  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
053  @version 1.5
054  @since 1.0
055  */
056 abstract class XMLReader<T> {
057 
058     /**
059      * Represents the XML element type.
060      */
061     enum Type {
062 
063         /**
064          * Denotes a element reader.
065          */
066         ELEM,
067 
068         /**
069          * Denotes a element attribute reader.
070          */
071         ATTR,
072 
073         /**
074          * Denotes a reader of elements of the same type.
075          */
076         LIST,
077 
078         /**
079          * Denotes a reader of the text of a element.
080          */
081         TEXT
082 
083     }
084 
085     private final String _name;
086     private final Type _type;
087 
088     /**
089      * Create a new XML reader with the given name and type.
090      *
091      @param name the element name of the reader
092      @param type the element type of the reader
093      @throws NullPointerException if one of the give arguments is {@code null}
094      */
095     XMLReader(final String name, final Type type) {
096         _name = requireNonNull(name);
097         _type = requireNonNull(type);
098     }
099 
100     /**
101      * Read the given type from the underlying XML stream {@code reader}.
102      *
103      <pre>{@code
104      * try (AutoCloseableXMLStreamReader xml = XML.reader(in)) {
105      *     // Move XML stream to first element.
106      *     xml.next();
107      *     return reader.read(xml);
108      * }
109      * }</pre>
110      *
111      @param xml the underlying XML stream {@code reader}
112      @param lenient lenient read mode
113      @return the data read from the XML stream, maybe {@code null}
114      @throws XMLStreamException if an error occurs while reading the value
115      @throws NullPointerException if the given {@code xml} stream reader is
116      *         {@code null}
117      */
118     public abstract T read(final XMLStreamReaderAdapter xml, final boolean lenient)
119         throws XMLStreamException;
120 
121     /**
122      * Create a new reader for the new mapped type {@code B}.
123      *
124      @param mapper the mapper function
125      @param <B> the target type of the new reader
126      @return a new reader
127      @throws NullPointerException if the given {@code mapper} function is
128      *         {@code null}
129      */
130     public <B> XMLReader<B> map(final Function<? super T, ? extends B> mapper) {
131         return map(mapper, null);
132     }
133 
134     /**
135      * Create a new reader for the new mapped type {@code B}.
136      *
137      @param mapper the mapper function
138      @param devault the default value if the mapping function fails and the
139      *        reader is in {@link io.jenetics.jpx.GPX.Reader.Mode#LENIENT} mode
140      @param <B> the target type of the new reader
141      @return a new reader
142      @throws NullPointerException if the given {@code mapper} function is
143      *         {@code null}
144      */
145     public <B> XMLReader<B> map(final Function<? super T, ? extends B> mapper, B devault) {
146         requireNonNull(mapper);
147 
148         return new XMLReader<B>(_name, _type) {
149             @Override
150             public B read(final XMLStreamReaderAdapter xml, final boolean lenient)
151                 throws XMLStreamException
152             {
153                 try {
154                     return mapper.apply(XMLReader.this.read(xml, lenient));
155                 catch (RuntimeException e) {
156                     if (!lenient) {
157                         throw new XMLStreamException(
158                             format(
159                                 "Invalid value for '%s': %s",
160                                 _name, e.getMessage()
161                             ),
162                             e
163                         );
164                     else {
165                         return devault;
166                     }
167                 }
168             }
169         };
170     }
171 
172     /**
173      * Return the name of the element processed by this reader.
174      *
175      @return the element name the reader is processing
176      */
177     String name() {
178         return _name;
179     }
180 
181     /**
182      * Return the element type of the reader.
183      *
184      @return the element type of the reader
185      */
186     Type type() {
187         return _type;
188     }
189 
190     @Override
191     public String toString() {
192         return format("Reader[%s, %s]", name(), type());
193     }
194 
195 
196     /* *************************************************************************
197      * Static reader factory methods.
198      * ************************************************************************/
199 
200     /**
201      * Return a {@code Reader} for reading an attribute of an element.
202      <p>
203      <b>XML</b>
204      <pre> {@code <element length="3"/>}</pre>
205      *
206      <b>Reader definition</b>
207      <pre>{@code
208      * final Reader<Integer> reader =
209      *     elem(
210      *         v -> (Integer)v[0],
211      *         "element",
212      *         attr("length").map(Integer::parseInt)
213      *     );
214      * }</pre>
215      *
216      @param name the attribute name
217      @return an attribute reader
218      @throws NullPointerException if the given {@code name} is {@code null}
219      */
220     public static XMLReader<String> attr(final String name) {
221         return new AttrReader(name);
222     }
223 
224     /**
225      * Return a {@code Reader} for reading the text of an element.
226      <p>
227      <b>XML</b>
228      <pre> {@code <element>1234<element>}</pre>
229      *
230      <b>Reader definition</b>
231      <pre>{@code
232      * final Reader<Integer> reader =
233      *     elem(
234      *         v -> (Integer)v[0],
235      *         "element",
236      *         text().map(Integer::parseInt)
237      *     );
238      * }</pre>
239      *
240      @return an element text reader
241      */
242     public static XMLReader<String> text() {
243         return new TextReader();
244     }
245 
246     /**
247      * Return a {@code Reader} for reading an object of type {@code T} from the
248      * XML element with the given {@code name}.
249      *
250      <p>
251      <b>XML</b>
252      <pre> {@code <property name="size">1234<property>}</pre>
253      *
254      <b>Reader definition</b>
255      <pre>{@code
256      * final XMLReader<Property> reader =
257      *     elem(
258      *         v -> {
259      *             final String name = (String)v[0];
260      *             final Integer value = (Integer)v[1];
261      *             return Property.of(name, value);
262      *         },
263      *         "property",
264      *         attr("name"),
265      *         text().map(Integer::parseInt)
266      *     );
267      * }</pre>
268      *
269      @param generator the generator function, which build the result object
270      *        from the given parameter array
271      @param name the name of the root (sub-tree) element
272      @param children the child element reader, which creates the values
273      *        forwarded to the {@code generator} function
274      @param <T> the reader result type
275      @return a node reader
276      @throws NullPointerException if one of the given arguments is {@code null}
277      @throws IllegalArgumentException if the given child readers contains more
278      *         than one <em>text</em> reader
279      */
280     public static <T> XMLReader<T> elem(
281         final Function<Object[], T> generator,
282         final String name,
283         final XMLReader<?>... children
284     ) {
285         requireNonNull(name);
286         requireNonNull(generator);
287         Stream.of(requireNonNull(children)).forEach(Objects::requireNonNull);
288 
289         return new ElemReader<>(name, generator, asList(children), Type.ELEM);
290     }
291 
292     /**
293      * Return a {@code Reader} which reads the value from the child elements of
294      * the given parent element {@code name}.
295      <p>
296      <b>XML</b>
297      <pre> {@code <min><property name="size">1234<property></min>}</pre>
298      *
299      <b>Reader definition</b>
300      <pre>{@code
301      * final XMLReader<Property> reader =
302      *     elem("min",
303      *         elem(
304      *             v -> {
305      *                 final String name = (String)v[0];
306      *                 final Integer value = (Integer)v[1];
307      *                 return Property.of(name, value);
308      *             },
309      *             "property",
310      *             attr("name"),
311      *             text().map(Integer::parseInt)
312      *         )
313      *     );
314      * }</pre>
315      *
316      @param name the parent element name
317      @param reader the child elements reader
318      @param <T> the result type
319      @return a node reader
320      @throws NullPointerException if one of the given arguments is {@code null}
321      */
322     public static <T> XMLReader<T> elem(
323         final String name,
324         final XMLReader<? extends T> reader
325     ) {
326         requireNonNull(name);
327         requireNonNull(reader);
328 
329         return elem(
330             v -> {
331                 @SuppressWarnings("unchecked")
332                 T value = v.length > (T)v[0null;
333                 return value;
334             },
335             name,
336             reader
337         );
338     }
339 
340     public static XMLReader<String> elem(final String name) {
341         return elem(name, text());
342     }
343 
344     public static XMLReader<Object> ignore(final String name) {
345         return new IgnoreReader(name);
346     }
347 
348     /**
349      * Return a {@code XMLReader} which collects the elements, read by the given
350      * child {@code reader}, and returns it as list of these elements.
351      <p>
352      <b>XML</b>
353      <pre> {@code
354      <properties length="3">
355      *     <property>-1878762439</property>
356      *     <property>-957346595</property>
357      *     <property>-88668137</property>
358      </properties>
359      * }</pre>
360      *
361      <b>Reader definition</b>
362      <pre>{@code
363      * XMLReader<List<Integer>> reader =
364      *     elem(
365      *         v -> (List<Integer>)v[0],
366      *         "properties",
367      *         elems(elem("property", text().map(Integer::parseInt)))
368      *     );
369      * }</pre>
370      *
371      @param reader the child element reader
372      @param <T> the element type
373      @return a list reader
374      */
375     public static <T> XMLReader<List<T>> elems(final XMLReader<? extends T> reader) {
376         return new ListReader<T>(reader);
377     }
378 
379     public static XMLReader<Document> doc(final String name) {
380         return new DocReader(name);
381     }
382 }
383 
384 
385 /* *****************************************************************************
386  * XML reader implementations.
387  * ****************************************************************************/
388 
389 /**
390  * Reader implementation for reading the attribute of the current node.
391  *
392  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
393  @version 1.2
394  @since 1.2
395  */
396 final class AttrReader extends XMLReader<String> {
397 
398     AttrReader(final String name) {
399         super(name, Type.ATTR);
400     }
401 
402     @Override
403     public String read(final XMLStreamReaderAdapter xml, final boolean lenient)
404         throws XMLStreamException
405     {
406         xml.require(START_ELEMENT, null, null);
407         return xml.getAttributeValue(null, name());
408     }
409 
410 }
411 
412 /**
413  * Reader implementation for reading the text of the current node.
414  *
415  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
416  @version 1.2
417  @since 1.2
418  */
419 final class TextReader extends XMLReader<String> {
420 
421     TextReader() {
422         super("", Type.TEXT);
423     }
424 
425     @Override
426     public String read(final XMLStreamReaderAdapter xml, final boolean lenient)
427         throws XMLStreamException
428     {
429         final StringBuilder out = new StringBuilder();
430 
431         int type = xml.getEventType();
432         do {
433             out.append(xml.getText());
434         while (xml.hasNext() && (type = xml.next()) == CHARACTERS || type == CDATA);
435 
436         return out.toString();
437     }
438 }
439 
440 /**
441  * Reader implementation for reading list of elements.
442  *
443  @param <T> the element type
444  *
445  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
446  @version 1.2
447  @since 1.2
448  */
449 final class ListReader<T> extends XMLReader<List<T>> {
450 
451     private final XMLReader<? extends T> _adoptee;
452 
453     ListReader(final XMLReader<? extends T> adoptee) {
454         super(adoptee.name(), Type.LIST);
455         _adoptee = adoptee;
456     }
457 
458     @Override
459     public List<T> read(final XMLStreamReaderAdapter xml, final boolean lenient)
460         throws XMLStreamException
461     {
462         xml.require(START_ELEMENT, null, name());
463         final T element = _adoptee.read(xml, lenient);
464         return element != null ? List.of(element: List.of();
465     }
466 
467 }
468 
469 /**
470  * This reader implementation ignores the content of the element with the given
471  * name.
472  *
473  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
474  @version 1.2
475  @since 1.2
476  */
477 final class IgnoreReader extends XMLReader<Object> {
478 
479     private final XMLReader<Object> _reader;
480 
481     IgnoreReader(final String name) {
482         super(name, Type.ELEM);
483         _reader = new ElemReader<>(name, o -> o, List.of(), Type.ELEM);
484     }
485 
486     @Override
487     public Object read(final XMLStreamReaderAdapter xml, final boolean lenient)
488         throws XMLStreamException
489     {
490         return _reader.read(xml, true);
491     }
492 }
493 
494 /**
495  * This reader implementation reads the XML nodes from a given base node.
496  *
497  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
498  @version 1.5
499  @since 1.5
500  */
501 final class DocReader extends XMLReader<Document> {
502 
503     DocReader(final String name) {
504         super(name, Type.ELEM);
505     }
506 
507     @Override
508     public Document read(final XMLStreamReaderAdapter xml, final boolean lenient)
509         throws XMLStreamException
510     {
511         Document doc = null;
512         try {
513             doc = XML.builder().newDocument();
514             XML.copy(new XMLStreamReaderAdapter(xml) {
515                 @Override
516                 public String getVersion() {
517 // Workaround for this bug:
518 //Caused by: java.lang.NullPointerException
519 //    at java.xml/com.sun.org.apache.xerces.internal.dom.CoreDocumentImpl.setXmlVersion(CoreDocumentImpl.java:865)
520 //    at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.SAX2DOM.setDocumentInfo(SAX2DOM.java:145)
521 //    at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.SAX2DOM.startElement(SAX2DOM.java:155)
522 //    at java.xml/com.sun.org.apache.xml.internal.serializer.ToXMLSAXHandler.closeStartTag(ToXMLSAXHandler.java:206)
523 //    at java.xml/com.sun.org.apache.xml.internal.serializer.ToXMLSAXHandler.characters(ToXMLSAXHandler.java:526)
524 //    at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.StAXStream2SAX.handleCharacters(StAXStream2SAX.java:248)
525 //    at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.StAXStream2SAX.bridge(StAXStream2SAX.java:155)
526 //    at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.StAXStream2SAX.parse(StAXStream2SAX.java:104)
527 //    at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transformIdentity(TransformerImpl.java:707)
528 //    at java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:774)
529                     return super.getVersion() != null
530                         super.getVersion()
531                         "1.0";
532                 }
533             }, doc);
534         catch (XMLStreamException|RuntimeException e) {
535             if (!lenient) {
536                 throw e;
537             }
538         }
539         // Mark the next XML element as consumed, by the StaX transformer: #82
540         xml.consumed();
541         return doc;
542     }
543 
544 }
545 
546 /**
547  * The main XML element reader implementation.
548  *
549  @param <T> the reader data type
550  *
551  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
552  @version 1.2
553  @since 1.2
554  */
555 final class ElemReader<T> extends XMLReader<T> {
556 
557     // Given parameters.
558     private final Function<Object[], T> _creator;
559     private final List<XMLReader<?>> _children;
560 
561     // Derived parameters.
562     private final Map<String, Integer> _readerIndexMapping = new HashMap<>();
563     private final int[] _attrReaderIndexes;
564     private final int[] _textReaderIndex;
565 
566     ElemReader(
567         final String name,
568         final Function<Object[], T> creator,
569         final List<XMLReader<?>> children,
570         final Type type
571     ) {
572         super(name, type);
573 
574         _creator = requireNonNull(creator);
575         _children = requireNonNull(children);
576 
577         for (int i = 0; i < _children.size(); ++i) {
578             _readerIndexMapping.put(_children.get(i).name(), i);
579         }
580         _attrReaderIndexes = IntStream.range(0, _children.size())
581             .filter(i -> _children.get(i).type() == Type.ATTR)
582             .toArray();
583         _textReaderIndex = IntStream.range(0, _children.size())
584             .filter(i -> _children.get(i).type() == Type.TEXT)
585             .toArray();
586 
587         if (_textReaderIndex.length > 1) {
588             throw new IllegalArgumentException(
589                 "Found more than one TEXT reader."
590             );
591         }
592     }
593 
594     @Override
595     public T read(final XMLStreamReaderAdapter xml, final boolean lenient)
596         throws XMLStreamException
597     {
598         while (xml.getEventType() == COMMENT) {
599             consumeComment(xml);
600         }
601         xml.require(START_ELEMENT, null, name());
602 
603         final List<ReaderResult> results = _children.stream()
604             .map(ReaderResult::of)
605             .toList();
606 
607         final ReaderResult text = _textReaderIndex.length == 1
608             ? results.get(_textReaderIndex[0])
609             null;
610 
611         for (int attrReaderIndex : _attrReaderIndexes) {
612             final ReaderResult result = results.get(attrReaderIndex);
613             try {
614                 result.put(result.reader().read(xml, lenient));
615             catch (IllegalArgumentException|NullPointerException e) {
616                 if (!lenientthrow e;
617             }
618         }
619 
620         if (xml.safeNext()) {
621             boolean hasNext = false;
622             do {
623                 switch (xml.getEventType()) {
624                     case COMMENT -> consumeComment(xml);
625                     case START_ELEMENT -> {
626                         final String localName = xml.getLocalName();
627                         Integer index = _readerIndexMapping.get(localName);
628 
629                         if (index == null && !lenient) {
630                             throw new XMLStreamException(format(
631                                 "Unexpected element <%s>.",
632                                 xml.getLocalName()
633                             ));
634                         }
635 
636                         final ReaderResult result = index != null
637                             ? results.get(index)
638                             : ReaderResult.of(elem(xml.getLocalName()));
639 
640                         if (result != null) {
641                             throwUnexpectedElement(xml, lenient, result);
642                             hasNext = xml.safeNext();
643                         }
644                     }
645                     case CHARACTERS, CDATA -> {
646                         if (text != null) {
647                             throwUnexpectedElement(xml, lenient, text);
648                         else {
649                             xml.next();
650                         }
651                         hasNext = true;
652                     }
653                     case END_ELEMENT, END_DOCUMENT -> {
654                         try {
655                             return _creator.apply(
656                                 results.stream()
657                                     .map(ReaderResult::value)
658                                     .toArray()
659                             );
660                         catch (IllegalArgumentException|NullPointerException e) {
661                             if (!lenient) {
662                                 throw new XMLStreamException(format(
663                                     "Invalid value for '%s'.", name()), e
664                                 );
665                             else {
666                                 return null;
667                             }
668                         }
669                     }
670                 }
671             while (hasNext);
672         }
673 
674         throw new XMLStreamException(format(
675             "Premature end of file while reading '%s'.", name()
676         ));
677     }
678 
679     private void consumeComment(final XMLStreamReader xmlthrows XMLStreamException {
680         assert xml.getEventType() == COMMENT;
681         if (xml.hasNext()) {
682             xml.next();
683         }
684     }
685 
686     private void throwUnexpectedElement(
687         final XMLStreamReaderAdapter xml,
688         final boolean lenient,
689         final ReaderResult text
690     )
691         throws XMLStreamException
692     {
693         try {
694             text.put(text.reader().read(xml, lenient));
695         catch (IllegalArgumentException|NullPointerException e) {
696             if (!lenient) {
697                 final XMLStreamException exp = new XMLStreamException(format(
698                     "Unexpected element <%s>.",
699                     xml.getLocalName()
700                 ));
701                 exp.addSuppressed(e);
702                 throw exp;
703             }
704         }
705     }
706 }
707 
708 /**
709  * Helper interface for storing the XML reader (intermediate) results.
710  *
711  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
712  @version 1.2
713  @since 1.2
714  */
715 interface ReaderResult {
716 
717     /**
718      * Return the underlying XML reader, which reads the result.
719      *
720      @return return the underlying XML reader
721      */
722     XMLReader<?> reader();
723 
724     /**
725      * Put the given {@code value} to the reader result.
726      *
727      @param value the reader result
728      */
729     void put(final Object value);
730 
731     /**
732      * Return the current reader result value.
733      *
734      @return the current reader result value
735      */
736     Object value();
737 
738     /**
739      * Create a reader result for the given XML reader
740      *
741      @param reader the XML reader
742      @return a reader result for the given reader
743      */
744     static ReaderResult of(final XMLReader<?> reader) {
745         return reader.type() == Type.LIST
746             new ListResult(reader)
747             new ValueResult(reader);
748     }
749 
750 }
751 
752 /**
753  * Result object for values read from XML elements.
754  *
755  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
756  @version 1.2
757  @since 1.2
758  */
759 final class ValueResult implements ReaderResult {
760 
761     private final XMLReader<?> _reader;
762     private Object _value;
763 
764     ValueResult(final XMLReader<?> reader) {
765         _reader = reader;
766     }
767 
768     @Override
769     public void put(final Object value) {
770         _value = value;
771     }
772 
773     @Override
774     public XMLReader<?> reader() {
775         return _reader;
776     }
777 
778 
779     @Override
780     public Object value() {
781         return _value;
782     }
783 
784 }
785 
786 /**
787  * Result object for list values read from XML elements.
788  *
789  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
790  @version 1.2
791  @since 1.2
792  */
793 final class ListResult implements ReaderResult {
794 
795     private final XMLReader<?> _reader;
796     private final List<Object> _value = new ArrayList<>();
797 
798     ListResult(final XMLReader<?> reader) {
799         _reader = reader;
800     }
801 
802     @Override
803     public void put(final Object value) {
804         if (value instanceof List<?> list) {
805             _value.addAll(list);
806         else {
807             _value.add(value);
808         }
809     }
810 
811     @Override
812     public XMLReader<?> reader() {
813         return _reader;
814     }
815 
816     @Override
817     public List<Object> value() {
818         return copyOf(_value);
819     }
820 
821 }