You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/09/13 09:49:14 UTC

[13/36] incubator-freemarker git commit: FREEMARKER-55: Adding more spring callable models.

FREEMARKER-55: Adding more spring callable models.


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/12f70fff
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/12f70fff
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/12f70fff

Branch: refs/heads/3
Commit: 12f70fff2db494e91bbc9926c76e529d781f4f18
Parents: a8e73ec
Author: Woonsan Ko <wo...@apache.org>
Authored: Wed Sep 6 10:01:10 2017 -0400
Committer: Woonsan Ko <wo...@apache.org>
Committed: Wed Sep 6 10:01:10 2017 -0400

----------------------------------------------------------------------
 .../AbstractSpringTemplateCallableModel.java    |  38 +++---
 .../freemarker/spring/model/BindDirective.java  |  19 ++-
 .../spring/model/BindErrorsDirective.java       | 122 +++++++++++++++++++
 .../spring/model/MessageFunction.java           |  36 ++++++
 .../spring/model/NestedPathDirective.java       | 109 +++++++++++++++++
 .../SpringFormTemplateCallableHashModel.java    |  56 +++++++++
 .../model/SpringTemplateCallableHashModel.java  |  78 ++++++++++++
 .../freemarker/spring/model/ThemeFunction.java  |  36 ++++++
 .../spring/web/view/FreeMarkerView.java         |  21 +---
 9 files changed, 480 insertions(+), 35 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/12f70fff/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java
index b82bed1..de95df5 100644
--- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java
@@ -23,8 +23,8 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
 import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
-import org.apache.freemarker.core.model.ObjectWrappingException;
 import org.apache.freemarker.core.model.TemplateCallableModel;
 import org.apache.freemarker.core.model.TemplateModel;
 import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
