eu.cdevreeze

yaidom

package yaidom

Yaidom is yet another Scala immutable DOM-like XML API. The other well-known Scala immutable DOM-like APIs are the standard scala.xml API and Anti-XML. The latter API is considered by many to be an improvement over the former, but both APIs:

Yaidom takes a different approach, avoiding XPath-like query support, and offering good namespace and decent (functional) update support. Yaidom is also characterized by mathematical precision and clarity. Still, the API remains practical and pragmatic. In particular, the API user has much configuration control over parsing and serialization, because yaidom exposes the underlying JAXP parsers and serializers, which can be configured by the library user.

Yaidom chooses its battles. For example, given that DTDs do not know about namespaces, yaidom offers good namespace support, but ignores DTDs entirely. Of course the underlying XML parser may still validate XML against a DTD, if so desired. As another example, yaidom tries to leave the handling of the gory details of XML processing (such as whitespace handling) as much as possible to JAXP (and JAXP parser/serializer configuration). As yet another example, yaidom knows nothing about (XML Schema) types of elements and attributes.

Yaidom, and in particular this package, contains the following layers:

It makes sense to read this documentation, because it helps in getting up-to-speed with yaidom.

Basic concepts

In real world XML, elements (and sometimes attributes) tend to have names within a certain namespace. There are 2 kinds of names at play here:

They are represented by immutable classes eu.cdevreeze.yaidom.QName and eu.cdevreeze.yaidom.EName, respectively.

Qualified names occur in XML, whereas expanded names do not. Yet qualified names have no meaning on their own. They need to be resolved to expanded names, via the in-scope namespaces. Note that the term "qualified name" is often used for what yaidom (and the Namespaces specification) calls "expanded name", and that most XML APIs do not distinguish between the 2 kinds of names. Yaidom has to clearly make this distinction, in order to model namespaces correctly.

To resolve qualified names to expanded names, yaidom distinguishes between:

They are represented by immutable classes eu.cdevreeze.yaidom.Declarations and eu.cdevreeze.yaidom.Scope, respectively.

Namespace declarations occur in XML, whereas in-scope namespaces do not. The latter are the accumulated effect of the namespace declarations of the element itself, if any, and those in ancestor elements.

Note: in the code examples below, we assume the following import:

import eu.cdevreeze.yaidom._

To see the resolution of qualified names in action, consider the following sample XML:

<book:Bookstore xmlns:book="http://bookstore/book" xmlns:auth="http://bookstore/author">
  <book:Book ISBN="978-0321356680" Price="35" Edition="2">
    <book:Title>Effective Java (2nd Edition)</book:Title>
    <book:Authors>
      <auth:Author>
        <auth:First_Name>Joshua</auth:First_Name>
        <auth:Last_Name>Bloch</auth:Last_Name>
      </auth:Author>
    </book:Authors>
  </book:Book>
  <book:Book ISBN="978-0981531649" Price="35" Edition="2">
    <book:Title>Programming in Scala: A Comprehensive Step-by-Step Guide, 2nd Edition</book:Title>
    <book:Authors>
      <auth:Author>
        <auth:First_Name>Martin</auth:First_Name>
        <auth:Last_Name>Odersky</auth:Last_Name>
      </auth:Author>
      <auth:Author>
        <auth:First_Name>Lex</auth:First_Name>
        <auth:Last_Name>Spoon</auth:Last_Name>
      </auth:Author>
      <auth:Author>
        <auth:First_Name>Bill</auth:First_Name>
        <auth:Last_Name>Venners</auth:Last_Name>
      </auth:Author>
    </book:Authors>
  </book:Book>
</book:Bookstore>

Consider the last element with qualified name QName("book:Book"). To resolve this qualified name as expanded name, we need to know the namespaces in scope at that element. To compute the in-scope namespaces, we need to accumulate the namespace declarations of the last book:Book element and of its ancestor element(s), starting with the root element.

The start Scope is "parent scope" Scope.Empty. Then, in the root element we find namespace declarations:

Declarations.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author")

This leads to the following namespaces in scope at the root element:

