From 6709117683e29b34a6e155cac30a160f86f95745 Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Sat, 26 Aug 2017 01:17:07 -0400
Subject: [PATCH 01/32] FREEMARKER-55: trying with a directive to replace
spring:bind jsp tag.
---
.../spring/model/BindDirective.java | 120 ++++++++++++++++++
.../spring/web/view/FreeMarkerView.java | 15 +++
2 files changed, 135 insertions(+)
create mode 100644 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
new file mode 100644
index 000000000..967344df0
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindDirective.java
@@ -0,0 +1,120 @@
+/*
+ * 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.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.impl.BeanModel;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.util.CallableUtils;
+import org.apache.freemarker.core.util.StringToIndexMap;
+import org.springframework.web.servlet.support.BindStatus;
+import org.springframework.web.servlet.support.RequestContext;
+import org.springframework.web.servlet.view.AbstractTemplateView;
+
+public class BindDirective implements TemplateDirectiveModel {
+
+ public static final String STATUS_VARIABLE_NAME = "status";
+
+ /**
+ * @see org.springframework.web.servlet.tags.NestedPathTag#NESTED_PATH_VARIABLE_NAME
+ */
+ private static final String NESTED_PATH_VARIABLE_NAME = "nestedPath";
+
+ private static final int PATH_PARAM_IDX = 0;
+ private static final int IGNORE_NESTED_PATH_PARAM_IDX = 1;
+
+ private static final String IGNORE_NESTED_PATH_PARAM_NAME = "ignoreNestedPath";
+
+ private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(
+ 1,
+ true,
+ StringToIndexMap.of(
+ IGNORE_NESTED_PATH_PARAM_NAME, IGNORE_NESTED_PATH_PARAM_IDX
+ ),
+ false
+ );
+
+ private final HttpServletRequest request;
+ private final HttpServletResponse response;
+
+ public BindDirective(HttpServletRequest request, HttpServletResponse response) {
+ this.request = request;
+ this.response = response;
+ }
+
+ @Override
+ public void execute(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env)
+ throws TemplateException, IOException {
+ final ObjectWrapper objectWrapper = env.getObjectWrapper();
+
+ if (!(objectWrapper instanceof DefaultObjectWrapper)) {
+ throw new TemplateException("The ObjectWrapper of environment wasn't instance of " + DefaultObjectWrapper.class.getName());
+ }
+
+ TemplateModel model = env.getDataModel().get(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE);
+ RequestContext requestContext = (RequestContext) ((DefaultObjectWrapper) objectWrapper).unwrap(model);
+
+ String resolvedPath = CallableUtils.getStringArgument(args, PATH_PARAM_IDX, this);
+ boolean ignoreNestedPath = CallableUtils.getOptionalBooleanArgument(args, IGNORE_NESTED_PATH_PARAM_IDX, this,
+ false);
+
+ if (!ignoreNestedPath) {
+ resolvedPath = resolveNestedPath(resolvedPath);
+ }
+
+ BindStatus status = requestContext.getBindStatus(resolvedPath);
+ env.setLocalVariable(STATUS_VARIABLE_NAME, new BeanModel(status, (DefaultObjectWrapper) objectWrapper));
+
+ callPlace.executeNestedContent(null, out, env);
+ }
+
+ @Override
+ public boolean isNestedContentSupported() {
+ return true;
+ }
+
+ @Override
+ public ArgumentArrayLayout getDirectiveArgumentArrayLayout() {
+ return ARGS_LAYOUT;
+ }
+
+ private String resolveNestedPath(final String path) {
+ 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;
+ }
+
+ return path;
+ }
+}
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 e827db5ab..1e94a9793 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,15 +25,20 @@
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;
import org.apache.freemarker.servlet.HttpRequestParametersHashModel;
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;
/**
* FreeMarker template based view implementation, with being able to provide a {@link ServletContextHashModel}
@@ -134,6 +139,10 @@ protected TemplateHashModel createModel(Map map, ObjectWrapperAn
model.putUnlistedModel(FreemarkerServlet.KEY_JSP_TAGLIBS, getTaglibFactory());
+ model.putUnlistedModel(FreemarkerServlet.KEY_INCLUDE, new IncludePage(request, response));
+
+ model.putUnlistedModel("spring", createSpringCallableHashModel(objectWrapper, request, response));
+
model.putAll(map);
return model;
@@ -165,4 +174,10 @@ protected HttpSessionHashModel getHttpSessionModel(ObjectWrapperAndUnwrapper obj
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));
+ return springCallableHash;
+ }
}
From 19a3277c5809e30243a921a2ee42d3a40ecf9ebc Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Mon, 28 Aug 2017 23:11:12 -0400
Subject: [PATCH 02/32] FREEMARKER-55: using namespace variable for status
variable.
---
.../freemarker/spring/model/BindDirective.java | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
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 967344df0..9ca7c6ae4 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
@@ -91,10 +91,15 @@ public void execute(TemplateModel[] args, CallPlace callPlace, Writer out, Envir
resolvedPath = resolveNestedPath(resolvedPath);
}
- BindStatus status = requestContext.getBindStatus(resolvedPath);
- env.setLocalVariable(STATUS_VARIABLE_NAME, new BeanModel(status, (DefaultObjectWrapper) objectWrapper));
-
- callPlace.executeNestedContent(null, out, env);
+ final TemplateModel oldStatusModel = env.getVariable(STATUS_VARIABLE_NAME);
+
+ try {
+ BindStatus status = requestContext.getBindStatus(resolvedPath);
+ env.setVariable(STATUS_VARIABLE_NAME, new BeanModel(status, (DefaultObjectWrapper) objectWrapper));
+ callPlace.executeNestedContent(null, out, env);
+ } finally {
+ env.setVariable(STATUS_VARIABLE_NAME, oldStatusModel);
+ }
}
@Override
From b656f1e1f7c1b0875526f2224db1fc793e1172d2 Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Tue, 29 Aug 2017 14:58:34 -0400
Subject: [PATCH 03/32] FREEMARKER-55: use nested content parameter instead of
variable
---
.../freemarker/spring/model/BindDirective.java | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
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 9ca7c6ae4..fb6838461 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
@@ -91,15 +91,11 @@ public void execute(TemplateModel[] args, CallPlace callPlace, Writer out, Envir
resolvedPath = resolveNestedPath(resolvedPath);
}
- final TemplateModel oldStatusModel = env.getVariable(STATUS_VARIABLE_NAME);
-
- try {
- BindStatus status = requestContext.getBindStatus(resolvedPath);
- env.setVariable(STATUS_VARIABLE_NAME, new BeanModel(status, (DefaultObjectWrapper) objectWrapper));
- callPlace.executeNestedContent(null, out, env);
- } finally {
- env.setVariable(STATUS_VARIABLE_NAME, oldStatusModel);
- }
+ //TODO: how to deal with htmlEscape when invoking #getBindStatus()?
+ BindStatus status = requestContext.getBindStatus(resolvedPath);
+ TemplateModel[] nestedContentArgs = new TemplateModel[] {
+ new BeanModel(status, (DefaultObjectWrapper) objectWrapper) };
+ callPlace.executeNestedContent(nestedContentArgs, out, env);
}
@Override
From aeaf0307736008b2921c7efa42cf7d31b2df6dbc Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Tue, 29 Aug 2017 23:02:21 -0400
Subject: [PATCH 04/32] FREEMARKER-55: splitting abstract class to keep common
things
---
.../AbstractSpringTemplateDirectiveModel.java | 97 +++++++++++++++++++
.../spring/model/BindDirective.java | 62 +++---------
2 files changed, 110 insertions(+), 49 deletions(-)
create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
new file mode 100644
index 000000000..9baf3f06b
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
@@ -0,0 +1,97 @@
+/*
+ * 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.ObjectWrapper;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.springframework.web.servlet.support.RequestContext;
+import org.springframework.web.servlet.view.AbstractTemplateView;
+
+public abstract class AbstractSpringTemplateDirectiveModel implements TemplateDirectiveModel {
+
+ /**
+ * @see org.springframework.web.servlet.tags.NestedPathTag#NESTED_PATH_VARIABLE_NAME
+ */
+ private static final String NESTED_PATH_VARIABLE_NAME = "nestedPath";
+
+ private final HttpServletRequest request;
+ private final HttpServletResponse response;
+
+ public AbstractSpringTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) {
+ this.request = request;
+ this.response = response;
+ }
+
+ @Override
+ public final void execute(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env)
+ throws TemplateException, IOException {
+ final ObjectWrapper objectWrapper = env.getObjectWrapper();
+
+ if (!(objectWrapper instanceof DefaultObjectWrapper)) {
+ throw new TemplateException(
+ "The ObjectWrapper of environment wasn't instance of " + DefaultObjectWrapper.class.getName());
+ }
+
+ TemplateModel rcModel = env.getVariable(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE);
+
+ if (rcModel == null) {
+ throw new TemplateException(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found.");
+ }
+
+ RequestContext requestContext = (RequestContext) ((DefaultObjectWrapper) objectWrapper).unwrap(rcModel);
+
+ executeInternal(args, callPlace, out, env, (DefaultObjectWrapper) objectWrapper, requestContext);
+ }
+
+ protected abstract void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env,
+ DefaultObjectWrapper objectWrapper, RequestContext requestContext) throws TemplateException, IOException;
+
+ protected final HttpServletRequest getRequest() {
+ return request;
+ }
+
+ protected final HttpServletResponse getResponse() {
+ return response;
+ }
+
+ protected final String resolveNestedPath(final Environment env, 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;
+ }
+
+ return path;
+ }
+}
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 fb6838461..18ba1ddab 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
@@ -29,8 +29,6 @@
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.ObjectWrapper;
-import org.apache.freemarker.core.model.TemplateDirectiveModel;
import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.impl.BeanModel;
import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
@@ -38,58 +36,35 @@
import org.apache.freemarker.core.util.StringToIndexMap;
import org.springframework.web.servlet.support.BindStatus;
import org.springframework.web.servlet.support.RequestContext;
-import org.springframework.web.servlet.view.AbstractTemplateView;
-public class BindDirective implements TemplateDirectiveModel {
-
- public static final String STATUS_VARIABLE_NAME = "status";
-
- /**
- * @see org.springframework.web.servlet.tags.NestedPathTag#NESTED_PATH_VARIABLE_NAME
- */
- private static final String NESTED_PATH_VARIABLE_NAME = "nestedPath";
+public class BindDirective extends AbstractSpringTemplateDirectiveModel {
private static final int PATH_PARAM_IDX = 0;
private static final int IGNORE_NESTED_PATH_PARAM_IDX = 1;
private static final String IGNORE_NESTED_PATH_PARAM_NAME = "ignoreNestedPath";
- private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(
- 1,
- true,
- StringToIndexMap.of(
- IGNORE_NESTED_PATH_PARAM_NAME, IGNORE_NESTED_PATH_PARAM_IDX
- ),
- false
- );
-
- private final HttpServletRequest request;
- private final HttpServletResponse response;
+ private static final ArgumentArrayLayout ARGS_LAYOUT =
+ ArgumentArrayLayout.create(
+ 1,
+ true,
+ StringToIndexMap.of(IGNORE_NESTED_PATH_PARAM_NAME, IGNORE_NESTED_PATH_PARAM_IDX),
+ false
+ );
public BindDirective(HttpServletRequest request, HttpServletResponse response) {
- this.request = request;
- this.response = response;
+ super(request, response);
}
@Override
- public void execute(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env)
- throws TemplateException, IOException {
- final ObjectWrapper objectWrapper = env.getObjectWrapper();
-
- if (!(objectWrapper instanceof DefaultObjectWrapper)) {
- throw new TemplateException("The ObjectWrapper of environment wasn't instance of " + DefaultObjectWrapper.class.getName());
- }
-
- TemplateModel model = env.getDataModel().get(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE);
- RequestContext requestContext = (RequestContext) ((DefaultObjectWrapper) objectWrapper).unwrap(model);
+ protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env,
+ DefaultObjectWrapper objectWrapper, RequestContext requestContext) throws TemplateException, IOException {
- String resolvedPath = CallableUtils.getStringArgument(args, PATH_PARAM_IDX, this);
+ final String path = CallableUtils.getStringArgument(args, PATH_PARAM_IDX, this);
boolean ignoreNestedPath = CallableUtils.getOptionalBooleanArgument(args, IGNORE_NESTED_PATH_PARAM_IDX, this,
false);
- if (!ignoreNestedPath) {
- resolvedPath = resolveNestedPath(resolvedPath);
- }
+ final String resolvedPath = (ignoreNestedPath) ? path : resolveNestedPath(env, path);
//TODO: how to deal with htmlEscape when invoking #getBindStatus()?
BindStatus status = requestContext.getBindStatus(resolvedPath);
@@ -107,15 +82,4 @@ public boolean isNestedContentSupported() {
public ArgumentArrayLayout getDirectiveArgumentArrayLayout() {
return ARGS_LAYOUT;
}
-
- private String resolveNestedPath(final String path) {
- 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;
- }
-
- return path;
- }
}
From 64adad829926532b17ce5a5965a2c40e594dd6fc Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Tue, 29 Aug 2017 23:07:29 -0400
Subject: [PATCH 05/32] FREEMARKER-55: simple clean up
---
.../java/org/apache/freemarker/spring/model/BindDirective.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
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 18ba1ddab..b7edbacda 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
@@ -68,8 +68,7 @@ protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer
//TODO: how to deal with htmlEscape when invoking #getBindStatus()?
BindStatus status = requestContext.getBindStatus(resolvedPath);
- TemplateModel[] nestedContentArgs = new TemplateModel[] {
- new BeanModel(status, (DefaultObjectWrapper) objectWrapper) };
+ TemplateModel[] nestedContentArgs = new TemplateModel[] { new BeanModel(status, objectWrapper) };
callPlace.executeNestedContent(nestedContentArgs, out, env);
}
From 2de73b082f1bf7c36d616263688f77d27bc50227 Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Wed, 30 Aug 2017 09:07:02 -0400
Subject: [PATCH 06/32] FREEMARKER-55: code clean ups
---
.../AbstractSpringTemplateDirectiveModel.java | 50 ++++++++++++++++---
.../spring/model/BindDirective.java | 15 +++---
2 files changed, 50 insertions(+), 15 deletions(-)
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
index 9baf3f06b..1c5332b81 100644
--- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
@@ -29,14 +29,18 @@
import org.apache.freemarker.core.Environment;
import org.apache.freemarker.core.TemplateException;
import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.ObjectWrappingException;
import org.apache.freemarker.core.model.TemplateDirectiveModel;
import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.springframework.web.servlet.support.BindStatus;
import org.springframework.web.servlet.support.RequestContext;
import org.springframework.web.servlet.view.AbstractTemplateView;
public abstract class AbstractSpringTemplateDirectiveModel implements TemplateDirectiveModel {
+ // TODO: namespace this into 'spring.nestedPath'??
/**
* @see org.springframework.web.servlet.tags.NestedPathTag#NESTED_PATH_VARIABLE_NAME
*/
@@ -55,9 +59,9 @@ public final void execute(TemplateModel[] args, CallPlace callPlace, Writer out,
throws TemplateException, IOException {
final ObjectWrapper objectWrapper = env.getObjectWrapper();
- if (!(objectWrapper instanceof DefaultObjectWrapper)) {
+ if (!(objectWrapper instanceof ObjectWrapperAndUnwrapper)) {
throw new TemplateException(
- "The ObjectWrapper of environment wasn't instance of " + DefaultObjectWrapper.class.getName());
+ "The ObjectWrapper of environment wasn't instance of ObjectWrapperAndUnwrapper.");
}
TemplateModel rcModel = env.getVariable(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE);
@@ -66,13 +70,14 @@ public final void execute(TemplateModel[] args, CallPlace callPlace, Writer out,
throw new TemplateException(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found.");
}
- RequestContext requestContext = (RequestContext) ((DefaultObjectWrapper) objectWrapper).unwrap(rcModel);
+ RequestContext requestContext = (RequestContext) ((ObjectWrapperAndUnwrapper) objectWrapper).unwrap(rcModel);
- executeInternal(args, callPlace, out, env, (DefaultObjectWrapper) objectWrapper, requestContext);
+ executeInternal(args, callPlace, out, env, (ObjectWrapperAndUnwrapper) objectWrapper, requestContext);
}
protected abstract void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env,
- DefaultObjectWrapper objectWrapper, RequestContext requestContext) throws TemplateException, IOException;
+ ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
+ throws TemplateException, IOException;
protected final HttpServletRequest getRequest() {
return request;
@@ -82,7 +87,40 @@ protected final HttpServletResponse getResponse() {
return response;
}
- protected final String resolveNestedPath(final Environment env, final String path) {
+ /**
+ * Find {@link BindStatus} with no {@code htmlEscape} option from {@link RequestContext} by the {@code path}
+ * and wrap it as a {@link TemplateModel}.
+ *
+ * NOTE: In FreeMarker, there is no need to depend on BindStatus#htmlEscape option
+ * as FreeMarker template expressions can easily set escape option by themselves.
+ * Therefore, this method always get a {@link BindStatus} with {@code htmlEscape} option set to {@code false}.
+ * @param env Environment
+ * @param objectWrapperAndUnwrapper ObjectWrapperAndUnwrapper
+ * @param requestContext Spring RequestContext
+ * @param path bind path
+ * @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 BindStatus object
+ */
+ protected final TemplateModel getBindStatusTemplateModel(Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper,
+ RequestContext requestContext, String path, boolean ignoreNestedPath) throws ObjectWrappingException {
+ final String resolvedPath = (ignoreNestedPath) ? path : resolveNestedPath(env, objectWrapperAndUnwrapper, path);
+ BindStatus status = requestContext.getBindStatus(resolvedPath, false);
+
+ if (status != null) {
+ if (!(objectWrapperAndUnwrapper instanceof DefaultObjectWrapper)) {
+ throw new IllegalArgumentException("objectWrapperAndUnwrapper is not a DefaultObjectWrapper.");
+ }
+
+ return ((DefaultObjectWrapper) objectWrapperAndUnwrapper).wrap(status);
+ }
+
+ return null;
+ }
+
+ 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);
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 b7edbacda..d7967c8a1 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
@@ -29,12 +29,10 @@
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.model.impl.BeanModel;
-import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
import org.apache.freemarker.core.util.CallableUtils;
import org.apache.freemarker.core.util.StringToIndexMap;
-import org.springframework.web.servlet.support.BindStatus;
import org.springframework.web.servlet.support.RequestContext;
public class BindDirective extends AbstractSpringTemplateDirectiveModel {
@@ -58,17 +56,16 @@ public BindDirective(HttpServletRequest request, HttpServletResponse response) {
@Override
protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env,
- DefaultObjectWrapper objectWrapper, RequestContext requestContext) throws TemplateException, IOException {
-
+ ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
+ throws TemplateException, IOException {
final String path = CallableUtils.getStringArgument(args, PATH_PARAM_IDX, this);
boolean ignoreNestedPath = CallableUtils.getOptionalBooleanArgument(args, IGNORE_NESTED_PATH_PARAM_IDX, this,
false);
- final String resolvedPath = (ignoreNestedPath) ? path : resolveNestedPath(env, path);
+ TemplateModel statusModel = getBindStatusTemplateModel(env, objectWrapperAndUnwrapper, requestContext, path,
+ ignoreNestedPath);
+ TemplateModel[] nestedContentArgs = new TemplateModel[] { statusModel };
- //TODO: how to deal with htmlEscape when invoking #getBindStatus()?
- BindStatus status = requestContext.getBindStatus(resolvedPath);
- TemplateModel[] nestedContentArgs = new TemplateModel[] { new BeanModel(status, objectWrapper) };
callPlace.executeNestedContent(nestedContentArgs, out, env);
}
From 63f42c3dfcb06b7873e7aec58c90894e8b291ced Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Wed, 30 Aug 2017 09:21:55 -0400
Subject: [PATCH 07/32] FREEMARKER-55: Adding javadocs
---
.../AbstractSpringTemplateDirectiveModel.java | 3 +++
.../freemarker/spring/model/BindDirective.java | 16 ++++++++++++++++
2 files changed, 19 insertions(+)
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
index 1c5332b81..e1b34b4f5 100644
--- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
@@ -38,6 +38,9 @@
import org.springframework.web.servlet.support.RequestContext;
import org.springframework.web.servlet.view.AbstractTemplateView;
+/**
+ * Abstract TemplateDirectiveModel for derived classes to support Spring MVC based templating environment.
+ */
public abstract class AbstractSpringTemplateDirectiveModel implements TemplateDirectiveModel {
// TODO: namespace this into 'spring.nestedPath'??
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 d7967c8a1..b4b8ad9fe 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
@@ -35,6 +35,22 @@
import org.apache.freemarker.core.util.StringToIndexMap;
import org.springframework.web.servlet.support.RequestContext;
+/**
+ * Provides TemplateModel wrapping BindStatus for the given bind path, working similarly
+ * to Spring Framework's <spring:bind /> JSP Tag Library.
+ *
+ * This directive supports the following parameters:
+ *
+ *
ignoreNestedPath: Set whether to ignore a nested path, if any. false by default.
+ *
path: The path to the bean or bean property to bind status information for.
+ *
+ *
+ *
+ * Note: Unlike Spring Framework's <spring:bind /> JSP Tag Library, this directive
+ * does not support htmlEscape parameter. It always has BindStatus not to escape HTML's
+ * because it is much easier to control escaping in FreeMarker Template expressions rather than depending on directives.
+ *
+ */
public class BindDirective extends AbstractSpringTemplateDirectiveModel {
private static final int PATH_PARAM_IDX = 0;
From 3b83475f111019842b6070581ca310890ba199d1 Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Sat, 2 Sep 2017 23:49:38 -0400
Subject: [PATCH 08/32] FREEMARKER-55: Adding initial message function.
---
.../AbstractSpringTemplateCallableModel.java | 106 ++++++++++++++++++
.../AbstractSpringTemplateDirectiveModel.java | 70 +-----------
.../AbstractSpringTemplateFunctionModel.java | 69 ++++++++++++
.../spring/model/BindDirective.java | 10 +-
.../spring/model/MessageFunction.java | 98 ++++++++++++++++
.../spring/web/view/FreeMarkerView.java | 2 +
6 files changed, 282 insertions(+), 73 deletions(-)
create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java
create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateFunctionModel.java
create mode 100644 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/AbstractSpringTemplateCallableModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java
new file mode 100644
index 000000000..431c066f6
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java
@@ -0,0 +1,106 @@
+/*
+ * 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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.freemarker.core.Environment;
+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;
+import org.springframework.web.servlet.support.BindStatus;
+import org.springframework.web.servlet.support.RequestContext;
+
+/**
+ * Abstract TemplateCallableModel for derived classes to support Spring MVC based templating environment.
+ */
+public abstract class AbstractSpringTemplateCallableModel implements TemplateCallableModel {
+
+ // TODO: namespace this into 'spring.nestedPath'??
+ /**
+ * @see org.springframework.web.servlet.tags.NestedPathTag#NESTED_PATH_VARIABLE_NAME
+ */
+ private static final String NESTED_PATH_VARIABLE_NAME = "nestedPath";
+
+ private final HttpServletRequest request;
+ private final HttpServletResponse response;
+
+ public AbstractSpringTemplateCallableModel(HttpServletRequest request, HttpServletResponse response) {
+ this.request = request;
+ this.response = response;
+ }
+
+ protected final HttpServletRequest getRequest() {
+ return request;
+ }
+
+ protected final HttpServletResponse getResponse() {
+ return response;
+ }
+
+ /**
+ * Find {@link BindStatus} with no {@code htmlEscape} option from {@link RequestContext} by the {@code path}
+ * and wrap it as a {@link TemplateModel}.
+ *
+ * NOTE: In FreeMarker, there is no need to depend on BindStatus#htmlEscape option
+ * as FreeMarker template expressions can easily set escape option by themselves.
+ * Therefore, this method always get a {@link BindStatus} with {@code htmlEscape} option set to {@code false}.
+ * @param env Environment
+ * @param objectWrapperAndUnwrapper ObjectWrapperAndUnwrapper
+ * @param requestContext Spring RequestContext
+ * @param path bind path
+ * @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 BindStatus object
+ */
+ protected final TemplateModel getBindStatusTemplateModel(Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper,
+ RequestContext requestContext, String path, boolean ignoreNestedPath) throws ObjectWrappingException {
+ final String resolvedPath = (ignoreNestedPath) ? path : resolveNestedPath(env, objectWrapperAndUnwrapper, path);
+ BindStatus status = requestContext.getBindStatus(resolvedPath, false);
+
+ if (status != null) {
+ if (!(objectWrapperAndUnwrapper instanceof DefaultObjectWrapper)) {
+ throw new IllegalArgumentException("objectWrapperAndUnwrapper is not a DefaultObjectWrapper.");
+ }
+
+ return ((DefaultObjectWrapper) objectWrapperAndUnwrapper).wrap(status);
+ }
+
+ return null;
+ }
+
+ 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;
+ }
+
+ return path;
+ }
+}
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
index e1b34b4f5..c4ed76c9a 100644
--- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateDirectiveModel.java
@@ -30,31 +30,18 @@
import org.apache.freemarker.core.TemplateException;
import org.apache.freemarker.core.model.ObjectWrapper;
import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
-import org.apache.freemarker.core.model.ObjectWrappingException;
import org.apache.freemarker.core.model.TemplateDirectiveModel;
import org.apache.freemarker.core.model.TemplateModel;
-import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
-import org.springframework.web.servlet.support.BindStatus;
import org.springframework.web.servlet.support.RequestContext;
import org.springframework.web.servlet.view.AbstractTemplateView;
/**
* Abstract TemplateDirectiveModel for derived classes to support Spring MVC based templating environment.
*/
-public abstract class AbstractSpringTemplateDirectiveModel implements TemplateDirectiveModel {
-
- // TODO: namespace this into 'spring.nestedPath'??
- /**
- * @see org.springframework.web.servlet.tags.NestedPathTag#NESTED_PATH_VARIABLE_NAME
- */
- private static final String NESTED_PATH_VARIABLE_NAME = "nestedPath";
-
- private final HttpServletRequest request;
- private final HttpServletResponse response;
+public abstract class AbstractSpringTemplateDirectiveModel extends AbstractSpringTemplateCallableModel implements TemplateDirectiveModel {
public AbstractSpringTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) {
- this.request = request;
- this.response = response;
+ super(request, response);
}
@Override
@@ -82,57 +69,4 @@ protected abstract void executeInternal(TemplateModel[] args, CallPlace callPlac
ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
throws TemplateException, IOException;
- protected final HttpServletRequest getRequest() {
- return request;
- }
-
- protected final HttpServletResponse getResponse() {
- return response;
- }
-
- /**
- * Find {@link BindStatus} with no {@code htmlEscape} option from {@link RequestContext} by the {@code path}
- * and wrap it as a {@link TemplateModel}.
- *
- * NOTE: In FreeMarker, there is no need to depend on BindStatus#htmlEscape option
- * as FreeMarker template expressions can easily set escape option by themselves.
- * Therefore, this method always get a {@link BindStatus} with {@code htmlEscape} option set to {@code false}.
- * @param env Environment
- * @param objectWrapperAndUnwrapper ObjectWrapperAndUnwrapper
- * @param requestContext Spring RequestContext
- * @param path bind path
- * @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 BindStatus object
- */
- protected final TemplateModel getBindStatusTemplateModel(Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper,
- RequestContext requestContext, String path, boolean ignoreNestedPath) throws ObjectWrappingException {
- final String resolvedPath = (ignoreNestedPath) ? path : resolveNestedPath(env, objectWrapperAndUnwrapper, path);
- BindStatus status = requestContext.getBindStatus(resolvedPath, false);
-
- if (status != null) {
- if (!(objectWrapperAndUnwrapper instanceof DefaultObjectWrapper)) {
- throw new IllegalArgumentException("objectWrapperAndUnwrapper is not a DefaultObjectWrapper.");
- }
-
- return ((DefaultObjectWrapper) objectWrapperAndUnwrapper).wrap(status);
- }
-
- return null;
- }
-
- 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;
- }
-
- return path;
- }
}
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateFunctionModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateFunctionModel.java
new file mode 100644
index 000000000..c0fe502c0
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateFunctionModel.java
@@ -0,0 +1,69 @@
+/*
+ * 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 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.ObjectWrapper;
+import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.TemplateFunctionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.springframework.web.servlet.support.RequestContext;
+import org.springframework.web.servlet.view.AbstractTemplateView;
+
+/**
+ * Abstract TemplateFunctionModel for derived classes to support Spring MVC based templating environment.
+ */
+public abstract class AbstractSpringTemplateFunctionModel extends AbstractSpringTemplateCallableModel
+ implements TemplateFunctionModel {
+
+ public AbstractSpringTemplateFunctionModel(HttpServletRequest request, HttpServletResponse response) {
+ super(request, response);
+ }
+
+ @Override
+ public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) throws TemplateException {
+ final ObjectWrapper objectWrapper = env.getObjectWrapper();
+
+ if (!(objectWrapper instanceof ObjectWrapperAndUnwrapper)) {
+ throw new TemplateException(
+ "The ObjectWrapper of environment wasn't instance of ObjectWrapperAndUnwrapper.");
+ }
+
+ TemplateModel rcModel = env.getVariable(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE);
+
+ if (rcModel == null) {
+ throw new TemplateException(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found.");
+ }
+
+ RequestContext requestContext = (RequestContext) ((ObjectWrapperAndUnwrapper) objectWrapper).unwrap(rcModel);
+
+ return executeInternal(args, callPlace, env, (ObjectWrapperAndUnwrapper) objectWrapper, requestContext);
+ }
+
+ protected abstract TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, Environment env,
+ ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
+ throws TemplateException;
+
+}
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 b4b8ad9fe..0fbbe2f66 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
@@ -75,12 +75,12 @@ protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer
ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
throws TemplateException, IOException {
final String path = CallableUtils.getStringArgument(args, PATH_PARAM_IDX, this);
- boolean ignoreNestedPath = CallableUtils.getOptionalBooleanArgument(args, IGNORE_NESTED_PATH_PARAM_IDX, this,
- false);
+ final boolean ignoreNestedPath = CallableUtils.getOptionalBooleanArgument(args, IGNORE_NESTED_PATH_PARAM_IDX,
+ this, false);
- TemplateModel statusModel = getBindStatusTemplateModel(env, objectWrapperAndUnwrapper, requestContext, path,
- ignoreNestedPath);
- TemplateModel[] nestedContentArgs = new TemplateModel[] { statusModel };
+ final TemplateModel statusModel = getBindStatusTemplateModel(env, objectWrapperAndUnwrapper, requestContext,
+ path, ignoreNestedPath);
+ final TemplateModel[] nestedContentArgs = new TemplateModel[] { statusModel };
callPlace.executeNestedContent(nestedContentArgs, out, env);
}
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
new file mode 100644
index 000000000..d44096367
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MessageFunction.java
@@ -0,0 +1,98 @@
+/*
+ * 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.util.List;
+
+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.model.impl.SimpleString;
+import org.apache.freemarker.core.util.CallableUtils;
+import org.apache.freemarker.core.util.StringToIndexMap;
+import org.springframework.context.MessageSource;
+import org.springframework.context.MessageSourceResolvable;
+import org.springframework.web.servlet.support.RequestContext;
+
+public class MessageFunction extends AbstractSpringTemplateFunctionModel {
+
+ private static final int CODE_PARAM_IDX = 0;
+ private static final int MESSAGE_PARAM_IDX = 1;
+
+ private static final String MESSAGE_PARAM_NAME = "message";
+
+ private static final ArgumentArrayLayout ARGS_LAYOUT =
+ ArgumentArrayLayout.create(
+ 1,
+ true,
+ StringToIndexMap.of(
+ MESSAGE_PARAM_NAME, MESSAGE_PARAM_IDX),
+ false);
+
+ public MessageFunction(HttpServletRequest request, HttpServletResponse response) {
+ super(request, response);
+ }
+
+ @Override
+ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, Environment env,
+ ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
+ throws TemplateException {
+ final MessageSource messageSource = requestContext.getMessageSource();
+
+ if (messageSource == null) {
+ throw new TemplateException("MessageSource not found.");
+ }
+
+ String message = null;
+
+ final String code = CallableUtils.getStringArgument(args, CODE_PARAM_IDX, this);
+
+ if (code != null && !code.isEmpty()) {
+ List
* Note: Unlike Spring Framework's <spring:bind /> JSP Tag Library, this directive
* does not support htmlEscape parameter. It always has BindStatus 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.
*
*/
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;
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 000000000..c98db6707
--- /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 TemplateModel wrapping the bind errors (type of org.springframework.validation.Errors)
+ * for the given name, working similarly to Spring Framework's <spring:hasBindErrors /> JSP Tag Library.
+ *
+ * This directive supports the following parameters:
+ *
+ *
name: The first positional parameter for the name of the bean that this directive should check.
+ *
+ *
+ *
+ * Some valid example(s):
+ *
+ *
+ * <@spring.hasBindErrors "email"; errors>
+ * <#-- nested content with using errors -->
+ * </@spring.hasBindErrors>
+ *
+ *
+ * Note: Unlike Spring Framework's <spring:hasBindErrors /> JSP Tag Library, this directive
+ * does not support htmlEscape parameter. It always has an org.springframework.validation.Errors
+ * instance not to escape HTML's because it is much easier to control escaping in FreeMarker Template expressions
+ * rather than depending on directives.
+ *
+ */
+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;
+ }
+
+}
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 c6d1c69db..46547a674 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.MessageSourceResolvable;
import org.springframework.web.servlet.support.RequestContext;
+/**
+ * A TemplateFunctionModel providing functionality equivalent to the Spring Framework's
+ * <spring:message /> JSP Tag Library.
+ * It retrieves the theme message with the given code or the resolved text by the given message parameter.
+ *
+ * This function supports the following parameters:
+ *
+ *
code: The first optional positional parameter. The key to use when looking up the message.
+ *
message arguments: Positional varargs after code parameter, as message arguments.
+ *
message: Named parameters as MessageResolvable object.
+ *
+ *
+ *
+ * This function requires either code parameter or message parameter at least.
+ *
+ *
+ * Some valid example(s):
+ *
+ *
+ * <#-- With 'code' positional parameter only -->
+ * ${spring.message("label.user.firstName")!}
+ *
+ * <#-- With 'code' positional parameter and message arguments (varargs) -->
+ * ${spring.message("message.user.form", user.firstName, user.lastName, user.email)}
+ *
+ * <#-- With 'message' named parameter (MessageResolvable object) -->
+ * ${spring.message(message=myMessageResolvable)}
+ *
+ *
+ * Note: Unlike Spring Framework's <spring:message /> JSP Tag Library, this function
+ * does not support htmlEscape parameter. It always returns the message not to escape HTML's
+ * because it is much easier to control escaping in FreeMarker Template expressions.
+ *
+ */
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;
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 000000000..37d437003
--- /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 TemplateModel setting spring.nestedPath by the given bind path, working similarly
+ * to Spring Framework's <spring:nestedPath /> JSP Tag Library.
+ *
+ * This directive supports the following parameters:
+ *
+ *
path: The first positional parameter to set a new nested path by appending it to the existing nested path if any existing.
+ */
+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;
+ }
+
+}
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 000000000..4ff355237
--- /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 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;
+ }
+
+}
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 000000000..dd2f71c35
--- /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 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;
+ }
+
+}
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 b3b065355..15a410375 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 org.springframework.context.MessageSource;
import org.springframework.web.servlet.support.RequestContext;
+/**
+ * A TemplateFunctionModel providing functionality equivalent to the Spring Framework's
+ * <spring:theme /> JSP Tag Library.
+ * It retrieves the theme message with the given code or the resolved text by the given message parameter.
+ *
+ * This function supports the following parameters:
+ *
+ *
code: The first optional positional parameter. The key to use when looking up the message.
+ *
message arguments: Positional varargs after code parameter, as message arguments.
+ *
message: Named parameters as MessageResolvable object.
+ *
+ *
+ *
+ * This function requires either code parameter or message parameter at least.
+ *
+ *
+ * Some valid example(s):
+ *
+ *
+ * <#-- With 'code' positional parameter only -->
+ * ${spring.theme("label.user.firstName")!}
+ *
+ * <#-- With 'code' positional parameter and message arguments (varargs) -->
+ * ${spring.theme("message.user.form", user.firstName, user.lastName, user.email)}
+ *
+ * <#-- With 'message' named parameter (MessageResolvable object) -->
+ * ${spring.theme(message=myMessageResolvable)}
+ *
+ *
+ * Note: Unlike Spring Framework's <spring:theme /> JSP Tag Library, this function
+ * does not support htmlEscape parameter. It always returns the message not to escape HTML's
+ * because it is much easier to control escaping in FreeMarker Template expressions.
+ *
+ */
public class ThemeFunction extends MessageFunction {
+ public static final String NAME = "theme";
+
public ThemeFunction(HttpServletRequest request, HttpServletResponse response) {
super(request, response);
}
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 a789f4edb..640cd132b 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.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.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 @@ protected TemplateHashModel createModel(Map map, ObjectWrapperAn
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 @@ protected HttpSessionHashModel getHttpSessionModel(ObjectWrapperAndUnwrapper obj
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;
- }
}
From 234f96ec2c2868c72f797bda42a8d0c854e63b87 Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Fri, 8 Sep 2017 12:21:53 -0400
Subject: [PATCH 13/32] FREEMARKER-55: Adding more functions.
---
.../AbstractSpringTemplateCallableModel.java | 11 +-
.../spring/model/BindErrorsDirective.java | 5 +-
.../freemarker/spring/model/EvalFunction.java | 153 ++++++++++++++++++
.../spring/model/MessageFunction.java | 14 +-
.../spring/model/MvcUrlFunction.java | 74 +++++++++
.../SpringTemplateCallableHashModel.java | 7 +-
.../spring/model/TransformFunction.java | 101 ++++++++++++
.../freemarker/spring/model/UrlFunction.java | 86 ++++++++++
8 files changed, 437 insertions(+), 14 deletions(-)
create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/EvalFunction.java
create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MvcUrlFunction.java
create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/TransformFunction.java
create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/UrlFunction.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 de95df58b..0d5d6eda7 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
@@ -73,14 +73,21 @@ protected final TemplateModel getBindStatusTemplateModel(Environment env, Object
RequestContext requestContext, String path, boolean ignoreNestedPath) throws TemplateException {
final String resolvedPath = (ignoreNestedPath) ? path : resolveNestedPath(env, objectWrapperAndUnwrapper, path);
BindStatus status = requestContext.getBindStatus(resolvedPath, false);
+ return wrapObject(objectWrapperAndUnwrapper, status);
+ }
+
+ protected final Object unwrapObject(ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, TemplateModel model) throws TemplateException {
+ return (model != null) ? objectWrapperAndUnwrapper.unwrap(model) : null;
+ }
- if (status != null) {
+ protected final TemplateModel wrapObject(ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, Object object) throws TemplateException {
+ if (object != null) {
if (!(objectWrapperAndUnwrapper instanceof DefaultObjectWrapper)) {
CallableUtils.newGenericExecuteException("objectWrapperAndUnwrapper is not a DefaultObjectWrapper.",
this, isFunction());
}
- return ((DefaultObjectWrapper) objectWrapperAndUnwrapper).wrap(status);
+ return ((DefaultObjectWrapper) objectWrapperAndUnwrapper).wrap(object);
}
return null;
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
index c98db6707..e35b6ee4d 100644
--- 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
@@ -30,7 +30,6 @@
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;
@@ -104,7 +103,7 @@ public ArgumentArrayLayout getDirectiveArgumentArrayLayout() {
}
private final TemplateModel getBindErrorsTemplateModel(Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper,
- RequestContext requestContext, String name) throws ObjectWrappingException {
+ RequestContext requestContext, String name) throws TemplateException {
final Errors errors = requestContext.getErrors(name, false);
if (errors != null && errors.hasErrors()) {
@@ -113,7 +112,7 @@ private final TemplateModel getBindErrorsTemplateModel(Environment env, ObjectWr
this, isFunction());
}
- return ((DefaultObjectWrapper) objectWrapperAndUnwrapper).wrap(errors);
+ return wrapObject(objectWrapperAndUnwrapper, errors);
}
return null;
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/EvalFunction.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/EvalFunction.java
new file mode 100644
index 000000000..a380f19ce
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/EvalFunction.java
@@ -0,0 +1,153 @@
+/*
+ * 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 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.context.expression.BeanFactoryResolver;
+import org.springframework.context.expression.EnvironmentAccessor;
+import org.springframework.context.expression.MapAccessor;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.expression.AccessException;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.PropertyAccessor;
+import org.springframework.expression.TypedValue;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.expression.spel.support.StandardTypeConverter;
+import org.springframework.web.servlet.support.RequestContext;
+
+/**
+ * A TemplateFunctionModel providing functionality equivalent to the Spring Framework's
+ * <spring:eval /> JSP Tag Library.
+ *
+ * Some valid example(s):
+ *
+ *
+ *
+ *
+ * Note: Unlike Spring Framework's <spring:message /> JSP Tag Library, this function
+ * does not support htmlEscape parameter. It always returns the message not to escape HTML's
+ * because it is much easier to control escaping in FreeMarker Template expressions.
+ *
+ */
+public class EvalFunction extends AbstractSpringTemplateFunctionModel {
+
+ public static final String NAME = "eval";
+
+ private static final int EXPRESSION_PARAM_IDX = 0;
+
+ private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, false, null, false);
+
+ private final ExpressionParser expressionParser = new SpelExpressionParser();
+
+ public EvalFunction(HttpServletRequest request, HttpServletResponse response) {
+ super(request, response);
+ }
+
+ @Override
+ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, Environment env,
+ ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
+ throws TemplateException {
+ final String expressionString = CallableUtils.getStringArgument(args, EXPRESSION_PARAM_IDX, this);
+ final Expression expression = expressionParser.parseExpression(expressionString);
+
+ // TODO: cache evaluationContext somewhere in request level....
+ EvaluationContext evaluationContext = createEvaluationContext(env, requestContext);
+
+ final Object result = expression.getValue(evaluationContext);
+ return wrapObject(objectWrapperAndUnwrapper, result);
+ }
+
+ @Override
+ public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
+ return ARGS_LAYOUT;
+ }
+
+ private EvaluationContext createEvaluationContext(final Environment env, final RequestContext requestContext) {
+ StandardEvaluationContext context = new StandardEvaluationContext();
+
+ context.addPropertyAccessor(new EnvironmentVariablesPropertyAccessor(env));
+ context.addPropertyAccessor(new MapAccessor());
+ context.addPropertyAccessor(new EnvironmentAccessor());
+ context.setBeanResolver(new BeanFactoryResolver(requestContext.getWebApplicationContext()));
+
+ ConversionService conversionService = getConversionService();
+
+ if (conversionService != null) {
+ context.setTypeConverter(new StandardTypeConverter(conversionService));
+ }
+
+ return context;
+ }
+
+ private ConversionService getConversionService() {
+ return (ConversionService) getRequest().getAttribute(ConversionService.class.getName());
+ }
+
+ private static class EnvironmentVariablesPropertyAccessor implements PropertyAccessor {
+
+ private final Environment env;
+
+ public EnvironmentVariablesPropertyAccessor(final Environment env) {
+ this.env = env;
+ }
+
+ @Override
+ public Class>[] getSpecificTargetClasses() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public void write(EvaluationContext context, Object target, String name, Object newValue)
+ throws AccessException {
+ // TODO Auto-generated method stub
+ }
+ }
+}
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 46547a674..94a1e0f3b 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
@@ -33,7 +33,6 @@
import org.apache.freemarker.core.model.TemplateCollectionModel;
import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.TemplateModelIterator;
-import org.apache.freemarker.core.model.impl.SimpleString;
import org.apache.freemarker.core.util.CallableUtils;
import org.apache.freemarker.core.util.StringToIndexMap;
import org.apache.freemarker.core.util._StringUtils;
@@ -111,10 +110,10 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace,
final TemplateModel messageResolvableModel = CallableUtils.getOptionalArgument(args, MESSAGE_RESOLVABLE_PARAM_IDX,
TemplateModel.class, this);
+ final MessageSourceResolvable messageResolvable = (MessageSourceResolvable) unwrapObject(
+ objectWrapperAndUnwrapper, messageResolvableModel);
- if (messageResolvableModel != null) {
- MessageSourceResolvable messageResolvable = (MessageSourceResolvable) objectWrapperAndUnwrapper
- .unwrap(messageResolvableModel);
+ if (messageResolvable != null) {
message = messageSource.getMessage(messageResolvable, requestContext.getLocale());
} else {
final String code = _StringUtils
@@ -127,10 +126,9 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace,
if (!messageArgsModel.isEmptyCollection()) {
msgArgumentList = new ArrayList<>();
TemplateModel msgArgModel;
- int i = 0;
- for (TemplateModelIterator tit = messageArgsModel.iterator(); tit.hasNext(); i++) {
+ for (TemplateModelIterator tit = messageArgsModel.iterator(); tit.hasNext(); ) {
msgArgModel = tit.next();
- msgArgumentList.add(objectWrapperAndUnwrapper.unwrap(msgArgModel));
+ msgArgumentList.add(unwrapObject(objectWrapperAndUnwrapper, msgArgModel));
}
}
@@ -143,7 +141,7 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace,
}
}
- return (message != null) ? new SimpleString(message) : null;
+ return wrapObject(objectWrapperAndUnwrapper, message);
}
@Override
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MvcUrlFunction.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MvcUrlFunction.java
new file mode 100644
index 000000000..047d5c2c8
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MvcUrlFunction.java
@@ -0,0 +1,74 @@
+/*
+ * 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 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.StringToIndexMap;
+import org.springframework.web.servlet.support.RequestContext;
+
+/**
+ * A TemplateFunctionModel providing functionality equivalent to the Spring Framework's
+ * spring:mvcUrl JSP Tag Library Function.
+ *
+ * Some valid example(s):
+ *
+ *
+ *
+ */
+public class MvcUrlFunction extends AbstractSpringTemplateFunctionModel {
+
+ public static final String NAME = "mvcUrl";
+
+ private static final int MAPPING_NAME_PARAM_IDX = 0;
+
+ private static final ArgumentArrayLayout ARGS_LAYOUT =
+ ArgumentArrayLayout.create(
+ 1,
+ false,
+ null,
+ false
+ );
+
+ public MvcUrlFunction(HttpServletRequest request, HttpServletResponse response) {
+ super(request, response);
+ }
+
+ @Override
+ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, Environment env,
+ ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
+ throws TemplateException {
+ // TODO
+ return null;
+ }
+
+ @Override
+ public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
+ return ARGS_LAYOUT;
+ }
+
+}
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
index dd2f71c35..e090cd92c 100644
--- 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
@@ -47,11 +47,16 @@ public final class SpringTemplateCallableHashModel implements TemplateHashModel,
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));
+ callablesMap.put(BindDirective.NAME, new BindDirective(request, response));
+
+ callablesMap.put(TransformFunction.NAME, new TransformFunction(request, response));
+ callablesMap.put(UrlFunction.NAME, new UrlFunction(request, response));
+ callablesMap.put(EvalFunction.NAME, new EvalFunction(request, response));
+ callablesMap.put(MvcUrlFunction.NAME, new MvcUrlFunction(request, response));
}
public TemplateModel get(String key) throws TemplateException {
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/TransformFunction.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/TransformFunction.java
new file mode 100644
index 000000000..ee3cc1c84
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/TransformFunction.java
@@ -0,0 +1,101 @@
+/*
+ * 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.beans.PropertyEditor;
+
+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.web.servlet.support.RequestContext;
+
+/**
+ * A TemplateFunctionModel providing functionality equivalent to the Spring Framework's
+ * <spring:transform /> JSP Tag Library.
+ *
+ * Note: Unlike Spring Framework's <spring:bind /> JSP Tag Library, this directive
+ * does not support htmlEscape parameter. It always has BindStatus not to escape HTML's
+ * because it is much easier to control escaping in FreeMarker Template expressions.
+ *
+ */
+public class TransformFunction extends AbstractSpringTemplateFunctionModel {
+
+ public static final String NAME = "transform";
+
+ private static final int PROPERTY_EDITOR_PARAM_IDX = 0;
+ private static final int VALUE_PARAM_IDX = 1;
+
+ private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(2, false, null, false);
+
+ public TransformFunction(HttpServletRequest request, HttpServletResponse response) {
+ super(request, response);
+ }
+
+ @Override
+ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, Environment env,
+ ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
+ throws TemplateException {
+ final TemplateModel editorModel = CallableUtils.getOptionalArgument(args, PROPERTY_EDITOR_PARAM_IDX,
+ TemplateModel.class, this);
+ final PropertyEditor editor = (PropertyEditor) unwrapObject(objectWrapperAndUnwrapper, editorModel);
+
+ final TemplateModel valueModel = CallableUtils.getOptionalArgument(args, VALUE_PARAM_IDX, TemplateModel.class, this);
+ final Object value = unwrapObject(objectWrapperAndUnwrapper, valueModel);
+
+ String valueAsString = null;
+
+ if (value != null) {
+ if (editor != null) {
+ editor.setValue(value);
+ valueAsString = editor.getAsText();
+ } else {
+ valueAsString = value.toString();
+ }
+ }
+
+ return wrapObject(objectWrapperAndUnwrapper, valueAsString);
+ }
+
+ @Override
+ public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
+ return ARGS_LAYOUT;
+ }
+
+}
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/UrlFunction.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/UrlFunction.java
new file mode 100644
index 000000000..402e74d03
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/UrlFunction.java
@@ -0,0 +1,86 @@
+/*
+ * 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 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.StringToIndexMap;
+import org.springframework.web.servlet.support.RequestContext;
+
+/**
+ * A TemplateFunctionModel providing functionality equivalent to the Spring Framework's
+ * <spring:url /> JSP Tag Library.
+ *
+ * Some valid example(s):
+ *
+ *
+ *
+ *
+ * Note: Unlike Spring Framework's <spring:message /> JSP Tag Library, this function
+ * does not support htmlEscape parameter. It always returns the message not to escape HTML's
+ * because it is much easier to control escaping in FreeMarker Template expressions.
+ *
* Note: Unlike Spring Framework's <spring:message /> JSP Tag Library, this function
@@ -93,7 +97,7 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace,
final String value = CallableUtils.getStringArgument(args, VALUE_PARAM_IDX, this);
final String context = CallableUtils.getOptionalStringArgument(args, CONTEXT_PARAM_IDX, this);
- List<_KeyValuePair> params = null;
+ List<_KeyValuePair> params = Collections.emptyList();
final TemplateHashModelEx2 paramsHashModel = (TemplateHashModelEx2) args[PARAMS_PARAM_IDX];
if (!paramsHashModel.isEmptyHash()) {
From 834abd7d6f338ee7fcc5bdaa060ce2d36495fc8b Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Mon, 11 Sep 2017 16:50:45 -0400
Subject: [PATCH 17/32] FREEMARKER-55: code cleanups
---
.../AbstractSpringTemplateCallableModel.java | 17 +++----
.../freemarker/spring/model/EvalFunction.java | 15 +++++-
.../spring/model/NestedPathDirective.java | 21 ++++----
.../SpringTemplateCallableHashModel.java | 49 ++++++++++---------
4 files changed, 55 insertions(+), 47 deletions(-)
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 0d5d6eda7..384a47be2 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
@@ -27,6 +27,7 @@
import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
import org.apache.freemarker.core.model.TemplateCallableModel;
import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateStringModel;
import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
import org.apache.freemarker.core.util.CallableUtils;
import org.springframework.web.servlet.support.BindStatus;
@@ -95,21 +96,15 @@ protected final TemplateModel wrapObject(ObjectWrapperAndUnwrapper objectWrapper
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);
+ protected SpringTemplateCallableHashModel getSpringTemplateCallableHashModel(final Environment env)
+ throws TemplateException {
+ return (SpringTemplateCallableHashModel) env.getVariable(SpringTemplateCallableHashModel.NAME);
}
private String resolveNestedPath(final Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper,
final String path) throws TemplateException {
- String curNestedPath = getCurrentNestedPath(env);
+ final TemplateStringModel curNestedPathModel = getSpringTemplateCallableHashModel(env).getNestedPathModel();
+ final String curNestedPath = (curNestedPathModel != null) ? curNestedPathModel.getAsString() : null;
if (curNestedPath != null && !path.startsWith(curNestedPath)
&& !path.equals(curNestedPath.substring(0, curNestedPath.length() - 1))) {
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/EvalFunction.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/EvalFunction.java
index 01c3cc57e..6ae0bb5f6 100644
--- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/EvalFunction.java
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/EvalFunction.java
@@ -66,6 +66,8 @@ public class EvalFunction extends AbstractSpringTemplateFunctionModel {
private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, false, null, false);
+ private static final String EVALUATION_CONTEXT_VAR_NAME = "org.apache.freemarker.spring.model.EVALUATION_CONTEXT";
+
private final ExpressionParser expressionParser = new SpelExpressionParser();
public EvalFunction(HttpServletRequest request, HttpServletResponse response) {
@@ -79,8 +81,17 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace,
final String expressionString = CallableUtils.getStringArgument(args, EXPRESSION_PARAM_IDX, this);
final Expression expression = expressionParser.parseExpression(expressionString);
- // TODO: cache evaluationContext somewhere in request level....
- EvaluationContext evaluationContext = createEvaluationContext(env, objectWrapperAndUnwrapper, requestContext);
+ EvaluationContext evaluationContext = null;
+ final SpringTemplateCallableHashModel springTemplateModel = getSpringTemplateCallableHashModel(env);
+ TemplateModel evaluationContextModel = springTemplateModel.get(EVALUATION_CONTEXT_VAR_NAME);
+
+ if (evaluationContextModel != null) {
+ evaluationContext = (EvaluationContext) unwrapObject(objectWrapperAndUnwrapper, evaluationContextModel);
+ } else {
+ evaluationContext = createEvaluationContext(env, objectWrapperAndUnwrapper, requestContext);
+ evaluationContextModel = wrapObject(objectWrapperAndUnwrapper, evaluationContext);
+ springTemplateModel.setEvaluationContextModel(evaluationContextModel);
+ }
final Object result = expression.getValue(evaluationContext);
return wrapObject(objectWrapperAndUnwrapper, result);
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
index 37d437003..9bd51ef6d 100644
--- 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
@@ -31,6 +31,7 @@
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.model.TemplateStringModel;
import org.apache.freemarker.core.util.CallableUtils;
import org.springframework.beans.PropertyAccessor;
import org.springframework.web.servlet.support.RequestContext;
@@ -59,13 +60,7 @@ public class NestedPathDirective extends AbstractSpringTemplateDirectiveModel {
private static final int PATH_PARAM_IDX = 0;
- private static final ArgumentArrayLayout ARGS_LAYOUT =
- ArgumentArrayLayout.create(
- 1,
- false,
- null,
- false
- );
+ private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, false, null, false);
public NestedPathDirective(HttpServletRequest request, HttpServletResponse response) {
super(request, response);
@@ -85,14 +80,18 @@ protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer
path += PropertyAccessor.NESTED_PROPERTY_SEPARATOR;
}
- String prevNestedPath = getCurrentNestedPath(env);
- String newNestedPath = (prevNestedPath != null) ? prevNestedPath + path : path;
+ final SpringTemplateCallableHashModel springTemplateModel = getSpringTemplateCallableHashModel(env);
+ final TemplateStringModel prevNestedPathModel = springTemplateModel.getNestedPathModel();
+ final String prevNestedPath = (prevNestedPathModel != null) ? prevNestedPathModel.getAsString() : null;
+ final String newNestedPath = (prevNestedPath != null) ? prevNestedPath + path : path;
+ final TemplateStringModel newNestedPathModel = (TemplateStringModel) wrapObject(objectWrapperAndUnwrapper,
+ newNestedPath);
try {
- setCurrentNestedPath(env, newNestedPath);
+ springTemplateModel.setNestedPathModel(newNestedPathModel);
callPlace.executeNestedContent(null, out, env);
} finally {
- setCurrentNestedPath(env, prevNestedPath);
+ springTemplateModel.setNestedPathModel(prevNestedPathModel);
}
}
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
index e090cd92c..7315947aa 100644
--- 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
@@ -29,7 +29,7 @@
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;
+import org.apache.freemarker.core.model.TemplateStringModel;
/**
* TemplateHashModel wrapper for templates using Spring directives and functions.
@@ -40,31 +40,26 @@ public final class SpringTemplateCallableHashModel implements TemplateHashModel,
public static final String NAME = "spring";
- public static final String NESTED_PATH = "nestedPath";
+ public static final String NESTED_PATH_MODEL = "nestedPathModel";
- private Map callablesMap = new HashMap<>();
+ public static final String EVALUATION_CONTEXT_MODEL = "evaluationContextModel";
- private String nestedPath;
+ private Map modelsMap = new HashMap<>();
public SpringTemplateCallableHashModel(final HttpServletRequest request, final HttpServletResponse 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));
- callablesMap.put(BindDirective.NAME, new BindDirective(request, response));
-
- callablesMap.put(TransformFunction.NAME, new TransformFunction(request, response));
- callablesMap.put(UrlFunction.NAME, new UrlFunction(request, response));
- callablesMap.put(EvalFunction.NAME, new EvalFunction(request, response));
- callablesMap.put(MvcUrlFunction.NAME, new MvcUrlFunction(request, response));
+ modelsMap.put(MessageFunction.NAME, new MessageFunction(request, response));
+ modelsMap.put(ThemeFunction.NAME, new ThemeFunction(request, response));
+ modelsMap.put(BindErrorsDirective.NAME, new BindErrorsDirective(request, response));
+ modelsMap.put(NestedPathDirective.NAME, new NestedPathDirective(request, response));
+ modelsMap.put(BindDirective.NAME, new BindDirective(request, response));
+ modelsMap.put(TransformFunction.NAME, new TransformFunction(request, response));
+ modelsMap.put(UrlFunction.NAME, new UrlFunction(request, response));
+ modelsMap.put(EvalFunction.NAME, new EvalFunction(request, response));
+ modelsMap.put(MvcUrlFunction.NAME, new MvcUrlFunction(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);
+ return modelsMap.get(key);
}
@Override
@@ -72,12 +67,20 @@ public boolean isEmptyHash() throws TemplateException {
return false;
}
- public String getNestedPath() {
- return nestedPath;
+ public TemplateStringModel getNestedPathModel() throws TemplateException {
+ return (TemplateStringModel) get(NESTED_PATH_MODEL);
+ }
+
+ public void setNestedPathModel(TemplateStringModel nestedPathModel) {
+ modelsMap.put(NESTED_PATH_MODEL, nestedPathModel);
+ }
+
+ public TemplateModel getEvaluationContextModel() throws TemplateException {
+ return (TemplateModel) get(EVALUATION_CONTEXT_MODEL);
}
- public void setNestedPath(String nestedPath) {
- this.nestedPath = nestedPath;
+ public void setEvaluationContextModel(TemplateModel evaluationContextModel) {
+ modelsMap.put(EVALUATION_CONTEXT_MODEL, evaluationContextModel);
}
}
From 2076c5e83786b5253ea80651f4a47ea87f263fd0 Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Mon, 11 Sep 2017 17:14:39 -0400
Subject: [PATCH 18/32] FREEMARKER-55: Removing mvcUrl function as it's not in
earlier version of spring 4.0.x
---
.../spring/model/MvcUrlFunction.java | 74 -------------------
.../SpringTemplateCallableHashModel.java | 1 -
2 files changed, 75 deletions(-)
delete mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MvcUrlFunction.java
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MvcUrlFunction.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MvcUrlFunction.java
deleted file mode 100644
index 047d5c2c8..000000000
--- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MvcUrlFunction.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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 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.StringToIndexMap;
-import org.springframework.web.servlet.support.RequestContext;
-
-/**
- * A TemplateFunctionModel providing functionality equivalent to the Spring Framework's
- * spring:mvcUrl JSP Tag Library Function.
- *
- * Some valid example(s):
- *
- *
- *
- */
-public class MvcUrlFunction extends AbstractSpringTemplateFunctionModel {
-
- public static final String NAME = "mvcUrl";
-
- private static final int MAPPING_NAME_PARAM_IDX = 0;
-
- private static final ArgumentArrayLayout ARGS_LAYOUT =
- ArgumentArrayLayout.create(
- 1,
- false,
- null,
- false
- );
-
- public MvcUrlFunction(HttpServletRequest request, HttpServletResponse response) {
- super(request, response);
- }
-
- @Override
- public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, Environment env,
- ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
- throws TemplateException {
- // TODO
- return null;
- }
-
- @Override
- public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
- return ARGS_LAYOUT;
- }
-
-}
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
index 7315947aa..26ae060d2 100644
--- 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
@@ -55,7 +55,6 @@ public SpringTemplateCallableHashModel(final HttpServletRequest request, final H
modelsMap.put(TransformFunction.NAME, new TransformFunction(request, response));
modelsMap.put(UrlFunction.NAME, new UrlFunction(request, response));
modelsMap.put(EvalFunction.NAME, new EvalFunction(request, response));
- modelsMap.put(MvcUrlFunction.NAME, new MvcUrlFunction(request, response));
}
public TemplateModel get(String key) throws TemplateException {
From 96c038d9ccc4a5d38cc8896370870d27af46af1f Mon Sep 17 00:00:00 2001
From: Woonsan Ko
Date: Mon, 11 Sep 2017 23:21:50 -0400
Subject: [PATCH 19/32] FREEMARKER-55: unit test template code
---
.../spring/example/mvc/users/User.java | 80 ++++++++++++++
.../example/mvc/users/UserController.java | 66 +++++++++++
.../example/mvc/users/UserRepository.java | 103 ++++++++++++++++++
.../spring/model/MessageFunctionTest.java | 61 +++++++++++
.../views/example/users/useredit.ftl | 88 +++++++++++++++
.../views/example/users/userlist.ftl | 44 ++++++++
.../model/MessageFunctionTest-context.xml | 46 ++++++++
.../spring/model/UsersMessages.properties | 7 ++
8 files changed, 495 insertions(+)
create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java
create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java
create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java
create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/model/MessageFunctionTest.java
create mode 100644 freemarker-spring/src/test/resources/META-INF/web-resources/views/example/users/useredit.ftl
create mode 100644 freemarker-spring/src/test/resources/META-INF/web-resources/views/example/users/userlist.ftl
create mode 100644 freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml
create mode 100644 freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/UsersMessages.properties
diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java
new file mode 100644
index 000000000..a1550b27e
--- /dev/null
+++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java
@@ -0,0 +1,80 @@
+/*
+ * 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.example.mvc.users;
+
+import java.util.Date;
+
+public class User {
+
+ private final String id;
+ private String password;
+ private String email;
+ private String firstName;
+ private String lastName;
+ private Date birthDate;
+
+ public User(final String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public Date getBirthDate() {
+ return birthDate;
+ }
+
+ public void setBirthDate(Date birthDate) {
+ this.birthDate = birthDate;
+ }
+}
diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java
new file mode 100644
index 000000000..e8c0d866d
--- /dev/null
+++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java
@@ -0,0 +1,66 @@
+/*
+ * 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.example.mvc.users;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+@Controller
+public class UserController {
+
+ @Autowired
+ private UserRepository userRepository;
+
+ @RequestMapping(value = "/users", method = RequestMethod.GET)
+ public String listUsers(Model model) {
+ List users = new LinkedList<>();
+
+ for (String id : userRepository.getUserIds()) {
+ users.add(userRepository.getUser(id));
+ }
+
+ model.addAttribute("users", users);
+
+ return "example/users/userlist";
+ }
+
+ @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
+ public String editUser(@PathVariable("id") String id, Model model) {
+ User user = userRepository.getUser(id);
+ model.addAttribute("user", user);
+ return "example/users/useredit";
+ }
+
+ public UserRepository getUserRepository() {
+ return userRepository;
+ }
+
+ public void setUserRepository(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+}
diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java
new file mode 100644
index 000000000..3e53d036c
--- /dev/null
+++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java
@@ -0,0 +1,103 @@
+/*
+ * 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.example.mvc.users;
+
+import java.util.Calendar;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class UserRepository {
+
+ private Map usersMap = new ConcurrentHashMap<>();
+ {
+ String id = "13c2ec8c-102c-4883-a282-3fe983e61515";
+ User user = new User(id);
+ user.setEmail("john@example.com");
+ user.setFirstName("John");
+ user.setLastName("Doe");
+ Calendar birthDate = Calendar.getInstance();
+ birthDate.set(Calendar.YEAR, 1971);
+ birthDate.set(Calendar.MONTH, Calendar.JANUARY);
+ birthDate.set(Calendar.DATE, 5);
+ user.setBirthDate(birthDate.getTime());
+ usersMap.put(id, user);
+
+ id = "04d6080b-2098-4eaf-90ee-7331caab5e91";
+ user = new User(id);
+ user.setEmail("jane@example.com");
+ user.setFirstName("Jane");
+ user.setLastName("Doe");
+ birthDate = Calendar.getInstance();
+ birthDate.set(Calendar.YEAR, 1970);
+ birthDate.set(Calendar.MONTH, Calendar.FEBRUARY);
+ birthDate.set(Calendar.DATE, 7);
+ user.setBirthDate(birthDate.getTime());
+ usersMap.put(id, user);
+ }
+
+ public synchronized Set getUserIds() {
+ return new TreeSet<>(usersMap.keySet());
+ }
+
+ public synchronized User getUser(final String id) {
+ if (id == null) {
+ throw new IllegalArgumentException("ID must be non-null.");
+ }
+
+ User user = usersMap.get(id);
+
+ if (user != null) {
+ return cloneUser(user, user.getId());
+ }
+
+ return null;
+ }
+
+ public synchronized User addOrUpdateUser(final User user) {
+ final String id = user.getId();
+ User newUser = cloneUser(user, id);
+ usersMap.put(id, newUser);
+ return cloneUser(newUser, id);
+ }
+
+ public synchronized boolean deleteUser(final String username) {
+ if (username == null) {
+ throw new IllegalArgumentException("Username must be non-null.");
+ }
+
+ final User user = usersMap.remove(username);
+ return user != null;
+ }
+
+ private User cloneUser(final User source, final String id) {
+ User clone = new User(id);
+ clone.setPassword(source.getPassword());
+ clone.setEmail(source.getEmail());
+ clone.setFirstName(source.getFirstName());
+ clone.setLastName(source.getLastName());
+ clone.setBirthDate(source.getBirthDate());
+ return clone;
+ }
+}
diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/MessageFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/MessageFunctionTest.java
new file mode 100644
index 000000000..03c32f458
--- /dev/null
+++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/MessageFunctionTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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 static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.xpath;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@WebAppConfiguration("classpath:META-INF/web-resources")
+@ContextConfiguration("MessageFunctionTest-context.xml")
+public class MessageFunctionTest {
+
+ @Autowired
+ private WebApplicationContext wac;
+
+ private MockMvc mockMvc;
+
+ @Before
+ public void setup() {
+ mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
+ }
+
+ @Test
+ public void getUsers() throws Exception {
+ mockMvc.perform(get("/users").accept(MediaType.parseMediaType("text/html")))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("text/html"))
+ .andExpect(xpath("/html/head/title").string("Spring MVC Form Example - Users"));
+ }
+}
diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/example/users/useredit.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/example/users/useredit.ftl
new file mode 100644
index 000000000..c206e9aeb
--- /dev/null
+++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/example/users/useredit.ftl
@@ -0,0 +1,88 @@
+<#ftl outputFormat="HTML">
+<#--
+ 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.
+-->
+
+
+Spring MVC Form Example - User Edit Form
+
+
+
+
+
+
+
+
+
diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/example/users/userlist.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/example/users/userlist.ftl
new file mode 100644
index 000000000..25c757f78
--- /dev/null
+++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/example/users/userlist.ftl
@@ -0,0 +1,44 @@
+<#ftl outputFormat="HTML">
+<#--
+ 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.
+-->
+
+
+Spring MVC Form Example - Users
+
+
+