@@ -37,12 +37,6 @@ import org.springframework.web.servlet.support.RequestContext;
  */
 public abstract class AbstractSpringTemplateCallableModel implements TemplateCallableModel {
 
-    // TODO: namespace this into 'spring.nestedPath'??
-    /**
-     * @see <code>org.springframework.web.servlet.tags.NestedPathTag#NESTED_PATH_VARIABLE_NAME</code>
-     */
-    private static final String NESTED_PATH_VARIABLE_NAME = "nestedPath";
-
     private final HttpServletRequest request;
     private final HttpServletResponse response;
 
@@ -73,10 +67,10 @@ public abstract class AbstractSpringTemplateCallableModel implements TemplateCal
      * @param ignoreNestedPath flag whether or not to ignore the nested path
      * @return {@link TemplateModel} wrapping a {@link BindStatus} with no {@code htmlEscape} option from {@link RequestContext}
      * by the {@code path}
-     * @throws ObjectWrappingException if fails to wrap the <code>BindStatus</code> object
+     * @throws TemplateException 
      */
     protected final TemplateModel getBindStatusTemplateModel(Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper,
-            RequestContext requestContext, String path, boolean ignoreNestedPath) throws ObjectWrappingException {
+            RequestContext requestContext, String path, boolean ignoreNestedPath) throws TemplateException {
         final String resolvedPath = (ignoreNestedPath) ? path : resolveNestedPath(env, objectWrapperAndUnwrapper, path);
         BindStatus status = requestContext.getBindStatus(resolvedPath, false);
 
@@ -94,15 +88,25 @@ public abstract class AbstractSpringTemplateCallableModel implements TemplateCal
 
     protected abstract boolean isFunction();
 
+    protected String getCurrentNestedPath(final Environment env) throws TemplateException {
+        SpringTemplateCallableHashModel springHash = (SpringTemplateCallableHashModel) env
+                .getVariable(SpringTemplateCallableHashModel.NAME);
+        return springHash.getNestedPath();
+    }
+
+    protected void setCurrentNestedPath(final Environment env, final String nestedPath) throws TemplateException {
+        SpringTemplateCallableHashModel springHash = (SpringTemplateCallableHashModel) env
+                .getVariable(SpringTemplateCallableHashModel.NAME);
+        springHash.setNestedPath(nestedPath);
+    }
+
     private String resolveNestedPath(final Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper,
-            final String path) {
-        // TODO: should read it from request or env??
-        //       or read spring.nestedPath first and read request attribute next??
-        String nestedPath = (String) request.getAttribute(NESTED_PATH_VARIABLE_NAME);
-
-        if (nestedPath != null && !path.startsWith(nestedPath)
-                && !path.equals(nestedPath.substring(0, nestedPath.length() - 1))) {
-            return nestedPath + path;
+            final String path) throws TemplateException {
+        String curNestedPath = getCurrentNestedPath(env);
+
+        if (curNestedPath != null && !path.startsWith(curNestedPath)
+                && !path.equals(curNestedPath.substring(0, curNestedPath.length() - 1))) {
+            return curNestedPath + path;
         }
 
         return path;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/12f70fff/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindDirective.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindDirective.java
index 0fbbe2f..99f5f86 100644
--- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindDirective.java
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindDirective.java
@@ -41,18 +41,31 @@ import org.springframework.web.servlet.support.RequestContext;
  * <P>
  * This directive supports the following parameters:
  * <UL>
- * <LI><code>ignoreNestedPath</code>: Set whether to ignore a nested path, if any. <code>false</code> by default.</LI>
- * <LI><code>path</code>: The path to the bean or bean property to bind status information for.</LI>
+ * <LI><code>path</code>: The first positional parameter pointing to the bean or bean property to bind status information for.</LI>
+ * <LI>
+ *   <code>ignoreNestedPath</code>: A named parameter to set whether to ignore a nested path, if any.
+ *   <code>false</code> by default.
+ * </LI>
  * </UL>
  * </P>
  * <P>
+ * Some valid example(s):
+ * </P>
+ * <PRE>
+ *   &lt;@spring.bind "user.email"; status&gt;
+ *     &lt;input type="text" name="email" value="${status.value!}" /&gt;
+ *   &lt;/@spring.bind&gt;
+ * </PRE>
+ * <P>
  * <EM>Note:</EM> Unlike Spring Framework's <code>&lt;spring:bind /&gt;</code> JSP Tag Library, this directive
  * does not support <code>htmlEscape</code> parameter. It always has <code>BindStatus</code> not to escape HTML's
- * because it is much easier to control escaping in FreeMarker Template expressions rather than depending on directives.
+ * because it is much easier to control escaping in FreeMarker Template expressions.
  * </P>
  */
 public class BindDirective extends AbstractSpringTemplateDirectiveModel {
 
+    public static final String NAME = "bind";
+
     private static final int PATH_PARAM_IDX = 0;
     private static final int IGNORE_NESTED_PATH_PARAM_IDX = 1;
 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/12f70fff/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindErrorsDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindErrorsDirective.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindErrorsDirective.java
new file mode 100644
index 0000000..c98db67
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindErrorsDirective.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.spring.model;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.freemarker.core.CallPlace;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.ArgumentArrayLayout;
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.ObjectWrappingException;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.util.CallableUtils;
+import org.springframework.validation.Errors;
+import org.springframework.web.servlet.support.RequestContext;
+
+/**
+ * Provides <code>TemplateModel</code> wrapping the bind errors (type of <code>org.springframework.validation.Errors</code>)
+ * for the given name, working similarly to Spring Framework's <code>&lt;spring:hasBindErrors /&gt;</code> JSP Tag Library.
+ * <P>
+ * This directive supports the following parameters:
+ * <UL>
+ * <LI><code>name</code>: The first positional parameter for the name of the bean that this directive should check.</LI>
+ * </UL>
+ * </P>
+ * <P>
+ * Some valid example(s):
+ * </P>
+ * <PRE>
+ * &lt;@spring.hasBindErrors "email"; errors&gt;
+ *   &lt;#-- nested content with using errors --&gt;
+ * &lt;/@spring.hasBindErrors&gt;
+ * </PRE>
+ * <P>
+ * <EM>Note:</EM> Unlike Spring Framework's <code>&lt;spring:hasBindErrors /&gt;</code> JSP Tag Library, this directive
+ * does not support <code>htmlEscape</code> parameter. It always has an <code>org.springframework.validation.Errors</code>
+ * instance not to escape HTML's because it is much easier to control escaping in FreeMarker Template expressions
+ * rather than depending on directives.
+ * </P>
+ */
+public class BindErrorsDirective extends AbstractSpringTemplateDirectiveModel {
+
+    public static final String NAME = "hasBindErrors";
+
+    private static final int NAME_PARAM_IDX = 0;
+
+    private static final ArgumentArrayLayout ARGS_LAYOUT =
+            ArgumentArrayLayout.create(
+                    1,
+                    false,
+                    null,
+                    false
+                    );
+
+    public BindErrorsDirective(HttpServletRequest request, HttpServletResponse response) {
+        super(request, response);
+    }
+
+    @Override
+    protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env,
+            ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
+                    throws TemplateException, IOException {
+        final String name = CallableUtils.getStringArgument(args, NAME_PARAM_IDX, this);
+
+        final TemplateModel bindErrorsModel = getBindErrorsTemplateModel(env, objectWrapperAndUnwrapper, requestContext, name);
+
+        if (bindErrorsModel != null) {
+            final TemplateModel[] nestedContentArgs = new TemplateModel[] { bindErrorsModel };
+            callPlace.executeNestedContent(nestedContentArgs, out, env);
+        }
+    }
+
+    @Override
+    public boolean isNestedContentSupported() {
+        return true;
+    }
+
+    @Override
+    public ArgumentArrayLayout getDirectiveArgumentArrayLayout() {
+        return ARGS_LAYOUT;
+    }
+
+    private final TemplateModel getBindErrorsTemplateModel(Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper,
+            RequestContext requestContext, String name) throws ObjectWrappingException {
+        final Errors errors = requestContext.getErrors(name, false);
+
+        if (errors != null && errors.hasErrors()) {
+            if (!(objectWrapperAndUnwrapper instanceof DefaultObjectWrapper)) {
+                CallableUtils.newGenericExecuteException("objectWrapperAndUnwrapper is not a DefaultObjectWrapper.",
+                        this, isFunction());
+            }
+
+            return ((DefaultObjectWrapper) objectWrapperAndUnwrapper).wrap(errors);
+        }
+
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/12f70fff/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MessageFunction.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MessageFunction.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MessageFunction.java
index c6d1c69..46547a6 100644
--- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MessageFunction.java
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MessageFunction.java
@@ -41,8 +41,44 @@ import org.springframework.context.MessageSource;
 import org.springframework.context.MessageSourceResolvable;
 import org.springframework.web.servlet.support.RequestContext;
 
+/**
+ * A <code>TemplateFunctionModel</code> providing functionality equivalent to the Spring Framework's
+ * <code>&lt;spring:message /&gt;</code> JSP Tag Library.
+ * It retrieves the theme message with the given code or the resolved text by the given <code>message</code> parameter.
+ * <P>
+ * This function supports the following parameters:
+ * <UL>
+ * <LI><code>code</code>: The first optional positional parameter. The key to use when looking up the message.
+ * <LI><code>message arguments</code>: Positional varargs after <code>code</code> parameter, as message arguments.</LI>
+ * <LI><code>message</code>: Named parameters as <code>MessageResolvable</code> object.</LI>
+ * </UL>
+ * </P>
+ * <P>
+ * This function requires either <code>code</code> parameter or <code>message</code> parameter at least.
+ * </P>
+ * <P>
+ * Some valid example(s):
+ * </P>
+ * <PRE>
+ * &lt;#-- With 'code' positional parameter only --&gt;
+ * ${spring.message("label.user.firstName")!}
+ *
+ * &lt;#-- With 'code' positional parameter and message arguments (varargs) --&gt;
+ * ${spring.message("message.user.form", user.firstName, user.lastName, user.email)}
+ *
+ * &lt;#-- With 'message' named parameter (<code>MessageResolvable</code> object) --&gt;
+ * ${spring.message(message=myMessageResolvable)}
+ * </PRE>
+ * <P>
+ * <EM>Note:</EM> Unlike Spring Framework's <code>&lt;spring:message /&gt;</code> JSP Tag Library, this function
+ * does not support <code>htmlEscape</code> parameter. It always returns the message not to escape HTML's
+ * because it is much easier to control escaping in FreeMarker Template expressions.
+ * </P>
+ */
 public class MessageFunction extends AbstractSpringTemplateFunctionModel {
 
+    public static final String NAME = "message";
+
     private static final int CODE_PARAM_IDX = 0;
     private static final int MESSAGE_RESOLVABLE_PARAM_IDX = 1;
     private static final int MESSAGE_ARGS_PARAM_IDX = 2;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/12f70fff/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/NestedPathDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/NestedPathDirective.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/NestedPathDirective.java
new file mode 100644
index 0000000..37d4370
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/NestedPathDirective.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.spring.model;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.freemarker.core.CallPlace;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.ArgumentArrayLayout;
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util.CallableUtils;
+import org.springframework.beans.PropertyAccessor;
+import org.springframework.web.servlet.support.RequestContext;
+
+/**
+ * Provides <code>TemplateModel</code> setting <code>spring.nestedPath</code> by the given bind path, working similarly
+ * to Spring Framework's <code>&lt;spring:nestedPath /&gt;</code> JSP Tag Library.
+ * <P>
+ * This directive supports the following parameters:
+ * <UL>
+ * <LI><code>path</code>: The first positional parameter to set a new nested path by appending it to the existing nested path if any existing.</LI>
+ * </UL>
+ * </P>
+ * <P>
+ * Some valid example(s):
+ * </P>
+ * <PRE>
+ *   &lt;@spring.nestedPath "user"&gt;
+ *     &lt;#-- nested content --/&gt;
+ *   &lt;/@spring.nestedPath&gt;
+ * </PRE>
+ */
+public class NestedPathDirective extends AbstractSpringTemplateDirectiveModel {
+
+    public static final String NAME = "nestedPath";
+
+    private static final int PATH_PARAM_IDX = 0;
+
+    private static final ArgumentArrayLayout ARGS_LAYOUT =
+            ArgumentArrayLayout.create(
+                    1,
+                    false,
+                    null,
+                    false
+                    );
+
+    public NestedPathDirective(HttpServletRequest request, HttpServletResponse response) {
+        super(request, response);
+    }
+
+    @Override
+    protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env,
+            ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
+                    throws TemplateException, IOException {
+        String path = CallableUtils.getStringArgument(args, PATH_PARAM_IDX, this);
+
+        if (path == null) {
+            path = "";
+        }
+
+        if (!path.isEmpty() && !path.endsWith(PropertyAccessor.NESTED_PROPERTY_SEPARATOR)) {
+            path += PropertyAccessor.NESTED_PROPERTY_SEPARATOR;
+        }
+
+        String prevNestedPath = getCurrentNestedPath(env);
+        String newNestedPath = (prevNestedPath != null) ? prevNestedPath + path : path;
+
+        try {
+            setCurrentNestedPath(env, newNestedPath);
+            callPlace.executeNestedContent(null, out, env);
+        } finally {
+            setCurrentNestedPath(env, prevNestedPath);
+        }
+    }
+
+    @Override
+    public boolean isNestedContentSupported() {
+        return true;
+    }
+
+    @Override
+    public ArgumentArrayLayout getDirectiveArgumentArrayLayout() {
+        return ARGS_LAYOUT;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/12f70fff/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringFormTemplateCallableHashModel.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringFormTemplateCallableHashModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringFormTemplateCallableHashModel.java
new file mode 100644
index 0000000..4ff3552
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringFormTemplateCallableHashModel.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.spring.model;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+
+/**
+ * TemplateHashModel wrapper for templates using Spring Form directives and functions.
+ */
+public final class SpringFormTemplateCallableHashModel implements TemplateHashModel, Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final String NAME = "form";
+
+    private Map<String, AbstractSpringTemplateCallableModel> callablesMap = new HashMap<>();
+
+    public SpringFormTemplateCallableHashModel(final HttpServletRequest request, final HttpServletResponse response) {
+    }
+
+    public TemplateModel get(String key) throws TemplateException {
+        return callablesMap.get(key);
+    }
+
+    @Override
+    public boolean isEmptyHash() throws TemplateException {
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/12f70fff/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringTemplateCallableHashModel.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringTemplateCallableHashModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringTemplateCallableHashModel.java
new file mode 100644
index 0000000..dd2f71c
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringTemplateCallableHashModel.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.spring.model;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.impl.SimpleString;
+
+/**
+ * TemplateHashModel wrapper for templates using Spring directives and functions.
+ */
+public final class SpringTemplateCallableHashModel implements TemplateHashModel, Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final String NAME = "spring";
+
+    public static final String NESTED_PATH = "nestedPath";
+
+    private Map<String, AbstractSpringTemplateCallableModel> callablesMap = new HashMap<>();
+
+    private String nestedPath;
+
+    public SpringTemplateCallableHashModel(final HttpServletRequest request, final HttpServletResponse response) {
+        callablesMap.put(BindDirective.NAME, new BindDirective(request, response));
+        callablesMap.put(MessageFunction.NAME, new MessageFunction(request, response));
+        callablesMap.put(ThemeFunction.NAME, new ThemeFunction(request, response));
+        callablesMap.put(BindErrorsDirective.NAME, new BindErrorsDirective(request, response));
+        callablesMap.put(NestedPathDirective.NAME, new NestedPathDirective(request, response));
+    }
+
+    public TemplateModel get(String key) throws TemplateException {
+        if (NESTED_PATH.equals(key)) {
+            return (nestedPath != null) ? new SimpleString(nestedPath) : null;
+        }
+
+        return callablesMap.get(key);
+    }
+
+    @Override
+    public boolean isEmptyHash() throws TemplateException {
+        return false;
+    }
+
+    public String getNestedPath() {
+        return nestedPath;
+    }
+
+    public void setNestedPath(String nestedPath) {
+        this.nestedPath = nestedPath;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/12f70fff/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/ThemeFunction.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/ThemeFunction.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/ThemeFunction.java
index b3b0653..15a4103 100644
--- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/ThemeFunction.java
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/ThemeFunction.java
@@ -25,8 +25,44 @@ import javax.servlet.http.HttpServletResponse;
 import org.springframework.context.MessageSource;
 import org.springframework.web.servlet.support.RequestContext;
 
+/**
+ * A <code>TemplateFunctionModel</code> providing functionality equivalent to the Spring Framework's
+ * <code>&lt;spring:theme /&gt;</code> JSP Tag Library.
+ * It retrieves the theme message with the given code or the resolved text by the given <code>message</code> parameter.
+ * <P>
+ * This function supports the following parameters:
+ * <UL>
+ * <LI><code>code</code>: The first optional positional parameter. The key to use when looking up the message.
+ * <LI><code>message arguments</code>: Positional varargs after <code>code</code> parameter, as message arguments.</LI>
+ * <LI><code>message</code>: Named parameters as <code>MessageResolvable</code> object.</LI>
+ * </UL>
+ * </P>
+ * <P>
+ * This function requires either <code>code</code> parameter or <code>message</code> parameter at least.
+ * </P>
+ * <P>
+ * Some valid example(s):
+ * </P>
+ * <PRE>
+ * &lt;#-- With 'code' positional parameter only --&gt;
+ * ${spring.theme("label.user.firstName")!}
+ *
+ * &lt;#-- With 'code' positional parameter and message arguments (varargs) --&gt;
+ * ${spring.theme("message.user.form", user.firstName, user.lastName, user.email)}
+ *
+ * &lt;#-- With 'message' named parameter (<code>MessageResolvable</code> object) --&gt;
+ * ${spring.theme(message=myMessageResolvable)}
+ * </PRE>
+ * <P>
+ * <EM>Note:</EM> Unlike Spring Framework's <code>&lt;spring:theme /&gt;</code> JSP Tag Library, this function
+ * does not support <code>htmlEscape</code> parameter. It always returns the message not to escape HTML's
+ * because it is much easier to control escaping in FreeMarker Template expressions.
+ * </P>
+ */
 public class ThemeFunction extends MessageFunction {
 
+    public static final String NAME = "theme";
+
     public ThemeFunction(HttpServletRequest request, HttpServletResponse response) {
         super(request, response);
     }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/12f70fff/freemarker-spring/src/main/java/org/apache/freemarker/spring/web/view/FreeMarkerView.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/web/view/FreeMarkerView.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/web/view/FreeMarkerView.java
index a789f4e..640cd13 100644
--- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/web/view/FreeMarkerView.java
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/web/view/FreeMarkerView.java
@@ -25,11 +25,8 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 
-import org.apache.freemarker.core.model.ObjectWrapper;
 import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
 import org.apache.freemarker.core.model.TemplateHashModel;
-import org.apache.freemarker.core.model.TemplateHashModelEx2;
-import org.apache.freemarker.core.model.impl.SimpleHash;
 import org.apache.freemarker.servlet.AllHttpScopesHashModel;
 import org.apache.freemarker.servlet.FreemarkerServlet;
 import org.apache.freemarker.servlet.HttpRequestHashModel;
@@ -38,9 +35,8 @@ import org.apache.freemarker.servlet.HttpSessionHashModel;
 import org.apache.freemarker.servlet.IncludePage;
 import org.apache.freemarker.servlet.ServletContextHashModel;
 import org.apache.freemarker.servlet.jsp.TaglibFactory;
-import org.apache.freemarker.spring.model.BindDirective;
-import org.apache.freemarker.spring.model.MessageFunction;
-import org.apache.freemarker.spring.model.ThemeFunction;
+import org.apache.freemarker.spring.model.SpringFormTemplateCallableHashModel;
+import org.apache.freemarker.spring.model.SpringTemplateCallableHashModel;
 
 /**
  * FreeMarker template based view implementation, with being able to provide a {@link ServletContextHashModel}
@@ -143,7 +139,10 @@ public class FreeMarkerView extends AbstractFreeMarkerView {
 
         model.putUnlistedModel(FreemarkerServlet.KEY_INCLUDE, new IncludePage(request, response));
 
-        model.putUnlistedModel("spring", createSpringCallableHashModel(objectWrapper, request, response));
+        model.putUnlistedModel(SpringTemplateCallableHashModel.NAME,
+                new SpringTemplateCallableHashModel(request, response));
+        model.putUnlistedModel(SpringFormTemplateCallableHashModel.NAME,
+                new SpringFormTemplateCallableHashModel(request, response));
 
         model.putAll(map);
 
@@ -176,12 +175,4 @@ public class FreeMarkerView extends AbstractFreeMarkerView {
         return sessionModel;
     }
 
-    private TemplateHashModelEx2 createSpringCallableHashModel(final ObjectWrapper objectWrapper,
-            final HttpServletRequest request, final HttpServletResponse response) {
-        final SimpleHash springCallableHash = new SimpleHash(objectWrapper);
-        springCallableHash.put("bind", new BindDirective(request, response));
-        springCallableHash.put("message", new MessageFunction(request, response));
-        springCallableHash.put("theme", new ThemeFunction(request, response));
-        return springCallableHash;
-    }
 }