1
votes

Is there any way to export a specific class constructor into the FreeMarker data model?

ObjectConstructor provides the power to access any accessible constructor:

Java:

myDataModel.put("objectConstructor", new ObjectConstructor()); 

Template:

<#assign aList = objectConstructor("java.util.ArrayList", 100)>

But I don't want to do that; if I have a class Foo with two constructors Foo(int x) and Foo(String name, int x), I want to somehow export an object into the data model as Foo so I can do this in a template:

<#assign myfoo1 = Foo(1) >
<#assign myfoo2 = Foo("Buffalo Bill", 2) >

I can do this manually with TemplateMethodModelEx but it requires me to implement exec(List arguments) and figure out how to pull the arguments out and push them into my Foo class constructor.

What I would like is a ClassConstructor object that takes a Class<T> argument of the class in question, and implements TemplateMethodModelEx and automatically does the casting, the same way that FreeMarker automatically handles Java object method calls, and I could do this in Java:

myDataModel.put("Foo", new ClassConstructor(Foo.class));

Is there a way to do this?

1

1 Answers

2
votes

Calling Java methods and constructors from FTL is the business of BeansWrapper, which is also the superclass of DefaultObjectWrapper and of most custom ObjectWrapper-s you find out there. So, ObjectWrapper.newInstance(Class clazz, List/*<TemplateModel>*/ arguments) can do the tricky part for you. The others are some trivial plumbing (and I went a bit fancy here with getting the ObjectWrapper - that's optional):

package freemarker.adhoc;

import java.util.List;

import freemarker.core.Environment;
import freemarker.ext.beans.BeansWrapper;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModelException;

/**
 * Creates an FTL method that calls the constructor of the given class. Supports overloading and varargs. 
 */
public class ConstructorTemplateModel implements TemplateMethodModelEx {

    private final Class constructorClass;
    private final BeansWrapper objectWrapper;

    /**
     * @param objectWrapper
     *            The same {@link ObjectWrapper} used in the {@link Configuration}, or {@code null} if we should
     *            find the current {@link ObjectWrapper} for each constructor call (slower).
     */
    public ConstructorTemplateModel(Class<?> constructorClass, BeansWrapper objectWrapper) {
        this.constructorClass = constructorClass;
        this.objectWrapper = objectWrapper;
    }

    @Override
    public Object exec(List/*<TemplateModel>*/ arguments) throws TemplateModelException {
        BeansWrapper objectWrapper = this.objectWrapper;
        if (objectWrapper == null) {
            objectWrapper = getCurrentBeansWrapper();
        }

        return objectWrapper.newInstance(constructorClass, arguments);
    }

    private BeansWrapper getCurrentBeansWrapper() {
        Environment env = Environment.getCurrentEnvironment();
        if (env == null) {
            throw new IllegalStateException("No ongoing template processing");
        }

        ObjectWrapper objectWrapper = env.getObjectWrapper();
        if (!(objectWrapper instanceof BeansWrapper)) {
            throw new IllegalStateException("The object wrapper must be a BeansWrapper. Object wrapper class: "
                    + objectWrapper.getClass().getName());
        }

        return (BeansWrapper) objectWrapper;
    }

}