Improve JSON Patch implementation.
Refactor JSON Patch application implementation to improve the property detection for which values are supposed to be set. Fixes #2177.
This commit is contained in:
@@ -36,6 +36,8 @@ import org.springframework.data.rest.core.support.EntityLookup;
|
||||
import org.springframework.data.rest.webmvc.PersistentEntityResource;
|
||||
import org.springframework.data.rest.webmvc.RootResourceInformation;
|
||||
import org.springframework.data.rest.webmvc.json.DomainObjectReader;
|
||||
import org.springframework.data.rest.webmvc.json.BindContextFactory;
|
||||
import org.springframework.data.rest.webmvc.json.patch.TestPropertyPathContext;
|
||||
import org.springframework.data.rest.webmvc.support.BackendIdHandlerMethodArgumentResolver;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -55,6 +57,8 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
*/
|
||||
class PersistentEntityResourceHandlerMethodArgumentResolverUnitTests {
|
||||
|
||||
private static final BindContextFactory FACTORY = mapper -> TestPropertyPathContext.INSTANCE;
|
||||
|
||||
HttpMessageConverter<?> converter;
|
||||
RootResourceInformationHandlerMethodArgumentResolver rootResourceResolver;
|
||||
BackendIdHandlerMethodArgumentResolver backendIdResolver;
|
||||
@@ -79,7 +83,7 @@ class PersistentEntityResourceHandlerMethodArgumentResolverUnitTests {
|
||||
|
||||
PersistentEntityResourceHandlerMethodArgumentResolver argumentResolver = new PersistentEntityResourceHandlerMethodArgumentResolver(
|
||||
Arrays.<HttpMessageConverter<?>> asList(converter), rootResourceResolver, backendIdResolver, reader,
|
||||
PluginRegistry.empty());
|
||||
PluginRegistry.empty(), FACTORY);
|
||||
|
||||
HttpServletRequest request = new MockHttpServletRequest("PUT", "/foo/4711");
|
||||
|
||||
@@ -103,7 +107,7 @@ class PersistentEntityResourceHandlerMethodArgumentResolverUnitTests {
|
||||
|
||||
PersistentEntityResourceHandlerMethodArgumentResolver argumentResolver = new PersistentEntityResourceHandlerMethodArgumentResolver(
|
||||
Arrays.<HttpMessageConverter<?>> asList(converter), rootResourceResolver, backendIdResolver, reader,
|
||||
PluginRegistry.of(Arrays.asList(lookup)));
|
||||
PluginRegistry.of(Arrays.asList(lookup)), FACTORY);
|
||||
|
||||
HttpServletRequest request = new MockHttpServletRequest("PUT", "/foo/someName");
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.springframework.data.mapping.PersistentEntity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty.Access;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -91,7 +92,7 @@ class MappedPropertiesUnitTests {
|
||||
|
||||
MappedProperties properties = MappedProperties.forDeserialization(entity, mapper);
|
||||
|
||||
assertThat(properties.isWritableProperty("anotherReadOnlyProperty")).isFalse();
|
||||
assertThat(properties.isWritableField("anotherReadOnlyProperty")).isFalse();
|
||||
assertThat(properties.getPersistentProperty("readOnlyProperty")).isNull();
|
||||
|
||||
properties = MappedProperties.forSerialization(entity, mapper);
|
||||
@@ -107,12 +108,12 @@ class MappedPropertiesUnitTests {
|
||||
|
||||
MappedProperties properties = MappedProperties.forDeserialization(entity, mapper);
|
||||
|
||||
assertThat(properties.isWritableProperty("someProperty")).isTrue();
|
||||
assertThat(properties.isWritableProperty("readOnlyProperty")).isFalse();
|
||||
assertThat(properties.isWritableProperty("anotherReadOnlyProperty")).isFalse();
|
||||
assertThat(properties.isWritableField("someProperty")).isTrue();
|
||||
assertThat(properties.isWritableField("readOnlyProperty")).isFalse();
|
||||
assertThat(properties.isWritableField("anotherReadOnlyProperty")).isFalse();
|
||||
|
||||
// Due to @JsonAnySetter
|
||||
assertThat(properties.isWritableProperty("someRandomProperty")).isTrue();
|
||||
assertThat(properties.isWritableField("someRandomProperty")).isTrue();
|
||||
}
|
||||
|
||||
@Test // #2130
|
||||
@@ -120,6 +121,13 @@ class MappedPropertiesUnitTests {
|
||||
assertThat(properties.getIgnoredProperties()).contains("notExposedByJackson");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresTypeLevelProperties() {
|
||||
|
||||
assertThat(properties.getIgnoredProperties()).contains("typeLevelIgnored");
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties("typeLevelIgnored")
|
||||
static class Sample {
|
||||
|
||||
public @Transient String notExposedBySpringData;
|
||||
@@ -128,6 +136,7 @@ class MappedPropertiesUnitTests {
|
||||
public @JsonProperty("email") String emailAddress;
|
||||
public @JsonProperty(access = Access.READ_ONLY) String readOnlyProperty;
|
||||
public @ReadOnlyProperty String anotherReadOnlyProperty;
|
||||
public String typeLevelIgnored;
|
||||
}
|
||||
|
||||
static class SampleWithJsonAnySetter {
|
||||
|
||||
@@ -40,7 +40,7 @@ class AddOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
AddOperation add = AddOperation.of("/1/complete", true);
|
||||
add.perform(todos, Todo.class);
|
||||
add.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.get(1).isComplete()).isTrue();
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class AddOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
AddOperation add = AddOperation.of("/1/description", "BBB");
|
||||
add.perform(todos, Todo.class);
|
||||
add.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.get(1).getDescription()).isEqualTo("BBB");
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class AddOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
AddOperation add = AddOperation.of("/1", new Todo(null, "D", true));
|
||||
add.perform(todos, Todo.class);
|
||||
add.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.size()).isEqualTo(4);
|
||||
assertThat(todos.get(0).getDescription()).isEqualTo("A");
|
||||
@@ -86,7 +86,7 @@ class AddOperationUnitTests {
|
||||
|
||||
Todo todo = new Todo(1L, "description", false);
|
||||
|
||||
AddOperation.of("/items/-", "Some text.").perform(todo, Todo.class);
|
||||
AddOperation.of("/items/-", "Some text.").perform(todo, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todo.getItems().get(0)).isEqualTo("Some text.");
|
||||
}
|
||||
@@ -100,7 +100,7 @@ class AddOperationUnitTests {
|
||||
JsonNode node = mapper.readTree("\"Some text.\"");
|
||||
JsonLateObjectEvaluator evaluator = new JsonLateObjectEvaluator(mapper, node);
|
||||
|
||||
AddOperation.of("/items/-", evaluator).perform(todo, Todo.class);
|
||||
AddOperation.of("/items/-", evaluator).perform(todo, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todo.getItems().get(0)).isEqualTo("Some text.");
|
||||
}
|
||||
@@ -110,7 +110,7 @@ class AddOperationUnitTests {
|
||||
|
||||
Todo todo = new Todo(1L, "description", false);
|
||||
|
||||
AddOperation.of("/uninitialized/-", "Text").perform(todo, Todo.class);
|
||||
AddOperation.of("/uninitialized/-", "Text").perform(todo, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todo.getUninitialized()).containsExactly("Text");
|
||||
}
|
||||
@@ -122,7 +122,7 @@ class AddOperationUnitTests {
|
||||
todos.add(new Todo(1L, "A", false));
|
||||
|
||||
Todo todo = new Todo(2L, "B", true);
|
||||
AddOperation.of("/1", todo).perform(todos, Todo.class);
|
||||
AddOperation.of("/1", todo).perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos).element(1).isEqualTo(todo);
|
||||
}
|
||||
@@ -134,7 +134,8 @@ class AddOperationUnitTests {
|
||||
todos.add(new Todo(1L, "A", false));
|
||||
|
||||
assertThatExceptionOfType(PatchException.class) //
|
||||
.isThrownBy(() -> AddOperation.of("/2", new Todo(2L, "B", true)).perform(todos, Todo.class)) //
|
||||
.isThrownBy(() -> AddOperation.of("/2", new Todo(2L, "B", true)).perform(todos, Todo.class,
|
||||
TestPropertyPathContext.INSTANCE)) //
|
||||
.withMessageContaining("index") //
|
||||
.withMessageContaining("2") //
|
||||
.withMessageContaining("1");
|
||||
@@ -152,7 +153,8 @@ class AddOperationUnitTests {
|
||||
TodoListWrapper outer = new TodoListWrapper(todoList);
|
||||
|
||||
Todo newTodo = new Todo(3L, "C", false);
|
||||
AddOperation.of("/todoList/todos/-", newTodo).perform(outer, TodoListWrapper.class);
|
||||
AddOperation.of("/todoList/todos/-", newTodo).perform(outer, TodoListWrapper.class,
|
||||
TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(outer.todoList.getTodos()).containsExactly(todos.get(0), todos.get(1), newTodo);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
*/
|
||||
package org.springframework.data.rest.webmvc.json.patch;
|
||||
|
||||
import static java.util.function.Predicate.not;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -33,7 +35,7 @@ class CopyOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
CopyOperation copy = CopyOperation.from("/0/complete").to("/1/complete");
|
||||
copy.perform(todos, Todo.class);
|
||||
copy.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.get(1).isComplete()).isTrue();
|
||||
}
|
||||
@@ -47,7 +49,7 @@ class CopyOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
CopyOperation copy = CopyOperation.from("/0/description").to("/1/description");
|
||||
copy.perform(todos, Todo.class);
|
||||
copy.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.get(1).getDescription()).isEqualTo("A");
|
||||
}
|
||||
@@ -61,7 +63,7 @@ class CopyOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
CopyOperation copy = CopyOperation.from("/0/complete").to("/1/description");
|
||||
copy.perform(todos, Todo.class);
|
||||
copy.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.get(1).getDescription()).isEqualTo("true");
|
||||
}
|
||||
@@ -75,7 +77,7 @@ class CopyOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
CopyOperation copy = CopyOperation.from("/1").to("/0");
|
||||
copy.perform(todos, Todo.class);
|
||||
copy.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.size()).isEqualTo(4);
|
||||
assertThat(todos.get(0).getId().longValue()).isEqualTo(2L); // NOTE: This could be problematic if you try to save it
|
||||
@@ -94,7 +96,7 @@ class CopyOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
CopyOperation copy = CopyOperation.from("/0").to("/2");
|
||||
copy.perform(todos, Todo.class);
|
||||
copy.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.size()).isEqualTo(4);
|
||||
assertThat(todos.get(2).getId().longValue()).isEqualTo(1L); // NOTE: This could be problematic if you try to save it
|
||||
@@ -113,7 +115,7 @@ class CopyOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
CopyOperation copy = CopyOperation.from("/0").to("/3");
|
||||
copy.perform(todos, Todo.class);
|
||||
copy.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.size()).isEqualTo(4);
|
||||
assertThat(todos.get(3).getId().longValue()).isEqualTo(1L); // NOTE: This could be problematic if you try to save it
|
||||
@@ -132,7 +134,7 @@ class CopyOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
CopyOperation copy = CopyOperation.from("/0").to("/-");
|
||||
copy.perform(todos, Todo.class);
|
||||
copy.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.size()).isEqualTo(4);
|
||||
assertThat(todos.get(3)).isEqualTo(new Todo(1L, "A", true)); // NOTE: This could be problematic if you try to save
|
||||
@@ -148,10 +150,48 @@ class CopyOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
CopyOperation copy = CopyOperation.from("/-").to("/0");
|
||||
copy.perform(todos, Todo.class);
|
||||
copy.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.size()).isEqualTo(4);
|
||||
assertThat(todos.get(0)).isEqualTo(new Todo(3L, "C", false)); // NOTE: This could be problematic if you try to save
|
||||
// it to a DB because there'll be duplicate IDs
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCopyingFromHiddenProperty() {
|
||||
|
||||
BindContext context = new TestPropertyPathContext() {
|
||||
|
||||
@Override
|
||||
public Optional<String> getReadableProperty(String segment, Class<?> type) {
|
||||
return Optional.of(segment).filter(not("description"::equals));
|
||||
}
|
||||
};
|
||||
|
||||
CopyOperation operation = CopyOperation.from("/description").to("/description");
|
||||
Todo target = new Todo(1L, "Description", false);
|
||||
|
||||
assertThatExceptionOfType(PatchException.class)
|
||||
.isThrownBy(() -> operation.perform(target, Todo.class, context))
|
||||
.withMessageContaining("readable property");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCopyingToHiddenProperty() {
|
||||
|
||||
BindContext context = new TestPropertyPathContext() {
|
||||
|
||||
@Override
|
||||
public Optional<String> getWritableProperty(String segment, Class<?> type) {
|
||||
return Optional.of(segment).filter(not("description"::equals));
|
||||
}
|
||||
};
|
||||
|
||||
CopyOperation operation = CopyOperation.from("/description").to("/description");
|
||||
Todo target = new Todo(1L, "Description", false);
|
||||
|
||||
assertThatExceptionOfType(PatchException.class)
|
||||
.isThrownBy(() -> operation.perform(target, Todo.class, context))
|
||||
.withMessageContaining("writable property");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ class JsonPatchUnitTests {
|
||||
|
||||
ClassPathResource resource = new ClassPathResource(jsonPatchFile, getClass());
|
||||
JsonNode node = new ObjectMapper().readValue(resource.getInputStream(), JsonNode.class);
|
||||
Patch patch = new JsonPatchPatchConverter(new ObjectMapper()).convert(node);
|
||||
Patch patch = new JsonPatchPatchConverter(new ObjectMapper(), TestPropertyPathContext.INSTANCE).convert(node);
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* 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.data.rest.webmvc.json.patch;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.data.rest.webmvc.json.BindContextFactory;
|
||||
import org.springframework.data.rest.webmvc.json.PersistentEntitiesBindContextFactory;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link JsonPointerMapping}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class JsonPointerMappingTests {
|
||||
|
||||
JsonPointerMapping verifier;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
|
||||
KeyValueMappingContext<?, ?> context = new KeyValueMappingContext<>();
|
||||
context.getPersistentEntity(Sample.class);
|
||||
|
||||
PersistentEntities entities = new PersistentEntities(Arrays.asList(context));
|
||||
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
this.verifier = new JsonPointerMapping(factory.getBindContextFor(mapper));
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifiesSimpleProperty() {
|
||||
verifier.forRead("/firstname", Sample.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifiesPathIntoCollection() {
|
||||
verifier.forRead("/collection/27/firstname", Sample.class);
|
||||
}
|
||||
|
||||
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
|
||||
static class Sample {
|
||||
String firstname;
|
||||
Collection<Sample> collection;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class MoveOperationUnitTests {
|
||||
MoveOperation move = MoveOperation.from("/0/complete").to("/1/complete");
|
||||
|
||||
assertThatExceptionOfType(PatchException.class)
|
||||
.isThrownBy(() -> move.perform(todos, Todo.class))
|
||||
.isThrownBy(() -> move.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE))
|
||||
.withMessage("Path '/0/complete' is not nullable");
|
||||
|
||||
assertThat(todos.get(1).isComplete()).isFalse();
|
||||
@@ -50,7 +50,7 @@ class MoveOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
MoveOperation move = MoveOperation.from("/0/description").to("/1/description");
|
||||
move.perform(todos, Todo.class);
|
||||
move.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.get(1).getDescription()).isEqualTo("A");
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class MoveOperationUnitTests {
|
||||
MoveOperation move = MoveOperation.from("/0/complete").to("/1/description");
|
||||
|
||||
assertThatExceptionOfType(PatchException.class)
|
||||
.isThrownBy(() -> move.perform(todos, Todo.class))
|
||||
.isThrownBy(() -> move.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE))
|
||||
.withMessage("Path '/0/complete' is not nullable");
|
||||
|
||||
assertThat(todos.get(1).getDescription()).isEqualTo("B");
|
||||
@@ -89,7 +89,7 @@ class MoveOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
MoveOperation move = MoveOperation.from("/1").to("/0");
|
||||
move.perform(todos, Todo.class);
|
||||
move.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.size()).isEqualTo(3);
|
||||
assertThat(todos.get(0).getId().longValue()).isEqualTo(2L);
|
||||
@@ -106,7 +106,7 @@ class MoveOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
MoveOperation move = MoveOperation.from("/0").to("/2");
|
||||
move.perform(todos, Todo.class);
|
||||
move.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.size()).isEqualTo(3);
|
||||
assertThat(todos.get(2).getId().longValue()).isEqualTo(1L);
|
||||
@@ -123,7 +123,7 @@ class MoveOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
MoveOperation move = MoveOperation.from("/0").to("/2");
|
||||
move.perform(todos, Todo.class);
|
||||
move.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.size()).isEqualTo(3);
|
||||
assertThat(todos.get(2).getId().longValue()).isEqualTo(1L);
|
||||
@@ -147,7 +147,7 @@ class MoveOperationUnitTests {
|
||||
expected.add(new Todo(4L, "E", false));
|
||||
|
||||
MoveOperation move = MoveOperation.from("/-").to("/1");
|
||||
move.perform(todos, Todo.class);
|
||||
move.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos).isEqualTo(expected);
|
||||
}
|
||||
@@ -168,7 +168,7 @@ class MoveOperationUnitTests {
|
||||
expected.add(new Todo(2L, "G", false));
|
||||
|
||||
MoveOperation move = MoveOperation.from("/1").to("/-");
|
||||
move.perform(todos, Todo.class);
|
||||
move.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class PatchOperationUnitTests {
|
||||
Todo todo = new Todo(1L, "A", false);
|
||||
|
||||
assertThatExceptionOfType(PatchException.class) //
|
||||
.isThrownBy(() -> it.perform(todo, Todo.class));
|
||||
.isThrownBy(() -> it.perform(todo, Todo.class, TestPropertyPathContext.INSTANCE));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class RemoveOperationTests {
|
||||
todos.add(new Todo(2L, "B", false));
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
RemoveOperation.valueAt("/1/description").perform(todos, Todo.class);
|
||||
RemoveOperation.valueAt("/1/description").perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.get(1).getDescription()).isNull();
|
||||
}
|
||||
@@ -45,7 +45,7 @@ class RemoveOperationTests {
|
||||
todos.add(new Todo(2L, "B", false));
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
RemoveOperation.valueAt("/1").perform(todos, Todo.class);
|
||||
RemoveOperation.valueAt("/1").perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.size()).isEqualTo(2);
|
||||
assertThat(todos.get(0).getDescription()).isEqualTo("A");
|
||||
|
||||
@@ -38,7 +38,7 @@ class ReplaceOperationTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
ReplaceOperation replace = ReplaceOperation.valueAt("/1/complete").with(true);
|
||||
replace.perform(todos, Todo.class);
|
||||
replace.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.get(1).isComplete()).isTrue();
|
||||
}
|
||||
@@ -52,7 +52,7 @@ class ReplaceOperationTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
ReplaceOperation replace = ReplaceOperation.valueAt("/1/description").with("BBB");
|
||||
replace.perform(todos, Todo.class);
|
||||
replace.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.get(1).getDescription()).isEqualTo("BBB");
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class ReplaceOperationTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
ReplaceOperation replace = ReplaceOperation.valueAt("/1/description").with(22);
|
||||
replace.perform(todos, Todo.class);
|
||||
replace.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todos.get(1).getDescription()).isEqualTo("22");
|
||||
}
|
||||
@@ -79,7 +79,7 @@ class ReplaceOperationTests {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ReplaceOperation replace = ReplaceOperation.valueAt("/type")
|
||||
.with(new JsonLateObjectEvaluator(mapper, mapper.readTree("{ \"value\" : \"new\" }")));
|
||||
replace.perform(todo, Todo.class);
|
||||
replace.perform(todo, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(todo.getType()).isNotNull();
|
||||
assertThat(todo.getType().getValue()).isNotNull();
|
||||
@@ -95,7 +95,7 @@ class ReplaceOperationTests {
|
||||
|
||||
ReplaceOperation.valueAt("/characters/protagonist") //
|
||||
.with(prepareValue("\"Pallo\"")) //
|
||||
.perform(book, Book.class);
|
||||
.perform(book, Book.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
assertThat(book.characters.get("protagonist")).isEqualTo("Pallo");
|
||||
}
|
||||
|
||||
@@ -17,21 +17,51 @@ package org.springframework.data.rest.webmvc.json.patch;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.rest.webmvc.json.patch.SpelPath.TypedSpelPath;
|
||||
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
|
||||
import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.data.rest.webmvc.json.BindContextFactory;
|
||||
import org.springframework.data.rest.webmvc.json.PersistentEntitiesBindContextFactory;
|
||||
import org.springframework.data.rest.webmvc.json.patch.SpelPath.UntypedSpelPath;
|
||||
import org.springframework.data.rest.webmvc.json.patch.SpelPath.WritingOperations;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SpelPath}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
class SpelPathUnitTests {
|
||||
|
||||
BindContext context;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
|
||||
KeyValueMappingContext<?, ?> context = new KeyValueMappingContext<>();
|
||||
context.getPersistentEntity(MapWrapper.class);
|
||||
context.getPersistentEntity(Todo.class);
|
||||
context.getPersistentEntity(Person.class);
|
||||
|
||||
PersistentEntities entities = new PersistentEntities(Arrays.asList(context));
|
||||
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities);
|
||||
|
||||
this.context = factory.getBindContextFor(new ObjectMapper());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listIndex() {
|
||||
|
||||
@@ -42,7 +72,7 @@ class SpelPathUnitTests {
|
||||
todos.add(new Todo(2L, "B", false));
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
Object value = expr.bindTo(Todo.class).getValue(todos);
|
||||
Object value = expr.bindForRead(Todo.class, context).getValue(todos);
|
||||
|
||||
assertThat(value).isEqualTo("B");
|
||||
}
|
||||
@@ -57,7 +87,7 @@ class SpelPathUnitTests {
|
||||
todos.add(new Todo(2L, "B", false));
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
Object value = expr.bindTo(Todo.class).getValue(todos);
|
||||
Object value = expr.bindForRead(Todo.class, context).getValue(todos);
|
||||
|
||||
assertThat(value).isEqualTo("C");
|
||||
}
|
||||
@@ -75,21 +105,22 @@ class SpelPathUnitTests {
|
||||
void cachesTypedSpelPath() {
|
||||
|
||||
UntypedSpelPath source = SpelPath.untyped("/description");
|
||||
TypedSpelPath left = source.bindTo(Todo.class);
|
||||
TypedSpelPath right = source.bindTo(Todo.class);
|
||||
WritingOperations left = source.bindForWrite(Todo.class, context);
|
||||
WritingOperations right = source.bindForWrite(Todo.class, context);
|
||||
|
||||
assertThat(left).isSameAs(right);
|
||||
}
|
||||
|
||||
@Test // DATAREST-1274
|
||||
void supportsMultiDigitCollectionIndex() {
|
||||
assertThat(SpelPath.untyped("/11/description").bindTo(Todo.class).getLeafType()).isEqualTo(String.class);
|
||||
assertThat(SpelPath.untyped("/11/description").bindForWrite(Todo.class, context).getLeafType())
|
||||
.isEqualTo(String.class);
|
||||
}
|
||||
|
||||
@Test // DATAREST-1338
|
||||
void handlesStringMapKeysInPathExpressions() {
|
||||
|
||||
TypedSpelPath path = SpelPath.untyped("people/Dave/name").bindTo(MapWrapper.class);
|
||||
WritingOperations path = SpelPath.untyped("people/Dave/name").bindForWrite(MapWrapper.class, context);
|
||||
|
||||
assertThat(path.getExpressionString()).isEqualTo("people['Dave'].name");
|
||||
assertThat(path.getLeafType()).isEqualTo(String.class);
|
||||
@@ -98,18 +129,55 @@ class SpelPathUnitTests {
|
||||
@Test // DATAREST-1338
|
||||
void handlesIntegerMapKeysInPathExpressions() {
|
||||
|
||||
TypedSpelPath path = SpelPath.untyped("peopleByInt/0/name").bindTo(MapWrapper.class);
|
||||
WritingOperations path = SpelPath.untyped("peopleByInt/0/name").bindForWrite(MapWrapper.class, context);
|
||||
|
||||
assertThat(path.getExpressionString()).isEqualTo("peopleByInt[0].name");
|
||||
assertThat(path.getLeafType()).isEqualTo(String.class);
|
||||
}
|
||||
|
||||
// DATAREST-1338
|
||||
@Test
|
||||
void failsAccessingPropertyIgnoredByJackson() {
|
||||
|
||||
static class Person {
|
||||
String name;
|
||||
String path = "peopleByInt/0/hiddenProperty";
|
||||
|
||||
assertThatExceptionOfType(PatchException.class) //
|
||||
.isThrownBy(() -> SpelPath.untyped(path).bindForWrite(MapWrapper.class, context)) //
|
||||
.withMessageContaining("hiddenProperty") //
|
||||
.withMessageContaining(Person.class.getName()) //
|
||||
.withMessageContaining(path); //
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsAccessingGetterIgnoredByJackson() {
|
||||
|
||||
String path = "peopleByInt/0/hiddenGetter";
|
||||
|
||||
assertThatExceptionOfType(PatchException.class) //
|
||||
.isThrownBy(() -> SpelPath.untyped(path).bindForWrite(MapWrapper.class, context)) //
|
||||
.withMessageContaining("hiddenGetter") //
|
||||
.withMessageContaining(Person.class.getName()) //
|
||||
.withMessageContaining(path); //
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapsRenamedProperty() {
|
||||
|
||||
WritingOperations path = SpelPath.untyped("demaner").bindForWrite(Person.class, context);
|
||||
|
||||
assertThat(path.getExpressionString()).isEqualTo("renamed");
|
||||
}
|
||||
|
||||
// DATAREST-1338
|
||||
|
||||
@Data
|
||||
static class Person {
|
||||
String name;
|
||||
@JsonIgnore String hiddenProperty;
|
||||
@Getter(onMethod = @__(@JsonIgnore)) String hiddenGetter;
|
||||
@JsonProperty("demaner") String renamed;
|
||||
}
|
||||
|
||||
@Data
|
||||
static class MapWrapper {
|
||||
Map<String, Person> people;
|
||||
Map<Integer, Person> peopleByInt;
|
||||
|
||||
@@ -33,10 +33,10 @@ class TestOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
TestOperation test = TestOperation.whetherValueAt("/0/complete").hasValue(false);
|
||||
test.perform(todos, Todo.class);
|
||||
test.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
TestOperation test2 = TestOperation.whetherValueAt("/1/complete").hasValue(true);
|
||||
test2.perform(todos, Todo.class);
|
||||
test2.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class TestOperationUnitTests {
|
||||
TestOperation test = TestOperation.whetherValueAt("/0/complete").hasValue(true);
|
||||
|
||||
assertThatExceptionOfType(PatchException.class) //
|
||||
.isThrownBy(() -> test.perform(todos, Todo.class));
|
||||
.isThrownBy(() -> test.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -63,6 +63,6 @@ class TestOperationUnitTests {
|
||||
todos.add(new Todo(3L, "C", false));
|
||||
|
||||
TestOperation test = TestOperation.whetherValueAt("/1").hasValue(new Todo(2L, "B", true));
|
||||
test.perform(todos, Todo.class);
|
||||
test.perform(todos, Todo.class, TestPropertyPathContext.INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* 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.data.rest.webmvc.json.patch;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class TestPropertyPathContext implements BindContext {
|
||||
|
||||
public static final BindContext INSTANCE = new TestPropertyPathContext();
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.rest.webmvc.json.patch.BindContext#getReadableProperty(java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public Optional<String> getReadableProperty(String segment, Class<?> type) {
|
||||
return Optional.of(segment);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.rest.webmvc.json.patch.BindContext#getWritableProperty(java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public Optional<String> getWritableProperty(String segment, Class<?> type) {
|
||||
return Optional.of(segment);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user