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} + +<#-- + * 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} + +<#-- + * messageArgs + * + * Macro to translate a message code with arguments into a message. + --> +<#macro messageArgs code, args>${springMacroRequestContext.getMessage(code, args)?no_esc} + +<#-- + * 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} + +<#-- + * 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} + +<#-- + * 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)> + + <#-- 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("")> + + + +<#-- + * 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("")> + + + +<#-- + * 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}" ${attributes?no_esc}<@closeTag/> + + +<#-- + * 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"/> + + +<#-- + * 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"/> + + +<#-- + * 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/> + + + +<#-- + * 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/> + + + +<#-- + * 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/> + + + +<#-- + * 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" ${attributes?no_esc}<@closeTag/> + ${separator?no_esc} + + + +<#-- + * 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" ${attributes?no_esc}<@closeTag/> + ${separator?no_esc} + + + + +<#-- + * 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" ${attributes?no_esc}/> + + +<#-- + * 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"> + ${error} + + <#if error_has_next>${separator?no_esc} + + + +<#-- + * 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 stringStatusValue?is_string && stringStatusValue == value>selected="selected" + + +<#-- + * 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> + + <#return false> + + +<#-- + * 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>> + 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