diff --git a/core/spring-boot-test/build.gradle b/core/spring-boot-test/build.gradle index 156829f7063..8720ea9e605 100644 --- a/core/spring-boot-test/build.gradle +++ b/core/spring-boot-test/build.gradle @@ -27,7 +27,7 @@ dependencies { api(project(":core:spring-boot")) api("org.springframework:spring-test") - optional("tools.jackson.core:jackson-databind") + optional("com.fasterxml.jackson.core:jackson-databind") optional("com.google.code.gson:gson") optional("com.jayway.jsonpath:json-path") optional("jakarta.json.bind:jakarta.json.bind-api") @@ -45,6 +45,7 @@ dependencies { optional("org.seleniumhq.selenium:selenium-api") optional("org.skyscreamer:jsonassert") optional("org.springframework:spring-web") + optional("tools.jackson.core:jackson-databind") testImplementation(project(":test-support:spring-boot-test-support")) testImplementation("ch.qos.logback:logback-classic") diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/Jackson2Tester.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/Jackson2Tester.java new file mode 100644 index 00000000000..99cf33f8072 --- /dev/null +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/Jackson2Tester.java @@ -0,0 +1,207 @@ +/* + * Copyright 2012-present the original author or 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 org.springframework.boot.test.json; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.ResolvableType; +import org.springframework.util.Assert; + +/** + * AssertJ based JSON tester backed by Jackson 2. Usually instantiated via + * {@link #initFields(Object, ObjectMapper)}, for example:
+ * public class ExampleObjectJsonTests {
+ *
+ *     private Jackson2Tester<ExampleObject> json;
+ *
+ *     @Before
+ *     public void setup() {
+ *         ObjectMapper objectMapper = new ObjectMapper();
+ *         Jackson2Tester.initFields(this, objectMapper);
+ *     }
+ *
+ *     @Test
+ *     public void testWriteJson() throws IOException {
+ *         ExampleObject object = //...
+ *         assertThat(json.write(object)).isEqualToJson("expected.json");
+ *     }
+ *
+ * }
+ * 
+ * + * See {@link AbstractJsonMarshalTester} for more details. + * + * @param the type under test + * @author Phillip Webb + * @author Madhura Bhave + * @author Diego Berrueta + * @since 4.0.0 + * @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3. + */ +@Deprecated(since = "4.0.0", forRemoval = true) +public class Jackson2Tester extends AbstractJsonMarshalTester { + + private final ObjectMapper objectMapper; + + private @Nullable Class view; + + /** + * Create a new {@link Jackson2Tester} instance. + * @param objectMapper the Jackson object mapper + */ + protected Jackson2Tester(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "'objectMapper' must not be null"); + this.objectMapper = objectMapper; + } + + /** + * Create a new {@link Jackson2Tester} instance. + * @param resourceLoadClass the source class used to load resources + * @param type the type under test + * @param objectMapper the Jackson object mapper + */ + public Jackson2Tester(Class resourceLoadClass, ResolvableType type, ObjectMapper objectMapper) { + this(resourceLoadClass, type, objectMapper, null); + } + + /** + * Create a new {@link Jackson2Tester} instance. + * @param resourceLoadClass the source class used to load resources + * @param type the type under test + * @param objectMapper the Jackson object mapper + * @param view the JSON view + */ + public Jackson2Tester(Class resourceLoadClass, ResolvableType type, ObjectMapper objectMapper, + @Nullable Class view) { + super(resourceLoadClass, type); + Assert.notNull(objectMapper, "'objectMapper' must not be null"); + this.objectMapper = objectMapper; + this.view = view; + } + + @Override + protected JsonContent getJsonContent(String json) { + Configuration configuration = Configuration.builder() + .jsonProvider(new JacksonJsonProvider(this.objectMapper)) + .mappingProvider(new JacksonMappingProvider(this.objectMapper)) + .build(); + Class resourceLoadClass = getResourceLoadClass(); + Assert.state(resourceLoadClass != null, "'resourceLoadClass' must not be null"); + return new JsonContent<>(resourceLoadClass, getType(), json, configuration); + } + + @Override + protected T readObject(InputStream inputStream, ResolvableType type) throws IOException { + return getObjectReader(type).readValue(inputStream); + } + + @Override + protected T readObject(Reader reader, ResolvableType type) throws IOException { + return getObjectReader(type).readValue(reader); + } + + private ObjectReader getObjectReader(ResolvableType type) { + ObjectReader objectReader = this.objectMapper.readerFor(getType(type)); + if (this.view != null) { + return objectReader.withView(this.view); + } + return objectReader; + } + + @Override + protected String writeObject(T value, ResolvableType type) throws IOException { + return getObjectWriter(type).writeValueAsString(value); + } + + private ObjectWriter getObjectWriter(ResolvableType type) { + ObjectWriter objectWriter = this.objectMapper.writerFor(getType(type)); + if (this.view != null) { + return objectWriter.withView(this.view); + } + return objectWriter; + } + + private JavaType getType(ResolvableType type) { + return this.objectMapper.constructType(type.getType()); + } + + /** + * Utility method to initialize {@link Jackson2Tester} fields. See + * {@link Jackson2Tester class-level documentation} for example usage. + * @param testInstance the test instance + * @param objectMapper the JSON mapper + * @see #initFields(Object, ObjectMapper) + */ + public static void initFields(Object testInstance, ObjectMapper objectMapper) { + new Jackson2FieldInitializer().initFields(testInstance, objectMapper); + } + + /** + * Utility method to initialize {@link Jackson2Tester} fields. See + * {@link Jackson2Tester class-level documentation} for example usage. + * @param testInstance the test instance + * @param objectMapperFactory a factory to create the object mapper + * @see #initFields(Object, ObjectMapper) + */ + public static void initFields(Object testInstance, ObjectFactory objectMapperFactory) { + new Jackson2FieldInitializer().initFields(testInstance, objectMapperFactory); + } + + /** + * Returns a new instance of {@link Jackson2Tester} with the view that should be used + * for json serialization/deserialization. + * @param view the view class + * @return the new instance + */ + public Jackson2Tester forView(Class view) { + Class resourceLoadClass = getResourceLoadClass(); + ResolvableType type = getType(); + Assert.state(resourceLoadClass != null, "'resourceLoadClass' must not be null"); + Assert.state(type != null, "'type' must not be null"); + return new Jackson2Tester<>(resourceLoadClass, type, this.objectMapper, view); + } + + /** + * {@link FieldInitializer} for Jackson. + */ + private static class Jackson2FieldInitializer extends FieldInitializer { + + protected Jackson2FieldInitializer() { + super(Jackson2Tester.class); + } + + @Override + protected AbstractJsonMarshalTester createTester(Class resourceLoadClass, ResolvableType type, + ObjectMapper marshaller) { + return new Jackson2Tester<>(resourceLoadClass, type, marshaller); + } + + } + +} diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JacksonTester.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JacksonTester.java index 94f3ff93779..3870834b63b 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JacksonTester.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JacksonTester.java @@ -103,6 +103,14 @@ public class JacksonTester extends AbstractJsonMarshalTester { this(resourceLoadClass, type, jsonMapper, null); } + /** + * Create a new {@link JacksonTester} instance. + * @param resourceLoadClass the source class used to load resources + * @param type the type under test + * @param jsonMapper the Jackson JSON mapper + * @param view the JSON view + * @since 4.0.0 + */ public JacksonTester(Class resourceLoadClass, ResolvableType type, JsonMapper jsonMapper, @Nullable Class view) { super(resourceLoadClass, type); diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/json/Jackson2TesterTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/json/Jackson2TesterTests.java new file mode 100644 index 00000000000..e04b3b926b7 --- /dev/null +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/json/Jackson2TesterTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present the original author or 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 org.springframework.boot.test.json; + +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.core.ResolvableType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Jackson2Tester}. + * + * @author Phillip Webb + * @deprecated since 4.0.0 for removal in 4.2.0 in favor of JacksonTesterTests + */ +@Deprecated(since = "4.0.0", forRemoval = true) +@SuppressWarnings("removal") +class Jackson2TesterTests extends AbstractJsonMarshalTesterTests { + + @Test + @SuppressWarnings("NullAway") // Test null check + void initFieldsWhenTestIsNullShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> Jackson2Tester.initFields(null, new ObjectMapper())) + .withMessageContaining("'testInstance' must not be null"); + } + + @Test + @SuppressWarnings("NullAway") // Test null check + void initFieldsWhenMarshallerIsNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> Jackson2Tester.initFields(new InitFieldsTestClass(), (ObjectMapper) null)) + .withMessageContaining("'marshaller' must not be null"); + } + + @Test + void initFieldsShouldSetNullFields() { + InitFieldsTestClass test = new InitFieldsTestClass(); + assertThat(test.test).isNull(); + assertThat(test.base).isNull(); + Jackson2Tester.initFields(test, new ObjectMapper()); + assertThat(test.test).isNotNull(); + assertThat(test.base).isNotNull(); + ResolvableType type = test.test.getType(); + assertThat(type).isNotNull(); + assertThat(type.resolve()).isEqualTo(List.class); + assertThat(type.resolveGeneric()).isEqualTo(ExampleObject.class); + } + + @Override + protected AbstractJsonMarshalTester createTester(Class resourceLoadClass, ResolvableType type) { + return new org.springframework.boot.test.json.Jackson2Tester<>(resourceLoadClass, type, new ObjectMapper()); + } + + abstract static class InitFieldsBaseClass { + + public org.springframework.boot.test.json.@Nullable Jackson2Tester base; + + public org.springframework.boot.test.json.Jackson2Tester baseSet = new org.springframework.boot.test.json.Jackson2Tester<>( + InitFieldsBaseClass.class, ResolvableType.forClass(ExampleObject.class), new ObjectMapper()); + + } + + static class InitFieldsTestClass extends InitFieldsBaseClass { + + public org.springframework.boot.test.json.@Nullable Jackson2Tester> test; + + public org.springframework.boot.test.json.Jackson2Tester testSet = new org.springframework.boot.test.json.Jackson2Tester<>( + InitFieldsBaseClass.class, ResolvableType.forClass(ExampleObject.class), new ObjectMapper()); + + } + +} diff --git a/module/spring-boot-jackson2/src/main/java/org/springframework/boot/jackson2/autoconfigure/Jackson2TesterAutoConfiguration.java b/module/spring-boot-jackson2/src/main/java/org/springframework/boot/jackson2/autoconfigure/Jackson2TesterAutoConfiguration.java new file mode 100644 index 00000000000..09a5a100739 --- /dev/null +++ b/module/spring-boot-jackson2/src/main/java/org/springframework/boot/jackson2/autoconfigure/Jackson2TesterAutoConfiguration.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-present the original author or 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 org.springframework.boot.jackson2.autoconfigure; + +import com.fasterxml.jackson.databind.json.JsonMapper; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.test.autoconfigure.TestAutoConfiguration; +import org.springframework.boot.test.autoconfigure.json.ConditionalOnJsonTesters; +import org.springframework.boot.test.autoconfigure.json.JsonMarshalTesterRuntimeHints; +import org.springframework.boot.test.autoconfigure.json.JsonTesterFactoryBean; +import org.springframework.boot.test.json.GsonTester; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.context.annotation.Scope; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link GsonTester}. + * + * @author Phjllip Webb + * @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3. + */ +@Deprecated(since = "4.0.0", forRemoval = true) +@TestAutoConfiguration(after = Jackson2AutoConfiguration.class) +@ConditionalOnJsonTesters +@SuppressWarnings("removal") +final class Jackson2TesterAutoConfiguration { + + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + @ConditionalOnBean(JsonMapper.class) + @ImportRuntimeHints(Jackson2TesterRuntimeHints.class) + FactoryBean> jacksonTesterFactoryBean(JsonMapper mapper) { + return new JsonTesterFactoryBean<>(JacksonTester.class, mapper); + } + + static class Jackson2TesterRuntimeHints extends JsonMarshalTesterRuntimeHints { + + Jackson2TesterRuntimeHints() { + super(JacksonTester.class); + } + + } + +} diff --git a/module/spring-boot-jackson2/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.json.AutoConfigureJson.imports b/module/spring-boot-jackson2/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.json.AutoConfigureJson.imports new file mode 100644 index 00000000000..83028f13a4c --- /dev/null +++ b/module/spring-boot-jackson2/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.json.AutoConfigureJson.imports @@ -0,0 +1 @@ +org.springframework.boot.jackson.autoconfigure.Jackson2AutoConfiguration diff --git a/module/spring-boot-jackson2/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters.imports b/module/spring-boot-jackson2/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters.imports new file mode 100644 index 00000000000..a470a949079 --- /dev/null +++ b/module/spring-boot-jackson2/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters.imports @@ -0,0 +1 @@ +org.springframework.boot.jackson.autoconfigure.Jackson2TesterAutoConfiguration diff --git a/module/spring-boot-jackson2/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.json.JsonTest.includes b/module/spring-boot-jackson2/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.json.JsonTest.includes new file mode 100644 index 00000000000..707c8d545cd --- /dev/null +++ b/module/spring-boot-jackson2/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.json.JsonTest.includes @@ -0,0 +1,2 @@ +org.springframework.boot.jackson2.JsonComponent +com.fasterxml.jackson.databind.Module \ No newline at end of file