Freemarker parse a String as Json - freemarker

Probably it's not possible,
but I would like to transform a json string in a map with freemarker
ex:
<#assign test = "{\"foo\":\"bar\", \"f\":4, \"text\":\"bla bla\"}">
and be able to get the text key from this string

Use ?eval_json (requires FreeMarker 2.3.31):
<#-- Using '...' instead of "..." for convenience: no need for \" escapes this way. -->
<#assign test = '{"foo":"bar", "f":4, "text":"bla bla"}'>
<#assign m = test?eval_json>
${m.foo} <#-- prints: bar -->
<#-- Dump the whole map: -->
<#list m as k, v>
${k} => ${v}
</#list>
Before 2.3.31, ?eval was popular for this purpose, but that actually expects FreeMarker expressions. That's a problem because it doesn't support null, or \uXXXX escapes (so parsing of such JSON will fail). Also it can be a security problem, because it supports accessing variables, and calling methods/functions, while JSON doesn't.

freemarker.sourceforge.net/docs/pgui_datamodel_method.html
in code:
// a class to parse Json, just add this method to your rendered template data
// with data.put("JsonParser", new FreemarkerJsonParser());
// or in shared variables http://freemarker.sourceforge.net/docs/pgui_config_sharedvariables.html
public class FreemarkerJsonParser implements TemplateMethodModel{
#Override
public Object exec(List args) throws TemplateModelException {
return new Gson().fromJson(s, new TypeToken<Map<String, String>>() {}.getType());((String) args.get(0));
}
}
in the template:
<#assign map = JsonParser("{\"foo\":\"bar\", \"f\":4, \"text\":\"bla bla\"}")>
${map.text}

Sounds like you need to define/implement a template that reads JSON.

Related

Change the scalar style used for all multi-line strings when serialising a dynamic model using YamlDotNet

