Easy JavaScript Parsing of Domino's ReadViewEntries XML
Remember the other day, while talking about JSON, I mentioned that parsing XML in JavaScript cross browser was a tedious nightmare. Well, I've found a simple way to get the information you want from Domino's ?ReadViewEntries XML using JavaScript.
Take a URL like this:
names.nsf/($VIMPeople)?ReadViewEntries&startkey=jake&count=10
Here's a cut-down snippet of the XML (restricted to 2 columns for the sake of brevity):
<?xml version="1.0" encoding="UTF-8"?> <viewentries toplevelentries="290"> <viewentry position="113" unid="061...926" noteid="956"> <entrydata columnnumber="0" name="$1"> <text>Jake Howlett/ROCKALL</text> </entrydata> <entrydata columnnumber="1" name="$0"> <text>Jake</text> </entrydata> </viewentry> <viewentry position="114" unid="791...7DC" noteid="9AE"> <entrydata columnnumber="0" name="$1"> <text>James Bond/ROCKALL</text> </entrydata>
Let's say we want to go through each of the ten rows and find the Notes username for each person. Here's the JavaScript to do that:
//assume root is the XML document object returned by an Ajax method! var entries = root.getElementsByTagName("viewentry"); for (var i=0; i<entries.length; i++) { columns = entries[i].getElementsByTagName("text"); alert(columns.item(0).firstChild.nodeValue); }
Hopefully the code explains itself. It's worth noting though that this looks for the first text column. It wouldn't find the first date-time column as this is not returned as a <text> element but as a <datetime> element. If you're working with number columns or date columns you'd have to alter the getElementsByTagName() part. Note that, to get the third column, you'd go on to look for .item(2).
What might not make sense from this code is the .firstChild.nodeValue part that follows the getElementsByTagName bit. Surely the <text> element we're working on doesn't have a child and in turn this doesn't have a node to get a value of. Au contraire. This is where it all gets really confusing.
In the DOM everything is a node. Even when you've got a hold of the <text> node you're looking for the text within it is still another node. The problem herein is the way different browsers deals with this. Mozilla infamously deals with all whitespace as nodes. Because of these differences it's a nightmare to try and directly access a certain node within each result in the following way, which, in this case, would work, but only in IE:
entries[i].childNodes[0].childNodes[0].firstChild.nodeValue
Trust me. For the sake of your sanity you don't want to try messing with childNodes!! Stick with getElementsByTagName() if you know the type of the column you're looking for.
As a result of this eureka-like moment I've updated the NamePicker demo code and it now works in all browsers. You don't know how long I spent trying to work all this out. Now I prefer JSON more than ever. It's just that using ReadViewEntries XML makes a lot more sense some times.
Finally. While looking for a solution to this I managed to find this dubious Googlewhack, which will probably only last a day or so. Get it while it's hot.
As I said the other day, ?ReadViewEntries with all of the extra arguments beats ?OpenView hands-down. I haven't had time to work on any server-side xsl prepackaging/view prepping with tags yet. I think there's a solution in there somewhere which can help us avoid the parsing nightmares. If I hit the proverbial dead end, I might try the web services route and mess about with the LS in there.
IBM... make JSON an output option for ?ReadViewEntries !!
Better yet, IBM should create XML that does not place the data type as the node name, it should be an attribute. Let the end user cast the data type if they want to.
<entrydata columnnumber="0" name="$134">
<value type="datetime">20011219T180000,00+00</value>
</entrydata>
Just another thing Domino developers have to compensate for.
Fabulous Jake! I tried the demo in Safari and I was unable to delete names I had added until after I saved the form.
Great stuff. Would you be up for a podcast with Julian and myself?
Warm regards,
Bruce
I'd be inclined to go old school and treat the xml as a stream (read: document collection). We're even supplied with the document levels for determining if there are categories within the view.
Can't use M$ XMLDOM parser or the minis running Safari will be left out. It's just a node tree, so it can be traversed using recursion. It's just unencrypted tagged data and I would take this any day over working with RPG-II. :)
I found I was constantly getting employee info out of our company Notes database so I wrote the below script to get every node/value out of the view and put it all in an object. I use the sarissa javascript package for cross-browser compatabilty
/***********************************************************
convert XML data into JavaScript object
pass url of xml - works only with Notes style xml
!! Important - requires previous loading of sarissa javascript library
************************************************************/
function NotesXmltoObject(xmlpath){
var xmldoc = Sarissa.getDomDocument();
retObject = new Object();
xmldoc.async = false;
xmldoc.load(xmlpath);
if( xmldoc.parseError != 0) {
retObject.Error = 'xml format error'
}else {
var NoOfNodes = xmldoc.getElementsByTagName('viewentry')[0].childNodes.length;
for (i = 0; i < NoOfNodes; i++) {
//loop through all Nodes in viewentries
var NoOfAttributes = xmldoc.getElementsByTagName('entrydata')[i].attributes.length
for (j = 0; j < NoOfAttributes; j++) {
//loop through all attributes associated with current Node
if (xmldoc.getElementsByTagName('entrydata')[i].attributes[j].nodeName == 'name') {
// if the current attribute has a value of "name" then get it's value
var xmlNode = xmldoc.getElementsByTagName('entrydata')[i].attributes[j].nodeValue
//also get the value of the text associated with this Node
var xlmNodeValue = xmldoc.getElementsByTagName('entrydata')[i].text
retObject[xmlNode] = xlmNodeValue //put them in an object for easy retreval
}
}
}
}
xmldoc = null;
return retObject;
};
Call it with
retObj = NotesXmltoObject(xmlpath);
Then you can populate field values with :
f.CCManager.value = retObj.Manager;
f.ClaimentsName.value = retObj.Name;
f.CostCentre.value = retObj.CostC;
Jeff. Even better, what about:
<entrydata columnnumber="0" name="$134" type="datetime">
20011219T180000,00+00
</entrydata>
Bruce. Run out of people to ask? ;o)
That would be fine for single values, but what about lists? Then you would have multiple "value" nodes. Might as well always use the value node.
Jeff. Does Notes View XML cater for lists as it stands?
Yup, uglier still.
<entrydata columnnumber="5" name="$147">
<textlist>
<text>First Value</text>
<text>Second Value</text>
<text>Third Value</text>
</textlist>
</entrydata>
*sigh*
LOL - @Jake I didn't say we would podcast anytime soon now did I :-). Ping me offline so we can discuss.
Warm regards,
Bruce
&preformat formats datetime into text
xPath example:
columnNode=xmldoc.selectNodes("/viewentries/viewentry/entrydata[@columnnumber='0']");
for(i=0;i<columnNode.length;i++){
alert(columnNode[i].text);
}
How to support selectNodes in FireFox:
{Link}
Google AJAXSLT: {Link}
{Link}
The solution you have given is good. But how can we add a separator like comma between the list of items? I'm thankful if you give a solution.
I've often wanted just basic XML from a view without having to reinvent it for every view. Here's an agent that takes a view and spits out basic, nested XML using the column titles. All categorized columns must come first. I haven't allowed for XML that needs CDATA tags yet. The URL must contain the viewname you want to get data from and can optionally contain the tag name for individual records.
Example URL:
{Link}
Sub Initialize
Dim session As New NotesSession
Dim db As NotesDatabase
Dim nav As NotesViewNavigator
Dim vc As NotesViewColumn
Dim entry As notesviewentry
Dim nextentry As notesviewentry
Dim firstentry As notesviewentry
Dim lastentry As NotesViewEntry
Dim view As NotesView
'get viewname and record descriptor parameters from the query string
Dim qs As String, viewname As String, recordname As String
qs = session.DocumentContext.Query_String(0)
param = "viewname"
arg2 = Instr(qs, "&" + param + "=" ) + Len(param) + 2
paramval = Mid ( qs, arg2)
If Instr(paramval, "&") > 0 Then
paramval = Left (paramval, Instr(paramval, "&")-1)
End If
viewname = paramval
param = "record"
arg2 = Instr(qs, "&" + param + "=" ) + Len(param) + 2
paramval = Mid ( qs, arg2)
If Instr(paramval, "&") > 0 Then
paramval = Left (paramval, Instr(paramval, "&")-1)
End If
record = paramval
If record = "" Then
record = "record"
End If
Dim i As Integer
Dim leveldeep As Integer
Set db = session.CurrentDatabase
Set view = db.GetView(viewname)
Set nav= view.CreateViewNav
Dim closingtags(80) As String
'get an array of column headings zero-based
Dim charr () As String
For i = 0 To view.ColumnCount-1
Redim Preserve charr(i) As String
Set vc = view.Columns(i)
charr(i) = vc.Title
Next
'determine how many of those columns are categories
Dim catcount As Integer, noncatcount As Integer
catcount = 0
Forall c In view.columns
If c.IsCategory Then
catcount = catcount + 1
End If
End Forall
noncatcount = view.ColumnCount - catcount
Print |Content-type: text/xml|
Print |<?xml version="1.0" encoding="UTF-8"?>|
Print |<viewentries>|
Set entry = nav.GetFirst
leveldeep = 0
While Not entry Is Nothing
'print categories
If entry.IsCategory Then
Print |<| + charr(entry.indentlevel) +| value="| + entry.ColumnValues(entry.indentlevel) +|">|
levelsdeep = entry.indentlevel
Else
'the tag that surrounds the data items is provided in the URL
Print |<|+record+|>|
'print all the rest of the columns
For i = catcount To noncatcount+1
Print |<| + charr(i) +|>|
Print entry.ColumnValues(i)
Print |</| + charr(i) +|>|
Next
Print |</|+record+|>|
End If
Set nextentry = nav.GetNext(entry)
If nextentry Is Nothing Then
'special case for end of view
For i = entry.IndentLevel-1 To 0 Step -1
Print |</| + charr(i) + |>|
Next
Else
If nextentry.IndentLevel < entry.indentlevel Then
For i = entry.IndentLevel -1 To nextentry.IndentLevel Step -1
Print |</| + charr(i) + |>|
Next
End If
End If
'loop to next entry
Set entry = nav.GetNext(entry)
Wend
Print |</viewentries>|
End Sub
Results in:
<?xml version="1.0" encoding="UTF-8" ?>
- <viewentries>
- <colorcat value="Normal">
- <language value="English">
- <data>
<color>blue</color>
<city>9/11/2007 6:45:42 PM</city>
<state>90</state>
</data>
- <data>
<color>yellow</color>
<city>9/14/2007 5:21:58 PM</city>
<state>82</state>
</data>
</language>
- <language value="French">
- <data>
<color>rouge</color>
<city>9/11/2007 6:45:30 PM</city>
<state>110</state>
</data>
- <data>
<color>narange</color>
<city>9/14/2007 5:17:25 PM</city>
<state>92</state>
</data>
- <data>
<color>verde</color>
<city>9/14/2007 5:18:06 PM</city>
<state>90</state>
</data>
</language>
- <language value="Spanish">
- <data>
<color>naranja</color>
<city>9/11/2007 6:46:52 PM</city>
<state>105</state>
</data>
- <data>
<color>rojo</color>
<city>9/14/2007 5:16:08 PM</city>
<state>102</state>
</data>
- <data>
<color>azul</color>
<city>9/14/2007 5:16:48 PM</city>
<state>90</state>
</data>
- <data>
<color>verde</color>
<city>9/14/2007 5:17:50 PM</city>
<state>91</state>
</data>
- <data>
<color>amarillo</color>
<city>9/14/2007 5:22:06 PM</city>
<state>84</state>
</data>
</language>
</colorcat>
- <colorcat value="Special">
- <language value="English">
- <data>
<color>fuscia</color>
<city>9/11/2007 6:45:50 PM</city>
<state>93</state>
</data>
- <data>
<color>mauve</color>
<city>9/11/2007 6:45:57 PM</city>
<state>92</state>
</data>
- <data>
<color>crimson</color>
<city>9/14/2007 5:21:48 PM</city>
<state>84</state>
</data>
- <data>
<color>sky blue</color>
<city>9/14/2007 5:22:16 PM</city>
<state>85</state>
</data>
</language>
- <language value="French">
- <data>
<color>mauve</color>
<city>9/14/2007 5:17:09 PM</city>
<state>91</state>
</data>
- <data>
<color>chartruse</color>
<city>9/14/2007 5:22:50 PM</city>
<state>85</state>
</data>
</language>
- <language value="Spanish">
- <data>
<color>azul cielo</color>
<city>9/14/2007 5:22:35 PM</city>
<state>87</state>
</data>
</language>
</colorcat>
</viewentries>