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 msgArgumentList = null; + // TODO: How to read message arguments from the varags? + + // TODO: Is it okay to set the default value to null to avoid NoSuchMessageException from Spring MessageSource? + message = messageSource.getMessage(code, (msgArgumentList == null) ? null : msgArgumentList.toArray(), + null, requestContext.getLocale()); + } else { + final TemplateModel messageModel = CallableUtils.getOptionalArgument(args, MESSAGE_PARAM_IDX, + TemplateModel.class, this); + if (messageModel != null) { + MessageSourceResolvable messageResolvable = (MessageSourceResolvable) objectWrapperAndUnwrapper + .unwrap(messageModel); + message = messageSource.getMessage(messageResolvable, requestContext.getLocale()); + } + } + + return (message != null) ? new SimpleString(message) : null; + } + + @Override + public ArgumentArrayLayout getFunctionArgumentArrayLayout() { + return ARGS_LAYOUT; + } + +} 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 1e94a9793..f34fd4be4 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 @@ -39,6 +39,7 @@ 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; /** * FreeMarker template based view implementation, with being able to provide a {@link ServletContextHashModel} @@ -178,6 +179,7 @@ private TemplateHashModelEx2 createSpringCallableHashModel(final ObjectWrapper o 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)); return springCallableHash; } } From 8f5eaaaf2df1914fef461802f2d7b511d4a99870 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Sun, 3 Sep 2017 00:08:31 -0400 Subject: [PATCH 09/32] FREEMARKER-55: read message args from positioned vargs. --- .../freemarker/spring/model/MessageFunction.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 d44096367..7bee8c1a2 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 @@ -19,6 +19,7 @@ package org.apache.freemarker.spring.model; +import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -29,7 +30,9 @@ 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.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; @@ -41,6 +44,7 @@ public class MessageFunction extends AbstractSpringTemplateFunctionModel { private static final int CODE_PARAM_IDX = 0; private static final int MESSAGE_PARAM_IDX = 1; + private static final int MESSAGE_ARGS_PARAM_IDX = 2; private static final String MESSAGE_PARAM_NAME = "message"; @@ -71,8 +75,17 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, final String code = CallableUtils.getStringArgument(args, CODE_PARAM_IDX, this); if (code != null && !code.isEmpty()) { + final TemplateCollectionModel messageArgsModel = (TemplateCollectionModel) args[MESSAGE_ARGS_PARAM_IDX]; List msgArgumentList = null; - // TODO: How to read message arguments from the varags? + + if (!messageArgsModel.isEmptyCollection()) { + msgArgumentList = new ArrayList<>(); + TemplateModel msgArgModel; + for (TemplateModelIterator tit = messageArgsModel.iterator(); tit.hasNext(); ) { + msgArgModel = tit.next(); + msgArgumentList.add(objectWrapperAndUnwrapper.unwrap(msgArgModel)); + } + } // TODO: Is it okay to set the default value to null to avoid NoSuchMessageException from Spring MessageSource? message = messageSource.getMessage(code, (msgArgumentList == null) ? null : msgArgumentList.toArray(), From b0aceddab9ac55c622fb596d2ed03444c2ff0884 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Sun, 3 Sep 2017 00:27:08 -0400 Subject: [PATCH 10/32] FREEMARKER-55: use vargs for code and args --- .../spring/model/MessageFunction.java | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) 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 7bee8c1a2..9579366c5 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 @@ -42,15 +42,14 @@ public class MessageFunction extends AbstractSpringTemplateFunctionModel { - private static final int CODE_PARAM_IDX = 0; - private static final int MESSAGE_PARAM_IDX = 1; - private static final int MESSAGE_ARGS_PARAM_IDX = 2; + private static final int MESSAGE_PARAM_IDX = 0; + private static final int MESSAGE_ARGS_PARAM_IDX = 1; private static final String MESSAGE_PARAM_NAME = "message"; private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create( - 1, + 0, true, StringToIndexMap.of( MESSAGE_PARAM_NAME, MESSAGE_PARAM_IDX), @@ -72,23 +71,24 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, String message = null; - final String code = CallableUtils.getStringArgument(args, CODE_PARAM_IDX, this); - - if (code != null && !code.isEmpty()) { - final TemplateCollectionModel messageArgsModel = (TemplateCollectionModel) args[MESSAGE_ARGS_PARAM_IDX]; - List msgArgumentList = null; - - if (!messageArgsModel.isEmptyCollection()) { - msgArgumentList = new ArrayList<>(); - TemplateModel msgArgModel; - for (TemplateModelIterator tit = messageArgsModel.iterator(); tit.hasNext(); ) { - msgArgModel = tit.next(); + final TemplateCollectionModel messageArgsModel = (TemplateCollectionModel) args[MESSAGE_ARGS_PARAM_IDX]; + + if (!messageArgsModel.isEmptyCollection()) { + String code = null; + List msgArgumentList = new ArrayList<>(); + TemplateModel msgArgModel; + int i = 0; + for (TemplateModelIterator tit = messageArgsModel.iterator(); tit.hasNext(); i++) { + msgArgModel = tit.next(); + if (i == 0) { + code = objectWrapperAndUnwrapper.unwrap(msgArgModel).toString(); + } else { msgArgumentList.add(objectWrapperAndUnwrapper.unwrap(msgArgModel)); } } // TODO: Is it okay to set the default value to null to avoid NoSuchMessageException from Spring MessageSource? - message = messageSource.getMessage(code, (msgArgumentList == null) ? null : msgArgumentList.toArray(), + message = messageSource.getMessage(code, (msgArgumentList.isEmpty()) ? null : msgArgumentList.toArray(), null, requestContext.getLocale()); } else { final TemplateModel messageModel = CallableUtils.getOptionalArgument(args, MESSAGE_PARAM_IDX, @@ -97,6 +97,8 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, MessageSourceResolvable messageResolvable = (MessageSourceResolvable) objectWrapperAndUnwrapper .unwrap(messageModel); message = messageSource.getMessage(messageResolvable, requestContext.getLocale()); + } else { + throw new TemplateException("Neither message code nor message resolvable was set."); } } From a8e73ec77a735b643be986e0c190205f3b7a2192 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 5 Sep 2017 08:33:55 -0400 Subject: [PATCH 11/32] Using CallableUtils to throw exceptions; use positional param for code --- .../AbstractSpringTemplateCallableModel.java | 6 +- .../AbstractSpringTemplateDirectiveModel.java | 17 +++- .../AbstractSpringTemplateFunctionModel.java | 14 +++- .../spring/model/MessageFunction.java | 77 +++++++++++-------- .../spring/model/ThemeFunction.java | 38 +++++++++ .../spring/web/view/FreeMarkerView.java | 2 + 6 files changed, 112 insertions(+), 42 deletions(-) create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/ThemeFunction.java diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java index 431c066f6..b82bed151 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 @@ -28,6 +28,7 @@ import org.apache.freemarker.core.model.TemplateCallableModel; 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.web.servlet.support.BindStatus; import org.springframework.web.servlet.support.RequestContext; @@ -81,7 +82,8 @@ protected final TemplateModel getBindStatusTemplateModel(Environment env, Object if (status != null) { if (!(objectWrapperAndUnwrapper instanceof DefaultObjectWrapper)) { - throw new IllegalArgumentException("objectWrapperAndUnwrapper is not a DefaultObjectWrapper."); + CallableUtils.newGenericExecuteException("objectWrapperAndUnwrapper is not a DefaultObjectWrapper.", + this, isFunction()); } return ((DefaultObjectWrapper) objectWrapperAndUnwrapper).wrap(status); @@ -90,6 +92,8 @@ protected final TemplateModel getBindStatusTemplateModel(Environment env, Object return null; } + protected abstract boolean isFunction(); + private String resolveNestedPath(final Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, final String path) { // TODO: should read it from request or env?? 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 c4ed76c9a..df6a2bc8d 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 @@ -32,13 +32,15 @@ import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper; import org.apache.freemarker.core.model.TemplateDirectiveModel; import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util.CallableUtils; 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 extends AbstractSpringTemplateCallableModel implements TemplateDirectiveModel { +public abstract class AbstractSpringTemplateDirectiveModel extends AbstractSpringTemplateCallableModel + implements TemplateDirectiveModel { public AbstractSpringTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) { super(request, response); @@ -50,14 +52,16 @@ public final void execute(TemplateModel[] args, CallPlace callPlace, Writer out, final ObjectWrapper objectWrapper = env.getObjectWrapper(); if (!(objectWrapper instanceof ObjectWrapperAndUnwrapper)) { - throw new TemplateException( - "The ObjectWrapper of environment wasn't instance of ObjectWrapperAndUnwrapper."); + CallableUtils.newGenericExecuteException( + "The ObjectWrapper of environment isn't an instance of ObjectWrapperAndUnwrapper.", this, + isFunction()); } TemplateModel rcModel = env.getVariable(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE); if (rcModel == null) { - throw new TemplateException(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found."); + CallableUtils.newGenericExecuteException( + AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found.", this, isFunction()); } RequestContext requestContext = (RequestContext) ((ObjectWrapperAndUnwrapper) objectWrapper).unwrap(rcModel); @@ -65,6 +69,11 @@ public final void execute(TemplateModel[] args, CallPlace callPlace, Writer out, executeInternal(args, callPlace, out, env, (ObjectWrapperAndUnwrapper) objectWrapper, requestContext); } + @Override + protected final boolean isFunction() { + return false; + } + protected abstract void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext) throws TemplateException, IOException; 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 index c0fe502c0..7851ad8ae 100644 --- 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 @@ -29,6 +29,7 @@ import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper; import org.apache.freemarker.core.model.TemplateFunctionModel; import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util.CallableUtils; import org.springframework.web.servlet.support.RequestContext; import org.springframework.web.servlet.view.AbstractTemplateView; @@ -47,14 +48,16 @@ public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environm final ObjectWrapper objectWrapper = env.getObjectWrapper(); if (!(objectWrapper instanceof ObjectWrapperAndUnwrapper)) { - throw new TemplateException( - "The ObjectWrapper of environment wasn't instance of ObjectWrapperAndUnwrapper."); + CallableUtils.newGenericExecuteException( + "The ObjectWrapper of environment isn't an instance of ObjectWrapperAndUnwrapper.", this, + isFunction()); } TemplateModel rcModel = env.getVariable(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE); if (rcModel == null) { - throw new TemplateException(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found."); + CallableUtils.newGenericExecuteException( + AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found.", this, isFunction()); } RequestContext requestContext = (RequestContext) ((ObjectWrapperAndUnwrapper) objectWrapper).unwrap(rcModel); @@ -62,6 +65,11 @@ public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environm return executeInternal(args, callPlace, env, (ObjectWrapperAndUnwrapper) objectWrapper, requestContext); } + @Override + protected final boolean isFunction() { + return true; + } + 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/MessageFunction.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/MessageFunction.java index 9579366c5..c6d1c69db 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 @@ -36,24 +36,26 @@ 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; 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 MESSAGE_PARAM_IDX = 0; - private static final int MESSAGE_ARGS_PARAM_IDX = 1; + 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; - private static final String MESSAGE_PARAM_NAME = "message"; + private static final String MESSAGE_RESOLVABLE_PARAM_NAME = "message"; private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create( - 0, + 1, true, - StringToIndexMap.of( - MESSAGE_PARAM_NAME, MESSAGE_PARAM_IDX), - false); + StringToIndexMap.of(MESSAGE_RESOLVABLE_PARAM_NAME, MESSAGE_RESOLVABLE_PARAM_IDX), + false + ); public MessageFunction(HttpServletRequest request, HttpServletResponse response) { super(request, response); @@ -63,42 +65,45 @@ public MessageFunction(HttpServletRequest request, HttpServletResponse response) public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext) throws TemplateException { - final MessageSource messageSource = requestContext.getMessageSource(); + final MessageSource messageSource = getMessageSource(requestContext); if (messageSource == null) { - throw new TemplateException("MessageSource not found."); + CallableUtils.newGenericExecuteException("MessageSource not found.", this); } String message = null; - final TemplateCollectionModel messageArgsModel = (TemplateCollectionModel) args[MESSAGE_ARGS_PARAM_IDX]; - - if (!messageArgsModel.isEmptyCollection()) { - String code = null; - List msgArgumentList = new ArrayList<>(); - TemplateModel msgArgModel; - int i = 0; - for (TemplateModelIterator tit = messageArgsModel.iterator(); tit.hasNext(); i++) { - msgArgModel = tit.next(); - if (i == 0) { - code = objectWrapperAndUnwrapper.unwrap(msgArgModel).toString(); - } else { - msgArgumentList.add(objectWrapperAndUnwrapper.unwrap(msgArgModel)); - } - } + final TemplateModel messageResolvableModel = CallableUtils.getOptionalArgument(args, MESSAGE_RESOLVABLE_PARAM_IDX, + TemplateModel.class, this); - // TODO: Is it okay to set the default value to null to avoid NoSuchMessageException from Spring MessageSource? - message = messageSource.getMessage(code, (msgArgumentList.isEmpty()) ? null : msgArgumentList.toArray(), - null, requestContext.getLocale()); + if (messageResolvableModel != null) { + MessageSourceResolvable messageResolvable = (MessageSourceResolvable) objectWrapperAndUnwrapper + .unwrap(messageResolvableModel); + message = messageSource.getMessage(messageResolvable, requestContext.getLocale()); } else { - final TemplateModel messageModel = CallableUtils.getOptionalArgument(args, MESSAGE_PARAM_IDX, - TemplateModel.class, this); - if (messageModel != null) { - MessageSourceResolvable messageResolvable = (MessageSourceResolvable) objectWrapperAndUnwrapper - .unwrap(messageModel); - message = messageSource.getMessage(messageResolvable, requestContext.getLocale()); + final String code = _StringUtils + .emptyToNull(CallableUtils.getOptionalStringArgument(args, CODE_PARAM_IDX, this)); + + if (code != null) { + List msgArgumentList = null; + final TemplateCollectionModel messageArgsModel = (TemplateCollectionModel) args[MESSAGE_ARGS_PARAM_IDX]; + + if (!messageArgsModel.isEmptyCollection()) { + msgArgumentList = new ArrayList<>(); + TemplateModel msgArgModel; + int i = 0; + for (TemplateModelIterator tit = messageArgsModel.iterator(); tit.hasNext(); i++) { + msgArgModel = tit.next(); + msgArgumentList.add(objectWrapperAndUnwrapper.unwrap(msgArgModel)); + } + } + + // Note: Pass null as default value to avoid NoSuchMessageException from Spring MessageSource + // since we want to take advantage of FreeMarker's default value expressions. + message = messageSource.getMessage(code, (msgArgumentList == null) ? null : msgArgumentList.toArray(), + null, requestContext.getLocale()); } else { - throw new TemplateException("Neither message code nor message resolvable was set."); + CallableUtils.newNullOrOmittedArgumentException(CODE_PARAM_IDX, this); } } @@ -110,4 +115,8 @@ public ArgumentArrayLayout getFunctionArgumentArrayLayout() { return ARGS_LAYOUT; } + protected MessageSource getMessageSource(final RequestContext requestContext) { + return requestContext.getMessageSource(); + } + } 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 new file mode 100644 index 000000000..b3b065355 --- /dev/null +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/ThemeFunction.java @@ -0,0 +1,38 @@ +/* + * 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.springframework.context.MessageSource; +import org.springframework.web.servlet.support.RequestContext; + +public class ThemeFunction extends MessageFunction { + + public ThemeFunction(HttpServletRequest request, HttpServletResponse response) { + super(request, response); + } + + protected MessageSource getMessageSource(final RequestContext requestContext) { + return requestContext.getTheme().getMessageSource(); + } + +} 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 f34fd4be4..a789f4edb 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 @@ -40,6 +40,7 @@ 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; /** * FreeMarker template based view implementation, with being able to provide a {@link ServletContextHashModel} @@ -180,6 +181,7 @@ private TemplateHashModelEx2 createSpringCallableHashModel(final ObjectWrapper o 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 12f70fff2db494e91bbc9926c76e529d781f4f18 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Wed, 6 Sep 2017 10:01:10 -0400 Subject: [PATCH 12/32] FREEMARKER-55: Adding more spring callable models. --- .../AbstractSpringTemplateCallableModel.java | 38 +++--- .../spring/model/BindDirective.java | 19 ++- .../spring/model/BindErrorsDirective.java | 122 ++++++++++++++++++ .../spring/model/MessageFunction.java | 36 ++++++ .../spring/model/NestedPathDirective.java | 109 ++++++++++++++++ .../SpringFormTemplateCallableHashModel.java | 56 ++++++++ .../SpringTemplateCallableHashModel.java | 78 +++++++++++ .../spring/model/ThemeFunction.java | 36 ++++++ .../spring/web/view/FreeMarkerView.java | 21 +-- 9 files changed, 480 insertions(+), 35 deletions(-) create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindErrorsDirective.java create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/NestedPathDirective.java create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringFormTemplateCallableHashModel.java create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringTemplateCallableHashModel.java diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java index b82bed151..de95df58b 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java @@ -23,8 +23,8 @@ import javax.servlet.http.HttpServletResponse; import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.TemplateException; import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper; -import org.apache.freemarker.core.model.ObjectWrappingException; import org.apache.freemarker.core.model.TemplateCallableModel; import org.apache.freemarker.core.model.TemplateModel; import org.apache.freemarker.core.model.impl.DefaultObjectWrapper; @@ -37,12 +37,6 @@ */ 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; @@ -73,10 +67,10 @@ protected final HttpServletResponse getResponse() { * @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 + * @throws TemplateException */ protected final TemplateModel getBindStatusTemplateModel(Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, - RequestContext requestContext, String path, boolean ignoreNestedPath) throws ObjectWrappingException { + RequestContext requestContext, String path, boolean ignoreNestedPath) throws TemplateException { final String resolvedPath = (ignoreNestedPath) ? path : resolveNestedPath(env, objectWrapperAndUnwrapper, path); BindStatus status = requestContext.getBindStatus(resolvedPath, false); @@ -94,15 +88,25 @@ protected final TemplateModel getBindStatusTemplateModel(Environment env, Object protected abstract boolean isFunction(); + protected String getCurrentNestedPath(final Environment env) throws TemplateException { + SpringTemplateCallableHashModel springHash = (SpringTemplateCallableHashModel) env + .getVariable(SpringTemplateCallableHashModel.NAME); + return springHash.getNestedPath(); + } + + protected void setCurrentNestedPath(final Environment env, final String nestedPath) throws TemplateException { + SpringTemplateCallableHashModel springHash = (SpringTemplateCallableHashModel) env + .getVariable(SpringTemplateCallableHashModel.NAME); + springHash.setNestedPath(nestedPath); + } + private String resolveNestedPath(final Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, - final String path) { - // TODO: should read it from request or env?? - // or read spring.nestedPath first and read request attribute next?? - String nestedPath = (String) request.getAttribute(NESTED_PATH_VARIABLE_NAME); - - if (nestedPath != null && !path.startsWith(nestedPath) - && !path.equals(nestedPath.substring(0, nestedPath.length() - 1))) { - return nestedPath + path; + final String path) throws TemplateException { + String curNestedPath = getCurrentNestedPath(env); + + if (curNestedPath != null && !path.startsWith(curNestedPath) + && !path.equals(curNestedPath.substring(0, curNestedPath.length() - 1))) { + return curNestedPath + path; } return path; 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 0fbbe2f66..99f5f86af 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindDirective.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindDirective.java @@ -41,18 +41,31 @@ *

