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

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(", ")}

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.

Assigning a variable from another variable

I am trying to assign a variable from another variable. My code looks like this
<#macro ctglink c rhs x y z m e b>
<#assign ctg>
<#if ctgroutes["${y}..${x}-${m}"]??>ctgroutes['${y}..${x}-${m}']
<#elseif ctgroutes["${x}..${y}-${m}"]??>ctgroutes['${x}..${y}-${m}']
<#else>{}</#if>
</#assign>
However, this ctg variable is evaluating to just ctgroutes['227..257-TPPMD04X02'] its not actually evaluating the string itself.
I have tried ?eval, and ?interpret and a bunch of other very hacky things to get this to work, no go. Even the {} is a string
Basically, I need the assign function to work like the old PHP eval() function or something. I am trying to access values in a Map whose keys are derived from the state of the data, so I don't see any easy way to query my Map without evaluating keys.
Update:
I forgot to include the elseif in there
Either way, I tried <#assign ctg = ctgroutes["${y}..${x}-${m}"]!ctgroutes["${x}..${y}-${m}"]> but I get the following error:
Caused by: freemarker.core.InvalidReferenceException: The following has evaluated to null or missing:
==> ctgroutes["${y}..${x}-${m}"]!ctgroutes["${x}..${y}-${m}"] [in template "RouteCompare-WptTable.ftlh" at line 5, column 24]
I would like a null result to just return an empty map, however that doesn't seem possible:
Caused by: java.lang.RuntimeException: freemarker.core.InvalidReferenceException: The following has evaluated to null or missing:
==> ctgroutes["${y}..${x}-${m}"]!ctgroutes["${x}..${y}-${m}"] [in template "RouteCompare-WptTable.ftlh" at line 5, column 24]
So basically, my goal is I need to assign a variable that can take 1 of 3 values:
ctgroutes["${y}..${x}-${m}"] // Assuming it is not null
ctgroutes["${x}..${y}-${m}"] // Assuming it is not null
{} // An empty map
What is the best way to do that?
If I understand well what you want to achieve, you can write it like this:
<#assign ctg = ctgroutes["${y}..${x}-${m}"]!ctgroutes["${x}..${y}-${m}"]!{}>
Also note that <#assign target>...</#assign> is for capturing the output printed between the two tags into the target variable (instead of actually printing it). So target will always store a string or markup value. Also things outside FreeMarker tags and ${} are just static text, and won't be parsed. So, the naive but working approach is just using #if/#elseif/#else and have a separate #assign ctg = ... inside each of them, but you can make this much sorter with the ! operator, as it was shown.

Cannot list a hash on key-value pairs in Freemarker

What's wrong with the following template?
package ${packageName}
public interface ${entityName} {
<#list methods as methodName, map >
public void ${methodName}(${map}) ;
</#list>
}
which gives on version 2.3.23:
freemarker.core.ParseException: Syntax error in template "javaclass.ftl" in line 5, column 29:
Encountered ",", but was expecting:
">"
at freemarker.core.FMParser.generateParseException(FMParser.java:5251)
at freemarker.core.FMParser.jj_consume_token(FMParser.java:5122)
at freemarker.core.FMParser.List(FMParser.java:1431)
at freemarker.core.FMParser.FreemarkerDirective(FMParser.java:2827)
at freemarker.core.FMParser.MixedContent(FMParser.java:3081)
at freemarker.core.FMParser.OptionalBlock(FMParser.java:3253)
at freemarker.core.FMParser.Root(FMParser.java:3432)
at freemarker.template.Template.<init>(Template.java:208)
at freemarker.cache.TemplateCache.loadTemplate(TemplateCache.java:495)
The documentation gives the following example for a hash structure
Listing hashes is very similar, but you need to provide two variable
names after the as; one for the hash key, and another for the
associated value. Assuming products is { "apple": 5, "banana": 10,
"kiwi": 15 }:
<#list products as name, price>
<p>${name}: ${price}
</#list>
<p>apple: 5
<p>banan: 10
<p>kiwi: 15
Note that my example is before submitting content.
That is expected since listing key-value was added in 2.3.25.
http://freemarker.org/docs/ref_directive_list.html#ref.directive.list
... and to list the key-value pairs of a hash (since 2.3.25):
<#list hash as key, value>
Part repeated for each key-value pair
</#list>
So, upgrade if you can or rewrite your list.
See also:
Freemarker iterating over hashmap keys

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.

checking if a map contains a value in Freemarker Template

I'm trying to check if a map contains a value to conditionally execute some freemarker code. This is what I've got so far:
<#if productLayout.layoutWidgetConfiguration[pos.id]??>
<#assign configId>${productLayout.layoutWidgetConfiguration[pos.id]}</#assign>
<#else>
<#assign configId></#assign>
</#if>
But I get this error, which basically fails the if condition.
Error executing FreeMarker template freemarker.core.UnexpectedTypeException: For "...[...]" left-hand operand: Expected a sequence or string (or something that's implicitly convertible to string), but this evaluated to an extended_hash (wrapper: f.t.SimpleHash):
==> productLayout.layoutWidgetConfiguration [in template "admin/pages/catalog/products/partials/productLayoutEditorRefreshZone.ftl" at line 7, column 22]
The failing instruction (print stack trace for 9 more):
==> #if productLayout.layoutWidgetConfigu... [in template "admin/pages/catalog/products/partials/productLayoutEditorRefreshZone.ftl" at line 7, column 17]
at freemarker.core.DynamicKeyName.dealWithNumericalKey(DynamicKeyName.java:141) ~[DynamicKeyName.class:2.3.20]
How can I check if a value exists in a map in a freemarker template?
Update Here:
It seems the hash doesn't like a Long key value if I change it to this, the if check works, but the value doesn't get retrieved even when it exists - so I guess the question now is how to retrieve a value from a hash with a java.lang.Long key?
<#assign configId = "">
<#if productLayout.layoutWidgetConfiguration[pos.id?string]?has_content>
Hello
<#assign configId = productLayout.layoutWidgetConfiguration[pos.id?string]>
</#if>
<h1>${pos.id}</h1>
[] only supports string hash (Map, etc.) keys and numerical sequence (List, array, etc.) indexes. For now the solution is not using [] for Map-s with non-String keys. You can use the Java API of the object instead, like myMap?api.get(nonStringKey), etc. Note that ?api has to be allowed in the configuration; see http://freemarker.org/docs/ref_builtins_expert.html#ref_buitin_api_and_has_api for more.

Resources