freemarker functions vs macros - freemarker

Hello freemarkers gurus
I understand that the difference between freemarker functions and macros is that macros can print to the output, but cannot return values, while functions can return values but cannot print to the output.
Well, I am having a problem because I need both to print and return values:
I am doing recursive tree exploration with freemarker and therefore I have a macro being called recurvively. As the tree is being explored , I need both to print node information to the output, but also compute and return statistics about the nodes explored ( for example the sum of a specific property of the nodes explored)
If I use macro being called recurvively I can print node information but cannot return the statistics to the calling entity.
If I use a function recursively called, I can return the statistics but cannot print node information on the output.
One solution could be to explore the tree twice, once to print node informations and another to collect statistics, but I would hate to use this unelegant solution.
Can someone propose a better solution?
Thanks

Or you can even use a global variable as storage for your stats:
<#global stats = [] />
<#-- then when you call your function -->
<#assign = method() />
<#function method param = "">
<#-- do something and before you return you push the stats to the global variable, if you choose my approach of "merging" sequences, be careful that you wrap the new stats item also in a sequence or it will fail miserably =) -->
<#global stats = stats + [{"statvar1": 10, "statvar2": 30}] />
<#return whateveryoulike />
</#function>

You can store the statistics in a non-#local variable. Like in the macro you do <#assign treeStats = ...> and then on call-site:
<#import my="myutils.ftl">
...
<#my.tree input />
<#assign stats = my.treeStats /> <#-- or whatever you want with my.treeStats -->
Yeah, it's awkward, but FreeMarker has no out-params to return a secondary result. Actually, you could do a hack with loop-variables, but it's maybe too confusing, plus if you really need a body, you can't use this trick:
<#my.tree input; res><#assign stats = res></#>

Related

Assigning empty string if XML node doesn't exist in Freemarker

I have an XML document passed as root to a Freemarker template. I want some values from this XML to be assigned to variables as a string and later concatenate/print them out.
<#assign MyVar = root.child1.child2.child3.mynode>
The issue here is that even when a path doesn't exist MyVar gets assigned with a sequence+hash which cannot be printed out or converted to string. This variable although returns false for ?has_content, it needs an extra step for these checks and I have this same issue with many variables and across template files and modules.
The only solution I have been able to find was
<#assign MyVar = root.child1.child2.child3.mynode>
<#assign MyVar = MyVar ?has_content?then(MyVar , "")>
I am looking for something like the Default Value Operator which also checks for nulls like ?has_content.
Does Freemarker provide any simpler one line function to check if a variable has no content and assign it with a default?
In short:
<#assign myVar = root.child1.child2.child3.mynode[0]!''>
Or just <#assign myVar = root.child1.child2.child3.mynode[0]!> if the implicit multi-typed default value doesn't cause problems (like when you just print it with ${}).
Why: XML queries (just like XPath queries) always return a sequence of matching nodes. There are maybe 0 such nodes (or 1, or multiple). An empty sequence is not a "missing value" according the template language. It's an inconvenient mismatch with the XML data-model. But while the sequence always exists, its 1st element ([0]) doesn't, so you can use all the missing value handler operators with it as usual.

Looping through NetSuite "objects" with FreeMarker

I am struggling to deal with a few aspects of the data being "passed" to Advanced PDF Templates in NetSuite and the fact there is no "object browser". I have seen, using:
<#list .data_model?keys as key>
${key} = ${.data_model[key]}<br />
</#list>
that there are data "objects":
companyinformation =
message =
nsfont =
preferences =
record =
record#title = Invoice
subsidiary =
subsidiary#title = Subsidiary
user =
Is there anyway to look deeper into each of these objects to see their properties?
You can dump the properties of the objects on the same way, that is, by iterating through their ?keys, because it's not only for Map-s, but for any value that has named subvariables. (Well, assuming the ObjectWrapper in the FreeMarker configuration is like that, but let's hope it is for now.)
Because this is going to be recursive, you will want to use a #macro. Be careful with infinite recursion though (usually, you want a maximum depth at least).
If FreeMarker is at least 2.3.25, you can also write <#list something as key, value>, which is nicer, and supports non-string keys.

How to refer to another instance in the iterate of the XForms action element?