Scope.Empty.resolve(Declarations.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author"))

which is equal to:

Scope.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author")

We find no other namespace declarations in the last book:Book element or its ancestor(s), so the computed scope is also the scope of the last book:Book element.

Then QName("book:Book") is resolved as follows:

Scope.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author").resolveQNameOption(QName("book:Book"))

which is equal to:

Some(EName("{http://bookstore/book}Book"))

This namespace support in yaidom has mathematical rigor. The immutable classes QName, EName, Declarations and Scope have precise definitions, reflected in their implementations, and they obey some interesting properties. For example, if we correctly define Scope operation relativize (along with resolve), we get:

scope1.resolve(scope1.relativize(scope2)) == scope2

This may not sound like much, but by getting the basics right, yaidom succeeds in offering first-class support for XML namespaces, without the magic and namespace-related bugs often found in other XML libraries.

There are 2 other basic concepts in this package, representing paths to elements:

They are represented by immutable classes eu.cdevreeze.yaidom.PathBuilder and eu.cdevreeze.yaidom.Path, respectively.

Path builders are like canonical XPath expressions, yet they do not contain the root element itself, and indexing starts with 0 instead of 1.

For example, the last name of the first author of the last book element has path:

Path.from(
EName("{http://bookstore/book}Book") -> 1,
EName("{http://bookstore/book}Authors") -> 0,
EName("{http://bookstore/author}Author") -> 0,
EName("{http://bookstore/author}Last_Name") -> 0
)

This path could be written as path builder as follows:

PathBuilder.from(QName("book:Book") -> 1, QName("book:Authors") -> 0, QName("auth:Author") -> 0, QName("auth:Last_Name") -> 0)

Using the Scope mentioned earlier, the latter path builder resolves to the path given before that, by invoking method PathBuilder.build(scope). In order for this to work, the Scope must be invertible. That is, there must be a one-to-one correspondence between prefixes ("" for the default namespace) and namespace URIs, because otherwise the index numbers may differ. Also note that the prefixes book and auth in the path builder are arbitrary, and need not match with the prefixes used in the XML tree itself.

Uniform query API traits

Yaidom provides a relatively small query API, to query an individual element for collections of child elements, descendant elements or descendant-or-self elements. The resulting collections are immutable Scala collections, that can further be manipulated using the Scala Collections API.

This query API is uniform, in that different element implementations share (most of) the same query API. It is also element-centric (unlike standard Scala XML and Anti-XML).

For example, consider the XML example given earlier, as a Scala XML literal named bookstore. We can wrap this Scala XML Elem into a yaidom wrapper of type eu.cdevreeze.yaidom.scalaxml.ScalaXmlElem, named bookstoreElem. Then we can query for all books, that is, all descendant-or-self elements with resolved (or expanded) name EName("{http://bookstore/book}Book"), as follows:

bookstoreElem filterElemsOrSelf (elem => elem.resolvedName == EName("{http://bookstore/book}Book"))

The result would be an immutable IndexedSeq of ScalaXmlElem instances, holding 2 book elements.

We could instead have written:

bookstoreElem.filterElemsOrSelf(EName("{http://bookstore/book}Book"))

with the same result.

Instead of searching for appropriate descendant-or-self elements, we could have searched for descendant elements only, without altering the result in this case:

bookstoreElem filterElems (elem => elem.resolvedName == EName("{http://bookstore/book}Book"))

or:

bookstoreElem.filterElems(EName("{http://bookstore/book}Book"))

We could even have searched for appropriate child elements only, without altering the result in this case:

bookstoreElem filterChildElems (elem => elem.resolvedName == EName("{http://bookstore/book}Book"))

or:

bookstoreElem.filterChildElems(EName("{http://bookstore/book}Book"))

or, knowing that all child elements are books:

bookstoreElem.findAllChildElems

We could find all authors of the Scala book as follows:

for {
  bookElem <- bookstoreElem filterChildElems (elem => elem.resolvedName == EName("{http://bookstore/book}Book"))
  if bookElem.attributeOption(EName("ISBN")) == Some("978-0981531649")
  authorElem <- bookElem filterElems (elem => elem.resolvedName == EName("{http://bookstore/author}Author"))
} yield authorElem

or:

for {
  bookElem <- bookstoreElem.filterChildElems(EName("{http://bookstore/book}Book"))
  if bookElem.attributeOption(EName("ISBN")) == Some("978-0981531649")
  authorElem <- bookElem.filterElems(EName("{http://bookstore/author}Author"))
} yield authorElem

We could even use operator notation, as follows:

for {
  bookElem <- bookstoreElem \ (elem => elem.resolvedName == EName("{http://bookstore/book}Book"))
  if (bookElem \@ EName("ISBN")) == Some("978-0981531649")
  authorElem <- bookElem \\ (elem => elem.resolvedName == EName("{http://bookstore/author}Author"))
} yield authorElem

or:

for {
  bookElem <- bookstoreElem \ EName("{http://bookstore/book}Book")
  if (bookElem \@ EName("ISBN")) == Some("978-0981531649")
  authorElem <- bookElem \\ EName("{http://bookstore/author}Author")
} yield authorElem

where \\ stands for filterElemsOrSelf.

There is no explicit support for filtering on the "self" element itself. In the example above, we might want to check if the root element has the expected EName, for instance. That is easy to express using a simple idiom, however. The last example then becomes:

for {
  bookstoreElem <- Vector(bookstoreElem)
  if bookstoreElem.resolvedName == EName("{http://bookstore/book}Bookstore")
  bookElem <- bookstoreElem \ EName("{http://bookstore/book}Book")
  if (bookElem \@ EName("ISBN")) == Some("978-0981531649")
  authorElem <- bookElem \\ EName("{http://bookstore/author}Author")
} yield authorElem

Now suppose the same XML is stored in a (org.w3c.dom) DOM tree, wrapped in a eu.cdevreeze.yaidom.dom.DomElem bookstoreElem. Then the same queries would use exactly the same code as above! The result would be a collection of DomElem instances instead of ScalaXmlElem instances, however. There are many more element implementations in yaidom, and they share (most of) the same query API. Therefore this query API is called a uniform query API.

The last example, using operator notation, looks a bit more "XPath-like". It is more verbose than queries in Scala XML, however, partly because in yaidom these operators cannot be chained. Yet this is with good reason. Yaidom does not blur the distinction between elements and element collections, and therefore does not offer any XPath experience. The small price paid in verbosity is made up for by precision. The yaidom query API traits have very precise definitions of their operations, as can be seen in the corresponding documentation.

The uniform query API traits turn minimal APIs into richer APIs, where each richer API is defined very precisely in terms of the minimal API. The top-level query API trait is eu.cdevreeze.yaidom.ParentElemLike. It needs to be given a method implementation to query for child elements (not child nodes in general, but just child elements!), and it offers methods to query for some or all child elements, descendant elements, and descendant-or-self elements. That is, the minimal API consists of abstract method findAllChildElems, and it offers methods such as filterChildElems, filterElems and filterElemsOrSelf. This trait has no knowledge about elements at all, other than the fact that elements can have child elements.

Sub-trait eu.cdevreeze.yaidom.ElemLike adds minimal knowledge about elements themselves, viz. that elements have a "resolved" (or expanded) name, and "resolved" attributes (mapping attribute expanded names to attribute values). That is, it needs to be given implementations of abstract methods resolvedName and resolvedAttributes, and then offers methods to query for attributes or child/descendant/descendant-or-self elements with a given expanded name. The trait is trivially defined in terms of its super-trait.

It is important to note that yaidom does not consider namespace declarations to be attributes themselves. Otherwise, there would have been circular dependencies between both concepts, because attributes with namespaces require in-scope namespaces and therefore namespace declarations for resolving the names of these attributes.

Note that traits eu.cdevreeze.yaidom.ElemLike and eu.cdevreeze.yaidom.ParentElemLike only know about elements, not about other kinds of nodes. Of course the actual element implementations mixing in this query API know about other node types, but that knowledge is outside the uniform query API. Note that the example queries above only use the minimal element knowledge that traits ElemLike and ParentElemLike have about elements. Therefore the query code can be used unchanged for different element implementations.

The ElemLike trait has sub-trait eu.cdevreeze.yaidom.PathAwareElemLike. It adds knowledge about paths. Paths can be queried (in the same way that elements can be queried in trait ParentElemLike), and elements can be found given a path.

For example, to query for the Scala book authors, the following alternative code can be used (if the used element implementation mixes in trait PathAwareElemLike, which is not the case for the Scala XML and DOM wrappers above):

for {
  authorPath <- bookstoreElem filterElemOrSelfPaths (elem => elem.resolvedName == EName("{http://bookstore/author}Author"))
  if authorPath.entries.contains(Path.Entry(EName("{http://bookstore/book}Book"), 1))
} yield bookstoreElem.getElemOrSelfByPath(authorPath)

The PathAwareElemLike trait has sub-trait eu.cdevreeze.yaidom.UpdatableElemLike. This trait offers functional updates at given paths. Whereas the super-traits know only about elements, this trait knows that elements have some node super-type.

Instead of functional updates at given paths, elements can also be "transformed" functionally without specifying any paths. This is offered by trait eu.cdevreeze.yaidom.TransformableElemLike, which unlike the traits above has no super-traits. The Scala XML and DOM wrappers above do not mix in this trait.

Some element implementations

The uniform query API traits, especially ParentElemLike and its sub-trait ElemLike are mixed in by many element implementations. In this package there are 2 immutable element implementations, eu.cdevreeze.yaidom.ElemBuilder and eu.cdevreeze.yaidom.Elem.

Class eu.cdevreeze.yaidom.Elem is the default element implementation of yaidom. It extends class eu.cdevreeze.yaidom.Node. The latter also has sub-classes for text nodes, comments, entity references and processing instructions. Class eu.cdevreeze.yaidom.Document contains a document Elem, but is not a Node sub-class itself.

The eu.cdevreeze.yaidom.Elem class has the following characteristics:

Creating such Elem trees by hand is a bit cumbersome, partly because scopes have to be passed to each Elem in the tree. The latter is not needed if we use class eu.cdevreeze.yaidom.ElemBuilder to create element trees by hand. When the tree has been fully created as ElemBuilder, invoke method ElemBuilder.build(parentScope) to turn it into an Elem.

Like their super-classes Node and NodeBuilder, classes Elem and ElemBuilder have very much in common. Both are immutable, easy to compose (ElemBuilder instances even more so), equality is reference equality, etc. The most important differences are as follows:

The Effective Java book element in the XML example above could have been written as ElemBuilder (without the inter-element whitespace) as follows:

import NodeBuilder._

elem(
  qname = QName("book:Book"),
  attributes = Vector(QName("ISBN") -> "978-0321356680", QName("Price") -> "35", QName("Edition") -> "2"),
  children = Vector(
    elem(
      qname = QName("book:Title"),
      children = Vector(
        text("Effective Java (2nd Edition)")
      )
    ),
    elem(
      qname = QName("book:Authors"),
      children = Vector(
        elem(
          qname = QName("auth:Author"),
          children = Vector(
            elem(
              qname = QName("auth:First_Name"),
              children = Vector(
                text("Joshua")
              )
            ),
            elem(
              qname = QName("auth:Last_Name"),
              children = Vector(
                text("Bloch")
              )
            )
          )
        )
      )
    )
  )
)

This ElemBuilder (say, eb) lacks namespace declarations for prefixes book and auth. So, the following returns false:

eb.canBuild(Scope.Empty)

while the following returns true:

eb.canBuild(Scope.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author"))

Indeed,

eb.build(Scope.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author"))

returns the element tree as Elem.

Note that the distinction between ElemBuilder and Elem "solves" the mismatch that immutable ("functional") element trees are constructed in a bottom-up manner, while namespace scoping works in a top-down manner. (See also Anti-XML issue 78, in https://github.com/djspiewak/anti-xml/issues/78).

There are many more element implementations in yaidom, most of them in sub-packages of this package. Yaidom is extensible in that new element implementations can be invented, for example elements that are better "roundtrippable" (at the expense of "composability"), or yaidom wrappers around other DOM-like APIs (such as XOM or JDOM2). The current element implementations in yaidom are:

This illustrates that especially trait ElemLike is a uniform query API in yaidom.

Packages and dependencies

Yaidom has the following packages, and layering between packages:

Indeed, all yaidom package dependencies are uni-directional.

Notes on performance

Yaidom can be quite memory-hungry. One particular cause of that is the possible creation of very many duplicate EName and QName instances. This can be the case while parsing XML into yaidom documents, or while querying yaidom element trees.

The user of the library can reduce memory consumption to a large extent, and yaidom facilitates that.

As for querying, prefer:

import ElemApi._

bookstoreElem filterElemsOrSelf withEName("{http://bookstore/book}", "Book")

to:

bookstoreElem.filterElemsOrSelf(EName("{http://bookstore/book}", "Book"))

to avoid unnecessary (large scale) EName object creation.

To reduce the memory footprint of parsed XML trees, see eu.cdevreeze.yaidom.ENameProvider and eu.cdevreeze.yaidom.QNameProvider.

For example, during the startup phase of an application, we could set the global ENameProvider as follows:

ENameProvider.globalENameProvider.become(new ENameProvider.ENameProviderUsingImmutableCache(knownENames))

Note that the global ENameProvider or QNameProvider can typically be configured rather late during development, but the memory cost savings can be substantial. Also note that the global ENameProvider or QNameProvider can be used implicitly in application code, by writing:

bookstoreElem filterElemsOrSelf getEName("{http://bookstore/book}", "Book")

using an implicit ENameProvider, whose members are in scope. Still, for querying the first alternative using withEName is better, but there are likely many scenarios in yaidom client code where an implicit ENameProvider or QNameProvider makes sense.

The bottom line is that yaidom can be configured to be far less memory-hungry, and that yaidom client code can also take some responsibility in reducing memory usage.

Linear Supertypes
AnyRef, Any
Ordering
  1. Alphabetic
  2. By inheritance
Inherited
  1. yaidom
  2. AnyRef
  3. Any
  1. Hide All
  2. Show all
Learn more about member selection
Visibility
  1. Public
  2. All

Type Members

  1. final case class Comment(text: String) extends Node with Product with Serializable

    Annotations
    @SerialVersionUID()
  2. final case class CommentBuilder(text: String) extends NodeBuilder with Product with Serializable

    Annotations
    @SerialVersionUID()
  3. trait ConverterToDocument[A] extends AnyRef

    Converter from A (which can be anything) to eu.cdevreeze.yaidom.Document.

    Converter from A (which can be anything) to eu.cdevreeze.yaidom.Document.

    A

    the type of the value to convert

  4. trait ConverterToElem[A] extends AnyRef

    Converter from A (which can be anything) to eu.cdevreeze.yaidom.Elem.

    Converter from A (which can be anything) to eu.cdevreeze.yaidom.Elem.

    A

    the type of the value to convert

  5. final case class Declarations(prefixNamespaceMap: Map[String, String]) extends Immutable with Product with Serializable

    Namespace declarations (and undeclarations), typically at the level of one element.

    Namespace declarations (and undeclarations), typically at the level of one element.

    For example, consider the following XML:

    <book:Bookstore xmlns:book="http://bookstore/book" xmlns:auth="http://bookstore/author">
      <book:Book ISBN="978-0321356680" Price="35" Edition="2">
        <book:Title>Effective Java (2nd Edition)</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Joshua</auth:First_Name>
            <auth:Last_Name>Bloch</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
    </book:Bookstore>

    Then only the root element contains namespace declarations, viz.:

    Declarations.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author")

    The Declarations is backed by a map from prefixes (or the empty string for the default namespace) to namespace URIs (or the empty string). If the mapped value is the empty string, it is an undeclaration.

    Prefix 'xml' is not allowed as key in this map. That prefix, mapping to namespace URI 'http://www.w3.org/XML/1998/namespace', is always available, without needing any declaration.

    This class does not depend on the Scope class.

    There are no methods for subset relationships on namespace declarations (unlike for class Scope). After all, in the presence of namespace undeclarations, such a subset relationship would become a bit unnatural.

  6. final class DocBuilder extends DocumentApi[ElemBuilder] with Immutable with Serializable

    Builder of a yaidom Document.

    Builder of a yaidom Document. Called DocBuilder instead of DocumentBuilder, because often a JAXP DocumentBuilder is in scope too. A DocBuilder is itself not a NodeBuilder.

    A DocBuilder is constructed from an optional URI, a document element (as ElemBuilder), top-level processing instruction builders, if any, and top-level comment builders, if any.

    Annotations
    @SerialVersionUID()
  7. final class Document extends DocumentApi[Elem] with Immutable with Serializable

    Document.

    Document. Although at first sight the document root element seems to be the root node, this is not entirely true. For example, there may be comments at top level, outside the document root element.

    The document is itself not a Node. This choice has the following advantages:

    • Documents are indeed prevented (at compile-time) from occurring as "child nodes"
    • The API is cleaner. For example, (unlike "elements") document methods like toTreeRepr should not be passed a parent scope. By not considering a Document a Node, it is more visible that parent scopes are irrelevant for Documents, unlike for "elements".

    A Document is constructed from an optional URI, a document element (as Elem), top-level processing instructions, if any, and top-level comments, if any.

    Note that class Document does not have any query methods for Elem instances. In particular, the ParentElemApi does not apply to documents. Therefore, given a document, querying for elements (other than the document element itself) always goes via the document element.

    Annotations
    @SerialVersionUID()
  8. trait DocumentApi[E <: ParentElemApi[E]] extends AnyRef

    Minimal API for Documents, having a type parameter for the element type.

    Minimal API for Documents, having a type parameter for the element type.

    This is a purely abstract API trait. It can be useful in generic code abstracting over multiple element implementations.

    E

    The element type

  9. trait DocumentConverter[A] extends AnyRef

    Converter from eu.cdevreeze.yaidom.Document to A (which can be anything, such as a DOM Document).

    Converter from eu.cdevreeze.yaidom.Document to A (which can be anything, such as a DOM Document).

    A

    the result type of the conversion

  10. final case class EName(namespaceUriOption: Option[String], localPart: String) extends Immutable with Product with Serializable

    Expanded name.

    Expanded name. See http://www.w3.org/TR/xml-names11/. It has a localPart and an optional namespace URI. Semantically like a QName in Java, but not keeping the prefix.

    To get an eu.cdevreeze.yaidom.EName from a eu.cdevreeze.yaidom.QName, the latter needs to be resolved against a eu.cdevreeze.yaidom.Scope.

    The short class name illustrates that expanded names are at least as important as qualified names, and should be equally easy to construct (using the companion object).

    Typical usage may lead to an explosion of different EName objects that are equal. Therefore, application code is encouraged to define and use constants for frequently used ENames. For example, for the XML Schema namespace (and analogous to the XLink constants in yaidom):

    val XsNamespace = "http://www.w3.org/2001/XMLSchema"
    
    val XsElementEName = EName(XsNamespace, "element")
    val XsAttributeEName = EName(XsNamespace, "attribute")
    val XsComplexTypeEName = EName(XsNamespace, "complexType")
    val XsSimpleTypeEName = EName(XsNamespace, "simpleType")
    // ...

    In this example, the EName constant names are in upper camel case, starting with the ("preferred") prefix, followed by the local part, and ending with suffix "EName".

    Implementation note: It was tried as alternative implementation to define EName as (Scala 2.10) value class. The EName would then wrap the expanded name as string representation (in James Clark notation). One cost would be that parsing the (optional) namespace URI and the local name would occur far more frequently. Another cost would be that the alternative implementation would not directly express that an EName is a combination of an optional namespace URI and a local part. Therefore that alternative implementation has been abandoned.

  11. trait ENameProvider extends AnyRef

    Provider of ENames, possibly from a cache of ENames.

    Provider of ENames, possibly from a cache of ENames. Typical implementations cache EName instances, to prevent any explosion of equal EName instances, thus unnecessarily increasing the memory footprint.

    Implementation notes

    It may seem rather lame that only the global ENameProvider variable can be updated. On the other hand, implicit ENameProvider parameters in many places in the API would change the API quite a bit. These implicit parameters would be implementation details leaking into the API. It was therefore decided not to introduce those implicit parameters, with the exception of only a few places inside implementation code in the library.

  12. final class Elem extends Node with UpdatableElemLike[Node, Elem] with TransformableElemLike[Node, Elem] with HasQName with HasText

    Immutable, thread-safe element node.

    Immutable, thread-safe element node. It is the default element implementation in yaidom. As the default element implementation among several alternative element implementations, it strikes a balance between loss-less roundtripping and composability.

    The parsers and serializers in packages eu.cdevreeze.yaidom.parse and eu.cdevreeze.yaidom.print return and take these default elements (or the corresponding Document instances), respectively.

    As for its query API, class eu.cdevreeze.yaidom.Elem is among the most powerful element implementations offered by yaidom. These elements offer all of the eu.cdevreeze.yaidom.UpdatableElemApi and eu.cdevreeze.yaidom.TransformableElemApi query APIs.

    See the documentation of the mixed-in query API traits for more details on the uniform query API offered by this class.

    The following example illustrates the use of the yaidom uniform query API in combination with some Elem-specific methods. In this XML scripting example the namespace prefix "xsd" is replaced by prefix "xs", including those in QName-valued attributes. The trivial XML file of this example is the following XML Schema:

    <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://book" elementFormDefault="qualified">
      <xsd:element name="book">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="isbn" type="xsd:string" />
            <xsd:element name="title" type="xsd:string" />
            <xsd:element name="authors" type="xsd:string" />
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
    </xsd:schema>

    The edit action can be performed on this schemaElem as follows, starting with some checks:

    // All descendant-or-self elements have the same Scope, mapping only prefix "xsd".
    require(schemaElem.findAllElemsOrSelf.map(_.scope).distinct == List(Scope.from("xsd" -> "http://www.w3.org/2001/XMLSchema")))
    
    // All descendant-or-self elements have a QName with prefix "xsd".
    require(schemaElem.findAllElemsOrSelf.map(_.qname.prefixOption).distinct == List(Some("xsd")))
    
    // All descendant-or-self elements have unprefixed attributes only.
    require(schemaElem.findAllElemsOrSelf.flatMap(_.attributes.toMap.keySet.map(_.prefixOption)).distinct == List(None))
    
    // All descendant-or-self elements with "type" attributes contain only QNames with prefix "xsd" in the values of those attributes.
    require(schemaElem.filterElemsOrSelf(e => (e \@ EName("type")).isDefined).forall(e => e.attributeAsQName(EName("type")).prefixOption == Some("xsd")))
    
    // Replaces prefix "xsd" by "xs" throughout the element tree, including in "type" attributes.
    val editedSchemaElem = schemaElem transformElemsOrSelf { elem =>
      val newScope = (elem.scope -- Set("xsd")) ++ Scope.from("xs" -> "http://www.w3.org/2001/XMLSchema")
      val newQName = QName("xs", elem.qname.localPart)
      val newTypeAttrOption = elem.attributeAsQNameOption(EName("type")).map(attr => QName("xs", attr.localPart).toString)
    
      elem.copy(qname = newQName, scope = newScope).plusAttributeOption(QName("type"), newTypeAttrOption)
    }

    Note that besides the uniform query API, this example uses some Elem-specific methods, such as attributeAsQName, copy and plusAttributeOption.

    Class Elem is immutable, and (should be) thread-safe. Hence, Elems do not know about their parent element, if any.

    An Elem has the following state:

    Note that namespace declarations are not considered to be attributes in Elem, just like in the rest of yaidom. Elem construction is unsuccessful if the element name and/or some attribute names cannot be resolved using the Scope of the element (ignoring the default namespace, if any, for attributes). As can be seen from the above-mentioned state, namespaces are first-class citizens.

    Elems can (relatively easily) be constructed manually in a bottom-up manner. Yet care must be taken to give the element and its descendants the correct Scope. Otherwise it is easy to introduce (prefixed) namespace undeclarations, which are not allowed in XML 1.0. The underlying issue is that functional Elem trees are created in a bottom-up manner, whereas namespace scoping works in a top-down manner. This is not a big issue in practice, since manual Elem creation is rather rare, and it is always possible to call method notUndeclaringPrefixes afterwards. An alternative method to create element trees by hand uses class eu.cdevreeze.yaidom.ElemBuilder. A manually created ElemBuilder can be converted to an Elem by calling method build.

    Round-tripping (parsing and serializing) is not entirely loss-less, but (in spite of the good composability and rather small state) not much is lost. Comments, processing instructions and entity references are retained. Attribute order is retained, although according to the XML InfoSet this order is irrelevant. Namespace declaration order is not necessarily retained, however. Superfluous namespace declarations are also lost. (That is because namespace declarations are not explicitly stored in Elems, but are implicit, viz. parentElem.scope.relativize(this.scope)). The short versus long form of an empty element is also not remembered.

    Equality has not been defined for class Elem (that is, it is reference equality). There is no clear sensible notion of equality for XML trees at the abstraction level of Elem. For example, think about prefixes, "ignorable whitespace", DTDs and XSDs, etc.

    Annotations
    @SerialVersionUID()
  13. trait ElemApi[E <: ElemApi[E]] extends ParentElemApi[E]

    This is the best known part of the yaidom uniform query API.

    This is the best known part of the yaidom uniform query API. It is a sub-trait of trait eu.cdevreeze.yaidom.ParentElemApi. Many DOM-like element implementations in yaidom mix in this trait (indirectly, because some implementing sub-trait is mixed in), thus sharing this query API.

    This trait typically does not show up in application code using yaidom, yet its (uniform) API does. Hence, it makes sense to read the documentation of this trait, knowing that the API is offered by multiple element implementations.

    This trait is purely abstract. The most common implementation of this trait is eu.cdevreeze.yaidom.ElemLike. That trait only knows about elements (and not about other nodes), and only knows the following about elements:

    • elements can have child elements (as promised by the trait's super-trait)
    • elements have a so-called "resolved name", which is an eu.cdevreeze.yaidom.EName
    • elements have zero or more "resolved attributes", mapping attribute names (as ENames) to attribute values

    Using this minimal knowledge alone, that trait offers methods to query for descendant elements, descendant-or-self methods, or sub-collections thereof. Element sub-collections can be queried by passing a predicate (as offered by the super-trait), or simply by passing an element EName.

    It is this minimal knowledge that makes this API uniform. On the one hand, that minimal knowledge is enough knowledge for providing a rather rich ElemApi query API, and on the other hand, that minimal knowledge is so fundamental to DOM-like elements that most yaidom DOM-like element implementations indeed offer this API.

    This query API leverages the Scala Collections API. Query results can be manipulated using the Collections API, and the query API implementation (in ElemLike) uses the Collections API internally.

    ElemApi examples

    It is easy to show that this small query API is already very useful. Consider the following example XML:

    <book:Bookstore xmlns:book="http://bookstore/book" xmlns:auth="http://bookstore/author">
      <book:Book ISBN="978-0321356680" Price="35" Edition="2">
        <book:Title>Effective Java (2nd Edition)</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Joshua</auth:First_Name>
            <auth:Last_Name>Bloch</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
      <book:Book ISBN="978-0981531649" Price="35" Edition="2">
        <book:Title>Programming in Scala: A Comprehensive Step-by-Step Guide, 2nd Edition</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Martin</auth:First_Name>
            <auth:Last_Name>Odersky</auth:Last_Name>
          </auth:Author>
          <auth:Author>
            <auth:First_Name>Lex</auth:First_Name>
            <auth:Last_Name>Spoon</auth:Last_Name>
          </auth:Author>
          <auth:Author>
            <auth:First_Name>Bill</auth:First_Name>
            <auth:Last_Name>Venners</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
    </book:Bookstore>

    Suppose this XML has been parsed into eu.cdevreeze.yaidom.Elem instance bookstoreElem. Then we can perform the following queries:

    val bookstoreNamespace = "http://bookstore/book"
    val authorNamespace = "http://bookstore/author"
    require(bookstoreElem.resolvedName == EName(bookstoreNamespace, "Bookstore"))
    
    val cheapBookElems =
      for {
        bookElem <- bookstoreElem \ EName(bookstoreNamespace, "Book")
        price <- bookElem \@ EName("Price")
        if price.toInt < 90
      } yield bookElem
    
    val cheapBookAuthors = {
      val result =
        for {
          cheapBookElem <- cheapBookElems
          authorElem <- cheapBookElem \\ EName(authorNamespace, "Author")
        } yield {
          val firstName = authorElem \ EName(authorNamespace, "First_Name") map (_.text) mkString ""
          val lastName = authorElem \ EName(authorNamespace, "Last_Name") map (_.text) mkString ""
          (firstName + " " + lastName).trim
        }
      result.toSet
    }

    Using more ParentElemApi query methods, we could instead have written:

    val cheapBookElems =
      for {
        bookElem <- bookstoreElem \ (e => e.resolvedName == EName(bookstoreNamespace, "Book"))
        price <- bookElem \@ EName("Price")
        if price.toInt < 90
      } yield bookElem
    
    val cheapBookAuthors = {
      val result =
        for {
          cheapBookElem <- cheapBookElems
          authorElem <- cheapBookElem \\ (e => e.resolvedName == EName(authorNamespace, "Author"))
        } yield {
          val firstName =
            authorElem \ (e => e.resolvedName == EName(authorNamespace, "First_Name")) map (_.text) mkString ""
          val lastName =
            authorElem \ (e => e.resolvedName == EName(authorNamespace, "Last_Name")) map (_.text) mkString ""
          (firstName + " " + lastName).trim
        }
      result.toSet
    }

    By replacing operator notation, we get the following equivalent queries:

    val cheapBookElems =
      for {
        bookElem <- bookstoreElem filterChildElems (e => e.resolvedName == EName(bookstoreNamespace, "Book"))
        price <- bookElem.attributeOption(EName("Price"))
        if price.toInt < 90
      } yield bookElem
    
    val cheapBookAuthors = {
      val result =
        for {
          cheapBookElem <- cheapBookElems
          authorElem <- cheapBookElem filterElemsOrSelf (e => e.resolvedName == EName(authorNamespace, "Author"))
        } yield {
          val firstName =
            authorElem filterChildElems (e => e.resolvedName == EName(authorNamespace, "First_Name")) map (_.text) mkString ""
          val lastName =
            authorElem filterChildElems (e => e.resolvedName == EName(authorNamespace, "Last_Name")) map (_.text) mkString ""
          (firstName + " " + lastName).trim
        }
      result.toSet
    }

    The queries above only use the following knowledge about the DOM-like elements: they offer the ElemApi and HasText APIs. As a consequence, the exact same queries work for other DOM-like element implementations as well. That is, bookstoreElem could instead have been of type eu.cdevreeze.yaidom.indexed.Elem, eu.cdevreeze.yaidom.resolved.Elem, eu.cdevreeze.yaidom.dom.DomElem or eu.cdevreeze.yaidom.scalaxml.ScalaXmlElem. Hence the ElemApi trait indeed offers a uniform element query API.

    ElemApi more formally

    From a formal point of view, ElemApi offers little of interest. After all, given super-trait ParentElemApi, as well as methods resolvedName and resolvedAttributes, the other methods are trivial to implement.

    For example, the semantics of method filterChildElems (taking an EName) is trivially defined as follows:

    elem.filterChildElems(ename) == elem.filterChildElems(e => e.resolvedName == ename)

    Other ParentElemApi methods taking a predicate also have a counterpart in ElemApi taking just an EName, and the latter ones are trivially defined in terms of the former ones, just like filterChildElems (taking an EName) above. After all, parent trait ParentElemApi is the foundation of the yaidom query API, yet sub-trait ElemApi makes it much more useful in practice, by adding some knowledge about element names and attributes.

    E

    The captured element subtype

  14. final class ElemBuilder extends NodeBuilder with ParentElemLike[ElemBuilder] with TransformableElemLike[NodeBuilder, ElemBuilder] with HasQName

    Builder for elements.

    Builder for elements. See eu.cdevreeze.yaidom.NodeBuilder.

    See the documentation of the mixed-in query API trait(s) for more details on the uniform query API offered by this class.

    Annotations
    @SerialVersionUID()
  15. trait ElemConverter[A] extends AnyRef

    Converter from eu.cdevreeze.yaidom.Elem to A (which can be anything, such as a DOM Element).

    Converter from eu.cdevreeze.yaidom.Elem to A (which can be anything, such as a DOM Element).

    A

    the result type of the conversion

  16. trait ElemLike[E <: ElemLike[E]] extends ParentElemLike[E] with ElemApi[E]

    API and implementation trait for elements as containers of elements, each having a name and possible attributes.

    API and implementation trait for elements as containers of elements, each having a name and possible attributes. This trait extends trait eu.cdevreeze.yaidom.ParentElemLike, adding knowledge about names of elements and of attributes.

    More precisely, this trait adds the following abstract methods to the abstract methods required by its super-trait: resolvedName and resolvedAttributes. Based on these abstract methods (and the super-trait), this trait offers a rich API for querying elements by (expanded) name, and for querying attributes.

    The purely abstract API offered by this trait is eu.cdevreeze.yaidom.ElemApi. See the documentation of that trait for examples of usage, and for a light formal treatment.

    E

    The captured element subtype

  17. final case class EntityRef(entity: String) extends Node with Product with Serializable

    An entity reference.

    An entity reference. For example:

    &hello;

    We obtain this entity reference as follows:

    EntityRef("hello")
    Annotations
    @SerialVersionUID()
  18. final case class EntityRefBuilder(entity: String) extends NodeBuilder with Product with Serializable

    Annotations
    @SerialVersionUID()
  19. trait HasParent[E <: HasParent[E]] extends AnyRef

    API and implementation trait for elements that can be asked for the ancestor elements, if any.

    API and implementation trait for elements that can be asked for the ancestor elements, if any.

    This trait only knows about elements, not about documents as root element parents.

    Based on abstract method parentOption alone, this trait offers a rich API for querying the element ancestry of an element.

    E

    The captured element subtype

  20. trait HasQName extends AnyRef

    Trait defining the contract for elements that have a QName, as well as attributes with QName keys.

    Trait defining the contract for elements that have a QName, as well as attributes with QName keys.

    Using this trait (possibly in combination with other "element traits") we can abstract over several element implementations.

  21. trait HasText extends AnyRef

    Trait defining the contract for elements as text containers.

    Trait defining the contract for elements as text containers. Typical element types are both an eu.cdevreeze.yaidom.ElemLike as well as a eu.cdevreeze.yaidom.HasText.

  22. trait NavigableElemApi[E <: NavigableElemApi[E]] extends ElemApi[E]

    This trait adds Path-based navigation support to its super-trait eu.cdevreeze.yaidom.ElemApi.

    This trait adds Path-based navigation support to its super-trait eu.cdevreeze.yaidom.ElemApi. The trait is the Path-based navigation part of the yaidom uniform query API. It is a sub-trait of trait eu.cdevreeze.yaidom.ElemApi.

    This trait typically does not show up in application code using yaidom, yet its (uniform) API does. Hence, it makes sense to read the documentation of this trait, knowing that the API is offered by multiple element implementations.

    This trait is purely abstract. The most common implementation of this trait is eu.cdevreeze.yaidom.NavigableElemLike. Most element implementations mixing in this trait also mix in sub-trait eu.cdevreeze.yaidom.PathAwareElemLike. See the latter trait for more information.

    E

    The captured element subtype

  23. trait NavigableElemLike[E <: NavigableElemLike[E]] extends ElemLike[E] with NavigableElemApi[E]

    API and implementation trait for elements as containers of elements, each having a name and possible attributes, as well as supporting navigation using paths.

    API and implementation trait for elements as containers of elements, each having a name and possible attributes, as well as supporting navigation using paths. This trait extends trait eu.cdevreeze.yaidom.ElemLike, adding navigation using paths.

    More precisely, this trait adds the following abstract method to the abstract methods required by its super-trait: findChildElemByPathEntry.

    The purely abstract API offered by this trait is eu.cdevreeze.yaidom.NavigableElemApi. See the documentation of that trait for more information.

    E

    The captured element subtype

  24. sealed trait Node extends Immutable with Serializable

    Immutable XML Node.

    Immutable XML Node. It is the default XML node type in yaidom. There are subclasses for different types of nodes, such as elements, text nodes, comments, entity references and processing instructions. See eu.cdevreeze.yaidom.Elem for the default element type in yaidom.

  25. sealed trait NodeBuilder extends Immutable with Serializable

    DSL to build Elems (or Documents) without having to pass parent Scopes around.

    DSL to build Elems (or Documents) without having to pass parent Scopes around. For example:

    import NodeBuilder._
    
    elem(
      qname = QName("dbclass:Magazine"),
      attributes = Vector(QName("Month") -> "February", QName("Year") -> "2009"),
      namespaces = Declarations.from("dbclass" -> "http://www.db-class.org"),
      children = Vector(
        elem(
          qname = QName("dbclass:Title"),
          children = Vector(text("Newsweek"))))).build()

    The latter expression could also be written as follows:

    elem(
    qname = QName("dbclass:Magazine"),
    attributes = Vector(QName("Month") -> "February", QName("Year") -> "2009"),
    namespaces = Declarations.from("dbclass" -> "http://www.db-class.org"),
    children = Vector(
      textElem(QName("dbclass:Title"), "Newsweek"))).build()

    There is an impedance mismatch between XML's scoping rules (which are top-down, from root to leaves) and "functional trees" (which are built bottom-up, from leaves to root). In the context of the Anti-XML library, Daniel Spiewak explained this impedance mismatch in https://github.com/djspiewak/anti-xml/issues/78. In yaidom, however, this impedance mismatch is far less severe. Yaidom distinguishes between eu.cdevreeze.yaidom.Node and eu.cdevreeze.yaidom.NodeBuilder, and eu.cdevreeze.yaidom.Elem and eu.cdevreeze.yaidom.ElemBuilder in particular. Elems have (fixed, resolved) Scopes, but ElemBuilders do not. Using NodeBuilders, Scope determination is postponed. Only ElemBuilders can have unbound prefixes, but only Elems have (resolved) scopes. Instead of a eu.cdevreeze.yaidom.Scope, an ElemBuilder has a eu.cdevreeze.yaidom.Declarations.

    Another reason that the above-mentioned impedance mismatch is less of a problem in practice is that typically the XML trees (as NodeBuilders or directly as Nodes) are built in a top-down manner. The eu.cdevreeze.yaidom.ConverterToDocuments in package eu.cdevreeze.yaidom.convert recursively build Elems in a top-down manner, possibly creating an Elem instance (for each element) twice (first without children, and finally as a copy with children added).

    When using NodeBuilders to create a Document, this Document typically contains no "ignorable whitespace". This may cause the Document not to be pretty-printed when using a (default) eu.cdevreeze.yaidom.print.DocumentPrinter to convert the Document to an XML string. See also the classes in package eu.cdevreeze.yaidom.print.

  26. trait ParentElemApi[E <: ParentElemApi[E]] extends AnyRef

    This is the foundation of the yaidom uniform query API.

    This is the foundation of the yaidom uniform query API. Many DOM-like element implementations in yaidom mix in this trait (indirectly, because some implementing sub-trait is mixed in), thus sharing this query API.

    This trait typically does not show up in application code using yaidom, yet its (uniform) API does. Hence, it makes sense to read the documentation of this trait, knowing that the API is offered by multiple element implementations.

    This trait is purely abstract. The most common implementation of this trait is eu.cdevreeze.yaidom.ParentElemLike. That trait only knows about elements (and not about other nodes), and only knows that elements can have child elements (again not knowing about other child nodes). Using this minimal knowledge alone, it offers methods to query for descendant elements, descendant-or-self methods, or sub-collections thereof. It is this minimal knowledge that makes this API uniform.

    This query API leverages the Scala Collections API. Query results can be manipulated using the Collections API, and the query API implementation (in ParentElemLike) uses the Collections API internally.

    ParentElemApi examples

    To illustrate usage of this API, consider the following example. Let's say we want to determine if some XML has its namespace declarations (if any) only at the root element level. We show the query code for several yaidom DOM-like element implementations.

    Note that it depends on the DOM-like element implementation how to query for namespace declarations, but the code to query for descendant or descendant-or-self elements remains the same. The method to retrieve all descendant elements is called findAllElems, and the method to retrieve all descendant-or-self elements is called findAllElemsOrSelf. The corresponding "filtering" methods are called filterElems and filterElemsOrSelf, respectively. Knowing this, it is easy to guess the other API method names.

    Let's start with a yaidom DOM wrapper, named rootElem, of type eu.cdevreeze.yaidom.dom.DomElem, and query for the "offending" descendant elements:

    rootElem filterElems (elem => !convert.DomConversions.extractNamespaceDeclarations(elem.wrappedNode.getAttributes).isEmpty)

    This returns all offending elements, that is, all descendant elements of the root element (excluding the root element itself) that have at least one namespace declaration.

    Now let's use an eu.cdevreeze.yaidom.ElemBuilder, again named rootElem:

    rootElem filterElems (elem => !elem.namespaces.isEmpty)

    The query is the same as the preceding one, except for the retrieval of namespace declarations of an element. (It should be noted that class ElemBuilder already has a method allDeclarationsAreAtTopLevel.)

    Finally, let's use a rootElem of type eu.cdevreeze.yaidom.indexed.Elem, which is immutable, but knows its ancestry:

    rootElem filterElems (elem => !elem.namespaces.isEmpty)

    This is exactly the same code as for ElemBuilder, because namespace declarations happen to be retrieved in the same way.

    If we want to query for all elements with namespace declarations, including the root element itself, we could write:

    rootElem filterElemsOrSelf (elem => !elem.namespaces.isEmpty)

    In summary, the extremely simple ParentElemApi query API is indeed a uniform query API, offered by many different yaidom DOM-like element implementations. It should be noted that most of these element implementations offer the ElemApi query API, which extends the ParentElemApi query API.

    ParentElemApi more formally

    In order to get started using the API, this more formal section can safely be skipped. On the other hand, this section may provide a deeper understanding of the API.

    The ParentElemApi trait can be understood in a precise mathematical sense, as shown below.

    The most fundamental method of this trait is findAllChildElems. The semantics of the other methods can be defined directly or indirectly in terms of this method.

    The basic operations definable in terms of that method are \ (alias for filterChildElems), \\ (alias for filterElemsOrSelf) and \\! (alias for findTopmostElemsOrSelf). Their semantics must be as if they had been defined as follows:

    def filterChildElems(p: E => Boolean): immutable.IndexedSeq[E] =
      this.findAllChildElems.filter(p)
    
    def filterElemsOrSelf(p: E => Boolean): immutable.IndexedSeq[E] =
      Vector(this).filter(p) ++ (this.findAllChildElems flatMap (_.filterElemsOrSelf(p)))
    
    def findTopmostElemsOrSelf(p: E => Boolean): immutable.IndexedSeq[E] =
      if (p(this)) Vector(this)
      else (this.findAllChildElems flatMap (_.findTopmostElemsOrSelf(p)))

    Moreover, we could have defined:

    def filterElems(p: E => Boolean): immutable.IndexedSeq[E] =
      this.findAllChildElems flatMap (_.filterElemsOrSelf(p))
    
    def findTopmostElems(p: E => Boolean): immutable.IndexedSeq[E] =
      this.findAllChildElems flatMap (_.findTopmostElemsOrSelf(p))

    and:

    def findAllElemsOrSelf: immutable.IndexedSeq[E] = filterElemsOrSelf(e => true)
    
    def findAllElems: immutable.IndexedSeq[E] = filterElems(e => true)

    The following properties must hold (in the absence of side-effects), and can indeed be proven (given the documented "definitions" of these operations):

    // Filtering
    
    elem.filterElems(p) == elem.findAllElems.filter(p)
    
    elem.filterElemsOrSelf(p) == elem.findAllElemsOrSelf.filter(p)
    
    // Finding topmost
    
    elem.findTopmostElems(p) == {
      elem.filterElems(p) filter { e =>
        val hasNoMatchingAncestor = elem.filterElems(p) forall { _.findElem(_ == e).isEmpty }
        hasNoMatchingAncestor
      }
    }
    
    elem.findTopmostElemsOrSelf(p) == {
      elem.filterElemsOrSelf(p) filter { e =>
        val hasNoMatchingAncestor = elem.filterElemsOrSelf(p) forall { _.findElem(_ == e).isEmpty }
        hasNoMatchingAncestor
      }
    }
    
    (elem.findTopmostElems(p) flatMap (_.filterElemsOrSelf(p))) == (elem.filterElems(p))
    
    (elem.findTopmostElemsOrSelf(p) flatMap (_.filterElemsOrSelf(p))) == (elem.filterElemsOrSelf(p))
    E

    The captured element subtype

  27. trait ParentElemLike[E <: ParentElemLike[E]] extends ParentElemApi[E]

    API and implementation trait for elements as containers of elements, as element nodes in a node tree.

    API and implementation trait for elements as containers of elements, as element nodes in a node tree. This trait knows very little about elements. It does not know about names, attributes, etc. All it knows about elements is that elements can have element children (other node types are entirely out of scope in this trait).

    The purely abstract API offered by this trait is eu.cdevreeze.yaidom.ParentElemApi. See the documentation of that trait for examples of usage, and for a more formal treatment. Below follows an even more formal treatment, with proofs by induction of important properties obeyed by methods of this API. It shows the mathematical rigor of the yaidom query API. API users that are only interested in how to use the API can safely skip that formal treatment.

    ParentElemLike more formally

    In order to get started using the API, this more formal section can safely be skipped. On the other hand, this section may provide a deeper understanding of the API.

    The only abstract method is findAllChildElems. Based on this method alone, this trait offers a rich API for querying elements. This is entirely consistent with the semantics defined in the ParentElemApi trait. Indeed, the implementation of the methods follows the semantics defined there.

    In the ParentElemApi trait, some (simple) provable laws were mentioned. Some proofs follow below.

    1. Proving property about filterElemsOrSelf

    Below follows a proof by structural induction of one of the laws mentioned in the documentation of trait ParentElemApi.

    First we make a few assumptions, for this proof, and (implicitly) for the other proofs:

    • The function literals used in the properties ("element predicates" in this case) have no side-effects
    • These function literals terminate normally, without throwing any exception
    • These function literals are "closed terms", so the function values that are instances of these function literals are not "true closures"
    • These function literals use "fresh" variables, thus avoiding shadowing of variables defined in the context of the function literal
    • Equality on the element type is an equivalence relation (reflexive, symmetric, transitive)

    Based on these assumptions, we prove by induction that:

    elm.filterElemsOrSelf(p) == elm.findAllElemsOrSelf.filter(p)

    Base case

    If elm has no child elements, then the LHS can be rewritten as follows:

    elm.filterElemsOrSelf(p)
    immutable.IndexedSeq(elm).filter(p) ++ (elm.findAllChildElems flatMap (_.filterElemsOrSelf(p))) // definition of filterElemsOrSelf
    immutable.IndexedSeq(elm).filter(p) ++ (Seq() flatMap (_.filterElemsOrSelf(p))) // there are no child elements
    immutable.IndexedSeq(elm).filter(p) ++ Seq() // flatMap on empty sequence returns empty sequence
    immutable.IndexedSeq(elm).filter(p) // property of concatenation: xs ++ Seq() == xs
    (immutable.IndexedSeq(elm) ++ Seq()).filter(p) // property of concatenation: xs ++ Seq() == xs
    (immutable.IndexedSeq(elm) ++ (elm.findAllChildElems flatMap (_ filterElemsOrSelf (e => true)))) filter p // flatMap on empty sequence (of child elements) returns empty sequence
    (immutable.IndexedSeq(elm).filter(e => true) ++ (elm.findAllChildElems flatMap (_ filterElemsOrSelf (e => true)))) filter p // filtering with predicate that is always true
    elm.filterElemsOrSelf(e => true) filter p // definition of filterElemsOrSelf
    elm.findAllElemsOrSelf filter p // definition of findAllElemsOrSelf

    which is the RHS.

    Inductive step

    For the inductive step, we use the following (general) properties:

    (xs.filter(p) ++ ys.filter(p)) == ((xs ++ ys) filter p) // referred to below as property (a)

    and:

    (xs flatMap (x => f(x) filter p)) == ((xs flatMap f) filter p) // referred to below as property (b)

    If elm does have child elements, the LHS can be rewritten as:

    elm.filterElemsOrSelf(p)
    immutable.IndexedSeq(elm).filter(p) ++ (elm.findAllChildElems flatMap (_.filterElemsOrSelf(p))) // definition of filterElemsOrSelf
    immutable.IndexedSeq(elm).filter(p) ++ (elm.findAllChildElems flatMap (ch => ch.findAllElemsOrSelf filter p)) // induction hypothesis
    immutable.IndexedSeq(elm).filter(p) ++ ((elm.findAllChildElems.flatMap(ch => ch.findAllElemsOrSelf)) filter p) // property (b)
    (immutable.IndexedSeq(elm) ++ (elm.findAllChildElems flatMap (_.findAllElemsOrSelf))) filter p // property (a)
    (immutable.IndexedSeq(elm) ++ (elm.findAllChildElems flatMap (_ filterElemsOrSelf (e => true)))) filter p // definition of findAllElemsOrSelf
    (immutable.IndexedSeq(elm).filter(e => true) ++ (elm.findAllChildElems flatMap (_ filterElemsOrSelf (e => true)))) filter p // filtering with predicate that is always true
    elm.filterElemsOrSelf(e => true) filter p // definition of filterElemsOrSelf
    elm.findAllElemsOrSelf filter p // definition of findAllElemsOrSelf

    which is the RHS.

    This completes the proof. Other above-mentioned properties can be proven by induction in a similar way.

    2. Proving property about filterElems

    From the preceding proven property it easily follows (without using a proof by induction) that:

    elm.filterElems(p) == elm.findAllElems.filter(p)

    After all, the LHS can be rewritten as follows:

    elm.filterElems(p)
    (elm.findAllChildElems flatMap (_.filterElemsOrSelf(p))) // definition of filterElems
    (elm.findAllChildElems flatMap (e => e.findAllElemsOrSelf.filter(p))) // using the property proven above
    (elm.findAllChildElems flatMap (_.findAllElemsOrSelf)) filter p // using property (b) above
    (elm.findAllChildElems flatMap (_ filterElemsOrSelf (e => true))) filter p // definition of findAllElemsOrSelf
    elm.filterElems(e => true) filter p // definition of filterElems
    elm.findAllElems filter p // definition of findAllElems

    which is the RHS.

    3. Proving property about findTopmostElemsOrSelf

    Given the above-mentioned assumptions, we prove by structural induction that:

    (elm.findTopmostElemsOrSelf(p) flatMap (_.filterElemsOrSelf(p))) == (elm.filterElemsOrSelf(p))

    Base case

    If elm has no child elements, and p(elm) holds, then LHS and RHS evaluate to immutable.IndexedSeq(elm).

    If elm has no child elements, and p(elm) does not hold, then LHS and RHS evaluate to immutable.IndexedSeq().

    Inductive step

    For the inductive step, we introduce the following additional (general) property, if f and g have the same types:

    ((xs flatMap f) flatMap g) == (xs flatMap (x => f(x) flatMap g)) // referred to below as property (c)

    This is also known as the "associativity law for monads". (Monadic types obey 3 laws: associativity, left unit and right unit.)

    If elm does have child elements, and p(elm) holds, the LHS can be rewritten as:

    (elm.findTopmostElemsOrSelf(p) flatMap (_.filterElemsOrSelf(p)))
    immutable.IndexedSeq(elm) flatMap (_.filterElemsOrSelf(p)) // definition of findTopmostElemsOrSelf, knowing that p(elm) holds
    elm.filterElemsOrSelf(p) // definition of flatMap, applied to singleton sequence

    which is the RHS. In this case, we did not even need the induction hypothesis.

    If elm does have child elements, and p(elm) does not hold, the LHS can be rewritten as:

    (elm.findTopmostElemsOrSelf(p) flatMap (_.filterElemsOrSelf(p)))
    (elm.findAllChildElems flatMap (_.findTopmostElemsOrSelf(p))) flatMap (_.filterElemsOrSelf(p)) // definition of findTopmostElemsOrSelf, knowing that p(elm) does not hold
    elm.findAllChildElems flatMap (ch => ch.findTopmostElemsOrSelf(p) flatMap (_.filterElemsOrSelf(p))) // property (c)
    elm.findAllChildElems flatMap (_.filterElemsOrSelf(p)) // induction hypothesis
    immutable.IndexedSeq() ++ (elm.findAllChildElems flatMap (_.filterElemsOrSelf(p))) // definition of concatenation
    immutable.IndexedSeq(elm).filter(p) ++ (elm.findAllChildElems flatMap (_.filterElemsOrSelf(p))) // definition of filter, knowing that p(elm) does not hold
    elm.filterElemsOrSelf(p) // definition of filterElems

    which is the RHS.

    4. Proving property about findTopmostElems

    From the preceding proven property it easily follows (without using a proof by induction) that:

    (elm.findTopmostElems(p) flatMap (_.filterElemsOrSelf(p))) == (elm.filterElems(p))

    After all, the LHS can be rewritten to:

    (elm.findTopmostElems(p) flatMap (_.filterElemsOrSelf(p)))
    (elm.findAllChildElems flatMap (_.findTopmostElemsOrSelf(p))) flatMap (_.filterElemsOrSelf(p)) // definition of findTopmostElems
    elm.findAllChildElems flatMap (ch => ch.findTopmostElemsOrSelf(p) flatMap (_.filterElemsOrSelf(p))) // property (c)
    elm.findAllChildElems flatMap (_.filterElemsOrSelf(p)) // using the property proven above
    elm.filterElems(p) // definition of filterElems

    which is the RHS.

    5. Properties used in the proofs above

    There are several (unproven) properties that were used in the proofs above:

    (xs.filter(p) ++ ys.filter(p)) == ((xs ++ ys) filter p) // property (a); filter distributes over concatenation
    
    (xs flatMap (x => f(x) filter p)) == ((xs flatMap f) filter p) // property (b)
    
    // Associativity law for monads
    ((xs flatMap f) flatMap g) == (xs flatMap (x => f(x) flatMap g)) // property (c)

    Property (a) is obvious, and stated without proof. Property (c) is known as the "associativity law for monads". Property (b) is proven below.

    To prove property (b), we use property (c), as well as the following property (d):

    (xs filter p) == (xs flatMap (y => if (p(y)) List(y) else Nil)) // property (d)

    Then property (b) can be proven as follows:

    xs flatMap (x => f(x) filter p)
    xs flatMap (x => f(x) flatMap (y => if (p(y)) List(y) else Nil))
    (xs flatMap f) flatMap (y => if (p(y)) List(y) else Nil) // property (c)
    (xs flatMap f) filter p

    Implementation notes

    Methods findAllElemsOrSelf, filterElemsOrSelf, findTopmostElemsOrSelf and findElemOrSelf use recursion in their implementations, but not tail-recursion. The lack of tail-recursion should not be a problem, due to limited XML tree depths in practice. It is comparable to an "idiomatic" Scala quicksort implementation in its lack of tail-recursion. Also in the case of quicksort, the lack of tail-recursion is acceptable due to limited recursion depths. If we want tail-recursive implementations of the above-mentioned methods (in particular the first 3 ones), we either lose the ordering of result elements in document order (depth-first), or we lose performance and/or clarity. That just is not worth it.

    E

    The captured element subtype

  28. final class Path extends Immutable

    Unique identification of a descendant (or self) Elem given a root Elem.

    Unique identification of a descendant (or self) Elem given a root Elem. It represents a unique path to an element, given a root element, independent of other types of nodes, as if the XML tree only consists of elements.

    For example, consider the following XML:

    <book:Bookstore xmlns:book="http://bookstore/book" xmlns:auth="http://bookstore/author">
      <book:Book ISBN="978-0321356680" Price="35" Edition="2">
        <book:Title>Effective Java (2nd Edition)</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Joshua</auth:First_Name>
            <auth:Last_Name>Bloch</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
      <book:Book ISBN="978-0981531649" Price="35" Edition="2">
        <book:Title>Programming in Scala: A Comprehensive Step-by-Step Guide, 2nd Edition</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Martin</auth:First_Name>
            <auth:Last_Name>Odersky</auth:Last_Name>
          </auth:Author>
          <auth:Author>
            <auth:First_Name>Lex</auth:First_Name>
            <auth:Last_Name>Spoon</auth:Last_Name>
          </auth:Author>
          <auth:Author>
            <auth:First_Name>Bill</auth:First_Name>
            <auth:Last_Name>Venners</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
    </book:Bookstore>

    Then the last name of the first author of the Scala book (viz. Odersky) has the following path:

    Path.from(
    EName("{http://bookstore/book}Book") -> 1,
    EName("{http://bookstore/book}Authors") -> 0,
    EName("{http://bookstore/author}Author") -> 0,
    EName("{http://bookstore/author}Last_Name") -> 0
    )

    or:

    PathBuilder.from(
    QName("book:Book") -> 1,
    QName("book:Authors") -> 0,
    QName("auth:Author") -> 0,
    QName("auth:Last_Name") -> 0).build(Scope.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author"))

    Path instances are useful in certain queries (see eu.cdevreeze.yaidom.PathAwareElemLike), and in "functional updates" (see eu.cdevreeze.yaidom.UpdatableElemLike).

    An eu.cdevreeze.yaidom.Path corresponds to one and only one canonical path of the element (modulo prefix names), which is the corresponding (canonical) XPath expression. See http://ns.inria.org/active-tags/glossary/glossary.html#canonical-path. There is one catch, though. The Path does not know the root element name, so that is not a part of the corresponding canonical XPath expression. See the documentation of method toCanonicalXPath.

    The Path contains an IndexedSeq of path entries for a specific child element, grandchild element etc., but the (root) element itself is referred to by an empty list of path entries.

    As an alternative to class Path, each element in a tree could be uniquely identified by "path entries" that only contained a child index instead of an element name plus element child index (of element children with the given name). Yet that would be far less easy to use. Hence Path.Entry instances each contain an element name plus index.

    Warning: indexing using Paths can be slow, especially in large XML trees. Hence, it is advisable to use class Path wisely in queries and "functional updates". Most queries for elements can be written without them (using the methods in trait ParentElemLike, instead of those added by subtrait PathAwareElemLike).

  29. trait PathAwareElemApi[E <: PathAwareElemApi[E]] extends NavigableElemApi[E]

    This is the Path-aware part of the yaidom uniform query API.

    This is the Path-aware part of the yaidom uniform query API. It is a sub-trait of trait eu.cdevreeze.yaidom.NavigableElemApi. Only a few DOM-like element implementations in yaidom mix in this trait (indirectly, because some implementing sub-trait is mixed in), thus sharing this query API.

    This trait typically does not show up in application code using yaidom, yet its (uniform) API does. Hence, it makes sense to read the documentation of this trait, knowing that the API is offered by multiple element implementations.

    This trait is purely abstract. The most common implementation of this trait is eu.cdevreeze.yaidom.PathAwareElemLike. That trait only knows about elements (and not about other nodes), and only knows the following about elements:

    • elements can have child elements (as promised by ancestor trait ParentElemLike)
    • elements have a so-called "resolved name" (as promised by parent trait ElemLike)
    • elements have zero or more "resolved attributes" (as promised by parent trait ElemLike)
    • elements can be queried for path entries relative to the parent element

    Using this minimal knowledge alone, that trait not only offers the methods of its parent trait, but also:

    • methods mirroring the ParentElemLike query methods, but returning Path objects instead of elements

    In other words, the PathAwareElemApi trait is quite a rich query API, considering the minimal knowledge it needs to have about elements.

    This query API leverages the Scala Collections API. Query results can be manipulated using the Collections API, and the query API implementation (in PathAwareElemLike) uses the Collections API internally.

    PathAwareElemApi examples

    To illustrate the use of this API, consider the following example XML:

    <book:Bookstore xmlns:book="http://bookstore/book" xmlns:auth="http://bookstore/author">
      <book:Book ISBN="978-0321356680" Price="35" Edition="2">
        <book:Title>Effective Java (2nd Edition)</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Joshua</auth:First_Name>
            <auth:Last_Name>Bloch</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
      <book:Book ISBN="978-0981531649" Price="35" Edition="2">
        <book:Title>Programming in Scala: A Comprehensive Step-by-Step Guide, 2nd Edition</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Martin</auth:First_Name>
            <auth:Last_Name>Odersky</auth:Last_Name>
          </auth:Author>
          <auth:Author>
            <auth:First_Name>Lex</auth:First_Name>
            <auth:Last_Name>Spoon</auth:Last_Name>
          </auth:Author>
          <auth:Author>
            <auth:First_Name>Bill</auth:First_Name>
            <auth:Last_Name>Venners</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
    </book:Bookstore>

    Suppose this XML has been parsed into eu.cdevreeze.yaidom.Elem instance bookstoreElem. Then we can perform the following query, using only the ElemApi query API:

    val bookstoreNamespace = "http://bookstore/book"
    val authorNamespace = "http://bookstore/author"
    require(bookstoreElem.resolvedName == EName(bookstoreNamespace, "Bookstore"))
    
    val scalaBookElems =
      for {
        bookElem <- bookstoreElem \\ EName(bookstoreNamespace, "Book")
        if (bookElem \ EName(bookstoreNamespace, "Title")).map(_.text).headOption.getOrElse("").contains("Scala")
      } yield bookElem

    This is a top-down approach for finding Scala books. A bottom-up approach, using the PathAwareElemApi query API, could be coded as follows:

    val scalaBookElems =
    for {
      titlePath <- bookstoreElem filterElemPaths (e => e.resolvedName == EName(bookstoreNamespace, "Title"))
      if bookstoreElem.getElemOrSelfByPath(titlePath).text.contains("Scala")
      bookPath <- titlePath.findAncestorPath(_.endsWithName(EName(bookstoreNamespace, "Book")))
    } yield bookstoreElem.getElemOrSelfByPath(bookPath)

    A few remarks are in order:

    • Be careful to invoke getElemOrSelfByPath (or findElemOrSelfByPath) on the correct element
    • Invoking these methods in tight loops may harm performance
    • Note that paths are not stable, when (functionally) updating elements
    • The "filtering" query methods specific to trait PathAwareElemApi take predicates on elements, and not on paths or element-path-combinations. Not much would have been gained from that, because typical queries for paths contain getElemOrSelfByPath calls anyway. After all, what else are paths for?

    In spite of these remarks, the methods specific to the PathAwareElemApi trait are a nice tool in the yaidom querying toolbox.

    PathAwareElemApi more formally

    In order to get started using the API, this more formal section can safely be skipped. On the other hand, this section may provide a deeper understanding of the API.

    The PathAwareElemApi trait can be understood more formally, as shown below.

    The most fundamental methods of this trait are findAllChildElemsWithPathEntries and findChildElemByPathEntry. The semantics of the other methods can be defined directly or indirectly in terms of method findAllChildElemsWithPathEntries.

    The following must hold (for resolved.Elem, which has structural equality defined):

    elem.findAllChildElemsWithPathEntries.map(_._1) == elem.findAllChildElems
    
    elem.findAllChildElemsWithPathEntries forall { case (che, pe) => elem.findChildElemByPathEntry(pe).get == che }

    The basic operations definable in terms of method findAllChildElemsWithPathEntries are filterChildElemPaths, filterElemOrSelfPaths and findTopmostElemOrSelfPaths, analogous to trait ParentElemApi. Their semantics must be as if they had been defined as follows:

    def filterChildElemPaths(p: E => Boolean): immutable.IndexedSeq[Path] =
      this.findAllChildElemsWithPathEntries collect { case (che, pe) if p(che) => Path(Vector(pe)) }
    
    def filterElemOrSelfPaths(p: E => Boolean): immutable.IndexedSeq[Path] =
      (if (p(this)) Vector(Path.Root) else Vector()) ++ {
        this.findAllChildElemsWithPathEntries flatMap { case (che, pe) =>
          che.filterElemOrSelfPaths(p).map(_.prepend(pe))
        }
      }
    
    def findTopmostElemOrSelfPaths(p: E => Boolean): immutable.IndexedSeq[Path] =
      if (p(this)) Vector(Path.Root)
      else {
        this.findAllChildElemsWithPathEntries flatMap { case (che, pe) =>
          che.findTopmostElemOrSelfPaths(p).map(_.prepend(pe))
        }
      }

    Moreover, we could have defined:

    def filterElemPaths(p: E => Boolean): immutable.IndexedSeq[Path] =
      this.findAllChildElemsWithPathEntries flatMap { case (che, pe) =>
        che.filterElemOrSelfPaths(p).map(_.prepend(pe))
      }
    
    def findTopmostElemPaths(p: E => Boolean): immutable.IndexedSeq[Path] =
      this.findAllChildElemsWithPathEntries flatMap { case (che, pe) =>
        che.findTopmostElemOrSelfPaths(p).map(_.prepend(pe))
      }

    and:

    def findAllElemOrSelfPaths: immutable.IndexedSeq[Path] = filterElemOrSelfPaths(e => true)
    
    def findAllElemPaths: immutable.IndexedSeq[Path] = filterElemPaths(e => true)

    Then, analogously to ParentElemApi, the following properties hold:

    elem.filterElemPaths(p) == elem.findAllElemPaths.filter(path => p(elem.findElemOrSelfByPath(path).get))
    
    elem.filterElemOrSelfPaths(p) == elem.findAllElemOrSelfPaths.filter(path => p(elem.findElemOrSelfByPath(path).get))

    etc.

    Knowing that for resolved.Elem instance elem, we have:

    (elem.findAllChildElemsWithPathEntries map (_._1)) == elem.findAllChildElems

    it follows that:

    (elem.filterChildElemPaths(p) map (path => elem.findElemOrSelfByPath(path).get)) == elem.filterChildElems(p)
    
    (elem.filterElemOrSelfPaths(p) map (path => elem.findElemOrSelfByPath(path).get)) == elem.filterElemsOrSelf(p)
    
    (elem.filterElemPaths(p) map (path => elem.findElemOrSelfByPath(path).get)) == elem.filterElems(p)

    etc., where findElemOrSelfByPath is defined recursively, using method findChildElemByPathEntry.

    No proofs are provided. Note that the similarities with trait ParentElemLike are striking.

    E

    The captured element subtype

  30. trait PathAwareElemLike[E <: PathAwareElemLike[E]] extends NavigableElemLike[E] with PathAwareElemApi[E]

    API and implementation trait for elements as containers of elements, each having a name and possible attributes, as well as having awareness of paths.

    API and implementation trait for elements as containers of elements, each having a name and possible attributes, as well as having awareness of paths. This trait extends trait eu.cdevreeze.yaidom.NavigableElemLike, adding knowledge about paths.

    More precisely, this trait adds the following abstract method to the abstract methods required by its super-trait: findAllChildElemsWithPathEntries. Based on this abstract method (and the super-trait), this trait offers a rich API for querying elements and paths.

    The purely abstract API offered by this trait is eu.cdevreeze.yaidom.PathAwareElemApi. See the documentation of that trait for examples of usage, and for a more formal treatment.

    E

    The captured element subtype

  31. final class PathBuilder extends Immutable

    Builder for Path instances.

    Builder for Path instances.

    For example:

    val path: Path = PathBuilder.from(QName("parent") -> 0, QName("child") -> 2).build(Scope.Empty)

    Note that the indexes are 0-based. Also note that the Scope passed to the build method must be invertible. Otherwise the resolution of QNames can break the indexes of the path builder components.

  32. final case class PrefixedName(prefix: String, localPart: String) extends QName with Product with Serializable

  33. final case class ProcessingInstruction(target: String, data: String) extends Node with Product with Serializable

    Annotations
    @SerialVersionUID()
  34. final case class ProcessingInstructionBuilder(target: String, data: String) extends NodeBuilder with Product with Serializable

    Annotations
    @SerialVersionUID()
  35. sealed trait QName extends Immutable with Serializable

    Qualified name.

    Qualified name. See http://www.w3.org/TR/xml-names11/. Semantically like a QName in Anti-XML, and not like a QName in Java.

    There are 2 types of QNames:

    QNames are meaningless outside their scope, which resolves the QName as an eu.cdevreeze.yaidom.EName.

    Typical usage may lead to an explosion of different QName objects that are equal. Therefore, application code is encouraged to define and use constants for frequently used QNames. For example, for the XML Schema namespace:

    val XsPrefix = "xs"
    
    val XsElementQName = QName(XsPrefix, "element")
    val XsAttributeQName = QName(XsPrefix, "attribute")
    val XsComplexTypeQName = QName(XsPrefix, "complexType")
    val XsSimpleTypeQName = QName(XsPrefix, "simpleType")
    // ...

    In this example, the QName constant names are in upper camel case, starting with the prefix, followed by the local part, and ending with suffix "QName".

    Implementation note: It was tried as alternative implementation to define the QName subclasses as (Scala 2.10) value classes. The QName would then wrap the qualified name as string representation (with or without prefix). One cost would be that parsing the (optional) prefix and the local name would occur far more frequently. Another cost would be that the alternative implementation would not directly express that a QName is a combination of an optional prefix and a local part. Therefore that alternative implementation has been abandoned.

  36. trait QNameProvider extends AnyRef

    Provider of QNames, possibly from a cache of QNames.

    Provider of QNames, possibly from a cache of QNames. Typical implementations cache QName instances, to prevent any explosion of equal QName instances, thus unnecessarily increasing the memory footprint.

    Implementation notes

    It may seem rather lame that only the global QNameProvider variable can be updated. On the other hand, implicit QNameProvider parameters in many places in the API would change the API quite a bit. These implicit parameters would be implementation details leaking into the API. It was therefore decided not to introduce those implicit parameters, with the exception of only a few places inside implementation code in the library.

  37. final case class Scope(prefixNamespaceMap: Map[String, String]) extends Immutable with Product with Serializable

    Scope mapping prefixes to namespace URIs, as well as holding an optional default namespace.

    Scope mapping prefixes to namespace URIs, as well as holding an optional default namespace. In other words, in-scope namespaces.

    The purpose of a eu.cdevreeze.yaidom.Scope is to resolve eu.cdevreeze.yaidom.QNames as eu.cdevreeze.yaidom.ENames.

    For example, consider the following XML:

    <book:Bookstore xmlns:book="http://bookstore/book">
      <book:Book ISBN="978-0321356680" Price="35" Edition="2">
        <book:Title>Effective Java (2nd Edition)</book:Title>
        <book:Authors>
          <auth:Author xmlns:auth="http://bookstore/author">
            <auth:First_Name>Joshua</auth:First_Name>
            <auth:Last_Name>Bloch</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
    </book:Bookstore>

    Then the (only) author element has the following scope:

    Scope.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author")

    After all, the root element has the following scope:

    Scope.Empty.resolve(Declarations.from("book" -> "http://bookstore/book"))

    which is the same as:

    Scope.from("book" -> "http://bookstore/book")

    The (only) book element has no namespace declarations, so it has the same scope. That is also true for the authors element inside the book element. The (only) author element introduces a new namespace, and its scope is as follows:

    Scope.from("book" -> "http://bookstore/book").resolve(Declarations.from("auth" -> "http://bookstore/author"))

    which is indeed:

    Scope.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author")

    The author element QName("auth:Author") has (optional) resolved name:

    Scope.from("book" -> "http://bookstore/book", "auth" -> "http://bookstore/author").resolveQNameOption(QName("auth:Author"))

    which is:

    Some(EName("{http://bookstore/author}Author"))

    A Scope must not contain prefix "xmlns" and must not contain namespace URI "http://www.w3.org/2000/xmlns/". Moreover, a Scope must not contain the XML namespace (prefix "xml", namespace URI "http://www.w3.org/XML/1998/namespace").

    The Scope is backed by a map from prefixes (or the empty string for the default namespace) to (non-empty) namespace URIs.

    This class depends on Declarations, but not the other way around.

    Concise querying using QNames which are converted to ENames

    EName-based querying is more robust than QName-based querying, but ENames are more verbose than QNames. We can get the best of both ENames and QNames by querying using (concise) QNames, and by using a Scope converting them to (stable) ENames.

    For example:

    val scope = Scope.from("xs" -> "http://www.w3.org/2001/XMLSchema")
    import scope._
    
    val elemDecls = schemaElem \\ withEName(QName("xs", "element").res)

    This is exactly equivalent to the following query:

    import ElemApi._
    
    val elemDecls = schemaElem \\ withEName("http://www.w3.org/2001/XMLSchema", "element")

    Scope more formally

    In order to get started using the class, this more formal section can safely be skipped. On the other hand, this section may provide a deeper understanding of the class.

    Method resolve resolves a Declarations against this Scope, returning a new Scope. It could be defined by the following equality:

    scope.resolve(declarations) == {
    val m = (scope.prefixNamespaceMap ++ declarations.withoutUndeclarations.prefixNamespaceMap) -- declarations.retainingUndeclarations.prefixNamespaceMap.keySet
    Scope(m)
    }

    The actual implementation may be more efficient than that, but it is consistent with this definition.

    Method relativize relativizes a Scope against this Scope, returning a Declarations. It could be defined by the following equality:

    scope1.relativize(scope2) == {
    val declared = scope2.prefixNamespaceMap filter { case (pref, ns) => scope1.prefixNamespaceMap.getOrElse(pref, "") != ns }
    val undeclared = scope1.prefixNamespaceMap.keySet -- scope2.prefixNamespaceMap.keySet
    Declarations(declared) ++ Declarations.undeclaring(undeclared)
    }

    Again, the actual implementation may be more efficient than that, but it is consistent with this definition.

    1. Property about two Scopes, and its proof

    Methods relativize and resolve obey the following equality:

    scope1.resolve(scope1.relativize(scope2)) == scope2

    Below follows the proof. We distinguish among the following cases:

    • Prefix p has the same mappings in scope1 and scope2
    • Prefix p has different mappings in scope1 and scope2
    • Prefix p only belongs to scope1
    • Prefix p only belongs to scope2
    • Prefix p belongs to neither scope

    Prefix p can be the empty string, for the default namespace. For each of these cases, we prove that:

    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap.get(p) == scope2.prefixNamespaceMap.get(p)

    Since there are no other cases, that would complete the proof.

    If prefix p has the same mappings in both scopes, then:

    scope1.relativize(scope2).prefixNamespaceMap.get(p).isEmpty

    so the following equalities hold:

    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap(p)
    scope1.prefixNamespaceMap(p)
    scope2.prefixNamespaceMap(p)

    so:

    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap.get(p) == scope2.prefixNamespaceMap.get(p)

    If prefix p has different mappings in both scopes, then:

    scope1.relativize(scope2).prefixNamespaceMap(p) == scope2.prefixNamespaceMap(p)
    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap(p) == scope2.prefixNamespaceMap(p)
    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap.get(p) == scope2.prefixNamespaceMap.get(p)

    If prefix p only belongs to scope1, then:

    scope1.relativize(scope2).prefixNamespaceMap(p) == "" // undeclaration
    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap.get(p).isEmpty
    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap.get(p) == scope2.prefixNamespaceMap.get(p) // both empty

    if prefix p only belongs to scope2, then:

    scope1.relativize(scope2).prefixNamespaceMap(p) == scope2.prefixNamespaceMap(p)
    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap(p) == scope2.prefixNamespaceMap(p)
    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap.get(p) == scope2.prefixNamespaceMap.get(p)

    if prefix p belongs to neither scope, then obviously:

    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap.get(p).isEmpty
    scope1.resolve(scope1.relativize(scope2)).prefixNamespaceMap.get(p) == scope2.prefixNamespaceMap.get(p) // both empty
    2. Property about Scope and Declarations

    Methods relativize and resolve also obey the following equality:

    scope.relativize(scope.resolve(declarations)) == scope.minimize(declarations)

    where scope.minimize(declarations) is defined by the following equality:

    scope.minimize(declarations) == {
    val declared = declarations.withoutUndeclarations.prefixNamespaceMap filter { case (pref, ns) => scope.prefixNamespaceMap.getOrElse(pref, "") != ns }
    val undeclared = declarations.retainingUndeclarations.prefixNamespaceMap.keySet.intersect(scope.prefixNamespaceMap.keySet)
    Declarations(declared) ++ Declarations.undeclaring(undeclared)
    }

    It can be proven by distinguishing among the following cases:

    • Prefix p has the same mappings in scope and declarations (so no undeclaration)
    • Prefix p has different mappings in scope and declarations (but no undeclaration)
    • Prefix p belongs to scope and is undeclared in declarations
    • Prefix p only belongs to scope, and does not occur in declarations
    • Prefix p only occurs in declarations, without being undeclared, and does not occur in scope
    • Prefix p only occurs in declarations, in an undeclaration, and does not occur in scope
    • Prefix p neither occurs in scope nor in declarations

    Prefix p can be the empty string, for the default namespace. For each of these cases, it can be proven that:

    scope.relativize(scope.resolve(declarations)).prefixNamespaceMap.get(p) == scope.minimize(declarations).prefixNamespaceMap.get(p)

    Since there are no other cases, that would complete the proof. The proof itself is left as an exercise for the reader, as they say.

    This and the preceding (proven) property are analogous to corresponding properties in the URI class.

  38. final case class Text(text: String, isCData: Boolean) extends Node with Product with Serializable

    Annotations
    @SerialVersionUID()
  39. final case class TextBuilder(text: String, isCData: Boolean) extends NodeBuilder with Product with Serializable

    Annotations
    @SerialVersionUID()
  40. trait TransformableElemApi[N, E <: N with TransformableElemApi[N, E]] extends AnyRef

    This is the element transformation part of the yaidom query and update API.

    This is the element transformation part of the yaidom query and update API. Only a few DOM-like element implementations in yaidom mix in this trait (indirectly, because some implementing sub-trait is mixed in), thus sharing this API.

    This trait typically does not show up in application code using yaidom, yet its (uniform) API does. Hence, it makes sense to read the documentation of this trait, knowing that the API is offered by multiple element implementations.

    This trait is purely abstract. The most common implementation of this trait is eu.cdevreeze.yaidom.TransformableElemLike. That trait only knows how to transform child elements. Using this minimal knowledge, the trait offers methods to transform descendant elements and descendant-or-self elements. Indeed, the trait is similar to ParentElemLike, except that it transforms elements instead of querying for elements.

    The big conceptual difference with "updatable" elements (in trait UpdatableElemLike[N, E]) is that "transformations" are about applying some transforming function to an element tree, while "(functional) updates" are about "updates" at given paths.

    TransformableElemApi examples

    To illustrate the use of this API, consider the following example XML:

    <book:Bookstore xmlns:book="http://bookstore/book" xmlns:auth="http://bookstore/author">
      <book:Book ISBN="978-0321356680" Price="35" Edition="2">
        <book:Title>Effective Java (2nd Edition)</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Joshua</auth:First_Name>
            <auth:Last_Name>Bloch</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
      <book:Book ISBN="978-0981531649" Price="35" Edition="2">
        <book:Title>Programming in Scala: A Comprehensive Step-by-Step Guide, 2nd Edition</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Martin</auth:First_Name>
            <auth:Last_Name>Odersky</auth:Last_Name>
          </auth:Author>
          <auth:Author>
            <auth:First_Name>Lex</auth:First_Name>
            <auth:Last_Name>Spoon</auth:Last_Name>
          </auth:Author>
          <auth:Author>
            <auth:First_Name>Bill</auth:First_Name>
            <auth:Last_Name>Venners</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
    </book:Bookstore>

    Suppose this XML has been parsed into eu.cdevreeze.yaidom.Elem variable named bookstoreElem. Then we can combine author first and last names as follows:

    val authorNamespace = "http://bookstore/author"
    
    bookstoreElem = bookstoreElem transformElems {
      case elem: Elem if elem.resolvedName == EName(authorNamespace, "Author") =>
        val firstName = (elem \ (_.localName == "First_Name")).headOption.map(_.text).getOrElse("")
        val lastName = (elem \ (_.localName == "Last_Name")).headOption.map(_.text).getOrElse("")
        val name = (firstName + " " + lastName).trim
        Node.textElem(QName("auth:Author"), elem.scope ++ Scope.from("auth" -> authorNamespace), name)
      case elem: Elem => elem
    }
    bookstoreElem = bookstoreElem.prettify(2)

    When using the TransformableElemApi API, keep the following in mind:

    • The transformElems and transformElemsOrSelf methods (and their Node sequence producing counterparts) may produce a lot of "garbage". If only a small portion of an element tree needs to be updated, the "update" methods in trait UpdatableElemApi may be a better fit.
    • Transformations operate in a bottom-up manner. This implies that parent scopes cannot be used for transforming child elements. Hence, namespace undeclarations may result, which are not allowed in XML 1.0 (except for the default namespace).

    Top-down transformations are still possible, by combining recursion with method transformChildElems (or transformChildElemsToNodeSeq). For example:

    def removePrefixedNamespaceUndeclarations(elem: Elem): Elem = {
    elem transformChildElems { e =>
      val newE = e.copy(scope = elem.scope.withoutDefaultNamespace ++ e.scope)
      removePrefixedNamespaceUndeclarations(newE)
    }
    }
    N

    The node supertype of the element subtype

    E

    The captured element subtype

  41. trait TransformableElemLike[N, E <: N with TransformableElemLike[N, E]] extends TransformableElemApi[N, E]

    API and implementation trait for transformable elements.

    API and implementation trait for transformable elements.

    More precisely, this trait has abstract methods transformChildElems and transformChildElemsToNodeSeq. Based on these abstract methods, this trait offers a rich API for transforming descendant elements or descendant-or-self elements.

    The purely abstract API offered by this trait is eu.cdevreeze.yaidom.TransformableElemApi. See the documentation of that trait for examples of usage.

    N

    The node supertype of the element subtype

    E

    The captured element subtype

  42. final case class UnprefixedName(localPart: String) extends QName with Product with Serializable

  43. trait UpdatableElemApi[N, E <: N with UpdatableElemApi[N, E]] extends PathAwareElemApi[E]

    This is the functional update part of the yaidom uniform query API.

    This is the functional update part of the yaidom uniform query API. It is a sub-trait of trait eu.cdevreeze.yaidom.PathAwareElemApi. Only a few DOM-like element implementations in yaidom mix in this trait (indirectly, because some implementing sub-trait is mixed in), thus sharing this query API.

    This trait typically does not show up in application code using yaidom, yet its (uniform) API does. Hence, it makes sense to read the documentation of this trait, knowing that the API is offered by multiple element implementations.

    This trait is purely abstract. The most common implementation of this trait is eu.cdevreeze.yaidom.UpdatableElemLike. The trait has all the knowledge of its super-trait, but in addition to that knows the following:

    • An element has child nodes, which may or may not be elements. Hence the extra type parameter for nodes.
    • An element knows the child node indexes of the path entries of the child elements.

    Obviously methods children, withChildren and childNodeIndexesByPathEntries must be consistent with methods such as findAllChildElems and findAllChildElemsWithPathEntries.

    Using this minimal knowledge alone, trait UpdatableElemLike not only offers the methods of its parent trait, but also:

    • methods to functionally update an element by replacing, adding or deleting child nodes
    • methods to functionally update an element by replacing descendant-or-self elements at specified paths

    In other words, the UpdatableElemApi trait is quite a rich query API, considering the minimal knowledge it needs to have about elements.

    For the conceptual difference with "transformable" elements, see trait eu.cdevreeze.yaidom.TransformableElemApi.

    This query API leverages the Scala Collections API. Query results can be manipulated using the Collections API, and the query API implementation (in UpdatableElemLike) uses the Collections API internally.

    UpdatableElemApi examples

    To illustrate the use of this API, consider the following example XML:

    <book:Bookstore xmlns:book="http://bookstore/book" xmlns:auth="http://bookstore/author">
      <book:Book ISBN="978-0321356680" Price="35" Edition="2">
        <book:Title>Effective Java (2nd Edition)</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Joshua</auth:First_Name>
            <auth:Last_Name>Bloch</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
      <book:Book ISBN="978-0981531649" Price="35" Edition="2">
        <book:Title>Programming in Scala: A Comprehensive Step-by-Step Guide, 2nd Edition</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Martin</auth:First_Name>
            <auth:Last_Name>Odersky</auth:Last_Name>
          </auth:Author>
          <auth:Author>
            <auth:First_Name>Lex</auth:First_Name>
            <auth:Last_Name>Spoon</auth:Last_Name>
          </auth:Author>
          <auth:Author>
            <auth:First_Name>Bill</auth:First_Name>
            <auth:Last_Name>Venners</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
    </book:Bookstore>

    Suppose this XML has been parsed into eu.cdevreeze.yaidom.Elem variable named bookstoreElem. Then we can add a book as follows, where we "forget" the 2nd author for the moment:

    import convert.ScalaXmlConversions._
    
    val bookstoreNamespace = "http://bookstore/book"
    val authorNamespace = "http://bookstore/author"
    
    val fpBookXml =
      <book:Book xmlns:book="http://bookstore/book" xmlns:auth="http://bookstore/author" ISBN="978-1617290657" Price="33">
        <book:Title>Functional Programming in Scala</book:Title>
        <book:Authors>
          <auth:Author>
            <auth:First_Name>Paul</auth:First_Name>
            <auth:Last_Name>Chiusano</auth:Last_Name>
          </auth:Author>
        </book:Authors>
      </book:Book>
    val fpBookElem = convertToElem(fpBookXml)
    
    bookstoreElem = bookstoreElem.plusChild(fpBookElem)

    Note that the namespace declarations for prefixes book and auth had to be repeated in the Scala XML literal for the added book, because otherwise the convertToElem method would throw an exception (since Elem instances cannot be created unless all element and attribute QNames can be resolved as ENames).

    The resulting bookstore seems ok, but if we print convertElem(bookstoreElem), the result does not look pretty. This can be fixed if the last assignment is replaced by:

    bookstoreElem = bookstoreElem.plusChild(fpBookElem).prettify(2)

    knowing that an indentation of 2 spaces has been used throughout the original XML. Method prettify is expensive, so it is best not to invoke it within a tight loop. As an alternative, formatting can be left to the DocumentPrinter, of course.

    The assignment above is the same as the following one:

    bookstoreElem = bookstoreElem.withChildren(bookstoreElem.children :+ fpBookElem).prettify(2)

    There are several methods to functionally update the children of an element. For example, method plusChild is overloaded, and the other variant can insert a child at a given 0-based position. Other "children update" methods are minusChild, withPatchedChildren and withUpdatedChildren.

    Let's now turn to functional update methods that take Path instances or collections thereof. In the example above the second author of the added book is missing. Let's fix that:

    val secondAuthorXml =
      <auth:Author xmlns:auth="http://bookstore/author">
        <auth:First_Name>Runar</auth:First_Name>
        <auth:Last_Name>Bjarnason</auth:Last_Name>
      </auth:Author>
    val secondAuthorElem = convertToElem(secondAuthorXml)
    
    val fpBookAuthorsPaths =
      for {
        authorsPath <- bookstoreElem filterElemPaths { e => e.resolvedName == EName(bookstoreNamespace, "Authors") }
        if authorsPath.findAncestorPath(path => path.endsWithName(EName(bookstoreNamespace, "Book")) &&
          bookstoreElem.getElemOrSelfByPath(path).attribute(EName("ISBN")) == "978-1617290657").isDefined
      } yield authorsPath
    
    require(fpBookAuthorsPaths.size == 1)
    val fpBookAuthorsPath = fpBookAuthorsPaths.head
    
    bookstoreElem = bookstoreElem.updated(fpBookAuthorsPath) { elem =>
      require(elem.resolvedName == EName(bookstoreNamespace, "Authors"))
      val rawResult = elem.plusChild(secondAuthorElem)
      rawResult transformElemsOrSelf (e => e.copy(scope = elem.scope.withoutDefaultNamespace ++ e.scope))
    }
    bookstoreElem = bookstoreElem.prettify(2)

    Clearly the resulting bookstore element is nicely formatted, but there was another possible issue that was taken into account. See the line of code transforming the "raw result". That line was added in order to prevent namespace undeclarations, which for XML version 1.0 are not allowed (with the exception of the default namespace). After all, the XML for the second author was created with only the auth namespace declared. Without the above-mentioned line of code, a namespace undeclaration for prefix book would have occurred in the resulting XML, thus leading to an invalid XML 1.0 element tree.

    To illustrate functional update methods taking collections of paths, let's remove the added book from the book store. Here is one (somewhat inefficient) way to do that:

    val bookPaths = bookstoreElem filterElemPaths (_.resolvedName == EName(bookstoreNamespace, "Book"))
    
    bookstoreElem = bookstoreElem.updatedWithNodeSeqAtPaths(bookPaths.toSet) { (elem, path) =>
      if ((elem \@ EName("ISBN")) == Some("978-1617290657")) Vector() else Vector(elem)
    }
    bookstoreElem = bookstoreElem.prettify(2)

    There are very many ways to write this functional update, using different functional update methods in trait UpdatableElemApi, or even only using transformation methods in trait TransformableElemApi (thus not using paths).

    The example code above is enough to get started using the UpdatableElemApi methods, but it makes sense to study the entire API, and practice with it. Always keep in mind that functional updates typically mess up formatting and/or namespace (un)declarations, unless these aspects are taken into account.

    N

    The node supertype of the element subtype

    E

    The captured element subtype

  44. trait UpdatableElemLike[N, E <: N with UpdatableElemLike[N, E]] extends PathAwareElemLike[E] with UpdatableElemApi[N, E]

    API and implementation trait for functionally updatable elements.

    API and implementation trait for functionally updatable elements. This trait extends trait eu.cdevreeze.yaidom.PathAwareElemLike, adding knowledge about child nodes in general, and about the correspondence between child path entries and child indexes.

    More precisely, this trait adds the following abstract methods to the abstract methods required by its super-trait: children, withChildren and childNodeIndexesByPathEntries. Based on these abstract methods (and the super-trait), this trait offers a rich API for querying elements and paths, and for functionally updating elements.

    The purely abstract API offered by this trait is eu.cdevreeze.yaidom.UpdatableElemApi. See the documentation of that trait for examples of usage, and for a more formal treatment.

    N

    The node supertype of the element subtype

    E

    The captured element subtype

Value Members

  1. object Declarations extends Serializable

  2. object DocBuilder extends Serializable

  3. object Document extends Serializable

  4. object EName extends Serializable

  5. object ENameProvider

  6. object Elem extends Serializable

  7. object ElemApi

    This companion object offers some convenience factory methods for "element predicates", that can be used in yaidom queries.

    This companion object offers some convenience factory methods for "element predicates", that can be used in yaidom queries. These factory objects turn ENames and local names into "element predicates".

    For example:

    elem \\ EName(xsNamespace, "element")

    can also be written as:

    elem \\ withEName(xsNamespace, "element")

    (thus avoiding EName instance construction, whether or not this makes any difference in practice).

    If the namespace is "obvious", and more friendly local-name-based querying is desired, the following could be written:

    elem \\ withLocalName("element")
  8. object Node extends Serializable

    This singleton object contains a DSL to easily create deeply nested Elems.

    This singleton object contains a DSL to easily create deeply nested Elems. It looks a lot like the DSL for NodeBuilders, using the same method names (so a local import for Node singleton object members may be needed).

    There is a catch, though. When using this DSL, scopes must be passed throughout the tree. These Scopes should typically be the same (or parent element scopes should be subscopes of child element scopes), because otherwise the corresponding XML may contain a lot of namespace undeclarations.

    Another thing to watch out for is that the "tree representations" conform to the NodeBuilder DSL, not to this one.

    In summary, the NodeBuilder DSL does have the advantage over this DSL that scopes do not have to be passed around. On the other hand, this Node DSL has the advantage that exceptions due to missing scope data are thrown immediately instead of later (when calling the build method, in the case of the NodeBuilder DSL).

    For example:

    import Node._
    
    val scope = Scope.from("dbclass" -> "http://www.db-class.org")
    
    elem(
      qname = QName("dbclass:Magazine"),
      attributes = Vector(QName("Month") -> "February", QName("Year") -> "2009"),
      scope = scope,
      children = Vector(
        elem(
          qname = QName("dbclass:Title"),
          scope = scope,
          children = Vector(text("Newsweek")))))

    The latter expression could also be written as follows:

    elem(
    qname = QName("dbclass:Magazine"),
    attributes = Vector(QName("Month") -> "February", QName("Year") -> "2009"),
    scope = scope,
    children = Vector(
      textElem(QName("dbclass:Title"), scope, "Newsweek")))
  9. object NodeBuilder extends Serializable

  10. object Path

  11. object PathBuilder

  12. object QName extends Serializable

  13. object QNameProvider

  14. object Scope extends Serializable

  15. object TreeReprParsers extends JavaTokenParsers

    Generator for parsers of "tree representation" expressions.

    Generator for parsers of "tree representation" expressions. The results from successful parses are NodeBuilder and DocBuilder instances.

    The "tree representation" expressions are themselves valid Scala code!

    Note: Parsing large "tree representations" is very slow.

  16. package convert

    Support for conversions from/to yaidom.

    Support for conversions from/to yaidom. This package mostly contains conversions between yaidom objects and JAXP DOM or StAX objects, in both directions.

    This conversion support is used by the Document parsers and printers in the parse and print packages, respectively. This package can also be used directly by consumers of the yaidom API.

    These JAXP-object conversions suggest that yaidom is optimistic about the available (heap) memory.

    This package depends on the eu.cdevreeze.yaidom package, and not the other way around. The eu.cdevreeze.yaidom.parse and eu.cdevreeze.yaidom.print packages depend on this package.

  17. package docaware

    This package contains element representations that contain the "context" of the element, including the URI of the containing document.

    This package contains element representations that contain the "context" of the element, including the URI of the containing document. In other words, this package contains element representations like indexed.Elem, except that the document URI is stored as well in each element.

    Below follows a simple example query, using the uniform query API:

    // Note the import of package docaware, and not of its members. That is indeed a best practice!
    import eu.cdevreeze.yaidom.docaware
    
    val docawareBookstoreElem = docaware.Elem(bookstoreElem)
    
    val scalaBookAuthors =
      for {
        bookElem <- docawareBookstoreElem \ EName("{http://bookstore/book}Book")
        if (bookElem \@ EName("ISBN")) == Some("978-0981531649")
        authorElem <- bookElem \\ EName("{http://bookstore/author}Author")
      } yield authorElem

    The query for Scala book authors would have been exactly the same if normal Elems had been used instead of docaware.Elems (replacing docawareBookstoreElem by bookstoreElem)!

  18. package dom

    Wrapper around class org.w3c.dom.Element, adapting it to the eu.cdevreeze.yaidom.ElemLike API.

    Wrapper around class org.w3c.dom.Element, adapting it to the eu.cdevreeze.yaidom.ElemLike API.

    This wrapper is not thread-safe, and should only be used if the immutable element classes such as eu.cdevreeze.yaidom.Elem are not the best fit.

    Such scenarios could be as follows:

    • Conversions from DOM to eu.cdevreeze.yaidom.Elem (and back) have more runtime costs than needed or wanted.
    • Roundtripping from XML string to "tree", and back to XML string should keep the resulting XML string as much as possible the same.
    • In-place updates (instead of "functional updates") of DOM trees are desired.

    Yet be aware that the advantages of immutability and thread-safety (offered by immutable Elem classes) are lost when using this wrapper API. Mutable DOM trees are also very easy to break, even via the ParentElemLike API, if element predicates with side-effects are used.

    To explain the "round-tripping" item above, note that class eu.cdevreeze.yaidom.Elem considers attributes in an element unordered, let alone namespace declarations. That is consistent with the XML InfoSet specification, but can sometimes be impractical. Using org.w3c.dom.Element instances, parsed from XML input sources, chances are that this order is retained.

    There are of course limitations to what formatting data is retained in a DOM tree. A good example is the short versus long form of an empty element. Typically parsers do not pass any information about this distinction, so it is unknown whether the XML input source used the long or short form for an empty element.

    It should also be noted that the configuration of XML parsers and serializers can be of substantial influence on the extent that "round-tripping" keeps the XML string the same. Whitespace handling is one such area in which different configurations can lead to quite different "round-tripping" results.

    Note that in one way these wrappers are somewhat unnatural: the ElemLike API uses immutable Scala collections everywhere, whereas the elements of those collections are mutable (!) DOM node wrappers. The wrappers are idiomatic Scala in their use of the Scala Collections API, whereas the wrapped DOM nodes come from a distant past, when imperative programming and "mutability everywhere" ruled.

    In comparison to XPath against DOM trees, the ElemLike API may be more verbose, but it requires no setup and "result set handling" boilerplate.

  19. package indexed

    This package contains element representations that contain the "context" of the element.

    This package contains element representations that contain the "context" of the element. That is, the elements in this package are pairs of a root element and a path (to the actual element itself).

    An example of where such a representation can be useful is XML Schema. After all, to interpret an element definition in an XML schema, we need context of the element definition to determine the target namespace, or to determine whether the element definition is top level, etc.

    Below follows a simple example query, using the uniform query API:

    // Note the import of package indexed, and not of its members. That is indeed a best practice!
    import eu.cdevreeze.yaidom.indexed
    
    val indexedBookstoreElem = indexed.Elem(bookstoreElem)
    
    val scalaBookAuthors =
      for {
        bookElem <- indexedBookstoreElem \ EName("{http://bookstore/book}Book")
        if (bookElem \@ EName("ISBN")) == Some("978-0981531649")
        authorElem <- bookElem \\ EName("{http://bookstore/author}Author")
      } yield authorElem

    The query for Scala book authors would have been exactly the same if normal Elems had been used instead of indexed.Elems (replacing indexedBookstoreElem by bookstoreElem)!

  20. package parse

    Support for parsing XML into yaidom Documents and Elems.

    Support for parsing XML into yaidom Documents and Elems. This package offers the eu.cdevreeze.yaidom.parse.DocumentParser trait, as well as several implementations. Those implementations use JAXP (SAX, DOM or StAX), and most of them use the convert package to convert JAXP artifacts to yaidom Documents.

    For example:

    val docParser = DocumentParserUsingSax.newInstance()
    
    val doc: Document = docParser.parse(docUri)

    This example chose a SAX-based implementation, and used the default configuration of that document parser.

    Having several different fully configurable JAXP-based implementations shows that yaidom is pessimistic about the transparency of parsing and printing XML. It also shows that yaidom is optimistic about the available (heap) memory and processing power, because of the 2 separated steps of JAXP parsing/printing and (in-memory) convert conversions. Using JAXP means that escaping of characters is something that JAXP deals with, and that's definitely better than trying to do it yourself.

    One DocumentParser implementation does not use any convert conversion. That is DocumentParserUsingSax. It is likely the fastest of the DocumentParser implementations.

    The preferred DocumentParser for XML (not HTML) parsing is DocumentParserUsingDomLS, if memory usage is not an issue. This DocumentParser implementation is best integrated with DOM, and is highly configurable, although DOM LS configuration is somewhat involved.

    This package depends on the eu.cdevreeze.yaidom and eu.cdevreeze.yaidom.convert packages, and not the other way around.

  21. package print

    Support for "printing" yaidom Documents and Elems.

    Support for "printing" yaidom Documents and Elems. This package offers the eu.cdevreeze.yaidom.print.DocumentPrinter trait, as well as several implementations. Most of those implementations use the convert package to convert yaidom Documents to JAXP artifacts, and all use JAXP (DOM, SAX or StAX).

    For example:

    val docPrinter = DocumentPrinterUsingSax.newInstance()
    
    docPrinter.print(doc, "UTF-8", System.out)

    This example chose a SAX-based implementation, and used the default configuration of that document printer.

    Having several different fully configurable JAXP-based implementations shows that yaidom is pessimistic about the transparency of parsing and printing XML. It also shows that yaidom is optimistic about the available (heap) memory and processing power, because of the 2 separated steps of JAXP parsing/printing and (in-memory) convert conversions. Using JAXP means that escaping of characters is something that JAXP deals with, and that's definitely better than trying to do it yourself.

    One DocumentPrinter implementation does not use any convert conversion. That is DocumentPrinterUsingSax. It is likely the fastest of the DocumentPrinter implementations, as well as the one using the least memory.

    The preferred DocumentPrinter for XML (not HTML) printing is DocumentPrinterUsingDomLS, if memory usage is not an issue. This DocumentPrinter implementation is best integrated with DOM, and is highly configurable, although DOM LS configuration is somewhat involved.

    This package depends on the eu.cdevreeze.yaidom and eu.cdevreeze.yaidom.convert packages, and not the other way around.

  22. package resolved

    This package contains element representations that can be compared for (some notion of "value") equality, unlike normal yaidom nodes.

    This package contains element representations that can be compared for (some notion of "value") equality, unlike normal yaidom nodes. That notion of equality is simple to understand, but "naive". The user is of the API must take control over what is compared for equality.

    See eu.cdevreeze.yaidom.resolved.Node for why this package is named resolved.

    The most important difference with normal Elems is that qualified names do not occur, but only expanded (element and attribute) names. This reminds of James Clark notation for XML trees and expanded names, where qualified names are absent.

    Moreover, the only nodes in this package are element and text nodes.

    Below follows a simple example query, using the uniform query API:

    // Note the import of package resolved, and not of its members. That is indeed a best practice!
    import eu.cdevreeze.yaidom.resolved
    
    val resolvedBookstoreElem = resolved.Elem(bookstoreElem)
    
    val scalaBookAuthors =
      for {
        bookElem <- resolvedBookstoreElem \ EName("{http://bookstore/book}Book")
        if (bookElem \@ EName("ISBN")) == Some("978-0981531649")
        authorElem <- bookElem \\ EName("{http://bookstore/author}Author")
      } yield authorElem

    The query for Scala book authors would have been exactly the same if normal Elems had been used instead of resolved.Elems (replacing resolvedBookstoreElem by bookstoreElem)!

  23. package scalaxml

    Wrapper around class scala.xml.Elem, adapting it to the eu.cdevreeze.yaidom.ElemLike API.

    Wrapper around class scala.xml.Elem, adapting it to the eu.cdevreeze.yaidom.ElemLike API.

    This wrapper brings the uniform yaidom query API to Scala XML literals (and Scala XML Elems in general).

    For some namespace-related pitfalls and peculiarities, see eu.cdevreeze.yaidom.scalaxml.ScalaXmlElem.

  24. package subtypeaware

    Query traits that resemble ParentElemApi and ParentElemLike, but offering query support for object hierarchies.

    Query traits that resemble ParentElemApi and ParentElemLike, but offering query support for object hierarchies.

    For example, XML Schema can be modeled with an object hierarchy, starting with some XsdElem super-type which mixes in trait SubtypeAwareParentElemApi, among other query traits. The object hierarchy could contain sub-classes of XsdElem such as XsdRootElem, GlobalElementDeclaration, etc. Then the SubtypeAwareParentElemApi trait makes it easy to query for all or some global element declarations, etc.

    There is no magic in these traits: it is just ParentElemApi and ParentElemLike underneath. It is only the syntactic convenience that makes the difference.

  25. package utils

    Several utilities, such as custom ENameProviders and QNameProviders.

    Several utilities, such as custom ENameProviders and QNameProviders. They are utilities "on top of yaidom", so the rest of yaidom has no dependencies on this package, but this package does depend on the rest of yaidom.

Inherited from AnyRef

Inherited from Any

Ungrouped