I am using the following code snippet to serialise a dynamic model of a project to a string (which is eventually exported to a YAML file).
dynamic exportModel = exportModelConvertor.ToDynamicModel(project);
var serializerBuilder = new SerializerBuilder();
var serializer = serializerBuilder.EmitDefaults().DisableAliases().Build();
using (var sw = new StringWriter())
{
serializer.Serialize(sw, exportModel);
string result = sw.ToString();
}
Any multi-line strings such as the following:
propertyName = "One line of text
followed by another line
and another line"
are exported in the following format:
propertyName: >
One line of text
followed by another line
and another line
Note the extra (unwanted) line breaks.
According to this YAML Multiline guide, the format used here is the folded block scalar style. Is there a way using YamlDotNet to change the style of this output for all multi-line string properties to literal block scalar style or one of the flow scalar styles?
The YamlDotNet documentation shows how to apply ScalarStyle.DoubleQuoted to a particular property using WithAttributeOverride but this requires a class name and the model to be serialised is dynamic. This also requires listing every property to change (of which there are many). I would like to change the style for all multi-line string properties at once.
To answer my own question, I've now worked out how to do this by deriving from the ChainedEventEmitter class and overriding void Emit(ScalarEventInfo eventInfo, IEmitter emitter). See code sample below.
public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter
{
public MultilineScalarFlowStyleEmitter(IEventEmitter nextEmitter)
: base(nextEmitter) { }
public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter)
{
if (typeof(string).IsAssignableFrom(eventInfo.Source.Type))
{
string value = eventInfo.Source.Value as string;
if (!string.IsNullOrEmpty(value))
{
bool isMultiLine = value.IndexOfAny(new char[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0;
if (isMultiLine)
eventInfo = new ScalarEventInfo(eventInfo.Source)
{
Style = ScalarStyle.Literal
};
}
}
nextEmitter.Emit(eventInfo, emitter);
}
}

Iterating over HashMap in Freemarker template displays map's methods

In Apache OfBiz application, I have such code in controller:
public static String runRequest(HttpServletRequest request, HttpServletResponse response) {
Map<String, Long> typesToCount = getTypesToCount();
request.setAttribute("types", typesToCount);
return HttpFinals.RETURN_SUCCESS;
}
And in freemarker template it's processed/iterated like so:
<table
<#list requestAttributes.types as key, value>
<tr>
<td>${key}</td>
<td>${value}</td>
</tr>
</#list>
</table>
On rendered html page I'm getting both actual map's string key's and map's methods names (put, remove, add etc.).
As for values they are not rendered at all the with following error:
FreeMarker template error: For "${...}" content: Expected a string or something automatically convertible to string (number, date or boolean), or "template output" , but this has evaluated to a method+sequence (wrapper: f.e.b.SimpleMethodModel)
I'm using freemarker 2.3.28
Map.entrySet() method returns a collection (Set<Map.Entry<K, V>>) of the mappings contained in this map. So we can iterate over key-value pair using getKey() and getValue() methods of Map.Entry<K, V>. This method is most common and should be used if you need both map keys and values in the loop.
Try this code to iterate through the values in FTL
<table>
<#list requestAttributes.entrySet() as requestAttribute>
<tr>
<td>${requestAttribute.getKey()}</td>
<td>${requestAttribute.getValue()}</td>
</tr>
</#list>
</table>
Basically, I managed to iterate through the map only after wrapping it in SimpleMapModel like so:
public static String runRequest(HttpServletRequest request, HttpServletResponse response) {
Map<String, Long> typesToCount = getTypesToCount();
request.setAttribute("types", new SimpleMapModel(typesToCount, new DefaultObjectWrapper())));
return HttpFinals.RETURN_SUCCESS;
}
and int ftl template:
<#list requestAttributes.types?keys as key>
<tr>
<td>${key}</td>
<td>${requestAttributes.types[key]}</td>
</tr>
</#list>
That works like that if FreeMarker is configured to use a pure BeansWrapper (as opposed to DefaultObjectWrapper) for its object_wrapper setting, and the BeansWrapper.simpleMapWrapper property is left on its default value, false. Needless to say, it's a quite problematic configuration to work with. Hopefully it isn't the default of OfBiz. Although for old frameworks this can happen, as many many years ago this was a way of working around some limitations of FreeMarker, since this way you can just use the Java API of Map. Later, the introduction of the ?api built-in has made this hack needless.

Abbreviating text in Freemarker

I want to achieve the equivalent of the StringUtils.abbreviate(String, int) method in Freemarker.
I am generating per-user SMS text using Freemarker templates. Most of the text is static but it contains fields that have a maximum character size. For these fields I need to be able to do the equivalent of:
Hello, ${abbrev(userName, 15)}, welcome to the wonderful world of example.com.
Keep in mind: this is just an example.
As a first cut, I tried doing it using this sample as a guide:
public class AbbreviateMethod implements TemplateMethodModelEx {
#Override
public Object exec(#SuppressWarnings("rawtypes") List arguments) throws TemplateModelException {
if (arguments.size() < 2)
throw new TemplateModelException("Needs 2 parameters");
return (abbreviate((String) arguments.get(0), ((Integer) arguments.get(1)).intValue()));
}
}
but I get the exception:
java.lang.ClassCastException: freemarker.template.SimpleScalar cannot be cast to java.lang.String
at com.example.util.AbbreviateMethod.exec(AbbreviateMethod.java:26)
at freemarker.core.MethodCall._eval(MethodCall.java:65)
at freemarker.core.Expression.eval(Expression.java:81)
at freemarker.core.DollarVariable.calculateInterpolatedStringOrMarkup(DollarVariable.java:96)
at freemarker.core.DollarVariable.accept(DollarVariable.java:59)
at freemarker.core.Environment.visit(Environment.java:327)
at freemarker.core.Environment.visit(Environment.java:333)
at freemarker.core.Environment.process(Environment.java:306)
Any ideas?
You need to explicit call SimpleScalar toString() to convert to String
Change your line to :
return (abbreviate(arguments.get(0).toString(), ((Integer) arguments.get(1)).intValue()));

