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

Fix HATEOAS Filter parameter encoding issue #177

Merged
merged 9 commits into from Jul 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -27,7 +27,7 @@ buildscript {

subprojects {

version = "2.2.4"
version = "2.2.5"

apply from: "${rootDir}/gradle/java.gradle"
apply from: "${rootDir}/gradle/groovy.gradle"
Expand Down
Expand Up @@ -95,7 +95,7 @@ class RestApiPlugin implements Plugin<Project> {
}

final String springVersion = "5.2.4.RELEASE"
final String pluginVersion = "2.2.4"
final String pluginVersion = "2.2.5"
final String libPhoneNumberVersion = "8.11.5"

project.afterEvaluate {
Expand Down
1 change: 1 addition & 0 deletions rest-api-micronaut/build.gradle
Expand Up @@ -17,6 +17,7 @@ dependencies {

testImplementation "io.micronaut:micronaut-core:${micronautVersion}"
testImplementation "io.micronaut:micronaut-router:${micronautVersion}"
testImplementation "io.reactivex.rxjava2:rxjava:2.2.12"

implementation "com.google.guava:guava:${guavaVersion}"
}
Expand Up @@ -43,8 +43,11 @@
import io.micronaut.http.filter.ServerFilterChain;
import io.micronaut.web.router.UriRouteMatch;
import io.reactivex.Flowable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -115,19 +118,20 @@ public Publisher<MutableHttpResponse<?>> doFilter(

return Flowable.fromPublisher(chain.proceed(request))
.doOnNext(
res -> {
response -> {
Optional<UriRouteMatch> potUriRouteMatch =
res.getAttributes()
response
.getAttributes()
.get(HttpAttributes.ROUTE_MATCH.toString(), UriRouteMatch.class);

if (potUriRouteMatch.isPresent()) {
UriRouteMatch uriRouteMatch = potUriRouteMatch.get();

if (uriRouteMatch.getProduces().contains(MediaType.APPLICATION_JSON_TYPE)) {

if (res.body() instanceof ResourceModel) {
if (response.body() instanceof ResourceModel) {

ResourceModel resourceModel = (ResourceModel) res.body();
ResourceModel resourceModel = (ResourceModel) response.body();
EntityModel entityModel = new EntityModel(resourceModel);

addProviderLinks(uriRouteMatch, resourceModel, entityModel);
Expand All @@ -145,39 +149,52 @@ public Publisher<MutableHttpResponse<?>> doFilter(
}
}

((MutableHttpResponse) res).body(entityModel);
((MutableHttpResponse) response).body(entityModel);

} else if (res.body() instanceof Collection || res.body() instanceof Slice) {
} else if (response.body() instanceof Collection
|| response.body() instanceof Slice) {

Iterable models;
CollectionModel collectionModel;

if (res.body() instanceof Page) {
collectionModel = new PaginationCollectionModel((Page) res.body());
models = ((Page) res.body()).getContent();
} else if (res.body() instanceof Slice) {
collectionModel = new PaginationCollectionModel((Slice) res.body());
models = ((Slice) res.body()).getContent();
if (response.body() instanceof Page) {
collectionModel = new PaginationCollectionModel((Page) response.body());
models = ((Page) response.body()).getContent();
} else if (response.body() instanceof Slice) {
collectionModel = new PaginationCollectionModel((Slice) response.body());
models = ((Slice) response.body()).getContent();
} else {
collectionModel = new CollectionModel();
models = (Collection) res.body();
models = (Collection) response.body();
}

ResourceLink collectionSelfLink = ResourceLink.selfLink(uriRouteMatch.getUri());
collectionModel.getLinks().add(addBaseUrl(collectionSelfLink));

if (collectionModel instanceof PaginationCollectionModel
&& res.body() instanceof Slice) {
&& response.body() instanceof Slice) {

String params =
StreamSupport.stream(request.getParameters().spliterator(), false)
.filter(
p -> !p.getKey().equals("page") && !p.getKey().equals("limit"))
.filter(p -> !p.getValue().isEmpty())
.map(p -> p.getKey() + "=" + p.getValue().get(0))
.map(
p -> {
String value;
try {
value =
URLEncoder.encode(
p.getValue().get(0),
StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
value = p.getValue().get(0);
}
return String.format("%s=%s", p.getKey(), value);
})
.collect(Collectors.joining("&"));

Slice slice = (Slice) res.body();
Slice slice = (Slice) response.body();

collectionModel
.getLinks()
Expand Down Expand Up @@ -261,7 +278,7 @@ public Publisher<MutableHttpResponse<?>> doFilter(
collectionModel.getData().add(entityModel);
}
}
((MutableHttpResponse) res).body(collectionModel);
((MutableHttpResponse) response).body(collectionModel);
}
}
}
Expand Down
@@ -0,0 +1,55 @@
package ch.silviowangler.rest.micronaut

import ch.silviowangler.rest.model.ResourceLink
import ch.silviowangler.rest.model.pagination.DefaultPage
import ch.silviowangler.rest.model.pagination.DefaultPageable
import ch.silviowangler.rest.model.pagination.Slice
import io.micronaut.http.HttpAttributes
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpRequestFactory
import io.micronaut.http.HttpResponseFactory
import io.micronaut.http.MediaType
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.filter.ServerFilterChain
import io.micronaut.web.router.UriRouteMatch
import io.reactivex.Flowable
import spock.lang.Specification
import spock.lang.Subject

class HateoasResponseFilterSpec extends Specification {

@Subject
HateoasResponseFilter hateoasResponseFilter = new HateoasResponseFilter([], "/api")

void "Ensure parameter encoding works as expected"() {

given: "a request"
HttpRequest<?> request = HttpRequestFactory.INSTANCE.get("/api/endpoint")
request.parameters.add("param1", "hello world")
request.parameters.add("param2", "this?is=a test")

and: "a response"
MutableHttpResponse<?> response = HttpResponseFactory.INSTANCE.ok()
Slice model = new DefaultPage([], new DefaultPageable(0, 10), 10)
UriRouteMatch uriRouteMatch = Mock()
_ * uriRouteMatch.getUri() >> "/endpoint"
_ * uriRouteMatch.getProduces() >> [MediaType.APPLICATION_JSON_TYPE]
response.attributes.put(HttpAttributes.ROUTE_MATCH.toString(), uriRouteMatch)
response.body(model)

and:
String expectedPageLink = "/api/endpoint?page=0&limit=10&param1=hello+world&param2=this%3Fis%3Da+test"

and:
ServerFilterChain chain = Mock()
_ * chain.proceed(_) >> { Flowable.just(response) }

when:
MutableHttpResponse<?> filteredResponse = Flowable.fromPublisher(hateoasResponseFilter.doFilter(request, chain)).blockingSingle()
List<ResourceLink> enrichedLinks = filteredResponse.body()["links"] as List<ResourceLink>

then: "ensure parameters are encoded correctly"
enrichedLinks.find { it["rel"] == "first" }["href"].toString() == expectedPageLink
enrichedLinks.find { it["rel"] == "last" }["href"].toString() == expectedPageLink
}
}
Expand Up @@ -24,10 +24,7 @@
package ch.silviowangler.rest.model;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -107,11 +104,6 @@ public static ResourceLink selfLink(String uri) {
}

public static ResourceLink relLink(String rel, String uri) {
try {
return new ResourceLink(
rel, "GET", URI.create(URLEncoder.encode(uri, StandardCharsets.UTF_8.toString())));
} catch (UnsupportedEncodingException e) {
return new ResourceLink(rel, "GET", URI.create(uri));
}
return new ResourceLink(rel, "GET", URI.create(uri));
}
}