Introduction
This lesson demonstrates how to traverse the Document Object Model (DOM) of an XML document. The DOM is a tree that represents the nodes of an XML document. The entire DOM is loaded into memory and consequently only works when the XML is reasonably small. For processing larger XML document, parsing using a SAX (Simple XML API) is advised.
Load XML into DOM
The most common way to load an XML document from a file or URL is to use the function xmlParse()
from the XML package. It returns a pointer to an internal C object. Another function that is sometimes used is xmlTreeParse()
which returns an R object. It is generally slower and less commonly used. It has the benefit of allowing symbolic traversal of the nodes, while the former relies on XPath and indexed node access.
–
library(XML)
xmlURL <- "http://artificium.us/lessons/06.r/l-6-324-parse-xml-dom/HockeyTeamRosters.xml"
xmlObj <- xmlParse(xmlURL)
To get a reference to the root node of the DOM, use xmlRoot()
.
Retrievals of Elements using XPath
The path below returns the last names of all players on all teams. The expression node[[1]]
access the first element in the list of returned elements.
xpathSApply()
is generally preferable over xpathApply()
for XPath expression as it attempts to simplify the return value as a vector when possible, rather than a list.
xpathSApply()
requires the root node of the XML document and an XPath expression. optionally, the function xmlValue
can be added as a parameter to retrieve the values of the matching nodes. The value of a node are the characters between the opening and closing tags, e.g., the value of the node (or element) <foo>bar</foo> is bar.
The code below retrieves the matching nodes.
nodeList <- xpathSApply(xmlObj, "//players/player/lastname")
aNode <- nodeList[[1]]
print(aNode)
## <lastname>Marchmont</lastname>
# get value of an element
aPlayerName <- xmlValue(aNode)
print(aPlayerName)
## [1] "Marchmont"
On the other hand, this code below, retrieves the values of the matching nodes. Note that it returns a vector rather than a list. If we had used xpathApply()
, we would have gotten a list object back.
nodeVals.v <- xpathSApply(xmlObj, "//players/player/lastname", xmlValue)
aNode <- nodeVals.v[1]
print(aNode)
## [1] "Marchmont"
nodeVals.l <- xpathApply(xmlObj, "//players/player/lastname", xmlValue)
aNode <- nodeVals.l[[1]]
print(aNode)
## [1] "Marchmont"
XPath on Elements
Let’s first inspect the returned list. Note that xpathSApply
returns elements. The <player> element has child elements of its own.
playerList <- xpathSApply(xmlObj, "//players/player")
aPlayer <- playerList[[1]]
print(aPlayer)
## <player num="63">
## <firstname>Brad</firstname>
## <lastname>Marchmont</lastname>
## <position>Forward</position>
## <salary>6125000</salary>
## <assistantcaptain/>
## <points>
## <goals>29</goals>
## <assists>40</assists>
## </points>
## </player>
Let’s access the <salary> element of a player using an XPath expression. Note how the XPath expression must start with ./ for the path to be relative to the <player> subtree. Also note that we passed xmlValue
to xpathSApply
to get the value of the <salary> element
salaryList <- xpathSApply(playerList[[1]], "./salary", xmlValue)
aNode <- salaryList[[1]]
print(aNode)
## [1] "6125000"
Processing Node Lists
An XPath expression returns a list object containing all XML elements (or nodes) that match the XPath expression. The elements in the list can be processed iteratively using an apply()
function or a loop.
In the code below, we retrieve all <player> elements for the team “Everglades” into the list object playerList. If the XPath expression could not find matching elements it returns an empty list of length 0. For each <player> element, we will extract the <lastname>, <position>, and <salary> elements using three different techniques to demonstrate. We will store the player’s last name and salary in a data frame. The data frame is initially empty with one row for each player. We could have set the initial number of rows to 0 rather than num.Players and R would allocate new rows as needed. Further note that all values in XML are text (character) and must be converted as necessary.
xmlObj <- xmlParse('HockeyTeamRosters.xml')
xpath <- "//team[@name='Pirates']/players/player"
playerList <- xpathSApply(xmlObj, xpath)
num.Players <- length(playerList)
# data frame for storing last name and salary
players.df <- data.frame(
name = character(num.Players),
position = character(num.Players),
salary = integer(num.Players)
)
# check if players were found
if (num.Players > 0)
{
# iterate through each player element
for (p in 1:num.Players)
{
# get the next player in the list
aPlayer <- playerList[[p]]
# get the name of the player using node access
pname <- xmlValue((aPlayer['lastname'])[[1]])
# get the position using positional access; this requires
# knowing the position of the element and being certain that
# it does not change -- a DTD is useful for that
pposition <- xmlValue(aPlayer[[3]])
# get the salary using XPath; be sure to start with ./
# this approach only works if we use xmlParse to get the
# DOM; it does not work with xmlTreeParse
salary.xpath <- "./salary"
salary.element <- xpathSApply(aPlayer, salary.xpath, xmlValue)
salary.value <- salary.element[[1]]
psalary <- as.integer(salary.value)
# store the name (by accessing a cell with row and column)
players.df[p,1] <- pname
# store name using a named column
players.df[p,'position'] <- pposition
# store the salary using a different technique to demonstrate
players.df$salary[p] <- psalary
}
}
Let’s break the code apart.
Missing Elements
Tutorial
References
No references.
Errata
None collected yet. Let us know.
LS0tDQp0aXRsZTogIlRyYXZlcnNlIGFuZCBQYXJzZSBYTUwgRE9NIGluIFIiDQpwYXJhbXM6DQogIGNhdGVnb3J5OiA2DQogIG51bWJlcjogMzI0DQogIHRpbWU6IDQ1DQogIGxldmVsOiBiZWdpbm5lcg0KICB0YWdzOiAicix4cGF0aCx4bWwsZG9tIg0KICBkZXNjcmlwdGlvbjogIkV4cGxhaW5zIGhvdyB0byB0cmF2ZXJzZSBhbiBYTUwgRG9jdW1lbnQgDQogICAgICAgICAgICAgICAgT2JqZWN0IE1vZGVsIChET00pDQogICAgICAgICAgICAgICAgdXNpbmcgYSBjb21iaW5hdGlvbiBvZiBYUGF0aCBhbmQgbm9kZSBhY2Nlc3MuIg0KZGF0ZTogIjxzbWFsbD5gciBTeXMuRGF0ZSgpYDwvc21hbGw+Ig0KYXV0aG9yOiAiPHNtYWxsPk1hcnRpbiBTY2hlZGxiYXVlcjwvc21hbGw+Ig0KZW1haWw6ICJtLnNjaGVkbGJhdWVyQG5ldS5lZHUiDQphZmZpbGl0YXRpb246ICJOb3J0aGVhc3Rlcm4gVW5pdmVyc2l0eSINCm91dHB1dDogDQogIGJvb2tkb3duOjpodG1sX2RvY3VtZW50MjoNCiAgICB0b2M6IHRydWUNCiAgICB0b2NfZmxvYXQ6IHRydWUNCiAgICBjb2xsYXBzZWQ6IGZhbHNlDQogICAgbnVtYmVyX3NlY3Rpb25zOiBmYWxzZQ0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICB0aGVtZTogc3BhY2VsYWINCiAgICBoaWdobGlnaHQ6IHRhbmdvDQotLS0NCg0KLS0tDQp0aXRsZTogIjxzbWFsbD5gciBwYXJhbXMkY2F0ZWdvcnlgLmByIHBhcmFtcyRudW1iZXJgPC9zbWFsbD48YnIvPjxzcGFuIHN0eWxlPSdjb2xvcjogIzJFNDA1MzsgZm9udC1zaXplOiAwLjllbSc+YHIgcm1hcmtkb3duOjptZXRhZGF0YSR0aXRsZWA8L3NwYW4+Ig0KLS0tDQoNCmBgYHtyIGNvZGU9eGZ1bjo6cmVhZF91dGY4KHBhc3RlMChoZXJlOjpoZXJlKCksJy9SL19pbnNlcnQyREIuUicpKSwgaW5jbHVkZSA9IEZBTFNFfQ0KYGBgDQoNCiMjIEludHJvZHVjdGlvbg0KDQpUaGlzIGxlc3NvbiBkZW1vbnN0cmF0ZXMgaG93IHRvIHRyYXZlcnNlIHRoZSBEb2N1bWVudCBPYmplY3QgTW9kZWwgKERPTSkgb2YgYW4gWE1MIGRvY3VtZW50LiBUaGUgRE9NIGlzIGEgdHJlZSB0aGF0IHJlcHJlc2VudHMgdGhlIG5vZGVzIG9mIGFuIFhNTCBkb2N1bWVudC4gVGhlIGVudGlyZSBET00gaXMgbG9hZGVkIGludG8gbWVtb3J5IGFuZCBjb25zZXF1ZW50bHkgb25seSB3b3JrcyB3aGVuIHRoZSBYTUwgaXMgcmVhc29uYWJseSBzbWFsbC4gRm9yIHByb2Nlc3NpbmcgbGFyZ2VyIFhNTCBkb2N1bWVudCwgcGFyc2luZyB1c2luZyBhIFNBWCAoU2ltcGxlIFhNTCBBUEkpIGlzIGFkdmlzZWQuDQoNCiMjIExvYWQgWE1MIGludG8gRE9NDQoNClRoZSBtb3N0IGNvbW1vbiB3YXkgdG8gbG9hZCBhbiBYTUwgZG9jdW1lbnQgZnJvbSBhIGZpbGUgb3IgVVJMIGlzIHRvIHVzZSB0aGUgZnVuY3Rpb24gPGNvZGU+eG1sUGFyc2UoKTwvY29kZT4gZnJvbSB0aGUgKipYTUwqKiBwYWNrYWdlLiBJdCByZXR1cm5zIGEgcG9pbnRlciB0byBhbiBpbnRlcm5hbCBDIG9iamVjdC4gQW5vdGhlciBmdW5jdGlvbiB0aGF0IGlzIHNvbWV0aW1lcyB1c2VkIGlzIDxjb2RlPnhtbFRyZWVQYXJzZSgpPC9jb2RlPiB3aGljaCByZXR1cm5zIGFuIFIgb2JqZWN0LiBJdCBpcyBnZW5lcmFsbHkgc2xvd2VyIGFuZCBsZXNzIGNvbW1vbmx5IHVzZWQuIEl0IGhhcyB0aGUgYmVuZWZpdCBvZiBhbGxvd2luZyBzeW1ib2xpYyB0cmF2ZXJzYWwgb2YgdGhlIG5vZGVzLCB3aGlsZSB0aGUgZm9ybWVyIHJlbGllcyBvbiBYUGF0aCBhbmQgaW5kZXhlZCBub2RlIGFjY2Vzcy4NCg0KLS0NCg0KYGBge3J9DQpsaWJyYXJ5KFhNTCkNCg0KeG1sVVJMIDwtICJodHRwOi8vYXJ0aWZpY2l1bS51cy9sZXNzb25zLzA2LnIvbC02LTMyNC1wYXJzZS14bWwtZG9tL0hvY2tleVRlYW1Sb3N0ZXJzLnhtbCINCnhtbE9iaiA8LSB4bWxQYXJzZSh4bWxVUkwpDQpgYGANCg0KVG8gZ2V0IGEgcmVmZXJlbmNlIHRvIHRoZSByb290IG5vZGUgb2YgdGhlIERPTSwgdXNlIDxjb2RlPnhtbFJvb3QoKTwvY29kZT4uDQoNCmBgYHtyfQ0Kcm9vdCA8LSB4bWxSb290KHhtbE9iaikNCmBgYA0KDQojIyBSZXRyaWV2YWxzIG9mIEVsZW1lbnRzIHVzaW5nIFhQYXRoDQoNClRoZSBwYXRoIGJlbG93IHJldHVybnMgdGhlIGxhc3QgbmFtZXMgb2YgYWxsIHBsYXllcnMgb24gYWxsIHRlYW1zLiBUaGUgZXhwcmVzc2lvbiA8Y29kZT5ub2RlW1sxXV08L2NvZGU+IGFjY2VzcyB0aGUgZmlyc3QgZWxlbWVudCBpbiB0aGUgbGlzdCBvZiByZXR1cm5lZCBlbGVtZW50cy4NCg0KPGNvZGU+eHBhdGhTQXBwbHkoKTwvY29kZT4gaXMgZ2VuZXJhbGx5IHByZWZlcmFibGUgb3ZlciA8Y29kZT54cGF0aEFwcGx5KCk8L2NvZGU+IGZvciBYUGF0aCBleHByZXNzaW9uIGFzIGl0IGF0dGVtcHRzIHRvIHNpbXBsaWZ5IHRoZSByZXR1cm4gdmFsdWUgYXMgYSB2ZWN0b3Igd2hlbiBwb3NzaWJsZSwgcmF0aGVyIHRoYW4gYSBsaXN0Lg0KDQo8Y29kZT54cGF0aFNBcHBseSgpPC9jb2RlPiByZXF1aXJlcyB0aGUgcm9vdCBub2RlIG9mIHRoZSBYTUwgZG9jdW1lbnQgYW5kIGFuIFhQYXRoIGV4cHJlc3Npb24uIG9wdGlvbmFsbHksIHRoZSBmdW5jdGlvbiA8Y29kZT54bWxWYWx1ZTwvY29kZT4gY2FuIGJlIGFkZGVkIGFzIGEgcGFyYW1ldGVyIHRvIHJldHJpZXZlIHRoZSB2YWx1ZXMgb2YgdGhlIG1hdGNoaW5nIG5vZGVzLiBUaGUgdmFsdWUgb2YgYSBub2RlIGFyZSB0aGUgY2hhcmFjdGVycyBiZXR3ZWVuIHRoZSBvcGVuaW5nIGFuZCBjbG9zaW5nIHRhZ3MsICplLmcuKiwgdGhlIHZhbHVlIG9mIHRoZSBub2RlIChvciBlbGVtZW50KSAqXDxmb29cPmJhclw8L2Zvb1w+KiBpcyAqYmFyKi4NCg0KVGhlIGNvZGUgYmVsb3cgcmV0cmlldmVzIHRoZSBtYXRjaGluZyBub2Rlcy4NCg0KYGBge3J9DQpub2RlTGlzdCA8LSB4cGF0aFNBcHBseSh4bWxPYmosICIvL3BsYXllcnMvcGxheWVyL2xhc3RuYW1lIikNCg0KYU5vZGUgPC0gbm9kZUxpc3RbWzFdXQ0KDQpwcmludChhTm9kZSkNCg0KIyBnZXQgdmFsdWUgb2YgYW4gZWxlbWVudA0KDQphUGxheWVyTmFtZSA8LSB4bWxWYWx1ZShhTm9kZSkNCnByaW50KGFQbGF5ZXJOYW1lKQ0KDQpgYGANCg0KT24gdGhlIG90aGVyIGhhbmQsIHRoaXMgY29kZSBiZWxvdywgcmV0cmlldmVzIHRoZSB2YWx1ZXMgb2YgdGhlIG1hdGNoaW5nIG5vZGVzLiBOb3RlIHRoYXQgaXQgcmV0dXJucyBhIHZlY3RvciByYXRoZXIgdGhhbiBhIGxpc3QuIElmIHdlIGhhZCB1c2VkIDxjb2RlPnhwYXRoQXBwbHkoKTwvY29kZT4sIHdlIHdvdWxkIGhhdmUgZ290dGVuIGEgbGlzdCBvYmplY3QgYmFjay4NCg0KYGBge3J9DQpub2RlVmFscy52IDwtIHhwYXRoU0FwcGx5KHhtbE9iaiwgIi8vcGxheWVycy9wbGF5ZXIvbGFzdG5hbWUiLCB4bWxWYWx1ZSkNCg0KYU5vZGUgPC0gbm9kZVZhbHMudlsxXQ0KDQpwcmludChhTm9kZSkNCg0Kbm9kZVZhbHMubCA8LSB4cGF0aEFwcGx5KHhtbE9iaiwgIi8vcGxheWVycy9wbGF5ZXIvbGFzdG5hbWUiLCB4bWxWYWx1ZSkNCg0KYU5vZGUgPC0gbm9kZVZhbHMubFtbMV1dDQoNCnByaW50KGFOb2RlKQ0KYGBgDQoNCiMjIFhQYXRoIG9uIEVsZW1lbnRzDQoNCkxldCdzIGZpcnN0IGluc3BlY3QgdGhlIHJldHVybmVkIGxpc3QuIE5vdGUgdGhhdCA8Y29kZT54cGF0aFNBcHBseTwvY29kZT4gcmV0dXJucyBlbGVtZW50cy4gVGhlIFw8cGxheWVyXD4gZWxlbWVudCBoYXMgY2hpbGQgZWxlbWVudHMgb2YgaXRzIG93bi4NCg0KYGBge3J9DQpwbGF5ZXJMaXN0IDwtIHhwYXRoU0FwcGx5KHhtbE9iaiwgIi8vcGxheWVycy9wbGF5ZXIiKQ0KDQphUGxheWVyIDwtIHBsYXllckxpc3RbWzFdXQ0KDQpwcmludChhUGxheWVyKQ0KYGBgDQoNCkxldCdzIGFjY2VzcyB0aGUgKlw8c2FsYXJ5XD4qIGVsZW1lbnQgb2YgYSBwbGF5ZXIgdXNpbmcgYW4gWFBhdGggZXhwcmVzc2lvbi4gTm90ZSBob3cgdGhlIFhQYXRoIGV4cHJlc3Npb24gbXVzdCBzdGFydCB3aXRoICouLyogZm9yIHRoZSBwYXRoIHRvIGJlIHJlbGF0aXZlIHRvIHRoZSAqXDxwbGF5ZXJcPiogc3VidHJlZS4gQWxzbyBub3RlIHRoYXQgd2UgcGFzc2VkIDxjb2RlPnhtbFZhbHVlPC9jb2RlPiB0byA8Y29kZT54cGF0aFNBcHBseTwvY29kZT4gdG8gZ2V0IHRoZSB2YWx1ZSBvZiB0aGUgKlw8c2FsYXJ5XD4qIGVsZW1lbnQNCg0KYGBge3J9DQpzYWxhcnlMaXN0IDwtIHhwYXRoU0FwcGx5KHBsYXllckxpc3RbWzFdXSwgIi4vc2FsYXJ5IiwgeG1sVmFsdWUpDQoNCmFOb2RlIDwtIHNhbGFyeUxpc3RbWzFdXQ0KDQpwcmludChhTm9kZSkNCmBgYA0KDQojIyBQcm9jZXNzaW5nIE5vZGUgTGlzdHMNCg0KQW4gWFBhdGggZXhwcmVzc2lvbiByZXR1cm5zIGEgbGlzdCBvYmplY3QgY29udGFpbmluZyBhbGwgWE1MIGVsZW1lbnRzIChvciBub2RlcykgdGhhdCBtYXRjaCB0aGUgWFBhdGggZXhwcmVzc2lvbi4gVGhlIGVsZW1lbnRzIGluIHRoZSBsaXN0IGNhbiBiZSBwcm9jZXNzZWQgaXRlcmF0aXZlbHkgdXNpbmcgYW4gPGNvZGU+YXBwbHkoKTwvY29kZT4gZnVuY3Rpb24gb3IgYSBsb29wLg0KDQpJbiB0aGUgY29kZSBiZWxvdywgd2UgcmV0cmlldmUgYWxsICpcPHBsYXllclw+KiBlbGVtZW50cyBmb3IgdGhlIHRlYW0gIkV2ZXJnbGFkZXMiIGludG8gdGhlIGxpc3Qgb2JqZWN0ICpwbGF5ZXJMaXN0Ki4gSWYgdGhlIFhQYXRoIGV4cHJlc3Npb24gY291bGQgbm90IGZpbmQgbWF0Y2hpbmcgZWxlbWVudHMgaXQgcmV0dXJucyBhbiBlbXB0eSBsaXN0IG9mIGxlbmd0aCAwLiBGb3IgZWFjaCAqXDxwbGF5ZXJcPiogZWxlbWVudCwgd2Ugd2lsbCBleHRyYWN0IHRoZSAqXDxsYXN0bmFtZVw+KiwgKlw8cG9zaXRpb25cPiosIGFuZCAqXDxzYWxhcnlcPiogZWxlbWVudHMgdXNpbmcgdGhyZWUgZGlmZmVyZW50IHRlY2huaXF1ZXMgdG8gZGVtb25zdHJhdGUuIFdlIHdpbGwgc3RvcmUgdGhlIHBsYXllcidzIGxhc3QgbmFtZSBhbmQgc2FsYXJ5IGluIGEgZGF0YSBmcmFtZS4gVGhlIGRhdGEgZnJhbWUgaXMgaW5pdGlhbGx5IGVtcHR5IHdpdGggb25lIHJvdyBmb3IgZWFjaCBwbGF5ZXIuIFdlIGNvdWxkIGhhdmUgc2V0IHRoZSBpbml0aWFsIG51bWJlciBvZiByb3dzIHRvIDAgcmF0aGVyIHRoYW4gKm51bS5QbGF5ZXJzKiBhbmQgUiB3b3VsZCBhbGxvY2F0ZSBuZXcgcm93cyBhcyBuZWVkZWQuIEZ1cnRoZXIgbm90ZSB0aGF0IGFsbCB2YWx1ZXMgaW4gWE1MIGFyZSB0ZXh0IChjaGFyYWN0ZXIpIGFuZCBtdXN0IGJlIGNvbnZlcnRlZCBhcyBuZWNlc3NhcnkuDQoNCmBgYHtyfQ0KeG1sT2JqIDwtIHhtbFBhcnNlKCdIb2NrZXlUZWFtUm9zdGVycy54bWwnKQ0KDQp4cGF0aCA8LSAiLy90ZWFtW0BuYW1lPSdQaXJhdGVzJ10vcGxheWVycy9wbGF5ZXIiDQpwbGF5ZXJMaXN0IDwtIHhwYXRoU0FwcGx5KHhtbE9iaiwgeHBhdGgpDQoNCm51bS5QbGF5ZXJzIDwtIGxlbmd0aChwbGF5ZXJMaXN0KQ0KDQojIGRhdGEgZnJhbWUgZm9yIHN0b3JpbmcgbGFzdCBuYW1lIGFuZCBzYWxhcnkNCnBsYXllcnMuZGYgPC0gZGF0YS5mcmFtZSgNCiAgbmFtZSA9IGNoYXJhY3RlcihudW0uUGxheWVycyksDQogIHBvc2l0aW9uID0gY2hhcmFjdGVyKG51bS5QbGF5ZXJzKSwNCiAgc2FsYXJ5ID0gaW50ZWdlcihudW0uUGxheWVycykNCikNCg0KIyBjaGVjayBpZiBwbGF5ZXJzIHdlcmUgZm91bmQNCmlmIChudW0uUGxheWVycyA+IDApDQp7DQogICMgaXRlcmF0ZSB0aHJvdWdoIGVhY2ggcGxheWVyIGVsZW1lbnQNCiAgZm9yIChwIGluIDE6bnVtLlBsYXllcnMpDQogIHsNCiAgICAjIGdldCB0aGUgbmV4dCBwbGF5ZXIgaW4gdGhlIGxpc3QNCiAgICBhUGxheWVyIDwtIHBsYXllckxpc3RbW3BdXQ0KICAgIA0KICAgICMgZ2V0IHRoZSBuYW1lIG9mIHRoZSBwbGF5ZXIgdXNpbmcgbm9kZSBhY2Nlc3MNCiAgICBwbmFtZSA8LSB4bWxWYWx1ZSgoYVBsYXllclsnbGFzdG5hbWUnXSlbWzFdXSkNCiAgICANCiAgICAjIGdldCB0aGUgcG9zaXRpb24gdXNpbmcgcG9zaXRpb25hbCBhY2Nlc3M7IHRoaXMgcmVxdWlyZXMNCiAgICAjIGtub3dpbmcgdGhlIHBvc2l0aW9uIG9mIHRoZSBlbGVtZW50IGFuZCBiZWluZyBjZXJ0YWluIHRoYXQNCiAgICAjIGl0IGRvZXMgbm90IGNoYW5nZSAtLSBhIERURCBpcyB1c2VmdWwgZm9yIHRoYXQNCiAgICBwcG9zaXRpb24gPC0geG1sVmFsdWUoYVBsYXllcltbM11dKQ0KICAgIA0KICAgICMgZ2V0IHRoZSBzYWxhcnkgdXNpbmcgWFBhdGg7IGJlIHN1cmUgdG8gc3RhcnQgd2l0aCAuLw0KICAgICMgdGhpcyBhcHByb2FjaCBvbmx5IHdvcmtzIGlmIHdlIHVzZSB4bWxQYXJzZSB0byBnZXQgdGhlDQogICAgIyBET007IGl0IGRvZXMgbm90IHdvcmsgd2l0aCB4bWxUcmVlUGFyc2UNCiAgICBzYWxhcnkueHBhdGggPC0gIi4vc2FsYXJ5Ig0KICAgIHNhbGFyeS5lbGVtZW50IDwtIHhwYXRoU0FwcGx5KGFQbGF5ZXIsIHNhbGFyeS54cGF0aCwgeG1sVmFsdWUpDQogICAgc2FsYXJ5LnZhbHVlIDwtIHNhbGFyeS5lbGVtZW50W1sxXV0NCiAgICBwc2FsYXJ5IDwtIGFzLmludGVnZXIoc2FsYXJ5LnZhbHVlKQ0KICAgIA0KICAgICMgc3RvcmUgdGhlIG5hbWUgKGJ5IGFjY2Vzc2luZyBhIGNlbGwgd2l0aCByb3cgYW5kIGNvbHVtbikNCiAgICBwbGF5ZXJzLmRmW3AsMV0gPC0gcG5hbWUNCiAgICAjIHN0b3JlIG5hbWUgdXNpbmcgYSBuYW1lZCBjb2x1bW4NCiAgICBwbGF5ZXJzLmRmW3AsJ3Bvc2l0aW9uJ10gPC0gcHBvc2l0aW9uDQogICAgIyBzdG9yZSB0aGUgc2FsYXJ5IHVzaW5nIGEgZGlmZmVyZW50IHRlY2huaXF1ZSB0byBkZW1vbnN0cmF0ZQ0KICAgIHBsYXllcnMuZGYkc2FsYXJ5W3BdIDwtIHBzYWxhcnkNCiAgfQ0KfQ0KDQpgYGANCg0KTGV0J3MgYnJlYWsgdGhlIGNvZGUgYXBhcnQuDQoNCiMjIE1pc3NpbmcgRWxlbWVudHMNCg0KIyMgUGxheSBHcm91bmQNCg0KYGBgIHhtbA0KPGZvbz4NCjwvZm9vPg0KYGBgDQoNCiMjIFR1dG9yaWFsDQoNCmBgYHs9aHRtbH0NCjxpZnJhbWUgc3JjPSIiIHdpZHRoPSI0ODAiIGhlaWdodD0iMjcwIiBmcmFtZWJvcmRlcj0iMCIgYWxsb3c9ImF1dG9wbGF5OyBmdWxsc2NyZWVuOyBwaWN0dXJlLWluLXBpY3R1cmUiIGFsbG93ZnVsbHNjcmVlbiBkYXRhLWV4dGVybmFsPSIxIj48L2lmcmFtZT4NCmBgYA0KDQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0KIyMgRmlsZXMgJiBSZXNvdXJjZXMNCg0KYGBge3IgemlwRmlsZXMsIGVjaG89RkFMU0V9DQp6aXBOYW1lID0gc3ByaW50ZigiTGVzc29uRmlsZXMtJXMtJXMuemlwIiwgDQogICAgICAgICAgICAgICAgIHBhcmFtcyRjYXRlZ29yeSwNCiAgICAgICAgICAgICAgICAgcGFyYW1zJG51bWJlcikNCg0KdGV4dEFMaW5rID0gcGFzdGUwKCJBbGwgRmlsZXMgZm9yIExlc3NvbiAiLCANCiAgICAgICAgICAgICAgIHBhcmFtcyRjYXRlZ29yeSwiLiIscGFyYW1zJG51bWJlcikNCg0KIyBkb3dubG9hZEZpbGVzTGluaygpIGlzIGluY2x1ZGVkIGZyb20gX2luc2VydDJEQi5SDQprbml0cjo6cmF3X2h0bWwoZG93bmxvYWRGaWxlc0xpbmsoIi4iLCB6aXBOYW1lLCB0ZXh0QUxpbmspKQ0KYGBgDQoNCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KDQojIyBSZWZlcmVuY2VzDQoNCk5vIHJlZmVyZW5jZXMuDQoNCiMjIEVycmF0YQ0KDQpOb25lIGNvbGxlY3RlZCB5ZXQuIExldCB1cyBrbm93Lg0KDQpgYGB7PWh0bWx9DQo8c2NyaXB0IHNyYz0iaHR0cHM6Ly9mb3JtLmpvdGZvcm0uY29tL3N0YXRpYy9mZWVkYmFjazIuanMiIHR5cGU9InRleHQvamF2YXNjcmlwdCI+DQogIG5ldyBKb3Rmb3JtRmVlZGJhY2soew0KICAgIGZvcm1JZDogIjIxMjE4NzA3Mjc4NDE1NyIsDQogICAgYnV0dG9uVGV4dDogIkZlZWRiYWNrIiwNCiAgICBiYXNlOiAiaHR0cHM6Ly9mb3JtLmpvdGZvcm0uY29tLyIsDQogICAgYmFja2dyb3VuZDogIiNGNTkyMDIiLA0KICAgIGZvbnRDb2xvcjogIiNGRkZGRkYiLA0KICAgIGJ1dHRvblNpZGU6ICJsZWZ0IiwNCiAgICBidXR0b25BbGlnbjogImNlbnRlciIsDQogICAgdHlwZTogZmFsc2UsDQogICAgd2lkdGg6IDcwMCwNCiAgICBoZWlnaHQ6IDUwMCwNCiAgICBpc0NhcmRGb3JtOiBmYWxzZQ0KICB9KTsNCjwvc2NyaXB0Pg0KYGBgDQpgYGB7ciBjb2RlPXhmdW46OnJlYWRfdXRmOChwYXN0ZTAoaGVyZTo6aGVyZSgpLCcvUi9fZGVwbG95S25pdC5SJykpLCBpbmNsdWRlID0gRkFMU0V9DQpgYGANCg==