/* * Copyright 2015-2017 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 org.springframework.data.rest.webmvc.json; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.*; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.Value; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.UUID; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Reference; import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.Version; import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.webmvc.mapping.Associations; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Charsets; /** * Unit tests for {@link DomainObjectReader}. * * @author Oliver Gierke * @author Craig Andrews * @author Mathias Düsterhöft * @author Ken Dombeck */ @RunWith(MockitoJUnitRunner.class) public class DomainObjectReaderUnitTests { @Mock ResourceMappings mappings; DomainObjectReader reader; PersistentEntities entities; @Before public void setUp() { KeyValueMappingContext<?, ?> mappingContext = new KeyValueMappingContext<>(); mappingContext.getPersistentEntity(SampleUser.class); mappingContext.getPersistentEntity(Person.class); mappingContext.getPersistentEntity(TypeWithGenericMap.class); mappingContext.getPersistentEntity(VersionedType.class); mappingContext.getPersistentEntity(SampleWithCreatedDate.class); mappingContext.getPersistentEntity(SampleWithTransient.class); mappingContext.getPersistentEntity(User.class); mappingContext.getPersistentEntity(Inner.class); mappingContext.getPersistentEntity(Outer.class); mappingContext.getPersistentEntity(Parent.class); mappingContext.getPersistentEntity(Product.class); mappingContext.getPersistentEntity(TransientReadOnlyProperty.class); mappingContext.getPersistentEntity(CollectionOfEnumWithMethods.class); mappingContext.getPersistentEntity(SampleWithReference.class); mappingContext.getPersistentEntity(Note.class); mappingContext.afterPropertiesSet(); this.entities = new PersistentEntities(Collections.singleton(mappingContext)); this.reader = new DomainObjectReader(entities, new Associations(mappings, mock(RepositoryRestConfiguration.class))); } @Test // DATAREST-461 public void doesNotConsiderIgnoredProperties() throws Exception { SampleUser user = new SampleUser("firstname", "password"); JsonNode node = new ObjectMapper().readTree("{}"); SampleUser result = reader.readPut((ObjectNode) node, user, new ObjectMapper()); assertThat(result.name).isNull(); assertThat(result.password).isEqualTo("password"); } @Test // DATAREST-556 public void considersMappedFieldNamesWhenApplyingNodeToDomainObject() throws Exception { ObjectMapper mapper = new ObjectMapper(); mapper.setPropertyNamingStrategy(PropertyNamingStrategy.UPPER_CAMEL_CASE); JsonNode node = new ObjectMapper().readTree("{\"FirstName\":\"Carter\",\"LastName\":\"Beauford\"}"); Person result = reader.readPut((ObjectNode) node, new Person("Dave", "Matthews"), mapper); assertThat(result.firstName).isEqualTo("Carter"); assertThat(result.lastName).isEqualTo("Beauford"); } @Test // DATAREST-605 public void mergesMapCorrectly() throws Exception { SampleUser user = new SampleUser("firstname", "password"); user.relatedUsers = Collections.singletonMap("parent", new SampleUser("firstname", "password")); JsonNode node = new ObjectMapper() .readTree("{ \"relatedUsers\" : { \"parent\" : { \"password\" : \"sneeky\", \"name\" : \"Oliver\" } } }"); SampleUser result = reader.readPut((ObjectNode) node, user, new ObjectMapper()); // Assert that the nested Map values also consider ignored properties assertThat(result.relatedUsers.get("parent").password).isEqualTo("password"); assertThat(result.relatedUsers.get("parent").name).isEqualTo("Oliver"); } @Test // DATAREST-701 @SuppressWarnings("unchecked") public void mergesNestedMapWithoutTypeInformation() throws Exception { ObjectMapper mapper = new ObjectMapper(); JsonNode node = mapper.readTree("{\"map\" : {\"a\": \"1\", \"b\": {\"c\": \"2\"}}}"); TypeWithGenericMap target = new TypeWithGenericMap(); target.map = new HashMap<String, Object>(); target.map.put("b", new HashMap<String, Object>()); TypeWithGenericMap result = reader.readPut((ObjectNode) node, target, mapper); assertThat(result.map.get("a")).isEqualTo((Object) "1"); Object object = result.map.get("b"); assertThat(object).isInstanceOf(Map.class); assertThat(((Map<Object, Object>) object).get("c")).isEqualTo((Object) "2"); } @Test(expected = IllegalArgumentException.class) // DATAREST-701 public void rejectsMergingUnknownDomainObject() throws Exception { ObjectMapper mapper = new ObjectMapper(); ObjectNode node = (ObjectNode) mapper.readTree("{}"); reader.readPut(node, "", mapper); } @Test // DATAREST-705 public void doesNotWipeIdAndVersionPropertyForPut() throws Exception { VersionedType type = new VersionedType(); type.id = 1L; type.version = 1L; type.firstname = "Dave"; ObjectMapper mapper = new ObjectMapper(); ObjectNode node = (ObjectNode) mapper.readTree("{ \"lastname\" : \"Matthews\" }"); VersionedType result = reader.readPut(node, type, mapper); assertThat(result.lastname).isEqualTo("Matthews"); assertThat(result.firstname).isNull(); assertThat(result.id).isEqualTo(1L); assertThat(result.version).isEqualTo(1L); } @Test // DATAREST-873 public void doesNotApplyInputToReadOnlyFields() throws Exception { ObjectMapper mapper = new ObjectMapper(); ObjectNode node = (ObjectNode) mapper.readTree("{}"); Date reference = new Date(); SampleWithCreatedDate sample = new SampleWithCreatedDate(); sample.createdDate = reference; assertThat(reader.readPut(node, sample, mapper).createdDate).isEqualTo(reference); } @Test // DATAREST-931 public void readsPatchForEntityNestedInCollection() throws Exception { Phone phone = new Phone(); phone.creationDate = new GregorianCalendar(); User user = new User(); user.phones.add(phone); ByteArrayInputStream source = new ByteArrayInputStream( "{ \"phones\" : [ { \"label\" : \"some label\" } ] }".getBytes(Charsets.UTF_8)); User result = reader.read(source, user, new ObjectMapper()); assertThat(result.phones.get(0).creationDate).isNotNull(); } @Test // DATAREST-919 @SuppressWarnings("unchecked") public void readsComplexNestedMapsAndArrays() throws Exception { Map<String, Object> childMap = new HashMap<String, Object>(); childMap.put("child1", "ok"); HashMap<String, Object> nestedMap = new HashMap<String, Object>(); nestedMap.put("c1", "v1"); TypeWithGenericMap map = new TypeWithGenericMap(); map.map = new HashMap<String, Object>(); map.map.put("sub1", "ok"); map.map.put("sub2", new ArrayList<String>(Arrays.asList("ok1", "ok2"))); map.map.put("sub3", new ArrayList<Object>(Arrays.asList(childMap))); map.map.put("sub4", nestedMap); ObjectMapper mapper = new ObjectMapper(); ObjectNode payload = (ObjectNode) mapper.readTree("{ \"map\" : { \"sub1\" : \"ok\"," + " \"sub2\" : [ \"ok1\", \"ok2\" ], \"sub3\" : [ { \"childOk1\" : \"ok\" }], \"sub4\" : {" + " \"c1\" : \"v1\", \"c2\" : \"new\" } } }"); TypeWithGenericMap result = reader.readPut(payload, map, mapper); assertThat(result.map.get("sub1")).isEqualTo((Object) "ok"); List<String> sub2 = as(result.map.get("sub2"), List.class); assertThat(sub2.get(0)).isEqualTo("ok1"); assertThat(sub2.get(1)).isEqualTo("ok2"); List<Map<String, String>> sub3 = as(result.map.get("sub3"), List.class); assertThat(sub3.get(0).get("childOk1")).isEqualTo("ok"); Map<Object, String> sub4 = as(result.map.get("sub4"), Map.class); assertThat(sub4.get("c1")).isEqualTo("v1"); assertThat(sub4.get("c2")).isEqualTo("new"); } @Test // DATAREST-938 public void nestedEntitiesAreUpdated() throws Exception { Inner inner = new Inner(); inner.name = "inner name"; inner.prop = "something"; Outer outer = new Outer(); outer.prop = "else"; outer.name = "outer name"; outer.inner = inner; JsonNode node = new ObjectMapper().readTree("{ \"inner\" : { \"name\" : \"new inner name\" } }"); Outer result = reader.doMerge((ObjectNode) node, outer, new ObjectMapper()); assertThat(result).isSameAs(outer); assertThat(result.prop).isEqualTo("else"); assertThat(result.inner.prop).isEqualTo("something"); assertThat(result.inner.name).isEqualTo("new inner name"); assertThat(result.inner).isSameAs(inner); } @Test // DATAREST-937 public void considersTransientProperties() throws Exception { SampleWithTransient sample = new SampleWithTransient(); sample.name = "name"; sample.temporary = "temp"; JsonNode node = new ObjectMapper().readTree("{ \"name\" : \"new name\", \"temporary\" : \"new temp\" }"); SampleWithTransient result = reader.readPut((ObjectNode) node, sample, new ObjectMapper()); assertThat(result.name).isEqualTo("new name"); assertThat(result.temporary).isEqualTo("new temp"); } @Test // DATAREST-953 public void writesArrayForPut() throws Exception { Child inner = new Child(); inner.items = new ArrayList<Item>(); inner.items.add(new Item()); Parent source = new Parent(); source.inner = inner; JsonNode node = new ObjectMapper().readTree("{ \"inner\" : { \"items\" : [ { \"some\" : \"value\" } ] } }"); Parent result = reader.readPut((ObjectNode) node, source, new ObjectMapper()); assertThat(result.inner.items.get(0).some).isEqualTo("value"); } @Test // DATAREST-956 public void writesArrayWithAddedItemForPut() throws Exception { Child inner = new Child(); inner.items = new ArrayList<Item>(); inner.items.add(new Item()); Parent source = new Parent(); source.inner = inner; JsonNode node = new ObjectMapper().readTree("{ \"inner\" : { \"items\" : [ " + "{ \"some\" : \"value1\" }," + "{ \"some\" : \"value2\" }," + "{ \"some\" : \"value3\" } ] } }"); Parent result = reader.readPut((ObjectNode) node, source, new ObjectMapper()); assertThat(result.inner.items).hasSize(3); assertThat(result.inner.items.get(0).some).isEqualTo("value1"); assertThat(result.inner.items.get(1).some).isEqualTo("value2"); assertThat(result.inner.items.get(2).some).isEqualTo("value3"); } @Test // DATAREST-956 public void writesArrayWithRemovedItemForPut() throws Exception { Child inner = new Child(); inner.items = new ArrayList<Item>(); inner.items.add(new Item("test1")); inner.items.add(new Item("test2")); inner.items.add(new Item("test3")); Parent source = new Parent(); source.inner = inner; JsonNode node = new ObjectMapper().readTree("{ \"inner\" : { \"items\" : [ { \"some\" : \"value\" } ] } }"); Parent result = reader.readPut((ObjectNode) node, source, new ObjectMapper()); assertThat(result.inner.items).hasSize(1); assertThat(result.inner.items.get(0).some).isEqualTo("value"); } @Test // DATAREST-959 public void addsElementToPreviouslyEmptyCollection() throws Exception { Parent source = new Parent(); source.inner = new Child(); source.inner.items = null; JsonNode node = new ObjectMapper().readTree("{ \"inner\" : { \"items\" : [ { \"some\" : \"value\" } ] } }"); Parent result = reader.readPut((ObjectNode) node, source, new ObjectMapper()); assertThat(result.inner.items).hasSize(1); assertThat(result.inner.items.get(0).some).isEqualTo("value"); } @Test // DATAREST-959 @SuppressWarnings("unchecked") public void turnsObjectIntoCollection() throws Exception { Parent source = new Parent(); source.inner = new Child(); source.inner.object = new Item("value"); JsonNode node = new ObjectMapper() .readTree("{ \"inner\" : { \"object\" : [ { \"some\" : \"value\" }, { \"some\" : \"otherValue\" } ] } }"); Parent result = reader.readPut((ObjectNode) node, source, new ObjectMapper()); assertThat(result.inner.object).isInstanceOf(Collection.class); Collection<?> collection = (Collection<?>) result.inner.object; assertThat(collection).hasSize(2); Iterator<Map<String, Object>> iterator = (Iterator<Map<String, Object>>) collection.iterator(); assertThat(iterator.next().get("some")).isEqualTo((Object) "value"); assertThat(iterator.next().get("some")).isEqualTo((Object) "otherValue"); } @Test // DATAREST-965 public void writesObjectWithRemovedItemsForPut() throws Exception { Child inner = new Child(); inner.items = new ArrayList<Item>(); inner.items.add(new Item("test1")); inner.items.add(new Item("test2")); Parent source = new Parent(); source.inner = inner; JsonNode node = new ObjectMapper().readTree("{ \"inner\" : { \"object\" : \"value\" } }"); Parent result = reader.readPut((ObjectNode) node, source, new ObjectMapper()); assertThat(result.inner.items).isNull(); assertThat((String) result.inner.object).isEqualTo("value"); } @Test // DATAREST-965 public void writesArrayWithRemovedObjectForPut() throws Exception { Child inner = new Child(); inner.object = "value"; Parent source = new Parent(); source.inner = inner; JsonNode node = new ObjectMapper().readTree("{ \"inner\" : { \"items\" : [ { \"some\" : \"value\" } ] } }"); Parent result = reader.readPut((ObjectNode) node, source, new ObjectMapper()); assertThat(result.inner.items).hasSize(1); assertThat(result.inner.items.get(0).some).isEqualTo("value"); assertThat(result.inner.object).isNull(); } @Test // DATAREST-986 public void readsComplexMap() throws Exception { ObjectMapper mapper = new ObjectMapper(); JsonNode node = mapper.readTree( "{ \"map\" : { \"en\" : { \"value\" : \"eventual\" }, \"de\" : { \"value\" : \"schlussendlich\" } } }"); Product result = reader.readPut((ObjectNode) node, new Product(), mapper); assertThat(result.map.get(Locale.ENGLISH)).isEqualTo(new LocalizedValue("eventual")); assertThat(result.map.get(Locale.GERMAN)).isEqualTo(new LocalizedValue("schlussendlich")); } @Test // DATAREST-987 public void handlesTransientPropertyWithoutFieldProperly() throws Exception { ObjectMapper mapper = new ObjectMapper(); JsonNode node = mapper.readTree("{ \"name\" : \"Foo\" }"); reader.readPut((ObjectNode) node, new TransientReadOnlyProperty(), mapper); } @Test // DATAREST-977 public void readsCollectionOfComplexEnum() throws Exception { CollectionOfEnumWithMethods sample = new CollectionOfEnumWithMethods(); sample.enums.add(SampleEnum.FIRST); ObjectMapper mapper = new ObjectMapper(); JsonNode node = mapper.readTree("{ \"enums\" : [ \"SECOND\", \"FIRST\" ] }"); CollectionOfEnumWithMethods result = reader.merge((ObjectNode) node, sample, mapper); assertThat(result.enums, contains(SampleEnum.SECOND, SampleEnum.FIRST)); } @Test // DATAREST-944 public void mergesAssociations() { List<Nested> originalCollection = Arrays.asList(new Nested(2, 3)); SampleWithReference source = new SampleWithReference(Arrays.asList(new Nested(1, 2), new Nested(2, 3))); SampleWithReference target = new SampleWithReference(originalCollection); SampleWithReference result = reader.mergeForPut(source, target, new ObjectMapper()); assertThat(result.nested).isEqualTo(source.nested); assertThat(result.nested == originalCollection).isFalse(); } @Test // DATAREST-944 public void mergesAssociationsAndKeepsMutableCollection() { ArrayList<Nested> originalCollection = new ArrayList<Nested>(Arrays.asList(new Nested(2, 3))); SampleWithReference source = new SampleWithReference( new ArrayList<Nested>(Arrays.asList(new Nested(1, 2), new Nested(2, 3)))); SampleWithReference target = new SampleWithReference(originalCollection); SampleWithReference result = reader.mergeForPut(source, target, new ObjectMapper()); assertThat(result.nested).isEqualTo(source.nested); assertThat(result.nested).isSameAs(originalCollection); } @Test // DATAREST-1030 public void patchWithReferenceToRelatedEntityIsResolvedCorrectly() throws Exception { Associations associations = mock(Associations.class); PersistentProperty<?> any = ArgumentMatchers.any(PersistentProperty.class); when(associations.isLinkableAssociation(any)).thenReturn(true); DomainObjectReader reader = new DomainObjectReader(entities, associations); Tag first = new Tag(); Tag second = new Tag(); Note note = new Note(); note.tags.add(first); note.tags.add(second); SimpleModule module = new SimpleModule(); module.addDeserializer(Tag.class, new SelectValueByIdSerializer<Tag>(Collections.singletonMap(second.id, second))); ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(module); ObjectNode readTree = (ObjectNode) mapper.readTree(String.format("{ \"tags\" : [ \"%s\"]}", second.id)); Note result = reader.doMerge(readTree, note, mapper); assertThat(result.tags).contains(second); } @SuppressWarnings("unchecked") private static <T> T as(Object source, Class<T> type) { assertThat(source).isInstanceOf(type); return (T) source; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class SampleUser { String name; @JsonIgnore String password; Map<String, SampleUser> relatedUsers; public SampleUser(String name, String password) { this.name = name; this.password = password; } protected SampleUser() {} } // DATAREST-556 @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class Person { String firstName, lastName; public Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } protected Person() {} } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class TypeWithGenericMap { Map<String, Object> map; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class VersionedType { @Id Long id; @Version Long version; String firstname, lastname; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class SampleWithCreatedDate { @CreatedDate // @ReadOnlyProperty // Date createdDate; } static class User { public List<Phone> phones = new ArrayList<Phone>(); } static class Phone { public Calendar creationDate; public String label; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class SampleWithTransient { String name; @Transient String temporary; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class Outer { String name; String prop; Inner inner; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class Inner { String name; String prop; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class Parent { Child inner; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class Child { List<Item> items; Object object; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) @NoArgsConstructor @AllArgsConstructor static class Item { String some; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class Product { Map<Locale, LocalizedValue> map = new HashMap<Locale, LocalizedValue>(); } @JsonAutoDetect(fieldVisibility = Visibility.ANY) @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode static class LocalizedValue { String value; } @JsonAutoDetect(getterVisibility = Visibility.ANY) static class TransientReadOnlyProperty { @Transient public String getName() { return null; } public void setName(String name) {} } // DATAREST-977 interface EnumInterface { String getFoo(); } static enum SampleEnum implements EnumInterface { FIRST { @Override public String getFoo() { return "first"; } }, SECOND { public String getFoo() { return "second"; } }; } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class CollectionOfEnumWithMethods { List<SampleEnum> enums = new ArrayList<SampleEnum>(); } @Value static class SampleWithReference { @Reference List<Nested> nested; } @Value static class Nested { int x, y; } // DATAREST-1030 @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class Note { @Id UUID id = UUID.randomUUID(); @Reference List<Tag> tags = new ArrayList<Tag>(); } @JsonAutoDetect(fieldVisibility = Visibility.ANY) static class Tag { @Id UUID id = UUID.randomUUID(); String name; } @RequiredArgsConstructor static class SelectValueByIdSerializer<T> extends JsonDeserializer<T> { private final Map<? extends Object, T> values; /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) */ @Override public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { String text = p.getText(); return values.entrySet().stream()// .filter(it -> it.getKey().toString().equals(text))// .map(it -> it.getValue())// .findFirst().orElse(null); } } }