Skip to content

Commit

Permalink
Feature: Asynchronous Views
Browse files Browse the repository at this point in the history
This changeset proposes a structure for an alternate `View`
annotation, `AsyncView`. If `AsyncView` is used with a controller,
it will try to load an `AsyncViewsRenderer` rather than a regular
one, which is allowed to return a `Publisher` rather than producing
an HTTP response synchronously from template context.

Changes so far:
- [x] Add `AsyncView` annotation
- [x] Add `AsyncViewsRenderer` interface
- [x] Refactor common items to `BaseViewsRenderer`
- [x] Make `ViewsRenderer` comply with `BaseViewsRenderer`
  • Loading branch information
sgammon committed Aug 29, 2019
1 parent a1ff284 commit 46e261c
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 85 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.micronaut.views;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MutableHttpResponse;
import org.reactivestreams.Publisher;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;


/**
* Asynchronous rendering interface for views in Micronaut. This interface works with reactive types to allow the event
* loop to take over when the renderer is paused, in cases where renderers support such signals.
*
* @see ViewsRenderer for the synchronous version
* @author Sam Gammon
* @since 1.3.0
*/
public interface AsyncViewsRenderer extends BaseViewsRenderer {
/**
* @param viewName view name to be render
* @param data response body to render it with a view
* @return A writable where the view will be written to.
*/
@Nonnull
Publisher<MutableHttpResponse<?>> render(@Nonnull String viewName, @Nullable Object data);

/**
* @param viewName view name to be render
* @param data response body to render it with a view
* @param request HTTP request
* @return A writable where the view will be written to.
*/
default @Nonnull Publisher<MutableHttpResponse<?>> render(
@Nonnull String viewName, @Nullable Object data, @Nonnull HttpRequest<?> request) {
return render(viewName, data);
}

/**
* @param viewName view name to be render
* @param data response body to render it with a view
* @param request HTTP request
* @param response HTTP response object.
* @return A writable where the view will be written to.
*/
default @Nonnull Publisher<MutableHttpResponse<?>> render(
@Nonnull String viewName,
@Nullable Object data,
@Nonnull HttpRequest<?> request,
@Nonnull HttpResponse<?> response) {
return render(viewName, data);
}
}
83 changes: 83 additions & 0 deletions views-core/src/main/java/io/micronaut/views/BaseViewsRenderer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package io.micronaut.views;


import io.micronaut.core.beans.BeanMap;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.util.HashMap;
import java.util.Map;


