Skip to content

Commit

Permalink
Merge branch '4.7.x' into 4.8.x
Browse files Browse the repository at this point in the history
  • Loading branch information
sdelamo committed Jan 31, 2025
2 parents be6262e + cfeff23 commit 48bcb55
Show file tree
Hide file tree
Showing 30 changed files with 795 additions and 153 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
public class JavaModelUtils {

public static final Map<String, String> NAME_TO_TYPE_MAP = new HashMap<>();
public static final Map<String, Type> NAME_TO_REAL_TYPE_MAP = new HashMap<>();
private static final ElementKind RECORD_KIND = ReflectionUtils.findDeclaredField(ElementKind.class, "RECORD").flatMap(field -> {
try {
return Optional.of((ElementKind) field.get(ElementKind.class));
Expand All @@ -62,6 +63,15 @@ public class JavaModelUtils {
JavaModelUtils.NAME_TO_TYPE_MAP.put("double", "D");
JavaModelUtils.NAME_TO_TYPE_MAP.put("float", "F");
JavaModelUtils.NAME_TO_TYPE_MAP.put("short", "S");
JavaModelUtils.NAME_TO_REAL_TYPE_MAP.put("void", Type.VOID_TYPE);
JavaModelUtils.NAME_TO_REAL_TYPE_MAP.put("boolean", Type.BOOLEAN_TYPE);
JavaModelUtils.NAME_TO_REAL_TYPE_MAP.put("char", Type.CHAR_TYPE);
JavaModelUtils.NAME_TO_REAL_TYPE_MAP.put("int", Type.INT_TYPE);
JavaModelUtils.NAME_TO_REAL_TYPE_MAP.put("byte", Type.BYTE_TYPE);
JavaModelUtils.NAME_TO_REAL_TYPE_MAP.put("long", Type.LONG_TYPE);
JavaModelUtils.NAME_TO_REAL_TYPE_MAP.put("double", Type.DOUBLE_TYPE);
JavaModelUtils.NAME_TO_REAL_TYPE_MAP.put("float", Type.FLOAT_TYPE);
JavaModelUtils.NAME_TO_REAL_TYPE_MAP.put("short", Type.SHORT_TYPE);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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.forms;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.tck.AssertionUtils;
import io.micronaut.http.tck.HttpResponseAssertion;
import io.micronaut.http.tck.TestScenario;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
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 FormBindingUsingMethodParametersTest {
private static final String SPEC_NAME = "FormBindingUsingMethodParametersTest";

@Test
public void formBindingUsingMethodParameters() throws IOException {
String body = "title=Building+Microservices&pages=100";
String expectedJson = "{\"title\":\"Building Microservices\",\"pages\":100}";
assertWithBody(body, expectedJson);

body = "title=Building+Microservices&pages=";
expectedJson = "{\"title\":\"Building Microservices\"}";
assertWithBody(body, expectedJson);
}

private static void assertWithBody(String body, String expectedJson) throws IOException {
TestScenario.builder()
.specName(SPEC_NAME)
.request(HttpRequest.POST("/book/save", body).contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE))
.assertion((server, request) ->
AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder()
.status(HttpStatus.OK)
.assertResponse(httpResponse -> {
Optional<String> bodyOptional = httpResponse.getBody(String.class);
assertTrue(bodyOptional.isPresent());
assertEquals(expectedJson, bodyOptional.get());
})
.build()))
.run();
}

@Requires(property = "spec.name", value = SPEC_NAME)
@Controller("/book")
static class SaveController {
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Post("/save")
Book save(String title, @Nullable Integer pages) {
return new Book(title, pages);
}
}

@Introspected
record Book(@NonNull String title, @Nullable Integer pages) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,19 @@ public Object visitType(TypeMirror t, Object o) {
String className = JavaModelUtils.getClassName(element);
resolvedValue = new AnnotationClassValue<>(className);
}
} else {
resolvedValue = switch (t.getKind()) {
case BOOLEAN -> new AnnotationClassValue<>(boolean.class);
case BYTE -> new AnnotationClassValue<>(byte.class);
case SHORT -> new AnnotationClassValue<>(short.class);
case INT -> new AnnotationClassValue<>(int.class);
case LONG -> new AnnotationClassValue<>(long.class);
case CHAR -> new AnnotationClassValue<>(char.class);
case FLOAT -> new AnnotationClassValue<>(float.class);
case DOUBLE -> new AnnotationClassValue<>(double.class);
case VOID -> new AnnotationClassValue<>(void.class);
default -> null;
};
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,45 @@ import java.lang.annotation.Retention
*/
class AnnotationMetadataWriterSpec extends AbstractTypeElementSpec {

void "test primitive classes in metadata"() {
given:
def annotationMetadata = buildTypeAnnotationMetadata("""
package inneranntest;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.*;
import io.micronaut.inject.annotation.Outer;
@io.micronaut.inject.annotation.MyAnnotationX(
clazz1 = void.class,
clazz2 = int.class,
clazz3 = long.class,
clazz4 = byte.class,
clazz5 = boolean.class,
clazz6 = char.class,
clazz7 = float.class,
clazz8 = double.class,
clazz9 = short.class
)
class Test {
}
""")

annotationMetadata = writeAndLoadMetadata('annmetadatatest.Test', annotationMetadata)

expect:
annotationMetadata.classValue(MyAnnotationX, "clazz1").get() == void
annotationMetadata.classValue(MyAnnotationX, "clazz2").get() == int
annotationMetadata.classValue(MyAnnotationX, "clazz3").get() == long
annotationMetadata.classValue(MyAnnotationX, "clazz4").get() == byte
annotationMetadata.classValue(MyAnnotationX, "clazz5").get() == boolean
annotationMetadata.classValue(MyAnnotationX, "clazz6").get() == char
annotationMetadata.classValue(MyAnnotationX, "clazz7").get() == float
annotationMetadata.classValue(MyAnnotationX, "clazz8").get() == double
annotationMetadata.classValue(MyAnnotationX, "clazz9").get() == short
}

void "test inner annotations in metadata"() {
given:
def annotationMetadata = buildTypeAnnotationMetadata("""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.micronaut.inject.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target({ElementType.TYPE})
public @interface MyAnnotationX {

Class<?> clazz1();

Class<?> clazz2();

Class<?> clazz3();

Class<?> clazz4();

Class<?> clazz5();

Class<?> clazz6();

Class<?> clazz7();

Class<?> clazz8();

Class<?> clazz9();

}
10 changes: 5 additions & 5 deletions src/main/docs/guide/httpServer/formData.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ To make data binding model customizations consistent between form data and JSON,

The advantage of this approach is that the same Jackson annotations used for customizing JSON binding can be used for form submissions.

In practice this means that to bind regular form data, the only change required to the previous JSON binding code is updating the api:http.MediaType[] consumed:
In practice this means that to bind regular form data, the only change required to the <<bindingUsingPOJOs, previous JSON binding code>> is updating the api:http.MediaType[] consumed:

snippet::io.micronaut.docs.server.json.PersonController[tags="class,regular,endclass", indent=0, title="Binding Form Data to POJOs"]
snippet::io.micronaut.docs.server.form.PersonController[tags="class,formbinding,endclass", title="Binding Form Data to POJOs"]

TIP: To avoid denial of service attacks, collection types and arrays created during binding are limited by the setting `jackson.arraySizeThreshold` in your configuration file (e.g `application.yml`)
TIP: To avoid denial-of-service attacks, collection types and arrays created during binding are limited by the setting `jackson.arraySizeThreshold` in your configuration file (e.g `application.yml`)

Alternatively, instead of using a POJO you can bind form data directly to method parameters (which works with JSON too!):

snippet::io.micronaut.docs.server.json.PersonController[tags="class,args,endclass", indent=0, title="Binding Form Data to Parameters"]
snippet::io.micronaut.docs.server.form.PersonController[tags="class,formsaveWithArgs,endclass", title="Binding Form Data to Parameters"]

As you can see from the example above, this approach lets you use features such as support for link:{jdkapi}/java.base/java/util/Optional.html[Optional] types and restrict the parameters to be bound. When using POJOs you must be careful to use Jackson annotations to exclude properties that should not be bound.
As you can see from the example above, this approach lets you use features such as support for `@Nullable` or link:{jdkapi}/java.base/java/util/Optional.html[Optional] types and restrict the parameters to be bound. When using POJOs you must be careful to use Jackson annotations to exclude properties that should not be bound.
147 changes: 0 additions & 147 deletions src/main/docs/guide/httpServer/jsonBinding.adoc
Original file line number Diff line number Diff line change
@@ -1,154 +1,7 @@
:jackson-annotations: https://fasterxml.github.io/jackson-annotations/javadoc/2.9/
:jackson-databind: https://fasterxml.github.io/jackson-databind/javadoc/2.9/
:jackson-core: https://fasterxml.github.io/jackson-core/javadoc/2.9/

The most common data interchange format nowadays is JSON.

By default, the api:http.annotation.Controller[] annotation specifies that the controllers in Micronaut framework consume and produce JSON by default.

Since Micronaut Framework 4.0, users must choose how they want to serialize (Jackson Databind or Micronaut Serialization). Both approaches allow the usage of https://micronaut-projects.github.io/micronaut-serialization/latest/guide/index.html#jacksonAnnotations[Jackson Annotations].

With either approach, the Micronaut framework reads incoming JSON in a non-blocking manner.

== Serialize using Micronaut Serialization

https://micronaut-projects.github.io/micronaut-serialization/latest/guide/index.html#quickStart[Micronaut Serialization] offers reflection-free serialization using build-time <<introspection, Bean Introspections>>. It supports alternative formats such as https://micronaut-projects.github.io/micronaut-serialization/latest/guide/index.html#jsonpQuick[JSON-P or JSON-B]. You need to add the following dependencies:

dependency:micronaut-serde-processor[groupId=io.micronaut.serde,scope=annotationProcessor]
dependency:micronaut-serde-jackson[groupId=io.micronaut.serde]

== Serialization using Jackson Databind

To serialize using https://github.com/FasterXML/jackson[Jackson] Databind include the following dependency:

dependency:micronaut-jackson-databind[]

== JsonMapper

You may be used to work with https://fasterxml.github.io/jackson-databind/javadoc/2.7/com/fasterxml/jackson/databind/ObjectMapper.html[Jackson's `ObjectMapper`]. However, we don't recommend using Jackson's `ObjectMapper` directly; instead you should use api:json.JsonMapper[], an API almost identical to Jackson's `ObjectMapper`. Moreover, both <<jsonBinding, Micronaut Serialization and Micronaut Jackson Databind>> implement api:json.JsonMapper[].

You can inject a bean of type `JsonMapper` or manually instantiate one via `JsonMapper.createDefault()`.

== Binding using Reactive Frameworks

From a developer perspective however, you can generally just work with Plain Old Java Objects (POJOs) and can optionally use a Reactive framework such as https://github.com/ReactiveX/RxJava[RxJava] or https://projectreactor.io[Project Reactor]. The following is an example of a controller that reads and saves an incoming POJO in a non-blocking way from JSON:

snippet::io.micronaut.docs.server.json.PersonController[tags="class,single,endclass", indent=0, title="Using Reactive Streams to Read the JSON"]

<1> The method receives a `Publisher` which emits the POJO once the JSON has been read
<2> The `map` method stores the instance in a `Map`
<3> An api:http.HttpResponse[] is returned

Using cURL from the command line, you can POST JSON to the `/people` URI:

.Using cURL to Post JSON
----
$ curl -X POST localhost:8080/people -d '{"firstName":"Fred","lastName":"Flintstone","age":45}'
----

== Binding Using CompletableFuture

The same method as the previous example can also be written with the link:{jdkapi}/java.base/java/util/concurrent/CompletableFuture.html[CompletableFuture] API instead:

snippet::io.micronaut.docs.server.json.PersonController[tags="class,future,endclass", indent=0, title="Using CompletableFuture to Read the JSON"]

The above example uses the `thenApply` method to achieve the same as the previous example.

== Binding using POJOs

Note however you can just as easily write:

snippet::io.micronaut.docs.server.json.PersonController[tags="class,regular,endclass", indent=0, title="Binding JSON POJOs"]

The Micronaut framework only executes your method once the data has been read in a non-blocking manner.

TIP: You can customize the output in various ways, such as using https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations[Jackson annotations].

== Jackson Configuration

If you use <<jsonBinding, `micronaut-jackson-databind`>>, the Jackson's `ObjectMapper` can be configured through configuration with the api:io.micronaut.jackson.JacksonConfiguration[] class.

All Jackson configuration keys start with `jackson`.

|=======
| dateFormat | String | The date format
| locale | String | Uses link:{jdkapi}/java.base/java/util/Locale.html#forLanguageTag-java.lang.String-[Locale.forLanguageTag]. Example: `en-US`
| timeZone | String |Uses link:{jdkapi}/java.base/java/util/TimeZone.html#getTimeZone-java.lang.String-[TimeZone.getTimeZone]. Example: `PST`
| serializationInclusion | String | One of link:{jackson-annotations}com/fasterxml/jackson/annotation/JsonInclude.Include.html[JsonInclude.Include]. Example: `ALWAYS`
| propertyNamingStrategy | String | Name of an instance of link:{jackson-databind}com/fasterxml/jackson/databind/PropertyNamingStrategy.html[PropertyNamingStrategy]. Example: `SNAKE_CASE`
| defaultTyping | String | The global defaultTyping for polymorphic type handling from enum link:{jackson-databind}com/fasterxml/jackson/databind/ObjectMapper.DefaultTyping.html[ObjectMapper.DefaultTyping]. Example: `NON_FINAL`
|=======

Example:

[configuration]
----
jackson:
serializationInclusion: ALWAYS
----

=== Features

If you use <<jsonBinding, `micronaut-jackson-databind`>>, all Jackson's features can be configured with their name as the key and a boolean to indicate enabled or disabled.

|======
|serialization | Map | link:{jackson-databind}com/fasterxml/jackson/databind/SerializationFeature.html[SerializationFeature]
|deserialization | Map | link:{jackson-databind}com/fasterxml/jackson/databind/DeserializationFeature.html[DeserializationFeature]
|mapper | Map | link:{jackson-databind}com/fasterxml/jackson/databind/MapperFeature.html[MapperFeature]
|parser | Map | link:{jackson-core}com/fasterxml/jackson/core/JsonParser.Feature.html[JsonParser.Feature]
|generator | Map | link:{jackson-core}com/fasterxml/jackson/core/JsonGenerator.Feature.html[JsonGenerator.Feature]
|factory | Map | link:{jackson-core}com/fasterxml/jackson/core/JsonFactory.Feature.html[JsonFactory.Feature]
|======

Example:

[configuration]
----
jackson:
serialization:
indentOutput: true
writeDatesAsTimestamps: false
deserialization:
useBigIntegerForInts: true
failOnUnknownProperties: false
----

=== Further customising `JsonFactory`

If you use <<jsonBinding, `micronaut-jackson-databind`>>, there may be situations where you wish to customise the `JsonFactory` used by the `ObjectMapper` beyond the configuration of features (for example to allow custom character escaping).
This can be achieved by providing your own `JsonFactory` bean, or by providing a `BeanCreatedEventListener<JsonFactory>` which configures the default bean on startup.

=== Support for `@JsonView`

If you use <<jsonBinding, `micronaut-jackson-databind`>>, you can use the `@JsonView` annotation on controller methods if you set `jackson.json-view.enabled` to `true` in your configuration file (e.g `application.yml`).

Jackson's `@JsonView` annotation lets you control which properties are exposed on a per-response basis. See https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON Views] for more information.

=== Beans

If you use <<jsonBinding, `micronaut-jackson-databind`>>, in addition to configuration, beans can be registered to customize Jackson. All beans that extend any of the following classes are registered with the object mapper:

* link:{jackson-databind}com/fasterxml/jackson/databind/Module.html[Module]
* link:{jackson-databind}com/fasterxml/jackson/databind/JsonDeserializer.html[JsonDeserializer]
* link:{jackson-databind}com/fasterxml/jackson/databind/JsonSerializer.html[JsonSerializer]
* link:{jackson-databind}com/fasterxml/jackson/databind/KeyDeserializer.html[KeyDeserializer]
* link:{jackson-databind}com/fasterxml/jackson/databind/deser/BeanDeserializerModifier.html[BeanDeserializerModifier]
* link:{jackson-databind}com/fasterxml/jackson/databind/ser/BeanSerializerModifier.html[BeanSerializerModifier]

=== Service Loader

Any modules registered via the service loader are also added to the default object mapper.

=== Number Precision

During JSON parsing, the framework may convert any incoming data to an intermediate object model. By default, this model uses `BigInteger`, `long` and `double` for numeric values. This means some information that could be represented by `BigDecimal` may be lost. For example, numbers with many decimal places that cannot be represented by `double` may be truncated, even if the target type for deserialization uses `BigDecimal`. Metadata on the number of trailing zeroes (`BigDecimal.precision()`), e.g. the difference between `0.12` and `0.120`, is also discarded.

If you need full accuracy for number types, use the following configuration:

[configuration]
----
jackson:
deserialization:
useBigIntegerForInts: true
useBigDecimalForFloats: true
----
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The same method as the previous example can also be written with the link:{jdkapi}/java.base/java/util/concurrent/CompletableFuture.html[CompletableFuture] API instead:

snippet::io.micronaut.docs.server.json.PersonController[tags="class,future,endclass", indent=0, title="Using CompletableFuture to Read the JSON"]

The above example uses the `thenApply` method to achieve the same as the previous example.
Loading

0 comments on commit 48bcb55

Please sign in to comment.