In Freemarker, how to translate each string in a list individually, then join the result

My model contains a list of two-letter language codes, eg (pseudo-code):
${info.languages} = [en, jp, mi]
I currently have mark-up in my template that formats these as a semicolon-space-separated list:
<#if info.languages??>
${info.languages?join("; ", "")}
</#if>
which gives
en; jp; mi
I'd like to show the English name for each language code in the semicolon-separated list instead. I know I can use Locale#getDisplayLanguage to do the lookup in Java, so I'm not worried about the actual translation part.
My question is how to tie this into the template while still taking advantage of the join built-in. I guess ideally I'd want to be able to chain the operators like so:
${info.languages?displayLanguage?join(", ", "")}
but it appears that the ?xyz syntax is reserved for core built-ins.
Tl;dr: Is there any way to combine a custom function with the join built-in? Or something else useful that I'm overlooking? Or is my only choice to have my custom function do the joining as well as the translation?
?join pretty much only exists for convenience, to address the most common case. In more generic cases you should use #list. For example:
<#list info.languages as lang>${my.displayLanguage(lang)}<#sep>, <#/list>
Of course if you do this on multiple places, you should probably move this snippet into a macro.
As of the ?xyz thing (they are called built-ins), yes, it's reserved for the template language.
Questions on where this sort of processing belongs aside, I figured out how to do what I described in my question.
In order to still use the ?join operator (since it really is the most common case in terms of joining the values in the list), just make sure the custom method returns a list of display names for the languages codes that were passed in.
The following works, assuming that the argument passed in is always a list:
public class DisplayLanguageMethod implements TemplateMethodModelEx {
#Override
public Object exec(List arguments) throws TemplateModelException {
if (arguments == null || arguments.isEmpty()) {
return null;
}
Object argObject = arguments.get(0);
if (argObject == null || !(argObject instanceof TemplateSequenceModel)) {
return Collections.emptyList();
}
TemplateSequenceModel argSequence = ((TemplateSequenceModel) argObject);
List<String> displayLanguages = new ArrayList<>();
for (int i = 0; i < argSequence.size(); i++) {
String languageCode = Objects.toString(argSequence.get(i), null);
String displayName = languageCode;
if (languageCode != null) {
Locale argLocale = Locale.forLanguageTag(languageCode);
if (argLocale != null) {
displayName = argLocale.getDisplayName(Locale.getDefault());
}
}
displayLanguages.add(displayName);
}
return displayLanguages;
}
}
placed into the context like this:
Map<String, Object> data = new HashMap<>();
data.put("displayLanguage", new DisplayLanguageMethod());
// get template, output writer etc as per usual
template.process(data, writer);
and then used in the template like so:
<#if info.languages??>
${displayLanguage(info.languages)?join("; ", "")}
</#if>

In freemarker is it possible to check to see if a file exists before including it?