/**
* Base views renderer interface, shared by both the synchronous and async renderers.
*/
public interface BaseViewsRenderer {

/**
* The file separator to use.
*
* @deprecated Use {@link File#separator} directly
*/
@Deprecated
String FILE_SEPARATOR = File.separator;

/**
* The extension separator.
*/
String EXTENSION_SEPARATOR = ".";

/**
* @param viewName view name to be render
* @return true if a template can be found for the supplied view name.
*/
boolean exists(@Nonnull String viewName);

/**
* Creates a view model for the given data.
* @param data The data
* @return The model
*/
default @Nonnull Map<String, Object> modelOf(@Nullable Object data) {
if (data == null) {
return new HashMap<>(0);
}
if (data instanceof Map) {
return (Map<String, Object>) data;
}
return BeanMap.of(data);
}

/**
* Returns a path with unix style folder
* separators that starts and ends with a "\".
*
* @param path The path to normalizeFile
* @deprecated Use {@link ViewUtils#normalizeFolder(String)} instead
* @return The normalized path
*/
@Nonnull
@Deprecated
default String normalizeFolder(@Nullable String path) {
return ViewUtils.normalizeFolder(path);
}

/**
* Returns a path that is converted to unix style file separators
* and never starts with a "\". If an extension is provided and the
* path ends with the extension, the extension will be stripped.
* The extension parameter supports extensions that do and do not
* begin with a ".".
*
* @param path The path to normalizeFile
* @param extension The file extension
* @deprecated Use {@link ViewUtils#normalizeFile(String, String)} instead
* @return The normalized path
*/
@Nonnull
@Deprecated
default String normalizeFile(@Nonnull String path, String extension) {
return ViewUtils.normalizeFile(path, extension);
}
}
39 changes: 26 additions & 13 deletions views-core/src/main/java/io/micronaut/views/ViewsFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
* @author Sergio del Amo
* @since 1.0
*/
@Requires(beans = ViewsRenderer.class)
@Requires(beans = BaseViewsRenderer.class)
@Filter("/**")
public class ViewsFilter implements HttpServerFilter {

Expand Down Expand Up @@ -113,26 +113,38 @@ public final Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
Optional<String> optionalView = resolveView(route, body);

if (optionalView.isPresent()) {

MediaType type = route.getValue(Produces.class, MediaType.class)
.orElse((route.getValue(View.class).isPresent() || body instanceof ModelAndView) ? MediaType.TEXT_HTML_TYPE : MediaType.APPLICATION_JSON_TYPE);
Optional<ViewsRenderer> optionalViewsRenderer = beanLocator.findBean(ViewsRenderer.class,
new ProducesMediaTypeQualifier<>(type));
.orElse((route.getValue(View.class).isPresent() ||
body instanceof ModelAndView) ?
MediaType.TEXT_HTML_TYPE :
MediaType.APPLICATION_JSON_TYPE);
Optional<BaseViewsRenderer> optionalViewsRenderer = beanLocator.findBean(
BaseViewsRenderer.class,
new ProducesMediaTypeQualifier<>(type));

if (optionalViewsRenderer.isPresent()) {
ViewsRenderer viewsRenderer = optionalViewsRenderer.get();
BaseViewsRenderer viewsRenderer = optionalViewsRenderer.get();
Map<String, Object> model = populateModel(request, viewsRenderer, body);
ModelAndView<Map<String, Object>> modelAndView = processModelAndView(request,
optionalView.get(),
model);
optionalView.get(),
model);
model = modelAndView.getModel().orElse(model);
String view = modelAndView.getView().orElse(optionalView.get());
if (viewsRenderer.exists(view)) {

Writable writable = viewsRenderer.render(view, model, request);
response.contentType(type);
((MutableHttpResponse<Object>) response).body(writable);
return Flowable.just(response);

if (viewsRenderer instanceof AsyncViewsRenderer) {
// it's an async renderer
AsyncViewsRenderer asyncRenderer = (AsyncViewsRenderer) optionalViewsRenderer.get();
return asyncRenderer.render(view, model, request, response);

} else if (viewsRenderer instanceof ViewsRenderer) {
ViewsRenderer syncRenderer = (ViewsRenderer) optionalViewsRenderer.get();
Writable writable = syncRenderer.render(view, model, request);
((MutableHttpResponse<Object>) response).body(writable);
return Flowable.just(response);

}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("view {} not found ", view);
Expand Down Expand Up @@ -174,7 +186,7 @@ protected ModelAndView<Map<String, Object>> processModelAndView(HttpRequest requ
* @param responseBody Response Body
* @return A model with the controllers response and enhanced with the decorators.
*/
protected Map<String, Object> populateModel(HttpRequest request, ViewsRenderer viewsRenderer, Object responseBody) {
protected Map<String, Object> populateModel(HttpRequest request, BaseViewsRenderer viewsRenderer, Object responseBody) {
return new HashMap<>(viewsRenderer.modelOf(resolveModel(responseBody)));
}

Expand Down Expand Up @@ -204,6 +216,7 @@ protected Object resolveModel(Object responseBody) {
@SuppressWarnings("WeakerAccess")
protected Optional<String> resolveView(AnnotationMetadata route, Object responseBody) {
Optional optionalViewName = route.getValue(View.class);

if (optionalViewName.isPresent()) {
return Optional.of((String) optionalViewName.get());
} else if (responseBody instanceof ModelAndView) {
Expand Down
75 changes: 3 additions & 72 deletions views-core/src/main/java/io/micronaut/views/ViewsRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,21 @@

package io.micronaut.views;

import io.micronaut.core.beans.BeanMap;
import io.micronaut.core.io.Writable;
import io.micronaut.http.HttpRequest;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.util.HashMap;
import java.util.Map;



/**
* Interface to be implemented by View Engines implementations.
*
* @author Sergio del Amo
* @since 1.0
*/
public interface ViewsRenderer {

/**
* The file separator to use.
*
* @deprecated Use {@link File#separator} directly
*/
@Deprecated
String FILE_SEPARATOR = File.separator;

/**
* The extension separator.
*/
String EXTENSION_SEPARATOR = ".";

public interface ViewsRenderer extends BaseViewsRenderer {
/**
* @param viewName view name to be render
* @param data response body to render it with a view
Expand All @@ -64,57 +48,4 @@ public interface ViewsRenderer {
@Nonnull HttpRequest<?> request) {
return render(viewName, data);
}

/**
* @param viewName view name to be render
* @return true if a template can be found for the supplied view name.
*/
boolean exists(@Nonnull String viewName);

/**
* Creates a view model for the given data.
* @param data The data
* @return The model
*/
default @Nonnull Map<String, Object> modelOf(@Nullable Object data) {
if (data == null) {
return new HashMap<>(0);
}
if (data instanceof Map) {
return (Map<String, Object>) data;
}
return BeanMap.of(data);
}

/**
* Returns a path with unix style folder
* separators that starts and ends with a "\".
*
* @param path The path to normalizeFile
* @deprecated Use {@link ViewUtils#normalizeFolder(String)} instead
* @return The normalized path
*/
@Nonnull
@Deprecated
default String normalizeFolder(@Nullable String path) {
return ViewUtils.normalizeFolder(path);
}

/**
* Returns a path that is converted to unix style file separators
* and never starts with a "\". If an extension is provided and the
* path ends with the extension, the extension will be stripped.
* The extension parameter supports extensions that do and do not
* begin with a ".".
*
* @param path The path to normalizeFile
* @param extension The file extension
* @deprecated Use {@link ViewUtils#normalizeFile(String, String)} instead
* @return The normalized path
*/
@Nonnull
@Deprecated
default String normalizeFile(@Nonnull String path, String extension) {
return ViewUtils.normalizeFile(path, extension);
}
}

0 comments on commit 46e261c

Please sign in to comment.