Review TemplateRenderer abstraction

Closes gh-818
This commit is contained in:
Stephane Nicoll
2019-02-08 10:53:49 +01:00
parent 6e9f542560
commit b6657211f6
17 changed files with 290 additions and 141 deletions

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2012-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
*
* http://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 io.spring.initializr.generator.io.template;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.function.Function;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Mustache.Compiler;
import com.samskivert.mustache.Mustache.TemplateLoader;
import com.samskivert.mustache.Template;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueRetrievalException;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
/**
* A {@link TemplateRenderer} using Mustache.
*
* @author Stephane Nicoll
*/
public class MustacheTemplateRenderer implements TemplateRenderer {
private final Compiler mustache;
private final Function<String, String> keyGenerator;
private final Cache templateCache;
public MustacheTemplateRenderer(String resourcePrefix, Cache templateCache) {
String prefix = (resourcePrefix.endsWith("/") ? resourcePrefix
: resourcePrefix + "/");
this.mustache = Mustache.compiler().withLoader(mustacheTemplateLoader(prefix));
this.keyGenerator = (name) -> String.format("%s%s", prefix, name);
this.templateCache = templateCache;
}
public MustacheTemplateRenderer(String resourcePrefix) {
this(resourcePrefix, null);
}
private static TemplateLoader mustacheTemplateLoader(String prefix) {
ResourceLoader resourceLoader = new DefaultResourceLoader();
return (name) -> {
String location = prefix + name + ".mustache";
return new InputStreamReader(
resourceLoader.getResource(location).getInputStream(),
StandardCharsets.UTF_8);
};
}
@Override
public String render(String templateName, Map<String, ?> model) throws IOException {
Template template = getTemplate(templateName);
return template.execute(model);
}
private Template getTemplate(String name) {
try {
if (this.templateCache != null) {
try {
return this.templateCache.get(this.keyGenerator.apply(name),
() -> loadTemplate(name));
}
catch (ValueRetrievalException ex) {
throw ex.getCause();
}
}
return loadTemplate(name);
}
catch (Throwable ex) {
throw new IllegalStateException("Cannot load template " + name, ex);
}
}
private Template loadTemplate(String name) throws Exception {
Reader template = this.mustache.loader.getTemplate(name);
return this.mustache.compile(template);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-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
*
* http://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 io.spring.initializr.generator.io.template;
import java.io.IOException;
import java.util.Map;
/**
* Template rendering abstraction.
*
* @author Stephane Nicoll
*/
@FunctionalInterface
public interface TemplateRenderer {
/**
* Render the template with the specified name and the specified model.
* @param templateName the name of the template
* @param model the model to use
* @return the rendering result
* @throws IOException if rendering the template failed
*/
String render(String templateName, Map<String, ?> model) throws IOException;
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-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
*
* http://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.
*/
/**
* Template rendering abstraction.
*/
package io.spring.initializr.generator.io.template;

View File

@@ -1,108 +0,0 @@
/*
* Copyright 2012-2018 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
*
* http://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 io.spring.initializr.util;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Mustache.Compiler;
import com.samskivert.mustache.Mustache.TemplateLoader;
import com.samskivert.mustache.Template;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ConcurrentReferenceHashMap;
/**
* A template renderer backed by Mustache.
*
* @author Dave Syer
*/
public class TemplateRenderer {
private static final Logger log = LoggerFactory.getLogger(TemplateRenderer.class);
private boolean cache = true;
private final Compiler mustache;
private final ConcurrentMap<String, Template> templateCaches = new ConcurrentReferenceHashMap<>();
public TemplateRenderer() {
this(mustacheCompiler());
}
public TemplateRenderer(Compiler mustache) {
this.mustache = mustache;
}
public boolean isCache() {
return this.cache;
}
public void setCache(boolean cache) {
this.cache = cache;
}
public String process(String name, Map<String, ?> model) {
try {
Template template = getTemplate(name);
return template.execute(model);
}
catch (Exception ex) {
log.error("Cannot render: " + name, ex);
throw new IllegalStateException("Cannot render template", ex);
}
}
public Template getTemplate(String name) {
if (this.cache) {
return this.templateCaches.computeIfAbsent(name, this::loadTemplate);
}
return loadTemplate(name);
}
protected Template loadTemplate(String name) {
try {
Reader template;
template = this.mustache.loader.getTemplate(name);
return this.mustache.compile(template);
}
catch (Exception ex) {
throw new IllegalStateException("Cannot load template " + name, ex);
}
}
private static Compiler mustacheCompiler() {
return Mustache.compiler().withLoader(mustacheTemplateLoader());
}
private static TemplateLoader mustacheTemplateLoader() {
ResourceLoader resourceLoader = new DefaultResourceLoader();
String prefix = "classpath:/templates/";
Charset charset = Charset.forName("UTF-8");
return (name) -> new InputStreamReader(
resourceLoader.getResource(prefix + name).getInputStream(), charset);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-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
*
* http://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 io.spring.initializr.generator.io.template;
import java.io.IOException;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link MustacheTemplateRenderer}.
*
* @author Stephane Nicoll
*/
class MustacheTemplateRendererTests {
private final Cache templatesCache = new ConcurrentMapCache("test");
@Test
void renderTemplate() throws IOException {
MustacheTemplateRenderer render = new MustacheTemplateRenderer(
"classpath:/templates/mustache", this.templatesCache);
assertThat(this.templatesCache.get("classpath:/templates/mustache/test"))
.isNull();
assertThat(render.render("test", Collections.singletonMap("key", "value")))
.isEqualTo("value");
assertThat(this.templatesCache.get("classpath:/templates/mustache/test"))
.isNotNull();
}
@Test
void renderTemplateWithoutCache() throws IOException {
MustacheTemplateRenderer render = new MustacheTemplateRenderer(
"classpath:/templates/mustache");
assertThat(render.render("test", Collections.singletonMap("key", "value")))
.isEqualTo("value");
}
@Test
void renderUnknownTemplate() {
MustacheTemplateRenderer render = new MustacheTemplateRenderer(
"classpath:/templates/mustache", this.templatesCache);
assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(() -> render.render("does-not-exist", Collections.emptyMap()))
.withMessageContaining("Cannot load template")
.withMessageContaining("does-not-exist");
}
}

View File

@@ -0,0 +1 @@
{{key}}