Object wrappers

When you add something to a container, it may receive any java object as a parameter, not necessarily a TemplateModel, as you could see in the FreeMarker API. This is because the container implementation can silently replace that object with the appropriate TemplateModel object. For example if you add a String to the container, perhaps it will be replaced with a SimpleScalar instance which stores the same text.

As for when the replacement occurs, it's the business of the container in question (i.e. the business of the class that implements the container interface), but it must happen at the latest when you get the subvariable, as the getter methods (according to the interfaces) return TemplateModel, not Object. For example, SimpleHash, SimpleSequence and SimpleCollection use the laziest strategy; they replace a non-TemplateModel subvariable with an appropriate TemplateModel object when you get the subvariable for the first time.

As for what java objects can be replaced, and with what TemplateModel implementations, it is either handled by the container implementation itself, or it delegates this to an ObjectWrapper instance. ObjectWrapper is an interface that specifies one method: TemplateModel wrap(java.lang.Object obj). You pass in an Object, and it returns the corresponding TemplateModel object, or throws a TemplateModelException if this is not possible. The replacement rules are coded into the ObjectWrapper implementation.

The most important ObjectWrapper implementations that the FreeMarker core provides are:

For a concrete example, let's see how the SimpleXxx classes work. SimpleHash, SimpleSequence and SimpleCollection use DEFAULT_WRAPPER to wrap the subvariables (unless you pass in an alternative wrapper in their constructor). So this example demonstrates DEFAULT_WRAPPER in action:

Map map = new HashMap();
map.put("anotherString", "blah");
map.put("anotherNumber", new Double(3.14));
List list = new ArrayList();
list.add("red");
list.add("green");
list.add("blue");

SimpleHash root = new SimpleHash();  // will use the default wrapper
root.put("theString", "wombat");
root.put("theNumber", new Integer(8));
root.put("theMap", map);
root.put("theList", list);  

Assuming that root is the data-model root, the resulting data-model is:

(root)
 |
 +- theString = "wombat"
 |
 +- theNumber = 8
 |
 +- theMap
 |   |
 |   +- anotherString = "blah"
 |   |
 |   +- anotherNumber = 3.14
 |
 +- theList
     |
     +- (1st) = "red"
     |
     +- (2nd) = "green"
     |
     +- (3rd) = "blue"  

Note that the Object-s inside theMap and theList are accessible as subvariables too. This is because when you, say, try to access theMap.anotherString, then the SimpleHash (which is used as root hash here) will silently replace the Map (theMap) with a SimpleHash instance that uses the same wrapper as the root hash, so when you try to access the anotherString subvariable of it, it will replace that with a SimpleScalar.

If you drop an ``arbitrary'' object into the data-model, DEFAULT_WRAPPER will invoke BEANS_WRAPPER to wrap the object:

SimpleHash root = new SimpleHash();
// expose a "simple" java objects:
root.put("theString", "wombat");
// expose an "arbitrary" java objects:
root.put("theObject", new TestObject("green mouse", 1200));  

Assuming this is TestObject:

public class TestObject {
    private String name;
    private int price;

    public TestObject(String name, int price) {
        this.name = name;
        this.price = price;
    }

    // JavaBean properties
    // Note that public fields are not visible directly;
    // you must write a getter method for them.
    public String getName() {return name;}
    public int getPrice() {return price;}
    
    // A method
    public double sin(double x) {
        return Math.sin(x);
    }
}  

The data-model will be:

(root)
 |
 +- theString = "wombat"
 |
 +- theObject
     |
     +- name = "green mouse"
     |
     +- price = 1200
     |
     +- number sin(number)  

So we can merge it with this template:

${theObject.name}
${theObject.price}
${theObject.sin(123)}  

Which will output:

green mouse
1200
-0,45990349068959124  

You have seen in earlier examples of this manual that we have used java.util.HashMap as root hash, and not SimpleHash or other FreeMarker specific class. It works because Template.process(...) automatically wraps the object you give as its data-model argument. It uses the object wrapper dictated by the Configuration level setting, object_wrapper (unless you explicitly specify an ObjectWrapper as its parameter). Thus, in simple FreeMarker application you need not know about TemplateModel-s at all. Note that the root need not be a java.util.Map. It can be anything that is wrapped so that it implements the TemplateHashModel interface.

The factory default value of the object_wrapper setting is ObjectWrapper.DEFAULT_WRAPPER. If you want to change it to, say, ObjectWrapper.BEANS_WRAPPER, you can configure the FreeMarker engine (before starting to use it from other threads) like this:

cfg.setObjectWrapper(ObjectWrapper.BEANS_WRAPPER);  

Note that you can set any object here that implements interface ObjectWrapper, so you can set your custom implementation as well.

For TemplateModel implementations that wrap basic Java container types, as java.util.Map-s and java.util.List-s, the convention is that they use the same object wrapper to wrap their subvariables as their parent container does. Technically correctly said, they are instantiated by their parent container (so it has full control over the creation of them), and the parent container create them so they will use the same object wrapper as the parent itself. Thus, if BEANS_WRAPPER is used for the wrapping of the root hash, it will be used for the wrapping of the subvariables (and the subvariables of the subvariables, etc.) as well. This is exactly the same phenomenon as you have seen with theMap.anotherString earlier.

FreeMarker Manual -- For FreeMarker 2.3.20
HTML generated: 2013-06-27 20:54:33 GMT
Edited with XMLMind XML Editor
Here!