Skip to content

Commit

Permalink
Correct custom media-type resolving (micronaut-projects#11102)
Browse files Browse the repository at this point in the history
  • Loading branch information
dstepanov authored Aug 21, 2024
1 parent 8ebc767 commit 777c0ff
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,7 @@ private void encodeHttpResponse(
}
if (messageBodyWriter == null || !responseBodyType.isInstance(body) || !messageBodyWriter.isWriteable(responseBodyType, responseMediaType)) {
responseBodyType = Argument.ofInstance(body);
messageBodyWriter = this.messageBodyHandlerRegistry
.findWriter(responseBodyType, List.of(responseMediaType))
.orElse(null);
messageBodyWriter = this.messageBodyHandlerRegistry.getWriter(responseBodyType, List.of(responseMediaType));
}
NettyBodyWriter<Object> closure = wrap(messageBodyWriter);
closeConnectionIfError(response, nettyRequest, outboundAccess);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2017-2023 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
*
* https://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.http.server.tck.tests.codec;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.tck.AssertionUtils;
import io.micronaut.http.tck.BodyAssertion;
import io.micronaut.http.tck.HttpResponseAssertion;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;

import static io.micronaut.http.tck.TestScenario.asserts;
import static org.junit.jupiter.api.Assertions.assertTrue;


@SuppressWarnings({
"java:S5960", // We're allowed assertions, as these are used in tests only
"checkstyle:MissingJavadocType",
"checkstyle:DesignForExtension"
})
public class JsonCodecAdditionalType2Test {
public static final String SPEC_NAME = "JsonCodecAdditionalType2Test";
public static final String CUSTOM_MEDIA_TYPE = "application/json+feed";

@Test
void itIsPossibleToCanRegisterAdditionTypesForJsonCodec() throws IOException {
assertRequest("/json-additional-codec");
assertRequest("/json-additional-codec/pojo");
}

private void assertRequest(String uri) throws IOException {
HttpResponseAssertion assertion = HttpResponseAssertion.builder()
.body(BodyAssertion.builder().body("https://jsonfeed.org").contains())
.status(HttpStatus.OK)
.assertResponse(response -> assertTrue(response.header("Content-Type").contains(CUSTOM_MEDIA_TYPE)))
.build();
Map<String, Object> config = Collections.singletonMap("micronaut.codec.json.additional-types", Collections.singletonList(CUSTOM_MEDIA_TYPE));
asserts(SPEC_NAME,
config,
HttpRequest.GET(uri).header(HttpHeaders.ACCEPT, CUSTOM_MEDIA_TYPE),
(server, request) -> AssertionUtils.assertDoesNotThrow(server, request, assertion));
}

@Requires(property = "spec.name", value = SPEC_NAME)
@Controller
static class JsonFeedController {

@Produces(CUSTOM_MEDIA_TYPE)
@Get("/json-additional-codec")
String index() {
return """
{
"version": "https://jsonfeed.org/version/1",
"title": "My Example Feed",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
]
}\
""";
}

@Produces(CUSTOM_MEDIA_TYPE)
@Get("/json-additional-codec/pojo")
JsonFeed pojo() {
return new JsonFeed("https://jsonfeed.org/version/1", "My Example Feed", "https://example.org/", "https://example.org/feed.json");
}
}

@Introspected
static class JsonFeed {
private final String version;
private final String title;
@JsonProperty("home_page_url")
private final String homePageUrl;
@JsonProperty("feed_url")
private final String feedUrl;

public JsonFeed(String version, String title, String homePageUrl, String feedUrl) {
this.version = version;
this.title = title;
this.homePageUrl = homePageUrl;
this.feedUrl = feedUrl;
}

public String getVersion() {
return version;
}

public String getTitle() {
return title;
}

public String getHomePageUrl() {
return homePageUrl;
}

public String getFeedUrl() {
return feedUrl;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,27 +67,20 @@ public final class DefaultMessageBodyHandlerRegistry extends AbstractMessageBody
@SuppressWarnings({"unchecked"})
@Override
protected <T> MessageBodyReader<T> findReaderImpl(Argument<T> type, List<MediaType> mediaTypes) {
List<MediaType> resolvedMediaTypes = resolveMediaTypes(mediaTypes);
return beanLocator.getBeansOfType(
Argument.of(MessageBodyReader.class), // Select all readers and eliminate by the type later
Qualifiers.byQualifiers(
// Filter by media types first before filtering by the type hierarchy
newMediaTypeQualifier(Argument.of(MessageBodyReader.class, type), mediaTypes, Consumes.class),
new MediaTypeQualifier<>(Argument.of(MessageBodyReader.class, type), resolvedMediaTypes, Consumes.class),
MatchArgumentQualifier.covariant(MessageBodyReader.class, type)
)
).stream()
.filter(reader -> mediaTypes.stream().anyMatch(mediaType -> reader.isReadable(type, mediaType)))
.filter(reader -> resolvedMediaTypes.stream().anyMatch(mediaType -> reader.isReadable(type, mediaType)))
.findFirst()
.orElse(null);
}

@NonNull
private <T, B> MediaTypeQualifier<B> newMediaTypeQualifier(Argument<T> type,
List<MediaType> mediaTypes,
Class<? extends Annotation> qualifierType) {
List<MediaType> resolvedMediaTypes = resolveMediaTypes(mediaTypes);
return new MediaTypeQualifier<>(type, resolvedMediaTypes, qualifierType);
}

@NonNull
private List<MediaType> resolveMediaTypes(List<MediaType> mediaTypes) {
if (codecConfigurations.isEmpty()) {
Expand All @@ -112,15 +105,16 @@ private List<MediaType> resolveMediaTypes(List<MediaType> mediaTypes) {
@SuppressWarnings({"unchecked"})
@Override
protected <T> MessageBodyWriter<T> findWriterImpl(Argument<T> type, List<MediaType> mediaTypes) {
List<MediaType> resolvedMediaTypes = resolveMediaTypes(mediaTypes);
return beanLocator.getBeansOfType(
Argument.of(MessageBodyWriter.class), // Select all writers and eliminate by the type later
Qualifiers.byQualifiers(
// Filter by media types first before filtering by the type hierarchy
newMediaTypeQualifier(Argument.of(MessageBodyWriter.class, type), mediaTypes, Produces.class),
new MediaTypeQualifier<>(Argument.of(MessageBodyWriter.class, type), resolvedMediaTypes, Produces.class),
MatchArgumentQualifier.contravariant(MessageBodyWriter.class, type)
)
).stream()
.filter(writer -> mediaTypes.stream().anyMatch(mediaType -> writer.isWriteable(type, mediaType)))
.filter(writer -> resolvedMediaTypes.stream().anyMatch(mediaType -> writer.isWriteable(type, mediaType)))
.findFirst().orElse(null);
}

Expand Down

0 comments on commit 777c0ff

Please sign in to comment.