renderAttributes,
+ final MediaType contentType,
+ final ServerWebExchange exchange) {
+ final Context context = Context.newBuilder(renderAttributes)
+ .resolver(valueResolvers)
+ .build();
+
+ final DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer();
+ final Charset charset = Optional.ofNullable(contentType).map(MimeType::getCharset)
+ .orElse(getDefaultCharset());
+
+ try (final Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(), charset)) {
+ template.apply(context, writer);
+ writer.flush();
+ } catch (IOException e) {
+ DataBufferUtils.release(dataBuffer);
+ return Mono.error(e);
+ } finally {
+ context.destroy();
+ }
+
+ return exchange.getResponse().writeWith(Flux.just(dataBuffer));
+ }
+}
diff --git a/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewResolver.java b/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewResolver.java
new file mode 100644
index 000000000..46ea4d4f6
--- /dev/null
+++ b/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewResolver.java
@@ -0,0 +1,609 @@
+/**
+ * Copyright (c) 2012-2015 Edgar Espina
+ *
+ * This file is part of Handlebars.java.
+ *
+ * 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 com.github.jknack.handlebars.springmvc.webflux;
+
+import static com.github.jknack.handlebars.springmvc.SpringUtils.createI18nSource;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ResourceBundle;
+import java.util.Set;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.MessageSource;
+import org.springframework.web.reactive.result.view.AbstractUrlBasedView;
+import org.springframework.web.reactive.result.view.UrlBasedViewResolver;
+
+import com.github.jknack.handlebars.Decorator;
+import com.github.jknack.handlebars.Formatter;
+import com.github.jknack.handlebars.Handlebars;
+import com.github.jknack.handlebars.Helper;
+import com.github.jknack.handlebars.HelperRegistry;
+import com.github.jknack.handlebars.ValueResolver;
+import com.github.jknack.handlebars.cache.HighConcurrencyTemplateCache;
+import com.github.jknack.handlebars.cache.NullTemplateCache;
+import com.github.jknack.handlebars.cache.TemplateCache;
+import com.github.jknack.handlebars.helper.DefaultHelperRegistry;
+import com.github.jknack.handlebars.helper.I18nHelper;
+import com.github.jknack.handlebars.helper.I18nSource;
+import com.github.jknack.handlebars.io.TemplateLoader;
+import com.github.jknack.handlebars.io.URLTemplateLoader;
+import com.github.jknack.handlebars.springmvc.MessageSourceHelper;
+import com.github.jknack.handlebars.springmvc.SpringTemplateLoader;
+
+/**
+ * @author OhChang Kwon(ohchang.kwon@navercorp.com)
+ */
+public class ReactiveHandlebarsViewResolver extends UrlBasedViewResolver
+ implements InitializingBean, HelperRegistry {
+
+ /** The slf4j logger. */
+ private static final Logger logger = LoggerFactory
+ .getLogger(ReactiveHandlebarsViewResolver.class);
+
+ /**
+ * The handlebars object.
+ */
+ private Handlebars handlebars;
+
+ /**
+ * The value's resolvers.
+ */
+ private ValueResolver[] valueResolvers = ValueResolver.VALUE_RESOLVERS;
+
+ /**
+ * Fail on missing file. Default is: true.
+ */
+ private boolean failOnMissingFile = true;
+
+ /**
+ * The helper registry.
+ */
+ private HelperRegistry registry = new DefaultHelperRegistry();
+
+ /** True, if the message helper (based on {@link MessageSource}) should be registered. */
+ private boolean registerMessageHelper = true;
+
+ /**
+ * If true, the i18n helpers will use a {@link MessageSource} instead of a plain
+ * {@link ResourceBundle} .
+ */
+ private boolean bindI18nToMessageSource;
+
+ /**
+ * If true, templates will be deleted once applied. Useful, in some advanced template inheritance
+ * use cases. Used by {{#block}} helper
. Default is: false.
+ * At any time you can override the default setup with:
+ *
+ *
+ * {{#block "footer" delete-after-merge=true}}
+ *
+ */
+ private boolean deletePartialAfterMerge;
+
+ /**
+ * Set variable formatters.
+ */
+ private Formatter[] formatters;
+
+ /** Location of the handlebars.js file. */
+ private String handlebarsJsFile;
+
+ /** Template cache. */
+ private TemplateCache templateCache = new HighConcurrencyTemplateCache();
+
+ /** Charset. */
+ private Charset charset = StandardCharsets.UTF_8;
+
+ /** instance template loader. */
+ private TemplateLoader templateLoader;
+
+ /**
+ * Creates a new {@link ReactiveHandlebarsViewResolver}.
+ *
+ * @param viewClass The view's class. Required.
+ */
+ public ReactiveHandlebarsViewResolver(final Class extends ReactiveHandlebarsView> viewClass) {
+ setViewClass(viewClass);
+ setPrefix(TemplateLoader.DEFAULT_PREFIX);
+ setSuffix(TemplateLoader.DEFAULT_SUFFIX);
+ }
+
+ /**
+ * Creates a new {@link ReactiveHandlebarsViewResolver}.
+ */
+ public ReactiveHandlebarsViewResolver() {
+ this(ReactiveHandlebarsView.class);
+ }
+
+ /**
+ * Creates a new {@link ReactiveHandlebarsViewResolver} that utilizes the parameter handlebars
+ * for the underlying template lifecycle management.
+ *
+ * @param handlebars The {@link Handlebars} instance used for template lifecycle management.
+ * Required.
+ */
+ public ReactiveHandlebarsViewResolver(final Handlebars handlebars) {
+ this(handlebars, ReactiveHandlebarsView.class);
+ }
+
+ /**
+ * Creates a new {@link ReactiveHandlebarsViewResolver} that utilizes the parameter handlebars
+ * for the underlying template lifecycle management.
+ *
+ * @param handlebars The {@link Handlebars} instance used for template lifecycle management.
+ * Required.
+ * @param viewClass The view's class. Required.
+ */
+ public ReactiveHandlebarsViewResolver(final Handlebars handlebars,
+ final Class extends ReactiveHandlebarsView> viewClass) {
+ this(viewClass);
+ this.handlebars = handlebars;
+ }
+
+ /**
+ * Creates a new {@link Handlebars} object using the parameter {@link TemplateLoader}.
+ *
+ * @param templateLoader A template loader.
+ * @return A new handlebar's object.
+ */
+ protected Handlebars createHandlebars(final TemplateLoader templateLoader) {
+ return new Handlebars(templateLoader);
+ }
+
+ /**
+ * Creates a new template loader.
+ *
+ * @param context The application's context.
+ * @return A new template loader.
+ */
+ protected TemplateLoader createTemplateLoader(final ApplicationContext context) {
+ URLTemplateLoader springTemplateLoader = new SpringTemplateLoader(context);
+ springTemplateLoader.setPrefix(getPrefix());
+ springTemplateLoader.setSuffix(getSuffix());
+ return springTemplateLoader;
+ }
+
+ /**
+ * Configure the handlebars view.
+ *
+ * @param view The handlebars view.
+ * @return The configured view.
+ * @throws IOException If a resource cannot be loaded.
+ */
+ protected AbstractUrlBasedView configure(final ReactiveHandlebarsView view)
+ throws IOException {
+ String url = view.getUrl();
+ if (StringUtils.isEmpty(url)) {
+ throw new IllegalArgumentException("View URL must not be empty");
+ }
+
+ url = url.substring(getPrefix().length(),
+ url.length() - getSuffix().length());
+
+ try {
+ view.setTemplate(handlebars.compile(url));
+ view.setValueResolvers(valueResolvers);
+ } catch (FileNotFoundException ex) {
+ if (failOnMissingFile) {
+ throw ex;
+ }
+ logger.debug("File not found: {}", url);
+ }
+
+ return view;
+ }
+
+ /**
+ * A handlebars instance.
+ *
+ * @return A handlebars instance.
+ */
+ public Handlebars getHandlebars() {
+ if (handlebars == null) {
+ throw new IllegalStateException(
+ "afterPropertiesSet() method hasn't been call it.");
+ }
+ return handlebars;
+ }
+
+ /**
+ * Set the value resolvers.
+ *
+ * @param valueResolvers The value resolvers. Required.
+ * @throws IllegalArgumentException If the value resolvers are null or empty.
+ */
+ void setValueResolvers(final ValueResolver... valueResolvers) {
+ if (ArrayUtils.isEmpty(valueResolvers)) {
+ throw new IllegalArgumentException("At least one value-resolver must be present.");
+ } else {
+ this.valueResolvers = valueResolvers;
+ }
+ }
+
+ /**
+ * Set variable formatters.
+ *
+ * @param formatters Formatters to add.
+ * @throws IllegalArgumentException If the formatters are null or empty.
+ */
+ public void setFormatters(final Formatter... formatters) {
+ if (ArrayUtils.isEmpty(formatters)) {
+ throw new IllegalArgumentException("At least one formatter must be present.");
+ } else {
+ this.formatters = formatters;
+ }
+ }
+
+ /**
+ * Set the handlebars.js location used it to compile/precompile template to JavaScript.
+ *
+ * Using handlebars.js 2.x:
+ *
+ *
+ *
+ * Handlebars handlebars = new Handlebars()
+ * .handlebarsJsFile("handlebars-v2.0.0.js");
+ *
+ *
+ * Using handlebars.js 1.x:
+ *
+ *
+ *
+ * Handlebars handlebars = new Handlebars()
+ * .handlebarsJsFile("handlebars-v4.0.4.js");
+ *
+ *
+ * Default handlebars.js is handlebars-v4.0.4.js
.
+ *
+ * @param location A classpath location of the handlebar.js file.
+ * @throws IllegalArgumentException If a location is null or empty string
+ */
+ public void setHandlebarsJsFile(final String location) {
+ if (StringUtils.isEmpty(location)) {
+ throw new IllegalArgumentException("Js file location is required");
+ }
+ this.handlebarsJsFile = location;
+ }
+
+ /**
+ * True, if the view resolver should fail on missing files. Default is: true.
+ *
+ * @param failOnMissingFile True, if the view resolver should fail on
+ * missing files. Default is: true.
+ */
+ public void setFailOnMissingFile(final boolean failOnMissingFile) {
+ this.failOnMissingFile = failOnMissingFile;
+ }
+
+ /**
+ * Register all the helpers in the map.
+ *
+ * @param helpers The helpers to be registered. Required.
+ * @see Handlebars#registerHelper(String, Helper)
+ */
+ public void setHelpers(final Map> helpers) {
+ Objects.requireNonNull(helpers, "The helpers are required.");
+ for (Map.Entry> helper : helpers.entrySet()) {
+ registry.registerHelper(helper.getKey(), helper.getValue());
+ }
+ }
+
+ /**
+ * Register all the helpers in the list. Each element of the list must be a helper source.
+ *
+ * @param helpers The helpers to be registered. Required.
+ * @see Handlebars#registerHelpers(Class)
+ * @see Handlebars#registerHelpers(Object)
+ */
+ public void setHelperSources(final List> helpers) {
+ Objects.requireNonNull(helpers, "The helpers are required.");
+ for (Object helper : helpers) {
+ registry.registerHelpers(helper);
+ }
+ }
+
+ /**
+ * Same as {@link #setRegisterMessageHelper(boolean)} with a false argument. The message helper
+ * wont be registered when you call this method.
+ *
+ * @return This handlebars view resolver.
+ */
+ public ReactiveHandlebarsViewResolver withoutMessageHelper() {
+ setRegisterMessageHelper(false);
+ return this;
+ }
+
+ /**
+ * True, if the message helper (based on {@link MessageSource}) should be registered. Default is:
+ * true.
+ *
+ * @param registerMessageHelper True, if the message helper (based on {@link MessageSource})
+ * should be registered. Default is: true.
+ */
+ public void setRegisterMessageHelper(final boolean registerMessageHelper) {
+ this.registerMessageHelper = registerMessageHelper;
+ }
+
+ /**
+ * @param bindI18nToMessageSource If true, the i18n helpers will use a {@link MessageSource}
+ * instead of a plain {@link ResourceBundle}. Default is: false.
+ */
+ public void setBindI18nToMessageSource(final boolean bindI18nToMessageSource) {
+ this.bindI18nToMessageSource = bindI18nToMessageSource;
+ }
+
+ /**
+ * If true, templates will be deleted once applied. Useful, in some advanced template inheritance
+ * use cases. Used by {{#block}} helper
. Default is: false.
+ * At any time you can override the default setup with:
+ *
+ *
+ * {{#block "footer" delete-after-merge=true}}
+ *
+ *
+ * @param deletePartialAfterMerge True for clearing up templates once they got applied. Used by
+ * {{#block}} helper
.
+ */
+ public void setDeletePartialAfterMerge(final boolean deletePartialAfterMerge) {
+ this.deletePartialAfterMerge = deletePartialAfterMerge;
+ }
+
+ /**
+ * @param templateCache Set a template cache. Default is: {@link HighConcurrencyTemplateCache}.
+ */
+ public void setTemplateCache(final TemplateCache templateCache) {
+ this.templateCache = templateCache;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setPrefix(final String prefix) {
+ super.setPrefix(prefix);
+ if (templateLoader != null) {
+ templateLoader.setPrefix(prefix);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setSuffix(final String suffix) {
+ super.setSuffix(suffix);
+ if (templateLoader != null) {
+ templateLoader.setSuffix(suffix);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Decorator decorator(final String name) {
+ return this.registry.decorator(name);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver registerDecorator(final String name,
+ final Decorator decorator) {
+ registry.registerDecorator(name, decorator);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver setCharset(final Charset charset) {
+ this.charset = charset;
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver registerHelpers(final Object helperSource) {
+ registry.registerHelpers(helperSource);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver registerHelpers(final Class> helperSource) {
+ registry.registerHelpers(helperSource);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Helper helper(final String name) {
+ return registry.helper(name);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Set>> helpers() {
+ return registry.helpers();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver registerHelper(final String name,
+ final Helper helper) {
+ registry.registerHelper(name, helper);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver registerHelperMissing(final Helper helper) {
+ registry.registerHelperMissing(helper);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver registerHelpers(final URI location) throws Exception {
+ registry.registerHelpers(location);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver registerHelpers(final File input)
+ throws Exception {
+ registry.registerHelpers(input);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver registerHelpers(final String filename,
+ final Reader source)
+ throws Exception {
+ registry.registerHelpers(filename, source);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver registerHelpers(final String filename,
+ final InputStream source)
+ throws Exception {
+ registry.registerHelpers(filename, source);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ReactiveHandlebarsViewResolver registerHelpers(final String filename,
+ final String source)
+ throws IOException {
+ registry.registerHelpers(filename, source);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ * Autoconfigure {@link Handlebars}, {@link I18nHelper}.
+ */
+ @Override
+ public void afterPropertiesSet() {
+ if (handlebars == null) {
+ // Creates a new template loader.
+ this.templateLoader = createTemplateLoader(getApplicationContext());
+
+ // Creates a new handlebars object.
+ handlebars = Objects.requireNonNull(createHandlebars(templateLoader),
+ "A handlebars object is required.");
+ }
+
+ handlebars.with(registry);
+
+ if (handlebarsJsFile != null) {
+ handlebars.handlebarsJsFile(handlebarsJsFile);
+ }
+
+ if (formatters != null) {
+ for (Formatter formatter : formatters) {
+ handlebars.with(formatter);
+ }
+ }
+
+ if (registerMessageHelper) {
+ handlebars.registerHelper("message", new MessageSourceHelper(getApplicationContext()));
+ }
+
+ if (bindI18nToMessageSource) {
+ I18nSource i18nSource = createI18nSource(getApplicationContext());
+
+ I18nHelper.i18n.setSource(i18nSource);
+ I18nHelper.i18nJs.setSource(i18nSource);
+ }
+
+ TemplateCache cache = handlebars.getCache();
+ if (cache == NullTemplateCache.INSTANCE) {
+ handlebars.with(templateCache);
+ }
+
+ handlebars.setDeletePartialAfterMerge(deletePartialAfterMerge);
+ handlebars.setCharset(charset);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected Class> requiredViewClass() {
+ return ReactiveHandlebarsView.class;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected AbstractUrlBasedView createView(final String viewName) {
+ try {
+ return configure((ReactiveHandlebarsView) super.createView(viewName));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsApp.java b/handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsApp.java
new file mode 100644
index 000000000..2434174ae
--- /dev/null
+++ b/handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsApp.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2012-2015 Edgar Espina
+ *
+ * This file is part of Handlebars.java.
+ *
+ * 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 com.github.jknack.handlebars.springmvc.webflux;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.ResourceBundleMessageSource;
+
+import com.github.jknack.handlebars.Handlebars;
+import com.github.jknack.handlebars.Helper;
+import com.github.jknack.handlebars.cache.NullTemplateCache;
+import com.github.jknack.handlebars.springmvc.SpringTemplateLoader;
+
+/**
+ * @author OhChang Kwon(ohchang.kwon@navercorp.com)
+ */
+@Configuration
+public class ReactiveHandlebarsApp {
+ public static CharSequence helperSource() {
+ return "helper source!";
+ }
+
+ @Bean
+ public ReactiveHandlebarsViewResolver viewResolver() {
+ ReactiveHandlebarsViewResolver resolver = new ReactiveHandlebarsViewResolver();
+
+ Helper helper = (context, options) -> "Spring Helper";
+ resolver.registerHelper("spring", helper);
+
+ resolver.setHelperSources(Collections.singletonList(ReactiveHandlebarsApp.class));
+
+ Map> helpers = new HashMap<>();
+ helpers.put("setHelper", helper);
+ resolver.setHelpers(helpers);
+
+ resolver.setTemplateCache(NullTemplateCache.INSTANCE);
+ resolver.setBindI18nToMessageSource(true);
+
+ return resolver;
+ }
+
+ @Bean
+ public ReactiveHandlebarsViewResolver parameterizedViewResolver(
+ final ApplicationContext context) {
+ ReactiveHandlebarsViewResolver resolver = new ReactiveHandlebarsViewResolver(new Handlebars(
+ new SpringTemplateLoader(context)));
+
+ // no cache for testing
+ resolver.setTemplateCache(NullTemplateCache.INSTANCE);
+
+ return resolver;
+ }
+
+ @Bean
+ public MessageSource messageSource() {
+ ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
+ messageSource.setBasename("messages");
+ return messageSource;
+ }
+
+ @Bean
+ public ReactiveHandlebarsViewResolver viewResolverWithoutMessageHelper() {
+ return new ReactiveHandlebarsViewResolver().withoutMessageHelper();
+ }
+}
diff --git a/handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewResolverIntegrationTest.java b/handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewResolverIntegrationTest.java
new file mode 100644
index 000000000..d6699e68d
--- /dev/null
+++ b/handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewResolverIntegrationTest.java
@@ -0,0 +1,244 @@
+/**
+ * Copyright (c) 2012-2015 Edgar Espina
+ *
+ * This file is part of Handlebars.java.
+ *
+ * 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 com.github.jknack.handlebars.springmvc.webflux;
+
+import static junit.framework.TestCase.assertNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.Locale;
+
+import org.junit.Assert;
+import org.junit.ComparisonFailure;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.web.reactive.result.view.View;
+
+import com.github.jknack.handlebars.Handlebars;
+
+/**
+ * @author OhChang Kwon(ohchang.kwon@navercorp.com)
+ */
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = ReactiveHandlebarsApp.class)
+public class ReactiveHandlebarsViewResolverIntegrationTest {
+ @Autowired
+ @Qualifier("viewResolver")
+ private ReactiveHandlebarsViewResolver viewResolver;
+
+ @Autowired
+ @Qualifier("parameterizedViewResolver")
+ private ReactiveHandlebarsViewResolver parameterizedViewResolver;
+
+ @Autowired
+ @Qualifier("viewResolverWithoutMessageHelper")
+ private ReactiveHandlebarsViewResolver viewResolverWithoutMessageHelper;
+
+ @Test
+ public void getHandlebars() {
+ assertNotNull(viewResolver);
+ assertNotNull(viewResolver.getHandlebars());
+ }
+
+ @Test
+ public void resolveView() {
+ assertNotNull(viewResolver);
+ View view = viewResolver.resolveViewName("template", Locale.getDefault())
+ .block(Duration.ofSeconds(10));
+ assertNotNull(view);
+ assertEquals(ReactiveHandlebarsView.class, view.getClass());
+ }
+
+ @Test
+ public void resolveViewWithParameterized() {
+ assertNotNull(parameterizedViewResolver);
+ View view = parameterizedViewResolver.resolveViewName("template", Locale.getDefault())
+ .block(Duration.ofSeconds(10));
+ assertNotNull(view);
+ assertEquals(ReactiveHandlebarsView.class, view.getClass());
+ }
+
+ @Test
+ public void resolveViewWithFallback() {
+ try {
+ assertNotNull(viewResolver);
+ viewResolver.setFailOnMissingFile(false);
+ View view = viewResolver.resolveViewName("invalidView", Locale.getDefault())
+ .block(Duration.ofSeconds(10));
+ assertNull(view);
+ } finally {
+ viewResolver.setFailOnMissingFile(true);
+ }
+ }
+
+ @Test
+ public void resolveViewWithFallbackParameterized() {
+ try {
+ assertNotNull(parameterizedViewResolver);
+ parameterizedViewResolver.setFailOnMissingFile(false);
+ View view = parameterizedViewResolver.resolveViewName("invalidView", Locale.getDefault())
+ .block(Duration.ofSeconds(10));
+ assertNull(view);
+ } finally {
+ parameterizedViewResolver.setFailOnMissingFile(true);
+ }
+ }
+
+ @Test(expected = UncheckedIOException.class)
+ public void failToResolve() {
+ try {
+ assertNotNull(viewResolver);
+ viewResolver.setFailOnMissingFile(true);
+ viewResolver.resolveViewName("invalidView", Locale.getDefault());
+ } finally {
+ viewResolver.setFailOnMissingFile(true);
+ }
+ }
+
+ @Test(expected = UncheckedIOException.class)
+ public void failToResolveParameterized() {
+ try {
+ assertNotNull(parameterizedViewResolver);
+ parameterizedViewResolver.setFailOnMissingFile(true);
+ parameterizedViewResolver.resolveViewName("invalidView", Locale.getDefault());
+ } finally {
+ parameterizedViewResolver.setFailOnMissingFile(true);
+ }
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void getHandlebarsFail() {
+ assertNotNull(new ReactiveHandlebarsViewResolver().getHandlebars());
+ }
+
+ @Test
+ public void messageHelper() throws IOException {
+ assertNotNull(viewResolver);
+ Handlebars handlebars = viewResolver.getHandlebars();
+ assertEquals("Handlebars Spring MVC!",
+ handlebars.compileInline("{{message \"hello\"}}").apply(new Object()));
+ assertEquals("Handlebars Spring MVC!",
+ handlebars.compileInline("{{i18n \"hello\"}}").apply(new Object()));
+ }
+
+ @Test
+ public void messageHelperWithParams() throws IOException {
+ assertNotNull(viewResolver);
+ Handlebars handlebars = viewResolver.getHandlebars();
+ assertEquals("Hello Handlebars!",
+ handlebars.compileInline("{{message \"hello.0\" \"Handlebars\"}}").apply(new Object()));
+ assertEquals("Hello Handlebars!",
+ handlebars.compileInline("{{i18n \"hello.0\" \"Handlebars\"}}").apply(new Object()));
+
+ assertEquals("Hello Spring MVC!",
+ handlebars.compileInline("{{message \"hello.0\" \"Spring MVC\"}}").apply(new Object()));
+ assertEquals("Hello Spring MVC!",
+ handlebars.compileInline("{{i18n \"hello.0\" \"Spring MVC\"}}").apply(new Object()));
+ }
+
+ @Test
+ public void i18nJs() throws IOException {
+ // maven classpath
+ String expected = "\n";
+
+ assertNotNull(viewResolver);
+ Handlebars handlebars = viewResolver.getHandlebars();
+ String output = handlebars.compileInline("{{i18nJs \"es_AR\"}}").apply(new Object());
+ try {
+ // maven classpath
+ assertEquals(expected, output);
+ } catch (ComparisonFailure ex) {
+ try {
+ // eclipse classpath
+ assertEquals("\n", output);
+ } catch (ComparisonFailure java18) {
+ // java 1.8
+ assertEquals("\n", output);
+ }
+ }
+ }
+
+ @Test
+ public void messageHelperWithDefaultValue() throws IOException {
+ assertNotNull(viewResolver);
+ Handlebars handlebars = viewResolver.getHandlebars();
+ assertEquals("hey",
+ handlebars.compileInline("{{message \"hi\" default='hey'}}").apply(new Object()));
+ }
+
+ @Test
+ public void customHelper() throws IOException {
+ assertNotNull(viewResolver);
+ Handlebars handlebars = viewResolver.getHandlebars();
+ assertEquals("Spring Helper", handlebars.compileInline("{{spring}}").apply(new Object()));
+ }
+
+ @Test
+ public void setCustomHelper() throws IOException {
+ assertNotNull(viewResolver);
+ Handlebars handlebars = viewResolver.getHandlebars();
+ assertEquals("Spring Helper", handlebars.compileInline("{{setHelper}}").apply(new Object()));
+ }
+
+ @Test
+ public void helperSource() throws IOException {
+ assertNotNull(viewResolver);
+ Handlebars handlebars = viewResolver.getHandlebars();
+ assertEquals("helper source!", handlebars.compileInline("{{helperSource}}").apply(new Object()));
+ }
+
+ @Test
+ public void viewResolverWithMessageHelper() {
+ assertNotNull(viewResolver);
+ assertNotNull(viewResolver.helper("message"));
+ }
+
+ @Test
+ public void viewResolverWithoutMessageHelper() {
+ assertNotNull(viewResolverWithoutMessageHelper);
+ Assert.assertNull(viewResolverWithoutMessageHelper.helper("message"));
+ }
+}
diff --git a/handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewTest.java b/handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewTest.java
new file mode 100644
index 000000000..f62ece809
--- /dev/null
+++ b/handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewTest.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2012-2015 Edgar Espina
+ *
+ * This file is part of Handlebars.java.
+ *
+ * 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 com.github.jknack.handlebars.springmvc.webflux;
+
+import static junit.framework.TestCase.assertNotNull;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import java.io.OutputStreamWriter;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Test;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+
+import com.github.jknack.handlebars.Context;
+import com.github.jknack.handlebars.Template;
+import com.github.jknack.handlebars.context.MapValueResolver;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * @author OhChang Kwon(ohchang.kwon@navercorp.com)
+ */
+public class ReactiveHandlebarsViewTest {
+
+ @Test
+ public void shouldRenderInternal() throws Exception {
+ // Given
+ final Map model = new HashMap<>();
+
+ final Template mockTemplate = createMock(Template.class);
+ final Capture context = EasyMock.newCapture();
+ mockTemplate.apply(capture(context), isA(OutputStreamWriter.class));
+
+ final MockServerHttpRequest testRequest = MockServerHttpRequest.get("/").build();
+ final MockServerWebExchange testExchange = MockServerWebExchange.from(testRequest);
+
+ replay(mockTemplate);
+
+ ReactiveHandlebarsView testCase = new ReactiveHandlebarsView();
+ testCase.setValueResolvers(MapValueResolver.INSTANCE);
+ testCase.setTemplate(mockTemplate);
+
+ // When
+ final Mono result = testCase.renderInternal(model, null, testExchange);
+
+ // Then
+ result.block(Duration.ofSeconds(10));
+ assertNotNull(context.getValue());
+ verify(mockTemplate);
+ }
+}
diff --git a/handlebars/src/test/java/mustache/specs/SpecRunner.java b/handlebars/src/test/java/mustache/specs/SpecRunner.java
index de8c2f7f1..1265bd181 100644
--- a/handlebars/src/test/java/mustache/specs/SpecRunner.java
+++ b/handlebars/src/test/java/mustache/specs/SpecRunner.java
@@ -122,6 +122,9 @@ private T invokeMethodChain(Object object, final Object[][] methodCalls)
}
Method method = getDeclaredMethod(object.getClass(), methodName,
classes);
+ if (!method.isAccessible()) {
+ method.setAccessible(true);
+ }
object = method.invoke(object, arguments);
}
return (T) object;
diff --git a/pom.xml b/pom.xml
index 4c515c94d..b879146c3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -84,16 +84,23 @@
org.springframework
spring-webmvc
- 3.1.1.RELEASE
+ ${spring.version}
+
+
+
+ org.springframework
+ spring-webflux
+ ${spring.version}
org.springframework
spring-test
- 3.1.1.RELEASE
+ ${spring.version}
test
+
org.slf4j
@@ -149,7 +156,7 @@
junit
junit
test
- 4.11
+ 4.12
@@ -425,6 +432,7 @@
2.1.4
1.9.12
0.8.1
+ 5.0.7.RELEASE
yyyy-MM-dd HH:mm:ssa
${maven.build.timestamp}