From 7e50e0b612c04598c3ce940a8a9d4c91dde9a609 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 26 Apr 2022 15:14:37 +0200 Subject: [PATCH] Add example showing `PropertyValueConverter` usage. Closes #637 --- mongodb/example/README.md | 7 +- mongodb/example/pom.xml | 4 + .../mongodb/converters/Address.java | 48 ++++++++ .../converters/AddressDocumentConverter.java | 48 ++++++++ .../converters/AddressJsonConverter.java | 51 +++++++++ .../converters/ApplicationConfiguration.java | 28 +++++ .../mongodb/converters/Customer.java | 53 +++++++++ .../converters/CustomerIntegrationTests.java | 105 ++++++++++++++++++ .../mongodb/converters/package-info.java | 4 + 9 files changed, 345 insertions(+), 3 deletions(-) create mode 100644 mongodb/example/src/main/java/example/springdata/mongodb/converters/Address.java create mode 100644 mongodb/example/src/main/java/example/springdata/mongodb/converters/AddressDocumentConverter.java create mode 100644 mongodb/example/src/main/java/example/springdata/mongodb/converters/AddressJsonConverter.java create mode 100644 mongodb/example/src/main/java/example/springdata/mongodb/converters/ApplicationConfiguration.java create mode 100644 mongodb/example/src/main/java/example/springdata/mongodb/converters/Customer.java create mode 100644 mongodb/example/src/test/java/example/springdata/mongodb/converters/CustomerIntegrationTests.java create mode 100644 mongodb/example/src/test/java/example/springdata/mongodb/converters/package-info.java diff --git a/mongodb/example/README.md b/mongodb/example/README.md index 7447a93b..0287fef1 100644 --- a/mongodb/example/README.md +++ b/mongodb/example/README.md @@ -2,9 +2,10 @@ This project contains examples for: -* Derived finder and geo spatial queries using a `Repository` interface. +* Derived query method and geospatial queries using a `Repository` interface. +* Usage of property-specific converters. * `EntityCallback` API usage for before convert/save interaction. -* Result projections for DTOs and interface types. -* Query metadata. +* Result projections for DTOs and interface types. +* Query metadata. * Unwrapping entities into the parent document. diff --git a/mongodb/example/pom.xml b/mongodb/example/pom.xml index af43a806..db1fcd5e 100644 --- a/mongodb/example/pom.xml +++ b/mongodb/example/pom.xml @@ -46,6 +46,10 @@ + + com.fasterxml.jackson.core + jackson-databind + org.springframework.data.examples spring-data-mongodb-example-utils diff --git a/mongodb/example/src/main/java/example/springdata/mongodb/converters/Address.java b/mongodb/example/src/main/java/example/springdata/mongodb/converters/Address.java new file mode 100644 index 00000000..af6180cc --- /dev/null +++ b/mongodb/example/src/main/java/example/springdata/mongodb/converters/Address.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2021 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 example.springdata.mongodb.converters; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +import org.springframework.data.geo.Point; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A domain object to capture addresses. + * + * @author Mark Paluch + */ +@Data +@RequiredArgsConstructor +public class Address { + + private final Point location; + private String street; + private String zipCode; + + Address(@JsonProperty("location") Map location, @JsonProperty("street") String street, + @JsonProperty("zipCode") String zipCode) { + + this.location = new Point(location.get("x"), location.get("y")); + this.street = street; + this.zipCode = zipCode; + } + +} diff --git a/mongodb/example/src/main/java/example/springdata/mongodb/converters/AddressDocumentConverter.java b/mongodb/example/src/main/java/example/springdata/mongodb/converters/AddressDocumentConverter.java new file mode 100644 index 00000000..e9a54489 --- /dev/null +++ b/mongodb/example/src/main/java/example/springdata/mongodb/converters/AddressDocumentConverter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 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 + * + * http://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 example.springdata.mongodb.converters; + +import org.bson.Document; + +import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.convert.MongoValueConverter; + +/** + * {@link MongoValueConverter} to write only the {@link Address#location} into a MongoDB {@link Document}. + * + * @author Mark Paluch + */ +class AddressDocumentConverter implements MongoValueConverter { + + @Override + public Address read(Document value, MongoConversionContext context) { + + Point point = new Point(context.read(value.get("x"), Double.class), context.read(value.get("y"), Double.class)); + + return new Address(point); + } + + @Override + public Document write(Address value, MongoConversionContext context) { + + Document document = new Document(); + document.put("x", value.getLocation().getX()); + document.put("y", value.getLocation().getY()); + + return document; + } +} diff --git a/mongodb/example/src/main/java/example/springdata/mongodb/converters/AddressJsonConverter.java b/mongodb/example/src/main/java/example/springdata/mongodb/converters/AddressJsonConverter.java new file mode 100644 index 00000000..7d4d356a --- /dev/null +++ b/mongodb/example/src/main/java/example/springdata/mongodb/converters/AddressJsonConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 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 + * + * http://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 example.springdata.mongodb.converters; + +import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.convert.MongoValueConverter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * {@link MongoValueConverter} to write {@link Address} into JSON through Jackson. + * + * @author Mark Paluch + */ +class AddressJsonConverter implements MongoValueConverter { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + public Address read(String value, MongoConversionContext context) { + + try { + return mapper.readValue(value, Address.class); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot unmarshal Address JSON", e); + } + } + + @Override + public String write(Address value, MongoConversionContext context) { + try { + return mapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot marshal Address into JSON", e); + } + } +} diff --git a/mongodb/example/src/main/java/example/springdata/mongodb/converters/ApplicationConfiguration.java b/mongodb/example/src/main/java/example/springdata/mongodb/converters/ApplicationConfiguration.java new file mode 100644 index 00000000..7197406a --- /dev/null +++ b/mongodb/example/src/main/java/example/springdata/mongodb/converters/ApplicationConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2021 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 example.springdata.mongodb.converters; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Test configuration. + * + * @author Mark Paluch + */ +@SpringBootApplication +class ApplicationConfiguration { + +} diff --git a/mongodb/example/src/main/java/example/springdata/mongodb/converters/Customer.java b/mongodb/example/src/main/java/example/springdata/mongodb/converters/Customer.java new file mode 100644 index 00000000..1155951f --- /dev/null +++ b/mongodb/example/src/main/java/example/springdata/mongodb/converters/Customer.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2021 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 example.springdata.mongodb.converters; + +import lombok.Data; + +import org.springframework.data.convert.ValueConverter; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.util.Assert; + +/** + * An entity to represent a customer. + * + * @author Mark Paluch + */ +@Data +@Document +public class Customer { + + private String id, firstname, lastname; + + @ValueConverter(AddressJsonConverter.class) private Address primary; + + @ValueConverter(AddressDocumentConverter.class) private Address secondary; + + /** + * Creates a new {@link Customer} with the given firstname and lastname. + * + * @param firstname must not be {@literal null} or empty. + * @param lastname must not be {@literal null} or empty. + */ + public Customer(String firstname, String lastname) { + + Assert.hasText(firstname, "Firstname must not be null or empty!"); + Assert.hasText(lastname, "Lastname must not be null or empty!"); + + this.firstname = firstname; + this.lastname = lastname; + } +} diff --git a/mongodb/example/src/test/java/example/springdata/mongodb/converters/CustomerIntegrationTests.java b/mongodb/example/src/test/java/example/springdata/mongodb/converters/CustomerIntegrationTests.java new file mode 100644 index 00000000..70f368b1 --- /dev/null +++ b/mongodb/example/src/test/java/example/springdata/mongodb/converters/CustomerIntegrationTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2014-2021 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 example.springdata.mongodb.converters; + +import static org.assertj.core.api.Assertions.*; + +import example.springdata.mongodb.util.MongoContainers; + +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; +import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** + * Integration tests for {@link Customer} conversions. + * + * @author Mark Paluch + */ +@Testcontainers +@DataMongoTest +class CustomerIntegrationTests { + + @Container // + private static MongoDBContainer mongoDBContainer = MongoContainers.getDefaultContainer(); + + @DynamicPropertySource + static void setProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl); + } + + @Autowired MongoOperations operations; + + @BeforeEach + void setUp() { + operations.remove(Customer.class).all(); + } + + /** + * Applies {@link org.springframework.data.mongodb.core.convert.MongoValueConverter} to individual properties. + * {@code primary} and {@code secondary} are configured with different converters of which the first one uses JSON + * conversion and the second converter interacts directly with a MongoDB {@link Document}. + */ + @Test + void appliesConverters() { + + var primaryAddress = new Address(new Point(4.1, 5.6)); + primaryAddress.setStreet("308 Negra Arroyo Lane"); + primaryAddress.setZipCode("87104"); + + var secondaryAddress = new Address(new Point(6.1, 7.6)); + secondaryAddress.setStreet("3828 Piermont Drive"); + secondaryAddress.setZipCode("87111"); + + var customer = new Customer("Walter", "White"); + customer.setPrimary(primaryAddress); + customer.setSecondary(secondaryAddress); + + operations.insert(customer); + + Document document = operations.findOne(new Query(), Document.class, "customer"); + + assertThat(document.get("primary")) + .isEqualTo("{\"location\":{\"x\":4.1,\"y\":5.6},\"street\":\"308 Negra Arroyo Lane\",\"zipCode\":\"87104\"}"); + assertThat(document.get("secondary")).isEqualTo( + new Document("x", secondaryAddress.getLocation().getX()).append("y", secondaryAddress.getLocation().getY())); + + Customer loadedCustomer = operations.findOne(new Query(), Customer.class, "customer"); + + // Primary address marshalled as JSON + assertThat(loadedCustomer.getPrimary()).isNotNull(); + assertThat(loadedCustomer.getPrimary().getStreet()).isEqualTo(primaryAddress.getStreet()); + assertThat(loadedCustomer.getPrimary().getZipCode()).isEqualTo(primaryAddress.getZipCode()); + assertThat(loadedCustomer.getPrimary().getLocation()).isEqualTo(primaryAddress.getLocation()); + + // Secondary address stores only location as the converter considers only points + assertThat(loadedCustomer.getSecondary()).isNotNull(); + assertThat(loadedCustomer.getSecondary().getStreet()).isNull(); + assertThat(loadedCustomer.getSecondary().getLocation()).isEqualTo(secondaryAddress.getLocation()); + + } +} diff --git a/mongodb/example/src/test/java/example/springdata/mongodb/converters/package-info.java b/mongodb/example/src/test/java/example/springdata/mongodb/converters/package-info.java new file mode 100644 index 00000000..f2c107aa --- /dev/null +++ b/mongodb/example/src/test/java/example/springdata/mongodb/converters/package-info.java @@ -0,0 +1,4 @@ +/** + * Package showing converter usage with MongoDB entities. + */ +package example.springdata.mongodb.converters;