Any XML experts here? Given the fragment below, p...
# cfml-general
j
Any XML experts here? Given the fragment below, parseXML()'d into an xmlDoc, how can I find the strategy.objectives.objective.objective_term_ref with
name
= "Active_Risk" and set the
weight
to -2 before saving the changed doc as an xml file?
Copy code
<?xml version="1.0" encoding="utf-8"?>
<!-- saved with AxiomaPortfolio API 2022.2.0 -->
<STRATEGY xmlns="<http://axiomainc.com/portfolioprecision>" strategy-id="FTDIBase" desc="Frontier">
  <OPTIONS>
    <ALLOW_GRANDFATHERING/>
  </OPTIONS>
  <LOCAL_UNIVERSE>
    <ASSET_REF name="CSH_USD__" action="include"/>
    <GROUP_REF name="AccountBenchmark" action="include"/>
  </LOCAL_UNIVERSE>
  <OBJECTIVE_TERMS>
    <RISK objective-term-id="Active_Risk" benchmark-ref="REBALANCING.BENCHMARK" riskmodel-ref="REBALANCING.RISK_MODEL" set-ref="MASTER">
    </RISK>
    <LINEAR objective-term-id="Factor_Tilt" group-ref="WW4AxiomaMH.Dividend Yield"/>
    <TAX_GAINS objective-term-id="Min_Cap_Gains" weighted="false"/>
    <TAX_LIABILITY objective-term-id="Min_Tax_Liability"/>
    <TRANSACTION_COST objective-term-id="Min_Transaction_Cost" base-set="MASTER"/>
  </OBJECTIVE_TERMS>
  <OBJECTIVES active-objective="Default">
    <OBJECTIVE objective-id="Default" target="MAXIMIZE">
      <OBJECTIVE_TERM_REF name="Min_Cap_Gains" weight="-1.0"/>
      <OBJECTIVE_TERM_REF name="Active_Risk" weight="-1.0"/>
      <OBJECTIVE_TERM_REF name="Min_Transaction_Cost" weight="-0.25"/>
      <OBJECTIVE_TERM_REF name="Factor_Tilt" weight="1.0"/>
    </OBJECTIVE>
  </OBJECTIVES>
s
When I have to do this, I convert the XML to CF, do what needs doing, then convert it back. It's probably not as performant as just working with an Xmldoc but I hate xmldocs.
❤️ 3
Copy code
<!---
	This function converts XML variables into Coldfusion Structures. It also
	returns the attributes for each XML node.
