-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: Reactive Soy (v2) #34
base: 6.0.x
Are you sure you want to change the base?
Changes from all commits
ba3bcc7
2daa496
2a68655
55268ec
b7be919
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/* | ||
* Copyright 2017-2019 original authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package 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. | ||
* | ||
* @see ViewsRenderer for synchronous view rendering | ||
* @see ReactiveViewRenderer for async/reactive view rendering | ||
* @author Sam Gammon (sam@bloombox.io) | ||
* @since 1.3.2 | ||
*/ | ||
public interface BaseViewsRenderer { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. introduces new |
||
|
||
/** | ||
* 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* Copyright 2017-2019 original authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package io.micronaut.views; | ||
|
||
import io.micronaut.http.HttpRequest; | ||
import io.micronaut.http.MutableHttpResponse; | ||
import io.reactivex.Flowable; | ||
|
||
import javax.annotation.Nonnull; | ||
import javax.annotation.Nullable; | ||
|
||
|
||
/** | ||
* Reactive 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 view rendering supports such signals. | ||
* | ||
* @see ViewsRenderer for the synchronous version | ||
* @author Sam Gammon (sam@bloombox.io) | ||
* @since 1.3.2 | ||
*/ | ||
public interface ReactiveViewRenderer extends BaseViewsRenderer { | ||
/** | ||
* @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 assembled so far. | ||
* @return A writable where the view will be written to. | ||
*/ | ||
@Nonnull | ||
Flowable<MutableHttpResponse<?>> render( | ||
@Nonnull String viewName, | ||
@Nullable Object data, | ||
@Nonnull HttpRequest<?> request, | ||
@Nonnull MutableHttpResponse<Object> response); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,7 +46,7 @@ | |
* @author Sergio del Amo | ||
* @since 1.0 | ||
*/ | ||
@Requires(beans = ViewsRenderer.class) | ||
@Requires(beans = BaseViewsRenderer.class) | ||
@Filter("/**") | ||
public class ViewsFilter implements HttpServerFilter { | ||
|
||
|
@@ -100,49 +100,67 @@ public int getOrder() { | |
} | ||
|
||
@Override | ||
public final Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, | ||
ServerFilterChain chain) { | ||
|
||
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) { | ||
return Flowable.fromPublisher(chain.proceed(request)) | ||
.switchMap(response -> { | ||
Optional<AnnotationMetadata> routeMatch = response.getAttribute(HttpAttributes.ROUTE_MATCH, | ||
AnnotationMetadata.class); | ||
if (routeMatch.isPresent()) { | ||
AnnotationMetadata route = routeMatch.get(); | ||
|
||
Object body = response.body(); | ||
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)); | ||
|
||
if (optionalViewsRenderer.isPresent()) { | ||
ViewsRenderer viewsRenderer = optionalViewsRenderer.get(); | ||
Map<String, Object> model = populateModel(request, viewsRenderer, body); | ||
ModelAndView<Map<String, Object>> modelAndView = processModelAndView(request, | ||
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); | ||
} else { | ||
return Flowable.error(new ViewNotFoundException("View [" + view + "] does not exist")); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return Flowable.just(response); | ||
}); | ||
.switchMap(response -> { | ||
Optional<AnnotationMetadata> routeMatch = response.getAttribute(HttpAttributes.ROUTE_MATCH, | ||
AnnotationMetadata.class); | ||
if (routeMatch.isPresent()) { | ||
AnnotationMetadata route = routeMatch.get(); | ||
|
||
Object body = response.body(); | ||
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<BaseViewsRenderer> optionalViewsRenderer = beanLocator.findBean( | ||
BaseViewsRenderer.class, | ||
new ProducesMediaTypeQualifier<>(type)); | ||
|
||
if (optionalViewsRenderer.isPresent()) { | ||
BaseViewsRenderer viewsRenderer = optionalViewsRenderer.get(); | ||
Map<String, Object> model = populateModel(request, viewsRenderer, body); | ||
ModelAndView<Map<String, Object>> modelAndView = processModelAndView(request, | ||
optionalView.get(), | ||
model); | ||
model = modelAndView.getModel().orElse(model); | ||
String view = modelAndView.getView().orElse(optionalView.get()); | ||
|
||
if (viewsRenderer.exists(view)) { | ||
response.contentType(type); | ||
try { | ||
if (viewsRenderer instanceof ReactiveViewRenderer) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in |
||
// it's an async renderer | ||
return ((ReactiveViewRenderer) viewsRenderer).render( | ||
view, model, request, ((MutableHttpResponse<Object>) 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); | ||
|
||
} | ||
} catch (ViewNotFoundException vne) { | ||
LOG.error(String.format("failed to resolve view: %s", view)); | ||
return Flowable.just(HttpResponse.serverError()); | ||
} | ||
} else { | ||
if (LOG.isDebugEnabled()) { | ||
LOG.debug("view {} not found ", view); | ||
} | ||
return Flowable.just(HttpResponse.serverError()); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return Flowable.just(response); | ||
}); | ||
} | ||
|
||
/** | ||
|
@@ -172,7 +190,9 @@ 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))); | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
upgrades soy to latest version