Skip to content

Commit

Permalink
imp: build route with a media type other than JSON (#9756)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdelamo authored Aug 22, 2023
1 parent 8dc34c5 commit c23c3cc
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 2 deletions.
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ managed-netty-http3 = "0.0.16.Final"
managed-netty-tcnative = "2.0.61.Final"
managed-reactive-streams = "1.0.4"
# This should be kept aligned with https://github.com/micronaut-projects/micronaut-reactor/blob/master/gradle.properties from the BOM
managed-reactor = "3.5.8"
managed-reactor = "3.5.9"
managed-snakeyaml = "2.0"
managed-java-parser-core = "3.25.4"
managed-ksp = "1.8.21-1.0.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,19 +399,39 @@ protected UriRoute buildRoute(HttpMethod httpMethod, String uri, MethodExecution
return buildRoute(httpMethod.name(), httpMethod, uri, executableHandle);
}

/**
* Build a route.
*
* @param httpMethod The HTTP method
* @param uri The URI
* @param mediaTypes The media types
* @param executableHandle The executable handle
*
* @since 4.2.0
* @return an {@link UriRoute}
*/
protected UriRoute buildRoute(HttpMethod httpMethod, String uri, List<MediaType> mediaTypes, MethodExecutionHandle<Object, Object> executableHandle) {
return buildRoute(httpMethod.name(), httpMethod, uri, mediaTypes, executableHandle);
}

private UriRoute buildRoute(String httpMethodName, HttpMethod httpMethod, String uri, MethodExecutionHandle<Object, Object> executableHandle) {
return buildRoute(httpMethodName, httpMethod, uri, List.of(MediaType.APPLICATION_JSON_TYPE), executableHandle);
}

private UriRoute buildRoute(String httpMethodName, HttpMethod httpMethod, String uri, List<MediaType> mediaTypes, MethodExecutionHandle<Object, Object> executableHandle) {
UriRoute route;
if (currentParentRoute != null) {
route = new DefaultUriRoute(
httpMethod,
currentParentRoute.uriMatchTemplate.nest(uri),
mediaTypes,
executableHandle,
httpMethodName,
conversionService
);
currentParentRoute.nestedRoutes.add((DefaultUriRoute) route);
} else {
route = new DefaultUriRoute(httpMethod, uri, executableHandle, httpMethodName, conversionService);
route = new DefaultUriRoute(httpMethod, uri, mediaTypes, executableHandle, httpMethodName, conversionService);
}

this.uriRoutes.add(route);
Expand Down Expand Up @@ -799,6 +819,23 @@ final class DefaultUriRoute extends AbstractRoute implements UriRoute {
this(httpMethod, new UriMatchTemplate(uriTemplate), Collections.singletonList(mediaType), targetMethod, httpMethodName, conversionService);
}

/**
* @param httpMethod The HTTP method
* @param uriTemplate The URI Template as a {@link CharSequence}
* @param mediaTypes The Media types
* @param targetMethod The target method execution handle
* @param httpMethodName The actual name of the method - may differ from {@link HttpMethod#name()} for non-standard http methods
* @param conversionService The conversion service
*/
DefaultUriRoute(HttpMethod httpMethod,
CharSequence uriTemplate,
List<MediaType> mediaTypes,
MethodExecutionHandle<Object, Object> targetMethod,
String httpMethodName,
ConversionService conversionService) {
this(httpMethod, new UriMatchTemplate(uriTemplate), mediaTypes, targetMethod, httpMethodName, conversionService);
}

/**
* @param httpMethod The HTTP method
* @param uriTemplate The URI Template as a {@link UriMatchTemplate}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package io.micronaut.context.router;

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.BeanContext;
import io.micronaut.context.ExecutionHandleLocator;
import io.micronaut.context.annotation.Executable;
import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.*;
import io.micronaut.http.annotation.*;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.inject.ExecutionHandle;
import io.micronaut.inject.MethodExecutionHandle;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.web.router.DefaultRouteBuilder;
import jakarta.inject.Singleton;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.junit.jupiter.api.Test;
import spock.lang.Specification;

import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@Property(name = "micronaut.http.client.follow-redirects", value = StringUtils.FALSE)
@Property(name = "spec.name", value = "RouteBuilderMediaTypeSpec")
@MicronautTest
class RouteBuilderMediaTypeTest extends Specification {

@Test
void createTest(@Client("/") HttpClient httpClient) {
BlockingHttpClient client = httpClient.toBlocking();
HttpRequest<?> request = HttpRequest.GET("/contact/create").accept(MediaType.TEXT_HTML);
HttpResponse<String> responseHtml = assertDoesNotThrow(() -> client.exchange(request, String.class));
assertTrue(responseHtml.getContentType().isPresent());
assertEquals(MediaType.TEXT_HTML_TYPE, responseHtml.getContentType().get());
String html = responseHtml.body();
assertNotNull(html);
}

@Test
void saveTest(@Client("/") HttpClient httpClient) {
BlockingHttpClient client = httpClient.toBlocking();
HttpRequest<?> request = HttpRequest.POST(UriBuilder.of("/contact").path("save").build(),
Map.of("firstName", "Sergio", "lastName", "del Amo"))
.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
HttpResponse<?> response = assertDoesNotThrow(() -> client.exchange(request, String.class));
assertEquals(HttpStatus.SEE_OTHER, response.getStatus());
assertEquals("/foo", response.getHeaders().get(HttpHeaders.LOCATION));
}

@Requires(property = "spec.name", value = "RouteBuilderMediaTypeSpec")
@Singleton
static class CreateSaveRouteBuilder extends DefaultRouteBuilder {

CreateSaveRouteBuilder(ExecutionHandleLocator executionHandleLocator,
BeanContext beanContext,
List<ContactController> contactControllerList) {
super(executionHandleLocator);
for (ContactController controller : contactControllerList) {
beanContext.getBeanDefinition(ContactController.class);
BeanDefinition<ContactController> bd = beanContext.getBeanDefinition(ContactController.class);
bd.findMethod("create", HttpRequest.class).ifPresent(m -> {
MethodExecutionHandle<Object, Object> executionHandle = ExecutionHandle.of(controller, (ExecutableMethod) m);
buildRoute(HttpMethod.GET, "/contact/create", executionHandle);
});
bd.findMethod("save", HttpRequest.class, Contact.class).ifPresent(m -> {
MethodExecutionHandle<Object, Object> executionHandle = ExecutionHandle.of(controller, (ExecutableMethod) m);
buildRoute(HttpMethod.POST, "/contact/save", Collections.singletonList(MediaType.APPLICATION_FORM_URLENCODED_TYPE), executionHandle);
});
}
}
}


@Requires(property = "spec.name", value = "RouteBuilderMediaTypeSpec")
@Singleton
static class ContactController {

@Produces(MediaType.TEXT_HTML)
@Get
@Executable
String create(HttpRequest request) {
return "<!DOCTYPE html><html><body></body></html>";
}

@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Post
@Executable
HttpResponse<?> save(HttpRequest request, @NotNull @Valid @Body Contact form) {
return HttpResponse.seeOther(URI.create("/foo"));
}
}

@Introspected
record Contact(String firstName, String lastName) {

}

}

0 comments on commit c23c3cc

Please sign in to comment.