mode_edit

Module: Maven Central

The fs2-data-xml module provides tools to parse XML data in a streaming manner.

This page covers the following topics:

Basic usage

To create a stream of XML events from an input stream, use the events pipe in fs2.data.xml package.

import cats.effect._

import fs2._
import fs2.data.xml._

val input = """<a xmlns:ns="http://test.ns">
              |  <ns:b ns:a="attribute">text</ns:b>
              |</a>
              |<a>
              |  <b/>
              |  test entity resolution &amp; normalization
              |</a>""".stripMargin
// input: String = """<a xmlns:ns="http://test.ns">
//   <ns:b ns:a="attribute">text</ns:b>
// </a>
// <a>
//   <b/>
//   test entity resolution &amp; normalization
// </a>"""

val stream = Stream.emits(input).through(events[IO])
// stream: Stream[IO, XmlEvent] = Stream(..)
stream.compile.toList.unsafeRunSync()
// res0: List[XmlEvent] = List(
//   StartTag(
//     QName(None, "a"),
//     List(
//       Attr(QName(Some("xmlns"), "ns"), List(XmlString("http://test.ns", false)))
//     ),
//     false
//   ),
//   XmlString(
//     """
//   """,
//     false
//   ),
//   StartTag(
//     QName(Some("ns"), "b"),
//     List(Attr(QName(Some("ns"), "a"), List(XmlString("attribute", false)))),
//     false
//   ),
//   XmlString("text", false),
//   EndTag(QName(Some("ns"), "b")),
//   XmlString(
//     """
// """,
//     false
//   ),
//   EndTag(QName(None, "a")),
//   StartTag(QName(None, "a"), List(), false),
//   XmlString(
//     """
//   """,
//     false
//   ),
//   StartTag(QName(None, "b"), List(), true),
//   EndTag(QName(None, "b")),
//   XmlString(
//     """
//   test entity resolution """,
//     false
//   ),
//   XmlEntityRef("amp"),
//   XmlString(
//     """ normalization
// """,
//     false
//   ),
//   EndTag(QName(None, "a"))
// )

The pipe validates the XML structure while parsing. It reads all the XML elements in the input stream and emits events as they are available.

Resolvers

Namespace can be resolved by using the namespaceResolver pipe.

val nsResolved = stream.through(namespaceResolver[IO])
// nsResolved: Stream[IO[x], XmlEvent] = Stream(..)
nsResolved.compile.toList.unsafeRunSync()
// res1: List[XmlEvent] = List(
//   StartTag(
//     QName(Some("http://www.w3.org/XML/1998/namespace"), "a"),
//     List(
//       Attr(QName(Some("xmlns"), "ns"), List(XmlString("http://test.ns", false)))
//     ),
//     false
//   ),
//   XmlString(
//     """
//   """,
//     false
//   ),
//   StartTag(
//     QName(Some("http://test.ns"), "b"),
//     List(
//       Attr(
//         QName(Some("http://test.ns"), "a"),
//         List(XmlString("attribute", false))
//       )
//     ),
//     false
//   ),
//   XmlString("text", false),
//   EndTag(QName(Some("http://test.ns"), "b")),
//   XmlString(
//     """
// """,
//     false
//   ),
//   EndTag(QName(Some("http://www.w3.org/XML/1998/namespace"), "a")),
//   StartTag(
//     QName(Some("http://www.w3.org/XML/1998/namespace"), "a"),
//     List(),
//     false
//   ),
//   XmlString(
//     """
//   """,
//     false
//   ),
//   StartTag(
//     QName(Some("http://www.w3.org/XML/1998/namespace"), "b"),
//     List(),
//     true
//   ),
//   EndTag(QName(Some("http://www.w3.org/XML/1998/namespace"), "b")),
//   XmlString(
//     """
// ...

Using the referenceResolver pipe, entity and character references can be resolved. By defaut the standard xmlEntities mapping is used, but it can be replaced by any mapping you see fit.

val entityResolved = stream.through(referenceResolver[IO]())
// entityResolved: Stream[IO[x], XmlEvent] = Stream(..)
entityResolved.compile.toList.unsafeRunSync()
// res2: List[XmlEvent] = List(
//   StartTag(
//     QName(None, "a"),
//     List(
//       Attr(QName(Some("xmlns"), "ns"), List(XmlString("http://test.ns", false)))
//     ),
//     false
//   ),
//   XmlString(
//     """
//   """,
//     false
//   ),
//   StartTag(
//     QName(Some("ns"), "b"),
//     List(Attr(QName(Some("ns"), "a"), List(XmlString("attribute", false)))),
//     false
//   ),
//   XmlString("text", false),
//   EndTag(QName(Some("ns"), "b")),
//   XmlString(
//     """
// """,
//     false
//   ),
//   EndTag(QName(None, "a")),
//   StartTag(QName(None, "a"), List(), false),
//   XmlString(
//     """
//   """,
//     false
//   ),
//   StartTag(QName(None, "b"), List(), true),
//   EndTag(QName(None, "b")),
//   XmlString(
//     """
//   test entity resolution """,
//     false
//   ),
//   XmlString("&", false),
//   XmlString(
//     """ normalization
// """,
//     false
//   ),
//   EndTag(QName(None, "a"))
// )

Normalization

Once entites and namespaces are resolved, the events might be numerous and can be normalized to avoid emitting too many of them. For instance, after reference resolution, consecutive text events can be merged. This is achieved by using the normalize pipe.

val normalized = entityResolved.through(normalize[IO])
// normalized: Stream[IO[x], XmlEvent] = Stream(..)
normalized.compile.toList.unsafeRunSync()
// res3: List[XmlEvent] = List(
//   StartTag(
//     QName(None, "a"),
//     List(
//       Attr(QName(Some("xmlns"), "ns"), List(XmlString("http://test.ns", false)))
//     ),
//     false
//   ),
//   XmlString(
//     """
//   """,
//     false
//   ),
//   StartTag(
//     QName(Some("ns"), "b"),
//     List(Attr(QName(Some("ns"), "a"), List(XmlString("attribute", false)))),
//     false
//   ),
//   XmlString("text", false),
//   EndTag(QName(Some("ns"), "b")),
//   XmlString(
//     """
// """,
//     false
//   ),
//   EndTag(QName(None, "a")),
//   StartTag(QName(None, "a"), List(), false),
//   XmlString(
//     """
//   """,
//     false
//   ),
//   StartTag(QName(None, "b"), List(), true),
//   EndTag(QName(None, "b")),
//   XmlString(
//     """
//   test entity resolution & normalization
// """,
//     false
//   ),
//   EndTag(QName(None, "a"))
// )