I am using an XForms action along with iterate. The iterate selects a set (using XPath) of nodes and repeats the action for it.The problem is I have multiple conditions for selecting the node set.
There should not be a readOnly node.
Should not be part of the ignoreProperties list (this list is in another instance).
Code:
<xf:action ev:event="setValues" iterate="
instance('allProps')/props/prop[
not(readOnly) and
not(instance('ignoreProperties')/ignoredProperties/property[text() = name]
]
">
The first condition not(readOnly) works. But the second condition does not work. I feel there is some problem with the context of the XPath nodes.
How should I replace the second condition to achieve the result ?
The target XML is a simple ignoredProperties document:
<ignoredProperties>
<property>c_name</property>
<property>c_tel_no</property>
</ignoredProperties>
This should work:
<xf:action ev:event="setValues" iterate="
instance('allProps')/props/prop[
not(readOnly) and
not(name = instance('ignoreProperties')/ignoredProperties/property)
]
">
The = operator works against multiple nodes, returning all the ones that match. With not() you can express that you don't want a match.
Explicitly selecting .../property/text() will not be necessary.
There seems to be something wrong with your calls to instance(). If you have:
<xf:instance id="ignoredProperties">
<ignoredProperties>
<property>c_name</property>
<property>c_tel_no</property>
</ignoredProperties>
</xf:instance>
Then instance('ignoredProperties') returns the <ignoredProperties> element. So you should write:
<xf:action ev:event="setValues" iterate="
instance('allProps')/prop[
not(readOnly) and
not(instance('ignoreProperties')/property[text() = name])
]
">
This also assumes your allProps instance has a <props> root element.
Further, the second condition appears wrong, as already shown in another answer. Write instead:
not(name = instance('ignoreProperties')/property)
In XPath 2, you could clarify that your not() are testing on node existence by using empty() instead:
<xf:action ev:event="setValues" iterate="
instance('allProps')/prop[
empty(readOnly) and
not(name = instance('ignoreProperties')/property)
]
">

How to do a projection on Freemarker sequences to extract a property?

Let's say I have some object container in a freemarker variable and container.content gives me a sequence of objects (I will call them "things") with names and a String getName() accessor. I would like to produce a comma-separated list of the names from the container.content sequence.
If I already had a sequence of names instead of a sequence of things with names, I could simply do names?join(", "). Is there something concise to extract the .name-s from container.content and join them afterwards? More generally, I am looking for a functional programming map (collect, projection) operation, but did not find one in the docs.
What I have tried for now:
Currently, I have <#list container.content as x>${x.name}<#if x?has_next>, </#if></#list> to reproduce that map-then-join operation, but I find that rather verbose and it looks like a smell to me to have basically reimplemented join.
Previously I had container.content?join(", ") and I got "Thing[name=A, otherStuff=...], Thing[name=B, otherStuff=...]" instead of "A, B", of course. I do not wish to modify that Thing#toString method to only return the name instead. I would like to keep that detailed representation for debugging purposes.
You can create function which will extract key values from input sequence:
<#function map seq key>
<#assign result = []>
<#list seq as item>
<#assign result = result + [item[key]]>
</#list>
<#return result>
</#function>
And then use it like that:
${map(container.content, "name")?join(", ")}

Inserting a child node when list is empty (XForms)

My problem is the following :
I usually have those data:
<structures>
<structure id="10">
<code>XXX</code>
</structure>
</structures>
so the table I display (single columns : code) is ok.
But in some cases, the data is the result a a query with no content, so the data is:
<structures/>
resulting in my table not displaying + error.
I am trying to insert, in the case of an empty instance, a single node so that the data would look like:
<structures>
<structure id="0"/>
</structures>
I am trying something like that :
<xforms:action ev:event="xforms-submit-done">
<xforms:insert if="0 = count(instance('{./instance-name}')/root/node())" context="instance('{./instance-name}')/root/node()" origin="xforms:element('structure', '')" />
</xforms:action>
but no node inserted when I look at the data in the inspector in the page.
Any obvious thing I am doing wrong?
There seems to be erros in your XPath if and context expressions:
if="0 = count(instance('{./instance-name}')/root/node())"
context="instance('{./instance-name}')/root/node()"
You are a using curly brackets { and }, I assume to have the behavior of attribute value templates (AVTs). But the if and context expressions are already XPath expressions, so you cannot use AVTs in them. Try instead:
if="0 = count(instance(instance-name)/root/node())"
context="instance(instance-name)/root/node()"
Also, the instance-name path is relative to something which might not be clear when reading or writing the expression. I would suggest using an absolute path for example instance('foo')/instance-name to make things clearer.
You don't provide the structure of the other instances, so I can tell for sure, but you'll expression above suppose that they have the form:
<xf:instance id="foo">
<some-root-element>
<root>
<structure/>
</root>
<some-root-element>
</xf:instance>
I don't know if that's what you intend.
Finally, you could replace count(something) = 0, with empty(something).

Resources