Skip to content
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 #26

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8376118
Feature: Soy Views Renderer
sgammon Aug 22, 2019
fd35a07
Support for SoySauce-based rendering
sgammon Aug 22, 2019
dbaff2b
Upgrade PRNG to faster implementation
sgammon Aug 25, 2019
9914dca
Cleanup after PRNG switchover
sgammon Aug 25, 2019
86797e8
Remove Soy patch, upgrade -> 2019-09-03
sgammon Sep 4, 2019
49656f7
Cleanup docstrings, copyright headers
sgammon Sep 4, 2019
ce87dcf
Fix version tags for views-soy (since: 1.2.1)
sgammon Sep 4, 2019
a5392b8
Remove DSI util - rely on normal Random
sgammon Sep 4, 2019
7b8b8eb
Remove Soy patch in CI
sgammon Sep 4, 2019
b0bea4d
Remove Soy runtime dependencies
sgammon Sep 4, 2019
7e656ff
Add coverage to Travis via Codecov
sgammon Sep 4, 2019
33944d7
Invoke jacocoFullReport in CI
sgammon Sep 4, 2019
8094791
Remove custom repositories in views-soy
sgammon Sep 4, 2019
dec5d05
Add documentation for Soy with Micronaut
sgammon Sep 4, 2019
b6d70ba
Support injection of CSS/XID renaming maps
sgammon Aug 24, 2019
a0ba048
Finish up docs, general cleanup for Soy
sgammon Sep 5, 2019
90cc831
Add docs for CSP nonce generation
sgammon Sep 5, 2019
f471e3b
Add configurable `Random` engine, and flag to `forceSecureRandom`
sgammon Sep 5, 2019
336ec76
Fixup copyright in SoyNamingMapProvider
sgammon Sep 5, 2019
f5b55eb
Feature: Asynchronous Views
sgammon Aug 29, 2019
3a1ca78
Fix response type with cast
sgammon Sep 4, 2019
73577d2
Fix copyright headers in added files for async render
sgammon Sep 5, 2019
841ff99
Merge branch 'feature/soy'
sgammon Sep 5, 2019
6f94cc1
Merge branch 'feature/csp-nonce'
sgammon Sep 5, 2019
5a14275
Feature: Soy CSP Integration
sgammon Aug 22, 2019
c54490f
Merge branch 'feature/async'
sgammon Sep 5, 2019
e7bdafa
Convert to ByteBuf from Writable for chunked payloads
sgammon Sep 10, 2019
0f4f7bf
Feature: Reactive Soy
sgammon Nov 13, 2019
3e9cf5f
Merge branch 'master' into soy/async-chunked
sgammon Nov 13, 2019
c600bf8
Reject responses for Soy render that don't have status code 200
sgammon Nov 13, 2019
c3444f7
Merge branch 'soy/async-chunked' of github.com:bloombox/micronaut-vie…
sgammon Nov 13, 2019
8a82029
Bigger chunk sizes, swap chunk if write exceeds size
sgammon Nov 13, 2019
99470af
Remove buffer max
sgammon Nov 13, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ cache:
directories:
- "$HOME/.gradle/caches/"
- "$HOME/.gradle/wrapper/"
after_success:
- bash <(curl -s https://codecov.io/bash)
26 changes: 25 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ buildscript {
classpath 'io.github.groovylang.groovydoc:groovydoc-gradle-plugin:1.0.1'
classpath "io.micronaut.docs:micronaut-docs-asciidoc-extensions:$micronautDocsVersion"
classpath "io.micronaut.docs:micronaut-docs-gradle-plugins:$micronautDocsVersion"
classpath "com.palantir:jacoco-coverage:0.4.0"
}
}

Expand Down Expand Up @@ -58,6 +59,11 @@ ext {
version: soyVersion,
group : 'com.google.template',
name : 'soy'
],
commonsIo: [
version: commonsIoVersion,
group : 'commons-io',
name : 'commons-io'
]
]

Expand Down Expand Up @@ -88,6 +94,7 @@ subprojects { Project subproject ->

apply plugin:"groovy"
apply plugin:"java"
apply plugin:"jacoco"
apply plugin: "io.spring.dependency-management"

if (subproject.name != "views") {
Expand Down Expand Up @@ -128,6 +135,21 @@ subprojects { Project subproject ->

}

jacoco {
toolVersion = '0.8.4'
}

jacocoTestReport {
additionalSourceDirs = files(sourceSets.main.allSource.srcDirs)
sourceDirectories = files(sourceSets.main.allSource.srcDirs)
classDirectories = files(sourceSets.main.output)
reports {
html.enabled = false
xml.enabled = true
csv.enabled = false
}
}

configurations {
documentation
all {
Expand Down Expand Up @@ -189,4 +211,6 @@ allprojects {
}
}
apply from: rootProject.file('gradle/license.gradle')
}
}

apply plugin: 'com.palantir.jacoco-full-report'
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ micronautTestVersion=1.1.0
protocVersion=3.5.1
groovyVersion=2.5.6
soyVersion=2019-09-03
commonsIoVersion=2.6
title=Micronaut Views
projectDesc=Provides integration between Micronaut and server-side views technologies
projectUrl=http://micronaut.io
Expand Down
2 changes: 1 addition & 1 deletion travis-build-pr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ EXIT_STATUS=0

./gradlew --stop
./gradlew testClasses --no-daemon
./gradlew check --no-daemon || EXIT_STATUS=$?
./gradlew check jacocoFullReport --no-daemon || EXIT_STATUS=$?


exit $EXIT_STATUS
2 changes: 1 addition & 1 deletion travis-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ if [[ $EXIT_STATUS -eq 0 ]]; then
./gradlew testClasses --no-daemon || EXIT_STATUS=$?

./gradlew --stop
./gradlew check --no-daemon || EXIT_STATUS=$?
./gradlew check jacocoFullReport --no-daemon || EXIT_STATUS=$?
fi
fi

Expand Down
2 changes: 1 addition & 1 deletion views-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dependencies {

compile "io.micronaut:micronaut-runtime:$micronautVersion"
compile "io.micronaut:micronaut-http:$micronautVersion"
compile "io.micronaut:micronaut-http-server:$micronautVersion"
compile "io.micronaut:micronaut-buffer-netty:$micronautVersion"

testCompile "io.micronaut:micronaut-http-client"
testCompile "io.micronaut:micronaut-inject-groovy"
Expand Down
98 changes: 98 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,98 @@
/*
* 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.
*/
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);
}
}
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
* @since 1.3.0
*/
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 render(
@Nonnull String viewName,
@Nullable Object data,
@Nonnull HttpRequest<?> request,
@Nonnull MutableHttpResponse<?> response);
}
44 changes: 28 additions & 16 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 @@ -99,9 +99,7 @@ public int getOrder() {
}

@Override
public final Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
ServerFilterChain chain) {

public Publisher doFilter(HttpRequest<?> request, ServerFilterChain chain) {
return Flowable.fromPublisher(chain.proceed(request))
.switchMap(response -> {
Optional<AnnotationMetadata> routeMatch = response.getAttribute(HttpAttributes.ROUTE_MATCH,
Expand All @@ -113,26 +111,39 @@ 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);
if (viewsRenderer.exists(view)) {
response.contentType(type);
((MutableHttpResponse<Object>) response).body(writable);
return Flowable.just(response);

if (viewsRenderer instanceof ReactiveViewRenderer) {
// it's an async renderer
return ((ReactiveViewRenderer) viewsRenderer).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 +185,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 +215,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
Loading