From 9983db15d8cae83395ffaaf0d3ae82a287e036c4 Mon Sep 17 00:00:00 2001 From: Adam Gent Date: Tue, 27 Mar 2012 12:52:17 -0400 Subject: [PATCH 1/3] Added template partial support through url parameters on the view. --- .../view/mustache/MustacheTemplateLoader.java | 48 ++++- .../servlet/view/mustache/MustacheView.java | 24 ++- .../view/mustache/MustacheViewResolver.java | 204 ++++++++++++++++-- .../mustache/MustacheViewResolverTest.java | 13 ++ 4 files changed, 258 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/springframework/web/servlet/view/mustache/MustacheTemplateLoader.java b/src/main/java/org/springframework/web/servlet/view/mustache/MustacheTemplateLoader.java index 128619f..307102c 100644 --- a/src/main/java/org/springframework/web/servlet/view/mustache/MustacheTemplateLoader.java +++ b/src/main/java/org/springframework/web/servlet/view/mustache/MustacheTemplateLoader.java @@ -18,6 +18,7 @@ import java.io.FileNotFoundException; import java.io.InputStreamReader; import java.io.Reader; +import java.util.Map; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; @@ -27,14 +28,35 @@ /** * @author Sean Scanlon - * + * @author Adam Gent */ public class MustacheTemplateLoader implements TemplateLoader, ResourceLoaderAware { private ResourceLoader resourceLoader; + private Map partialAliases; + + private String suffix = ""; + private String prefix = ""; + + public MustacheTemplateLoader withPartialAliases(Map aliases) { + MustacheTemplateLoader loader = cloneTemplateLoader(); + return loader; + + } + + protected MustacheTemplateLoader cloneTemplateLoader() { + MustacheTemplateLoader loader = new MustacheTemplateLoader(); + loader.setPrefix(prefix); + loader.setSuffix(suffix); + loader.setResourceLoader(resourceLoader); + loader.setPartialAliases(partialAliases); + return loader; + } public Reader getTemplate(String filename) throws Exception { - Resource resource = resourceLoader.getResource(filename); + String fn = partialAliases != null && partialAliases.containsKey(filename) ? + partialAliases.get(filename) : filename; + Resource resource = resourceLoader.getResource(getPrefix() + fn + getSuffix()); if (resource.exists()) { return new InputStreamReader(resource.getInputStream()); } @@ -46,4 +68,26 @@ public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } + protected String getSuffix() { + return suffix; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + protected String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public Map getPartialAliases() { + return partialAliases; + } + public void setPartialAliases(Map aliases) { + this.partialAliases = aliases; + } } diff --git a/src/main/java/org/springframework/web/servlet/view/mustache/MustacheView.java b/src/main/java/org/springframework/web/servlet/view/mustache/MustacheView.java index 18dfa8f..4a62962 100644 --- a/src/main/java/org/springframework/web/servlet/view/mustache/MustacheView.java +++ b/src/main/java/org/springframework/web/servlet/view/mustache/MustacheView.java @@ -32,25 +32,35 @@ public class MustacheView extends AbstractTemplateView { private Template template; + private Map partialAliases; @Override protected void renderMergedTemplateModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { response.setContentType(getContentType()); - final Writer writer = response.getWriter(); - try { - template.execute(model, writer); - } finally { - writer.flush(); - } + renderTemplate(model, getTemplate(), response.getWriter()); } + + protected void renderTemplate(Map model, Template template, Writer writer) throws Exception { + try { + template.execute(model, writer); + } finally { + if (writer != null) + writer.flush(); + } + } public void setTemplate(Template template) { this.template = template; } - public Template getTemplate() { return template; } + public Map getPartialAliases() { + return partialAliases; + } + public void setPartialAliases(Map templateAliases) { + this.partialAliases = templateAliases; + } } diff --git a/src/main/java/org/springframework/web/servlet/view/mustache/MustacheViewResolver.java b/src/main/java/org/springframework/web/servlet/view/mustache/MustacheViewResolver.java index 69a842b..2188a84 100644 --- a/src/main/java/org/springframework/web/servlet/view/mustache/MustacheViewResolver.java +++ b/src/main/java/org/springframework/web/servlet/view/mustache/MustacheViewResolver.java @@ -15,61 +15,87 @@ */ package org.springframework.web.servlet.view.mustache; +import static java.util.Collections.reverse; +import static org.springframework.util.StringUtils.endsWithIgnoreCase; +import static org.springframework.util.StringUtils.hasText; +import static org.springframework.util.StringUtils.startsWithIgnoreCase; +import static org.springframework.util.StringUtils.trimLeadingCharacter; + +import java.io.FileNotFoundException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Required; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.StringUtils; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.view.AbstractTemplateViewResolver; import org.springframework.web.servlet.view.AbstractUrlBasedView; import com.samskivert.mustache.Mustache; -import com.samskivert.mustache.Template; import com.samskivert.mustache.Mustache.Compiler; +import com.samskivert.mustache.Mustache.TemplateLoader; + /** * @author Sean Scanlon * */ public class MustacheViewResolver extends AbstractTemplateViewResolver implements ViewResolver, - InitializingBean { - - private MustacheTemplateLoader templateLoader; + InitializingBean, ResourceLoaderAware { + + private String innerPartialAlias = "inner"; + private String layoutPartialAlias = "layout"; + private String layoutTemplateName = "layout"; + + private ResourceLoader resourceLoader; + private MustacheTemplateLoader templateLoader; private Compiler compiler; private boolean standardsMode = false; private boolean escapeHTML = true; - + public MustacheViewResolver() { setViewClass(MustacheView.class); + setPrefix("/WEB-INF/views/"); + setSuffix(".mustache"); } @Override protected Class requiredViewClass() { return MustacheView.class; } - + @Override - protected AbstractUrlBasedView buildView(String viewName) throws Exception { - - final MustacheView view = (MustacheView) super.buildView(viewName); - - Template template = compiler.compile(templateLoader.getTemplate(view.getUrl())); - view.setTemplate(template); - - return view; - } - + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + public void setTemplateLoader(MustacheTemplateLoader templateLoader) { + this.templateLoader = templateLoader; + } + @Override public void afterPropertiesSet() throws Exception { + if (templateLoader == null) { + templateLoader = new MustacheTemplateLoader(); + templateLoader.setPrefix(getPrefix()); + templateLoader.setSuffix(getSuffix()); + templateLoader.setResourceLoader(resourceLoader); + } compiler = Mustache.compiler() .escapeHTML(escapeHTML) .standardsMode(standardsMode) .withLoader(templateLoader); } - - @Required - public void setTemplateLoader(MustacheTemplateLoader templateLoader) { - this.templateLoader = templateLoader; - } + + /** * Whether or not standards mode is enabled. @@ -87,6 +113,140 @@ public void setStandardsMode(boolean standardsMode) { */ public void setEscapeHTML(boolean escapeHTML) { this.escapeHTML = escapeHTML; + } + + + @Override + protected AbstractUrlBasedView buildView(String viewName) throws Exception { + + final MustacheView view = (MustacheView) super.buildView(viewName); + + URI uri = new URI(viewName); + String newViewName = uri.getPath(); + Map aliases = parseQueryString(uri.getQuery()); + view.setPartialAliases(aliases); + + /* + * Normalize the path. + */ + if (endsWithIgnoreCase(getPrefix(), "/") && startsWithIgnoreCase(newViewName, "/")) { + newViewName = trimLeadingCharacter(newViewName, '/'); + } + TemplateLoader templateLoader = this.templateLoader.withPartialAliases(aliases); + Compiler compiler = this.compiler.withLoader(templateLoader); + /* + * Reset the view url + */ + view.setUrl(getPrefix() + newViewName + getSuffix()); + + aliases.put(getInnerPartialAlias(), newViewName); + /* + * Go find the parent layout template if one is not defined. + * We walk up the tree to find the parent. + */ + if ( ! hasText(aliases.get(getLayoutPartialAlias())) ) { + String parentTemplate = findParentLayoutTemplate(newViewName, templateLoader); + if (hasText(parentTemplate)) + aliases.put(getLayoutPartialAlias(), parentTemplate); + } + + String templateName = null; + + boolean found = hasText(templateName = aliases.get(getLayoutPartialAlias())) + || hasText(templateName = aliases.get(getInnerPartialAlias())); + + if (found) + view.setTemplate(compiler.compile(templateLoader.getTemplate(templateName))); + else + throw new IllegalStateException("Body is missing"); + + return view; } + /** + * Finds the parent layout template that will surround the inner layout template. + * By default it will look in the same directory as the inner template for a template called + * 'layout'. + * @param newViewName not null. + * @param templateLoader not null. + * @return null if not found. + * @throws Exception + */ + protected String findParentLayoutTemplate(String newViewName, TemplateLoader templateLoader) throws Exception { + String parentTemplate = null; + String[] paths = StringUtils.split(newViewName, "/"); + if (paths == null) return null; + List ps = new ArrayList(); + String cur = ""; + for (String p : paths) { + if (hasText(p)) { + cur = cur + "/" + p; + ps.add(p); + } + } + reverse(ps); + if (! ps.isEmpty()) { + ps.remove(0); + for (String p : ps) { + try { + String n = p + "/" + getLayoutTemplateName(); + templateLoader.getTemplate(n); + parentTemplate = n; + break; + } catch (FileNotFoundException e1) { + //Ignore as there is no layout template. + } + } + } + return parentTemplate; + } + + /** + * Strategy to parse the query string for template aliases. + * This is hardly robust for all URI query strings but works for now. + * @param query the query string not including the leading '?'. + * @return not null. + */ + protected Map parseQueryString(String query) { + Map params = new HashMap(); + if (! hasText(query) ) return params; + try { + for (String param : query.split("&")) { + String pair[] = param.split("="); + String key = URLDecoder.decode(pair[0], "UTF-8"); + String value = ""; + if (pair.length > 1) { + value = URLDecoder.decode(pair[1], "UTF-8"); + } + params.put(key, value); + } + return params; + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public String getInnerPartialAlias() { + return innerPartialAlias; + } + public void setInnerPartialAlias(String innerPartialAlias) { + this.innerPartialAlias = innerPartialAlias; + } + + public String getLayoutPartialAlias() { + return layoutPartialAlias; + } + public void setLayoutPartialAlias(String layoutPartialAlias) { + this.layoutPartialAlias = layoutPartialAlias; + } + + public String getLayoutTemplateName() { + return layoutTemplateName; + } + + public void setLayoutTemplateName(String layoutTemplateName) { + this.layoutTemplateName = layoutTemplateName; + } + + } diff --git a/src/test/java/org/springframework/web/servlet/view/mustache/MustacheViewResolverTest.java b/src/test/java/org/springframework/web/servlet/view/mustache/MustacheViewResolverTest.java index 9cdf747..21a8d4b 100644 --- a/src/test/java/org/springframework/web/servlet/view/mustache/MustacheViewResolverTest.java +++ b/src/test/java/org/springframework/web/servlet/view/mustache/MustacheViewResolverTest.java @@ -15,9 +15,13 @@ */ package org.springframework.web.servlet.view.mustache; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import java.io.StringReader; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; import org.junit.Before; import org.junit.Test; @@ -32,6 +36,7 @@ public class MustacheViewResolverTest { private MustacheViewResolver viewResolver; private MustacheTemplateLoader templateLoader; private String viewName; + private Map partialAlises = new HashMap(); @Before public void setUp() throws Exception { @@ -48,7 +53,15 @@ public void setUp() throws Exception { @Test public void testBuildView() throws Exception { + Mockito.doReturn(templateLoader).when(templateLoader).withPartialAliases(partialAlises); Mockito.doReturn(new StringReader("")).when(templateLoader).getTemplate(viewName); + assertNotNull(viewResolver.buildView(viewName)); } + + @Test + public void testParseQuery() throws Exception { + URI i = new URI("a/b?layout=bingo"); + assertEquals("bingo", viewResolver.parseQueryString(i.getQuery()).get("layout")); + } } From d5941655abbf76ca482d5a89fc61a4757e9d4b51 Mon Sep 17 00:00:00 2001 From: Adam Gent Date: Tue, 27 Mar 2012 13:13:59 -0400 Subject: [PATCH 2/3] Documentation on partial aliases. --- README.md | 41 ++++++++++++++++--- .../servlet/view/mustache/MustacheView.java | 2 +- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4c31f17..6961179 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,40 @@ spring config - - - - - + + + + + + +layout and partials support +--------------------------- + +You can now use partials in your templates. The partials are either resolved through aliases +that you supply as URI parameters in your view name or by explicitely putting view name as a partial. +There are two reserved partial aliases called "inner" and "layout". Inner is the actual template +view name (below its 'recruite/submit'). "layout" is the wrapping template. Partial aliases are also +available in the model as a hashmap with the key 'partialAliases'. + +Example Java: + + @RequestMapping(value="/m", method = RequestMethod.POST) + public String mobilePost(Map model) { + return "recruite/submit?layout=recruite/layout&header=recruite/header"; + } + +/WEB-INF/views/recruite/submit.mustache: + + Hello World! + {{> recruite/explicit }} + +/WEB-INF/views/recruite/layout.mustache: + + {{> header}} + + {{> inner}} + + + + diff --git a/src/main/java/org/springframework/web/servlet/view/mustache/MustacheView.java b/src/main/java/org/springframework/web/servlet/view/mustache/MustacheView.java index 4a62962..630b608 100644 --- a/src/main/java/org/springframework/web/servlet/view/mustache/MustacheView.java +++ b/src/main/java/org/springframework/web/servlet/view/mustache/MustacheView.java @@ -37,7 +37,7 @@ public class MustacheView extends AbstractTemplateView { @Override protected void renderMergedTemplateModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { - + model.put("partialAliases", getPartialAliases()); response.setContentType(getContentType()); renderTemplate(model, getTemplate(), response.getWriter()); } From 5783742f46e4e8b4627df7ac1cff3b2655dbd362 Mon Sep 17 00:00:00 2001 From: Adam Gent Date: Thu, 19 Apr 2012 08:27:21 -0400 Subject: [PATCH 3/3] Fixed bug. --- .../web/servlet/view/mustache/MustacheViewResolver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/web/servlet/view/mustache/MustacheViewResolver.java b/src/main/java/org/springframework/web/servlet/view/mustache/MustacheViewResolver.java index 2188a84..7b995be 100644 --- a/src/main/java/org/springframework/web/servlet/view/mustache/MustacheViewResolver.java +++ b/src/main/java/org/springframework/web/servlet/view/mustache/MustacheViewResolver.java @@ -174,14 +174,14 @@ protected AbstractUrlBasedView buildView(String viewName) throws Exception { */ protected String findParentLayoutTemplate(String newViewName, TemplateLoader templateLoader) throws Exception { String parentTemplate = null; - String[] paths = StringUtils.split(newViewName, "/"); + String[] paths = StringUtils.tokenizeToStringArray(newViewName, "/"); if (paths == null) return null; List ps = new ArrayList(); String cur = ""; for (String p : paths) { if (hasText(p)) { cur = cur + "/" + p; - ps.add(p); + ps.add(cur); } } reverse(ps);