Load FreeMarker templates from database

21,317

Solution 1

We use a StringTemplateLoader to load our tempates which we got from the db (as Dan Vinton suggested)

Here is an example:

StringTemplateLoader stringLoader = new StringTemplateLoader();
String firstTemplate = "firstTemplate";
stringLoader.putTemplate(firstTemplate, freemarkerTemplate);
// It's possible to add more than one template (they might include each other)
// String secondTemplate = "<#include \"greetTemplate\"><@greet/> World!";
// stringLoader.putTemplate("greetTemplate", secondTemplate);
Configuration cfg = new Configuration();
cfg.setTemplateLoader(stringLoader);
Template template = cfg.getTemplate(firstTemplate);

Edit You don't have to load all templates at startup. Whenever we will access the template, we'll fetch it from the DB and load it through the StringLoader and by calling template.process() we generate (in our case) the XML output.

Solution 2

A couple of ways:

  • Create a new implementation of TemplateLoader to load templates direct from the database, and pass it to your Configuration instance using setTemplateLoader() prior to loading any templates.

  • Use a StringTemplateLoader that you configure from your database when your application starts. Add it to the configuration as above.

Edit in light of the questioner's edit, your own implementation of TemplateLoader looks like the way to go. Check the Javadoc here, it's a simple little interface with only four methods, and its behaviour is well documented.

Solution 3

Since 2.3.20 you can simply construct a Template using a string:

public Template(String name,
                String sourceCode,
                Configuration cfg)
         throws IOException

which is a convenience constructor for Template(name, new StringReader(sourceCode), cfg).

Solution 4

For those looking for some code, here it is. Take a look at the comments in the code for a better understanding.

DBTemplate:

@Entity
public class DBTemplate implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    private long templateId;

    private String content; // Here's where the we store the template

    private LocalDateTime modifiedOn;

}

TemplateLoader implementation (EMF is an instance of an EntityManagerFactory):

public class TemplateLoaderImpl implements TemplateLoader {

    public TemplateLoaderImpl() { }

    /**
     * Retrieves the associated template for a given id.
     *
     * When Freemarker calls this function it appends a locale
     * trying to find a specific version of a file. For example,
     * if we need to retrieve the layout with id = 1, then freemarker
     * will first try to load layoutId = 1_en_US, followed by 1_en and
     * finally layoutId = 1.
     * That's the reason why we have to catch NumberFormatException
     * even if it is comes from a numeric field in the database.
     *
     * @param layoutId
     * @return a template instance or null if not found.
     * @throws IOException if a severe error happens, like not being
     * able to access the database.
     */
    @Override
    public Object findTemplateSource(String templateId) throws IOException {

        EntityManager em = null;

        try {
            long id = Long.parseLong(templateId);
            em = EMF.getInstance().getEntityManager();
            DBTemplateService service = new DBTemplateService(em);
            Optional<DBTemplate> result = service.find(id);
            if (result.isPresent()) {
                return result.get();
            } else {
                return null;
            }
        } catch (NumberFormatException e) {
            return null;
        } catch (Exception e) {
            throw new IOException(e);
        } finally {
            if (em != null && em.isOpen()) {
                em.close();
            }
        }
    }


    /**
     * Returns the last modification date of a given template.
     * If the item does not exist any more in the database, this
     * method will return Long's MAX_VALUE to avoid freemarker's
     * from recompiling the one in its cache.
     *
     * @param templateSource
     * @return
     */
    @Override
    public long getLastModified(Object templateSource) {
        EntityManager em = null;
        try {
            em = EMF.getInstance().getEntityManager();
            DBTemplateService service = new DBTemplateService(em);
            // Optimize to only retrieve the date
            Optional<DBTemplate> result = service.find(((DBTemplate) templateSource).getTemplateId());
            if (result.isPresent()) {
                return result.get().getModifiedOn().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
            } else {
                return Long.MAX_VALUE;
            }
        } finally {
            if (em != null && em.isOpen()) {
                em.close();
            }
        }
    }

    /**
     * Returns a Reader from a template living in Freemarker's cache.
     */
    @Override
    public Reader getReader(Object templateSource, String encoding) throws IOException {
        return new StringReader(((DBTemplate) templateSource).getContent());
    }

    @Override
    public void closeTemplateSource(Object templateSource) throws IOException {
        // Nothing to do here...
    }

}

Setup the configuration class:

...
TemplateLoaderImpl loader = new TemplateLoaderImpl();

templateConfig = new Configuration(Configuration.VERSION_2_3_25);
templateConfig.setTemplateLoader(loader);
...

And finally, use it:

...
long someId = 3L;
Template template = templateConfig.getTemplate("" + someId);
...

This works great, and allows you to use all of Freemarker's features like imports, includes, etc. Look at the following examples:

<#import "1" as layout> <!-- Use a template id. -->
<@layout.mainLayout>
...

Or in:

<#include "3"> <!-- Use a template id. -->
...

I use this loader on my own CMS (CinnamonFramework) and works like a charm.

Best,

Solution 5

Old question, but for anyone having the same issue, I achieved an easy solution without the need of a custom template loader or having to load the template at startup.

Suppose you have in your database the dynamic template:

database:

<p>Hello <b>${params.user}</b>!</p>

You can just create a Freemarker file (ftlh) that interprets a string received (content) and generates a template from it, using interpret:

dynamic.ftlh:

<#assign inlineTemplate = content?interpret>
<@inlineTemplate />

Then in your java code you only need to get the string from your database (just like retrieving any other data from the database), and use the file that has interpret to generate the template:

java:

String content = getFromDatabase(); 
Configuration cfg = getConfiguration(); 
String filePath = "dynamic.ftlh";

Map<String, Object> params = new HashMap<String, Object>();
params.put("user", "World");

Map<String, Object> root = new HashMap<>();
root.put("content", content);   
root.put("params", params);     

Template template = cfg.getTemplate(filePath);

try (Writer out = new StringWriter()) {
    template.process(root, out);
    String result = out.toString();
    System.out.println(result);
}

(Change the methods getFromDatabase() and getConfiguration() to whatever you want to get the dynamic content from the database and get the Freemarker configuration object, respectively)

This should print:

<p>Hello <b>World</b>!</p>

Then you can change your dynamic content in the database or create others, add new parameters and so on, without the need of creating other Freemarker files (ftlh).

Share:
21,317
Dónal
Author by

Dónal

I earn a living by editing text files. I can be contacted at: [email protected] You can find out about all the different kinds of text files I've edited at: My StackOverflow Careers profile

Updated on August 21, 2020

Comments

  • Dónal
    Dónal almost 4 years

    I would like to store my FreeMarker templates in a database table that looks something like:

    template_name | template_content
    ---------------------------------
    hello         |Hello ${user}
    goodbye       |So long ${user}
    

    When a request is received for a template with a particular name, this should cause a query to be executed, which loads the relevant template content. This template content, together with the data model (the value of the 'user' variable in the examples above), should then be passed to FreeMarker.

    However, the FreeMarker API seems to assume that each template name corresponds to a file of the same name within a particular directory of the filesystem. Is there any way I can easily have my templates loaded from the DB instead of the filesystem?

    EDIT: I should have mentioned that I would like to be able to add templates to the database while the application is running, so I can't simply load all templates at startup into a new StringTemplateLoader (as suggested below).