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:
-
freemarker.template.DefaultObjectWrapper: It replaces String with SimpleScalar, Number with SimpleNumber, List and array with SimpleSequence, Map with SimpleHash, Boolean with TemplateBooleanModel.TRUE or TemplateBooleanModel.FALSE, W3C DOM nodes with freemarker.ext.dom.NodeModel. For Jython objects, this wrapper will invoke freemarker.ext.jython.JythonWrapper. For all other objects, it will invoke BeansWrapper.
-
freemarker.ext.beans.BeansWrapper: It can expose JavaBean properties and other members of arbitrary objects using Java reflection; there is a separated chapter about it. DefaultObjectWrapper extends this wrapper.
For a concrete example, let's see how the SimpleXxx classes work:
| |||
Assuming that root is the data-model root, the resulting data-model is:
| |||
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:
| |||
Assuming this is TestObject:
| |||
The data-model will be:
| |||
So we can merge it with this template:
| |||
Which will output:
| |||
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.