--->

	<cffunction
		name="ConvertXmlToStruct"
		access="public"
		returntype="struct"
		output="true"
		hint="Parse raw XML response body into ColdFusion structs and arrays and return it."
	>
		<cfargument name="xmlNode" type="string" required="true"/>
		<cfargument name="str" type="struct" default="#structNew()#"/>
		<!--- Setup local variables for recurse: --->
		<cfset var i = 0/>
		<cfset var axml = arguments.xmlNode/>
		<cfset var atr = ''>
		<cfset var astr = arguments.str/>
		<cfset var n = ""/>
		<cfset var tmpContainer = ""/>
		<cfset axml = xmlSearch( xmlParse( arguments.xmlNode ), "/node()" )>
		<cfset axml = axml[ 1 ]/>
		<!--- For each children of context node: --->
		<cfloop from="1" to="#arrayLen( axml.XmlChildren )#" index="i">
			<!--- Read XML node name without namespace: --->
			<cfset n = replace( axml.XmlChildren[ i ].XmlName, axml.XmlChildren[ i ].XmlNsPrefix & ":", "" )/>
			<!--- If key with that name exists within output struct ... --->
			<cfif structKeyExists( astr, n )>
				<!--- ... and is not an array... --->
				<cfif not isArray( astr[ n ] )>
					<!--- ... get this item into temp variable, ... --->
					<cfset tmpContainer = astr[ n ]/>
					<!--- ... setup array for this item beacuse we have multiple items with same name, ... --->
					<cfset astr[ n ] = arrayNew( 1 )/>
					<!--- ... and reassing temp item as a first element of new array: --->
					<cfset astr[ n ][ 1 ] = tmpContainer/>
				<cfelse>
					<!--- Item is already an array: --->
				</cfif>
				<cfif arrayLen( axml.XmlChildren[ i ].XmlChildren ) gt 0>
					<!--- recurse call: get complex item: --->
					<cfset astr[ n ][ arrayLen( astr[ n ] ) + 1 ] = convertXmlToStruct(
						axml.XmlChildren[ i ],
						structNew()
					)/>
				<cfelse>
					<!--- else: assign node value as last element of array: --->
					<cfset astr[ n ][ arrayLen( astr[ n ] ) + 1 ] = axml.XmlChildren[ i ].XmlText/>
				</cfif>
			<cfelse>
				<!---
					This is not a struct. This may be first tag with some name.
					This may also be one and only tag with this name.
				--->
				<!--- If context child node has child nodes (which means it will be complex type): --->
				<cfif arrayLen( axml.XmlChildren[ i ].XmlChildren ) gt 0>
					<!--- recurse call: get complex item: --->
					<cfset astr[ n ] = convertXmlToStruct( axml.XmlChildren[ i ], structNew() )/>
				<cfelse>
					<cfif isStruct( aXml.XmlAttributes ) AND structCount( aXml.XmlAttributes )>
						<cfset at_list = structKeyList( aXml.XmlAttributes )>
						<cfloop from="1" to="#listLen( at_list )#" index="atr">
							<cfif listGetAt( at_list, atr ) CONTAINS "xmlns:">
								<!--- remove any namespace attributes --->
								<cfset structDelete( axml.XmlAttributes, listGetAt( at_list, atr ) )>
							</cfif>
						</cfloop>
						<!--- if there are any atributes left, append them to the response --->
						<cfif structCount( axml.XmlAttributes ) GT 0>
							<cfset astr[ '_attributes' ] = axml.XmlAttributes/>
						</cfif>
					</cfif>
					<!--- else: assign node value as last element of array: --->
					<!--- if there are any attributes on this element --->
					<cfif isStruct( aXml.XmlChildren[ i ].XmlAttributes ) AND structCount(
						aXml.XmlChildren[ i ].XmlAttributes
					) GT 0>
						<!--- assign the text --->
						<cfset astr[ n ] = axml.XmlChildren[ i ].XmlText/>
						<!---
							check if there are no attributes with xmlns: , we dont want namespaces to be in the response
						--->
						<cfset attrib_list = structKeyList( axml.XmlChildren[ i ].XmlAttributes )/>
						<cfloop from="1" to="#listLen( attrib_list )#" index="attrib">
							<cfif listGetAt( attrib_list, attrib ) CONTAINS "xmlns:">
								<!--- remove any namespace attributes --->
								<cfset structDelete(
									axml.XmlChildren[ i ].XmlAttributes,
									listGetAt( attrib_list, attrib )
								)>
							</cfif>
						</cfloop>
						<!--- if there are any atributes left, append them to the response --->
						<cfif structCount( axml.XmlChildren[ i ].XmlAttributes ) GT 0>
							<cfset astr[ n & '_attributes' ] = axml.XmlChildren[ i ].XmlAttributes/>
						</cfif>
					<cfelse>
						<cfset astr[ n ] = axml.XmlChildren[ i ].XmlText/>
					</cfif>
				</cfif>
			</cfif>
		</cfloop>
		<!--- return struct: --->
		<cfreturn astr/>
	</cffunction>
and
Copy code
/**
	*  @param input      Data to convert into XML. (Required)
 	* @param element      Used to name the root element. (Required)
	* @param attributeMeta Checks for the presence of a key in attributeMeta when rendering the key in input, and writes out the contents as attribute/value pairs for the corresponding key
 	* @return Returns a string. 
 	* @author Phil Arnold (<mailto:philip.r.j.arnold@googlemail.com|philip.r.j.arnold@googlemail.com>) with modifications by Samuel Knowlton (<mailto:sam@inleague.io|sam@inleague.io>) - removed lcasing and added support for attributeMeta
 	* @version September 9, 2009 / September 29, 2021 
 	**/
   
	public string function toXML( required input, required string element, struct attributeMeta = {} ) {
		var i = 0;
		var s = createObject( "java", "java.lang.StringBuilder" ).init()
		var s1 = "";
		s1 = arguments.element;
		if ( right( s1, 1 ) eq "s" ) {
			s1 = left( s1, len( s1 ) - 1 );
		}

		s.append( renderXMLAttribute( arguments.element, arguments.attributeMeta ) );
		if ( isArray( arguments.input ) ) {
			for ( i = 1; i lte arrayLen( arguments.input ); i = i + 1 ) {
				if ( isSimpleValue( arguments.input[ i ] ) ) {
					s.append( renderXMLAttribute( s1, attributeMeta ) & arguments.input[ i ] & "</#s1#>" );
				}
				else {
					s.append( toXML( arguments.input[ i ], s1 ) );
				}
			}
		}
		else if ( isStruct( arguments.input ) ) {
			for ( i in arguments.input ) {
				if ( isSimpleValue( arguments.input[ i ] ) ) {
					s.append( renderXMLAttribute( i, attributeMeta ) & arguments.input[ i ] & "</#i#>" );
				}
				else {
					s.append( toXML( arguments.input[ i ], i ) );
				}
			}
		}
		else {
			s.append( xmlFormat( arguments.input ) );
		}
		s.append( "</#arguments.element#>" );

		return s.toString();
	}

	/**
	 * renderXMLAttribute
	 * @hint A totally low-rent way to render out XML attributes (e.g. <foo bar="baz">) by checking for the presence of the key in a "sister struct" containing attribute meta nodes. Can't handle multiple nodes with the same name.
	 * @return String OPENING attribute only (e.g. just <foo bar="baz">)
	 */

	public string function renderXMLAttribute( required string key, required struct attributeMeta ) {
		var attr = '<#arguments.key#';
		if ( attributeMeta.keyExists( arguments.key ) ) {
			for ( var metaKey in arguments.attributeMeta[ arguments.key ].keyArray() ) {
				attr &= ' #metaKey#="' & arguments.attributeMeta[ arguments.key ][ metaKey ] & '"';
			}
		}
		attr &= '>';
		return attr;
	}
