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:

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");

// Get the object wrapper we will use. The details of the
// construction aren't important now (see JavaDocs for that):
ObjectWrapper wrapper
        = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_21)
          .build();

// Normaly, you would create a plain java.util.HashMap here, and
// later let the ObjectWrapper wrap it in Template.process(...).
// But for demonstration purposes we will bypass the object wrapper
// and create a SimpleHash directly that's like the wrapper would
// create:
SimpleHash root = new SimpleHash(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, DefaultObjectWrapper will fall back to BeansWrapper to wrap the it:

SimpleHash root = new SimpleHash(wrapper);
// 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.

You can configure what ObjectWrapper FreeMarker uses with cfg.setObjectWrapper(ObjectWrapper), where cfg is the Configuration instance. Note that you can plug in your custom ObjectWrapper implementation here, hence you have full control over how templates see the data-model.

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 (that's why the SimpleHash constructor has an ObjectWrapper parameter). 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 wrapper is used for the wrapping of the root hash (see it in the examples above), 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.21
HTML generated: 2014-10-12 19:11:24 GMT
Edited with XMLMind XML Editor
Here!