Add Mustache support for Spring WebFlux apps
This commit moves the existing Spring MVC Mustache support to its own `servlet` package and adds a new one under `reactive` for the WebFlux web applications. New `MustacheView` and `MustacheViewResolver` types resolve and render Mustache views for WebFlux applications. Since this templating engine is now supported by two flavors of Spring web apps, the `spring-boot-starter-mustache` does not depend anymore on the `spring-boot-starter-web` one: it's up to the developer to add the relevant starter `web` or `webflux` to their application. Fixes gh-8648
This commit is contained in:
parent
c2e5fd031a
commit
7e77e648bf
|
@ -28,7 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
|
||||||
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
|
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration;
|
import org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
|
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.mustache.web.MustacheViewResolver;
|
import org.springframework.boot.autoconfigure.mustache.servlet.MustacheViewResolver;
|
||||||
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
|
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
|
|
@ -30,7 +30,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
||||||
import org.springframework.boot.autoconfigure.mustache.web.MustacheViewResolver;
|
import org.springframework.boot.autoconfigure.mustache.servlet.MustacheViewResolver;
|
||||||
import org.springframework.boot.autoconfigure.template.TemplateLocation;
|
import org.springframework.boot.autoconfigure.template.TemplateLocation;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
@ -43,6 +43,7 @@ import org.springframework.core.env.Environment;
|
||||||
* {@link EnableAutoConfiguration Auto-configuration} for Mustache.
|
* {@link EnableAutoConfiguration Auto-configuration} for Mustache.
|
||||||
*
|
*
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
|
* @author Brian Clozel
|
||||||
* @since 1.2.2
|
* @since 1.2.2
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ -123,4 +124,34 @@ public class MustacheAutoConfiguration {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnWebApplication(type = Type.REACTIVE)
|
||||||
|
protected static class MustacheReactiveWebConfiguration {
|
||||||
|
|
||||||
|
private final MustacheProperties mustache;
|
||||||
|
|
||||||
|
protected MustacheReactiveWebConfiguration(MustacheProperties mustache) {
|
||||||
|
this.mustache = mustache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(org.springframework.boot.autoconfigure
|
||||||
|
.mustache.reactive.MustacheViewResolver.class)
|
||||||
|
public org.springframework.boot.autoconfigure
|
||||||
|
.mustache.reactive.MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler) {
|
||||||
|
org.springframework.boot.autoconfigure
|
||||||
|
.mustache.reactive.MustacheViewResolver resolver
|
||||||
|
= new org.springframework.boot.autoconfigure
|
||||||
|
.mustache.reactive.MustacheViewResolver(mustacheCompiler);
|
||||||
|
resolver.setPrefix(this.mustache.getPrefix());
|
||||||
|
resolver.setSuffix(this.mustache.getSuffix());
|
||||||
|
resolver.setViewNames(this.mustache.getViewNames());
|
||||||
|
resolver.setRequestContextAttribute(this.mustache.getRequestContextAttribute());
|
||||||
|
resolver.setCharset(this.mustache.getCharsetName());
|
||||||
|
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 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 org.springframework.boot.autoconfigure.mustache.reactive;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.samskivert.mustache.Mustache;
|
||||||
|
import com.samskivert.mustache.Template;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.reactive.result.view.AbstractUrlBasedView;
|
||||||
|
import org.springframework.web.reactive.result.view.View;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring WebFlux {@link View} using the Mustache template engine.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class MustacheView extends AbstractUrlBasedView {
|
||||||
|
|
||||||
|
private Mustache.Compiler compiler;
|
||||||
|
|
||||||
|
private String charset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the JMustache compiler to be used by this view.
|
||||||
|
* <p>Typically this property is not set directly. Instead a single
|
||||||
|
* {@link Mustache.Compiler} is expected in the Spring application context
|
||||||
|
* which is used to compile Mustache templates.
|
||||||
|
* @param compiler the Mustache compiler
|
||||||
|
*/
|
||||||
|
public void setCompiler(Mustache.Compiler compiler) {
|
||||||
|
this.compiler = compiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the charset used for reading Mustache template files.
|
||||||
|
* @param charset the charset to use for reading template files
|
||||||
|
*/
|
||||||
|
public void setCharset(String charset) {
|
||||||
|
this.charset = charset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkResourceExists(Locale locale) throws Exception {
|
||||||
|
return resolveResource() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Resource resolveResource() {
|
||||||
|
Resource resource = getApplicationContext().getResource(getUrl());
|
||||||
|
if (resource == null || !resource.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Mono<Void> renderInternal(Map<String, Object> model,
|
||||||
|
MediaType contentType, ServerWebExchange exchange) {
|
||||||
|
Resource resource = resolveResource();
|
||||||
|
if (resource == null) {
|
||||||
|
return Mono.error(new IllegalStateException("Could not find Mustache template with URL ["
|
||||||
|
+ getUrl() + "]"));
|
||||||
|
}
|
||||||
|
DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer();
|
||||||
|
try (Reader reader = getReader(resource)) {
|
||||||
|
Template template = this.compiler.compile(reader);
|
||||||
|
Charset charset = getCharset(contentType).orElse(getDefaultCharset());
|
||||||
|
try (Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(), charset)) {
|
||||||
|
template.execute(model, writer);
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Throwable exc) {
|
||||||
|
return Mono.error(exc);
|
||||||
|
}
|
||||||
|
return exchange.getResponse().writeWith(Flux.just(dataBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Reader getReader(Resource resource) throws IOException {
|
||||||
|
if (this.charset != null) {
|
||||||
|
return new InputStreamReader(resource.getInputStream(), this.charset);
|
||||||
|
}
|
||||||
|
return new InputStreamReader(resource.getInputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Charset> getCharset(MediaType mediaType) {
|
||||||
|
return (mediaType != null ? Optional.ofNullable(mediaType.getCharset()) : Optional.empty());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 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 org.springframework.boot.autoconfigure.mustache.reactive;
|
||||||
|
|
||||||
|
import com.samskivert.mustache.Mustache;
|
||||||
|
|
||||||
|
import org.springframework.web.reactive.result.view.AbstractUrlBasedView;
|
||||||
|
import org.springframework.web.reactive.result.view.UrlBasedViewResolver;
|
||||||
|
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring WebFlux {@link ViewResolver} for Mustache.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class MustacheViewResolver extends UrlBasedViewResolver {
|
||||||
|
|
||||||
|
private final Mustache.Compiler compiler;
|
||||||
|
|
||||||
|
private String charset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@code MustacheViewResolver} backed by a default
|
||||||
|
* instance of a {@link Mustache.Compiler}.
|
||||||
|
*/
|
||||||
|
public MustacheViewResolver() {
|
||||||
|
this.compiler = Mustache.compiler();
|
||||||
|
setViewClass(requiredViewClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@code MustacheViewResolver} backed by a custom
|
||||||
|
* instance of a {@link Mustache.Compiler}.
|
||||||
|
* @param compiler the Mustache compiler used to compile templates
|
||||||
|
*/
|
||||||
|
public MustacheViewResolver(Mustache.Compiler compiler) {
|
||||||
|
this.compiler = compiler;
|
||||||
|
setViewClass(requiredViewClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the charset.
|
||||||
|
* @param charset the charset
|
||||||
|
*/
|
||||||
|
public void setCharset(String charset) {
|
||||||
|
this.charset = charset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> requiredViewClass() {
|
||||||
|
return MustacheView.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AbstractUrlBasedView createUrlBasedView(String viewName) {
|
||||||
|
MustacheView view = (MustacheView) super.createUrlBasedView(viewName);
|
||||||
|
view.setCompiler(this.compiler);
|
||||||
|
view.setCharset(this.charset);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for Mustache with Spring WebFlux.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.autoconfigure.mustache.reactive;
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2015 the original author or authors.
|
* Copyright 2012-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.boot.autoconfigure.mustache.web;
|
package org.springframework.boot.autoconfigure.mustache.servlet;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2016 the original author or authors.
|
* Copyright 2012-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.boot.autoconfigure.mustache.web;
|
package org.springframework.boot.autoconfigure.mustache.servlet;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2015 the original author or authors.
|
* Copyright 2012-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,4 +17,4 @@
|
||||||
/**
|
/**
|
||||||
* Auto-configuration for Mustache with Spring MVC.
|
* Auto-configuration for Mustache with Spring MVC.
|
||||||
*/
|
*/
|
||||||
package org.springframework.boot.autoconfigure.mustache.web;
|
package org.springframework.boot.autoconfigure.mustache.servlet;
|
|
@ -34,7 +34,7 @@ import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfigura
|
||||||
import org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration;
|
import org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
|
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.mustache.web.MustacheViewResolver;
|
import org.springframework.boot.autoconfigure.mustache.servlet.MustacheViewResolver;
|
||||||
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
|
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||||
import org.springframework.boot.test.util.EnvironmentTestUtils;
|
import org.springframework.boot.test.util.EnvironmentTestUtils;
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2012-2017 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 org.springframework.boot.autoconfigure.mustache;
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
|
||||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
|
||||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
|
||||||
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.context.annotation.Import;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.test.annotation.DirtiesContext;
|
|
||||||
import org.springframework.test.context.junit4.SpringRunner;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integration tests for {@link MustacheAutoConfiguration}.
|
|
||||||
*
|
|
||||||
* @author Dave Syer
|
|
||||||
*/
|
|
||||||
@RunWith(SpringRunner.class)
|
|
||||||
@DirtiesContext
|
|
||||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
|
|
||||||
"spring.mustache.prefix:classpath:/mustache-templates/" })
|
|
||||||
public class MustacheAutoConfigurationIntegrationTests {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ServletWebServerApplicationContext context;
|
|
||||||
|
|
||||||
private int port;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void init() {
|
|
||||||
this.port = this.context.getWebServer().getPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHomePage() throws Exception {
|
|
||||||
String body = new TestRestTemplate().getForObject("http://localhost:" + this.port,
|
|
||||||
String.class);
|
|
||||||
assertThat(body.contains("Hello World")).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testPartialPage() throws Exception {
|
|
||||||
String body = new TestRestTemplate()
|
|
||||||
.getForObject("http://localhost:" + this.port + "/partial", String.class);
|
|
||||||
assertThat(body.contains("Hello World")).isTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@MinimalWebConfiguration
|
|
||||||
@Controller
|
|
||||||
public static class Application {
|
|
||||||
|
|
||||||
@RequestMapping("/")
|
|
||||||
public String home(Map<String, Object> model) {
|
|
||||||
model.put("time", new Date());
|
|
||||||
model.put("message", "Hello World");
|
|
||||||
model.put("title", "Hello App");
|
|
||||||
return "home";
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequestMapping("/partial")
|
|
||||||
public String layout(Map<String, Object> model) {
|
|
||||||
model.put("time", new Date());
|
|
||||||
model.put("message", "Hello World");
|
|
||||||
model.put("title", "Hello App");
|
|
||||||
return "partial";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(Application.class, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Target(ElementType.TYPE)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
@Import({ MustacheAutoConfiguration.class,
|
|
||||||
ServletWebServerFactoryAutoConfiguration.class,
|
|
||||||
DispatcherServletAutoConfiguration.class,
|
|
||||||
PropertyPlaceholderAutoConfiguration.class })
|
|
||||||
protected @interface MinimalWebConfiguration {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 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 org.springframework.boot.autoconfigure.mustache;
|
||||||
|
|
||||||
|
import com.samskivert.mustache.Mustache;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.mustache.servlet.MustacheViewResolver;
|
||||||
|
import org.springframework.boot.test.util.EnvironmentTestUtils;
|
||||||
|
import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link MustacheAutoConfiguration}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
public class MustacheAutoConfigurationTests {
|
||||||
|
|
||||||
|
private AnnotationConfigWebApplicationContext webContext;
|
||||||
|
|
||||||
|
private GenericReactiveWebApplicationContext reactiveWebContext;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerBeansForServletApp() {
|
||||||
|
loadWithServlet(null);
|
||||||
|
assertThat(this.webContext.getBeansOfType(Mustache.Compiler.class)).hasSize(1);
|
||||||
|
assertThat(this.webContext.getBeansOfType(MustacheResourceTemplateLoader.class)).hasSize(1);
|
||||||
|
assertThat(this.webContext.getBeansOfType(MustacheViewResolver.class)).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerCompilerForServletApp() {
|
||||||
|
loadWithServlet(CustomCompilerConfiguration.class);
|
||||||
|
assertThat(this.webContext.getBeansOfType(MustacheResourceTemplateLoader.class)).hasSize(1);
|
||||||
|
assertThat(this.webContext.getBeansOfType(MustacheViewResolver.class)).hasSize(1);
|
||||||
|
assertThat(this.webContext.getBeansOfType(Mustache.Compiler.class)).hasSize(1);
|
||||||
|
assertThat(this.webContext.getBean(Mustache.Compiler.class).standardsMode).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerBeansForReactiveApp() {
|
||||||
|
loadWithReactive(null);
|
||||||
|
assertThat(this.reactiveWebContext.getBeansOfType(Mustache.Compiler.class)).hasSize(1);
|
||||||
|
assertThat(this.reactiveWebContext.getBeansOfType(MustacheResourceTemplateLoader.class)).hasSize(1);
|
||||||
|
assertThat(this.reactiveWebContext.getBeansOfType(MustacheViewResolver.class)).isEmpty();
|
||||||
|
assertThat(this.reactiveWebContext
|
||||||
|
.getBeansOfType(org.springframework.boot.autoconfigure.mustache.reactive.MustacheViewResolver.class)
|
||||||
|
).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerCompilerForReactiveApp() {
|
||||||
|
loadWithReactive(CustomCompilerConfiguration.class);
|
||||||
|
assertThat(this.reactiveWebContext.getBeansOfType(Mustache.Compiler.class)).hasSize(1);
|
||||||
|
assertThat(this.reactiveWebContext.getBeansOfType(MustacheResourceTemplateLoader.class)).hasSize(1);
|
||||||
|
assertThat(this.reactiveWebContext.getBeansOfType(MustacheViewResolver.class)).isEmpty();
|
||||||
|
assertThat(this.reactiveWebContext
|
||||||
|
.getBeansOfType(org.springframework.boot.autoconfigure.mustache.reactive.MustacheViewResolver.class)
|
||||||
|
).hasSize(1);
|
||||||
|
assertThat(this.reactiveWebContext.getBean(Mustache.Compiler.class).standardsMode).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadWithServlet(Class<?> config) {
|
||||||
|
this.webContext = new AnnotationConfigWebApplicationContext();
|
||||||
|
EnvironmentTestUtils.addEnvironment(this.webContext,
|
||||||
|
"spring.mustache.prefix=classpath:/mustache-templates/");
|
||||||
|
if (config != null) {
|
||||||
|
this.webContext.register(config);
|
||||||
|
}
|
||||||
|
this.webContext.register(BaseConfiguration.class);
|
||||||
|
this.webContext.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadWithReactive(Class<?> config) {
|
||||||
|
this.reactiveWebContext = new GenericReactiveWebApplicationContext();
|
||||||
|
EnvironmentTestUtils.addEnvironment(this.reactiveWebContext,
|
||||||
|
"spring.mustache.prefix=classpath:/mustache-templates/");
|
||||||
|
if (config != null) {
|
||||||
|
this.reactiveWebContext.register(config);
|
||||||
|
}
|
||||||
|
this.reactiveWebContext.register(BaseConfiguration.class);
|
||||||
|
this.reactiveWebContext.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Import({MustacheAutoConfiguration.class})
|
||||||
|
protected static class BaseConfiguration {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
protected static class CustomCompilerConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Mustache.Compiler compiler(Mustache.TemplateLoader mustacheTemplateLoader) {
|
||||||
|
return Mustache.compiler().standardsMode(true).withLoader(mustacheTemplateLoader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 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 org.springframework.boot.autoconfigure.mustache.reactive;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link org.springframework.boot.autoconfigure.mustache.reactive.MustacheViewResolver}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
public class MustacheViewResolverTests {
|
||||||
|
|
||||||
|
private MustacheViewResolver resolver = new MustacheViewResolver();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void init() {
|
||||||
|
GenericApplicationContext applicationContext = new GenericApplicationContext();
|
||||||
|
applicationContext.refresh();
|
||||||
|
this.resolver.setApplicationContext(applicationContext);
|
||||||
|
this.resolver.setPrefix("classpath:/mustache-templates/");
|
||||||
|
this.resolver.setSuffix(".html");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolveNonExistent() throws Exception {
|
||||||
|
assertThat(this.resolver.resolveViewName("bar", null).block()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolveExisting() throws Exception {
|
||||||
|
assertThat(this.resolver.resolveViewName("foo", null).block()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 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 org.springframework.boot.autoconfigure.mustache.reactive;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import com.samskivert.mustache.Mustache;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
|
import org.springframework.mock.http.server.reactive.MockServerWebExchange;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link MustacheView}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
public class MustacheViewTests {
|
||||||
|
|
||||||
|
private GenericApplicationContext context = new GenericApplicationContext();
|
||||||
|
|
||||||
|
private MockServerWebExchange exchange;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void init() {
|
||||||
|
this.context.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void viewResolvesHandlebars() throws Exception {
|
||||||
|
this.exchange = MockServerHttpRequest.get("/test").toExchange();
|
||||||
|
MustacheView view = new MustacheView();
|
||||||
|
view.setCompiler(Mustache.compiler());
|
||||||
|
view.setUrl("classpath:/mustache-templates/foo.html");
|
||||||
|
view.setCharset(StandardCharsets.UTF_8.displayName());
|
||||||
|
view.setApplicationContext(this.context);
|
||||||
|
view.render(Collections.singletonMap("World", "Spring"), MediaType.TEXT_HTML, this.exchange).block();
|
||||||
|
assertThat(this.exchange.getResponse().getBodyAsString().block()).isEqualTo("Hello Spring");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2017 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 org.springframework.boot.autoconfigure.mustache.reactive;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.samskivert.mustache.Mustache;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.WebApplicationType;
|
||||||
|
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.mustache.MustacheResourceTemplateLoader;
|
||||||
|
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration Tests for {@link MustacheAutoConfiguration}, {@link MustacheViewResolver}
|
||||||
|
* and {@link MustacheView}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
|
||||||
|
properties = "spring.main.web-application-type=reactive")
|
||||||
|
public class MustacheWebIntegrationTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WebTestClient client;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHomePage() throws Exception {
|
||||||
|
String result = (String) this.client.get().uri("/").exchange()
|
||||||
|
.expectStatus().isOk()
|
||||||
|
.expectBody(String.class).returnResult().getResponseBody();
|
||||||
|
assertThat(result).contains("Hello App").contains("Hello World");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPartialPage() throws Exception {
|
||||||
|
String result = (String) this.client.get().uri("/partial").exchange()
|
||||||
|
.expectStatus().isOk()
|
||||||
|
.expectBody(String.class).returnResult().getResponseBody();
|
||||||
|
assertThat(result).contains("Hello App").contains("Hello World");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Import({ReactiveWebServerAutoConfiguration.class,
|
||||||
|
WebFluxAutoConfiguration.class,
|
||||||
|
HttpHandlerAutoConfiguration.class,
|
||||||
|
PropertyPlaceholderAutoConfiguration.class})
|
||||||
|
@Controller
|
||||||
|
public static class Application {
|
||||||
|
|
||||||
|
@RequestMapping("/")
|
||||||
|
public String home(Model model) {
|
||||||
|
model.addAttribute("time", new Date());
|
||||||
|
model.addAttribute("message", "Hello World");
|
||||||
|
model.addAttribute("title", "Hello App");
|
||||||
|
return "home";
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping("/partial")
|
||||||
|
public String layout(Model model) {
|
||||||
|
model.addAttribute("time", new Date());
|
||||||
|
model.addAttribute("message", "Hello World");
|
||||||
|
model.addAttribute("title", "Hello App");
|
||||||
|
return "partial";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MustacheViewResolver viewResolver() {
|
||||||
|
Mustache.Compiler compiler = Mustache.compiler().withLoader(
|
||||||
|
new MustacheResourceTemplateLoader("classpath:/mustache-templates/", ".html"));
|
||||||
|
MustacheViewResolver resolver = new MustacheViewResolver(compiler);
|
||||||
|
resolver.setPrefix("classpath:/mustache-templates/");
|
||||||
|
resolver.setSuffix(".html");
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication application = new SpringApplication(Application.class);
|
||||||
|
application.setWebApplicationType(WebApplicationType.REACTIVE);
|
||||||
|
application.run(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.boot.autoconfigure.mustache.web;
|
package org.springframework.boot.autoconfigure.mustache.servlet;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.boot.autoconfigure.mustache.web;
|
package org.springframework.boot.autoconfigure.mustache.servlet;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.boot.autoconfigure.mustache.web;
|
package org.springframework.boot.autoconfigure.mustache.servlet;
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
|
@ -21,6 +21,10 @@
|
||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Compile -->
|
<!-- Compile -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-mustache</artifactId>
|
<artifactId>spring-boot-starter-mustache</artifactId>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>spring-boot-starter-mustache</artifactId>
|
<artifactId>spring-boot-starter-mustache</artifactId>
|
||||||
<name>Spring Boot Mustache Starter</name>
|
<name>Spring Boot Mustache Starter</name>
|
||||||
<description>Starter for building MVC web applications using Mustache views</description>
|
<description>Starter for building web applications using Mustache views</description>
|
||||||
<url>http://projects.spring.io/spring-boot/</url>
|
<url>http://projects.spring.io/spring-boot/</url>
|
||||||
<organization>
|
<organization>
|
||||||
<name>Pivotal Software, Inc.</name>
|
<name>Pivotal Software, Inc.</name>
|
||||||
|
@ -22,10 +22,6 @@
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter</artifactId>
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.samskivert</groupId>
|
<groupId>com.samskivert</groupId>
|
||||||
<artifactId>jmustache</artifactId>
|
<artifactId>jmustache</artifactId>
|
||||||
|
|
Loading…
Reference in New Issue