* 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.
  • + *
  • path: The first positional parameter pointing to the bean or bean property to bind status information for.
  • + *
  • + * ignoreNestedPath: A named parameter to set whether to ignore a nested path, if any. + * false by default. + *
  • *
*

*

+ * Some valid example(s): + *

+ *
+ *   <@spring.bind "user.email"; status>
+ *     <input type="text" name="email" value="${status.value!}" />
+ *   </@spring.bind>
+ * 
+ *

* 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.
  • + *
+ *

+ *

+ * Some valid example(s): + *

+ *
+ *   <@spring.nestedPath "user">
+ *     <#-- nested content --/>
+ *   </@spring.nestedPath>
+ * 
+ */ +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. + *

+ * Some valid example(s): + *

+ *
+ * 
+ *

+ * Some valid example(s): + *

+ *
+ *   <@spring.bind "user"; status>
+ *     ${spring.transform(status, user.birthDate)}
+ *   </@spring.bind>
+ * 
+ *

+ * 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. + *

+ */ +public class UrlFunction extends AbstractSpringTemplateFunctionModel { + + public static final String NAME = "url"; + + private static final int VALUE_PARAM_IDX = 0; + private static final int CONTEXT_PARAM_IDX = 1; + + private static final String CONTEXT_PARAM_NAME = "context"; + + + // TODO: How to deal with parameters (spring:param tags)?? + + + private static final ArgumentArrayLayout ARGS_LAYOUT = + ArgumentArrayLayout.create( + 1, + false, + StringToIndexMap.of(CONTEXT_PARAM_NAME, CONTEXT_PARAM_IDX), + false + ); + + public UrlFunction(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; + } + +} From 6f025c79534117a0f87eb17445e1a76cf06be418 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Mon, 11 Sep 2017 14:13:56 -0400 Subject: [PATCH 14/32] FREEMARKER-55: use Environment variables instead of pageContext in eval function. --- .../freemarker/spring/model/EvalFunction.java | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) 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 a380f19ce..01c3cc57e 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 @@ -80,7 +80,7 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, final Expression expression = expressionParser.parseExpression(expressionString); // TODO: cache evaluationContext somewhere in request level.... - EvaluationContext evaluationContext = createEvaluationContext(env, requestContext); + EvaluationContext evaluationContext = createEvaluationContext(env, objectWrapperAndUnwrapper, requestContext); final Object result = expression.getValue(evaluationContext); return wrapObject(objectWrapperAndUnwrapper, result); @@ -91,10 +91,11 @@ public ArgumentArrayLayout getFunctionArgumentArrayLayout() { return ARGS_LAYOUT; } - private EvaluationContext createEvaluationContext(final Environment env, final RequestContext requestContext) { + private EvaluationContext createEvaluationContext(final Environment env, + final ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, final RequestContext requestContext) { StandardEvaluationContext context = new StandardEvaluationContext(); - context.addPropertyAccessor(new EnvironmentVariablesPropertyAccessor(env)); + context.addPropertyAccessor(new EnvironmentVariablesPropertyAccessor(env, objectWrapperAndUnwrapper)); context.addPropertyAccessor(new MapAccessor()); context.addPropertyAccessor(new EnvironmentAccessor()); context.setBeanResolver(new BeanFactoryResolver(requestContext.getWebApplicationContext())); @@ -112,42 +113,51 @@ private ConversionService getConversionService() { return (ConversionService) getRequest().getAttribute(ConversionService.class.getName()); } - private static class EnvironmentVariablesPropertyAccessor implements PropertyAccessor { + private class EnvironmentVariablesPropertyAccessor implements PropertyAccessor { private final Environment env; + private final ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper; - public EnvironmentVariablesPropertyAccessor(final Environment env) { + public EnvironmentVariablesPropertyAccessor(final Environment env, + final ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper) { this.env = env; + this.objectWrapperAndUnwrapper = objectWrapperAndUnwrapper; } @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; + try { + return (target == null && env.getVariable(name) != null); + } catch (TemplateException e) { + throw new AccessException("Can't get environment variable by name, '" + name + "'.", e); + } } @Override public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { - // TODO Auto-generated method stub - return null; + try { + TemplateModel model = env.getVariable(name); + Object value = unwrapObject(objectWrapperAndUnwrapper, model); + return new TypedValue(value); + } catch (TemplateException e) { + throw new AccessException("Can't get environment variable by name, '" + name + "'.", e); + } } @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 + throw new UnsupportedOperationException(); } } } From 8bdc5fdcf6507a00113e8f396cdf9a90c8c3857c Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Mon, 11 Sep 2017 15:45:46 -0400 Subject: [PATCH 15/32] FREEMARKER-55: Adding url function --- .../freemarker/spring/model/UrlFunction.java | 189 +++++++++++++++++- 1 file changed, 179 insertions(+), 10 deletions(-) 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 index 402e74d03..fe1b1fd2c 100644 --- 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 @@ -19,6 +19,14 @@ package org.apache.freemarker.spring.model; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -27,9 +35,16 @@ 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.TemplateHashModelEx2; +import org.apache.freemarker.core.model.TemplateHashModelEx2.KeyValuePairIterator; import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateStringModel; +import org.apache.freemarker.core.util.CallableUtils; import org.apache.freemarker.core.util.StringToIndexMap; +import org.apache.freemarker.core.util._KeyValuePair; import org.springframework.web.servlet.support.RequestContext; +import org.springframework.web.servlet.support.RequestDataValueProcessor; +import org.springframework.web.util.UriUtils; /** * A TemplateFunctionModel providing functionality equivalent to the Spring Framework's @@ -51,20 +66,21 @@ public class UrlFunction extends AbstractSpringTemplateFunctionModel { private static final int VALUE_PARAM_IDX = 0; private static final int CONTEXT_PARAM_IDX = 1; + private static final int PARAMS_PARAM_IDX = 2; private static final String CONTEXT_PARAM_NAME = "context"; + /** + * Absolute URL pattern. e.g, http(s)://example.com, mailto:john@example.com, tel:123-456-7890. + */ + private static final Pattern ABS_URL_PATTERN = Pattern.compile("^((([A-Za-z]+?:)?\\/\\/)|[A-Za-z]+:)[\\w.-]+"); - // TODO: How to deal with parameters (spring:param tags)?? + private static final String URL_TEMPLATE_DELIMITER_PREFIX = "{"; + private static final String URL_TEMPLATE_DELIMITER_SUFFIX = "}"; - private static final ArgumentArrayLayout ARGS_LAYOUT = - ArgumentArrayLayout.create( - 1, - false, - StringToIndexMap.of(CONTEXT_PARAM_NAME, CONTEXT_PARAM_IDX), - false - ); + private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, false, + StringToIndexMap.of(CONTEXT_PARAM_NAME, CONTEXT_PARAM_IDX), true); public UrlFunction(HttpServletRequest request, HttpServletResponse response) { super(request, response); @@ -74,8 +90,75 @@ public UrlFunction(HttpServletRequest request, HttpServletResponse response) { public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext) throws TemplateException { - // TODO - return null; + final String value = CallableUtils.getStringArgument(args, VALUE_PARAM_IDX, this); + final String context = CallableUtils.getOptionalStringArgument(args, CONTEXT_PARAM_IDX, this); + + List<_KeyValuePair> params = null; + final TemplateHashModelEx2 paramsHashModel = (TemplateHashModelEx2) args[PARAMS_PARAM_IDX]; + + if (!paramsHashModel.isEmptyHash()) { + params = new ArrayList<>(); + + TemplateHashModelEx2.KeyValuePair pair; + TemplateModel paramNameModel; + TemplateModel paramValueModel; + String paramName; + String paramValue; + + for (KeyValuePairIterator pairIt = paramsHashModel.keyValuePairIterator(); pairIt.hasNext();) { + pair = pairIt.next(); + paramNameModel = pair.getKey(); + paramValueModel = pair.getValue(); + + if ((paramNameModel instanceof TemplateStringModel) + && (paramValueModel instanceof TemplateStringModel)) { + paramName = ((TemplateStringModel) paramNameModel).getAsString(); + paramValue = ((TemplateStringModel) paramValueModel).getAsString(); + + if (paramName.isEmpty()) { + CallableUtils.newArgumentValueException(PARAMS_PARAM_IDX, + "Parameter name must be a non-blank string.", this); + } + + params.add(new _KeyValuePair(paramName, paramValue)); + } else { + CallableUtils.newArgumentValueException(PARAMS_PARAM_IDX, + "Parameter name and value must be string.", this); + } + } + } + + final UrlType urlType = determineUrlType(value); + + StringBuilder urlBuilder = new StringBuilder(); + + if (urlType == UrlType.CONTEXT_RELATIVE) { + if (context == null) { + urlBuilder.append(getRequest().getContextPath()); + } else if (context.endsWith("/")) { + urlBuilder.append(context.substring(0, context.length() - 1)); + } else { + urlBuilder.append(context); + } + } + + Set templateParams = new HashSet<>(); + urlBuilder.append(replaceUriTemplateParams(value, params, templateParams)); + urlBuilder.append(createQueryString(params, templateParams, (urlBuilder.indexOf("?") == -1))); + + String urlString = urlBuilder.toString(); + + if (urlType != UrlType.ABSOLUTE) { + urlString = getResponse().encodeURL(urlString); + } + + RequestDataValueProcessor processor = requestContext.getRequestDataValueProcessor(); + + if ((processor != null) && (getRequest() instanceof HttpServletRequest)) { + urlString = processor.processUrl(getRequest(), urlString); + } + + return wrapObject(objectWrapperAndUnwrapper, urlString); } @Override @@ -83,4 +166,90 @@ public ArgumentArrayLayout getFunctionArgumentArrayLayout() { return ARGS_LAYOUT; } + private UrlType determineUrlType(final String value) { + Matcher m = ABS_URL_PATTERN.matcher(value); + + if (m.matches()) { + return UrlType.ABSOLUTE; + } else if (value.startsWith("/")) { + return UrlType.CONTEXT_RELATIVE; + } else { + return UrlType.RELATIVE; + } + } + + private String replaceUriTemplateParams(String uri, List<_KeyValuePair> params, Set usedParams) + throws TemplateException { + final String encoding = getResponse().getCharacterEncoding(); + + String paramName; + String paramValue; + + for (_KeyValuePair pair : params) { + paramName = pair.getKey(); + paramValue = pair.getValue(); + + String template = URL_TEMPLATE_DELIMITER_PREFIX + paramName + URL_TEMPLATE_DELIMITER_SUFFIX; + + if (uri.contains(template)) { + usedParams.add(paramName); + + try { + uri = uri.replace(template, UriUtils.encodePath(paramValue, encoding)); + } catch (UnsupportedEncodingException e) { + CallableUtils.newGenericExecuteException("Cannot encode URI. " + e, this); + } + } else { + template = URL_TEMPLATE_DELIMITER_PREFIX + '/' + paramName + URL_TEMPLATE_DELIMITER_SUFFIX; + + if (uri.contains(template)) { + usedParams.add(paramName); + + try { + uri = uri.replace(template, UriUtils.encodePathSegment(paramValue, encoding)); + } catch (UnsupportedEncodingException e) { + CallableUtils.newGenericExecuteException("Cannot encode URI. " + e, this); + } + } + } + } + + return uri; + } + + private String createQueryString(List<_KeyValuePair> params, Set usedParams, boolean includeQueryStringDelimiter) + throws TemplateException { + final String encoding = getResponse().getCharacterEncoding(); + final StringBuilder queryStringBuilder = new StringBuilder(); + + String paramName; + String paramValue; + + for (_KeyValuePair pair : params) { + paramName = pair.getKey(); + paramValue = pair.getValue(); + + if (!usedParams.contains(paramName)) { + queryStringBuilder + .append((includeQueryStringDelimiter && queryStringBuilder.length() == 0) ? "?" : "&"); + + try { + queryStringBuilder.append(UriUtils.encodeQueryParam(paramName, encoding)); + + if (paramValue != null) { + queryStringBuilder.append('='); + queryStringBuilder.append(UriUtils.encodeQueryParam(paramValue, encoding)); + } + } catch (UnsupportedEncodingException e) { + CallableUtils.newGenericExecuteException("Cannot encode query parameter. " + e, this); + } + } + } + + return queryStringBuilder.toString(); + } + + private enum UrlType { + CONTEXT_RELATIVE, RELATIVE, ABSOLUTE + } } From 075bd88777888a12a194844f8b961c8bd7333a49 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Mon, 11 Sep 2017 15:51:01 -0400 Subject: [PATCH 16/32] FREEMARKER-55: fixing NPE when there's no params --- .../org/apache/freemarker/spring/model/UrlFunction.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 index fe1b1fd2c..1e966a125 100644 --- 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 @@ -21,6 +21,7 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -53,6 +54,9 @@ * Some valid example(s): *

*
+ * ${spring.url('/usereditaction.do')}
+ * ${spring.url('/usereditaction.do', param1='value1', param2='value2')}
+ * ${spring.url('/usereditaction.do', context='/othercontext', param1='value1', param2='value2')}
  * 
*

* 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 + + + +

Editing User: ${spring.eval("user.firstName + ' ' + user.lastName")}

+ +

${spring.message("user.form.message", user.firstName, user.lastName, user.email)}

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
${spring.message("user.id")!}${user.id}
${spring.message("user.password")!} + <@spring.bind "user.password"; status> + + +
${spring.message("user.email")!} + <@spring.bind "user.email"; status> + + +
${spring.message("user.firstName")!} + <@spring.bind "user.firstName"; status> + + +
${spring.message("user.lastName")!} + <@spring.bind "user.lastName"; status> + + +
${spring.message("user.birthDate")!} + <@spring.bind "user.birthDate"; status> + ${spring.transform(status.editor, status.actualValue)!} + +
+ + +
+
+ + + 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 + + +

Users

+ + + + + + + + + + <#list users as user> + + + + + + +
NameE-Mail
${user.firstName} ${user.lastName}${user.email}
+ + diff --git a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml new file mode 100644 index 000000000..f68f584e9 --- /dev/null +++ b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/UsersMessages.properties b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/UsersMessages.properties new file mode 100644 index 000000000..ade76f40a --- /dev/null +++ b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/UsersMessages.properties @@ -0,0 +1,7 @@ +user.form.message=Edit info for {0} {1} <{2}> +user.id=ID +user.password=Password +user.email=E-Mail +user.firstName=First name +user.lastName=Last name +user.birthDate=Birth Date From d5c31a1b4e5753e7dbbeb1b1971c3b9bcf5643e4 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Mon, 11 Sep 2017 23:24:37 -0400 Subject: [PATCH 20/32] FREEMARKER-55: unit test template code --- .../{model => example/mvc/users}/UsersMessages.properties | 0 .../freemarker/spring/model/MessageFunctionTest-context.xml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename freemarker-spring/src/test/resources/org/apache/freemarker/spring/{model => example/mvc/users}/UsersMessages.properties (100%) diff --git a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/UsersMessages.properties b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersMessages.properties similarity index 100% rename from freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/UsersMessages.properties rename to freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersMessages.properties diff --git a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml index f68f584e9..862de4466 100644 --- a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml +++ b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml @@ -26,7 +26,7 @@ - + From 77b7f30c5f61158c138f42e0f61c3b47b8df6d2d Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 10:58:28 -0400 Subject: [PATCH 21/32] FREEMARKER-55: Adding unit test for message function. --- .../spring/model/MessageFunction.java | 2 +- .../spring/example/mvc/users/User.java | 6 ++-- .../example/mvc/users/UserController.java | 31 ++++++++++++----- .../example/mvc/users/UserRepository.java | 14 ++++---- .../spring/model/MessageFunctionTest.java | 34 +++++++++++++++---- .../views/example/users/userlist.ftl | 2 ++ .../model/message-function-basic-usages.ftl | 32 +++++++++++++++++ .../mvc/users/UsersMessages.properties | 1 + .../mvc/users/users-mvc-context.xml} | 2 +- 9 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/message-function-basic-usages.ftl rename freemarker-spring/src/test/resources/org/apache/freemarker/spring/{model/MessageFunctionTest-context.xml => example/mvc/users/users-mvc-context.xml} (95%) 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 94a1e0f3b..e64aba59c 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 @@ -65,7 +65,7 @@ * <#-- 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) --> + * <#-- With 'message' named parameter (assuming a MessageResolvable object is set to a model attribute) --> * ${spring.message(message=myMessageResolvable)} * *

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 index a1550b27e..1aa051574 100644 --- 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 @@ -23,18 +23,18 @@ public class User { - private final String id; + private final Integer id; private String password; private String email; private String firstName; private String lastName; private Date birthDate; - public User(final String id) { + public User(final Integer id) { this.id = id; } - public String getId() { + public Integer getId() { return id; } 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 index e8c0d866d..80a158d82 100644 --- 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 @@ -23,36 +23,51 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; @Controller public class UserController { + private static final String DEFAULT_USER_LIST_VIEW_NAME = "example/users/userlist"; + + private static final String DEFAULT_USER_EDIT_VIEW_NAME = "example/users/useredit"; + @Autowired private UserRepository userRepository; - @RequestMapping(value = "/users", method = RequestMethod.GET) - public String listUsers(Model model) { + @RequestMapping(value = "/users/", method = RequestMethod.GET) + public String listUsers(@RequestParam(value = "viewName", required = false) String viewName, Model model) { List users = new LinkedList<>(); - for (String id : userRepository.getUserIds()) { + for (Integer id : userRepository.getUserIds()) { users.add(userRepository.getUser(id)); } model.addAttribute("users", users); - return "example/users/userlist"; + return (StringUtils.hasText(viewName)) ? viewName : DEFAULT_USER_LIST_VIEW_NAME; } - @RequestMapping(value = "/users/{id}", method = RequestMethod.GET) - public String editUser(@PathVariable("id") String id, Model model) { + @RequestMapping(value = "/users/{id:\\d+}", method = RequestMethod.GET) + public String getUser(@PathVariable("id") Integer id, + @RequestParam(value = "viewName", required = false) String viewName, Model model) { User user = userRepository.getUser(id); - model.addAttribute("user", user); - return "example/users/useredit"; + + if (user != null) { + model.addAttribute("user", user); + } else { + model.addAttribute("errorMessage", + new DefaultMessageSourceResolvable(new String[] { "user.error.notfound" }, new Object[] { id })); + } + + return (StringUtils.hasText(viewName)) ? viewName : DEFAULT_USER_EDIT_VIEW_NAME; } public UserRepository getUserRepository() { 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 index 3e53d036c..5049521a2 100644 --- 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 @@ -30,9 +30,9 @@ @Repository public class UserRepository { - private Map usersMap = new ConcurrentHashMap<>(); + private Map usersMap = new ConcurrentHashMap<>(); { - String id = "13c2ec8c-102c-4883-a282-3fe983e61515"; + Integer id = 101; User user = new User(id); user.setEmail("john@example.com"); user.setFirstName("John"); @@ -44,7 +44,7 @@ public class UserRepository { user.setBirthDate(birthDate.getTime()); usersMap.put(id, user); - id = "04d6080b-2098-4eaf-90ee-7331caab5e91"; + id = 102; user = new User(id); user.setEmail("jane@example.com"); user.setFirstName("Jane"); @@ -57,11 +57,11 @@ public class UserRepository { usersMap.put(id, user); } - public synchronized Set getUserIds() { + public synchronized Set getUserIds() { return new TreeSet<>(usersMap.keySet()); } - public synchronized User getUser(final String id) { + public synchronized User getUser(final Integer id) { if (id == null) { throw new IllegalArgumentException("ID must be non-null."); } @@ -76,7 +76,7 @@ public synchronized User getUser(final String id) { } public synchronized User addOrUpdateUser(final User user) { - final String id = user.getId(); + final Integer id = user.getId(); User newUser = cloneUser(user, id); usersMap.put(id, newUser); return cloneUser(newUser, id); @@ -91,7 +91,7 @@ public synchronized boolean deleteUser(final String username) { return user != null; } - private User cloneUser(final User source, final String id) { + private User cloneUser(final User source, final Integer id) { User clone = new User(id); clone.setPassword(source.getPassword()); clone.setEmail(source.getEmail()); 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 index 03c32f458..803e3dec9 100644 --- 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 @@ -20,10 +20,13 @@ package org.apache.freemarker.spring.model; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 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.apache.freemarker.spring.example.mvc.users.User; +import org.apache.freemarker.spring.example.mvc.users.UserRepository; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,12 +41,15 @@ @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration("classpath:META-INF/web-resources") -@ContextConfiguration("MessageFunctionTest-context.xml") +@ContextConfiguration(locations = { "classpath:org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml" }) public class MessageFunctionTest { @Autowired private WebApplicationContext wac; + @Autowired + private UserRepository userRepository; + private MockMvc mockMvc; @Before @@ -52,10 +58,26 @@ public void setup() { } @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")); + public void getMessageFunctionBasicUsages() throws Exception { + final Integer userId = userRepository.getUserIds().iterator().next(); + final User user = userRepository.getUser(userId); + mockMvc.perform(get("/users/{userId}/", userId).param("viewName", "test/model/message-function-basic-usages") + .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) + .andExpect(xpath("//div[@id='userId']/text()").string(wac.getMessage("user.id", null, null))) + .andExpect(xpath("//div[@id='userEmail']/text()").string(wac.getMessage("user.email", null, null))) + .andExpect(xpath("//div[@id='userInfoWithArgs']/text()").string(wac.getMessage("user.form.message", + new Object[] { user.getFirstName(), user.getLastName(), user.getEmail() }, null))); + } + + @Test + public void getMessageFunctionWithMessageSourceResolvable() throws Exception { + final Integer nonExistingUserId = 0; + mockMvc.perform( + get("/users/{userId}/", nonExistingUserId).param("viewName", "test/model/message-function-basic-usages") + .accept(MediaType.parseMediaType("text/html"))) + .andExpect(status().isOk()).andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) + .andExpect(xpath("//div[@id='errorMessage']/text()") + .string(wac.getMessage("user.error.notfound", new Object[] { nonExistingUserId }, null))); } } 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 index 25c757f78..aef5c9162 100644 --- 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 @@ -21,6 +21,7 @@ Spring MVC Form Example - Users +

Users

@@ -41,4 +42,5 @@ + diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/message-function-basic-usages.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/message-function-basic-usages.ftl new file mode 100644 index 000000000..fe1e65ef2 --- /dev/null +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/message-function-basic-usages.ftl @@ -0,0 +1,32 @@ +<#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. +--> + + + +<#if user??> +
${spring.message("user.id")!}
+
${spring.message("user.email")!}
+
${spring.message("user.form.message", user.firstName, user.lastName, user.email)!}
+<#else> +
${spring.message(message=errorMessage)!}
+ + + + diff --git a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersMessages.properties b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersMessages.properties index ade76f40a..497607c9f 100644 --- a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersMessages.properties +++ b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersMessages.properties @@ -5,3 +5,4 @@ user.email=E-Mail user.firstName=First name user.lastName=Last name user.birthDate=Birth Date +user.error.notfound=User not found by ID: {0} diff --git a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml similarity index 95% rename from freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml rename to freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml index 862de4466..0dbc95089 100644 --- a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/model/MessageFunctionTest-context.xml +++ b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml @@ -26,7 +26,7 @@ - + From a274d2a6149b432656c5aa61eedd5b6213168378 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 11:22:15 -0400 Subject: [PATCH 22/32] FREEMARKER-55: Adding unit test for theme function. --- .../spring/model/ThemeFunction.java | 8 +-- .../spring/model/MessageFunctionTest.java | 4 +- .../spring/model/ThemeFunctionTest.java | 72 +++++++++++++++++++ .../model/theme-function-basic-usages.ftl | 26 +++++++ .../mvc/users/UsersTheme-default.properties | 1 + .../example/mvc/users/users-mvc-context.xml | 8 +++ 6 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java create mode 100644 freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/theme-function-basic-usages.ftl create mode 100644 freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersTheme-default.properties 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 15a410375..b03055e98 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 @@ -45,13 +45,9 @@ *

*
  * <#-- With 'code' positional parameter only -->
- * ${spring.theme("label.user.firstName")!}
+ * ${spring.theme("styleSheet")!}
  *
- * <#-- 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)}
+ * <link rel="stylesheet" href="${spring.theme('styleSheet')}" type="text/css" />
  * 
*

* Note: Unlike Spring Framework's <spring:theme /> JSP Tag Library, this function 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 index 803e3dec9..5be6dafb2 100644 --- 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 @@ -58,7 +58,7 @@ public void setup() { } @Test - public void getMessageFunctionBasicUsages() throws Exception { + public void testMessageFunctionBasicUsages() throws Exception { final Integer userId = userRepository.getUserIds().iterator().next(); final User user = userRepository.getUser(userId); mockMvc.perform(get("/users/{userId}/", userId).param("viewName", "test/model/message-function-basic-usages") @@ -71,7 +71,7 @@ public void getMessageFunctionBasicUsages() throws Exception { } @Test - public void getMessageFunctionWithMessageSourceResolvable() throws Exception { + public void testMessageFunctionWithMessageSourceResolvable() throws Exception { final Integer nonExistingUserId = 0; mockMvc.perform( get("/users/{userId}/", nonExistingUserId).param("viewName", "test/model/message-function-basic-usages") diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java new file mode 100644 index 000000000..637ca5f16 --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java @@ -0,0 +1,72 @@ +/* + * 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.MockMvcResultHandlers.print; +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.apache.freemarker.spring.example.mvc.users.UserRepository; +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.ui.context.ThemeSource; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration("classpath:META-INF/web-resources") +@ContextConfiguration(locations = { "classpath:org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml" }) +public class ThemeFunctionTest { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ThemeSource themeSource; + + private MockMvc mockMvc; + + @Before + public void setup() { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + public void testThemeFunctionBasicUsages() throws Exception { + final Integer userId = userRepository.getUserIds().iterator().next(); + mockMvc.perform(get("/users/{userId}/", userId).param("viewName", "test/model/theme-function-basic-usages") + .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) + .andExpect(xpath("//link[@rel='stylesheet']/@href").string( + themeSource.getTheme("default").getMessageSource().getMessage("styleSheet", null, null))); + } +} diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/theme-function-basic-usages.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/theme-function-basic-usages.ftl new file mode 100644 index 000000000..bc50875b3 --- /dev/null +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/theme-function-basic-usages.ftl @@ -0,0 +1,26 @@ +<#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. +--> + + + + + + + diff --git a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersTheme-default.properties b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersTheme-default.properties new file mode 100644 index 000000000..1d7bcc2d1 --- /dev/null +++ b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersTheme-default.properties @@ -0,0 +1 @@ +styleSheet=/themes/style/default.css diff --git a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml index 0dbc95089..f5fcf24d3 100644 --- a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml +++ b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml @@ -43,4 +43,12 @@ + + + + + + + + From 9987acfe0f399ad81d0373a8e7ee81e47674f614 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 11:33:39 -0400 Subject: [PATCH 23/32] FREEMARKER-55: adding unit test for theme function. --- .../apache/freemarker/spring/model/ThemeFunction.java | 2 ++ .../freemarker/spring/model/ThemeFunctionTest.java | 9 +++++++-- .../views/test/model/theme-function-basic-usages.ftl | 5 +++++ .../example/mvc/users/UsersTheme-default.properties | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) 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 b03055e98..4f201ad42 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 @@ -48,6 +48,8 @@ * ${spring.theme("styleSheet")!} * * <link rel="stylesheet" href="${spring.theme('styleSheet')}" type="text/css" /> + * + * <div id="user" class="${spring.theme('userClass', 'selected')}">...</div> * *

* Note: Unlike Spring Framework's <spring:theme /> JSP Tag Library, this function diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java index 637ca5f16..ed00f5edd 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java @@ -30,6 +30,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -62,11 +63,15 @@ public void setup() { @Test public void testThemeFunctionBasicUsages() throws Exception { + final MessageSource defaultThemeMessageSource = themeSource.getTheme("default").getMessageSource(); + final Integer userId = userRepository.getUserIds().iterator().next(); mockMvc.perform(get("/users/{userId}/", userId).param("viewName", "test/model/theme-function-basic-usages") .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) - .andExpect(xpath("//link[@rel='stylesheet']/@href").string( - themeSource.getTheme("default").getMessageSource().getMessage("styleSheet", null, null))); + .andExpect(xpath("//link[@rel='stylesheet']/@href") + .string(defaultThemeMessageSource.getMessage("styleSheet", null, null))) + .andExpect(xpath("//div[@id='user']/@class") + .string(defaultThemeMessageSource.getMessage("userClass", new Object[] { "selected" }, null))); } } diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/theme-function-basic-usages.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/theme-function-basic-usages.ftl index bc50875b3..efb503a94 100644 --- a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/theme-function-basic-usages.ftl +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/theme-function-basic-usages.ftl @@ -22,5 +22,10 @@ + +

+ ${user.firstName!} ${user.lastName!} +
+ diff --git a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersTheme-default.properties b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersTheme-default.properties index 1d7bcc2d1..b89d3a0ea 100644 --- a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersTheme-default.properties +++ b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersTheme-default.properties @@ -1 +1,2 @@ styleSheet=/themes/style/default.css +userClass=user-{0} From d7916709f9897c4ff8c6ae146ee191df77a43427 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 13:41:02 -0400 Subject: [PATCH 24/32] FREEMARKER-55: Adding unit test for eval function. --- .../freemarker/spring/model/EvalFunction.java | 19 ++++- .../spring/model/EvalFunctionTest.java | 80 +++++++++++++++++++ .../spring/model/MessageFunctionTest.java | 2 +- .../spring/model/ThemeFunctionTest.java | 2 +- .../test/model/eval-function-basic-usages.ftl | 46 +++++++++++ 5 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java create mode 100644 freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/eval-function-basic-usages.ftl 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 6ae0bb5f6..f155df171 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 @@ -51,9 +51,26 @@ * Some valid example(s): *

*
+ * <#assign expression="T(java.lang.Math).max(12.34, 56.78)" />
+ * Max number: ${spring.eval(expression)}
+ * 
+ * <#assign expression="user.id" />
+ * User ID: ${spring.eval(expression)!}
+ * 
+ * User ID: ${spring.eval('user.id')!}
+ * 
+ * <#assign expression="user.firstName + ' ' + user.lastName" />
+ * User Name: ${spring.eval(expression)!}
+ * 
+ * <#assign expression="users[0].id" />
+ * First User's ID: ${spring.eval(expression)!}
+ *
+ * <#assign expression="{0,1,1,2,3,5,8,13}" />
+ * <#assign numbers=spring.eval(expression) />
+ * Numbers: <#list numbers as number>${number}<#sep>, </#list>
  * 
*

- * Note: Unlike Spring Framework's <spring:message /> JSP Tag Library, this function + * Note: Unlike Spring Framework's <spring:eval /> 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. *

diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java new file mode 100644 index 000000000..953b57199 --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.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.model; + +import static org.hamcrest.Matchers.equalToIgnoringWhiteSpace; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +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.apache.freemarker.spring.example.mvc.users.User; +import org.apache.freemarker.spring.example.mvc.users.UserRepository; +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(locations = { "classpath:org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml" }) +public class EvalFunctionTest { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private UserRepository userRepository; + + private MockMvc mockMvc; + + private long startTimeMillis; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + + startTimeMillis = System.currentTimeMillis(); + System.setProperty("EvalFunctionTest.startTimeMillis", Long.toString(startTimeMillis)); + } + + @Test + public void testMessageFunctionBasicUsages() throws Exception { + final Integer userId = userRepository.getUserIds().iterator().next(); + final User user = userRepository.getUser(userId); + mockMvc.perform(get("/users/").param("viewName", "test/model/eval-function-basic-usages") + .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) + .andExpect(xpath("//div[@id='maxNumber']/text()").number(56.78)) + .andExpect(xpath("//div[@id='user-%s']/text()", userId) + .string(equalToIgnoringWhiteSpace(user.getFirstName() + " " + user.getLastName()))) + .andExpect(xpath("//div[@id='firstUserId']/text()").string(userId.toString())) + .andExpect(xpath("//div[@id='fibonacci']/text()").string("0, 1, 1, 2, 3, 5, 8, 13")); + } + +} 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 index 5be6dafb2..0ddbe27aa 100644 --- 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 @@ -53,7 +53,7 @@ public class MessageFunctionTest { private MockMvc mockMvc; @Before - public void setup() { + public void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java index ed00f5edd..81b186b54 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java @@ -57,7 +57,7 @@ public class ThemeFunctionTest { private MockMvc mockMvc; @Before - public void setup() { + public void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/eval-function-basic-usages.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/eval-function-basic-usages.ftl new file mode 100644 index 000000000..b5b1dd63d --- /dev/null +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/eval-function-basic-usages.ftl @@ -0,0 +1,46 @@ +<#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. +--> + + + +<#assign expression="T(java.lang.Math).max(12.34, 56.78)" /> +
${spring.eval(expression)}
+ +
    + <#list users as user> +
  • + <#assign expression="user.id" /> +
    + <#assign expression="user.firstName + ' ' + user.lastName" /> + ${spring.eval(expression)!} +
    +
  • + +
+ +<#assign expression="users[0].id" /> +
${spring.eval(expression)!}
+ +<#assign expression="{0,1,1,2,3,5,8,13}" /> +<#assign numbers=spring.eval(expression) /> +
<#list numbers as number>${number}<#sep>,
+ + + From e4598db9cb9c0513a36033ce4fcd39a6fa901231 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 14:25:21 -0400 Subject: [PATCH 25/32] FREEMARKER-55: Adding unit test for url function. --- .../java/org/apache/freemarker/spring/model/UrlFunction.java | 4 ++++ 1 file changed, 4 insertions(+) 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 index 1e966a125..2220800b6 100644 --- 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 @@ -55,7 +55,11 @@ *

*
  * ${spring.url('/usereditaction.do')}
+ * 
  * ${spring.url('/usereditaction.do', param1='value1', param2='value2')}
+ * 
+ * ${spring.url('/users/{userId}/edit.do', userId='123')}
+ * 
  * ${spring.url('/usereditaction.do', context='/othercontext', param1='value1', param2='value2')}
  * 
*

From 4672252edb58455d4727ad8e233c892ddf0fe5fb Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 14:25:49 -0400 Subject: [PATCH 26/32] FREEMARKER-55: Adding unit test for url function. --- .../spring/model/UrlFunctionTest.java | 76 +++++++++++++++++++ .../test/model/url-function-basic-usages.ftl | 62 +++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/model/UrlFunctionTest.java create mode 100644 freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/url-function-basic-usages.ftl diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/UrlFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/UrlFunctionTest.java new file mode 100644 index 000000000..c2e1ce3cc --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/UrlFunctionTest.java @@ -0,0 +1,76 @@ +/* + * 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.MockMvcResultHandlers.print; +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.apache.freemarker.spring.example.mvc.users.UserRepository; +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(locations = { "classpath:org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml" }) +public class UrlFunctionTest { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private UserRepository userRepository; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + public void testThemeFunctionBasicUsages() throws Exception { + final Integer userId = userRepository.getUserIds().iterator().next(); + mockMvc.perform(get("/users/").param("viewName", "test/model/url-function-basic-usages") + .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) + .andExpect(xpath("//h2[@id='usersListHeader']//a/@href", userId).string("/users/")) + .andExpect(xpath("//h3[@id='usersListHeaderWithSortParams']//a/@href", userId) + .string("/users/?sortField=birthDate&sortDirection=descending")) + .andExpect(xpath("//h2[@id='otherAppsUsersListHeader']//a/@href", userId).string("/otherapp/users/")) + .andExpect(xpath("//h3[@id='otherAppsUsersListHeaderWithSortParams']//a/@href", userId) + .string("/otherapp/users/?sortField=birthDate&sortDirection=descending")) + .andExpect(xpath("//div[@id='user-%s']//a[@class='userIdLink']/@href", userId).string("/users/" + userId + "/")) + .andExpect(xpath("//div[@id='user-%s']//a[@class='userNameLink']/@href", userId).string("/users/" + userId + "/")) + .andExpect(xpath("//div[@id='freeMarkerManualUrl']//a/@href", userId) + .string("http://freemarker.org/docs/index.html")); + } +} diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/url-function-basic-usages.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/url-function-basic-usages.ftl new file mode 100644 index 000000000..870ba3c12 --- /dev/null +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/url-function-basic-usages.ftl @@ -0,0 +1,62 @@ +<#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. +--> + + + +

+ <#assign pathInfo="/users/" /> + Users List +

+ +

+ <#assign pathInfo="/users/" /> + Users List +

+ +

+ <#assign pathInfo="/users/" /> + Users List +

+ +

+ <#assign pathInfo="/users/" /> + Users List +

+ + + +
+ <#assign pathInfo="http://freemarker.org/docs/index.html" /> + Apache FreeMarker Manual +
+ + + From 7a93fa0ac9d5594080b3b8749976f80030de6e4f Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 15:02:44 -0400 Subject: [PATCH 27/32] FREEMARKER-55: adding unit test for bind directive. --- .../spring/model/BindDirectiveTest.java | 73 +++++++++++++++++++ .../spring/model/EvalFunctionTest.java | 5 -- .../model/bind-directive-basic-usages.ftl | 55 ++++++++++++++ 3 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindDirectiveTest.java create mode 100644 freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/bind-directive-basic-usages.ftl diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindDirectiveTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindDirectiveTest.java new file mode 100644 index 000000000..cd1af42ea --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindDirectiveTest.java @@ -0,0 +1,73 @@ +/* + * 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.MockMvcResultHandlers.print; +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.apache.freemarker.spring.example.mvc.users.User; +import org.apache.freemarker.spring.example.mvc.users.UserRepository; +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(locations = { "classpath:org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml" }) +public class BindDirectiveTest { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private UserRepository userRepository; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + public void testMessageFunctionBasicUsages() throws Exception { + final Integer userId = userRepository.getUserIds().iterator().next(); + final User user = userRepository.getUser(userId); + mockMvc.perform(get("/users/{userId}.", userId).param("viewName", "test/model/bind-directive-basic-usages") + .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) + .andExpect(xpath("//input[@name='email']/@value").string(user.getEmail())) + .andExpect(xpath("//input[@name='firstName']/@value").string(user.getFirstName())) + .andExpect(xpath("//input[@name='lastName']/@value").string(user.getLastName())) + .andExpect(xpath("//div[@id='statusValueNotReachable']/text()").string("")); + } + +} diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java index 953b57199..be6f6dd15 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java @@ -53,14 +53,9 @@ public class EvalFunctionTest { private MockMvc mockMvc; - private long startTimeMillis; - @Before public void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); - - startTimeMillis = System.currentTimeMillis(); - System.setProperty("EvalFunctionTest.startTimeMillis", Long.toString(startTimeMillis)); } @Test diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/bind-directive-basic-usages.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/bind-directive-basic-usages.ftl new file mode 100644 index 000000000..2f0e535ee --- /dev/null +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/bind-directive-basic-usages.ftl @@ -0,0 +1,55 @@ +<#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. +--> + + + + + + + + + + + + + + + + + + +
E-Mail + <@spring.bind "user.email"; status> + + +
First Name + <@spring.bind "user.firstName"; status> + + +
Last Name + <@spring.bind "user.lastName"; status> + + +
+ +
<#if status??>${status.value!}
+ + + From 86a8a5f6933ccf128cae0ca46ef116717aa949be Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 15:06:04 -0400 Subject: [PATCH 28/32] FREEMARKER-55: Adding unit test for nestedPath directive. --- .../spring/model/NestedPathDirectiveTest.java | 72 +++++++++++++++++++ .../nestedpath-directive-basic-usages.ftl | 55 ++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/model/NestedPathDirectiveTest.java create mode 100644 freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/nestedpath-directive-basic-usages.ftl diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/NestedPathDirectiveTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/NestedPathDirectiveTest.java new file mode 100644 index 000000000..d02478fdf --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/NestedPathDirectiveTest.java @@ -0,0 +1,72 @@ +/* + * 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.MockMvcResultHandlers.print; +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.apache.freemarker.spring.example.mvc.users.User; +import org.apache.freemarker.spring.example.mvc.users.UserRepository; +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(locations = { "classpath:org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml" }) +public class NestedPathDirectiveTest { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private UserRepository userRepository; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + public void testMessageFunctionBasicUsages() throws Exception { + final Integer userId = userRepository.getUserIds().iterator().next(); + final User user = userRepository.getUser(userId); + mockMvc.perform(get("/users/{userId}.", userId).param("viewName", "test/model/nestedpath-directive-basic-usages") + .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) + .andExpect(xpath("//input[@name='email']/@value").string(user.getEmail())) + .andExpect(xpath("//input[@name='firstName']/@value").string(user.getFirstName())) + .andExpect(xpath("//input[@name='lastName']/@value").string(user.getLastName())); + } + +} diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/nestedpath-directive-basic-usages.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/nestedpath-directive-basic-usages.ftl new file mode 100644 index 000000000..13849bf37 --- /dev/null +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/nestedpath-directive-basic-usages.ftl @@ -0,0 +1,55 @@ +<#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.nestedPath "user"> + + + + + + + + + + + + + + + +
E-Mail + <@spring.bind "email"; status> + + +
First Name + <@spring.bind "firstName"; status> + + +
Last Name + <@spring.bind "lastName"; status> + + +
+ + + + From 74a300f144ce99d03e021416f4f6ce7d191024af Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 17:30:02 -0400 Subject: [PATCH 29/32] FREEMARKER-55: Adding unit test for BindErrorsDirective --- .../spring/model/BindErrorsDirective.java | 10 ++- .../spring/example/mvc/users/User.java | 5 +- .../example/mvc/users/UserController.java | 15 ++++ .../spring/model/BindDirectiveTest.java | 2 +- .../spring/model/BindErrorsDirectiveTest.java | 83 +++++++++++++++++++ .../spring/model/EvalFunctionTest.java | 2 +- .../spring/model/MessageFunctionTest.java | 4 +- .../spring/model/NestedPathDirectiveTest.java | 2 +- .../spring/model/ThemeFunctionTest.java | 2 +- .../spring/model/UrlFunctionTest.java | 2 +- .../binderrors-directive-basic-usages.ftl | 70 ++++++++++++++++ .../mvc/users/UsersMessages.properties | 1 + 12 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindErrorsDirectiveTest.java create mode 100644 freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/binderrors-directive-basic-usages.ftl 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 e35b6ee4d..6d0c95455 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 @@ -49,8 +49,14 @@ * Some valid example(s): *

*
- * <@spring.hasBindErrors "email"; errors>
- *   <#-- nested content with using errors -->
+ * <@spring.hasBindErrors "user"; errors>
+ *   <div class="errors">
+ *     <#list errors.allErrors as error>
+ *       <div class="error">
+ *         ${spring.message(message=error)!}
+ *       </div>
+ *     </#list>
+ *   </div>
  * </@spring.hasBindErrors>
  * 
*

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 index 1aa051574..e349a4861 100644 --- 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 @@ -23,13 +23,16 @@ public class User { - private final Integer id; + private Integer id; private String password; private String email; private String firstName; private String lastName; private Date birthDate; + public User() { + } + public User(final Integer id) { this.id = id; } 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 index 80a158d82..43b06a447 100644 --- 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 @@ -27,6 +27,8 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.StringUtils; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -70,6 +72,19 @@ public String getUser(@PathVariable("id") Integer id, return (StringUtils.hasText(viewName)) ? viewName : DEFAULT_USER_EDIT_VIEW_NAME; } + @RequestMapping(value = "/users/", method = RequestMethod.POST) + public String createUser(@RequestParam(value = "viewName", required = false) String viewName, User user, + BindingResult bindingResult, Model model) { + model.addAttribute("user", user); + + if (!StringUtils.hasText(user.getEmail())) { + bindingResult.addError(new FieldError("user", "email", user.getEmail(), true, + new String[] { "user.error.invalid.email" }, new Object[] { user.getEmail() }, "E-Mail is blank.")); + } + + return (StringUtils.hasText(viewName)) ? viewName : DEFAULT_USER_EDIT_VIEW_NAME; + } + public UserRepository getUserRepository() { return userRepository; } diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindDirectiveTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindDirectiveTest.java index cd1af42ea..b2a6cd592 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindDirectiveTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindDirectiveTest.java @@ -58,7 +58,7 @@ public void setUp() { } @Test - public void testMessageFunctionBasicUsages() throws Exception { + public void testBasicUsages() throws Exception { final Integer userId = userRepository.getUserIds().iterator().next(); final User user = userRepository.getUser(userId); mockMvc.perform(get("/users/{userId}.", userId).param("viewName", "test/model/bind-directive-basic-usages") diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindErrorsDirectiveTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindErrorsDirectiveTest.java new file mode 100644 index 000000000..15520cb80 --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/BindErrorsDirectiveTest.java @@ -0,0 +1,83 @@ +/* + * 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.hamcrest.Matchers.equalToIgnoringWhiteSpace; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +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.apache.freemarker.spring.example.mvc.users.User; +import org.apache.freemarker.spring.example.mvc.users.UserRepository; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +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(locations = { "classpath:org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml" }) +public class BindErrorsDirectiveTest { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private UserRepository userRepository; + + @Autowired + private MessageSource messageSource; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + public void testBasicUsages() throws Exception { + final User user = new User(); + user.setFirstName("Paul"); + user.setLastName("Temple"); + // set invalid email intentionally to test BindErrorsDirective... + user.setEmail(""); + + mockMvc.perform(post("/users/").param("viewName", "test/model/binderrors-directive-basic-usages") + .param("firstName", user.getFirstName()).param("lastName", user.getLastName()) + .param("email", user.getEmail()).accept(MediaType.parseMediaType("text/html"))) + .andExpect(status().isOk()).andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) + .andExpect(xpath("//div[@class='error']").string(equalToIgnoringWhiteSpace( + messageSource.getMessage("user.error.invalid.email", new Object[] { user.getEmail() }, null)))) + .andExpect(xpath("//input[@name='firstName']/@value").string(user.getFirstName())) + .andExpect(xpath("//input[@name='lastName']/@value").string(user.getLastName())); + } + +} diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java index be6f6dd15..45d98b248 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/EvalFunctionTest.java @@ -59,7 +59,7 @@ public void setUp() { } @Test - public void testMessageFunctionBasicUsages() throws Exception { + public void testBasicUsages() throws Exception { final Integer userId = userRepository.getUserIds().iterator().next(); final User user = userRepository.getUser(userId); mockMvc.perform(get("/users/").param("viewName", "test/model/eval-function-basic-usages") 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 index 0ddbe27aa..d043bfe8e 100644 --- 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 @@ -58,7 +58,7 @@ public void setUp() { } @Test - public void testMessageFunctionBasicUsages() throws Exception { + public void testBasicUsages() throws Exception { final Integer userId = userRepository.getUserIds().iterator().next(); final User user = userRepository.getUser(userId); mockMvc.perform(get("/users/{userId}/", userId).param("viewName", "test/model/message-function-basic-usages") @@ -71,7 +71,7 @@ public void testMessageFunctionBasicUsages() throws Exception { } @Test - public void testMessageFunctionWithMessageSourceResolvable() throws Exception { + public void testWithMessageSourceResolvable() throws Exception { final Integer nonExistingUserId = 0; mockMvc.perform( get("/users/{userId}/", nonExistingUserId).param("viewName", "test/model/message-function-basic-usages") diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/NestedPathDirectiveTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/NestedPathDirectiveTest.java index d02478fdf..a4999f5da 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/NestedPathDirectiveTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/NestedPathDirectiveTest.java @@ -58,7 +58,7 @@ public void setUp() { } @Test - public void testMessageFunctionBasicUsages() throws Exception { + public void testBasicUsages() throws Exception { final Integer userId = userRepository.getUserIds().iterator().next(); final User user = userRepository.getUser(userId); mockMvc.perform(get("/users/{userId}.", userId).param("viewName", "test/model/nestedpath-directive-basic-usages") diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java index 81b186b54..804b76d6e 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/ThemeFunctionTest.java @@ -62,7 +62,7 @@ public void setUp() { } @Test - public void testThemeFunctionBasicUsages() throws Exception { + public void testBasicUsages() throws Exception { final MessageSource defaultThemeMessageSource = themeSource.getTheme("default").getMessageSource(); final Integer userId = userRepository.getUserIds().iterator().next(); diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/UrlFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/UrlFunctionTest.java index c2e1ce3cc..8435bfcd5 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/UrlFunctionTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/UrlFunctionTest.java @@ -57,7 +57,7 @@ public void setUp() { } @Test - public void testThemeFunctionBasicUsages() throws Exception { + public void testBasicUsages() throws Exception { final Integer userId = userRepository.getUserIds().iterator().next(); mockMvc.perform(get("/users/").param("viewName", "test/model/url-function-basic-usages") .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/binderrors-directive-basic-usages.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/binderrors-directive-basic-usages.ftl new file mode 100644 index 000000000..d608615ee --- /dev/null +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/binderrors-directive-basic-usages.ftl @@ -0,0 +1,70 @@ +<#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.hasBindErrors "user"; errors> +

+ <#list errors.allErrors as error> +
+ ${spring.message(message=error)!} +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + +
E-Mail + <@spring.bind "user.email"; status> + + +
First Name + <@spring.bind "user.firstName"; status> + + +
Last Name + <@spring.bind "user.lastName"; status> + + +
+ +
+
+ + + diff --git a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersMessages.properties b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersMessages.properties index 497607c9f..aa12742bd 100644 --- a/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersMessages.properties +++ b/freemarker-spring/src/test/resources/org/apache/freemarker/spring/example/mvc/users/UsersMessages.properties @@ -6,3 +6,4 @@ user.firstName=First name user.lastName=Last name user.birthDate=Birth Date user.error.notfound=User not found by ID: {0} +user.error.invalid.email=Invalid E-Mail address: {0} From bd564327db67c19a6a39b092a1be806ed4b7af08 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 21:11:31 -0400 Subject: [PATCH 30/32] FREEMARKER-55: Adding unit test for transform function --- .../spring/model/TransformFunction.java | 2 +- .../spring/example/mvc/users/User.java | 6 ++ .../example/mvc/users/UserController.java | 18 ++++- .../spring/model/TransformFunctionTest.java | 76 +++++++++++++++++++ .../model/transform-function-basic-usages.ftl | 28 +++++++ 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 freemarker-spring/src/test/java/org/apache/freemarker/spring/model/TransformFunctionTest.java create mode 100644 freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/transform-function-basic-usages.ftl 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 index ee3cc1c84..d6c381e87 100644 --- 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 @@ -46,7 +46,7 @@ *

*
  *   <@spring.bind "user"; status>
- *     ${spring.transform(status, user.birthDate)}
+ *     ${spring.transform(status.editor, user.birthDate)}
  *   </@spring.bind>
  * 
*

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 index e349a4861..23d31dc8d 100644 --- 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 @@ -80,4 +80,10 @@ public Date getBirthDate() { public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } + + @Override + public String toString() { + return super.toString() + " {id=" + id + ", firstName='" + firstName + "', lastName='" + lastName + "', email='" + + email + "', 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 index 43b06a447..09a416d32 100644 --- 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 @@ -19,16 +19,22 @@ package org.apache.freemarker.spring.example.mvc.users; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.LinkedList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.propertyeditors.CustomDateEditor; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -44,6 +50,12 @@ public class UserController { @Autowired private UserRepository userRepository; + @InitBinder("user") + public void customizeBinding(WebDataBinder binder) { + binder.registerCustomEditor(Date.class, "birthDate", + new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true)); + } + @RequestMapping(value = "/users/", method = RequestMethod.GET) public String listUsers(@RequestParam(value = "viewName", required = false) String viewName, Model model) { List users = new LinkedList<>(); @@ -73,8 +85,8 @@ public String getUser(@PathVariable("id") Integer id, } @RequestMapping(value = "/users/", method = RequestMethod.POST) - public String createUser(@RequestParam(value = "viewName", required = false) String viewName, User user, - BindingResult bindingResult, Model model) { + public String createUser(@RequestParam(value = "viewName", required = false) String viewName, + @ModelAttribute("user") User user, BindingResult bindingResult, Model model) { model.addAttribute("user", user); if (!StringUtils.hasText(user.getEmail())) { @@ -82,6 +94,8 @@ public String createUser(@RequestParam(value = "viewName", required = false) Str new String[] { "user.error.invalid.email" }, new Object[] { user.getEmail() }, "E-Mail is blank.")); } + // No saving for now... + return (StringUtils.hasText(viewName)) ? viewName : DEFAULT_USER_EDIT_VIEW_NAME; } diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/TransformFunctionTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/TransformFunctionTest.java new file mode 100644 index 000000000..d3fd6807a --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/TransformFunctionTest.java @@ -0,0 +1,76 @@ +/* + * 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.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.xpath; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +import org.apache.freemarker.spring.example.mvc.users.User; +import org.apache.freemarker.spring.example.mvc.users.UserRepository; +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(locations = { "classpath:org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml" }) +public class TransformFunctionTest { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private UserRepository userRepository; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + public void testBasicUsages() throws Exception { + final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + final User user = new User(); + user.setFirstName("Paul"); + user.setLastName("Temple"); + user.setBirthDate(dateFormat.parse("1980-12-23")); + user.setEmail("paul.temple@example.com"); + + mockMvc.perform(post("/users/").param("viewName", "test/model/transform-function-basic-usages") + .param("firstName", user.getFirstName()).param("lastName", user.getLastName()) + .param("email", user.getEmail()).param("birthDate", dateFormat.format(user.getBirthDate())) + .accept(MediaType.parseMediaType("text/html"))).andDo(print()) + .andExpect(xpath("//div[@id='userBirthDate']/text()").string(dateFormat.format(user.getBirthDate()))); + } +} diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/transform-function-basic-usages.ftl b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/transform-function-basic-usages.ftl new file mode 100644 index 000000000..0fb9e2935 --- /dev/null +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/transform-function-basic-usages.ftl @@ -0,0 +1,28 @@ +<#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.bind "user.birthDate"; status> +

${spring.transform(status.editor, status.actualValue)}
+ + + + \ No newline at end of file From ef9ba120ebdc85d87567c1fb4200f3d752e89ab6 Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 21:53:45 -0400 Subject: [PATCH 31/32] FREEMARKER-55: javadocs and cleanups --- .../AbstractSpringTemplateCallableModel.java | 49 ++++++++-------- .../AbstractSpringTemplateDirectiveModel.java | 33 ++++++++--- .../AbstractSpringTemplateFunctionModel.java | 32 ++++++++--- .../spring/model/BindErrorsDirective.java | 24 +++----- .../freemarker/spring/model/EvalFunction.java | 16 ++++-- .../spring/model/MessageFunction.java | 23 +++----- .../spring/model/NestedPathDirective.java | 4 +- .../SpringFormTemplateCallableHashModel.java | 56 ------------------- .../SpringTemplateCallableHashModel.java | 13 ++++- .../spring/model/TransformFunction.java | 21 +++---- .../freemarker/spring/model/UrlFunction.java | 2 +- .../spring/web/view/FreeMarkerView.java | 3 - 12 files changed, 124 insertions(+), 152 deletions(-) delete mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringFormTemplateCallableHashModel.java diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/AbstractSpringTemplateCallableModel.java index 384a47be2..641fbdbfb 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 @@ -28,8 +28,6 @@ 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; import org.springframework.web.servlet.support.RequestContext; @@ -41,15 +39,28 @@ public abstract class AbstractSpringTemplateCallableModel implements TemplateCal private final HttpServletRequest request; private final HttpServletResponse response; + /** + * Constructs with servlet request and response. + * @param request servlet request + * @param response servlet response + */ public AbstractSpringTemplateCallableModel(HttpServletRequest request, HttpServletResponse response) { this.request = request; this.response = response; } + /** + * Return servlet request. + * @return servlet request + */ protected final HttpServletRequest getRequest() { return request; } + /** + * Return servlet response. + * @return servlet response + */ protected final HttpServletResponse getResponse() { return response; } @@ -68,34 +79,22 @@ protected final HttpServletResponse getResponse() { * @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 TemplateException + * @throws TemplateException if template exception occurs */ - protected final TemplateModel getBindStatusTemplateModel(Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, - RequestContext requestContext, String path, boolean ignoreNestedPath) throws TemplateException { + protected final TemplateModel getBindStatusTemplateModel(Environment env, + ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, 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; + return (status != null) ? objectWrapperAndUnwrapper.wrap(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(object); - } - - return null; - } - - protected abstract boolean isFunction(); - + /** + * Return the internal TemplateHashModel wrapper for templating in Spring Framework applications. + * @param env environment + * @return the internal TemplateHashModel wrapper for templating in Spring Framework applications + * @throws TemplateException if template exception occurs + */ protected SpringTemplateCallableHashModel getSpringTemplateCallableHashModel(final Environment env) throws TemplateException { return (SpringTemplateCallableHashModel) env.getVariable(SpringTemplateCallableHashModel.NAME); 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 df6a2bc8d..0ce34d0cc 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 @@ -42,10 +42,22 @@ public abstract class AbstractSpringTemplateDirectiveModel extends AbstractSpringTemplateCallableModel implements TemplateDirectiveModel { + /** + * Construct directive with servlet request and response. + * @param request servlet request + * @param response servlet response + */ public AbstractSpringTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) { super(request, response); } + /** + * Execute this directive. + *

+ * This method establishes Spring's RequestContext and invokes {@link #executeInternal(TemplateModel[], CallPlace, Writer, Environment, ObjectWrapperAndUnwrapper, RequestContext)} + * which must be implemented by derived directive classes. + *

+ */ @Override public final void execute(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env) throws TemplateException, IOException { @@ -53,15 +65,14 @@ public final void execute(TemplateModel[] args, CallPlace callPlace, Writer out, if (!(objectWrapper instanceof ObjectWrapperAndUnwrapper)) { CallableUtils.newGenericExecuteException( - "The ObjectWrapper of environment isn't an instance of ObjectWrapperAndUnwrapper.", this, - isFunction()); + "The ObjectWrapper of environment isn't an instance of ObjectWrapperAndUnwrapper.", this, false); } TemplateModel rcModel = env.getVariable(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE); if (rcModel == null) { CallableUtils.newGenericExecuteException( - AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found.", this, isFunction()); + AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found.", this, false); } RequestContext requestContext = (RequestContext) ((ObjectWrapperAndUnwrapper) objectWrapper).unwrap(rcModel); @@ -69,11 +80,17 @@ public final void execute(TemplateModel[] args, CallPlace callPlace, Writer out, executeInternal(args, callPlace, out, env, (ObjectWrapperAndUnwrapper) objectWrapper, requestContext); } - @Override - protected final boolean isFunction() { - return false; - } - + /** + * Interal execution method that is supposed to be implemented by derived directive classes. + * @param args argument models + * @param callPlace the place where this is being called + * @param out output writer + * @param env template execution environment + * @param objectWrapperAndUnwrapper ObjectWrapperAndUnwrapper + * @param requestContext Spring RequestContext + * @throws TemplateException if template exception occurs + * @throws IOException if IO exception occurs + */ protected abstract void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext) throws TemplateException, IOException; 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 index 7851ad8ae..137837c29 100644 --- 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 @@ -39,25 +39,36 @@ public abstract class AbstractSpringTemplateFunctionModel extends AbstractSpringTemplateCallableModel implements TemplateFunctionModel { + /** + * Construct function with servlet request and response. + * @param request servlet request + * @param response servlet response + */ public AbstractSpringTemplateFunctionModel(HttpServletRequest request, HttpServletResponse response) { super(request, response); } + /** + * Execute this function. + *

+ * This method establishes Spring's RequestContext and invokes {@link #executeInternal(TemplateModel[], CallPlace, Environment, ObjectWrapperAndUnwrapper, RequestContext)} + * which must be implemented by derived function classes. + *

+ */ @Override public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) throws TemplateException { final ObjectWrapper objectWrapper = env.getObjectWrapper(); if (!(objectWrapper instanceof ObjectWrapperAndUnwrapper)) { CallableUtils.newGenericExecuteException( - "The ObjectWrapper of environment isn't an instance of ObjectWrapperAndUnwrapper.", this, - isFunction()); + "The ObjectWrapper of environment isn't an instance of ObjectWrapperAndUnwrapper.", this, true); } TemplateModel rcModel = env.getVariable(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE); if (rcModel == null) { CallableUtils.newGenericExecuteException( - AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found.", this, isFunction()); + AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + " not found.", this, true); } RequestContext requestContext = (RequestContext) ((ObjectWrapperAndUnwrapper) objectWrapper).unwrap(rcModel); @@ -65,11 +76,16 @@ public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environm return executeInternal(args, callPlace, env, (ObjectWrapperAndUnwrapper) objectWrapper, requestContext); } - @Override - protected final boolean isFunction() { - return true; - } - + /** + * Interal execution method that is supposed to be implemented by derived directive classes. + * @param args argument models + * @param callPlace the place where this is being called + * @param env template execution environment + * @param objectWrapperAndUnwrapper ObjectWrapperAndUnwrapper + * @param requestContext Spring RequestContext + * @return function execution result template model + * @throws TemplateException if template exception occurs + */ 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/BindErrorsDirective.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/BindErrorsDirective.java index 6d0c95455..d775cd146 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 @@ -31,7 +31,6 @@ 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.DefaultObjectWrapper; import org.apache.freemarker.core.util.CallableUtils; import org.springframework.validation.Errors; import org.springframework.web.servlet.support.RequestContext; @@ -72,13 +71,7 @@ public class BindErrorsDirective extends AbstractSpringTemplateDirectiveModel { private static final int NAME_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 BindErrorsDirective(HttpServletRequest request, HttpServletResponse response) { super(request, response); @@ -90,7 +83,8 @@ protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer throws TemplateException, IOException { final String name = CallableUtils.getStringArgument(args, NAME_PARAM_IDX, this); - final TemplateModel bindErrorsModel = getBindErrorsTemplateModel(env, objectWrapperAndUnwrapper, requestContext, name); + final TemplateModel bindErrorsModel = getBindErrorsTemplateModel(env, objectWrapperAndUnwrapper, requestContext, + name); if (bindErrorsModel != null) { final TemplateModel[] nestedContentArgs = new TemplateModel[] { bindErrorsModel }; @@ -108,17 +102,13 @@ public ArgumentArrayLayout getDirectiveArgumentArrayLayout() { return ARGS_LAYOUT; } - private final TemplateModel getBindErrorsTemplateModel(Environment env, ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, - RequestContext requestContext, String name) throws TemplateException { + private final TemplateModel getBindErrorsTemplateModel(Environment env, + ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext, String name) + throws TemplateException { 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 wrapObject(objectWrapperAndUnwrapper, errors); + return objectWrapperAndUnwrapper.wrap(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 index f155df171..52cd62b27 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 @@ -103,15 +103,16 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, TemplateModel evaluationContextModel = springTemplateModel.get(EVALUATION_CONTEXT_VAR_NAME); if (evaluationContextModel != null) { - evaluationContext = (EvaluationContext) unwrapObject(objectWrapperAndUnwrapper, evaluationContextModel); + evaluationContext = (EvaluationContext) objectWrapperAndUnwrapper.unwrap(evaluationContextModel); } else { evaluationContext = createEvaluationContext(env, objectWrapperAndUnwrapper, requestContext); - evaluationContextModel = wrapObject(objectWrapperAndUnwrapper, evaluationContext); + evaluationContextModel = objectWrapperAndUnwrapper.wrap(evaluationContext); springTemplateModel.setEvaluationContextModel(evaluationContextModel); } final Object result = expression.getValue(evaluationContext); - return wrapObject(objectWrapperAndUnwrapper, result); + + return (result != null) ? objectWrapperAndUnwrapper.wrap(result) : null; } @Override @@ -170,8 +171,13 @@ public boolean canRead(EvaluationContext context, Object target, String name) th public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { try { TemplateModel model = env.getVariable(name); - Object value = unwrapObject(objectWrapperAndUnwrapper, model); - return new TypedValue(value); + + if (model != null) { + Object value = objectWrapperAndUnwrapper.unwrap(model); + return new TypedValue(value); + } else { + return null; + } } catch (TemplateException e) { throw new AccessException("Can't get environment variable by name, '" + name + "'.", e); } 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 e64aba59c..8b98652b0 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 @@ -84,13 +84,8 @@ public class MessageFunction extends AbstractSpringTemplateFunctionModel { private static final String MESSAGE_RESOLVABLE_PARAM_NAME = "message"; - private static final ArgumentArrayLayout ARGS_LAYOUT = - ArgumentArrayLayout.create( - 1, - true, - StringToIndexMap.of(MESSAGE_RESOLVABLE_PARAM_NAME, MESSAGE_RESOLVABLE_PARAM_IDX), - false - ); + private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, true, + StringToIndexMap.of(MESSAGE_RESOLVABLE_PARAM_NAME, MESSAGE_RESOLVABLE_PARAM_IDX), false); public MessageFunction(HttpServletRequest request, HttpServletResponse response) { super(request, response); @@ -108,10 +103,10 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, String message = null; - final TemplateModel messageResolvableModel = CallableUtils.getOptionalArgument(args, MESSAGE_RESOLVABLE_PARAM_IDX, - TemplateModel.class, this); - final MessageSourceResolvable messageResolvable = (MessageSourceResolvable) unwrapObject( - objectWrapperAndUnwrapper, messageResolvableModel); + final TemplateModel messageResolvableModel = CallableUtils.getOptionalArgument(args, + MESSAGE_RESOLVABLE_PARAM_IDX, TemplateModel.class, this); + final MessageSourceResolvable messageResolvable = (messageResolvableModel != null) + ? (MessageSourceResolvable) objectWrapperAndUnwrapper.unwrap(messageResolvableModel) : null; if (messageResolvable != null) { message = messageSource.getMessage(messageResolvable, requestContext.getLocale()); @@ -126,9 +121,9 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, if (!messageArgsModel.isEmptyCollection()) { msgArgumentList = new ArrayList<>(); TemplateModel msgArgModel; - for (TemplateModelIterator tit = messageArgsModel.iterator(); tit.hasNext(); ) { + for (TemplateModelIterator tit = messageArgsModel.iterator(); tit.hasNext();) { msgArgModel = tit.next(); - msgArgumentList.add(unwrapObject(objectWrapperAndUnwrapper, msgArgModel)); + msgArgumentList.add(objectWrapperAndUnwrapper.unwrap(msgArgModel)); } } @@ -141,7 +136,7 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, } } - return wrapObject(objectWrapperAndUnwrapper, message); + return (message != null) ? objectWrapperAndUnwrapper.wrap(message) : null; } @Override 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 9bd51ef6d..a743c3f9b 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 @@ -84,8 +84,8 @@ protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer 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); + final TemplateStringModel newNestedPathModel = (TemplateStringModel) objectWrapperAndUnwrapper + .wrap(newNestedPath); try { springTemplateModel.setNestedPathModel(newNestedPathModel); 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 deleted file mode 100644 index 4ff355237..000000000 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/SpringFormTemplateCallableHashModel.java +++ /dev/null @@ -1,56 +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 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 index 26ae060d2..4e42923c0 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 @@ -32,16 +32,27 @@ import org.apache.freemarker.core.model.TemplateStringModel; /** - * TemplateHashModel wrapper for templates using Spring directives and functions. + * TemplateHashModel wrapper for templates using Spring directives, functions and internal models. */ public final class SpringTemplateCallableHashModel implements TemplateHashModel, Serializable { private static final long serialVersionUID = 1L; + /** + * Spring namespace model name. + */ public static final String NAME = "spring"; + /** + * Name of the internal nested path template model, which is equivalent to the nestedPath pageContext attribute + * in Spring Framework JSP tag libraries. + */ + // NOTE: The model name, "nestedPathModel", must be different from the "nestedPath" directive model's name. public static final String NESTED_PATH_MODEL = "nestedPathModel"; + /** + * Name of the internal evaluation context template model used by EvalFunction to cache EvaluationContext. + */ public static final String EVALUATION_CONTEXT_MODEL = "evaluationContextModel"; private Map modelsMap = new HashMap<>(); 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 index d6c381e87..9d4065051 100644 --- 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 @@ -40,14 +40,9 @@ * Some valid example(s): *

*
- * 
- *

- * Some valid example(s): - *

- *
- *   <@spring.bind "user"; status>
- *     ${spring.transform(status.editor, user.birthDate)}
- *   </@spring.bind>
+ * <@spring.bind "user.birthDate"; status>
+ *   <div id="userBirthDate">${spring.transform(status.editor, status.actualValue)}</div>
+ * </@spring.bind>
  * 
*

* Note: Unlike Spring Framework's <spring:bind /> JSP Tag Library, this directive @@ -74,10 +69,12 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, throws TemplateException { final TemplateModel editorModel = CallableUtils.getOptionalArgument(args, PROPERTY_EDITOR_PARAM_IDX, TemplateModel.class, this); - final PropertyEditor editor = (PropertyEditor) unwrapObject(objectWrapperAndUnwrapper, editorModel); + final PropertyEditor editor = (editorModel != null) + ? (PropertyEditor) objectWrapperAndUnwrapper.unwrap(editorModel) : null; - final TemplateModel valueModel = CallableUtils.getOptionalArgument(args, VALUE_PARAM_IDX, TemplateModel.class, this); - final Object value = unwrapObject(objectWrapperAndUnwrapper, valueModel); + final TemplateModel valueModel = CallableUtils.getOptionalArgument(args, VALUE_PARAM_IDX, TemplateModel.class, + this); + final Object value = (valueModel != null) ? objectWrapperAndUnwrapper.unwrap(valueModel) : null; String valueAsString = null; @@ -90,7 +87,7 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, } } - return wrapObject(objectWrapperAndUnwrapper, valueAsString); + return (valueAsString != null) ? objectWrapperAndUnwrapper.wrap(valueAsString) : null; } @Override 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 index 2220800b6..ae70a4594 100644 --- 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 @@ -166,7 +166,7 @@ public TemplateModel executeInternal(TemplateModel[] args, CallPlace callPlace, urlString = processor.processUrl(getRequest(), urlString); } - return wrapObject(objectWrapperAndUnwrapper, urlString); + return objectWrapperAndUnwrapper.wrap(urlString); } @Override 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 604cc3d6f..c8fcbd285 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 @@ -35,7 +35,6 @@ 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.SpringFormTemplateCallableHashModel; import org.apache.freemarker.spring.model.SpringTemplateCallableHashModel; /** @@ -141,8 +140,6 @@ protected TemplateHashModel createModel(Map map, ObjectWrapperAn model.putUnlistedModel(SpringTemplateCallableHashModel.NAME, new SpringTemplateCallableHashModel(request, response)); - model.putUnlistedModel(SpringFormTemplateCallableHashModel.NAME, - new SpringFormTemplateCallableHashModel(request, response)); model.putAll(map); From 1c7c98745a87d86d3f953ed99a26e239a3bcaaaa Mon Sep 17 00:00:00 2001 From: Woonsan Ko Date: Tue, 12 Sep 2017 22:16:09 -0400 Subject: [PATCH 32/32] FREEMARKER-55: Updating change logs --- FM3-CHANGE-LOG.txt | 14 +++++++++++ .../spring/model/BindErrorsDirective.java | 8 ++++++- .../freemarker/spring/model/EvalFunction.java | 8 ++++++- .../spring/model/MessageFunction.java | 9 ++++++-- .../spring/model/NestedPathDirective.java | 8 ++++++- .../spring/model/TransformFunction.java | 8 ++++++- .../freemarker/spring/model/UrlFunction.java | 9 ++++++-- .../freemarker/spring/model/package.html | 23 +++++++++++++++++++ .../org/apache/freemarker/spring/package.html | 23 +++++++++++++++++++ .../freemarker/spring/web/view/package.html | 23 +++++++++++++++++++ 10 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/model/package.html create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/package.html create mode 100644 freemarker-spring/src/main/java/org/apache/freemarker/spring/web/view/package.html diff --git a/FM3-CHANGE-LOG.txt b/FM3-CHANGE-LOG.txt index e1bbb44c1..0febe03c4 100644 --- a/FM3-CHANGE-LOG.txt +++ b/FM3-CHANGE-LOG.txt @@ -476,6 +476,20 @@ This is about the Spring Framework Support (freemarker-spring): FREEMARKER-54, F - SpringResourceTemplateLoader, a new TemplateLoader to load templates from Spring Framework's Resources. - New FreeMarkerView and FreeMarkerViewResolver for MVC support. FreeMarkerView supports TaglibFactory and other models by default like FreemarkerServlet does. +- Directives and Functions Support to replace Spring JSP Tag Libraries in spring.tld: + - : No need since FreeMarker Built-In's and escaping directives are better. + - : No need since FreeMarker Built-In's and escaping directives are better. + - : Replaced by spring.message function. e.g, ${spring.message(...)} + - : Replaced by spring.theme function. e.g, ${spring.theme(...)} + - : No need since spring.message(...) and spring.theme(...) functions support + positional varargs for variable length arguments. + - : Replaced by <@spring.hasBindErrors ... /> directive. + - : Replaced by <@spring.nestedPath ... /> directive. + - : Replaced by <@spring.bind ... /> directive. + - : Replaced by spring.transform(...) function. + - : Replaced by spring.url(...) function. + - : No need since spring.url(...) function supports named vargs for variable length parameters. + - : Replaced by spring.eval(...) function. Core / Miscellaneous .................... 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 d775cd146..2b737802a 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 @@ -71,7 +71,13 @@ public class BindErrorsDirective extends AbstractSpringTemplateDirectiveModel { private static final int NAME_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 BindErrorsDirective(HttpServletRequest request, HttpServletResponse response) { super(request, response); 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 52cd62b27..39d210e52 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 @@ -81,7 +81,13 @@ public class EvalFunction extends AbstractSpringTemplateFunctionModel { private static final int EXPRESSION_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 + ); private static final String EVALUATION_CONTEXT_VAR_NAME = "org.apache.freemarker.spring.model.EVALUATION_CONTEXT"; 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 8b98652b0..f30e07b2b 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 @@ -84,8 +84,13 @@ public class MessageFunction extends AbstractSpringTemplateFunctionModel { private static final String MESSAGE_RESOLVABLE_PARAM_NAME = "message"; - private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, true, - StringToIndexMap.of(MESSAGE_RESOLVABLE_PARAM_NAME, MESSAGE_RESOLVABLE_PARAM_IDX), false); + private static final ArgumentArrayLayout ARGS_LAYOUT = + ArgumentArrayLayout.create( + 1, + true, + StringToIndexMap.of(MESSAGE_RESOLVABLE_PARAM_NAME, MESSAGE_RESOLVABLE_PARAM_IDX), + false + ); public MessageFunction(HttpServletRequest request, HttpServletResponse response) { super(request, response); 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 a743c3f9b..b24dfaf9a 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 @@ -60,7 +60,13 @@ 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); 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 index 9d4065051..6110652c4 100644 --- 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 @@ -57,7 +57,13 @@ public class TransformFunction extends AbstractSpringTemplateFunctionModel { 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); + private static final ArgumentArrayLayout ARGS_LAYOUT = + ArgumentArrayLayout.create( + 2, + false, + null, + false + ); public TransformFunction(HttpServletRequest request, HttpServletResponse response) { super(request, response); 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 index ae70a4594..47c83abb3 100644 --- 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 @@ -87,8 +87,13 @@ public class UrlFunction extends AbstractSpringTemplateFunctionModel { private static final String URL_TEMPLATE_DELIMITER_SUFFIX = "}"; - private static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, false, - StringToIndexMap.of(CONTEXT_PARAM_NAME, CONTEXT_PARAM_IDX), true); + private static final ArgumentArrayLayout ARGS_LAYOUT = + ArgumentArrayLayout.create( + 1, + false, + StringToIndexMap.of(CONTEXT_PARAM_NAME, CONTEXT_PARAM_IDX), + true + ); public UrlFunction(HttpServletRequest request, HttpServletResponse response) { super(request, response); diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/package.html b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/package.html new file mode 100644 index 000000000..5a98e442c --- /dev/null +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/package.html @@ -0,0 +1,23 @@ + + + +

FreeMarker Directives and Functions Support for Spring Framework MVC

+ + diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/package.html b/freemarker-spring/src/main/java/org/apache/freemarker/spring/package.html new file mode 100644 index 000000000..05ba1ff33 --- /dev/null +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/package.html @@ -0,0 +1,23 @@ + + + +

Spring Framework (MVC) Support

+ + diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/web/view/package.html b/freemarker-spring/src/main/java/org/apache/freemarker/spring/web/view/package.html new file mode 100644 index 000000000..98b1417d0 --- /dev/null +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/web/view/package.html @@ -0,0 +1,23 @@ + + + +

Spring Framework MVC View Support

+ +