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/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..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
@@ -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 {
-
+ model.put("partialAliases", getPartialAliases());
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..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
@@ -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.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(cur);
+ }
+ }
+ 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"));
+ }
}