
1. A Developer Tool for ACF Programming - XML
When crafting XML documents in your code, you might often refer to sample documents, especially in API documentation for web services. To facilitate this process for developers, we've created a handy tool that generates ACF source code based on these sample XML documents. Written in the ACF language, it uses XML functions to parse provided samples and create corresponding source code. Once you use this tool to generate the foundational code, you can easily insert actual data elements, enabling the generation of practical results directly applicable in your applications.
Needed plugin version for this to work: 1.7.0.5 -pre-release.
- Download at: Downloads
Let's say, we have this sample from some service:
<?xml version="1.0"?>
<customers>
<customer id="55000">
<name>Charter Group</name>
<address>
<street>100 Main</street>
<city>Framingham</city>
<state>MA</state>
<zip>01701</zip>
</address>
<address>
<street>720 Prospect</street>
<city>Framingham</city>
<state>MA</state>
<zip>01701</zip>
<Phone>
<type>Mobile</type>
<number>12345678</number>
</Phone>
</address>
<address>
<street>120 Ridge</street>
<state>MA</state>
<zip>01760</zip>
</address>
</customer>
</customers>
Then, running it through this tool, we got the following ACF code:
Function MakeCustomerAddressesXML()
XML xmlvar;
xmlVar["customers.customer"] =
XML("name","Charter Group");
XMLref addressRef = FindElement(xmlVar["customers.customer.address"]);
addressRef["[]"] = XML("street","100 Main","city","Framingham","state","MA"
,"zip","01701");
addressRef["[]"] = XML("street","720 Prospect","city","Framingham","state","MA"
,"zip","01701","Phone",XML("type","Mobile","number","12345678"));
addressRef["[]"] = XML("street","120 Ridge","state","MA","zip","01760"
);
// Assign attributes to the XML...
XMLattributes(xmlVar["customers.customer"], XML("id","55000");
return string(XmlVar);
End
Running this will produce the sample directly:
<?xml version="1.0" encoding="UTF-8"?>
<customers>
<customer id="55000">
<name>Charter Group</name>
<address>
<street>100 Main</street>
<city>Framingham</city>
<state>MA</state>
<zip>01701</zip>
</address>
<address>
<street>720 Prospect</street>
<city>Framingham</city>
<state>MA</state>
<zip>01701</zip>
<Phone>
<type>Mobile</type>
<number>12345678</number>
</Phone>
</address>
<address>
<street>120 Ridge</street>
<state>MA</state>
<zip>01760</zip>
</address>
</customer>
</customers>
Now, when we have this sample, we can easily replace the hard-coded data values in it with actual database content, or add more structures to it when necessary.
1.1. Source code for the tool ↑
Here is the source code for the developer tool:
package xmlTools "XML tool developer package"
/*
Developer tool Package to produce ACF source code from XML source.
Main function is:
ACFSource = ToolCreateACF_fromXML (xml source);
From FileMaker script:
Set Field [Demo::Result; ACF_Run("ToolCreateACF_fromXML"; $XMLSource)]
*/
/*
Count the elements with the same name, to determine if it is an array or not.
*/
function checkIfArray ( array string aKeys, string token )
int count = 0;
int vc = sizeof (aKeys);
int i;
for ( i = 1, vc )
if ( aKeys[i] == token && akeys[i] != "") then
count ++;
end if
end for
return count;
end
/*
Handle leaf nodes on any given level. Use XML(....) function for all leaf nodes.
Don't touch array nodes. Mark them as done in the array (that is transferred by reference)
*/
function HandleLeafNodes (XML so, string Key, array string aKeys, string xmlvarName)
int i, arrcnt, hit=0, vc = sizeof (aKeys) ;
string prefix = "", result = "XML(", thiskey;
// Loop and check if any of the nodes on this level are LEAF nodes, and then create a XML function call for them.
for (i = 1, vc)
if ( aKeys[i] != "") then
thiskey = aKeys[i];
arrcnt = checkIfArray( aKeys, thisKey ); // is it an array? - only handle leaf nodes that is no an array.
if ( list_keys(so[thiskey]) == "" && arrcnt == 1) then // leaf node
result += prefix + '"'+thiskey+'","' + string(so[thiskey]) + '"';
hit++;
if ( mod(hit,3) == 0) then
result += "\n\t\t";
end if
aKeys[i] = ""; // Mark it as done.
prefix = ",";
end if
end if
end for
if ( hit > 0) then
return xmlvarName + '["'+Key+'"] = \n\t\t' + result + ");\n";
end if
return "";
end
/*
Make a key, as period-separated tokens.
*/
function MakeKey ( string key, string thisKey)
string qKey;
if ( key == "") then
qKey = thisKey ;
else
qKey = key + "." + thisKey ;
end if
return qKey;
end
/*
Recursive function to create parameter lists to the XML function. To handle leaf
nodes
*/
function KeyValuePairList (XML so, string key, string token, string xmlvarName)
string keys = list_keys(so), thisKey, result;
string KeyValuePairList, prefix; // decl return type;
int hit=0, i, vc = valuecount ( keys );
if ( vc > 0) then
result = "XML(";
prefix = "";
for (i = 1, vc)
thiskey = GetValue(keys,i);
if ( list_keys(so[thiskey]) == "") then // leaf node
result += prefix + '"'+thiskey+'","' + string(so[thiskey]) + '"';
else
result += prefix + '"'+thiskey+'",' + KeyValuePairList(so[thiskey], key, thiskey, xmlvarName) ;
end if
prefix = ",";
hit++;
if ( mod(hit,3) == 0) then
result += "\n\t\t";
end if
end for
result += ")";
else
// No keys, it's a string...
result = '"' + string (so) + '"';
end if
return result;
end
/*
Main recursive function to convert XML to ACF source code.
*/
function convertXMLtoACFrecursive ( XML so, string key, string token, string xmlvarName)
string convertXMLtoACFrecursive;
string result = "", qKey, refName;
string keys = list_keys(so), thisKey;
array string aKeys = explode ( "\r", keys);
int vc = sizeof ( aKeys );
// Handle leaf nodes.
if ( vc == 1 && aKeys[1] == "" ) then
result = xmlvarName+'["'+key+'"] = "'+ string(so)+'";\n';
else
aKeys = explode ( "\r", keys);
end if
int i,j , arrcnt;
result += HandleLeafNodes ( so, Key, aKeys, xmlvarName); // special handling for leaf nodes on this level.
for ( i=1, vc)
thisKey = aKeys[i] ;
if ( thisKey != "") then
qKey = MakeKey (key, thisKey);
arrcnt = checkIfArray( aKeys, thisKey );
if ( arrcnt == 1) then
// dig into the next level
result += convertXMLtoACFrecursive(so[thisKey], qKey, thisKey, xmlvarName);
else /* Handle arrays */
refName = thisKey+"Ref";
result += 'XMLref '+refName+' = FindElement('+xmlvarName+'["'+qKey+'"]);\n';
arrcnt = 0;
for ( j = i, vc)
if ( aKeys[j] == thisKey) then
arrcnt++;
result += refName + '["[]"] = ' + KeyValuePairList (so[thisKey+'['+arrcnt+']'], thiskey, thiskey, xmlvarName)+";\n";
aKeys[j] = ""; // flag the key as "done".
end if
end for
end if
end if
end for
return result;
end
/*
Loop XML a second time, only to pick up the attributes of the nodes.
They should be assigned after the XML has been created.
*/
function makeAttributesDeclarations ( XML so, string key, string token, string xmlvarName)
string makeAttributesDeclarations;
XML attributes;
string result = "", qKey, refName;
string keys = list_keys(so), thisKey;
array string aKeys = explode ( "\r", keys);
int vc = sizeof ( aKeys );
int i,j , arrcnt;
// We have some keys, loop the keys...
for ( i=1, vc)
thisKey = aKeys[i] ;
if ( thisKey != "") then
qKey = MakeKey (key, thisKey);
arrcnt = checkIfArray( aKeys, thisKey );
if ( arrcnt == 1) then
if ( has_XMLattributes (so[thisKey])) then
attributes = XMLattributes(so[thisKey]);
result += "XMLattributes(" + xmlvarName + '["'+qKey+'"], ' +
KeyValuePairList (attributes["root"], thiskey, thiskey, xmlvarName)+"; // one Unique\n";
end if
// dig into the next level
result += makeAttributesDeclarations(so[thisKey], qKey, thisKey, xmlvarName);
else /* Handle arrays */
refName = thisKey+"Ref";
arrcnt = 0;
for ( j = i, vc)
if ( aKeys[j] == thisKey) then
arrcnt++;
if ( has_XMLattributes (so[thisKey+'['+arrcnt+']'])) then
attributes = XMLattributes(so[thisKey+'['+arrcnt+']']);
result += "XMLattributes(" + xmlvarName + '["'+MakeKey(key,thisKey+'['+arrcnt+']')+'"], ' +
KeyValuePairList (attributes["root"], thiskey, thiskey, xmlvarName)+"; // inloop\n";
end if
result += makeAttributesDeclarations (so[thisKey+'['+arrcnt+']'], thisKey+'['+arrcnt+']', thiskey, xmlvarName)+";\n";
aKeys[j] = ""; // flag the key as "done".
end if
end for
end if
end if
end for
return result;
end
/*
Start function for the tool. The parameter is XML source in text format.
The return value is the produced ACF source code.
*/
function ToolCreateACF_fromXML ( string xmlsource )
XML so = xmlsource;
string result, attributes;
result = "XML xmlvar;\n"+convertXMLtoACFrecursive (so, "", "", "xmlVar");
// make a second run for all the attributes.
attributes = makeAttributesDeclarations(so, "", "", "xmlVar");
if ( attributes != "") then
result += "// Assign attributes to the XML...\n";
result += attributes;
end if
return result;
end
1.2. Another Example for 24sevenoffice financial system API ↑
I just looked up the API documentation for this API and found the "SaveInvoice" API documentation.
I just copied their sample and ran it on this tool, I came up with this ACF code:
XML xmlvar;
xmlVar["soap:Envelope.soap:Body.SaveInvoices.invoices.InvoiceOrder"] = XML("CustomerId","1","OrderStatus","Offer");
XMLref InvoiceRowRef = FindElement(xmlvar["soap:Envelope.soap:Body.SaveInvoices.invoices.InvoiceOrder.InvoiceRows.InvoiceRow"]);
InvoiceRowRef["[]"] = XML("ProductId","1","Price","1.00","Name","Test","Quantity","1.00");
InvoiceRowRef["[]"] = XML("ProductId","22","Price","100.00","Name","Test2","Quantity","11.00");
XMLattributes (xmlvar["soap:Envelope"],
XML("xmlns:xsi", "https://www.w3.org/2001/XMLSchema-instance",
"xmlns:xsd", "https://www.w3.org/2001/XMLSchema",
"xmlns:soap", "https://schemas.xmlsoap.org/soap/envelope/"));
XMLattributes (xmlVar["soap:Envelope.soap:Body.SaveInvoices"],
XML ("xmlns", "https://24sevenOffice.com/webservices"));
I also added a second invoice row to it, to demonstrate how easy it is to do just that.
Here is the output:
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<SaveInvoices xmlns="https://24sevenOffice.com/webservices">
<invoices>
<InvoiceOrder>
<CustomerId>1</CustomerId>
<OrderStatus>Offer</OrderStatus>
<InvoiceRows>
<InvoiceRow>
<ProductId>1</ProductId>
<Price>1.00</Price>
<Name>Test</Name>
<Quantity>1.00</Quantity>
</InvoiceRow>
<InvoiceRow>
<ProductId>22</ProductId>
<Price>100.00</Price>
<Name>Test2</Name>
<Quantity>11.00</Quantity>
</InvoiceRow>
</InvoiceRows>
</InvoiceOrder>
</invoices>
</SaveInvoices>
</soap:Body>
</soap:Envelope>
1.3. Do we have some tools for JSON as well? ↑
Yes, we have. In the ACF language, the syntax for manipulating and handling JSON data and XML data is the same, just only different names of the datatypes involved. For the process of this, we just turn the JSON into an XML, run the tool on it, and then do some substitute on the result to make it work for JSON.
The Small starter method, ToolCreateACF_fromXML
can be duplicated and modified. Here is the original method:
/*
Start function for the tool. Parameter is XML source in text format.
Return value is the produced ACF source code.
*/
function ToolCreateACF_fromXML ( string xmlsource )
XML so = xmlsource;
string result;
result = "XML xmlvar;\n"+convertXMLtoACFrecursive (so, "", "", "xmlVar");
return result;
end
Now, we duplicate it and make some minor changes to it, and viola, we have JSON to ACF function as well.
function ToolCreateACF_fromJSON ( string JSONsource )
JSON js = JSONsource;
XML so;
so["root"] = js;
string result;
result = "JSON JsonVar;\n"+convertXMLtoACFrecursive (so, "", "", "JsonVar");
result = substitute ( result, "XML(", "JSON(");
result = substitute ( result, "XMLref ", "JSONref ");
result = substitute ( result, '["root.', '["');
return result;
end
Let us give it a try. I found some examples of JSON on the net. It looks like this:
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
},
"GlossSee": "markup"
}
}
}
}
}
Then we ran this on the new function:
JSON JsonVar;
JsonVar["glossary"] =
JSON("title","example glossary");
JsonVar["glossary.GlossDiv"] =
JSON("title","S");
JsonVar["glossary.GlossDiv.GlossList.GlossEntry"] =
JSON("Abbrev","ISO 8879:1986","Acronym","SGML","GlossSee","markup"
,"GlossTerm","Standard Generalized Markup Language","ID","SGML","SortAs","SGML"
);
JsonVar["glossary.GlossDiv.GlossList.GlossEntry.GlossDef"] =
JSON("para","A meta-markup language, used to create markup languages such as DocBook.");
JSONref GlossSeeAlsoRef = FindElement(JsonVar["glossary.GlossDiv.GlossList.GlossEntry.GlossDef.GlossSeeAlso"]);
GlossSeeAlsoRef["[]"] = "GML";
GlossSeeAlsoRef["[]"] = "XML";
After testing, the ACF code produces the same result. The order of the fields is not the same, that's because we sort it alphabetically to faster query operation in the JSON. It does not matter for any practical use of it.
{
"glossary": {
"GlossDiv": {
"GlossList": {
"GlossEntry": {
"Abbrev": "ISO 8879:1986",
"Acronym": "SGML",
"GlossDef": {
"GlossSeeAlso": [
"GML",
"XML"
],
"para": "A meta-markup language, used to create markup languages such as DocBook."
},
"GlossSee": "markup",
"GlossTerm": "Standard Generalized Markup Language",
"ID": "SGML",
"SortAs": "SGML"
}
},
"title": "S"
},
"title": "example glossary"
}
}
NOTE: Arrays in JSON (using square brackets instead of curly brackets - do not exist in XML. The only way we can identify array notation when we go via XML is to have at least two instances of the array. Then the parser will see there is more than one node with the same tag and will handle it as an array.
If we in the example above change the JSON to have "GlossList as an array, we could change it this way:
{
"glossary": {
"GlossDiv": {
"GlossList": [{
"GlossEntry": {
"Abbrev": "ISO 8879:1986",
"Acronym": "SGML",
"GlossDef": {
"GlossSeeAlso": [
"GML",
"XML"
],
"para": "A meta-markup language, used to create markup languages such as DocBook."
},
"GlossSee": "markup",
"GlossTerm": "Standard Generalized Markup Language",
"ID": "SGML",
"SortAs": "SGML"
}
},{
"GlossEntry": {"Abbrev": "Dummy entry"}
}],
"title": "S"
},
"title": "example glossary"
}
}
As you can see, we have added a second dummy element to the array for our tool to recognize it as an array.
This will produce this ACF code:
JSON JsonVar;
JsonVar["glossary"] =
JSON("title","example glossary");
JsonVar["glossary.GlossDiv"] =
JSON("title","S");
JSONref GlossListRef = FindElement(JsonVar["glossary.GlossDiv.GlossList"]);
GlossListRef["[]"] = JSON("GlossEntry",JSON("Abbrev","ISO 8879:1986","Acronym","SGML","GlossDef",JSON("GlossSeeAlso","GML","GlossSeeAlso","GML","para","A meta-markup language, used to create markup languages such as DocBook."
)
,"GlossSee","markup","GlossTerm","Standard Generalized Markup Language","ID","SGML"
,"SortAs","SGML"));
GlossListRef["[]"] = JSON("GlossEntry",JSON("Abbrev","Dummy entry"));
The dummy entry we added comes on its own line, as can be removed.
1.4. Creating Composite Structures within Arrays in JSON/XML ↑
Take, for instance, a financial export scenario. It's common to encounter multiple "invoice" nodes, each harboring a complex internal structure. If we initially isolate this intricate segment, treating it as a distinct operation, we can efficiently craft these recurring structures into a separate XML variable. Following this, we apply the process to the section encasing the repeating structure.
Once these two components are prepared, we can seamlessly merge them using the empty-bracket notation previously mentioned, culminating in the formation of the complete XML structure.
Consider an export comprising a single root structure, followed by several invoices, each containing multiple invoice lines. The strategy involves first constructing the invoice framework minus the lines. Next, we incorporate the lines through separate operations. Lastly, we assemble the root structure, methodically integrating each invoice into it.
1.4.1. Example composite structure in JSON. ↑
Let's consider a scenario using a glossary. Our goal is to create a separate JSON variable for the inner structure. Initially, I used the development tool to generate a basic structure, replacing "JsonVar" with "GlossEntry". After extracting this from the complete sample, I reran the tool to create the root structure. Finally, I set up a loop to generate three instances of "GlossEntry" within the root structure.
Here's the refined example:
function JsonTest ()
// Declarations....
JSON GlossEntry;
JSON JsonVar;
JSONref ge_GlossSeeAlsoRef;
// Create the surrounding structure....
JsonVar["glossary"] =
JSON("title","example glossary");
JsonVar["glossary.GlossDiv"] =
JSON("title","S");
// Loop to add three elements...
for ( i=1, 3)
GlossEntry = "{}"; // Reset the GlossEntry
GlossEntry["GlossEntry"] =
JSON("Abbrev","ISO 8879:1986-"+string(i),"Acronym","SGML","GlossSee","markup"
,"GlossTerm","Standard Generalized Markup Language","ID","SGML","SortAs","SGML");
GlossEntry["GlossEntry.GlossDef"] =
JSON("para","A meta-markup language, used to create markup languages such as DocBook.");
ge_GlossSeeAlsoRef = FindElement(GlossEntry["GlossEntry.GlossDef.GlossSeeAlso"]);
ge_GlossSeeAlsoRef["[]"] = "GML";
ge_GlossSeeAlsoRef["[]"] = "XML";
// Add it as an array element to the surrounding structure
JsonVar["glossary.GlossDiv.GlossList[]"] = GlossEntry;
end for
return string (JsonVar);
end
That produced this result:
{
"glossary": {
"GlossDiv": {
"GlossList": [
{
"GlossEntry": {
"Abbrev": "ISO 8879:1986-1",
"Acronym": "SGML",
"GlossDef": {
"GlossSeeAlso": [
"GML",
"XML"
],
"para": "A meta-markup language, used to create markup languages such as DocBook."
},
"GlossSee": "markup",
"GlossTerm": "Standard Generalized Markup Language",
"ID": "SGML",
"SortAs": "SGML"
}
},
{
"GlossEntry": {
"Abbrev": "ISO 8879:1986-2",
"Acronym": "SGML",
"GlossDef": {
"GlossSeeAlso": [
"GML",
"XML"
],
"para": "A meta-markup language, used to create markup languages such as DocBook."
},
"GlossSee": "markup",
"GlossTerm": "Standard Generalized Markup Language",
"ID": "SGML",
"SortAs": "SGML"
}
},
{
"GlossEntry": {
"Abbrev": "ISO 8879:1986-3",
"Acronym": "SGML",
"GlossDef": {
"GlossSeeAlso": [
"GML",
"XML"
],
"para": "A meta-markup language, used to create markup languages such as DocBook."
},
"GlossSee": "markup",
"GlossTerm": "Standard Generalized Markup Language",
"ID": "SGML",
"SortAs": "SGML"
}
}
],
"title": "S"
},
"title": "example glossary"
}
}
Enjoy the tool and the examples presented in this section.
Ole K Hornnes