I need to create a Freemarker directive that will pass the unprocessed content of the tag to a java method. The purpose is to load default template fragments into a DB so they can be examined and edited through another process, then output the result of the edited content if any. I have created a macro that almost does this, the problem is that the template fragment is processed before being passed to the java method.
Here is my current code:
<#macro process name>
<#local content><#nested/></#local>
${myjava.processContent(name, content)}
</#macro>
<@process 'this-content-name'><p>This is some content about ${companyName}</p></@process>
Java:
//Add and instance of this class to the Freemarker data model for use in templates.
public class MyJava {
public String processContent(String name, String content) {
log.debug("Data To Process: name = {}, content = {}", name, content);
String contentToOutput = getFromDb(name);
if(contentToOutput == null) {
insertContentInDb(name, content);
contentToOutput = content;
}
TemplateModel model = Environment.getCurrentEnvironment().getDataModel();
Configuration config = Environment.getCurrentEnvironment().getConfiguration();
Template template = new Template("template-from-content", contentToOutput, config);
return FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
}
}
When running the above I need to see the following logged:
Data To Process: name = this-content-name, content = <p>This is some content about ${companyName}</p>
Unfortunately I see this:
Data To Process: name = this-content-name, content = <p>This is some content about My Company Name</p>
The problem is clearly that the <#nested/>
tag is processed by the template engine before it is stored in the local variable in my macro.
Looking into the Freemarker source code I see that Environment
has invokeNestedContent()
that appears to handle processing the content of the macro and appears to have methods for retrieving the raw content of the tag, i.e. <p>This is some content about ${companyName}</p>
. Unfortunately those methods have package-private access and are not accessible from my code.
Is there another way to retrieve the RAW unprocessed content of a custom directive in Freemarker?
It seems like this would be very useful functionality for building content management systems.
Based on the suggestion in @ddekany's answer I was able to come up with two potential solutions which each have some trade offs.
Using TemplateDirectiveModel
and Environment.getCurrentDirectiveCallPlace()
instead of a macro. The downside of this method is that I am not able to access the containing page's data_model when rendering the TemplateDirectiveModel
. Additionally, the content retreived includes the full tag, not just its nested content so I have to parse that out.
Implementing a freemarker utility class in package freemarker.core
inside my own source tree. This allows me to access package-private methods, but is obviously not supported by Freemarker.
Option 1 Java:
@Slf4j
public class ContentTemplateDirective implements TemplateDirectiveModel {
private HostData hostDataService;
public ContentTemplateDirective(HostData hostDataService) {
this.hostDataService = hostDataService;
}
@Override
public void execute(Environment environment, Map params, TemplateModel[] loopVars,
TemplateDirectiveBody templateDirectiveBody) throws TemplateException, IOException {
if (!params.containsKey("name")) {
throw new TemplateModelException(
"This directive requires a 'name' parameter.");
}
if (loopVars.length != 0) {
throw new TemplateModelException(
"This directive doesn't allow loop variables.");
}
String name = params.get("name").toString();
log.debug("name = {}", name);
String content = getRawDirectiveContent(true);
content = hostDataService.removeTag(content, "<@content>");
log.debug("content = {}", content);
//This looks for an Element with this name in the DB. If it is not found it creates one with this content.
//If it is found it overrides this content with the content from the DB.
content = hostDataService.processElement(name, content, true);
Configuration config = environment.getConfiguration();
TemplateModel model = environment.getDataModel();
Template template = new Template("string-template", content, config);
StringWriter result = new StringWriter(1024);
template.process(model, result);
String output = result.toString();
//Write the output.
environment.getOut().write(output);
}
public static String getRawDirectiveContent(boolean trim) {
log.debug("Getting Raw Directive Content");
DirectiveCallPlace dcp = Environment.getCurrentEnvironment().getCurrentDirectiveCallPlace();
String content = dcp.getTemplate().getSource(dcp.getBeginColumn(), dcp.getBeginLine(), dcp.getEndColumn(), dcp.getEndLine());
return trim ? content.trim() : content;
}
}
Option 1 FTL:
<#assign content = "com.myapp.ContentTemplateDirective"?new(hostDataService)>
<!-- Does not work. Fails to find testValue when evaluating the content. -->
<#assign testValue>Some Test Value</#assign>
<@content name='page.h2'><h2>This is the default content. Here is a test value: ${testValue}</h2></@content>
<!-- Works, but the testValue has to be assigned inside the <@content> tag so can't access externally assigned model attributes. -->
<@content name='page.h2'>
<#assign testValue>Some Test Value</#assign>
<h2>This is the default content. Here is a test value: ${testValue}</h2></@content>
Option 2 Java (in package freemarker.core):
public class MyFreemarkerUtil {
public static String getRawMacroContent(boolean trim) {
log.debug("Getting Raw Macro Content");
StringBuilder sb = new StringBuilder();
Environment environment = Environment.getCurrentEnvironment();
Macro.Context invokingMacroContext = environment.getCurrentMacroContext();
TemplateObject callPlace = invokingMacroContext.callPlace;
TemplateElement[] nestedContentBuffer = callPlace instanceof TemplateElement ? ((TemplateElement) callPlace).getChildBuffer() : null;
if (nestedContentBuffer != null) {
for (TemplateElement templateElement : nestedContentBuffer) {
log.debug("templateElement = {}", templateElement);
sb.append(templateElement.getSource());
}
}
if(trim) {
return sb.toString().trim();
} else {
return sb.toString();
}
}
}
Option 2 FTL:
<#macro content name>
<#local temlateSource = hostDataService.processContent(name)/>
<#local inlineTemplate = temlateSource?interpret/>
<@inlineTemplate/>
</#macro>
<!-- This works. It is able to find the 'testValue' assigned outside the <@content> tag.-->
<#assign testValue>Some Test Value</#assign>
<@content 'page.h2'><h2>This is the default content. Here is a test value: ${testValue}</h2></@content>
After reviewing my options I decided to go with #2 as it worked and should be easy to maintain as long as the Freemarker internal Macro code has package-private access.