diff --git a/spring-mvc-extension/README.md b/spring-mvc-extension/README.md new file mode 100644 index 000000000..79774cb8f --- /dev/null +++ b/spring-mvc-extension/README.md @@ -0,0 +1,55 @@ +#Mustache extension for Spring MVC + +To enable mustache views in your Spring MVC application add the `@EnableMustache` annotation to your configuration class. + +###Support for messages and themes + +This extension provides annotations to let you handle message resolving in your model attributes. + + + + + + + + + + + + + + + + + +
AnnotationDescription
@MessageAnnotated methods will have their return values resolved to a message.
@ThemeAnnotated methods will have their return values resolved to a theme message.
+ +Both `@Message` and `@Theme` can resolve embedded values. + + public class MustacheGreetingContext { + + private final String name; + + public MustacheGreetingContext() { + this("name"); + } + + public MustacheGreetingContext(final String name) { + this.name = name; + } + + @Message + public String getGreeting() { + return "${my.greeting.property}"; + } + + @Message + public String getName() { + return name; + } + + @Theme + public String getColor() { + return "color"; + } + } diff --git a/spring-mvc-extension/pom.xml b/spring-mvc-extension/pom.xml new file mode 100644 index 000000000..e095ea39a --- /dev/null +++ b/spring-mvc-extension/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + + nl.onedott.mustachejava.extension + springmvc + 0.9.0 + Spring MVC extension for mustache.java + http://github.com/spullara/mustache.java + + + + Bart Tegenbosch + bart@onedott.nl + Onedott + http://www.onedott.nl + + + + + 3.2.0.RELEASE + + + + + + com.github.spullara.mustache.java + compiler + 0.8.16 + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + + org.springframework + spring-webmvc + ${spring.version} + + + + org.springframework + spring-test + ${spring.version} + compile + + + + org.mockito + mockito-all + 1.9.5 + test + + + + junit + junit + 4.11 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.9 + + -Xmx128M + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + UTF-8 + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + diff --git a/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/SpringMustacheException.java b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/SpringMustacheException.java new file mode 100644 index 000000000..4662b3ee9 --- /dev/null +++ b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/SpringMustacheException.java @@ -0,0 +1,20 @@ +package nl.onedott.mustachejava.extension.springmvc; + +import com.github.mustachejava.MustacheException; + +/** + * @author Bart Tegenbosch + */ +public class SpringMustacheException extends MustacheException { + public SpringMustacheException(final String message) { + super(message); + } + + public SpringMustacheException(final String message, final Throwable cause) { + super(message, cause); + } + + public SpringMustacheException(final Throwable cause) { + super(cause); + } +} diff --git a/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/annotation/EnableMustache.java b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/annotation/EnableMustache.java new file mode 100644 index 000000000..006ed98a0 --- /dev/null +++ b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/annotation/EnableMustache.java @@ -0,0 +1,23 @@ +package nl.onedott.mustachejava.extension.springmvc.annotation; + +import nl.onedott.mustachejava.extension.springmvc.config.MustacheConfiguration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * Enable Mustache views and model processing for Spring MVC. + * + * @author Bart Tegenbosch + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(MustacheConfiguration.class) +public @interface EnableMustache { + /* + * The resource location for mustache templates. + * Supports resource prefixes like {@code file:} and {@code classpath:} + */ + String value() default "mustache/"; +} diff --git a/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/annotation/Message.java b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/annotation/Message.java new file mode 100644 index 000000000..ef06c5bad --- /dev/null +++ b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/annotation/Message.java @@ -0,0 +1,13 @@ +package nl.onedott.mustachejava.extension.springmvc.annotation; + +import java.lang.annotation.*; + +/** + * @author Bart Tegenbosch + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Message { + +} diff --git a/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/annotation/Theme.java b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/annotation/Theme.java new file mode 100644 index 000000000..23906ac1e --- /dev/null +++ b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/annotation/Theme.java @@ -0,0 +1,13 @@ +package nl.onedott.mustachejava.extension.springmvc.annotation; + +import java.lang.annotation.*; + +/** + * @author Bart Tegenbosch + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Theme { + +} diff --git a/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/config/MustacheConfiguration.java b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/config/MustacheConfiguration.java new file mode 100644 index 000000000..f374bcd96 --- /dev/null +++ b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/config/MustacheConfiguration.java @@ -0,0 +1,59 @@ +package nl.onedott.mustachejava.extension.springmvc.config; + +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.MustacheFactory; +import nl.onedott.mustachejava.extension.springmvc.annotation.EnableMustache; +import nl.onedott.mustachejava.extension.springmvc.mustache.MessageSupportReflectionObjectHandler; +import nl.onedott.mustachejava.extension.springmvc.view.MustacheView; +import nl.onedott.mustachejava.extension.springmvc.view.MustacheViewResolver; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.web.servlet.ViewResolver; + +import java.util.Map; + +/** + * Imported by using the {@link nl.onedott.mustachejava.extension.springmvc.annotation.EnableMustache} annotation. + * + * @author Bart Tegenbosch + */ +@Configuration +public class MustacheConfiguration implements ImportAware { + + @Autowired + private ApplicationContext context; + + private Resource resourceLocation; + + @Override + public void setImportMetadata(final AnnotationMetadata importMetadata) { + String className = EnableMustache.class.getName(); + setProperties(importMetadata.getAnnotationAttributes(className)); + } + + @Bean + public MustacheFactory mustacheFactory() throws Exception { + DefaultMustacheFactory factory = new DefaultMustacheFactory(resourceLocation.getFile()); + factory.setObjectHandler(new MessageSupportReflectionObjectHandler(context)); + return factory; + } + + @Bean + public ViewResolver viewResolver() throws Exception { + MustacheViewResolver viewResolver = new MustacheViewResolver(mustacheFactory()); + + viewResolver.setViewClass(MustacheView.class); + viewResolver.setSuffix(".mustache"); + return viewResolver; + } + + public void setProperties(final Map attributes) { + String pattern = (String) attributes.get("value"); + resourceLocation = context.getResource(pattern); + } +} diff --git a/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/mustache/MessageSupportReflectionObjectHandler.java b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/mustache/MessageSupportReflectionObjectHandler.java new file mode 100644 index 000000000..c9b7a331a --- /dev/null +++ b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/mustache/MessageSupportReflectionObjectHandler.java @@ -0,0 +1,40 @@ +package nl.onedott.mustachejava.extension.springmvc.mustache; + +import com.github.mustachejava.reflect.ReflectionObjectHandler; +import com.github.mustachejava.reflect.ReflectionWrapper; +import com.github.mustachejava.util.Wrapper; +import nl.onedott.mustachejava.extension.springmvc.annotation.Message; +import nl.onedott.mustachejava.extension.springmvc.annotation.Theme; +import org.springframework.context.MessageSource; +import org.springframework.util.Assert; + +import java.lang.reflect.Method; + +/** + * Enables wrapper with message support. + * @author Bart Tegenbosch + */ +public class MessageSupportReflectionObjectHandler extends ReflectionObjectHandler { + + private final MessageSource messageSource; + + public MessageSupportReflectionObjectHandler(final MessageSource messageSource) { + Assert.notNull(messageSource); + this.messageSource = messageSource; + } + + @Override + public Wrapper find(final String name, final Object[] scopes) { + Wrapper wrapper = super.find(name, scopes); + if (wrapper instanceof ReflectionWrapper) { + ReflectionWrapper w = (ReflectionWrapper) wrapper; + Method method = w.getMethod(); + + if (method.isAnnotationPresent(Message.class) || method.isAnnotationPresent(Theme.class)) { + wrapper = new MessageSupportReflectionWrapper(w, messageSource); + } + } + + return wrapper; + } +} diff --git a/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/mustache/MessageSupportReflectionWrapper.java b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/mustache/MessageSupportReflectionWrapper.java new file mode 100644 index 000000000..a9acd0c88 --- /dev/null +++ b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/mustache/MessageSupportReflectionWrapper.java @@ -0,0 +1,91 @@ +package nl.onedott.mustachejava.extension.springmvc.mustache; + +import com.github.mustachejava.MustacheException; +import com.github.mustachejava.reflect.ReflectionWrapper; +import com.github.mustachejava.util.GuardException; +import nl.onedott.mustachejava.extension.springmvc.SpringMustacheException; +import nl.onedott.mustachejava.extension.springmvc.annotation.Message; +import nl.onedott.mustachejava.extension.springmvc.annotation.Theme; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.MessageSource; +import org.springframework.context.NoSuchMessageException; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.support.RequestContextUtils; + +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.InvocationTargetException; +import java.util.Locale; + +/** + * Enables resolving messages using the return values of the method as message codes. + * Also supports embedded values e.g. {@code "${my.property}"}. + * @author Bart Tegenbosch + */ +public class MessageSupportReflectionWrapper extends ReflectionWrapper { + private final MessageSource messageSource; + + public MessageSupportReflectionWrapper(final ReflectionWrapper rw, final MessageSource messageSource) { + super(rw); + this.messageSource = messageSource; + } + + @Override + public Object call(final Object[] scopes) throws GuardException { + if (method != null) { + String code = returnValue(scopes).toString(); + Locale locale = resolveLocale(); + + if (messageSource instanceof ApplicationContext) { + ConfigurableListableBeanFactory beanFactory = + (ConfigurableListableBeanFactory) ((ApplicationContext) messageSource) + .getAutowireCapableBeanFactory(); + code = beanFactory.resolveEmbeddedValue(code); + } + + if (method.isAnnotationPresent(Message.class)) { + try { + return messageSource.getMessage(code, getArguments(), locale); + } catch (NoSuchMessageException e) { + throw new SpringMustacheException(e); + } + } + + if (method.isAnnotationPresent(Theme.class)) { + org.springframework.ui.context.Theme theme = + RequestContextUtils.getTheme(getRequest()); + + if (theme == null) { + throw new SpringMustacheException("No theme found"); + } + + + return theme.getMessageSource().getMessage(code, getArguments(), locale); + } + } + return super.call(scopes); + } + + private Object returnValue(Object[] scopes) { + guardCall(scopes); + Object scope = oh.coerce(unwrap(scopes)); + + try { + return method.invoke(scope, arguments); + } catch (IllegalAccessException e) { + throw new MustacheException(e); + } catch (InvocationTargetException e) { + throw new MustacheException(e); + } + } + + public Locale resolveLocale() { + return RequestContextUtils.getLocale(getRequest()); + } + + private HttpServletRequest getRequest() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + return attributes.getRequest(); + } +} diff --git a/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/view/MustacheView.java b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/view/MustacheView.java new file mode 100644 index 000000000..9310985f9 --- /dev/null +++ b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/view/MustacheView.java @@ -0,0 +1,27 @@ +package nl.onedott.mustachejava.extension.springmvc.view; + +import com.github.mustachejava.MustacheFactory; +import org.springframework.util.Assert; +import org.springframework.web.servlet.view.AbstractTemplateView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +/** + * @author Bart Tegenbosch + */ +public class MustacheView extends AbstractTemplateView { + + private MustacheFactory mustacheFactory; + + @Override + protected void renderMergedTemplateModel(final Map model, final HttpServletRequest request, final HttpServletResponse response) throws Exception { + mustacheFactory.compile(getUrl()).execute(response.getWriter(), model); + } + + public void setMustacheFactory(final MustacheFactory mustacheFactory) { + Assert.notNull(mustacheFactory, "mustacheFactory cannot be null"); + this.mustacheFactory = mustacheFactory; + } +} diff --git a/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/view/MustacheViewResolver.java b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/view/MustacheViewResolver.java new file mode 100644 index 000000000..5f0371f41 --- /dev/null +++ b/spring-mvc-extension/src/main/java/nl/onedott/mustachejava/extension/springmvc/view/MustacheViewResolver.java @@ -0,0 +1,29 @@ +package nl.onedott.mustachejava.extension.springmvc.view; + +import com.github.mustachejava.MustacheFactory; +import org.springframework.web.servlet.view.AbstractTemplateViewResolver; +import org.springframework.web.servlet.view.AbstractUrlBasedView; + +/** + * @author Bart Tegenbosch + */ +public class MustacheViewResolver extends AbstractTemplateViewResolver { + + private final MustacheFactory mustacheFactory; + + public MustacheViewResolver(final MustacheFactory mustacheFactory) { + this.mustacheFactory = mustacheFactory; + } + + @Override + protected Class requiredViewClass() { + return MustacheView.class; + } + + @Override + protected AbstractUrlBasedView buildView(final String viewName) throws Exception { + MustacheView view = (MustacheView) super.buildView(viewName); + view.setMustacheFactory(mustacheFactory); + return view; + } +} diff --git a/spring-mvc-extension/src/test/java/nl/onedott/mustachejava/extension/springmvc/SpringRequestContextSupport.java b/spring-mvc-extension/src/test/java/nl/onedott/mustachejava/extension/springmvc/SpringRequestContextSupport.java new file mode 100644 index 000000000..c15dab053 --- /dev/null +++ b/spring-mvc-extension/src/test/java/nl/onedott/mustachejava/extension/springmvc/SpringRequestContextSupport.java @@ -0,0 +1,67 @@ +package nl.onedott.mustachejava.extension.springmvc; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.ui.context.ThemeSource; +import org.springframework.ui.context.support.SimpleTheme; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.ThemeResolver; + +import javax.servlet.http.HttpServletRequest; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Bart Tegenbosch + */ +public abstract class SpringRequestContextSupport { + + private MockServletContext servletContext; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private StaticWebApplicationContext webApplicationContext; + + public void setup() throws Exception { + servletContext = new MockServletContext(); + webApplicationContext = new StaticWebApplicationContext(); + webApplicationContext.setServletContext(servletContext); + response = new MockHttpServletResponse(); + request = new MockHttpServletRequest(); + request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, webApplicationContext); + + RequestAttributes attributes = new ServletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(attributes); + + setupTheme("default"); + } + + public StaticWebApplicationContext webApplicationContext() { + return webApplicationContext; + } + + public MockHttpServletRequest request() { + return request; + } + + public MockHttpServletResponse response() { + return response; + } + + public void setupTheme(String themeName) { + ThemeResolver themeResolver = mock(ThemeResolver.class); + when(themeResolver.resolveThemeName(any(HttpServletRequest.class))).thenReturn(themeName); + + ThemeSource themeSource = mock(ThemeSource.class); + when(themeSource.getTheme(any(String.class))).thenReturn(new SimpleTheme(themeName, webApplicationContext)); + + request.setAttribute(DispatcherServlet.THEME_RESOLVER_ATTRIBUTE, themeResolver); + request.setAttribute(DispatcherServlet.THEME_SOURCE_ATTRIBUTE, themeSource); + } +} diff --git a/spring-mvc-extension/src/test/java/nl/onedott/mustachejava/extension/springmvc/mustache/MessageSupportReflectionObjectHandlerTest.java b/spring-mvc-extension/src/test/java/nl/onedott/mustachejava/extension/springmvc/mustache/MessageSupportReflectionObjectHandlerTest.java new file mode 100644 index 000000000..44ef00673 --- /dev/null +++ b/spring-mvc-extension/src/test/java/nl/onedott/mustachejava/extension/springmvc/mustache/MessageSupportReflectionObjectHandlerTest.java @@ -0,0 +1,92 @@ +package nl.onedott.mustachejava.extension.springmvc.mustache; + +import com.github.mustachejava.util.Wrapper; +import nl.onedott.mustachejava.extension.springmvc.SpringRequestContextSupport; +import nl.onedott.mustachejava.extension.springmvc.annotation.Message; +import nl.onedott.mustachejava.extension.springmvc.annotation.Theme; +import org.junit.Before; +import org.junit.Test; +import org.springframework.web.context.support.StaticWebApplicationContext; + +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class MessageSupportReflectionObjectHandlerTest extends SpringRequestContextSupport { + private StaticWebApplicationContext context; + + @Before + public void setup() throws Exception { + super.setup(); + context = webApplicationContext(); + context.addMessage("greeting", Locale.ENGLISH, "Hello"); + context.addMessage("name", Locale.ENGLISH, "mustache"); + context.addMessage("color", Locale.ENGLISH, "red"); + context.refresh(); + } + + @Test + public void testReturnMessageSupportReflectionWrapper() throws Exception { + MessageSupportReflectionObjectHandler h = new MessageSupportReflectionObjectHandler(context); + + // Return MessageSupportReflectionWrapper when annotated + Wrapper wrapper1 = h.find("greeting", new Object[] { new MustacheGreetingContext() }); + assertTrue(wrapper1 instanceof MessageSupportReflectionWrapper); + + // Return default wrappers when not annotated + Wrapper wrapper2 = h.find("age", new Object[] { new MustacheGreetingContext() }); + assertFalse(wrapper2 instanceof MessageSupportReflectionWrapper); + } + + @Test + public void testResolvedMessage() throws Exception { + Object[] scopes = new Object[] { new MustacheGreetingContext() }; + MessageSupportReflectionObjectHandler h = new MessageSupportReflectionObjectHandler(context); + + // Message + Wrapper wrapper1 = h.find("greeting", scopes); + assertEquals("Hello", wrapper1.call(scopes)); + + // Message + Wrapper wrapper2 = h.find("name", scopes); + assertEquals("mustache", wrapper2.call(scopes)); + + // Theme message + Wrapper wrapper3 = h.find("color", scopes); + assertEquals("red", wrapper3.call(scopes)); + } + + public static class MustacheGreetingContext { + + private final String name; + + public MustacheGreetingContext() { + this("name"); + } + + public MustacheGreetingContext(final String name) { + this.name = name; + } + + public int getAge() { + return 9; + } + + @Message + public String getGreeting() { + return "greeting"; + } + + @Message + public String getName() { + return name; + } + + @Theme + public String getColor() { + return "color"; + } + } +} diff --git a/spring-mvc-extension/src/test/java/nl/onedott/mustachejava/extension/springmvc/view/MustacheViewTest.java b/spring-mvc-extension/src/test/java/nl/onedott/mustachejava/extension/springmvc/view/MustacheViewTest.java new file mode 100644 index 000000000..b54db3e6a --- /dev/null +++ b/spring-mvc-extension/src/test/java/nl/onedott/mustachejava/extension/springmvc/view/MustacheViewTest.java @@ -0,0 +1,51 @@ +package nl.onedott.mustachejava.extension.springmvc.view; + +import com.github.mustachejava.DefaultMustacheFactory; +import nl.onedott.mustachejava.extension.springmvc.SpringRequestContextSupport; +import nl.onedott.mustachejava.extension.springmvc.mustache.MessageSupportReflectionObjectHandler; +import org.junit.Before; +import org.junit.Test; +import org.springframework.ui.ModelMap; + +import static org.junit.Assert.assertEquals; + +public class MustacheViewTest extends SpringRequestContextSupport { + private DefaultMustacheFactory mustacheFactory; + private MustacheView view; + + @Before + public void setup() throws Exception { + super.setup(); + + mustacheFactory = new DefaultMustacheFactory("templates/"); + mustacheFactory.setObjectHandler(new MessageSupportReflectionObjectHandler(webApplicationContext())); + + view = new MustacheView(); + view.setApplicationContext(webApplicationContext()); + view.setMustacheFactory(mustacheFactory); + } + + @Test + public void testRenderMergedTemplateModel() throws Exception { + ModelMap model = new ModelMap("dog", new Dog("Bello")); + view.setUrl("view.mustache"); + view.render(model, request(), response()); + assertEquals("My dog's name is Bello and says \"woof!\"", response().getContentAsString().trim()); + } + + public class Dog { + private final String name; + + private Dog(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String speak() { + return "woof!"; + } + } +} diff --git a/spring-mvc-extension/src/test/resources/templates/view.mustache b/spring-mvc-extension/src/test/resources/templates/view.mustache new file mode 100644 index 000000000..7c565125d --- /dev/null +++ b/spring-mvc-extension/src/test/resources/templates/view.mustache @@ -0,0 +1 @@ +My dog's name is {{dog.name}} and says "{{dog.speak}}"