From 9bc6f7615b5cfa04ef48c8886402694e070e0c31 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 10 Mar 2020 23:33:55 -0700 Subject: [PATCH] Add support for fully-qualified links - also make the linkrel configurable Fixes #148 --- .../ContentRestAutoConfiguration.java | 11 +- .../SpringBootContentRestConfigurer.java | 9 +- .../ContentRestAutoConfigurationTest.java | 9 +- .../SpringBootContentRestConfigurerTest.java | 13 + .../src/main/asciidoc/rest-baseuri.adoc | 1 + .../asciidoc/rest-fullyqualifiedlinks.adoc | 104 ++ .../src/main/asciidoc/rest-index.adoc | 2 + .../src/main/asciidoc/rest-linkrel.adoc | 83 ++ .../src/main/asciidoc/rest-store.adoc | 47 +- ...ResourceHandlerMethodArgumentResolver.java | 4 - .../StoreHandlerMethodArgumentResolver.java | 163 ++-- .../links/ContentLinksResourceProcessor.java | 69 +- .../content/rest/StoreRestResource.java | 1 + .../rest/config/RestConfiguration.java | 9 + .../ContentPropertyRestEndpointsIT.java | 893 +++++++++++------- .../rest/links/BaseUriContentLinksIT.java | 36 +- .../content/rest/links/ContentLinkRelIT.java | 90 ++ ...ntLinkTests.java => ContentLinkTests.java} | 6 +- .../content/rest/links/ContentLinksIT.java | 33 +- .../ContentLinksResourceProcessorIT.java | 20 +- .../rest/links/ContextPathContentLinksIT.java | 20 +- .../rest/links/EntityContentLinksIT.java | 94 ++ .../content/rest/support/TestEntity6.java | 1 + .../support/TestEntityContentRepository.java | 5 +- 24 files changed, 1194 insertions(+), 529 deletions(-) create mode 100644 spring-content-rest/src/main/asciidoc/rest-fullyqualifiedlinks.adoc create mode 100644 spring-content-rest/src/main/asciidoc/rest-linkrel.adoc create mode 100644 spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkRelIT.java rename spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/{EntityContentLinkTests.java => ContentLinkTests.java} (95%) create mode 100644 spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinksIT.java diff --git a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java index c19377a6d..7e068f139 100644 --- a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java +++ b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/ContentRestAutoConfiguration.java @@ -22,6 +22,7 @@ public class ContentRestAutoConfiguration { public static class ContentRestProperties { private URI baseUri; + private boolean fullyQualifiedLinks = false; public URI getBaseUri() { return baseUri; @@ -30,7 +31,15 @@ public URI getBaseUri() { public void setBaseUri(URI baseUri) { this.baseUri = baseUri; } - } + + public boolean fullyQualifiedLinks() { + return this.fullyQualifiedLinks; + } + + public void setFullyQualifiedLinks(boolean fullyQualifiedLinks) { + this.fullyQualifiedLinks = fullyQualifiedLinks; + } + } @Bean public SpringBootContentRestConfigurer springBootContentRestConfigurer() { diff --git a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java index 3fb793d3e..997d1e3c8 100644 --- a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java +++ b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/rest/boot/autoconfigure/SpringBootContentRestConfigurer.java @@ -23,9 +23,14 @@ public SpringBootContentRestConfigurer(ContentRestProperties properties) { @Override public void configure(RestConfiguration config) { - if (properties == null || properties.getBaseUri() == null) + if (properties == null) { return; + } - config.setBaseUri(properties.getBaseUri()); + if (properties.getBaseUri() != null) { + config.setBaseUri(properties.getBaseUri()); + } + + config.setFullyQualifiedLinks(properties.fullyQualifiedLinks()); } } diff --git a/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/ContentRestAutoConfigurationTest.java b/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/ContentRestAutoConfigurationTest.java index 774571cbd..60160b246 100644 --- a/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/ContentRestAutoConfigurationTest.java +++ b/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/ContentRestAutoConfigurationTest.java @@ -50,21 +50,22 @@ public class ContentRestAutoConfigurationTest { }); }); - Context("given an environment specifying a base uri", () -> { + Context("given an environment specifying rest properties", () -> { BeforeEach(() -> { System.setProperty("spring.content.rest.base-uri", "/contentApi"); + System.setProperty("spring.content.rest.content-links", "false"); }); AfterEach(() -> { System.clearProperty("spring.content.rest.base-uri"); }); - It("should have a filesystem properties bean with the correct root set", () -> { + It("should have a filesystem properties bean with the correct properties set", () -> { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(TestConfig.class); context.setServletContext(new MockServletContext()); context.refresh(); - assertThat(context.getBean(ContentRestAutoConfiguration.ContentRestProperties.class).getBaseUri(), - is(URI.create("/contentApi"))); + assertThat(context.getBean(ContentRestAutoConfiguration.ContentRestProperties.class).getBaseUri(), is(URI.create("/contentApi"))); + assertThat(context.getBean(ContentRestAutoConfiguration.ContentRestProperties.class).fullyQualifiedLinks(), is(false)); assertThat(context.getBean(SpringBootContentRestConfigurer.class), is(not(nullValue()))); diff --git a/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/SpringBootContentRestConfigurerTest.java b/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/SpringBootContentRestConfigurerTest.java index f6c38902a..36435dd88 100644 --- a/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/SpringBootContentRestConfigurerTest.java +++ b/spring-content-autoconfigure/src/test/java/org/springframework/content/rest/boot/SpringBootContentRestConfigurerTest.java @@ -14,6 +14,7 @@ import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.JustBeforeEach; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -56,6 +57,17 @@ public class SpringBootContentRestConfigurerTest { }); }); + Context("given a fullyQualifiedLinks property setting", () -> { + + BeforeEach(() -> { + properties.setFullyQualifiedLinks(true); + }); + + It("should set the property on the RestConfiguration", () -> { + verify(restConfig).setFullyQualifiedLinks(eq(true)); + }); + }); + Context("given a null base uri property", () -> { It("should not set the property on the RestConfiguration", () -> { @@ -71,6 +83,7 @@ public class SpringBootContentRestConfigurerTest { It("should not set the property on the RestConfiguration", () -> { verify(restConfig, never()).setBaseUri(anyObject()); + verify(restConfig, never()).setFullyQualifiedLinks(anyBoolean()); }); }); }); diff --git a/spring-content-rest/src/main/asciidoc/rest-baseuri.adoc b/spring-content-rest/src/main/asciidoc/rest-baseuri.adoc index f7956b05a..8e007b923 100644 --- a/spring-content-rest/src/main/asciidoc/rest-baseuri.adoc +++ b/spring-content-rest/src/main/asciidoc/rest-baseuri.adoc @@ -32,3 +32,4 @@ class CustomContentRestMvcConfiguration { } ---- ==== + diff --git a/spring-content-rest/src/main/asciidoc/rest-fullyqualifiedlinks.adoc b/spring-content-rest/src/main/asciidoc/rest-fullyqualifiedlinks.adoc new file mode 100644 index 000000000..216043ca1 --- /dev/null +++ b/spring-content-rest/src/main/asciidoc/rest-fullyqualifiedlinks.adoc @@ -0,0 +1,104 @@ +== Fully Qualified Links +By default, and where possible, Spring Content REST exports Spring Resources to shortened link URIs. These will often +match the Spring Data Rest Entity URI. + +Given the following example: + +==== +[source, java] +---- + @Entity + public class Dvd { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @ContentId + private UUID contentId; + + @ContentLength + private Long contentLength; + + @MimeType + private String mimeType; + + // getters and setters + } + + public interface DvdRepository extends CrudRepository {} + + public interface DvdStore extends ContentStore {} +---- +==== + +As there is only a single associated Spring Resource, Spring Content REST will generate the following URI: + +==== +[source, java] +---- + "_links" : { + "self" : { + ... + }, + "dvd" : { + ... + }, + "dvds" : { + "href" : "http://localhost:8080/dvds/1" + } + } +---- +==== + +To generate fully qualified link URIs set the following property: + +==== +[source, java] +---- +spring.content.rest.fullyQualifiedLinks=true +---- +==== + +Or if you are not using Spring Boot, you can do the following: + +==== +[source, java] +---- +@Configuration +class CustomContentRestMvcConfiguration { + + @Bean + public ContentRestConfigurer contentRestConfigurer() { + + return new ContentRestConfigurer() { + + @Override + public void configure(RestConfiguration config) { + config.setFullyQualifiedLinks(true); + } + }; + } +} +---- +==== + +Spring Content REST will now generate links as follows: + +==== +[source, java] +---- + "_links" : { + "self" : { + ... + }, + "dvd" : { + ... + }, + "content" : { + "href" : "http://localhost:8080/dvds/1/content" + } + } +---- +==== + +where `content` is the extracted property name taken from the field `contentId`. \ No newline at end of file diff --git a/spring-content-rest/src/main/asciidoc/rest-index.adoc b/spring-content-rest/src/main/asciidoc/rest-index.adoc index 2c6a52ac8..fcbeedb9c 100644 --- a/spring-content-rest/src/main/asciidoc/rest-index.adoc +++ b/spring-content-rest/src/main/asciidoc/rest-index.adoc @@ -36,6 +36,8 @@ include::{spring-versions-jpa-docs}/jpaversions-rest.adoc[leveloffset=+1] include::rest-cors.adoc[leveloffset=+1] include::rest-baseuri.adoc[leveloffset=+1] +include::rest-linkrel.adoc[leveloffset=+1] +include::rest-fullyqualifiedlinks.adoc[leveloffset=+1] //[[appendix]] //= Appendix diff --git a/spring-content-rest/src/main/asciidoc/rest-linkrel.adoc b/spring-content-rest/src/main/asciidoc/rest-linkrel.adoc new file mode 100644 index 000000000..7258bc278 --- /dev/null +++ b/spring-content-rest/src/main/asciidoc/rest-linkrel.adoc @@ -0,0 +1,83 @@ +== Changing the link relation + +For each exported Spring Resource, Spring Content REST will generate a suitable linkrel. + +However, it can sometimes be useful to control this yourself. + +Given the following example: + +==== +[source, java] +---- + @Entity + public class Dvd { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @ContentId + private UUID contentId; + + @ContentLength + private Long contentLength; + + @MimeType + private String mimeType; + + // getters and setters + } + + public interface DvdRepository extends CrudRepository {} + + public interface DvdStore extends ContentStore {} +---- +==== + +Spring Content REST will export the Spring Resource to the following linkrel: + +==== +[source, java] +---- + "_links" : { + "self" : { + ... + }, + "dvd" : { + ... + }, + "dvds" : { + "href" : "http://localhost:8080/dvds/1" + } + } +---- +==== + +Specifying a `linkRel` on the StoreRestResource, as follows: + +==== +[source, java] +---- + @StoreRestResource(linkRel="content") + public interface DvdStore extends ContentStore {} +---- +==== + +will result in the following linkrel instead: + +==== +[source, java] +---- + "_links" : { + "self" : { + ... + }, + "dvd" : { + ... + }, + "content" : { + "href" : "http://localhost:8080/dvds/1" + } + } +---- +==== + diff --git a/spring-content-rest/src/main/asciidoc/rest-store.adoc b/spring-content-rest/src/main/asciidoc/rest-store.adoc index 405bbcca9..adcd25b8f 100644 --- a/spring-content-rest/src/main/asciidoc/rest-store.adoc +++ b/spring-content-rest/src/main/asciidoc/rest-store.adoc @@ -103,8 +103,8 @@ The following describes typical store scenarios and how they are exported with S === Resources -Spring Resources, managed by a Spring Content Store, are standard Spring Resources that, when exported using Spring -Content REST, are accessible by REST endpoint. +Spring Content Stores manage Spring Resources that, when exported using Spring Content REST, are accessible by REST +endpoint. Consider the following Store interface: @@ -118,17 +118,14 @@ Consider the following Store interface: In this example, the Store's Resources are exported to the URI `/dvds`. The path is derived from the uncapitalized, pluralized, simple class name of the interface. When interacting with this endpoint any additional path is deemed to be the Resource's location and will be used to fetch the Resource using the Store's `getResource` method. -For example, a GET request to `/dvds/comedy/monty_pythons_flying_circus.mp4` assumes -`/comedy/monty_pythons_flying_circus.mp4` is the location of the Resource. - -The HTTP methods are then mapped onto the relevant Resource methods; GET maps to `getInputStream`, PUT maps onto -`getOutputStream` and so on. +For example, a GET request to `/dvds/comedy/monty_pythons_flying_circus.mp4` will fetch from the `DvdStore` (`/dvds`), +the Resource `/comedy/monty_pythons_flying_circus.mp4`. === Entity Resources -Entity Resources are associated with Spring Data Entities. +Entity Resources are Spring Resource associated with Spring Data Entities. -Assume the following `Entity` class with associated `Repository` and `Store` interfaces: +Assume the following `Entity` class, `Repository` and `Store` interfaces: ==== [source, java] @@ -136,9 +133,11 @@ Assume the following `Entity` class with associated `Repository` and `Store` int @Entity public class Dvd { @Id - @ContentId - private UUID id; - + private Long id; + + @ContentId + private UUID contentId; + @ContentLength private Long contentLength; @@ -148,20 +147,20 @@ Assume the following `Entity` class with associated `Repository` and `Store` int // getters and setters } - public interface DvdRepository extends CrudRepository {} + public interface DvdRepository extends CrudRepository {} public interface DvdStore extends ContentStore {} ---- ==== -In this example a single Resource (the DVD video stream) is associated with a Dvd Entity by annotating additional fields -on the Entity using the `@ContentId` and `@MimeType` annotations. In this example Spring Data REST exports a collection -resource at `/dvds`. The path is derived from the uncapitalized, pluralized, simple class name of the domain class being -managed. `Dvd` in this case. Item resources are also exported to the URI `/dvds/{id}`. The HTTP methods used to -request this endpoint map into the methods of `CrudRepository`. +In this example a single Spring Resource (the DVD's video stream) is associated with a Dvd Entity by annotating additional +fields on the Entity using the `@ContentId`, `@ContentLength` and `@MimeType` annotations. In this example Spring Data +REST exports a collection resource to `/dvds`. The path is derived from the uncapitalized, pluralized, simple class +name of the domain class. Item resources are also exported to the URI `/dvds/{id}`. The HTTP methods used to request +this endpoint map onto the methods of `CrudRepository`. -Similarly, Spring Content REST also exports Entity Resources to the URI `/dvds/{id}`. In this case the HTTP methods -map onto the methods on `ContentStore` as follows:- +Similarly, Spring Content REST also exports any associated Spring Resources to the URI `/dvds/{id}`. In this case the +HTTP methods map onto the methods of `ContentStore` as follows:- - GET -> getContent - POST/PUT -> setContent @@ -169,10 +168,10 @@ map onto the methods on `ContentStore` as follows:- === Property Resources -Property Resources are associated with the properties of Spring Data Entities. This allows many Resources to be -associated with a single Entity. +Property Resources are associated with the properties of Spring Data Entities, that themselves maybe Spring Data Entities. +This allows many Resources to be associated with a single Entity. -Consider the following `Entity` model with `Repository` and `Store` interfaces: +Consider the following `Entity` model, with `Repository` and `Store` interfaces: ==== [source, java] @@ -328,7 +327,7 @@ Resources links for both into the HAL responses created by Spring Data REST. == The Store Resource -Spring Content REST exports Store Resources to `/{store}/**`. The resource path can be customized using the +Spring Content REST exports Store Resources to `/{store}/**`. The resource path and linkrel can be customized using the `@StoreRestResource` annotation on the Store interface. === Supported HTTP Methods diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceHandlerMethodArgumentResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceHandlerMethodArgumentResolver.java index edc13cd72..c9be3c312 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceHandlerMethodArgumentResolver.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceHandlerMethodArgumentResolver.java @@ -76,10 +76,6 @@ public Object resolveArgument(MethodParameter methodParameter, ModelAndViewConta HttpMethod method = HttpMethod.valueOf(nativeWebRequest.getNativeRequest(HttpServletRequest.class).getMethod()); r = (Resource) this.resolveProperty(method, this.getRepositories(), this.getStores(), pathSegments, (i, e, p, propertyIsEmbedded) -> { - if (p == null) { - return new ContentStoreUtils.NonExistentResource(); - } - AssociativeStore s = i.getImplementation(AssociativeStore.class); Resource resource = s.getResource(p); resource = new AssociatedResourceImpl(p, resource); diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreHandlerMethodArgumentResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreHandlerMethodArgumentResolver.java index 3c20330f7..c1e836c30 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreHandlerMethodArgumentResolver.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreHandlerMethodArgumentResolver.java @@ -4,8 +4,10 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.Optional; +import java.util.UUID; import javax.persistence.Embeddable; +import javax.persistence.Entity; import javax.servlet.http.HttpServletRequest; import internal.org.springframework.content.rest.utils.ContentStoreUtils; @@ -141,96 +143,94 @@ protected Object resolveProperty(HttpMethod method, Repositories repositorie } PersistentProperty property = getContentPropertyDefinition(entity, contentProperty); + Class propertyClass = property.getActualType(); - // get property class - Class propertyClass = null; - if (!PersistentEntityUtils.isPropertyMultiValued(property)) { - propertyClass = property.getActualType(); - } - // null multi-valued content property - else if (PersistentEntityUtils.isPropertyMultiValued(property)) { - propertyClass = property.getActualType(); - } + if (isPrimitiveProperty(propertyClass)) { - // get property store - ContentStoreInfo info = ContentStoreUtils.findContentStore(stores, propertyClass); - if (info == null) { - throw new IllegalStateException(String.format("Store for property %s not found", property.getName())); + ContentStoreInfo info = ContentStoreUtils.findContentStore(stores, domainObj.getClass()); + if (info == null) { + throw new IllegalStateException(String.format("Store for property %s not found", property.getName())); + } + + return resolver.resolve(info, domainObj, domainObj, false); } // get or create property value PersistentPropertyAccessor accessor = property.getOwner().getPropertyAccessor(domainObj); Object propVal = accessor.getProperty(property); + if (propVal == null) { - // null single-valued property + if (!PersistentEntityUtils.isPropertyMultiValued(property)) { propVal = instantiate(propertyClass); accessor.setProperty(property, propVal); } - // null multi-valued content property else { - if (property.isArray()) { - Object member = instantiate(propertyClass); - try { - member = StoreRestController.save(repositories, member); - } - catch (HttpRequestMethodNotSupportedException e) { - e.printStackTrace(); - } - - Object newArray = Array.newInstance(propertyClass, 1); - Array.set(newArray, 0, member); - accessor.setProperty(property, newArray); - propVal = member; - } - else if (property.isCollectionLike()) { - Object member = instantiate(propertyClass); - @SuppressWarnings("unchecked") - Collection contentCollection = (Collection) accessor.getProperty(property); - contentCollection.add(member); - propVal = member; - } +// if (property.isArray()) { +// Object member = instantiate(propertyClass); +// try { +// member = StoreRestController.save(repositories, member); +// } +// catch (HttpRequestMethodNotSupportedException e) { +// e.printStackTrace(); +// } +// +// Object newArray = Array.newInstance(propertyClass, 1); +// Array.set(newArray, 0, member); +// accessor.setProperty(property, newArray); +// propVal = member; +// } else if (property.isCollectionLike()) { +// Object member = instantiate(propertyClass); +// @SuppressWarnings("unchecked") +// Collection contentCollection = (Collection) accessor.getProperty(property); +// contentCollection.add(member); +// propVal = member; +// } } } else { - if (contentPropertyId != null) { + if (isCollectionElementRequest(contentPropertyId)) { if (property.isArray()) { - Object componentValue = null; - for (Object content : (Object[])propVal) { - if (BeanUtils.hasFieldWithAnnotation(content, ContentId.class) && BeanUtils.getFieldWithAnnotation(content, ContentId.class) != null) { - String candidateId = BeanUtils.getFieldWithAnnotation(content, ContentId.class).toString(); - if (candidateId.equals(contentPropertyId)) { - componentValue = content; - break; - } - } - } - propVal = componentValue; +// Object componentValue = null; +// for (Object content : (Object[]) propVal) { +// if (BeanUtils.hasFieldWithAnnotation(content, ContentId.class) && +// BeanUtils.getFieldWithAnnotation(content, ContentId.class) != null) { +// String candidateId = BeanUtils.getFieldWithAnnotation(content, ContentId.class).toString(); +// if (candidateId.equals(contentPropertyId)) { +// componentValue = content; +// break; +// } +// } +// } +// propVal = componentValue; } else if (property.isCollectionLike()) { - Object componentValue = null; - for (Object content : (Collection)propVal) { - if (BeanUtils.hasFieldWithAnnotation(content, ContentId.class) && BeanUtils.getFieldWithAnnotation(content, ContentId.class) != null) { - String candidateId = BeanUtils.getFieldWithAnnotation(content, ContentId.class).toString(); - if (candidateId.equals(contentPropertyId)) { - componentValue = content; - break; - } - } - } - propVal = componentValue; +// Object componentValue = null; +// for (Object content : (Collection) propVal) { +// if (BeanUtils.hasFieldWithAnnotation(content, ContentId.class) && BeanUtils +// .getFieldWithAnnotation(content, ContentId.class) != null) { +// String candidateId = BeanUtils.getFieldWithAnnotation(content, ContentId.class) +// .toString(); +// if (candidateId.equals(contentPropertyId)) { +// componentValue = content; +// break; +// } +// } +// } +// propVal = componentValue; } - } else if (contentPropertyId == null && + } else if (isCollectionRequest(contentPropertyId) && (PersistentEntityUtils.isPropertyMultiValued(property) && (method.equals(HttpMethod.GET) || method.equals(HttpMethod.DELETE)))) { throw new MethodNotAllowedException("GET", null); - } else if (contentPropertyId == null) { + } else if (isCollectionRequest(contentPropertyId) ) { if (property.isArray()) { - Object member = instantiate(propertyClass); - Object newArray = Array.newInstance(propertyClass,Array.getLength(propVal) + 1); - System.arraycopy(propVal, 0, newArray, 0, Array.getLength(propVal)); - Array.set(newArray, Array.getLength(propVal), member); - accessor.setProperty(property, newArray); - propVal = member; - } else if (property.isCollectionLike()) { +// Object member = instantiate(propertyClass); +// Object newArray = Array.newInstance(propertyClass, Array.getLength(propVal) + 1); +// System.arraycopy(propVal, 0, newArray, 0, Array.getLength(propVal)); +// Array.set(newArray, Array.getLength(propVal), member); +// accessor.setProperty(property, newArray); +// propVal = member; + } + else if (property.isCollectionLike()) { Object member = instantiate(propertyClass); @SuppressWarnings("unchecked") Collection contentCollection = (Collection) accessor.getProperty(property); @@ -240,6 +240,12 @@ else if (property.isCollectionLike()) { } } + // get property store + ContentStoreInfo info = ContentStoreUtils.findContentStore(stores, propertyClass); + if (info == null) { + throw new IllegalStateException(String.format("Store for property %s not found", property.getName())); + } + boolean embeddedProperty = false; if (PersistentEntityUtils.isPropertyMultiValued(property) || propVal.getClass().getAnnotation(Embeddable.class) != null) { embeddedProperty = true; @@ -248,9 +254,25 @@ else if (property.isCollectionLike()) { return resolver.resolve(info, domainObj, propVal, embeddedProperty); } + private boolean isCollectionElementRequest(String contentPropertyId) { + return contentPropertyId != null; + } + + private boolean isCollectionRequest(String contentPropertyId) { + return contentPropertyId == null; + } + private PersistentProperty getContentPropertyDefinition(PersistentEntity persistentEntity, String contentProperty) { PersistentProperty prop = persistentEntity.getPersistentProperty(contentProperty); + if (prop == null) { + for (PersistentProperty candidate : persistentEntity.getPersistentProperties(ContentId.class)) { + if (candidate.getName().contains(contentProperty)) { + prop = candidate; + } + } + } + if (null == prop) { throw new ResourceNotFoundException(); } @@ -258,6 +280,10 @@ private PersistentProperty getContentPropertyDefinition(PersistentEntity propClass) { + return propClass.isPrimitive() || propClass.equals(UUID.class); + } + private Object findOne(RepositoryInvokerFactory repoInvokerFactory, Repositories repositories, String repository, String id) throws HttpRequestMethodNotSupportedException { @@ -308,6 +334,7 @@ protected Object findOne(RepositoryInvokerFactory repoInvokerFactory, Repositori } private Object instantiate(Class clazz) { + Object newObject = null; try { newObject = clazz.newInstance(); diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/links/ContentLinksResourceProcessor.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/links/ContentLinksResourceProcessor.java index da36b0769..8bd2df60f 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/links/ContentLinksResourceProcessor.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/links/ContentLinksResourceProcessor.java @@ -1,14 +1,8 @@ package internal.org.springframework.content.rest.links; -import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -21,18 +15,14 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; -import org.springframework.beans.InvalidPropertyException; import org.springframework.content.commons.annotations.ContentId; import org.springframework.content.commons.storeservice.ContentStoreInfo; import org.springframework.content.commons.storeservice.ContentStoreService; import org.springframework.content.commons.utils.BeanUtils; +import org.springframework.content.rest.StoreRestResource; import org.springframework.content.rest.config.RestConfiguration; import org.springframework.core.io.Resource; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.rest.core.mapping.RepositoryResourceMappings; -import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.BaseUri; import org.springframework.data.rest.webmvc.PersistentEntityResource; import org.springframework.hateoas.Link; @@ -43,7 +33,6 @@ import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder; import static java.lang.String.format; @@ -73,6 +62,10 @@ public ContentLinksResourceProcessor(ContentStoreService stores, RestConfigurati this.config = config; } + RestConfiguration getRestConfiguration() { + return config; + } + public PersistentEntityResource process(final PersistentEntityResource resource) { Object object = resource.getContent(); @@ -85,12 +78,19 @@ public PersistentEntityResource process(final PersistentEntityResource resource) Field[] fields = BeanUtils.findFieldsWithAnnotation(object.getClass(), ContentId.class, new BeanWrapperImpl(object)); if (fields.length == 1) { + if (store != null) { - // for compatibility with v0.x.0 versions - originalLink(config.getBaseUri(), store, entityId).ifPresent((l) -> resource.add(l)); + if (config.fullyQualifiedLinks()) { + resource.add(fullyQualifiedLink(config.getBaseUri(), store, entityId, fields[0].getName())); + } + else { + // for compatibility with v0.x.0 versions + originalLink(config.getBaseUri(), store, entityId).ifPresent((l) -> addLink(resource, l)); - resource.add(shortcutLink(config.getBaseUri(), store, entityId, StringUtils.uncapitalize(ContentStoreUtils.getSimpleName(store)))); + addLink(resource, shortcutLink(config.getBaseUri(), store, entityId, StringUtils + .uncapitalize(ContentStoreUtils.getSimpleName(store)))); + } } } else if (fields.length > 1) { for (Field field : fields) { @@ -101,6 +101,39 @@ public PersistentEntityResource process(final PersistentEntityResource resource) return resource; } + private void addLink(PersistentEntityResource resource, Link l) { + + if (resource.hasLink(l.getRel())) { + for (Link existingLink : resource.getLinks(l.getRel())) { + if (existingLink.getHref().equals(l.getHref())) { + return; + } + } + } + + resource.add(l); + } + + private String propertyLinkRel(ContentStoreInfo storeInfo, String name) { + String contentRel = StringUtils.uncapitalize(ContentStoreUtils.propertyName(name)); + Class storeIface = storeInfo.getInterface(); + StoreRestResource exportSpec = storeIface.getAnnotation(StoreRestResource.class); + if (exportSpec != null && !StringUtils.isEmpty(exportSpec.linkRel())) { + contentRel = exportSpec.linkRel(); + } + return contentRel; + } + + private String entityRel(ContentStoreInfo storeInfo, String defaultLinkRel) { + String entityLinkRel = defaultLinkRel; + Class storeIface = storeInfo.getInterface(); + StoreRestResource exportSpec = storeIface.getAnnotation(StoreRestResource.class); + if (exportSpec != null && !StringUtils.isEmpty(exportSpec.linkRel())) { + entityLinkRel = exportSpec.linkRel(); + } + return entityLinkRel; + } + private Optional originalLink(URI baseUri, ContentStoreInfo store, Object id) { if (id == null) { @@ -110,14 +143,14 @@ private Optional originalLink(URI baseUri, ContentStoreInfo store, Object return Optional.of(shortcutLink(baseUri, store, id, ContentStoreUtils.storePath(store))); } - private Link shortcutLink(URI baseUri, ContentStoreInfo store, Object id, String rel) { + private Link shortcutLink(URI baseUri, ContentStoreInfo store, Object id, String defaultLinkRel) { LinkBuilder builder = null; builder = StoreLinkBuilder.linkTo(new BaseUri(baseUri), store); builder = builder.slash(id); - return builder.withRel(rel); + return builder.withRel(entityRel(store, defaultLinkRel)); } private Link fullyQualifiedLink(URI baseUri, ContentStoreInfo store, Object id, String fieldName) { @@ -128,7 +161,7 @@ private Link fullyQualifiedLink(URI baseUri, ContentStoreInfo store, Object id, String property = StringUtils.uncapitalize(ContentStoreUtils.propertyName(fieldName)); builder = builder.slash(property); - return builder.withRel(property); + return builder.withRel(propertyLinkRel(store, fieldName)); } public static class StoreLinkBuilder extends LinkBuilderSupport { diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/StoreRestResource.java b/spring-content-rest/src/main/java/org/springframework/content/rest/StoreRestResource.java index 94b3678bf..149df217f 100644 --- a/spring-content-rest/src/main/java/org/springframework/content/rest/StoreRestResource.java +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/StoreRestResource.java @@ -18,4 +18,5 @@ */ String path() default ""; + String linkRel() default ""; } diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java b/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java index 5cbeab1f6..c4881eb8d 100644 --- a/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java +++ b/spring-content-rest/src/main/java/org/springframework/content/rest/config/RestConfiguration.java @@ -39,6 +39,7 @@ public class RestConfiguration implements InitializingBean { private URI baseUri = NO_URI; private StoreCorsRegistry corsRegistry; + private boolean fullyQualifiedLinks = false; public RestConfiguration() { this.corsRegistry = new StoreCorsRegistry(); @@ -52,6 +53,14 @@ public void setBaseUri(URI baseUri) { this.baseUri = baseUri; } + public boolean fullyQualifiedLinks() { + return fullyQualifiedLinks; + } + + public void setFullyQualifiedLinks(boolean fullyQualifiedLinks) { + this.fullyQualifiedLinks = fullyQualifiedLinks; + } + public StoreCorsRegistry getCorsRegistry() { return corsRegistry; } diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/controllers/ContentPropertyRestEndpointsIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/controllers/ContentPropertyRestEndpointsIT.java index 305e561c0..321de881d 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/controllers/ContentPropertyRestEndpointsIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/controllers/ContentPropertyRestEndpointsIT.java @@ -1,32 +1,26 @@ package internal.org.springframework.content.rest.controllers; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; -import static java.lang.String.format; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.Optional; import java.util.TimeZone; +import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner; +import internal.org.springframework.content.rest.support.StoreConfig; +import internal.org.springframework.content.rest.support.TestEntity2; +import internal.org.springframework.content.rest.support.TestEntity2Repository; +import internal.org.springframework.content.rest.support.TestEntity3; +import internal.org.springframework.content.rest.support.TestEntity3ContentRepository; +import internal.org.springframework.content.rest.support.TestEntity3Repository; +import internal.org.springframework.content.rest.support.TestEntityChild; +import internal.org.springframework.content.rest.support.TestEntityChildContentRepository; import org.apache.commons.io.IOUtils; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.content.rest.config.RestConfiguration; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; @@ -41,342 +35,563 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; -import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner; - -import internal.org.springframework.content.rest.support.StoreConfig; -import internal.org.springframework.content.rest.support.TestEntity2; -import internal.org.springframework.content.rest.support.TestEntity2Repository; -import internal.org.springframework.content.rest.support.TestEntityChild; -import internal.org.springframework.content.rest.support.TestEntityChildContentRepository; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; +import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(Ginkgo4jSpringRunner.class) // @Ginkgo4jConfiguration(threads=1) @WebAppConfiguration @ContextConfiguration(classes = { - StoreConfig.class, - DelegatingWebMvcConfiguration.class, - RepositoryRestMvcConfiguration.class, - RestConfiguration.class }) + StoreConfig.class, + DelegatingWebMvcConfiguration.class, + RepositoryRestMvcConfiguration.class, + RestConfiguration.class }) @Transactional @ActiveProfiles("store") public class ContentPropertyRestEndpointsIT { - @Autowired - private TestEntity2Repository repository2; + @Autowired private TestEntity2Repository repository2; + @Autowired private TestEntityChildContentRepository contentRepository2; + private TestEntity2 testEntity2; - @Autowired - private TestEntityChildContentRepository contentRepository2; + @Autowired private TestEntity3Repository repository3; + @Autowired private TestEntity3ContentRepository contentRepository3; + private TestEntity3 testEntity3; + +// @Autowired private ArrayEntityRepository arrayEntityRepo; +// @Autowired private ArrayEntityChildRepository arrayEntityChildRepo; +// @Autowired private ArrayEntityChildStore arrayEntityChildStore; +// private ArrayEntity arrayEntity; @Autowired - private WebApplicationContext context; - - private Version versionTests; - private LastModifiedDate lastModifiedDateTests; - - private MockMvc mvc; - - private TestEntity2 testEntity2; - - { - Describe("Content/Content Collection REST Endpoints", () -> { - BeforeEach(() -> { - mvc = MockMvcBuilders.webAppContextSetup(context).build(); - }); - Context("given an Entity with a simple content property", () -> { - BeforeEach(() -> { - testEntity2 = repository2.save(new TestEntity2()); - }); - Context("given that is has no content", () -> { - Context("a GET to /{repository}/{id}/{contentProperty}", () -> { - It("should return 404", () -> { - mvc.perform( - get("/files/" + testEntity2.getId() + "/child")) - .andExpect(status().isNotFound()); - }); - }); - Context("a PUT to /{repository}/{id}/{contentProperty}", () -> { - It("should create the content", () -> { - mvc.perform( - put("/files/" + testEntity2.getId() + "/child") - .content("Hello New Spring Content World!") - .contentType("text/plain")) - .andExpect(status().is2xxSuccessful()); - - Optional fetched = repository2 - .findById(testEntity2.getId()); - assertThat(fetched.isPresent(), is(true)); - assertThat(fetched.get().getChild().contentId, - is(not(nullValue()))); - assertThat(fetched.get().getChild().contentLen, is(31L)); - assertThat(fetched.get().getChild().mimeType, is("text/plain")); - assertThat( - IOUtils.toString(contentRepository2 - .getContent(fetched.get().getChild())), - is("Hello New Spring Content World!")); - - }); - }); - }); - Context("given that it has content", () -> { - BeforeEach(() -> { - String content = "Hello Spring Content World!"; - - testEntity2.setChild(new TestEntityChild()); - testEntity2.getChild().mimeType = "text/plain"; - contentRepository2.setContent(testEntity2.getChild(),new ByteArrayInputStream(content.getBytes())); - testEntity2 = repository2.save(testEntity2); - - versionTests.setMvc(mvc); - versionTests.setUrl("/files/" + testEntity2.getId() + "/child"); - versionTests.setRepo(repository2); - versionTests.setStore(contentRepository2); - versionTests.setEtag(format("\"%s\"", testEntity2.getVersion())); - - lastModifiedDateTests.setMvc(mvc); - lastModifiedDateTests.setUrl("/files/" + testEntity2.getId() + "/child"); - lastModifiedDateTests.setLastModifiedDate(testEntity2.getModifiedDate()); - lastModifiedDateTests.setEtag(testEntity2.getVersion().toString()); - lastModifiedDateTests.setContent(content); - }); - Context("given the content property is accessed via the /{repository}/{id}/{contentProperty} endpoint", () -> { - Context("a GET to /{repository}/{id}/{contentProperty}", () -> { - It("should return the content", () -> { - MockHttpServletResponse response = mvc - .perform(get("/files/" + testEntity2.getId() + "/child") - .accept("text/plain")) - .andExpect(status().isOk()) - .andExpect(header().string("etag", is("\"1\""))) - .andExpect(header().string("last-modified", LastModifiedDate.isWithinASecond(testEntity2.getModifiedDate()))) - .andReturn().getResponse(); - - assertThat(response, is(not(nullValue()))); - assertThat(response.getContentAsString(), is("Hello Spring Content World!")); - }); - }); - Context("a GET to /{repository}/{id}/{contentProperty} with a mime type that matches a renderer", () -> { - It("should return the rendition and 200", () -> { - MockHttpServletResponse response = mvc - .perform(get( - "/files/" + testEntity2.getId() - + "/child") - .accept("text/html")) - .andExpect(status().isOk()).andReturn() - .getResponse(); - - assertThat(response, is(not(nullValue()))); - assertThat(response.getContentAsString(), is( - "Hello Spring Content World!")); - }); - }); - Context("a GET to /{repository}/{id}/{contentProperty} with multiple mime types the last of which matches the content", () -> { - It("should return the original content and 200", () -> { - MockHttpServletResponse response = mvc - .perform(get("/files/" - + testEntity2.getId() - + "/child").accept( - new String[] { "text/xml", - "text/*" })) - .andExpect(status().isOk()).andReturn() - .getResponse(); - - assertThat(response, is(not(nullValue()))); - assertThat(response.getContentAsString(), - is("Hello Spring Content World!")); - }); - }); - Context("a PUT to /{repository}/{id}/{contentProperty}", () -> { - It("should create the content", () -> { - mvc.perform( - put("/files/" + testEntity2.getId() + "/child") - .content("Hello New Spring Content World!") - .contentType("text/plain")) - .andExpect(status().is2xxSuccessful()); - - Optional fetched = repository2 - .findById(testEntity2.getId()); - assertThat(fetched.isPresent(), is(true)); - assertThat(fetched.get().getChild().contentId, - is(not(nullValue()))); - assertThat(fetched.get().getChild().contentLen, is(31L)); - assertThat(fetched.get().getChild().mimeType, is("text/plain")); - }); - }); - Context("a DELETE to /{repository}/{id}/{contentProperty}", () -> { - It("should delete the content", () -> { - mvc.perform(delete( - "/files/" + testEntity2.getId() + "/child")) - .andExpect(status().isNoContent()); - - Optional fetched = repository2 - .findById(testEntity2.getId()); - assertThat(fetched.isPresent(), is(true)); - assertThat(fetched.get().getChild().contentId, is(nullValue())); - assertThat(fetched.get().getChild().contentLen, is(0L)); - }); - }); - - versionTests = Version.tests(); - lastModifiedDateTests = LastModifiedDate.tests(); - }); - - Context("given the content property is accessed via the /{repository}/{id}/{contentProperty}/{contentId} endpoint", () -> { - BeforeEach(() -> { - versionTests.setUrl("/files/" + testEntity2.getId() + "/child/" + testEntity2.getChild().contentId); - lastModifiedDateTests.setUrl("/files/" + testEntity2.getId() + "/child/" + testEntity2.getChild().contentId); - }); - Context("a GET to /{repository}/{id}/{contentProperty}/{contentId}", () -> { - It("should return the content", () -> { - mvc.perform(get("/files/" + testEntity2.getId() + "/child/" + testEntity2.getChild().contentId) - .accept("text/plain")) - .andExpect(status().isOk()) - .andExpect(header().string("etag", is("\"1\""))) - .andExpect(header().string("last-modified", LastModifiedDate.isWithinASecond(testEntity2.getModifiedDate()))) - .andExpect(content().string(is("Hello Spring Content World!"))); - }); - }); - Context("a GET to /{repository}/{id}/{contentProperty}/{contentId} with a mime type that matches a renderer", () -> { - It("should return the rendition and 200", () -> { - MockHttpServletResponse response = mvc - .perform(get( - "/files/" + testEntity2.getId() - + "/child/" - + testEntity2.getChild().contentId) - .accept("text/html")) - .andExpect(status().isOk()).andReturn() - .getResponse(); - - assertThat(response, is(not(nullValue()))); - assertThat(response.getContentAsString(), is( - "Hello Spring Content World!")); - }); - }); - Context("a GET to /{repository}/{id}/{contentProperty}/{contentId} with multiple mime types the last of which matches the content", () -> { - It("should return the original content and 200", () -> { - MockHttpServletResponse response = mvc - .perform(get("/files/" - + testEntity2.getId() - + "/child/" - + testEntity2.getChild().contentId).accept( - new String[] { "text/xml", - "text/*" })) - .andExpect(status().isOk()).andReturn() - .getResponse(); - - assertThat(response, is(not(nullValue()))); - assertThat(response.getContentAsString(), - is("Hello Spring Content World!")); - }); - }); - Context("a PUT to /{repository}/{id}/{contentProperty}/{contentId}", () -> { - It("should overwrite the content", () -> { - mvc.perform(put("/files/" - + testEntity2.getId() + "/child/" - + testEntity2.getChild().contentId).content( - "Hello Modified Spring Content World!") - .contentType("text/plain")) - .andExpect(status().isOk()); - - assertThat( - IOUtils.toString(contentRepository2 - .getContent(testEntity2.getChild())), - is("Hello Modified Spring Content World!")); - }); - }); - Context("a DELETE to /{repository}/{id}/{contentProperty}/{contentId}", () -> { - It("should delete the content", () -> { - mvc.perform(delete("/files/" - + testEntity2.getId() + "/child/" - + testEntity2.getChild().contentId)) - .andExpect(status().isNoContent()); - - Optional fetched = repository2 - .findById(testEntity2.getId()); - assertThat(fetched.isPresent(), is(true)); - }); - }); - - versionTests = Version.tests(); - lastModifiedDateTests = LastModifiedDate.tests(); - }); - }); - }); - - Context("given an Entity with a collection content property", () -> { - BeforeEach(() -> { - testEntity2 = repository2.save(new TestEntity2()); - }); - Context("given that is has no content", () -> { - Context("a GET to /{repository}/{id}/{contentProperty}", () -> { - It("should return 406 MethodNotAllowed", () -> { - mvc.perform(get( - "/files/" + testEntity2.getId() + "/children/")) - .andExpect(status().isMethodNotAllowed()); - }); - }); - Context("a PUT to /{repository}/{id}/{contentProperty}", () -> { - It("should append the content to the entity's content property collection", - () -> { - mvc.perform(put("/files/" + testEntity2.getId() - + "/children/").content( - "Hello New Spring Content World!") - .contentType("text/plain")) - .andExpect(status().is2xxSuccessful()); - - Optional fetched = repository2 - .findById(testEntity2.getId()); - assertThat(fetched.isPresent(), is(true)); - assertThat(fetched.get().getChildren().size(), is(1)); - assertThat(fetched.get().getChildren().get(0).contentLen, - is(31L)); - assertThat(fetched.get().getChildren().get(0).mimeType, - is("text/plain")); - }); - }); - Context("a POST to /{repository}/{id}/{contentProperty}", () -> { - It("should append the content to the entity's content property collection", - () -> { - - String content = "Hello New Spring Content World!"; - - mvc.perform(fileUpload("/files/" - + testEntity2.getId() + "/children/") - .file(new MockMultipartFile("file", - "tests-file.txt", "text/plain", - content.getBytes()))) - .andExpect(status().is2xxSuccessful()); - - Optional fetched = repository2 - .findById(testEntity2.getId()); - assertThat(fetched.isPresent(), is(true)); - assertThat(fetched.get().getChildren().size(), is(1)); - assertThat(fetched.get().getChildren().get(0).contentLen, - is(31L)); - assertThat(fetched.get().getChildren().get(0).fileName, - is("tests-file.txt")); - assertThat(fetched.get().getChildren().get(0).mimeType, - is("text/plain")); - }); - }); - Context("a DELETE to /{repository}/{id}/{contentProperty}", () -> { - It("should return a 405 MethodNotAllowed", () -> { - mvc.perform(delete( - "/files/" + testEntity2.getId() + "/children/")) - .andExpect(status().isMethodNotAllowed()); - }); - }); - }); - }); + private WebApplicationContext context; + + private Version versionTests; + private LastModifiedDate lastModifiedDateTests; + + private MockMvc mvc; + + + + + { + Describe("Content/Content Collection REST Endpoints", () -> { + BeforeEach(() -> { + mvc = MockMvcBuilders.webAppContextSetup(context).build(); + }); + Context("given an Entity with a simple content property", () -> { + BeforeEach(() -> { + testEntity2 = repository2.save(new TestEntity2()); + }); + Context("given that is has no content", () -> { + Context("a GET to /{repository}/{id}/{contentProperty}", () -> { + It("should return 404", () -> { + mvc.perform( + get("/files/" + testEntity2.getId() + "/child")) + .andExpect(status().isNotFound()); + }); + }); + Context("a PUT to /{repository}/{id}/{contentProperty}", () -> { + It("should create the content", () -> { + mvc.perform( + put("/files/" + testEntity2.getId() + "/child") + .content("Hello New Spring Content World!") + .contentType("text/plain")) + .andExpect(status().is2xxSuccessful()); + + Optional fetched = repository2 + .findById(testEntity2.getId()); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getChild().contentId, + is(not(nullValue()))); + assertThat(fetched.get().getChild().contentLen, is(31L)); + assertThat(fetched.get().getChild().mimeType, is("text/plain")); + assertThat( + IOUtils.toString(contentRepository2 + .getContent(fetched.get().getChild())), + is("Hello New Spring Content World!")); + + }); + }); + }); + Context("given that it has content", () -> { + BeforeEach(() -> { + String content = "Hello Spring Content World!"; + + testEntity2.setChild(new TestEntityChild()); + testEntity2.getChild().mimeType = "text/plain"; + contentRepository2 + .setContent(testEntity2.getChild(), new ByteArrayInputStream(content.getBytes())); + testEntity2 = repository2.save(testEntity2); + + versionTests.setMvc(mvc); + versionTests.setUrl("/files/" + testEntity2.getId() + "/child"); + versionTests.setRepo(repository2); + versionTests.setStore(contentRepository2); + versionTests.setEtag(format("\"%s\"", testEntity2.getVersion())); + + lastModifiedDateTests.setMvc(mvc); + lastModifiedDateTests.setUrl("/files/" + testEntity2.getId() + "/child"); + lastModifiedDateTests.setLastModifiedDate(testEntity2.getModifiedDate()); + lastModifiedDateTests.setEtag(testEntity2.getVersion().toString()); + lastModifiedDateTests.setContent(content); + }); + Context("given the content property is accessed via the /{repository}/{id}/{contentProperty} endpoint", () -> { + Context("a GET to /{repository}/{id}/{contentProperty}", () -> { + It("should return the content", () -> { + MockHttpServletResponse response = mvc + .perform(get("/files/" + testEntity2.getId() + "/child") + .accept("text/plain")) + .andExpect(status().isOk()) + .andExpect(header().string("etag", is("\"1\""))) + .andExpect(header().string("last-modified", LastModifiedDate + .isWithinASecond(testEntity2.getModifiedDate()))) + .andReturn().getResponse(); + + assertThat(response, is(not(nullValue()))); + assertThat(response.getContentAsString(), is("Hello Spring Content World!")); + }); + }); + Context("a GET to /{repository}/{id}/{contentProperty} with a mime type that matches a renderer", () -> { + It("should return the rendition and 200", () -> { + MockHttpServletResponse response = mvc + .perform(get( + "/files/" + testEntity2.getId() + + "/child") + .accept("text/html")) + .andExpect(status().isOk()).andReturn() + .getResponse(); + + assertThat(response, is(not(nullValue()))); + assertThat(response.getContentAsString(), is( + "Hello Spring Content World!")); + }); + }); + Context("a GET to /{repository}/{id}/{contentProperty} with multiple mime types the last of which matches the content", () -> { + It("should return the original content and 200", () -> { + MockHttpServletResponse response = mvc + .perform(get("/files/" + + testEntity2.getId() + + "/child").accept( + new String[] {"text/xml", + "text/*"})) + .andExpect(status().isOk()).andReturn() + .getResponse(); + + assertThat(response, is(not(nullValue()))); + assertThat(response.getContentAsString(), + is("Hello Spring Content World!")); + }); + }); + Context("a PUT to /{repository}/{id}/{contentProperty}", () -> { + It("should create the content", () -> { + mvc.perform( + put("/files/" + testEntity2.getId() + "/child") + .content("Hello New Spring Content World!") + .contentType("text/plain")) + .andExpect(status().is2xxSuccessful()); + + Optional fetched = repository2 + .findById(testEntity2.getId()); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getChild().contentId, + is(not(nullValue()))); + assertThat(fetched.get().getChild().contentLen, is(31L)); + assertThat(fetched.get().getChild().mimeType, is("text/plain")); + }); + }); + Context("a DELETE to /{repository}/{id}/{contentProperty}", () -> { + It("should delete the content", () -> { + mvc.perform(delete( + "/files/" + testEntity2.getId() + "/child")) + .andExpect(status().isNoContent()); + + Optional fetched = repository2 + .findById(testEntity2.getId()); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getChild().contentId, is(nullValue())); + assertThat(fetched.get().getChild().contentLen, is(0L)); + }); + }); + + versionTests = Version.tests(); + lastModifiedDateTests = LastModifiedDate.tests(); + }); + + Context("given the content property is accessed via the /{repository}/{id}/{contentProperty}/{contentId} endpoint", () -> { + BeforeEach(() -> { + versionTests.setUrl("/files/" + testEntity2.getId() + "/child/" + testEntity2 + .getChild().contentId); + lastModifiedDateTests.setUrl("/files/" + testEntity2.getId() + "/child/" + testEntity2 + .getChild().contentId); + }); + Context("a GET to /{repository}/{id}/{contentProperty}/{contentId}", () -> { + It("should return the content", () -> { + mvc.perform(get("/files/" + testEntity2.getId() + "/child/" + testEntity2 + .getChild().contentId) + .accept("text/plain")) + .andExpect(status().isOk()) + .andExpect(header().string("etag", is("\"1\""))) + .andExpect(header().string("last-modified", LastModifiedDate + .isWithinASecond(testEntity2.getModifiedDate()))) + .andExpect(content().string(is("Hello Spring Content World!"))); + }); + }); + Context("a GET to /{repository}/{id}/{contentProperty}/{contentId} with a mime type that matches a renderer", () -> { + It("should return the rendition and 200", () -> { + MockHttpServletResponse response = mvc + .perform(get( + "/files/" + testEntity2.getId() + + "/child/" + + testEntity2.getChild().contentId) + .accept("text/html")) + .andExpect(status().isOk()).andReturn() + .getResponse(); + + assertThat(response, is(not(nullValue()))); + assertThat(response.getContentAsString(), is( + "Hello Spring Content World!")); + }); + }); + Context("a GET to /{repository}/{id}/{contentProperty}/{contentId} with multiple mime types the last of which matches the content", () -> { + It("should return the original content and 200", () -> { + MockHttpServletResponse response = mvc + .perform(get("/files/" + + testEntity2.getId() + + "/child/" + + testEntity2.getChild().contentId).accept( + new String[] {"text/xml", + "text/*"})) + .andExpect(status().isOk()).andReturn() + .getResponse(); + + assertThat(response, is(not(nullValue()))); + assertThat(response.getContentAsString(), + is("Hello Spring Content World!")); + }); + }); + Context("a PUT to /{repository}/{id}/{contentProperty}/{contentId}", () -> { + It("should overwrite the content", () -> { + mvc.perform(put("/files/" + + testEntity2.getId() + "/child/" + + testEntity2.getChild().contentId).content( + "Hello Modified Spring Content World!") + .contentType("text/plain")) + .andExpect(status().isOk()); + + assertThat( + IOUtils.toString(contentRepository2 + .getContent(testEntity2.getChild())), + is("Hello Modified Spring Content World!")); + }); + }); + Context("a DELETE to /{repository}/{id}/{contentProperty}/{contentId}", () -> { + It("should delete the content", () -> { + mvc.perform(delete("/files/" + + testEntity2.getId() + "/child/" + + testEntity2.getChild().contentId)) + .andExpect(status().isNoContent()); + + Optional fetched = repository2 + .findById(testEntity2.getId()); + assertThat(fetched.isPresent(), is(true)); + }); + }); + + versionTests = Version.tests(); + lastModifiedDateTests = LastModifiedDate.tests(); + }); + }); + }); + + Context("given an Entity with a primitive content property and content", () -> { + + BeforeEach(() -> { + testEntity3 = repository3.save(new TestEntity3()); + + String content = "Hello Spring Content World!"; + + testEntity3 = contentRepository3 + .setContent(testEntity3, new ByteArrayInputStream(content.getBytes())); + testEntity3.setMimeType("text/plain"); + testEntity3 = repository3.save(testEntity3); + + // versionTests.setMvc(mvc); + // versionTests.setUrl("/files/" + testEntity2.getId() + "/child"); + // versionTests.setRepo(repository2); + // versionTests.setStore(contentRepository2); + // versionTests.setEtag(format("\"%s\"", testEntity2.getVersion())); + // + // lastModifiedDateTests.setMvc(mvc); + // lastModifiedDateTests.setUrl("/files/" + testEntity2.getId() + "/child"); + // lastModifiedDateTests.setLastModifiedDate(testEntity2.getModifiedDate()); + // lastModifiedDateTests.setEtag(testEntity2.getVersion().toString()); + // lastModifiedDateTests.setContent(content); + }); + Context("given the content property is accessed via the /{repository}/{id}/{contentProperty} endpoint", () -> { + Context("a GET to /{repository}/{id}/{contentProperty}", () -> { + It("should return the content", () -> { + MockHttpServletResponse response = mvc + .perform(get("/testEntity3s/" + testEntity3.getId() + "/content") + .accept("text/plain")) + .andExpect(status().isOk()) + // .andExpect(header().string("etag", is("\"1\""))) + // .andExpect(header().string("last-modified", LastModifiedDate.isWithinASecond(testEntity2.getModifiedDate()))) + .andReturn().getResponse(); + + assertThat(response, is(not(nullValue()))); + assertThat(response.getContentAsString(), is("Hello Spring Content World!")); + }); + }); + Context("a GET to /{repository}/{id}/{contentProperty} with a mime type that matches a renderer", () -> { + It("should return the rendition and 200", () -> { + MockHttpServletResponse response = mvc + .perform(get( + "/testEntity3s/" + testEntity3.getId() + + "/content") + .accept("text/html")) + .andExpect(status().isOk()).andReturn() + .getResponse(); + + assertThat(response, is(not(nullValue()))); + assertThat(response.getContentAsString(), is( + "Hello Spring Content World!")); + }); + }); + Context("a GET to /{repository}/{id}/{contentProperty} with multiple mime types the last of which matches the content", () -> { + It("should return the original content and 200", () -> { + MockHttpServletResponse response = mvc + .perform(get("/testEntity3s/" + + testEntity3.getId() + + "/content").accept( + new String[] {"text/xml", + "text/*"})) + .andExpect(status().isOk()).andReturn() + .getResponse(); + + assertThat(response, is(not(nullValue()))); + assertThat(response.getContentAsString(), + is("Hello Spring Content World!")); + }); + }); + Context("a PUT to /{repository}/{id}/{contentProperty}", () -> { + It("should create the content", () -> { + mvc.perform( + put("/testEntity3s/" + testEntity3.getId() + "/content") + .content("Hello New Spring Content World!") + .contentType("text/plain")) + .andExpect(status().is2xxSuccessful()); + + Optional fetched = repository3.findById(testEntity3.getId()); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getContentId(), is(not(nullValue()))); + assertThat(fetched.get().getLen(), is(31L)); + assertThat(fetched.get().getMimeType(), is("text/plain")); + }); + }); + Context("a DELETE to /{repository}/{id}/{contentProperty}", () -> { + It("should delete the content", () -> { + mvc.perform(delete( + "/testEntity3s/" + testEntity3.getId() + "/content")) + .andExpect(status().isNoContent()); + + Optional fetched = repository3.findById(testEntity3.getId()); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getContentId(), is(nullValue())); + assertThat(fetched.get().getLen(), is(0L)); + }); + }); + + // versionTests = Version.tests(); + // lastModifiedDateTests = LastModifiedDate.tests(); + }); }); - } + + Context("given an Entity with a primitive content property but no content", () -> { + + BeforeEach(() -> { + testEntity3 = repository3.save(new TestEntity3()); + + // versionTests.setMvc(mvc); + // versionTests.setUrl("/files/" + testEntity2.getId() + "/child"); + // versionTests.setRepo(repository2); + // versionTests.setStore(contentRepository2); + // versionTests.setEtag(format("\"%s\"", testEntity2.getVersion())); + // + // lastModifiedDateTests.setMvc(mvc); + // lastModifiedDateTests.setUrl("/files/" + testEntity2.getId() + "/child"); + // lastModifiedDateTests.setLastModifiedDate(testEntity2.getModifiedDate()); + // lastModifiedDateTests.setEtag(testEntity2.getVersion().toString()); + // lastModifiedDateTests.setContent(content); + }); + Context("a GET to /{repository}/{id}/{contentProperty}", () -> { + It("should return 404", () -> { + mvc.perform( + get("/testEntity3s/" + testEntity3.getId() + "/content")) + .andExpect(status().isNotFound()); + }); + }); + Context("a PUT to /{repository}/{id}/{contentProperty}", () -> { + It("should create the content", () -> { + mvc.perform( + put("/testEntity3s/" + testEntity3.getId() + "/content") + .content("Hello New Spring Content World!") + .contentType("text/plain")) + .andExpect(status().is2xxSuccessful()); + + Optional fetched = repository3.findById(testEntity3.getId()); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getContentId(), is(not(nullValue()))); + assertThat(fetched.get().getLen(), is(31L)); + assertThat(fetched.get().getMimeType(), is("text/plain")); + try (InputStream actual = contentRepository3.getContent(fetched.get())) { + assertThat(IOUtils.toString(actual), is("Hello New Spring Content World!")); + } + }); + }); + }); + +// Context("given an Entity with a array content property", () -> { +// BeforeEach(() -> { +// arrayEntity = arrayEntityRepo.save(new ArrayEntity()); +// }); +// Context("given that is has no content", () -> { +// Context("a GET to /{repository}/{id}/{contentProperty}", () -> { +// It("should return 406 MethodNotAllowed", () -> { +// mvc.perform(get( +// "/testEntity6s/" + arrayEntity.getId() + "/childArray/")) +// .andExpect(status().isMethodNotAllowed()); +// }); +// }); +// }); +// }); + + Context("given an Entity with a collection content property", () -> { + BeforeEach(() -> { + testEntity2 = repository2.save(new TestEntity2()); + }); + Context("given that is has no content", () -> { + Context("a GET to /{repository}/{id}/{contentProperty}", () -> { + It("should return 406 MethodNotAllowed", () -> { + mvc.perform(get( + "/files/" + testEntity2.getId() + "/children/")) + .andExpect(status().isMethodNotAllowed()); + }); + }); + Context("a PUT to /{repository}/{id}/{contentProperty}", () -> { + It("should append the content to the entity's content property collection", + () -> { + mvc.perform(put("/files/" + testEntity2.getId() + + "/children/").content( + "Hello New Spring Content World!") + .contentType("text/plain")) + .andExpect(status().is2xxSuccessful()); + + Optional fetched = repository2 + .findById(testEntity2.getId()); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getChildren().size(), is(1)); + assertThat(fetched.get().getChildren().get(0).contentLen, + is(31L)); + assertThat(fetched.get().getChildren().get(0).mimeType, + is("text/plain")); + }); + }); + Context("a POST to /{repository}/{id}/{contentProperty}", () -> { + It("should append the content to the entity's content property collection", + () -> { + + String content = "Hello New Spring Content World!"; + + mvc.perform(fileUpload("/files/" + + testEntity2.getId() + "/children/") + .file(new MockMultipartFile("file", + "tests-file.txt", "text/plain", + content.getBytes()))) + .andExpect(status().is2xxSuccessful()); + + Optional fetched = repository2 + .findById(testEntity2.getId()); + assertThat(fetched.isPresent(), is(true)); + assertThat(fetched.get().getChildren().size(), is(1)); + assertThat(fetched.get().getChildren().get(0).contentLen, + is(31L)); + assertThat(fetched.get().getChildren().get(0).fileName, + is("tests-file.txt")); + assertThat(fetched.get().getChildren().get(0).mimeType, + is("text/plain")); + }); + }); + Context("a DELETE to /{repository}/{id}/{contentProperty}", () -> { + It("should return a 405 MethodNotAllowed", () -> { + mvc.perform(delete( + "/files/" + testEntity2.getId() + "/children/")) + .andExpect(status().isMethodNotAllowed()); + }); + }); + }); + }); + }); + } + +// @Entity +// @Getter +// @Setter +// @NoArgsConstructor +// public static class ArrayEntity { +// +// @Id +// private Long id; +// +// private ArrayEntityChild[] childArray; +// } +// +// public interface ArrayEntityRepository extends CrudRepository {} +// +// @Entity +// @Getter +// @Setter +// @NoArgsConstructor +// public static class ArrayEntityChild { +// +// @Id +// private Long id; +// +// @ContentId +// private UUID contentId; +// +// @ContentLength +// private Long contentLen; +// +// @MimeType +// private String mimeType; +// } +// +// public interface ArrayEntityChildRepository extends CrudRepository {} +// public interface ArrayEntityChildStore extends ContentStore {} @Test - public void noop() { - } - - private static String toHeaderDateFormat(Date dt) { - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("GMT")); - return format.format(dt); - } + public void noop() { + } + + private static String toHeaderDateFormat(Date dt) { + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + return format.format(dt); + } } diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/BaseUriContentLinksIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/BaseUriContentLinksIT.java index 65a88c196..19f887f68 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/BaseUriContentLinksIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/BaseUriContentLinksIT.java @@ -4,12 +4,9 @@ import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner; import internal.org.springframework.content.rest.support.BaseUriConfig; import internal.org.springframework.content.rest.support.TestEntity; -import internal.org.springframework.content.rest.support.TestEntity2; -import internal.org.springframework.content.rest.support.TestEntity2Repository; import internal.org.springframework.content.rest.support.TestEntity3; import internal.org.springframework.content.rest.support.TestEntity3ContentRepository; import internal.org.springframework.content.rest.support.TestEntity3Repository; -import internal.org.springframework.content.rest.support.TestEntityChildContentRepository; import internal.org.springframework.content.rest.support.TestEntityContentRepository; import internal.org.springframework.content.rest.support.TestEntityRepository; import org.junit.Test; @@ -62,7 +59,7 @@ public class BaseUriContentLinksIT { private TestEntity testEntity; private TestEntity3 testEntity3; - private EntityContentLinkTests entityContentLinkTests; + private ContentLinkTests contentLinkTests; { Describe("given the spring content baseUri property is set to contentApi", () -> { @@ -74,30 +71,15 @@ public class BaseUriContentLinksIT { BeforeEach(() -> { testEntity3 = repository3.save(new TestEntity3()); - entityContentLinkTests.setMvc(mvc); - entityContentLinkTests.setRepository(repository3); - entityContentLinkTests.setStore(contentRepository3); - entityContentLinkTests.setTestEntity(testEntity3); - entityContentLinkTests.setUrl("/api/testEntity3s/" + testEntity3.getId()); - entityContentLinkTests.setLinkRel("testEntity3s"); - entityContentLinkTests.setExpectedLinkRegex("http://localhost/contentApi/testEntity3s/" + testEntity3.getId()); + contentLinkTests.setMvc(mvc); + contentLinkTests.setRepository(repository3); + contentLinkTests.setStore(contentRepository3); + contentLinkTests.setTestEntity(testEntity3); + contentLinkTests.setUrl("/api/testEntity3s/" + testEntity3.getId()); + contentLinkTests.setLinkRel("testEntity3"); + contentLinkTests.setExpectedLinkRegex("http://localhost/contentApi/testEntity3s/" + testEntity3.getId() ); }); - entityContentLinkTests = new EntityContentLinkTests(); - }); - - Context("given an Entity and a Store specifying a store path", () -> { - BeforeEach(() -> { - testEntity = repository.save(new TestEntity()); - - entityContentLinkTests.setMvc(mvc); - entityContentLinkTests.setRepository(repository); - entityContentLinkTests.setStore(contentRepository); - entityContentLinkTests.setTestEntity(testEntity); - entityContentLinkTests.setUrl("/api/testEntities/" + testEntity.getId()); - entityContentLinkTests.setLinkRel("testEntity"); - entityContentLinkTests.setExpectedLinkRegex("http://localhost/contentApi/testEntitiesContent/" + testEntity.getId()); - }); - entityContentLinkTests = new EntityContentLinkTests(); + contentLinkTests = new ContentLinkTests(); }); }); } diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkRelIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkRelIT.java new file mode 100644 index 000000000..c2fa3a0c9 --- /dev/null +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkRelIT.java @@ -0,0 +1,90 @@ +package internal.org.springframework.content.rest.links; + +import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; +import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner; +import internal.org.springframework.content.rest.support.BaseUriConfig; +import internal.org.springframework.content.rest.support.TestEntity; +import internal.org.springframework.content.rest.support.TestEntity3; +import internal.org.springframework.content.rest.support.TestEntity3ContentRepository; +import internal.org.springframework.content.rest.support.TestEntity3Repository; +import internal.org.springframework.content.rest.support.TestEntityContentRepository; +import internal.org.springframework.content.rest.support.TestEntityRepository; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.content.rest.config.HypermediaConfiguration; +import org.springframework.content.rest.config.RestConfiguration; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; + +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; + +@RunWith(Ginkgo4jSpringRunner.class) +@Ginkgo4jConfiguration(threads = 1) +@WebAppConfiguration +@ContextConfiguration(classes = { + BaseUriConfig.class, + DelegatingWebMvcConfiguration.class, + RepositoryRestMvcConfiguration.class, + RestConfiguration.class, + HypermediaConfiguration.class }) +@Transactional +@ActiveProfiles("store") +public class ContentLinkRelIT { + + @Autowired + TestEntityRepository repository; + @Autowired + TestEntityContentRepository contentRepository; + @Autowired + TestEntity3Repository repository3; + @Autowired + TestEntity3ContentRepository contentRepository3; + + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + private TestEntity testEntity; + private TestEntity3 testEntity3; + + private ContentLinkTests contentLinkTests; + + { + Describe("given an exporting store specifying a linkRel of foo", () -> { + BeforeEach(() -> { + mvc = MockMvcBuilders.webAppContextSetup(context).build(); + }); + + Context("given an Entity and a Store specifying a linkRel and a store path", () -> { + BeforeEach(() -> { + testEntity = repository.save(new TestEntity()); + + contentLinkTests.setMvc(mvc); + contentLinkTests.setRepository(repository); + contentLinkTests.setStore(contentRepository); + contentLinkTests.setTestEntity(testEntity); + contentLinkTests.setUrl("/api/testEntities/" + testEntity.getId()); + contentLinkTests.setLinkRel("foo"); + contentLinkTests.setExpectedLinkRegex("http://localhost/contentApi/testEntitiesContent/" + testEntity.getId() ); + }); + contentLinkTests = new ContentLinkTests(); + }); + }); + } + + @Test + public void noop() { + } +} diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinkTests.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkTests.java similarity index 95% rename from spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinkTests.java rename to spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkTests.java index fbf6d68a7..0643bb2c3 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinkTests.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinkTests.java @@ -2,7 +2,10 @@ import java.io.ByteArrayInputStream; import java.io.StringReader; +import java.util.List; +import java.util.regex.Matcher; +import com.theoryinpractise.halbuilder.api.Link; import com.theoryinpractise.halbuilder.api.ReadableRepresentation; import com.theoryinpractise.halbuilder.api.RepresentationFactory; import com.theoryinpractise.halbuilder.standard.StandardRepresentationFactory; @@ -17,7 +20,6 @@ import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.FIt; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.CoreMatchers.is; @@ -29,7 +31,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Setter -public class EntityContentLinkTests { +public class ContentLinkTests { private MockMvc mvc; diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksIT.java index a14954e21..1fdcb2fbf 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksIT.java @@ -59,7 +59,7 @@ public class ContentLinksIT { private TestEntity testEntity; private TestEntity3 testEntity3; - private EntityContentLinkTests entityContentLinkTests; + private ContentLinkTests contentLinkTests; { Describe("given no baseUri are set", () -> { @@ -71,30 +71,15 @@ public class ContentLinksIT { BeforeEach(() -> { testEntity3 = repository3.save(new TestEntity3()); - entityContentLinkTests.setMvc(mvc); - entityContentLinkTests.setRepository(repository3); - entityContentLinkTests.setStore(contentRepository3); - entityContentLinkTests.setTestEntity(testEntity3); - entityContentLinkTests.setUrl("/testEntity3s/" + testEntity3.getId()); - entityContentLinkTests.setLinkRel("testEntity3"); - entityContentLinkTests.setExpectedLinkRegex("http://localhost/testEntity3s/" + testEntity3.getId()); + contentLinkTests.setMvc(mvc); + contentLinkTests.setRepository(repository3); + contentLinkTests.setStore(contentRepository3); + contentLinkTests.setTestEntity(testEntity3); + contentLinkTests.setUrl("/testEntity3s/" + testEntity3.getId()); + contentLinkTests.setLinkRel("testEntity3"); + contentLinkTests.setExpectedLinkRegex("http://localhost/testEntity3s/" + testEntity3.getId() ); }); - entityContentLinkTests = new EntityContentLinkTests(); - }); - - Context("given an Entity and a Store specifying a store path", () -> { - BeforeEach(() -> { - testEntity = repository.save(new TestEntity()); - - entityContentLinkTests.setMvc(mvc); - entityContentLinkTests.setRepository(repository); - entityContentLinkTests.setStore(contentRepository); - entityContentLinkTests.setTestEntity(testEntity); - entityContentLinkTests.setUrl("/testEntities/" + testEntity.getId()); - entityContentLinkTests.setLinkRel("testEntity"); - entityContentLinkTests.setExpectedLinkRegex("http://localhost/testEntitiesContent/" + testEntity.getId()); - }); - entityContentLinkTests = new EntityContentLinkTests(); + contentLinkTests = new ContentLinkTests(); }); }); } diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksResourceProcessorIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksResourceProcessorIT.java index 67b02d8d7..95ca6bd75 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksResourceProcessorIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContentLinksResourceProcessorIT.java @@ -29,10 +29,10 @@ import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.AfterEach; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.FIt; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It; import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.JustBeforeEach; import static org.hamcrest.CoreMatchers.hasItem; @@ -93,9 +93,23 @@ public class ContentLinksResourceProcessorIT { resource = build.build(); }); - It("should add an original link and a shortcut link for the content id property", () -> { - assertThat(resource.getLinks("testEntity3s"), hasItem(hasProperty("href", is("http://localhost/contentApi/testEntity3s/999")))); + It("should add an entity content links", () -> { assertThat(resource.getLinks("testEntity3"), hasItem(hasProperty("href", is("http://localhost/contentApi/testEntity3s/999")))); + assertThat(resource.getLinks("testEntity3s"), hasItem(hasProperty("href", is("http://localhost/contentApi/testEntity3s/999")))); + }); + + Context("when fully qualified links property is true", () -> { + BeforeEach(() -> { + processor.getRestConfiguration().setFullyQualifiedLinks(true); + }); + + AfterEach(() -> { + processor.getRestConfiguration().setFullyQualifiedLinks(false); + }); + + It("should add an entity content link against a 'content' linkrel", () -> { + assertThat(resource.getLinks("content"), hasItem(hasProperty("href", is("http://localhost/contentApi/testEntity3s/999/content")))); + }); }); }); diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContextPathContentLinksIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContextPathContentLinksIT.java index 947d7e622..b7a0e3390 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContextPathContentLinksIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/ContextPathContentLinksIT.java @@ -66,7 +66,7 @@ public class ContextPathContentLinksIT { private TestEntity3 testEntity3; - private EntityContentLinkTests entityContentLinkTests; + private ContentLinkTests contentLinkTests; { Describe("given the spring content baseUri property is set to contentApi", () -> { @@ -78,16 +78,16 @@ public class ContextPathContentLinksIT { BeforeEach(() -> { testEntity3 = repository3.save(new TestEntity3()); - entityContentLinkTests.setMvc(mvc); - entityContentLinkTests.setRepository(repository3); - entityContentLinkTests.setStore(contentRepository3); - entityContentLinkTests.setTestEntity(testEntity3); - entityContentLinkTests.setUrl("/contextPath/testEntity3s/" + testEntity3.getId()); - entityContentLinkTests.setContextPath("/contextPath"); - entityContentLinkTests.setLinkRel("testEntity3s"); - entityContentLinkTests.setExpectedLinkRegex("http://localhost/contextPath/testEntity3s/" + testEntity3.getId()); + contentLinkTests.setMvc(mvc); + contentLinkTests.setRepository(repository3); + contentLinkTests.setStore(contentRepository3); + contentLinkTests.setTestEntity(testEntity3); + contentLinkTests.setUrl("/contextPath/testEntity3s/" + testEntity3.getId()); + contentLinkTests.setContextPath("/contextPath"); + contentLinkTests.setLinkRel("testEntity3"); + contentLinkTests.setExpectedLinkRegex("http://localhost/contextPath/testEntity3s/" + testEntity3.getId() ); }); - entityContentLinkTests = new EntityContentLinkTests(); + contentLinkTests = new ContentLinkTests(); }); }); } diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinksIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinksIT.java new file mode 100644 index 000000000..ddd31772e --- /dev/null +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/links/EntityContentLinksIT.java @@ -0,0 +1,94 @@ +package internal.org.springframework.content.rest.links; + +import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; +import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner; +import internal.org.springframework.content.rest.support.StoreConfig; +import internal.org.springframework.content.rest.support.TestEntity; +import internal.org.springframework.content.rest.support.TestEntity3; +import internal.org.springframework.content.rest.support.TestEntity3ContentRepository; +import internal.org.springframework.content.rest.support.TestEntity3Repository; +import internal.org.springframework.content.rest.support.TestEntityContentRepository; +import internal.org.springframework.content.rest.support.TestEntityRepository; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.content.rest.config.HypermediaConfiguration; +import org.springframework.content.rest.config.RestConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration; + +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.BeforeEach; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context; +import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe; + +@RunWith(Ginkgo4jSpringRunner.class) +@Ginkgo4jConfiguration(threads = 1) +@WebAppConfiguration +@ContextConfiguration(classes = { + StoreConfig.class, + DelegatingWebMvcConfiguration.class, + RepositoryRestMvcConfiguration.class, + RestConfiguration.class, + HypermediaConfiguration.class }) +@Transactional +@ActiveProfiles("store") +public class EntityContentLinksIT { + + @Autowired + TestEntityRepository repository; + @Autowired + TestEntityContentRepository contentRepository; + @Autowired + TestEntity3Repository repository3; + @Autowired + TestEntity3ContentRepository contentRepository3; + + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + private TestEntity testEntity; + private TestEntity3 testEntity3; + + private ContentLinkTests contentLinkTests; + + { + Describe("EntityLinks", () -> { + + BeforeEach(() -> { + mvc = MockMvcBuilders.webAppContextSetup(context).build(); + }); + + Context("when entity links are enabled", () -> { + + BeforeEach(() -> { + testEntity3 = repository3.save(new TestEntity3()); + + contentLinkTests.setMvc(mvc); + contentLinkTests.setRepository(repository3); + contentLinkTests.setStore(contentRepository3); + contentLinkTests.setTestEntity(testEntity3); + contentLinkTests.setUrl("/testEntity3s/" + testEntity3.getId()); + contentLinkTests.setLinkRel("testEntity3s"); + contentLinkTests.setExpectedLinkRegex("http://localhost/testEntity3s/" + testEntity3.getId()); + }); + contentLinkTests = new ContentLinkTests(); + }); + }); + } + + @Test + public void noop() { + } +} diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntity6.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntity6.java index 395e8dc9a..157fc33d1 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntity6.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntity6.java @@ -9,6 +9,7 @@ import org.springframework.content.commons.annotations.ContentId; import org.springframework.content.commons.annotations.ContentLength; import org.springframework.content.commons.annotations.MimeType; +import org.springframework.data.rest.core.annotation.RestResource; import lombok.Getter; import lombok.Setter; diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntityContentRepository.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntityContentRepository.java index 60f89240b..e7b8ad672 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntityContentRepository.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/support/TestEntityContentRepository.java @@ -2,11 +2,10 @@ import org.springframework.content.commons.renditions.Renderable; import org.springframework.content.fs.store.FilesystemContentStore; +import org.springframework.content.rest.StoreRestResource; import org.springframework.web.bind.annotation.CrossOrigin; -import internal.org.springframework.content.rest.annotations.ContentStoreRestResource; - @CrossOrigin(origins = "http://www.someurl.com") -@ContentStoreRestResource(path = "testEntitiesContent") +@StoreRestResource(path = "testEntitiesContent", linkRel ="foo") public interface TestEntityContentRepository extends FilesystemContentStore, Renderable { }