diff --git a/src/main/docs/guide/breaks.adoc b/src/main/docs/guide/breaks.adoc index 6535c4888..e39af6c1b 100644 --- a/src/main/docs/guide/breaks.adoc +++ b/src/main/docs/guide/breaks.adoc @@ -2,10 +2,17 @@ This section outlines the breaking changes done in major versions of Micronaut V === 3.0.0 +* A Reactive Views renderer api:views.ReactiveViewsRenderer[] has been introduced. `Soy` implementation uses it. Other template engine implementation render to a `Writable` +by implementing `api:views.WritableViewsRenderer[]`. `ViewsRenderer` is now a interface class extended by both pi:views.ReactiveViewsRenderer[]` and api:views.WritableViewsRenderer[]. + * api:views.model.ViewModelProcessor[] no longer assumes a `Map` model and must be typed to the exact type of the model you would like to process. * api:views.model.ViewsRenderer[] are now typed. Moreover, provided `ViewsRenderer` don't specify `@Produces(MediaType.TEXT_HTML)` and responses content type respect the content type defined for the route. +* `ViewsRenderer::modelOf` method has been moved to `ViewUtils::modelOf` + +* Constant `EXTENSION_SEPARATOR` has been moved from `ViewsRenderer` to `ViewUtils` + == 2.0.0 * The `micronaut-views` dependency is no longer published. Replace the dependency with the one specific to the view implementation being used. diff --git a/test-suite/src/test/groovy/views/NonHTMLViewRendererSpec.groovy b/test-suite/src/test/groovy/views/NonHTMLViewRendererSpec.groovy index 0aeafb8aa..d353c66a9 100644 --- a/test-suite/src/test/groovy/views/NonHTMLViewRendererSpec.groovy +++ b/test-suite/src/test/groovy/views/NonHTMLViewRendererSpec.groovy @@ -17,6 +17,7 @@ import io.micronaut.http.client.HttpClient import io.micronaut.runtime.server.EmbeddedServer import io.micronaut.views.View import io.micronaut.views.ViewsRenderer +import io.micronaut.views.WritableViewsRenderer import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification @@ -88,7 +89,7 @@ class NonHTMLViewRendererSpec extends Specification { @Produces(MediaType.TEXT_XML) @Requires(property = "spec.name", value = "CsvViewRendererSpec") @Singleton - static class XmlViewRenderer implements ViewsRenderer { + static class XmlViewRenderer implements WritableViewsRenderer { @Override Writable render(@NonNull String viewName, @Nullable Library data, @NonNull HttpRequest request) { new Writable() { @@ -110,7 +111,7 @@ class NonHTMLViewRendererSpec extends Specification { @Produces(MediaType.TEXT_CSV) @Requires(property = "spec.name", value = "CsvViewRendererSpec") @Singleton - static class SingleBookViewRenderer implements ViewsRenderer { + static class SingleBookViewRenderer implements WritableViewsRenderer { // this renderer should not be used because it specifies a different type @Override @@ -137,7 +138,7 @@ class NonHTMLViewRendererSpec extends Specification { @Produces(MediaType.TEXT_CSV) @Requires(property = "spec.name", value = "CsvViewRendererSpec") @Singleton - static class CsvViewRenderer implements ViewsRenderer { + static class CsvViewRenderer implements WritableViewsRenderer { @Override Writable render(@NonNull String viewName, @Nullable Library data, @NonNull HttpRequest request) { new Writable() { diff --git a/views-core/src/main/java/io/micronaut/views/ReactiveViewsRenderer.java b/views-core/src/main/java/io/micronaut/views/ReactiveViewsRenderer.java index 13b8be22c..5d8f07e4a 100644 --- a/views-core/src/main/java/io/micronaut/views/ReactiveViewsRenderer.java +++ b/views-core/src/main/java/io/micronaut/views/ReactiveViewsRenderer.java @@ -24,6 +24,7 @@ /** * Reactive implementation of {@link ViewsRenderer}. * @author Sergio del Amo + * @param The model type * @since 3.0.0 */ public interface ReactiveViewsRenderer extends ViewsRenderer { diff --git a/views-core/src/main/java/io/micronaut/views/ViewUtils.java b/views-core/src/main/java/io/micronaut/views/ViewUtils.java index 002a9d1d5..7d1805bb7 100644 --- a/views-core/src/main/java/io/micronaut/views/ViewUtils.java +++ b/views-core/src/main/java/io/micronaut/views/ViewUtils.java @@ -17,6 +17,10 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.beans.BeanMap; + +import java.util.HashMap; +import java.util.Map; /** * Utility methods for views. @@ -25,6 +29,26 @@ * @since 1.1.0 */ public class ViewUtils { + /** + * Extension separator. + */ + public static final String EXTENSION_SEPARATOR = "."; + + /** + * Creates a view model for the given data. + * @param data The data + * @return The model + */ + @NonNull + public static Map modelOf(@Nullable Object data) { + if (data == null) { + return new HashMap<>(0); + } + if (data instanceof Map) { + return (Map) data; + } + return BeanMap.of(data); + } /** * Returns a path with unix style folder diff --git a/views-core/src/main/java/io/micronaut/views/ViewsRenderer.java b/views-core/src/main/java/io/micronaut/views/ViewsRenderer.java index f3329201d..d57412c01 100644 --- a/views-core/src/main/java/io/micronaut/views/ViewsRenderer.java +++ b/views-core/src/main/java/io/micronaut/views/ViewsRenderer.java @@ -19,8 +19,8 @@ import io.micronaut.core.order.Ordered; /** - * Interface to be implemented by View Engines implementations. - * Implemented by either {@link WritableViewsRender} or {@link ReactiveViewsRenderer} + * Base Interface to be implemented by View Engines implementations. + * You should implement either {@link WritableViewsRenderer} or {@link ReactiveViewsRenderer}. * @param The model type * @author Sergio del Amo * @since 1.0 @@ -31,5 +31,5 @@ public interface ViewsRenderer extends Ordered { * @param viewName view name to be rendered * @return true if a template can be found for the supplied view name. */ - boolean canRender(@NonNull String viewName); + boolean exists(@NonNull String viewName); } diff --git a/views-core/src/main/java/io/micronaut/views/WritableViewsRenderer.java b/views-core/src/main/java/io/micronaut/views/WritableViewsRenderer.java index 443af9ac1..525013c23 100644 --- a/views-core/src/main/java/io/micronaut/views/WritableViewsRenderer.java +++ b/views-core/src/main/java/io/micronaut/views/WritableViewsRenderer.java @@ -24,6 +24,7 @@ * Writes the view into {@link Writable}. * @author Sergio del Amo * @since 3.0.0 + * @param The model type */ public interface WritableViewsRenderer extends ViewsRenderer { diff --git a/views-freemarker/src/main/java/io/micronaut/views/freemarker/FreemarkerViewsRenderer.java b/views-freemarker/src/main/java/io/micronaut/views/freemarker/FreemarkerViewsRenderer.java index 6afb2b311..edf6a65d2 100644 --- a/views-freemarker/src/main/java/io/micronaut/views/freemarker/FreemarkerViewsRenderer.java +++ b/views-freemarker/src/main/java/io/micronaut/views/freemarker/FreemarkerViewsRenderer.java @@ -27,7 +27,7 @@ import io.micronaut.http.HttpRequest; import io.micronaut.views.ViewUtils; import io.micronaut.views.ViewsConfiguration; -import io.micronaut.views.ViewsRenderer; +import io.micronaut.views.WritableViewsRenderer; import io.micronaut.views.exceptions.ViewRenderingException; import io.micronaut.core.annotation.Nullable; @@ -47,7 +47,7 @@ @Requires(property = FreemarkerViewsRendererConfigurationProperties.PREFIX + ".enabled", notEquals = "false") @Requires(classes = Configuration.class) @Singleton -public class FreemarkerViewsRenderer implements ViewsRenderer { +public class FreemarkerViewsRenderer implements WritableViewsRenderer { protected final ViewsConfiguration viewsConfiguration; protected final FreemarkerViewsRendererConfigurationProperties freemarkerMicronautConfiguration; @@ -70,7 +70,7 @@ public FreemarkerViewsRenderer(ViewsConfiguration viewsConfiguration, public Writable render(@NonNull String viewName, @Nullable T data, @NonNull HttpRequest request) { ArgumentUtils.requireNonNull("viewName", viewName); return (writer) -> { - Map context = modelOf(data); + Map context = ViewUtils.modelOf(data); String location = viewLocation(viewName); Template template = freemarkerMicronautConfiguration.getTemplate(location); try { @@ -96,7 +96,7 @@ public boolean exists(@NonNull String view) { private String viewLocation(String name) { return ViewUtils.normalizeFile(name, extension) + - EXTENSION_SEPARATOR + + ViewUtils.EXTENSION_SEPARATOR + extension; } diff --git a/views-handlebars/src/main/java/io/micronaut/views/handlebars/HandlebarsViewsRenderer.java b/views-handlebars/src/main/java/io/micronaut/views/handlebars/HandlebarsViewsRenderer.java index f47a2b57f..3f94f515e 100644 --- a/views-handlebars/src/main/java/io/micronaut/views/handlebars/HandlebarsViewsRenderer.java +++ b/views-handlebars/src/main/java/io/micronaut/views/handlebars/HandlebarsViewsRenderer.java @@ -27,14 +27,14 @@ import io.micronaut.http.HttpRequest; import io.micronaut.views.ViewUtils; import io.micronaut.views.ViewsConfiguration; -import io.micronaut.views.ViewsRenderer; +import io.micronaut.views.WritableViewsRenderer; import io.micronaut.views.exceptions.ViewRenderingException; import io.micronaut.core.annotation.Nullable; import jakarta.inject.Inject; import jakarta.inject.Singleton; /** - * Renders Views with with Handlebars.java. + * Renders Views with Handlebars.java. * * @author Sergio del Amo * @see https://jknack.github.io/handlebars.java/ @@ -44,7 +44,7 @@ @Requires(property = HandlebarsViewsRendererConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE) @Requires(classes = Handlebars.class) @Singleton -public class HandlebarsViewsRenderer implements ViewsRenderer { +public class HandlebarsViewsRenderer implements WritableViewsRenderer { protected final ViewsConfiguration viewsConfiguration; protected final ResourceLoader resourceLoader; @@ -91,7 +91,7 @@ public boolean exists(@NonNull String viewName) { if (viewName == null) { return false; } - String location = viewLocation(viewName) + EXTENSION_SEPARATOR + extension(); + String location = viewLocation(viewName) + ViewUtils.EXTENSION_SEPARATOR + extension(); return resourceLoader.getResource(location).isPresent(); } diff --git a/views-pebble/src/main/java/io/micronaut/views/pebble/PebbleEngineFactory.java b/views-pebble/src/main/java/io/micronaut/views/pebble/PebbleEngineFactory.java index ac74e3cef..906ca72f6 100644 --- a/views-pebble/src/main/java/io/micronaut/views/pebble/PebbleEngineFactory.java +++ b/views-pebble/src/main/java/io/micronaut/views/pebble/PebbleEngineFactory.java @@ -17,6 +17,8 @@ import java.util.List; import java.util.Optional; + +import io.micronaut.views.ViewUtils; import jakarta.inject.Inject; import jakarta.inject.Singleton; import com.mitchellbosecke.pebble.PebbleEngine; @@ -30,7 +32,6 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.views.ViewsConfiguration; -import io.micronaut.views.ViewsRenderer; import java.util.concurrent.ExecutorService; import jakarta.inject.Named; @@ -110,7 +111,7 @@ public PebbleEngine create() { } else { Loader loader = new ClasspathLoader(); loader.setPrefix(viewsConfiguration.getFolder()); - loader.setSuffix(ViewsRenderer.EXTENSION_SEPARATOR + configuration.getDefaultExtension()); + loader.setSuffix(ViewUtils.EXTENSION_SEPARATOR + configuration.getDefaultExtension()); builder.loader(loader); } diff --git a/views-pebble/src/main/java/io/micronaut/views/pebble/PebbleViewsRenderer.java b/views-pebble/src/main/java/io/micronaut/views/pebble/PebbleViewsRenderer.java index 344c7ced0..fb231be1e 100644 --- a/views-pebble/src/main/java/io/micronaut/views/pebble/PebbleViewsRenderer.java +++ b/views-pebble/src/main/java/io/micronaut/views/pebble/PebbleViewsRenderer.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpRequest; +import io.micronaut.views.WritableViewsRenderer; import jakarta.inject.Inject; import jakarta.inject.Singleton; import com.mitchellbosecke.pebble.PebbleEngine; @@ -24,7 +25,6 @@ import io.micronaut.core.io.Writable; import io.micronaut.core.util.StringUtils; import io.micronaut.views.ViewUtils; -import io.micronaut.views.ViewsRenderer; /** * Renders Views with Pebble. @@ -37,7 +37,7 @@ @Singleton @Requires(property = PebbleConfigurationProperties.ENABLED, notEquals = StringUtils.FALSE) @Requires(classes = PebbleEngine.class) -public class PebbleViewsRenderer implements ViewsRenderer { +public class PebbleViewsRenderer implements WritableViewsRenderer { private final String extension; private final PebbleEngine engine; @@ -50,7 +50,7 @@ public PebbleViewsRenderer(PebbleConfiguration configuration, PebbleEngine engin @Override public Writable render(String name, T data, @NonNull HttpRequest request) { - return (writer) -> engine.getTemplate(ViewUtils.normalizeFile(name, extension)).evaluate(writer, modelOf(data)); + return (writer) -> engine.getTemplate(ViewUtils.normalizeFile(name, extension)).evaluate(writer, ViewUtils.modelOf(data)); } @Override diff --git a/views-rocker/src/main/java/io/micronaut/views/rocker/RockerViewsRenderer.java b/views-rocker/src/main/java/io/micronaut/views/rocker/RockerViewsRenderer.java index 8c4e54569..ee69c1ce1 100644 --- a/views-rocker/src/main/java/io/micronaut/views/rocker/RockerViewsRenderer.java +++ b/views-rocker/src/main/java/io/micronaut/views/rocker/RockerViewsRenderer.java @@ -19,10 +19,11 @@ import io.micronaut.core.io.Writable; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.http.HttpRequest; +import io.micronaut.views.ViewUtils; import io.micronaut.views.ViewsConfiguration; -import io.micronaut.views.ViewsRenderer; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.views.WritableViewsRenderer; import jakarta.inject.Inject; import jakarta.inject.Singleton; import java.util.Map; @@ -35,7 +36,7 @@ * @param The model type */ @Singleton -public class RockerViewsRenderer implements ViewsRenderer { +public class RockerViewsRenderer implements WritableViewsRenderer { protected final RockerEngine rockerEngine; protected final ViewsConfiguration viewsConfiguration; @@ -62,7 +63,7 @@ public RockerViewsRenderer(ViewsConfiguration viewsConfiguration, public Writable render(@NonNull String view, @Nullable T data, @NonNull HttpRequest request) { ArgumentUtils.requireNonNull("view", view); - Map context = modelOf(data); + Map context = ViewUtils.modelOf(data); BindableRockerModel model = rockerConfiguration.isRelaxed() ? rockerEngine.template(view).relaxedBind(context) : rockerEngine.template(view).bind(context); diff --git a/views-soy/src/main/java/io/micronaut/views/soy/SoySauceViewsRenderer.java b/views-soy/src/main/java/io/micronaut/views/soy/SoySauceViewsRenderer.java index bc0e83743..60e976134 100644 --- a/views-soy/src/main/java/io/micronaut/views/soy/SoySauceViewsRenderer.java +++ b/views-soy/src/main/java/io/micronaut/views/soy/SoySauceViewsRenderer.java @@ -31,10 +31,9 @@ import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.annotation.Filter; import io.micronaut.http.annotation.Produces; import io.micronaut.views.ModelAndView; -import io.micronaut.views.ReactiveViewRenderer; +import io.micronaut.views.ReactiveViewsRenderer; import io.micronaut.views.ViewsConfiguration; import io.micronaut.views.ViewsResolver; import io.micronaut.views.csp.CspConfiguration; @@ -42,6 +41,7 @@ import io.micronaut.views.exceptions.ViewRenderingException; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import jakarta.inject.Singleton; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,12 +65,12 @@ * @since 1.2.1 * @param The model type */ -@Filter(Filter.MATCH_ALL_PATTERN) +@Singleton @Produces(MediaType.TEXT_HTML) @Requires(property = SoyViewsRendererConfigurationProperties.PREFIX + ".enabled", notEquals = "false") @Requires(property = SoyViewsRendererConfigurationProperties.PREFIX + ".engine", notEquals = "tofu") @SuppressWarnings({"WeakerAccess", "UnstableApiUsage"}) -public class SoySauceViewsRenderer implements ReactiveViewRenderer { +public class SoySauceViewsRenderer implements ReactiveViewsRenderer { private static final Logger LOG = LoggerFactory.getLogger(SoySauceViewsRenderer.class); private static final String INJECTED_NONCE_PROPERTY = "csp_nonce"; private static final String SOY_CONTEXT_SENTINEL = "__soy_context__"; @@ -221,6 +221,7 @@ public Map modelOf(@Nullable Object data) { * @param response HTTP response object assembled so far. * @return A writable where the view will be written to. */ + @Override @NonNull public Publisher> render(@NonNull String viewName, @Nullable T data, @@ -253,7 +254,7 @@ public Publisher> render(@NonNull String viewName, Map dataMap = (Map) data; if (dataMap.size() == 1 && dataMap.containsKey(SOY_CONTEXT_SENTINEL)) { // it's a packaged soy context - context = (SoyContextMediator)dataMap.get(SOY_CONTEXT_SENTINEL); + context = (SoyContextMediator) dataMap.get(SOY_CONTEXT_SENTINEL); } else { // otherwise, use it directly as a map //noinspection unchecked diff --git a/views-thymeleaf/src/main/java/io/micronaut/views/thymeleaf/ThymeleafViewsRenderer.java b/views-thymeleaf/src/main/java/io/micronaut/views/thymeleaf/ThymeleafViewsRenderer.java index ff1ec0088..711f6cbb6 100644 --- a/views-thymeleaf/src/main/java/io/micronaut/views/thymeleaf/ThymeleafViewsRenderer.java +++ b/views-thymeleaf/src/main/java/io/micronaut/views/thymeleaf/ThymeleafViewsRenderer.java @@ -14,6 +14,9 @@ * limitations under the License. */ package io.micronaut.views.thymeleaf; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.io.ResourceLoader; import io.micronaut.core.io.Writable; import io.micronaut.core.io.scan.ClassPathResourceLoader; @@ -21,18 +24,16 @@ import io.micronaut.http.HttpRequest; import io.micronaut.views.ViewUtils; import io.micronaut.views.ViewsConfiguration; -import io.micronaut.views.ViewsRenderer; +import io.micronaut.views.WritableViewsRenderer; import io.micronaut.views.exceptions.ViewRenderingException; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; import org.thymeleaf.TemplateEngine; -import org.thymeleaf.context.Context; import org.thymeleaf.context.IContext; import org.thymeleaf.exceptions.TemplateEngineException; import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver; import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; + import java.io.Writer; import java.util.Locale; @@ -47,7 +48,7 @@ * @param The model type */ @Singleton -public class ThymeleafViewsRenderer implements ViewsRenderer { +public class ThymeleafViewsRenderer implements WritableViewsRenderer { protected final AbstractConfigurableTemplateResolver templateResolver; protected final TemplateEngine engine; @@ -75,7 +76,7 @@ public Writable render(@NonNull String viewName, ArgumentUtils.requireNonNull("viewName", viewName); ArgumentUtils.requireNonNull("request", request); return (writer) -> { - IContext context = new WebContext(request, request.getLocale().orElse(Locale.US), modelOf(data)); + IContext context = new WebContext(request, request.getLocale().orElse(Locale.US), ViewUtils.modelOf(data)); render(viewName, context, writer); }; } diff --git a/views-velocity/src/main/java/io/micronaut/views/velocity/VelocityViewsRenderer.java b/views-velocity/src/main/java/io/micronaut/views/velocity/VelocityViewsRenderer.java index 6b15dbaed..3ac4c506e 100644 --- a/views-velocity/src/main/java/io/micronaut/views/velocity/VelocityViewsRenderer.java +++ b/views-velocity/src/main/java/io/micronaut/views/velocity/VelocityViewsRenderer.java @@ -20,7 +20,7 @@ import io.micronaut.http.HttpRequest; import io.micronaut.views.ViewUtils; import io.micronaut.views.ViewsConfiguration; -import io.micronaut.views.ViewsRenderer; +import io.micronaut.views.WritableViewsRenderer; import io.micronaut.views.exceptions.ViewRenderingException; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; @@ -47,7 +47,7 @@ * @param The model type */ @Singleton -public class VelocityViewsRenderer implements ViewsRenderer { +public class VelocityViewsRenderer implements WritableViewsRenderer { protected final VelocityEngine velocityEngine; protected final ViewsConfiguration viewsConfiguration; @@ -74,7 +74,7 @@ public VelocityViewsRenderer(ViewsConfiguration viewsConfiguration, public Writable render(@NonNull String view, @Nullable T data, @NonNull HttpRequest request) { ArgumentUtils.requireNonNull("view", view); return (writer) -> { - Map context = modelOf(data); + Map context = ViewUtils.modelOf(data); final VelocityContext velocityContext = new VelocityContext(context); render(view, velocityContext, StandardCharsets.UTF_8.name(), writer); };