checking if a map contains a value in Freemarker Template - freemarker

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.

Related

Problem detecting and using a nullable value in Freemarker

I have a POJO object that I have serialized from JSON (in Java). I am using an object wrapper constructed via:
DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_27);
builder.setExposeFields(true);
objectWrapper = builder.build();
I use the setExposeFields(true) because the object I am wrapping is not a Java bean, but rather just a POJO that contains public fields.
I am doing the following in my template:
<#ConditionOccurrence co = c/>
<#macro ConditionOccurrence co>
<#list co?keys as key>
${key}
</#list>
${co.occurrenceStartDate!'wtf'}
${co["occurrenceStartDate"]}
A condition occurrence of: ${codesetName(co.codesetId, "any condition")}
<#if co.first!false>- for the first time in the person's history</#if>
<#if (co["occurrenceStartDate"])??>co.OSD is null: </#if>
</#macro>
Note, the 'c' is an element in a sequence, and is not important to the exact problem I am having.
The output of the template shows this:
stopReason
getClass
gender
CorrelatedCriteria
providerSpecialty
occurrenceStartDate
occurrenceEndDate
visitType
accept
codesetId
hashCode
conditionSourceConcept
equals
conditionType
toString
conditionTypeExclude
class
first
age
org.ohdsi.circe.cohortdefinition.DateRange#68e62ca4
org.ohdsi.circe.cohortdefinition.DateRange#68e62ca4
A condition occurrence of: Psoriasis
- for the first time in the person's history
co.OSD is null:
The first set of lines are all the keys in my POJO. This is correct.
the two lines of output:
org.ohdsi.circe.cohortdefinition.DateRange#68e62ca4
org.ohdsi.circe.cohortdefinition.DateRange#68e62ca4
This is showing that the field occurrenceStartDate is an object of type DateRange. note this could be null in some cases, so I am checking how to check for null...
The next part of the output:
- for the first time in the person's history
co.OSD is null:
This is showing that it is reading the 'first' attribute of the object correctly, and I have switched the raw JSON from 'true' to 'false' and the template responds properly to the change in this value. Note, in the object, the 'first' field is type Boolean.
The second line: co.OSD is null is what is confounding me. I confirmed earlier that outputting the 'occurrenceStartDate' field shows that it holds a DateRange object. But, this statement is evaluating to TRUE (ie: it is null):
#if (co["occurrenceStartDate"])??>co.OSD is null: </#if>
I have tried with both dot notation and bracket notation. For some reason, the ?? operator on that field is saying it is null. Note, the underlying object isn't a simple String or Number type, it is a simple POJO class DateRange with 3 String properties on it. Again, these are not JavaBeans, these are just POJOs.
Can anyone explain why the ?? operator says it is empty when it is clearly referencing an object? Btw: if I attempt to access co.occurrenceStartDate at all, it results in a template error that I'm referencing a null value, so the core problem here is why does the wrapper thing it is a null?
Thank you in advance for your help.
The ?? operator means "is present", not "is missing". So your line should be:
<#if !(co.occurrenceStartDate??)>co.OSD is null: </#if>

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.

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

Iterating over a map of Object: List in Freemarker

I'm trying to iterate in a freemarker template over a HashMap<SeapSubscription, List<PiNotice>>.
The map doesn't contain any nulls (in keys or values).
The code in Freemarker is:
<#list subscriptionsWithPiNotices?keys as s>
${s.title}
<#list subscriptionsWithPiNotices[s] as piNotice>
Autoritate contractanta: ${piNotice.contractingAuthorityName}
.
.
</#list>
</#list>
If I remove the iteration from the second list (<#list subscriptionsWithPiNotices[s] as piNotice>) it all works (that is iterating over the map keys, but when I add the second part, trying to iterate over the map-s value, i get a Null / missing exception
FreeMarker template error: The following has evaluated to null or missing:
==> subscriptionsWithPiNotices[s] [in template "seap-subscription-newsletter.ftl" at line 21, column 16]
Tip: If the failing expression is known to be legally null/missing,
either specify a default value with myOptionalVar!myDefault, or use
<#if myOptionalVar??>when-present<#else>when-missing. (These
only cover the last step of the expression; to cover the whole
expression, use parenthessis: (myOptionVar.foo)!myDefault,
(myOptionVar.foo)??
The failing instruction (FTL stack trace):
==> #list subscriptionsWithPiNotices[s] a... [in template "seap-subscription-newsletter.ftl" at line 21, column 9]
#list subscriptionsWithPiNotices?keys... [in template "seap-subscription-newsletter.ftl" at line 18, column 5]
I repeat, I dumped that HashMap, and it only has one key with one ArrayList having one item inside. So there's no reason to report a null, is it ?
Finally i managed to solve the issue, by configuring a
BeansWrapper bw = BeansWrapper.getDefaultInstance();
bw.setSimpleMapWrapper(true);
cfg.setObjectWrapper(bw);
and then using the
map(key) syntax instead of map[key] or map.key. It seems to work with any type of key class.
So this is the old issue that the [] operator only supports string keys. Since 2.3.22 you can use someMap?api.get(someNonStringKey) to work that around. It needs some configuring to enable, but nothing that breaks an existing application. See this answer or this FAQ entry.

Resources