From e3318c9219a6766677c9f5dd667e2f8358f86cf5 Mon Sep 17 00:00:00 2001 From: ohchang-kwon Date: Wed, 25 Jul 2018 18:31:28 +0900 Subject: [PATCH 1/2] Add spring-webflux compatibility to handlebars-springmvc (#642) * update spring version (3.1.1.RELEASE -> 5.0.7.RELEASE) * add spring-webflux * update Junit version (4.11 -> 4.12) * add handlebar spring reactive view and view resolver --- handlebars-springmvc/pom.xml | 10 + .../springmvc/HandlebarsViewResolver.java | 31 +- .../handlebars/springmvc/SpringUtils.java | 64 ++ .../webflux/ReactiveHandlebarsView.java | 127 ++++ .../ReactiveHandlebarsViewResolver.java | 609 ++++++++++++++++++ .../webflux/ReactiveHandlebarsApp.java | 86 +++ ...HandlebarsViewResolverIntegrationTest.java | 244 +++++++ .../webflux/ReactiveHandlebarsViewTest.java | 75 +++ pom.xml | 14 +- 9 files changed, 1227 insertions(+), 33 deletions(-) create mode 100644 handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/SpringUtils.java create mode 100644 handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsView.java create mode 100644 handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewResolver.java create mode 100644 handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsApp.java create mode 100644 handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewResolverIntegrationTest.java create mode 100644 handlebars-springmvc/src/test/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsViewTest.java diff --git a/handlebars-springmvc/pom.xml b/handlebars-springmvc/pom.xml index 27bf5e3f5..dcccd6a84 100644 --- a/handlebars-springmvc/pom.xml +++ b/handlebars-springmvc/pom.xml @@ -26,6 +26,16 @@ spring-webmvc + + org.springframework + spring-webflux + + + + org.apache.commons + commons-lang3 + + javax.servlet diff --git a/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/HandlebarsViewResolver.java b/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/HandlebarsViewResolver.java index 9a1cc6450..b5d73df69 100644 --- a/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/HandlebarsViewResolver.java +++ b/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/HandlebarsViewResolver.java @@ -17,6 +17,7 @@ */ package com.github.jknack.handlebars.springmvc; +import static com.github.jknack.handlebars.springmvc.SpringUtils.createI18nSource; import static java.util.Objects.requireNonNull; import java.io.File; @@ -26,10 +27,7 @@ import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.ResourceBundle; @@ -264,33 +262,6 @@ public void afterPropertiesSet() { handlebars.setCharset(charset); } - /** - * Creates a new i18n source. - * - * @param context The application context. - * @return A new i18n source. - */ - private static I18nSource createI18nSource(final ApplicationContext context) { - return new I18nSource() { - @Override - public String message(final String key, final Locale locale, final Object... args) { - return context.getMessage(key, args, locale); - } - - @Override - public String[] keys(final String basename, final Locale locale) { - ResourceBundle bundle = ResourceBundle.getBundle(basename, locale); - Enumeration keys = bundle.getKeys(); - List result = new ArrayList(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - result.add(key); - } - return result.toArray(new String[result.size()]); - } - }; - } - /** * Creates a new {@link Handlebars} object using the parameter {@link TemplateLoader}. * diff --git a/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/SpringUtils.java b/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/SpringUtils.java new file mode 100644 index 000000000..4892e9e34 --- /dev/null +++ b/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/SpringUtils.java @@ -0,0 +1,64 @@ +/** + * 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; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; + +import org.springframework.context.ApplicationContext; + +import com.github.jknack.handlebars.helper.I18nSource; + +/** + * @author OhChang Kwon(ohchang.kwon@navercorp.com) + */ +public final class SpringUtils { + /** private constructor for Util class. */ + private SpringUtils() { + } + + /** + * Creates a new i18n source. + * + * @param context The application context. + * @return A new i18n source. + */ + public static I18nSource createI18nSource(final ApplicationContext context) { + return new I18nSource() { + @Override + public String message(final String key, final Locale locale, final Object... args) { + return context.getMessage(key, args, locale); + } + + @Override + public String[] keys(final String basename, final Locale locale) { + ResourceBundle bundle = ResourceBundle.getBundle(basename, locale); + Enumeration keys = bundle.getKeys(); + List result = new ArrayList<>(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + result.add(key); + } + return result.toArray(new String[0]); + } + }; + } +} diff --git a/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsView.java b/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsView.java new file mode 100644 index 000000000..2a5091aea --- /dev/null +++ b/handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/webflux/ReactiveHandlebarsView.java @@ -0,0 +1,127 @@ +/** + * 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.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.MediaType; +import org.springframework.util.MimeType; +import org.springframework.web.reactive.result.view.AbstractUrlBasedView; +import org.springframework.web.server.ServerWebExchange; + +import com.github.jknack.handlebars.Context; +import com.github.jknack.handlebars.Template; +import com.github.jknack.handlebars.ValueResolver; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * A handlebars reactive view implementation. + * + * @author OhChang Kwon(ohchang.kwon@navercorp.com) + */ +public class ReactiveHandlebarsView extends AbstractUrlBasedView { + + /** + * The compiled template. + */ + private Template template; + + /** + * The value's resolvers. + */ + private ValueResolver[] valueResolvers; + + /** + * 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; + } + } + + /** + * @return The underlying template for this view. + */ + public Template getTemplate() { + return this.template; + } + + /** + * Set the compiled template. + * + * @param template The compiled template. Required. + */ + void setTemplate(final Template template) { + this.template = Objects.requireNonNull(template, + "A handlebars template is required."); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean checkResourceExists(final Locale locale) { + return template != null; + } + + /** + * {@inheritDoc} + */ + @Override + protected Mono renderInternal(final Map 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 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 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/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} From f142a6b9a69644342fd5aabcec73a7cd9ff86b56 Mon Sep 17 00:00:00 2001 From: ohchang-kwon Date: Wed, 25 Jul 2018 19:31:54 +0900 Subject: [PATCH 2/2] Add spring-webflux compatibility to handlebars-springmvc (#642) * Fix test fail --- handlebars/src/test/java/mustache/specs/SpecRunner.java | 3 +++ 1 file changed, 3 insertions(+) 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;