If you want to export a document and all of its descendants, I recommend making a subroutine that, given a document and a collection, adds the document's children to the collection and then loops through each of it's children and calls said function recursively... or if they aren't parent/response documents, whatever other logic that says "here's the starter document, add related documents." Then you can export the collection.
There isn't a way to get only specific fields from the exporter.
However, if you learn XSL, you can do quite a bit.
Your needs might be completely different from mine, but my goal was to take a database of Company, Product, and Factory documents and get a data structure like:
<ExportGrouped daddy="JSmart523">
<COMPANY CREATED_TIMESTAMP="somedate" LAST_UPDATE_TIMESTAMP="somedate" DOMINOUNID="12345678901234567890123456789012"
COMPANYNAME="Acme R Us" OLD_SYSTEM_ID="42" OTHERATTRIBUTES="...">
<PRODUCT CREATED_TIMESTAMP="somedate" LAST_UPDATE_TIMESTAMP="somedate" DOMINOUNID="12345678901234567890123456789012"
PRODUCTNAME="..." OTHERATTRIBUTES="..."/>
<PRODUCT CREATED_TIMESTAMP="somedate" LAST_UPDATE_TIMESTAMP="somedate" DOMINOUNID="12345678901234567890123456789012"
PRODUCTNAME="..." OTHERATTRIBUTES="..."/>
<FACILITY CREATED_TIMESTAMP="somedate" LAST_UPDATE_TIMESTAMP="somedate" DOMINOUNID="12345678901234567890123456789012"
NAME="..." OTHERATTRIBUTES="..."/>
<FACILITY CREATED_TIMESTAMP="somedate" LAST_UPDATE_TIMESTAMP="somedate" DOMINOUNID="12345678901234567890123456789012"
NAME="..." OTHERATTRIBUTES="..."/>
</COMPANY>
</ExportGrouped>
which meant one node per document, field names as attributes (with exceptions I've omitted), and having response documents' nodes within their parent documents' nodes.
I tried to do it in just one XSL file but then every main document required a second pass through the database to find the related documents, which meant that the more data I had, the length of time it took to process the XSL grew exponentially! Instead I broke it up into two xsl files: Pass 1 got the data I wanted, then pass 2 organized it in the order I wanted.
However, field names aren't case sensitive in Lotus Notes and that smacked me in the head, so, in addition to pass 1 and pass 2, ...
Pass 0: Translate Item Names to lower case
While field names are not case sensitive in Notes, XSL is case sensitive. For any given document, field name FieldName
might be fieldname
or FIELDNAME
. At first I tried to handle this within my first XSL file but it made the code messy and slow, so I added a "zeroth" pass to my process. I also took the opportunity to remove nodes I didn't want, such as config documents or any bitmaps since the destination wasn't going to store them. The input XML file was always the entire Notes database (default NotesDXLExporter options so design wasn't included) or, when just debugging something, a single document.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:dxl="http://www.lotus.com/dxl" version="1.0">
<xsl:output indent="no"/>
<!-- Thanks to http://stackoverflow.com/questions/586231/how-can-i-convert-a-string-to-upper-or-lower-case-with-xslt for this method of using translate() to convert to lower case within xsl 1.0 -->
<xsl:variable name="smallCase" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upperCase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:template match="*|text()|@*">
<xsl:copy>
<xsl:apply-templates select="*|text()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="dxl:item/@name">
<xsl:attribute name="name">
<xsl:value-of select="translate(.,$upperCase,$smallCase)"/>
</xsl:attribute>
</xsl:template>
<!-- easily identifiable nodes we will not need -->
<xsl:template match="dxl:document[@form='Configuration']"/>
<xsl:template match="dxl:document[@form='Counter']"/>
<xsl:template match="dxl:document[@form='Preference']"/>
<xsl:template match="dxl:document[@form='Void']"/>
<xsl:template match="dxl:databaseinfo|dxl:updatedby|dxl:revisions|dxl:notesbitmap|dxl:compositedata|dxl:launchsettings|dxl:item[dxl:rawitemdata]|dxl:embeddedcontrol"/>
</xsl:stylesheet>
Pass 2: All But Structure
My second file did most of the work, creating the COMPANY, PRODUCT, AND FACILITY nodes, but not rearranging them - they were still siblings, direct children of the root node. (Caveat: I didn't have to worry about responses to responses. Your code might differ.)
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:dxl="http://www.lotus.com/dxl" version="1.0" exclude-result-prefixes="dxl">
<xsl:output indent="no"/>
<!-- / -->
<xsl:template match="/">
<xsl:element name="ExportUngrouped" namespace="">
<xsl:apply-templates select="*"/>
</xsl:element>
</xsl:template>
<!--
/*
Uncaught root element!
If this template matches then we have a problem. The root node this XSL is meant to process is /dxl:database or /dxl:document
-->
<xsl:template match="/*" priority="0">
<xsl:element name="badelement">
<xsl:attribute name="Expected">/database or /document</xsl:attribute>
<xsl:attribute name="ExpectedNamespace">http://www.lotus.com/dxl</xsl:attribute>
<xsl:attribute name="ActualNodeName">
<xsl:call-template name="XPathOfCurrentNode"/>
</xsl:attribute>
<xsl:attribute name="ActualNamespace">
<xsl:value-of select="namespace-uri()"/>
</xsl:attribute>
<xsl:copy>
<xsl:copy-of select="@*"/>
</xsl:copy>
</xsl:element>
</xsl:template>
<!-- dxl:database -->
<xsl:template match="dxl:database">
<xsl:attribute name="replicaid">
<xsl:value-of select="@replicaid"/>
</xsl:attribute>
<!-- other stuff I wanted at the root database node -->
<!--This is just an example, btw! -->
<xsl:variable name="daddy" select="dxl:document[@form='FAQ']/dxl:item[@name='DocTitle'][. = 'WhosYourDaddy'][1]/../dxl:item[@name='DaddyName']"/>
<xsl:if test="$daddy">
<xsl:attribute name="daddy"><xsl:value-of select="$daddy"/></xsl:attribute>
</xsl:if>
<xsl:apply-templates select="dxl:document"/>
</xsl:template>
<!-- dxl:document nodes I want. I recommend making a spreadsheet that documents the forms you are looking for and generates the desired XSL. In my real XSL file, this wasn't pretty and indented, it was a copy & paste of one line per xsl:template tag. -->
<xsl:template match="dxl:document[@form='Company']">
<xsl:element name="COMPANY">
<xsl:call-template name="DocumentElementContents"/>
</xsl:element>
</xsl:template>
<xsl:template match="dxl:document[@form='Product']">
<xsl:element name="PRODUCT">
<xsl:call-template name="DocumentElementContents"/>
</xsl:element>
</xsl:template>
<xsl:template match="dxl:document[@form='Factory']">
<xsl:element name="FACILITY">
<xsl:call-template name="DocumentElementContents"/>
</xsl:element>
</xsl:template>
<!-- dxl:document nodes that we somehow missed -->
<xsl:template match="dxl:document">
<xsl:element name="uncaughtdocument">
<xsl:attribute name="form">
<xsl:value-of select="@form"/>
</xsl:attribute>
<xsl:call-template name="DocumentElementContents"/>
</xsl:element>
</xsl:template>
<!--
*************************************************
Templates for AttributeItems mode
Called by named template DocumentElementContents
AttributeItems mode adds attributes to the element that is created for a given dxl:document.
Where possible, data is copied here.
*************************************************
-->
<!-- AttributeItems dxl:noteinfo -->
<xsl:template mode="AttributeItems" match="dxl:noteinfo">
<xsl:attribute name="CREATED_TIMESTAMP">
<xsl:value-of select="dxl:created/dxl:datetime"/>
</xsl:attribute>
<xsl:attribute name="LAST_UPDATE_TIMESTAMP">
<xsl:value-of select="dxl:modified/dxl:datetime"/>
</xsl:attribute>
<xsl:attribute name="DOMINOUNID">
<xsl:value-of select="@unid"/>
</xsl:attribute>
</xsl:template>
<!--
Called by DocumentElementContents.
Describes what to do with an item when we are looking for items to include as attributes.
I recommend making an Excel spreadsheet to generate this part because then your project documentation will be your code generator.
-->
<xsl:template mode="AttributeItems" match="dxl:document[@form='Company']/dxl:item[@name='CompanyName']">
<xsl:attribute name="COMPANYNAME">
<xsl:call-template name="AttributeSafeValueOfItem"/>
</xsl:attribute>
</xsl:template>
<xsl:template mode="AttributeItems" match="dxl:document[@form='Company']/dxl:item[@name='CompanyID']">
<xsl:attribute name="OLD_SYSTEM_ID">
<xsl:call-template name="AttributeSafeValueOfItem"/>
</xsl:attribute>
</xsl:template>
<!-- etc, etc -->
<!-- If an item is not caught above while we are in AttributeItems mode, ignore it. -->
<xsl:template mode="AttributeItems" match="dxl:item"/>
<!--
*************************************************
Templates for ElementItems mode
*************************************************
-->
<!--
Called by DocumentElementContents.
Describes what to do with an item when we are looking through the items to see what element nodes should be added
-->
<!-- Your code goes here for each element node to be created within the node created for that document. I omitted this part because it didn't help answer (my interpretation of) your question. -->
<!-- If an item is not caught above while we are in ElementItems mode, ignore it. -->
<xsl:template mode="ElementItems" match="dxl:item"/>
<!--
"DocumentElementContents"
generic code to be called for each NotesDocument we are exporting.
-->
<xsl:template name="DocumentElementContents">
<xsl:choose>
<xsl:when test="@parent">
<xsl:attribute name="MainDoc">false</xsl:attribute>
<xsl:attribute name="ParentUNID">
<xsl:value-of select="@parent"/>
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="MainDoc">true</xsl:attribute>
</xsl:otherwise>
</xsl:choose>
<xsl:apply-templates mode="AttributeItems" select="dxl:item|dxl:noteinfo"/>
<xsl:apply-templates mode="ElementItems" select="dxl:item"/>
</xsl:template>
<!--
AttributeSafeValueOfItem
Outputs the values of the item, delimited by commas if there are multiple values.
Current node expected to be an item, but will work with any node where we want to export the content of all descendants that do not have the string 'list' in them. (e.g. item/textlist will be excluded but values within item/textlist/test will be exported)
-->
<xsl:template name="AttributeSafeValueOfItem">
<xsl:for-each select=".//*[not(contains(local-name(),'list'))]">
<xsl:value-of select="."/>
<xsl:if test="position() != last()">, </xsl:if>
</xsl:for-each>
</xsl:template>
<!--
XPathOfCurrentNode
For debugging. Outputs a string that is the XPath of the current node.
-->
<xsl:template name="XPathOfCurrentNode">
<xsl:for-each select="ancestor-or-self::*">
<xsl:call-template name="XPathOfCurrentNode_NodeLevel"/>
</xsl:for-each>
</xsl:template>
<!--
XPathOfCurrentNode_NodeLevel
For debugging. Called by XPathOfCurrentNode for each ancestor-or-self::.
-->
<xsl:template name="XPathOfCurrentNode_NodeLevel">
<xsl:variable name="precedingCount" select="count(preceding-sibling::*[name(.)=name(current())])"/>
<xsl:text>/</xsl:text>
<!--<xsl:value-of select="namespace-uri()"/>-->
<xsl:value-of select="name(.)"/>
<xsl:if test="$precedingCount or count(following-sibling::*[name(.)=name(current())])">
<xsl:text>[</xsl:text>
<xsl:value-of select="$precedingCount+1"/>
<xsl:text>]</xsl:text>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Note the exclude-result-prefixes
attribute in the xsl:stylesheet
tag so that dxl:
isn't peppered throughout the output.
Pass 3: Reorg!
At this point I had everything I needed except it was in the wrong order. Your final structure may differ, but for me, all I needed to do at this point was to move each child node within the parent node so that each COMPANY node contained each relevant PRODUCT and FACILITY node.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<!--
After the previous xsl file has processed the code, we have all we need but it's not organized. It's in the order of documents received.
We want to group all of the nodes related to a case within the main doc node itself.
-->
<xsl:output indent="yes"/>
<xsl:key name="kSupportingDocsByParent" match="/ExportUngrouped/*[@ParentUNID and @MainDoc='false']" use="@ParentUNID"/>
<xsl:template match="/ExportUngrouped" priority="1">
<ExportGrouped>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="*[@MainDoc='true']">
<xsl:sort select="@ParentUNID"/>
</xsl:apply-templates>
</ExportGrouped>
</xsl:template>
<xsl:template match="/ExportUngrouped/*[@MainDoc='true']" priority="1">
<xsl:copy>
<xsl:apply-templates select="@*|*"/>
<xsl:for-each select="key('kSupportingDocsByParent',@DOMINOUNID)">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template match="@MainDoc|@OtherAttributesTheFinalOutputShouldNotHave" priority="1"/>
<xsl:template match="/ExportUngrouped/*/node()|@*" priority="0">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
Done!
This is definitely a beefier answer than you had probably wanted. I hope it helps you or someone else!