diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java
index b68ab78edbd9..fbeaee6c92a0 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java
@@ -46,8 +46,6 @@
* instance via the "configuration" property. This allows to share a FreeMarker
* Configuration for web and email usage for example.
*
- *
TODO: macros
- *
*
This configurer registers a template loader for this package, allowing to
* reference the "spring.ftl" macro library contained in this package:
*
diff --git a/spring-webflux/src/main/resources/org/springframework/web/reactive/result/view/freemarker/spring.ftl b/spring-webflux/src/main/resources/org/springframework/web/reactive/result/view/freemarker/spring.ftl
new file mode 100644
index 000000000000..eec24b02485b
--- /dev/null
+++ b/spring-webflux/src/main/resources/org/springframework/web/reactive/result/view/freemarker/spring.ftl
@@ -0,0 +1,355 @@
+<#ftl output_format="HTML" strip_whitespace=true>
+<#--
+ * spring.ftl
+ *
+ * This file consists of a collection of FreeMarker macros aimed at easing
+ * some of the common requirements of web applications - in particular
+ * handling of forms.
+ *
+ * Spring's FreeMarker support will automatically make this file and therefore
+ * all macros within it available to any application using Spring's
+ * FreeMarkerConfigurer.
+ *
+ * To take advantage of these macros, the "exposeSpringMacroHelpers" property
+ * of the FreeMarker class needs to be set to "true". This will expose a
+ * RequestContext under the name "springMacroRequestContext", as needed by
+ * the macros in this library.
+ *
+ * @author Darren Davison
+ * @author Juergen Hoeller
+ * @author Issam El-atif
+ * @since 5.2
+ -->
+
+<#--
+ * message
+ *
+ * Macro to translate a message code into a message.
+ -->
+<#macro message code>${springMacroRequestContext.getMessage(code)?no_esc}#macro>
+
+<#--
+ * messageText
+ *
+ * Macro to translate a message code into a message,
+ * using the given default text if no message found.
+ -->
+<#macro messageText code, text>${springMacroRequestContext.getMessage(code, text)?no_esc}#macro>
+
+<#--
+ * messageArgs
+ *
+ * Macro to translate a message code with arguments into a message.
+ -->
+<#macro messageArgs code, args>${springMacroRequestContext.getMessage(code, args)?no_esc}#macro>
+
+<#--
+ * messageArgsText
+ *
+ * Macro to translate a message code with arguments into a message,
+ * using the given default text if no message found.
+ -->
+<#macro messageArgsText code, args, text>${springMacroRequestContext.getMessage(code, args, text)?no_esc}#macro>
+
+<#--
+ * url
+ *
+ * Takes a relative URL and makes it absolute from the server root by
+ * adding the context root for the web application.
+ -->
+<#macro url relativeUrl extra...><#if extra?? && extra?size!=0>${springMacroRequestContext.getContextUrl(relativeUrl,extra)?no_esc}<#else>${springMacroRequestContext.getContextUrl(relativeUrl)?no_esc}#if>#macro>
+
+<#--
+ * bind
+ *
+ * Exposes a BindStatus object for the given bind path, which can be
+ * a bean (e.g. "person") to get global errors, or a bean property
+ * (e.g. "person.name") to get field errors. Can be called multiple times
+ * within a form to bind to multiple command objects and/or field names.
+ *
+ * This macro will participate in the default HTML escape setting for the given
+ * RequestContext. This can be customized by calling "setDefaultHtmlEscape"
+ * on the "springMacroRequestContext" context variable, or via the
+ * "defaultHtmlEscape" context-param in web.xml (same as for the JSP bind tag).
+ * Also regards a "htmlEscape" variable in the namespace of this library.
+ *
+ * Producing no output, the following context variable will be available
+ * each time this macro is referenced (assuming you import this library in
+ * your templates with the namespace 'spring'):
+ *
+ * spring.status : a BindStatus instance holding the command object name,
+ * expression, value, and error messages and codes for the path supplied
+ *
+ * @param path the path (string value) of the value required to bind to.
+ * Spring defaults to a command name of "command" but this can be
+ * overridden by user configuration.
+ -->
+<#macro bind path>
+ <#if htmlEscape?exists>
+ <#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)>
+ <#else>
+ <#assign status = springMacroRequestContext.getBindStatus(path)>
+ #if>
+ <#-- assign a temporary value, forcing a string representation for any
+ kind of variable. This temp value is only used in this macro lib -->
+ <#if status.value?exists && status.value?is_boolean>
+ <#assign stringStatusValue=status.value?string>
+ <#else>
+ <#assign stringStatusValue=status.value?default("")>
+ #if>
+#macro>
+
+<#--
+ * bindEscaped
+ *
+ * Similar to spring:bind, but takes an explicit HTML escape flag rather
+ * than relying on the default HTML escape setting.
+ -->
+<#macro bindEscaped path, htmlEscape>
+ <#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)>
+ <#-- assign a temporary value, forcing a string representation for any
+ kind of variable. This temp value is only used in this macro lib -->
+ <#if status.value?exists && status.value?is_boolean>
+ <#assign stringStatusValue=status.value?string>
+ <#else>
+ <#assign stringStatusValue=status.value?default("")>
+ #if>
+#macro>
+
+<#--
+ * formInput
+ *
+ * Display a form input field of type 'text' and bind it to an attribute
+ * of a command or bean.
+ *
+ * @param path the name of the field to bind to
+ * @param attributes any additional attributes for the element
+ * (such as class or CSS styles or size)
+ -->
+<#macro formInput path attributes="" fieldType="text">
+ <@bind path/>
+ ${stringStatusValue}#if>" ${attributes?no_esc}<@closeTag/>
+#macro>
+
+<#--
+ * formPasswordInput
+ *
+ * Display a form input field of type 'password' and bind it to an attribute
+ * of a command or bean. No value will ever be displayed. This functionality
+ * can also be obtained by calling the formInput macro with a 'type' parameter
+ * of 'password'.
+ *
+ * @param path the name of the field to bind to
+ * @param attributes any additional attributes for the element
+ * (such as class or CSS styles or size)
+ -->
+<#macro formPasswordInput path attributes="">
+ <@formInput path, attributes, "password"/>
+#macro>
+
+<#--
+ * formHiddenInput
+ *
+ * Generate a form input field of type 'hidden' and bind it to an attribute
+ * of a command or bean. This functionality can also be obtained by calling
+ * the formInput macro with a 'type' parameter of 'hidden'.
+ *
+ * @param path the name of the field to bind to
+ * @param attributes any additional attributes for the element
+ * (such as class or CSS styles or size)
+ -->
+<#macro formHiddenInput path attributes="">
+ <@formInput path, attributes, "hidden"/>
+#macro>
+
+<#--
+ * formTextarea
+ *
+ * Display a text area and bind it to an attribute of a command or bean.
+ *
+ * @param path the name of the field to bind to
+ * @param attributes any additional attributes for the element
+ * (such as class or CSS styles or size)
+ -->
+<#macro formTextarea path attributes="">
+ <@bind path/>
+
+#macro>
+
+<#--
+ * formSingleSelect
+ *
+ * Show a selectbox (dropdown) input element allowing a single value to be chosen
+ * from a list of options.
+ *
+ * @param path the name of the field to bind to
+ * @param options a map (value=label) of all the available options
+ * @param attributes any additional attributes for the element
+ * (such as class or CSS styles or size)
+-->
+<#macro formSingleSelect path options attributes="">
+ <@bind path/>
+
+#macro>
+
+<#--
+ * formMultiSelect
+ *
+ * Show a listbox of options allowing the user to make 0 or more choices from
+ * the list of options.
+ *
+ * @param path the name of the field to bind to
+ * @param options a map (value=label) of all the available options
+ * @param attributes any additional attributes for the element
+ * (such as class or CSS styles or size)
+-->
+<#macro formMultiSelect path options attributes="">
+ <@bind path/>
+
+#macro>
+
+<#--
+ * formRadioButtons
+ *
+ * Show radio buttons.
+ *
+ * @param path the name of the field to bind to
+ * @param options a map (value=label) of all the available options
+ * @param separator the HTML tag or other character list that should be used to
+ * separate each option (typically ' ' or ' ')
+ * @param attributes any additional attributes for the element
+ * (such as class or CSS styles or size)
+-->
+<#macro formRadioButtons path options separator attributes="">
+ <@bind path/>
+ <#list options?keys as value>
+ <#assign id="${status.expression?replace('[','')?replace(']','')}${value_index}">
+ checked="checked"#if> ${attributes?no_esc}<@closeTag/>
+ ${separator?no_esc}
+ #list>
+#macro>
+
+<#--
+ * formCheckboxes
+ *
+ * Show checkboxes.
+ *
+ * @param path the name of the field to bind to
+ * @param options a map (value=label) of all the available options
+ * @param separator the HTML tag or other character list that should be used to
+ * separate each option (typically ' ' or ' ')
+ * @param attributes any additional attributes for the element
+ * (such as class or CSS styles or size)
+-->
+<#macro formCheckboxes path options separator attributes="">
+ <@bind path/>
+ <#list options?keys as value>
+ <#assign id="${status.expression?replace('[','')?replace(']','')}${value_index}">
+ <#assign isSelected = contains(status.actualValue?default([""]), value)>
+ checked="checked"#if> ${attributes?no_esc}<@closeTag/>
+ ${separator?no_esc}
+ #list>
+
+#macro>
+
+<#--
+ * formCheckbox
+ *
+ * Show a single checkbox.
+ *
+ * @param path the name of the field to bind to
+ * @param attributes any additional attributes for the element
+ * (such as class or CSS styles or size)
+-->
+<#macro formCheckbox path attributes="">
+ <@bind path />
+ <#assign id="${status.expression?replace('[','')?replace(']','')}">
+ <#assign isSelected = status.value?? && status.value?string=="true">
+
+ checked="checked"#if> ${attributes?no_esc}/>
+#macro>
+
+<#--
+ * showErrors
+ *
+ * Show validation errors for the currently bound field, with
+ * optional style attributes.
+ *
+ * @param separator the HTML tag or other character list that should be used to
+ * separate each option (typically ' ' or ' ')
+ * @param classOrStyle either the name of a CSS class element (which is defined in
+ * the template or an external CSS file) or an inline style. If the value passed
+ * in here contains a colon (:) then a 'style=' attribute will be used,
+ * otherwise a 'class=' attribute will be used.
+-->
+<#macro showErrors separator classOrStyle="">
+ <#list status.errorMessages as error>
+ <#if classOrStyle == "">
+ ${error}
+ <#else>
+ <#if classOrStyle?index_of(":") == -1><#assign attr="class"><#else><#assign attr="style">#if>
+ ${error}
+ #if>
+ <#if error_has_next>${separator?no_esc}#if>
+ #list>
+#macro>
+
+<#--
+ * checkSelected
+ *
+ * Check a value in a list to see if it is the currently selected value.
+ * If so, add the 'selected="selected"' text to the output.
+ * Handles values of numeric and string types.
+ * This function is used internally but can be accessed by user code if required.
+ *
+ * @param value the current value in a list iteration
+-->
+<#macro checkSelected value>
+ <#if stringStatusValue?is_number && stringStatusValue == value?number>selected="selected"#if>
+ <#if stringStatusValue?is_string && stringStatusValue == value>selected="selected"#if>
+#macro>
+
+<#--
+ * contains
+ *
+ * Macro to return true if the list contains the scalar, false if not.
+ * Surprisingly not a FreeMarker builtin.
+ * This function is used internally but can be accessed by user code if required.
+ *
+ * @param list the list to search for the item
+ * @param item the item to search for in the list
+ * @return true if item is found in the list, false otherwise
+-->
+<#function contains list item>
+ <#list list as nextInList>
+ <#if nextInList == item><#return true>#if>
+ #list>
+ <#return false>
+#function>
+
+<#--
+ * closeTag
+ *
+ * Simple macro to close an HTML tag that has no body with '>' or '/>',
+ * depending on the value of a 'xhtmlCompliant' variable in the namespace
+ * of this library.
+-->
+<#macro closeTag>
+ <#if xhtmlCompliant?exists && xhtmlCompliant>/><#else>>#if>
+#macro>
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DummyMacroRequestContext.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DummyMacroRequestContext.java
new file mode 100644
index 000000000000..7a1adbd25b6d
--- /dev/null
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DummyMacroRequestContext.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.web.reactive.result.view;
+
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.util.UriTemplate;
+
+/**
+ * Dummy request context used for VTL and FTL macro tests.
+ *
+ * @author Darren Davison
+ * @author Juergen Hoeller
+ * @author Issam El-atif
+ *
+ * @see org.springframework.web.reactive.result.view.RequestContext
+ */
+public class DummyMacroRequestContext {
+
+ private final ServerWebExchange exchange;
+
+ private final ModelMap model;
+
+ private final GenericApplicationContext context;
+
+ private Map messageMap;
+
+ private String contextPath;
+
+ public DummyMacroRequestContext(ServerWebExchange exchange, ModelMap model, GenericApplicationContext context) {
+ this.exchange = exchange;
+ this.model = model;
+ this.context = context;
+ }
+
+ public void setMessageMap(Map messageMap) {
+ this.messageMap = messageMap;
+ }
+
+ /**
+ * @see org.springframework.web.reactive.result.view.RequestContext#getMessage(String)
+ */
+ public String getMessage(String code) {
+ return this.messageMap.get(code);
+ }
+
+ /**
+ * @see org.springframework.web.reactive.result.view.RequestContext#getMessage(String, String)
+ */
+ public String getMessage(String code, String defaultMsg) {
+ String msg = this.messageMap.get(code);
+ return (msg != null ? msg : defaultMsg);
+ }
+
+ /**
+ * @see org.springframework.web.reactive.result.view.RequestContext#getMessage(String, List)
+ */
+ public String getMessage(String code, List> args) {
+ return this.messageMap.get(code) + args;
+ }
+
+ /**
+ * @see org.springframework.web.reactive.result.view.RequestContext#getMessage(String, List, String)
+ */
+ public String getMessage(String code, List> args, String defaultMsg) {
+ String msg = this.messageMap.get(code);
+ return (msg != null ? msg + args : defaultMsg);
+ }
+
+ public void setContextPath(String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+ /**
+ * @see org.springframework.web.reactive.result.view.RequestContext#getContextPath()
+ */
+ public String getContextPath() {
+ return this.contextPath;
+ }
+
+ /**
+ * @see org.springframework.web.reactive.result.view.RequestContext#getContextUrl(String)
+ */
+ public String getContextUrl(String relativeUrl) {
+ return getContextPath() + relativeUrl;
+ }
+
+ /**
+ * @see org.springframework.web.reactive.result.view.RequestContext#getContextUrl(String, Map)
+ */
+ public String getContextUrl(String relativeUrl, Map params) {
+ UriTemplate template = new UriTemplate(relativeUrl);
+ return getContextPath() + template.expand(params).toASCIIString();
+ }
+
+ /**
+ * @see org.springframework.web.reactive.result.view.RequestContext#getBindStatus(String)
+ */
+ public BindStatus getBindStatus(String path) throws IllegalStateException {
+ return new BindStatus(new RequestContext(this.exchange, this.model, this.context), path, false);
+ }
+
+ /**
+ * @see org.springframework.web.reactive.result.view.RequestContext#getBindStatus(String, boolean)
+ */
+ public BindStatus getBindStatus(String path, boolean htmlEscape) throws IllegalStateException {
+ return new BindStatus(new RequestContext(this.exchange, this.model, this.context), path, true);
+ }
+
+}
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurerTests.java
new file mode 100644
index 000000000000..1c683d9b5ad5
--- /dev/null
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurerTests.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.web.reactive.result.view.freemarker;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Properties;
+
+import freemarker.cache.ClassTemplateLoader;
+import freemarker.cache.MultiTemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import org.junit.Test;
+
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
+import org.springframework.ui.freemarker.SpringTemplateLoader;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIOException;
+
+/**
+ * @author Juergen Hoeller
+ * @author Issam El-atif
+ */
+public class FreeMarkerConfigurerTests {
+
+ @Test
+ public void freeMarkerConfigurerDefaultEncoding() throws IOException, TemplateException {
+ FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
+ configurer.afterPropertiesSet();
+ Configuration cfg = configurer.getConfiguration();
+ assertThat(cfg.getDefaultEncoding()).isEqualTo("UTF-8");
+ }
+
+ @Test
+ public void freeMarkerConfigurerWithConfigLocation() {
+ FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
+ configurer.setConfigLocation(new FileSystemResource("myprops.properties"));
+ Properties props = new Properties();
+ props.setProperty("myprop", "/mydir");
+ configurer.setFreemarkerSettings(props);
+ assertThatIOException().isThrownBy(
+ configurer::afterPropertiesSet);
+ }
+
+ @Test
+ public void freeMarkerConfigurerWithResourceLoaderPath() throws Exception {
+ FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
+ configurer.setTemplateLoaderPath("file:/mydir");
+ configurer.afterPropertiesSet();
+ Configuration cfg = configurer.getConfiguration();
+ assertThat(cfg.getTemplateLoader()).isInstanceOf(MultiTemplateLoader.class);
+ MultiTemplateLoader multiTemplateLoader = (MultiTemplateLoader)cfg.getTemplateLoader();
+ assertThat(multiTemplateLoader.getTemplateLoader(0)).isInstanceOf(SpringTemplateLoader.class);
+ assertThat(multiTemplateLoader.getTemplateLoader(1)).isInstanceOf(ClassTemplateLoader.class);
+ }
+
+ @Test
+ @SuppressWarnings("rawtypes")
+ public void freeMarkerConfigurerWithNonFileResourceLoaderPath() throws Exception {
+ FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
+ configurer.setTemplateLoaderPath("file:/mydir");
+ Properties settings = new Properties();
+ settings.setProperty("localized_lookup", "false");
+ configurer.setFreemarkerSettings(settings);
+ configurer.setResourceLoader(new ResourceLoader() {
+ @Override
+ public Resource getResource(String location) {
+ if (!("file:/mydir".equals(location) || "file:/mydir/test".equals(location))) {
+ throw new IllegalArgumentException(location);
+ }
+ return new ByteArrayResource("test".getBytes(), "test");
+ }
+ @Override
+ public ClassLoader getClassLoader() {
+ return getClass().getClassLoader();
+ }
+ });
+ configurer.afterPropertiesSet();
+ assertThat(configurer.getConfiguration()).isInstanceOf(Configuration.class);
+ Configuration fc = configurer.getConfiguration();
+ Template ft = fc.getTemplate("test");
+ assertThat(FreeMarkerTemplateUtils.processTemplateIntoString(ft, new HashMap())).isEqualTo("test");
+ }
+
+ @Test // SPR-12448
+ public void freeMarkerConfigurationAsBean() {
+ DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+ RootBeanDefinition loaderDef = new RootBeanDefinition(SpringTemplateLoader.class);
+ loaderDef.getConstructorArgumentValues().addGenericArgumentValue(new DefaultResourceLoader());
+ loaderDef.getConstructorArgumentValues().addGenericArgumentValue("/freemarker");
+ RootBeanDefinition configDef = new RootBeanDefinition(Configuration.class);
+ configDef.getPropertyValues().add("templateLoader", loaderDef);
+ beanFactory.registerBeanDefinition("freeMarkerConfig", configDef);
+ beanFactory.getBean(Configuration.class);
+ }
+
+}
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java
new file mode 100644
index 000000000000..bf40990ed6d4
--- /dev/null
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.web.reactive.result.view.freemarker;
+
+import java.io.FileWriter;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Map;
+
+import freemarker.template.Configuration;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
+import org.springframework.mock.web.test.server.MockServerWebExchange;
+import org.springframework.tests.sample.beans.TestBean;
+import org.springframework.ui.ExtendedModelMap;
+import org.springframework.ui.ModelMap;
+import org.springframework.util.FileCopyUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.reactive.result.view.DummyMacroRequestContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Darren Davison
+ * @author Juergen Hoeller
+ * @author Issam El-atif
+ */
+public class FreeMarkerMacroTests {
+
+ private MockServerWebExchange exchange;
+
+ private Configuration freeMarkerConfig;
+
+ @Before
+ public void setUp() throws Exception {
+ this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path"));
+ FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
+ configurer.setTemplateLoaderPaths("classpath:/", "file://" + System.getProperty("java.io.tmpdir"));
+ this.freeMarkerConfig = configurer.createConfiguration();
+ }
+
+ @Test
+ public void testName() throws Exception {
+ assertThat(getMacroOutput("NAME")).isEqualTo("Darren");
+ }
+
+ @Test
+ public void testAge() throws Exception {
+ assertThat(getMacroOutput("AGE")).isEqualTo("99");
+ }
+
+ @Test
+ public void testMessage() throws Exception {
+ assertThat(getMacroOutput("MESSAGE")).isEqualTo("Howdy Mundo");
+ }
+
+ @Test
+ public void testDefaultMessage() throws Exception {
+ assertThat(getMacroOutput("DEFAULTMESSAGE")).isEqualTo("hi planet");
+ }
+
+ @Test
+ public void testMessageArgs() throws Exception {
+ assertThat(getMacroOutput("MESSAGEARGS")).isEqualTo("Howdy[World]");
+ }
+
+ @Test
+ public void testMessageArgsWithDefaultMessage() throws Exception {
+ assertThat(getMacroOutput("MESSAGEARGSWITHDEFAULTMESSAGE")).isEqualTo("Hi");
+ }
+
+ @Test
+ public void testUrl() throws Exception {
+ assertThat(getMacroOutput("URL")).isEqualTo("/springtest/aftercontext.html");
+ }
+
+ @Test
+ public void testUrlParams() throws Exception {
+ assertThat(getMacroOutput("URLPARAMS")).isEqualTo("/springtest/aftercontext/bar?spam=bucket");
+ }
+
+ @Test
+ public void testForm1() throws Exception {
+ assertThat(getMacroOutput("FORM1")).isEqualTo("");
+ }
+
+ @Test
+ public void testForm2() throws Exception {
+ assertThat(getMacroOutput("FORM2")).isEqualTo("");
+ }
+
+ @Test
+ public void testForm3() throws Exception {
+ assertThat(getMacroOutput("FORM3")).isEqualTo("");
+ }
+
+ @Test
+ public void testForm4() throws Exception {
+ assertThat(getMacroOutput("FORM4")).isEqualTo("");
+ }
+
+ @Test
+ public void testForm9() throws Exception {
+ assertThat(getMacroOutput("FORM9")).isEqualTo("");
+ }
+
+ @Test
+ public void testForm10() throws Exception {
+ assertThat(getMacroOutput("FORM10")).isEqualTo("");
+ }
+
+ @Test
+ public void testForm11() throws Exception {
+ assertThat(getMacroOutput("FORM11")).isEqualTo("");
+ }
+
+ @Test
+ public void testForm12() throws Exception {
+ assertThat(getMacroOutput("FORM12")).isEqualTo("");
+ }
+
+ @Test
+ public void testForm13() throws Exception {
+ assertThat(getMacroOutput("FORM13")).isEqualTo("");
+ }
+
+ @Test
+ public void testForm15() throws Exception {
+ String output = getMacroOutput("FORM15");
+ assertThat(output.startsWith("")).as("Wrong output: " + output).isTrue();
+ assertThat(output.contains("")).as("Wrong output: " + output).isTrue();
+ }
+
+ @Test
+ public void testForm16() throws Exception {
+ String output = getMacroOutput("FORM16");
+ assertThat(output.startsWith(
+ "")).as("Wrong output: " + output).isTrue();
+ assertThat(output.contains(
+ "")).as("Wrong output: " + output).isTrue();
+ }
+
+ @Test
+ public void testForm17() throws Exception {
+ assertThat(getMacroOutput("FORM17")).isEqualTo("");
+ }
+
+ @Test
+ public void testForm18() throws Exception {
+ String output = getMacroOutput("FORM18");
+ assertThat(output.startsWith(
+ "")).as("Wrong output: " + output).isTrue();
+ assertThat(output.contains(
+ "")).as("Wrong output: " + output).isTrue();
+ }
+
+ private String getMacroOutput(String name) throws Exception {
+ String macro = fetchMacro(name);
+ assertThat(macro).isNotNull();
+
+ FileSystemResource resource = new FileSystemResource(System.getProperty("java.io.tmpdir") + "/tmp.ftl");
+ FileCopyUtils.copy("<#import \"spring.ftl\" as spring />\n" + macro, new FileWriter(resource.getPath()));
+
+ Map msgMap = new HashMap<>();
+ msgMap.put("hello", "Howdy");
+ msgMap.put("world", "Mundo");
+
+ TestBean darren = new TestBean("Darren", 99);
+ TestBean fred = new TestBean("Fred");
+ fred.setJedi(true);
+ darren.setSpouse(fred);
+ darren.setJedi(true);
+ darren.setStringArray(new String[] {"John", "Fred"});
+
+ Map names = new HashMap<>();
+ names.put("Darren", "Darren Davison");
+ names.put("John", "John Doe");
+ names.put("Fred", "Fred Bloggs");
+ names.put("Rob&Harrop", "Rob Harrop");
+
+ ModelMap model = new ExtendedModelMap();
+ DummyMacroRequestContext rc = new DummyMacroRequestContext(this.exchange, model, new GenericApplicationContext());
+ rc.setMessageMap(msgMap);
+ rc.setContextPath("/springtest");
+
+ model.put("command", darren);
+ model.put("springMacroRequestContext", rc);
+ model.put("msgArgs", new Object[] { "World" });
+ model.put("nameOptionMap", names);
+ model.put("options", names.values());
+
+ FreeMarkerView view = new FreeMarkerView();
+ view.setBeanName("myView");
+ view.setUrl("tmp.ftl");
+ view.setConfiguration(freeMarkerConfig);
+
+ view.render(model, null, this.exchange).subscribe();
+
+ // tokenize output and ignore whitespace
+ String output = this.exchange.getResponse().getBodyAsString().block();
+ output = output.replace("\r\n", "\n");
+ return output.trim();
+ }
+
+ private String fetchMacro(String name) throws Exception {
+ ClassPathResource resource = new ClassPathResource("test-macro.ftl", getClass());
+ assertThat(resource.exists()).isTrue();
+ String all = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream()));
+ all = all.replace("\r\n", "\n");
+ String[] macros = StringUtils.delimitedListToStringArray(all, "\n\n");
+ for (String macro : macros) {
+ if (macro.startsWith(name)) {
+ return macro.substring(macro.indexOf("\n")).trim();
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/freemarker/test-macro.ftl b/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/freemarker/test-macro.ftl
new file mode 100644
index 000000000000..ee19fac64b9a
--- /dev/null
+++ b/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/freemarker/test-macro.ftl
@@ -0,0 +1,82 @@
+<#--
+test template for FreeMarker macro test class
+-->
+<#import "spring.ftl" as spring />
+
+NAME
+${command.name}
+
+AGE
+${command.age}
+
+MESSAGE
+<@spring.message "hello"/> <@spring.message "world"/>
+
+DEFAULTMESSAGE
+<@spring.messageText "no.such.code", "hi"/> <@spring.messageText "no.such.code", "planet"/>
+
+MESSAGEARGS
+<@spring.messageArgs "hello", msgArgs/>
+
+MESSAGEARGSWITHDEFAULTMESSAGE
+<@spring.messageArgsText "no.such.code", msgArgs, "Hi"/>
+
+URL
+<@spring.url "/aftercontext.html"/>
+
+URLPARAMS
+<@spring.url relativeUrl="/aftercontext/{foo}?spam={spam}" foo="bar" spam="bucket"/>
+
+FORM1
+<@spring.formInput "command.name", ""/>
+
+FORM2
+<@spring.formInput "command.name", 'class="myCssClass"'/>
+
+FORM3
+<@spring.formTextarea "command.name", ""/>
+
+FORM4
+<@spring.formTextarea "command.name", "rows=10 cols=30"/>
+
+FORM5
+<@spring.formSingleSelect "command.name", nameOptionMap, ""/>
+
+FORM6
+<@spring.formMultiSelect "command.spouses", nameOptionMap, ""/>
+
+FORM7
+<@spring.formRadioButtons "command.name", nameOptionMap, " ", ""/>
+
+FORM8
+<@spring.formCheckboxes "command.stringArray", nameOptionMap, " ", ""/>
+
+FORM9
+<@spring.formPasswordInput "command.name", ""/>
+
+FORM10
+<@spring.formHiddenInput "command.name", ""/>
+
+FORM11
+<@spring.formInput "command.name", "", "text"/>
+
+FORM12
+<@spring.formInput "command.name", "", "hidden"/>
+
+FORM13
+<@spring.formInput "command.name", "", "password"/>
+
+FORM14
+<@spring.formSingleSelect "command.name", options, ""/>
+
+FORM15
+<@spring.formCheckbox "command.name"/>
+
+FORM16
+<@spring.formCheckbox "command.jedi"/>
+
+FORM17
+<@spring.formInput "command.spouses[0].name", ""/>
+
+FORM18
+<@spring.formCheckbox "command.spouses[0].jedi" />
\ No newline at end of file