DATAREST-1213 - ETag creation now uses proxy target for projections.

Original pull request: #355.
This commit is contained in:
Dario Seidl
2019-05-26 18:48:56 +02:00
committed by Oliver Drotbohm
parent 3cb683c137
commit e3a88ff9ca
9 changed files with 190 additions and 4 deletions

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2019 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.jpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Version;
/**
* @author Dario Seidl
*/
@Entity
public class Category {
public @Id @GeneratedValue Long id;
public @Version Long version = 0L;
public String name;
public Category() {
}
public Category(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2019 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.jpa;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.rest.core.config.Projection;
/**
* @author Dario Seidl
*/
@Projection(name = "open", types = Category.class)
public interface CategoryProjection {
String getName();
@Value("calculated-#{target.name}")
String getCalculatedName();
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2019 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.jpa;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
/**
* @author Dario Seidl
*/
@RepositoryRestResource(collectionResourceRel = "categories", path = "categories")
public interface CategoryRepository extends CrudRepository<Category, Long> {
}

View File

@@ -57,7 +57,7 @@ public class RepositoryControllerIntegrationTests extends AbstractControllerInte
RepositoryLinksResource resource = controller.listRepositories().getBody();
assertThat(resource.getLinks()).hasSize(8);
assertThat(resource.getLinks()).hasSize(9);
assertThat(resource.hasLink("people")).isTrue();
assertThat(resource.hasLink("orders")).isTrue();
@@ -66,5 +66,6 @@ public class RepositoryControllerIntegrationTests extends AbstractControllerInte
assertThat(resource.hasLink("authors")).isTrue();
assertThat(resource.hasLink("receipts")).isTrue();
assertThat(resource.hasLink("items")).isTrue();
assertThat(resource.hasLink("categories")).isTrue();
}
}

View File

@@ -61,6 +61,7 @@ import com.jayway.jsonpath.JsonPath;
* @author Greg Turnquist
* @author Mark Paluch
* @author Ľubomír Varga
* @author Dario Seidl
*/
@Transactional
@ContextConfiguration(classes = JpaRepositoryConfig.class)
@@ -240,6 +241,48 @@ public class JpaWebTests extends CommonWebTests {
assertNull(JsonPath.read(frodo.getContentAsString(), "$.lastName"));
}
@Test // DATAREST-1213
public void createThenPatchWithProjection() throws Exception {
Link categoriesLink = client.discoverUnique(LinkRelation.of("categories"));
MockHttpServletResponse test = postAndGet(categoriesLink, "{ \"name\" : \"test\" }",
MediaType.APPLICATION_JSON);
Link testLink = client.assertHasLinkWithRel(IanaLinkRelations.SELF, test);
assertThat((String) JsonPath.read(test.getContentAsString(), "$.name")).isEqualTo("test");
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(testLink.getHref());
String uri = builder.queryParam("projection", "open").build().toUriString();
MockHttpServletResponse patched = patchAndGet(new Link(uri), "{ \"name\" : \"patched\" }", MediaType.APPLICATION_JSON);
assertThat((String) JsonPath.read(patched.getContentAsString(), "$.name")).isEqualTo("patched");
assertThat((String) JsonPath.read(patched.getContentAsString(), "$.calculatedName")).isEqualTo("calculated-patched");
}
@Test // DATAREST-1213
public void createThenPutWithProjection() throws Exception {
Link categoriesLink = client.discoverUnique(LinkRelation.of("categories"));
MockHttpServletResponse test = postAndGet(categoriesLink, "{ \"name\" : \"test\" }",
MediaType.APPLICATION_JSON);
Link testLink = client.assertHasLinkWithRel(IanaLinkRelations.SELF, test);
assertThat((String) JsonPath.read(test.getContentAsString(), "$.name")).isEqualTo("test");
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(testLink.getHref());
String uri = builder.queryParam("projection", "open").build().toUriString();
MockHttpServletResponse patched = putAndGet(new Link(uri), "{ \"name\" : \"put\" }", MediaType.APPLICATION_JSON);
assertThat((String) JsonPath.read(patched.getContentAsString(), "$.name")).isEqualTo("put");
assertThat((String) JsonPath.read(patched.getContentAsString(), "$.calculatedName")).isEqualTo("calculated-put");
}
@Test
public void listsSiblingsWithContentCorrectly() throws Exception {
assertPersonWithNameAndSiblingLink("John");

View File

@@ -95,6 +95,7 @@ public class ProfileIntegrationTests extends AbstractControllerIntegrationTests
assertThat(client.discoverUnique(profileLink, "orders", MediaType.ALL)).isNotNull();
assertThat(client.discoverUnique(profileLink, "receipts", MediaType.ALL)).isNotNull();
assertThat(client.discoverUnique(profileLink, "addresses", MediaType.ALL)).isNotNull();
assertThat(client.discoverUnique(profileLink, "categories", MediaType.ALL)).isNotNull();
}
@Test // DATAREST-638

View File

@@ -37,6 +37,7 @@ import org.springframework.util.Assert;
* instances.
*
* @author Oliver Gierke
* @author Dario Seidl
* @soundtrack Ron Spielman Trio - Matchstick
*/
@RequiredArgsConstructor
@@ -59,7 +60,7 @@ public class HttpHeadersPreparer {
public HttpHeaders prepareHeaders(Optional<PersistentEntityResource> resource) {
return resource//
.map(it -> prepareHeaders(it.getPersistentEntity(), it.getContent()))//
.map(it -> prepareHeaders(it.getPersistentEntity(), it.getTargetEntity()))//
.orElseGet(() -> new HttpHeaders());
}
@@ -75,6 +76,8 @@ public class HttpHeadersPreparer {
Assert.notNull(entity, "PersistentEntity must not be null!");
Assert.notNull(value, "Entity value must not be null!");
Assert.isInstanceOf(entity.getType(), value, () ->
String.format("Target bean of type %s is not of type of the persistent entity (%s)!", value.getClass().getName(), entity.getType().getName()));
// Add ETag
HttpHeaders headers = ETag.from(entity, value).addTo(new HttpHeaders());

View File

@@ -24,6 +24,7 @@ import java.util.List;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.projection.TargetAware;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
@@ -38,6 +39,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
*
* @author Jon Brisbin
* @author Oliver Gierke
* @author Dario Seidl
*/
public class PersistentEntityResource extends EntityModel<Object> {
@@ -88,13 +90,27 @@ public class PersistentEntityResource extends EntityModel<Object> {
return entity;
}
/**
* Returns the underlying instance. If the instance is a dynamic JDK proxy, the proxy target is returned.
*
* @return
*/
public Object getTargetEntity() {
Object content = getContent();
if (content instanceof TargetAware) {
return ((TargetAware) content).getTarget();
} else {
return content;
}
}
/**
* Returns the {@link PersistentPropertyAccessor} for the underlying content bean.
*
* @return
*/
public PersistentPropertyAccessor<?> getPropertyAccessor() {
return entity.getPropertyAccessor(getContent());
return entity.getPropertyAccessor(getTargetEntity());
}
/**

View File

@@ -32,6 +32,7 @@ import org.springframework.util.Assert;
* A value object to represent ETags.
*
* @author Oliver Gierke
* @author Dario Seidl
*/
@EqualsAndHashCode
public final class ETag {
@@ -74,7 +75,7 @@ public final class ETag {
Assert.notNull(resource, "PersistentEntityResource must not be null!");
return from(resource.getPersistentEntity(), resource.getContent());
return from(resource.getPersistentEntity(), resource.getTargetEntity());
}
/**