We are trying to build a system in freemarker where extension files can be optionally added to replace blocks of the standard template.
We have gotten to this point
<#attempt>
<#include "extension.ftl">
<#recover>
Standard output
</#attempt>
So - if the extension.ftl file exists it will be used otherwise the part inside of the recover block is output.
The problem with this is that freemarker always logs the error that caused the recover block to trigger.
So we need one of two things:
Don't call the include if the file doesn't exist - thus the need to check for file existence.
-OR-
A way to prevent the logging of the error inside the recover block without changing the logging to prevent ALL freemarker errors from showing up.
easier solution would be:
<#attempt>
<#import xyz.ftl>
your_code_here
<#recover>
</#attempt>
We've written a custom macro which solves this for us. In early testing, it works well. To include it, add something like this (where mm is a Spring ModelMap):
mm.addAttribute(IncludeIfExistsMacro.MACRO_NAME, new IncludeIfExistsMacro());
import java.io.IOException;
import java.util.Map;
import org.apache.commons.io.FilenameUtils;
import freemarker.cache.TemplateCache;
import freemarker.cache.TemplateLoader;
import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
/**
* This macro optionally includes the template given by path. If the template isn't found, it doesn't
* throw an exception; instead it renders the nested content (if any).
*
* For example:
* <#include_if_exists path="module/${behavior}.ftl">
* <#include "default.ftl">
* </#include_if_exists>
*
* #param path the path of the template to be included if it exists
* #param nested (optional) body could be include directive or any other block of code
*/
public class IncludeIfExistsMacro implements TemplateDirectiveModel {
private static final String PATH_PARAM = "path";
public static final String MACRO_NAME = "include_if_exists";
#Override
public void execute(Environment environment, Map map, TemplateModel[] templateModel,
TemplateDirectiveBody directiveBody) throws TemplateException, IOException {
if (! map.containsKey(PATH_PARAM)) {
throw new RuntimeException("missing required parameter '" + PATH_PARAM + "' for macro " + MACRO_NAME);
}
// get the current template's parent directory to use when searching for relative paths
final String currentTemplateName = environment.getTemplate().getName();
final String currentTemplateDir = FilenameUtils.getPath(currentTemplateName);
// look up the path relative to the current working directory (this also works for absolute paths)
final String path = map.get(PATH_PARAM).toString();
final String fullTemplatePath = TemplateCache.getFullTemplatePath(environment, currentTemplateDir, path);
TemplateLoader templateLoader = environment.getConfiguration().getTemplateLoader();
if (templateLoader.findTemplateSource(fullTemplatePath) != null) {
// include the template for the path, if it's found
environment.include(environment.getTemplateForInclusion(fullTemplatePath, null, true));
} else {
// otherwise render the nested content, if there is any
if (directiveBody != null) {
directiveBody.render(environment.getOut());
}
}
}
}
I had this exact need as well but I didn't want to use FreeMarker's ObjectConstructor (it felt too much like a scriptlet for my taste).
I wrote a custom FileTemplateLoader:
public class CustomFileTemplateLoader
extends FileTemplateLoader {
private static final String STUB_FTL = "/tools/empty.ftl";
public CustomFileTemplateLoader(File baseDir) throws IOException {
super(baseDir);
}
#Override
public Object findTemplateSource(String name) throws IOException {
Object result = null;
if (name.startsWith("optional:")) {
result = super.findTemplateSource(name.replace("optional:", ""));
if (result == null) {
result = super.findTemplateSource(STUB_FTL);
}
}
if (result == null) {
result = super.findTemplateSource(name);
}
return result;
}
}
And my corresponding FreeMarker macro:
<#macro optional_include name>
<#include "/optional:" + name>
</#macro>
An empty FTL file was required (/tools/empty.ftl) which just contains a comment explaining its existence.
The result is that an "optional" include will just include this empty FTL if the requested FTL cannot be found.
You can use also use Java method to check file exist or not.
Java Method-
public static boolean checkFileExistance(String filePath){
File tmpDir = new File(filePath);
boolean exists = tmpDir.exists();
return exists;
}
Freemarker Code-
<#assign fileExists = (Static["ClassName"].checkFileExistance("Filename"))?c/>
<#if fileExists = "true">
<#include "/home/demo.ftl"/>
<#else>
<#include "/home/index.ftl">
</#if>
Try this to get the base path:
<#assign objectConstructor = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign file = objectConstructor("java.io.File","")>
<#assign path = file.getAbsolutePath()>
<script type="text/javascript">
alert("${path?string}");
</script>
Then this to walk the directory structure:
<#assign objectConstructor = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign file = objectConstructor("java.io.File","target/test.ftl")>
<#assign exist = file.exists()>
<script type="text/javascript">
alert("${exist?string}");
</script>

Resources