diff --git a/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/LineItem.java b/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/LineItem.java index 020c0b0da..50a47b289 100644 --- a/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/LineItem.java +++ b/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/LineItem.java @@ -26,17 +26,19 @@ import java.util.UUID; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Reference; import org.springframework.data.rest.core.config.Projection; +import org.springframework.data.rest.tests.shop.Product.ProductNameOnlyProjection; /** * @author Oliver Gierke + * @author Craig Andrews */ @Data @EqualsAndHashCode(of = "id") public class LineItem { - @Projection(name = "productsOnly", types = { LineItem.class }) + @Projection(name = "productsOnly", types = LineItem.class) public interface LineItemProductsOnlyProjection { - List getProducts(); + List getProducts(); } private final @Id UUID id = UUID.randomUUID(); diff --git a/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Order.java b/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Order.java index ca2728ac0..0b66fad7a 100644 --- a/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Order.java +++ b/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Order.java @@ -24,16 +24,18 @@ import java.util.UUID; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Reference; import org.springframework.data.rest.core.config.Projection; +import org.springframework.data.rest.tests.shop.LineItem.LineItemProductsOnlyProjection; /** * @author Oliver Gierke + * @author Craig Andrews */ @Value public class Order { - - @Projection(name = "itemsOnly", types = { Order.class }) + + @Projection(name = "itemsOnly", types = Order.class) public interface OrderItemsOnlyProjection { - List getItems(); + List getItems(); } private final @Id UUID id = UUID.randomUUID(); diff --git a/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Product.java b/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Product.java index 9cea9d608..6ba8487ea 100644 --- a/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Product.java +++ b/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Product.java @@ -26,13 +26,13 @@ import org.springframework.data.rest.core.config.Projection; /** * @author Oliver Gierke + * @author Craig Andrews */ - @Value @RequiredArgsConstructor public class Product { - @Projection(name = "nameOnly", types = { Product.class }) + @Projection(name = "nameOnly", types = Product.class) public interface ProductNameOnlyProjection { String getName(); } diff --git a/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopConfiguration.java b/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopConfiguration.java index a6fe3fb20..9b2f4443f 100644 --- a/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopConfiguration.java +++ b/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.map.repository.config.EnableMapRepositories; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.tests.shop.Customer.Gender; +import org.springframework.data.rest.tests.shop.Product.ProductNameOnlyProjection; import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; @@ -59,15 +60,16 @@ public class ShopConfiguration { } @Bean - public ResourceProcessor> productNameOnlyProjectionResourceProcessor() { - return new ResourceProcessor>() { + public ResourceProcessor> productNameOnlyProjectionResourceProcessor() { + + return new ResourceProcessor>() { /* * (non-Javadoc) * @see org.springframework.hateoas.ResourceProcessor#process(org.springframework.hateoas.ResourceSupport) */ @Override - public Resource process(Resource resource) { + public Resource process(Resource resource) { resource.add(new Link("alpha", "beta")); return resource; } diff --git a/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopIntegrationTests.java b/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopIntegrationTests.java index 61b585408..95a582a37 100644 --- a/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopIntegrationTests.java +++ b/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopIntegrationTests.java @@ -18,7 +18,7 @@ package org.springframework.data.rest.tests.shop; import static org.hamcrest.CoreMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import java.util.HashMap; +import java.util.Collections; import java.util.Map; import org.junit.Test; @@ -32,7 +32,10 @@ import org.springframework.test.web.servlet.ResultActions; import com.jayway.jsonpath.JsonPath; /** + * Integration tests for projections. + * * @author Oliver Gierke + * @author Craig Andrews */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ShopConfiguration.class) @@ -59,6 +62,47 @@ public class ShopIntegrationTests extends AbstractWebIntegrationTests { expectRelatedResource("customer", actions); } + /** + * @see DATAREST-221 + */ + @Test + public void renderProductNameOnlyProjection() throws Exception { + + Map arguments = Collections.singletonMap("projection", "nameOnly"); + + client.follow(client.discoverUnique("products").expand(arguments))// + .andExpect(status().isOk())// + .andExpect(jsonPath("$._embedded.products[0].name", notNullValue()))// + .andExpect(jsonPath("$._embedded.products[0].price").doesNotExist()); + } + + /** + * @see DATAREST-221 + */ + @Test + public void renderProductNameOnlyProjectionResourceProcessor() throws Exception { + + Map arguments = Collections.singletonMap("projection", "nameOnly"); + + client.follow(client.discoverUnique("products").expand(arguments))// + .andExpect(status().isOk())// + .andExpect(jsonPath("$._embedded.products[0]._links.beta").exists()); + } + + /** + * @see DATAREST-221 + */ + @Test + public void renderOrderItemsOnlyProjectionResourceProcessor() throws Exception { + + Map arguments = Collections.singletonMap("projection", "itemsOnly"); + + client.follow(client.discoverUnique("orders").expand(arguments))// + .andExpect(status().isOk())// + .andExpect(jsonPath("$._embedded.orders[0].items[0].products[0].name").exists())// + .andExpect(jsonPath("$._embedded.orders[0].items[0].products[0]._links.beta").exists()); + } + private static void expectRelatedResource(String name, ResultActions actions) throws Exception { int dotIndex = name.lastIndexOf('.'); @@ -70,36 +114,4 @@ public class ShopIntegrationTests extends AbstractWebIntegrationTests { actions.andExpect(jsonPath(prefix.concat("_links.").concat(suffix)).exists()); actions.andExpect(jsonPath(prefix.concat("_embedded.").concat(suffix)).exists()); } - - @Test - public void renderProductNameOnlyProjection() throws Exception { - Map arguments = new HashMap<>(); - arguments.put("projection", "nameOnly"); - client.follow(client.discoverUnique("products").expand(arguments)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._embedded.products[0].name", notNullValue())) - .andExpect(jsonPath("$._embedded.products[0].price").doesNotExist()); - } - - @Test - public void renderProductNameOnlyProjectionResourceProcessor() throws Exception { - Map arguments = new HashMap<>(); - arguments.put("projection", "nameOnly"); - client.follow(client.discoverUnique("products").expand(arguments)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._embedded.products[0]._links.beta").exists()); - } - - /** - * @see DATAREST-221 - */ - @Test - public void renderOrderItemsOnlyProjectionResourceProcessor() throws Exception { - Map arguments = new HashMap<>(); - arguments.put("projection", "itemsOnly"); - client.follow(client.discoverUnique("orders").expand(arguments)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._embedded.orders[0].items[0].products[0].name").exists()) - .andExpect(jsonPath("$._embedded.orders[0].items[0].products[0]._links.beta").exists()); - } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index 56cbbb624..b7ae61471 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -41,6 +41,7 @@ import org.springframework.data.repository.support.RepositoryInvokerFactory; import org.springframework.data.rest.core.UriToEntityConverter; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.support.EntityLookup; import org.springframework.data.rest.core.support.SelfLinkProvider; import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler; @@ -110,13 +111,16 @@ public class PersistentEntityJackson2Module extends SimpleModule { * * @param associations must not be {@literal null}. * @param entities must not be {@literal null}. - * @param config must not be {@literal null}. * @param converter must not be {@literal null}. - * @param linkProvider must not be {@literal null}. + * @param collector must not be {@literal null}. + * @param factory must not be {@literal null}. + * @param lookupObjectSerializer must not be {@literal null}. + * @param invoker must not be {@literal null}. + * @param assembler must not be {@literal null}. */ public PersistentEntityJackson2Module(Associations associations, PersistentEntities entities, UriToEntityConverter converter, LinkCollector collector, RepositoryInvokerFactory factory, - LookupObjectSerializer lookupObjectSerializer, ResourceProcessorInvoker resourceProcessorInvoker, + LookupObjectSerializer lookupObjectSerializer, ResourceProcessorInvoker invoker, EmbeddedResourcesAssembler assembler) { super(new Version(2, 0, 0, null, "org.springframework.data.rest", "jackson-module")); @@ -126,9 +130,9 @@ public class PersistentEntityJackson2Module extends SimpleModule { Assert.notNull(converter, "UriToEntityConverter must not be null!"); Assert.notNull(collector, "LinkCollector must not be null!"); - NestedEntitySerializer serializer = new NestedEntitySerializer(entities, assembler, resourceProcessorInvoker); + NestedEntitySerializer serializer = new NestedEntitySerializer(entities, assembler, invoker); addSerializer(new PersistentEntityResourceSerializer(collector)); - addSerializer(new ProjectionSerializer(collector, associations, resourceProcessorInvoker, false)); + addSerializer(new ProjectionSerializer(collector, associations, invoker, false)); addSerializer(new ProjectionResourceContentSerializer(false)); setSerializerModifier( @@ -540,7 +544,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { private final LinkCollector collector; private final Associations associations; - private final ResourceProcessorInvoker resourceProcessorInvoker; + private final ResourceProcessorInvoker invoker; private final boolean unwrapping; /** @@ -549,15 +553,17 @@ public class PersistentEntityJackson2Module extends SimpleModule { * * @param collector must not be {@literal null}. * @param mappings must not be {@literal null}. + * @param invoker must not be {@literal null}. * @param unwrapping */ - private ProjectionSerializer(LinkCollector collector, Associations mappings, ResourceProcessorInvoker resourceProcessorInvoker, boolean unwrapping) { + private ProjectionSerializer(LinkCollector collector, Associations mappings, ResourceProcessorInvoker invoker, + boolean unwrapping) { super(TargetAware.class); this.collector = collector; this.associations = mappings; - this.resourceProcessorInvoker = resourceProcessorInvoker; + this.invoker = invoker; this.unwrapping = unwrapping; } @@ -583,14 +589,6 @@ public class PersistentEntityJackson2Module extends SimpleModule { } } - private ProjectionResource toResource(TargetAware value) { - Object target = value.getTarget(); - Links links = associations.getMetadataFor(value.getTargetClass()).isExported() ? collector.getLinksFor(target) - : new Links(); - Resource resource = resourceProcessorInvoker.invokeProcessorsFor(new Resource(value, links)); - return new ProjectionResource(resource.getContent(), resource.getLinks()); - } - /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonSerializer#isUnwrappingSerializer() @@ -606,7 +604,24 @@ public class PersistentEntityJackson2Module extends SimpleModule { */ @Override public JsonSerializer unwrappingSerializer(NameTransformer unwrapper) { - return new ProjectionSerializer(collector, associations, resourceProcessorInvoker, true); + return new ProjectionSerializer(collector, associations, invoker, true); + } + + /** + * Creates a {@link ProjectionResource} for the given {@link TargetAware}. + * + * @param value must not be {@literal null}. + * @return + */ + private ProjectionResource toResource(TargetAware value) { + + Object target = value.getTarget(); + ResourceMetadata metadata = associations.getMetadataFor(value.getTargetClass()); + Links links = metadata.isExported() ? collector.getLinksFor(target) : new Links(); + + Resource resource = invoker.invokeProcessorsFor(new Resource(value, links)); + + return new ProjectionResource(resource.getContent(), resource.getLinks()); } }