apache-camelfreemarkerspring-camel

Ftl file not rendering in Apache Camel+Springboot+Email integration


Apache Camel+Springboot+Email integration. I am trying to render a ftl template as a reply to an email query send to my backend chatbot. However, my .ftl file fails to render with an error as below. I am novice with FTLs. Any suggestions where I am going wrong or how to fix this will be great and very appreciated!

reemarker.core.InvalidReferenceException: The following has evaluated to null or missing:
==> ftlDataMap  [in template "MyFtl.ftl" at line 61, column 14]
----
Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
FTL stack trace ("~" means nesting-related):
    - Failed at: #if ftlDataMap.replyMessages?size  [in template "MyFtl.ftl" at line 61, column 9]

MyFtl.ftl:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Email Reply</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            line-height: 1.6;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
        }
        .email-container {
            width: 100%;
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
            background-color: #fff;
        }
        .title-ribbon {
            background-color: #007bff;
            color: #fff;
            padding: 10px;
            border-radius: 5px 5px 0 0;
            font-size: 20px;
            font-weight: bold;
            text-align: center;
        }
        .reply-section {
            margin: 20px 0;
        }
        .reply-section p {
            margin: 10px 0;
        }
        .content-html {
            background-color: #f9f9f9;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            word-wrap: break-word;
        }
        .original-message {
            border-top: 1px solid #ddd;
            padding-top: 20px;
        }
        .separator {
            margin: 20px 0;
            border-top: 2px dashed #ddd;
        }
    </style>
</head>
<body>
    <div class="email-container">
        <div class="title-ribbon">
            Email Reply
        </div>

        <#if ftlDataMap.replyMessages?size > 0>
            <#list ftlDataMap.replyMessages as reply>
                <#if reply.replyMessage?size > 0>
                    <#list reply.replyMessage?reverse as item>
                        <#if item.type == "AI Reply">
                            <p><strong>AI Reply:</strong> ${item.value}</p>
                        <#else>
                            <div class="content-html">${item.value?html}</div>
                        </#if>
                    </#list>
                <#else>
                    <p>No replies found.</p>
                </#if>
            </#list>
        <#else>
            <p>No reply messages found.</p>
        </#if>

        <div class="separator"></div>

        <div class="original-message">
            <p><strong>-----Original Message-----</strong></p>
            <#if ftlDataMap.replyMessages?size > 0>
                <#list ftlDataMap.replyMessages[0].originalMessage as item>
                    <p><strong>${item.label}</strong> ${item.value}</p>
                </#list>
            <#else>
                <p>No original message content found.</p>
            </#if>
        </div>
    </div>
</body>
</html>

My Java class:

public class CamelIMAPProcessor implements Processor {

    private static final Logger LOG = LoggerFactory.getLogger(CamelIMAPProcessor.class);

    @Override
    public void process(Exchange exchange) throws Exception {
        
        Map<String, Object> ftlDataMap = new LinkedHashMap<>();
        List<ValueExampleObject> originalMessage = new ArrayList<>();
        Map<String, Object> reply = new LinkedHashMap<>();
        List<Object> previousResp = new ArrayList<>();
        List<ValueExampleObject> chatBotReply = new ArrayList<>();

        String backendResponse = exchange.getIn().getBody(String.class);
        String originalEmailBody = (String) exchange.getProperty("originalEmailBody");
        String user_message = "";
        String fromAddress = exchange.getProperty("From", String.class);
        String toAddress = exchange.getIn().getHeader("To", String.class);
        String subject = exchange.getIn().getHeader("Subject", String.class);
        String timeStamp = exchange.getIn().getHeader("Date", String.class);

        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode rootNode = objectMapper.readTree(backendResponse);
        JsonNode groupInfo = rootNode.path("group_info");
        JsonNode conversationObjects = groupInfo.path("conversation_objects");

        // Extract "chat-text" and "html-embed" objects
        for (JsonNode objectNode : conversationObjects) {
            String objectType = objectNode.path("object_type").asText();
            JsonNode objectContent = objectNode.path("object_content");

            if ("chat-text".equals(objectType)) {
                LOG.info("Inside chat-text....");
                String myBackendResponseResponse = objectContent.path("response").path("textData").asText();
                user_message = objectContent.path("query").asText();
                chatBotReply.add(new ValueExampleObject("AI Reply", myBackendResponseResponse));
            }

            if ("html-embed".equals(objectType)) {
                LOG.info("Inside html-embed-response....");
                String htmlContent = objectContent.path("content").asText();
                chatBotReply.add(new ValueExampleObject("Embedded Content", htmlContent));
            }
        }
        reply.put("replyMessage", chatBotReply);
        originalMessage.add(new ValueExampleObject("-----Original Message-----", ""));
        originalMessage.add(new ValueExampleObject("From: ", fromAddress));
        originalMessage.add(new ValueExampleObject("Sent: ", timeStamp != null ? timeStamp : ""));
        originalMessage.add(new ValueExampleObject("To: ", toAddress));
        originalMessage.add(new ValueExampleObject("Subject: ", subject != null ? subject : ""));
        originalMessage.add(new ValueExampleObject("User Query:", user_message));
        originalMessage.add(new ValueExampleObject("", ""));
        reply.put("originalMessage", originalMessage);
        if (!reply.isEmpty()) {
            previousResp.add(reply);
            ftlDataMap.put("replyMessages", previousResp);
        }
        String output = createHtmlFromFreeMarkerTemplate(ftlDataMap);
        exchange.getIn().setBody(output);
    }

    private String createHtmlFromFreeMarkerTemplate(Map<String, Object> obj {
        try {
            Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);
            cfg.setClassForTemplateLoading(CamelIMAPProcessor.class, "/templates");
            cfg.setDefaultEncoding("UTF-8");
            cfg.setLocale(Locale.US);
            cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);

            Template template = cfg.getTemplate("MyFtl.ftl");
            StringWriter writer = new StringWriter();
            template.process(obj, writer);

            return writer.toString();
        } catch (Exception e) {
            // handle exc
        }
    }
}

Solution

  • You have this Java local variable, Map<String, Object> ftlDataMap, but the name of that (ftlDataMap) is transparent to everything, as it's just a local variable, and its name is lost during compilation (apart from debug info). So you can't refer to that in the template with that name. Anyway, you are using that Map as the root of the data-model, so the keys in that Map are the top-level variable names in the template. So instead of ftlDataMap.replyMessages you should just write replyMessages.

    Also <#if ftlDataMap.replyMessages?size > 0> is incorrect, a the > in > 0 will close the tag. Use <#if ftlDataMap.replyMessages?size != 0> instead. (Where you really need to use > operator, if it's not already inside parentheses for some reason, you have to write gt instead.)