Converter from A (which can be anything) to eu.cdevreeze.yaidom.Document.
Converter from A (which can be anything) to eu.cdevreeze.yaidom.Document.
the type of the value to convert
Converter from A (which can be anything) to eu.cdevreeze.yaidom.Elem.
Converter from A (which can be anything) to eu.cdevreeze.yaidom.Elem.
the type of the value to convert
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.
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.
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:
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.
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.
The element type
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).
the result type of the conversion
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.
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.
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.
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.
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:
ENames) to attribute valuesUsing 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 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.ElemApi
This query API leverages the Scala Collections API. Query results can be manipulated using the Collections API, and the
query API implementation (in ) uses the Collections API internally.ElemLike
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 . Then we can perform the
following queries:bookstoreElem
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 query methods, we could instead have written:ParentElemApi
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 and ElemApi
APIs. As a consequence, the exact same queries work for other DOM-like element implementations as well. That is, HasText
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 bookstoreElem trait indeed
offers a uniform element query API.ElemApi
From a formal point of view, offers little of interest. After all, given super-trait ElemApi, as well
as methods ParentElemApi and resolvedName, the other methods are trivial to implement.resolvedAttributes
For example, the semantics of method (taking an filterChildElems) is trivially defined as follows:EName
elem.filterChildElems(ename) == elem.filterChildElems(e => e.resolvedName == ename)Other methods taking a predicate also have a counterpart in ParentElemApi taking just an ElemApi, and
the latter ones are trivially defined in terms of the former ones, just like EName (taking an filterChildElems) above.
After all, parent trait EName is the foundation of the yaidom query API, yet sub-trait ParentElemApi makes it
much more useful in practice, by adding some knowledge about element names and attributes.
ElemApi
The captured element subtype
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.
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).
the result type of the conversion
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.
The captured element subtype
An entity reference.
An entity reference. For example:
&hello;
We obtain this entity reference as follows:
EntityRef("hello")
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.
The captured element subtype
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.
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.
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.
The captured element subtype
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.
The captured element subtype
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.
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.
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 ) uses the Collections API internally.ParentElemLike
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 , and the method to retrieve all descendant-or-self elements is called findAllElems.
The corresponding "filtering" methods are called findAllElemsOrSelf and filterElems, respectively. Knowing this,
it is easy to guess the other API method names.filterElemsOrSelf
Let's start with a yaidom DOM wrapper, named , of type eu.cdevreeze.yaidom.dom.DomElem, and query for the
"offending" descendant elements:rootElem
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 already has a method ElemBuilder.)allDeclarationsAreAtTopLevel
Finally, let's use a of type eu.cdevreeze.yaidom.indexed.Elem, which is immutable, but knows its ancestry:rootElem
rootElem filterElems (elem => !elem.namespaces.isEmpty)This is exactly the same code as for , because namespace declarations happen to be retrieved in the same way.ElemBuilder
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 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
ParentElemApi query API, which extends the ElemApi query API.ParentElemApi
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 trait can be understood in a precise mathematical sense, as shown below.ParentElemApi
The most fundamental method of this trait is . The semantics of the other methods can be defined
directly or indirectly in terms of this method.findAllChildElems
The basic operations definable in terms of that method are (alias for \), filterChildElems (alias for \\)
and filterElemsOrSelf (alias for \\!). Their semantics must be as if they had been defined as follows:findTopmostElemsOrSelf
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))
The captured element subtype
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.
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.
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:
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.
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.
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.
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.
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
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.
The captured element subtype
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).
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:
ParentElemLike)ElemLike)ElemLike)Using this minimal knowledge alone, that trait not only offers the methods of its parent trait, but also:
ParentElemLike query methods, but returning Path objects instead of elementsIn other words, the trait is quite a rich query API, considering the minimal knowledge it needs to
have about elements.PathAwareElemApi
This query API leverages the Scala Collections API. Query results can be manipulated using the Collections API, and the
query API implementation (in ) uses the Collections API internally.PathAwareElemLike
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 . Then we can perform the
following query, using only the bookstoreElem query API:ElemApi
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 query API,
could be coded as follows:PathAwareElemApi
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:
getElemOrSelfByPath (or findElemOrSelfByPath) on the correct elementPathAwareElemApi 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 trait are a nice tool in the yaidom querying toolbox.PathAwareElemApi
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 trait can be understood more formally, as shown below.PathAwareElemApi
The most fundamental methods of this trait are and findAllChildElemsWithPathEntries.
The semantics of the other methods can be defined directly or indirectly in terms of method findChildElemByPathEntry.findAllChildElemsWithPathEntries
The following must hold (for , which has structural equality defined):resolved.Elem
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 are findAllChildElemsWithPathEntries,
filterChildElemPaths and filterElemOrSelfPaths, analogous to trait findTopmostElemOrSelfPaths. Their semantics must be
as if they had been defined as follows:ParentElemApi
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 , the following properties hold:ParentElemApi
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 instance resolved.Elem, we have:elem
(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 is defined recursively, using method findElemOrSelfByPath.findChildElemByPathEntry
No proofs are provided. Note that the similarities with trait are striking.
ParentElemLike
The captured element subtype
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.
The captured element subtype
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.
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.
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.
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.
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.
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")
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.
Methods relativize and resolve obey the following equality:
scope1.resolve(scope1.relativize(scope2)) == scope2
Below follows the proof. We distinguish among the following cases:
p has the same mappings in scope1 and scope2p has different mappings in scope1 and scope2p only belongs to scope1p only belongs to scope2p belongs to neither scopePrefix 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 emptyMethods 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:
p has the same mappings in scope and declarations (so no undeclaration)p has different mappings in scope and declarations (but no undeclaration)p belongs to scope and is undeclared in declarationsp only belongs to scope, and does not occur in declarationsp only occurs in declarations, without being undeclared, and does not occur in scopep only occurs in declarations, in an undeclaration, and does not occur in scopep neither occurs in scope nor in declarationsPrefix 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.
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.
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 . Then we can combine
author first and last names as follows:bookstoreElem
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:
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.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) } }
The node supertype of the element subtype
The captured element subtype
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.
The node supertype of the element subtype
The captured element subtype
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:
Obviously methods , children and withChildren must be consistent with
methods such as childNodeIndexesByPathEntries and findAllChildElems.findAllChildElemsWithPathEntries
Using this minimal knowledge alone, trait not only offers the methods of its parent trait, but also:UpdatableElemLike
In other words, the trait is quite a rich query API, considering the minimal knowledge it needs to
have about elements.UpdatableElemApi
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 ) uses the Collections API internally.UpdatableElemLike
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 . Then we can add a book
as follows, where we "forget" the 2nd author for the moment:bookstoreElem
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 and book had to be repeated in the Scala XML literal
for the added book, because otherwise the auth method would throw an exception (since convertToElem instances
cannot be created unless all element and attribute QNames can be resolved as ENames).Elem
The resulting bookstore seems ok, but if we print , the result does not look pretty.
This can be fixed if the last assignment is replaced by:convertElem(bookstoreElem)
bookstoreElem = bookstoreElem.plusChild(fpBookElem).prettify(2)knowing that an indentation of 2 spaces has been used throughout the original XML. Method is expensive, so it
is best not to invoke it within a tight loop. As an alternative, formatting can be left to the prettify, of
course.DocumentPrinter
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 is overloaded,
and the other variant can insert a child at a given 0-based position. Other "children update" methods are plusChild,
minusChild and withPatchedChildren.withUpdatedChildren
Let's now turn to functional update methods that take instances or collections thereof. In the example above
the second author of the added book is missing. Let's fix that:Path
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 namespace declared. Without the above-mentioned line of code, a namespace
undeclaration for prefix auth would have occurred in the resulting XML, thus leading to an invalid XML 1.0 element tree.book
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 ,
or even only using transformation methods in trait UpdatableElemApi (thus not using paths).TransformableElemApi
The example code above is enough to get started using the 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.
UpdatableElemApi
The node supertype of the element subtype
The captured element subtype
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.
The node supertype of the element subtype
The captured element subtype
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")
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")))
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.
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.
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)!
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:
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.
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)!
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.
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.
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)!
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.
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.
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.
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:
book:Title, and unprefixed names, such asEdition{http://bookstore/book}Title(in James Clark notation), and not having a namespace, such asEditionThey 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:
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 lastbook:Bookelement 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:This leads to the following namespaces in scope at the root element:
which is equal to:
We find no other namespace declarations in the last
book:Bookelement or its ancestor(s), so the computed scope is also the scope of the lastbook:Bookelement.Then
QName("book:Book")is resolved as follows:which is equal to:
This namespace support in yaidom has mathematical rigor. The immutable classes
QName,EName,DeclarationsandScopehave precise definitions, reflected in their implementations, and they obey some interesting properties. For example, if we correctly define Scope operationrelativize(along withresolve), we get: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:
This path could be written as path builder as follows:
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 prefixesbookandauthin 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, namedbookstoreElem. Then we can query for all books, that is, all descendant-or-self elements with resolved (or expanded) nameEName("{http://bookstore/book}Book"), as follows:The result would be an immutable IndexedSeq of
ScalaXmlEleminstances, 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:
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:
or:
bookstoreElem.filterChildElems(EName("{http://bookstore/book}Book"))or, knowing that all child elements are books:
We could find all authors of the Scala book as follows:
or:
We could even use operator notation, as follows:
or:
where
\\stands forfilterElemsOrSelf.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:
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 ofDomEleminstances instead ofScalaXmlEleminstances, 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 asfilterChildElems,filterElemsandfilterElemsOrSelf. 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
resolvedNameandresolvedAttributes, 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
ElemLikeandParentElemLikehave about elements. Therefore the query code can be used unchanged for different element implementations.The
ElemLiketrait 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 traitParentElemLike), 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):The
PathAwareElemLiketrait 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
ParentElemLikeand its sub-traitElemLikeare 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 aNodesub-class itself.The eu.cdevreeze.yaidom.Elem class has the following characteristics:
HasParentScope, but noDeclarationsparseandprintofferDocumentParserandDocumentPrinterclasses for parsing/serializing these defaultElem(andDocument) instancesCreating such
Elemtrees by hand is a bit cumbersome, partly because scopes have to be passed to eachElemin 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 asElemBuilder, invoke methodElemBuilder.build(parentScope)to turn it into anElem.Like their super-classes
NodeandNodeBuilder, classesElemandElemBuilderhave very much in common. Both are immutable, easy to compose (ElemBuilderinstances even more so), equality is reference equality, etc. The most important differences are as follows:Scope, anElemBuildercontains aDeclarationsElemBuildereasier to compose than anElem, because no Scope needs to be passed around throughout the treeElemBuilderuses a minimal query API, mixing in only traitsParentElemLikeandTransformableElemLikeElemBuilderneither keeps nor knows about Scopes, so does not know about resolved element/attribute namesThe Effective Java book element in the XML example above could have been written as
ElemBuilder(without the inter-element whitespace) as follows:This
ElemBuilder(say,eb) lacks namespace declarations for prefixesbookandauth. So, the following returnsfalse:while the following returns
true:Indeed,
returns the element tree as
Elem.Note that the distinction between
ElemBuilderandElem"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:
Elemby hand. See above.ElemLikequery API, but knows its ancestry, despite being immutable! This element implementation is handy for querying XML schemas, for example, because in schemas the ancestry of queried elements typically matters.indexed.Elem, but also stores the document URI.ElemLikequery API.ElemLikeandHasParentquery APIs.This illustrates that especially trait
ElemLikeis a uniform query API in yaidom.Packages and dependencies
Yaidom has the following packages, and layering between packages:
convertpackage depends on the yaidom root package.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:
to:
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:
using an implicit ENameProvider, whose members are in scope. Still, for querying the first alternative using
withENameis 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.