j
trying it
s
There are probably java libs that will do this better/faster
j
I was parsing using a java converter for the same reason but it won't write the attributes back out
s
yeah attributes are a pain for some reason
j
I can't even remember how to use a tag-based udf 🙂
s
should just run it through that cfscript converter, I hate that I still have it in tags but am lazy
d
I would do this for your example:
Copy code
data = xmlparse(xmldata);

objs = data.strategy.objectives.objective.xmlchildren;

arrayMap( data.strategy.objectives.objective.xmlchildren, function( node ) {
						if (node.xmlattributes.name eq "Active_Risk"){
						    node.xmlattributes.weight = "-2.0";
						};
					} )
s
^^^ that's a lot more surgical if that's really the only change you gotta make
d
yeah exactly
j
I have to make a number of changes but something like that could work
d
it will error if there is a node without a name or weight attribute. so you have to check for that.
👍🏼 1
s
we do what we do because we have a lot of transformations where we have to read XML A, then input it into JSON B, then also produce XML C, because what are third party integrations without technology from both yesterday and twenty years ago
j
totally get that
t
This works
Copy code
XMLSearch(xmlDoc, "//*[local-name()='OBJECTIVE_TERM_REF' and @name='Active_Risk']/@weight")[1].XmlText = "-2";
ToString(xmlDoc);
so not sure what's wrong with my xpath string...
s
It seems to be something to do with the namespace declaration "xmlns" attribute. I can't get any results on XmlSearch with that there.
Other than that, I was heading towards basically the same XPath that @Tim recommended.
t
oh... okay. I know how to deal with that, but I've only had to do it with namespaces prefixes before.
j
Daniel's suggestion worked. Thanks for the help!
d
I tried xpath one time long time ago and when I could not consistantly follow the documention to search xml data I gave up and started using nodejs and in nodejs it works.
in cfml I like fine grained search with xmlparse. that is pretty solid.
s
The efficient use of xpath is really one of my favorite things about xml, but everybody is different. 🙂
s
I'm not constitutionally capable of using the word 'favorite' and 'xml' in the same sentence
t
edited to work with the namespace
j
Very interesting
yes that is elegant
@Tim how would I modify that xpath to find any node under
constraints
with
constraint-id
= Global_DNH given the fragment below?
Copy code
<CONSTRAINTS>
    <BUDGET constraint-id="Budget" desc="" enabled="true" use-budget-value="true" unit="CURRENCY"/>
    <LIMIT_NAMES constraint-id="Max_Number_Of_Names" desc="" enabled="true" scope="AGGREGATE" grandfather-bounds="true" max="400">
      <SET_REF name="MASTER"/>
    </LIMIT_NAMES>
    <THRESHOLD_BUY_TRADE constraint-id="Threshold_Buy" desc="" enabled="true" scope="ASSET" min="0.05" unit="PERCENT">
      <SET_REF name="MASTER"/>
    </THRESHOLD_BUY_TRADE>
    <MULTI_ASSET_NO_WASH constraint-id="Dual_Listed_Wash_Sales" desc="" enabled="false" scope="AGGREGATE">
      <SET_REF name="MASTER"/>
    </MULTI_ASSET_NO_WASH>
    <LIMIT_BUY constraint-id="Global_DNB" desc="" enabled="true" scope="ASSET" max="0.0" unit="CURRENCY">
      <SET_REF name="DonotBuyConstraint"/>
    </LIMIT_BUY>
    <LIMIT_HOLDING constraint-id="Global_DNH" desc="" enabled="true" scope="ASSET" min="0.0" grandfather-bounds="false" max="0.0" unit="CURRENCY">
      <SET_REF name="DonotHoldConstraint"/>
    </LIMIT_HOLDING>
t
//*[local-name() = 'CONSTRAINTS']/*[@constraint-id='Global_DNH']
, I think.
//*[local-name() = TAG_NAME]
gets an array of all the given tags in the document.
/
then goes to a child of that tag.
@
is the prefix for an attribute.
*
is "get all the child tags" just like
*
is "get all the files" in file path.
The brackets
[...]
contain a condition to be evaluated on the tags
so we get all the tags in the document
//*
then look for ones called CONSTRAINTS (
local-name() = 'CONSTRAINTS'
). Then look for the children of that (
/*
) with an attribute of constraint-id (
[@constraint-id='blah']
)
that then returns an array of elements. s o you can loop through it and do whatever. Or, like in the first case, just grab the first one if you know it'll be unique, and edit it.
j
Great explanation. Very elegant