From 0d0dc6d9ea90446aebef585fcc2daf55ee95efb3 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Thu, 25 Aug 2022 22:34:59 -0700 Subject: [PATCH] MappingContext now generates a single content path per content property - Fixes #1048 --- .../ContentPropertyBuilderVisitor.java | 13 +- .../mappingcontext/ClassWalkerTest.java | 43 +--- .../src/main/asciidoc/rest-store.adoc | 17 -- .../ContentStoreContentService.java | 10 +- ...ResourceHandlerMethodArgumentResolver.java | 6 +- .../AssociativeStoreResourceResolver.java | 14 +- .../resolvers/DefaultEntityResolver.java | 31 +-- .../resolvers/EntityResolution.java | 4 +- .../resolvers/ResourceResolver.java | 4 +- .../resolvers/RevisionEntityResolver.java | 5 +- .../resolvers/StoreResourceResolver.java | 14 +- .../rest/io/AssociatedStoreResource.java | 3 + .../rest/io/AssociatedStoreResourceImpl.java | 21 +- .../links/ContentLinksResourceProcessor.java | 4 +- .../ContentLinksResourceProcessorIT.java | 5 +- .../StoreResolverRestConfigurationIT.java | 25 +- .../ContentPropertyRestEndpointsIT.java | 222 ++++++------------ 17 files changed, 161 insertions(+), 280 deletions(-) diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentPropertyBuilderVisitor.java b/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentPropertyBuilderVisitor.java index e14da34d2..1d85ad22a 100644 --- a/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentPropertyBuilderVisitor.java +++ b/spring-content-commons/src/main/java/org/springframework/content/commons/mappingcontext/ContentPropertyBuilderVisitor.java @@ -25,8 +25,6 @@ /** * Returns a map of "path"'s to content properties for the given class. * - * - * * @author warrenpa * */ @@ -102,8 +100,11 @@ public boolean visitClassEnd(Class klazz) { } this.properties = new HashMap(); - this.properties.put(fullyQualify(propertyName(""), this.getKeySeparator()), contentProperty); - this.properties.put(contentProperty.getContentPropertyPath().replace(this.getContentPropertySeparator(), this.getKeySeparator()), contentProperty); + if (isNotRootContentProperty()) { + this.properties.put(fullyQualify(propertyName(""), this.getKeySeparator()), contentProperty); + } else { + this.properties.put(contentProperty.getContentPropertyPath().replace(this.getContentPropertySeparator(), this.getKeySeparator()), contentProperty); + } } for (Entry entry : subProperties.entrySet()) { @@ -214,6 +215,10 @@ protected String propertyName(String name) { return segments[0]; } + private boolean isNotRootContentProperty() { + return StringUtils.hasLength(fullyQualify(propertyName(""), this.getKeySeparator())); + } + private static String[] split(String name) { if (!StringUtils.hasLength(name)) { return new String[]{}; diff --git a/spring-content-commons/src/test/java/org/springframework/content/commons/mappingcontext/ClassWalkerTest.java b/spring-content-commons/src/test/java/org/springframework/content/commons/mappingcontext/ClassWalkerTest.java index d40b0770b..1e4b0cf01 100644 --- a/spring-content-commons/src/test/java/org/springframework/content/commons/mappingcontext/ClassWalkerTest.java +++ b/spring-content-commons/src/test/java/org/springframework/content/commons/mappingcontext/ClassWalkerTest.java @@ -60,7 +60,7 @@ public class ClassWalkerTest { expectedSubClassProperty.setContentLengthPropertyPath("child.contentLength"); expectedSubClassProperty.setMimeTypePropertyPath("child.contentMimeType"); expectedSubClassProperty.setOriginalFileNamePropertyPath("child.contentOriginalFileName"); - assertThat(visitor.getProperties(), hasEntry("child/content", expectedSubClassProperty)); + assertThat(visitor.getProperties(), hasEntry("child", expectedSubClassProperty)); ContentProperty expectedSubSubClassProperty = new ContentProperty(); expectedSubSubClassProperty.setContentPropertyPath("child.subChild.content"); @@ -69,7 +69,6 @@ public class ClassWalkerTest { expectedSubSubClassProperty.setMimeTypePropertyPath("child.subChild.contentMimeType"); expectedSubSubClassProperty.setOriginalFileNamePropertyPath("child.subChild.contentOriginalFileName"); assertThat(visitor.getProperties(), hasEntry("child/subChild", expectedSubSubClassProperty)); - assertThat(visitor.getProperties(), hasEntry("child/subChild/content", expectedSubSubClassProperty)); }); }); @@ -79,14 +78,6 @@ public class ClassWalkerTest { ClassWalker walker = new ClassWalker(UncorrelatedAttrClass.class); walker.accept(visitor); - ContentProperty expectedProperty = new ContentProperty(); - expectedProperty.setContentPropertyPath("content"); - expectedProperty.setContentIdPropertyPath("contentId"); - expectedProperty.setContentLengthPropertyPath("len"); - expectedProperty.setMimeTypePropertyPath("mimeType"); - expectedProperty.setOriginalFileNamePropertyPath("originalFileName"); - assertThat(visitor.getProperties(), hasEntry("", expectedProperty)); - ContentProperty expectedProperty2 = new ContentProperty(); expectedProperty2.setContentPropertyPath("content"); expectedProperty2.setContentIdPropertyPath("contentId"); @@ -103,14 +94,6 @@ public class ClassWalkerTest { ClassWalker walker = new ClassWalker(CorrelatedAttrClass.class); walker.accept(visitor); - ContentProperty expectedProperty = new ContentProperty(); - expectedProperty.setContentPropertyPath("content"); - expectedProperty.setContentIdPropertyPath("contentId"); - expectedProperty.setContentLengthPropertyPath("contentLen"); - expectedProperty.setMimeTypePropertyPath("contentMimeType"); - expectedProperty.setOriginalFileNamePropertyPath("contentOriginalFileName"); - assertThat(visitor.getProperties(), hasEntry("", expectedProperty)); - ContentProperty expectedProperty2 = new ContentProperty(); expectedProperty2.setContentPropertyPath("content"); expectedProperty2.setContentIdPropertyPath("contentId"); @@ -134,14 +117,6 @@ public class ClassWalkerTest { expectedProperty.setMimeTypePropertyPath("child.mimeType"); expectedProperty.setOriginalFileNamePropertyPath("child.originalFileName"); assertThat(visitor.getProperties(), hasEntry("child", expectedProperty)); - - ContentProperty expectedProperty2 = new ContentProperty(); - expectedProperty2.setContentPropertyPath("child.content"); - expectedProperty2.setContentIdPropertyPath("child.contentId"); - expectedProperty2.setContentLengthPropertyPath("child.len"); - expectedProperty2.setMimeTypePropertyPath("child.mimeType"); - expectedProperty2.setOriginalFileNamePropertyPath("child.originalFileName"); - assertThat(visitor.getProperties(), hasEntry("child/content", expectedProperty2)); }); }); @@ -185,14 +160,6 @@ public class ClassWalkerTest { expectedProperty.setOriginalFileNamePropertyPath("child1.originalFileName"); assertThat(visitor.getProperties(), hasEntry("child1", expectedProperty)); - ContentProperty expectedProperty2 = new ContentProperty(); - expectedProperty2.setContentPropertyPath("child1.content"); - expectedProperty2.setContentIdPropertyPath("child1.contentId"); - expectedProperty2.setContentLengthPropertyPath("child1.len"); - expectedProperty2.setMimeTypePropertyPath("child1.mimeType"); - expectedProperty2.setOriginalFileNamePropertyPath("child1.originalFileName"); - assertThat(visitor.getProperties(), hasEntry("child1/content", expectedProperty2)); - ContentProperty expectedProperty3 = new ContentProperty(); expectedProperty3.setContentPropertyPath("child2.content"); expectedProperty3.setContentIdPropertyPath("child2.contentId"); @@ -200,14 +167,6 @@ public class ClassWalkerTest { expectedProperty3.setMimeTypePropertyPath("child2.mimeType"); expectedProperty3.setOriginalFileNamePropertyPath("child2.originalFileName"); assertThat(visitor.getProperties(), hasEntry("child2", expectedProperty3)); - - ContentProperty expectedProperty4 = new ContentProperty(); - expectedProperty4.setContentPropertyPath("child2.content"); - expectedProperty4.setContentIdPropertyPath("child2.contentId"); - expectedProperty4.setContentLengthPropertyPath("child2.len"); - expectedProperty4.setMimeTypePropertyPath("child2.mimeType"); - expectedProperty4.setOriginalFileNamePropertyPath("child2.originalFileName"); - assertThat(visitor.getProperties(), hasEntry("child2/content", expectedProperty4)); }); }); } diff --git a/spring-content-rest/src/main/asciidoc/rest-store.adoc b/spring-content-rest/src/main/asciidoc/rest-store.adoc index bac8e3148..1329e013a 100644 --- a/spring-content-rest/src/main/asciidoc/rest-store.adoc +++ b/spring-content-rest/src/main/asciidoc/rest-store.adoc @@ -413,20 +413,3 @@ All content types except `application/json` ==== DELETE Removes the Resource's content - -== The Collection Property Resource - -Spring Content REST exports Property Collection Resources to `/{store}/{id}/{property}`. The resource path can be -customized using the `@StoreRestResource` annotation on the Store interface. - -=== Supported HTTP Methods - -Content collection resources support `PUT` and `POST`. - -==== PUT/POST - -Appends new content to the collection of Resources - -===== Supported media types - -All content types except `application/json` diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java index b6277dba8..66e7b42f2 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java @@ -71,7 +71,7 @@ public void getContent(HttpServletRequest request, HttpServletResponse response, AssociatedStoreResource storeResource = (AssociatedStoreResource)resource; ContentProperty property = storeResource.getContentProperty(); - Method[] methodsToUse = getExportedMethodsFor(storeResource.getStoreInfo().getInterface(), PropertyPath.from(property.getContentPropertyPath())).getContentMethods(); + Method[] methodsToUse = getExportedMethodsFor(storeResource.getStoreInfo().getInterface(), storeResource.getPropertyPath()).getContentMethods(); if (methodsToUse.length > 1) { throw new IllegalStateException("Too many getContent methods"); @@ -156,7 +156,7 @@ public void setContent(HttpServletRequest request, HttpServletResponse response, property.setOriginalFileName(domainObject, source.getFilename()); } - Method[] methodsToUse = getExportedMethodsFor(storeResource.getStoreInfo().getInterface(), PropertyPath.from(property.getContentPropertyPath())).setContentMethods(); + Method[] methodsToUse = getExportedMethodsFor(storeResource.getStoreInfo().getInterface(), storeResource.getPropertyPath()).setContentMethods(); if (methodsToUse.length > 1) { RestConfiguration.DomainTypeConfig dtConfig = config.forDomainType(storeResource.getStoreInfo().getDomainObjectClass()); @@ -177,7 +177,7 @@ public void setContent(HttpServletRequest request, HttpServletResponse response, try { Object targetObj = storeResource.getStoreInfo().getImplementation(ContentStore.class); ReflectionUtils.makeAccessible(methodToUse); - Object updatedDomainObj = ReflectionUtils.invokeMethod(methodToUse, targetObj, domainObject, PropertyPath.from(property.getContentPropertyPath()), contentArg); + Object updatedDomainObj = ReflectionUtils.invokeMethod(methodToUse, targetObj, domainObject, storeResource.getPropertyPath(), contentArg); repoInvoker.invokeSave(updatedDomainObj); } finally { @@ -191,7 +191,7 @@ public void unsetContent(Resource resource) throws MethodNotAllowedException { AssociatedStoreResource storeResource = (AssociatedStoreResource)resource; ContentProperty property = storeResource.getContentProperty(); - Method[] methodsToUse = getExportedMethodsFor(storeResource.getStoreInfo().getInterface(), PropertyPath.from(property.getContentPropertyPath())).unsetContentMethods(); + Method[] methodsToUse = getExportedMethodsFor(storeResource.getStoreInfo().getInterface(), storeResource.getPropertyPath()).unsetContentMethods(); if (methodsToUse.length == 0) { throw new MethodNotAllowedException(); @@ -207,7 +207,7 @@ public void unsetContent(Resource resource) throws MethodNotAllowedException { ReflectionUtils.makeAccessible(methodsToUse[0]); - Object updatedDomainObj = ReflectionUtils.invokeMethod(methodsToUse[0], targetObj, updateObject, PropertyPath.from(property.getContentPropertyPath())); + Object updatedDomainObj = ReflectionUtils.invokeMethod(methodsToUse[0], targetObj, updateObject, storeResource.getPropertyPath()); updateObject = updatedDomainObj; property.setMimeType(updateObject, null); 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 18b3d9336..e16696e2c 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 @@ -58,8 +58,8 @@ public ResourceHandlerMethodArgumentResolver(ApplicationContext context, RestCon this.entityResolvers = entityResolvers; - resolvers.add(new StoreResourceResolver()); - resolvers.add(new AssociativeStoreResourceResolver()); + resolvers.add(new StoreResourceResolver(this.mappingContext)); + resolvers.add(new AssociativeStoreResourceResolver(this.mappingContext)); } RestConfiguration getConfig() { @@ -126,7 +126,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m } } - return matchedResolver.resolve(webRequest, info, result.getEntity(), result.getContentProperty()); + return matchedResolver.resolve(webRequest, info, result.getEntity(), result.getProperty()); } else if (Store.class.isAssignableFrom(info.getInterface())) { diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/AssociativeStoreResourceResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/AssociativeStoreResourceResolver.java index d84e24197..610c367e7 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/AssociativeStoreResourceResolver.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/AssociativeStoreResourceResolver.java @@ -1,6 +1,6 @@ package internal.org.springframework.content.rest.controllers.resolvers; -import org.springframework.content.commons.mappingcontext.ContentProperty; +import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.AssociativeStore; import org.springframework.content.commons.storeservice.StoreInfo; @@ -11,14 +11,20 @@ public class AssociativeStoreResourceResolver implements ResourceResolver { + private MappingContext mappingContext; + + public AssociativeStoreResourceResolver(MappingContext mappingContext) { + this.mappingContext = mappingContext; + } + @Override public String getMapping() { return "/{repository}/{id}/**"; } @Override - public Resource resolve(NativeWebRequest nativeWebRequest, StoreInfo info, Object domainObj, ContentProperty property) { - Resource r = info.getImplementation(AssociativeStore.class).getResource(domainObj, PropertyPath.from(property.getContentPropertyPath())); - return new AssociatedStoreResourceImpl(info, property, domainObj, r); + public Resource resolve(NativeWebRequest nativeWebRequest, StoreInfo info, Object domainObj, PropertyPath propertyPath) { + Resource r = info.getImplementation(AssociativeStore.class).getResource(domainObj, propertyPath); + return new AssociatedStoreResourceImpl(info, domainObj, propertyPath, mappingContext.getContentProperty(domainObj.getClass(), propertyPath.getName()), r); } } \ No newline at end of file diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/DefaultEntityResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/DefaultEntityResolver.java index 8467abd98..1afed4dbf 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/DefaultEntityResolver.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/DefaultEntityResolver.java @@ -12,6 +12,7 @@ import java.util.Iterator; import java.util.Locale; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Optional; import javax.servlet.AsyncContext; @@ -31,6 +32,7 @@ import org.springframework.content.commons.mappingcontext.ContentProperty; import org.springframework.content.commons.mappingcontext.MappingContext; +import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.Store; import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.content.commons.storeservice.Stores; @@ -46,6 +48,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.AntPathMatcher; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestMapping; @@ -71,19 +74,10 @@ public class DefaultEntityResolver implements EntityResolver { private ApplicationContext context; private Repositories repositories; private Stores stores; -// private StoreInfo storeInfo; private ConversionService converters; private String mapping; private MappingContext mappingContext; -// public DefaultEntityResolver(ApplicationContext context, Repositories repositories, StoreInfo storeInfo, String[] pathSegments, ConversionService converters) { -// this.context = context; -// this.repositories = repositories; -// this.storeInfo = storeInfo; -// this.pathSegments = pathSegments; -// this.converters = converters; -// } - public DefaultEntityResolver(ApplicationContext context, Repositories repositories, Stores stores, ConversionService converters, String mapping, MappingContext mappingContext) { this.context = context; this.repositories = repositories; @@ -127,17 +121,12 @@ public EntityResolution resolve(String pathInfo) { } String propertyPath = matcher.extractPathWithinPattern(this.mapping, pathInfo); - ContentProperty contentProperty = null; - if (propertyPath != null) { - contentProperty = mappingContext.getContentProperty(info.getDomainObjectClass(), propertyPath); - } else { - contentProperty = selectPrimaryContentProperty(info); - } - if (contentProperty == null) { - throw new ResourceNotFoundException(); + if (!StringUtils.hasText(propertyPath)) { + ContentProperty property = selectPrimaryContentProperty(info); + propertyPath = property.getContentPropertyPath(); } - return new EntityResolution(domainObj, contentProperty); + return new EntityResolution(domainObj, PropertyPath.from(propertyPath)); } @Override @@ -268,8 +257,10 @@ private RepositoryInvoker resolveRootResourceInformation(StoreInfo info, String } private ContentProperty selectPrimaryContentProperty(StoreInfo info) { - ContentProperty contentProperty; - contentProperty = mappingContext.getContentProperties(info.getDomainObjectClass()).iterator().next(); + ContentProperty contentProperty = null; + try { + contentProperty = mappingContext.getContentProperties(info.getDomainObjectClass()).iterator().next(); + } catch (NoSuchElementException nsee) {} return contentProperty; } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/EntityResolution.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/EntityResolution.java index f4fc476ed..7080d5b4f 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/EntityResolution.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/EntityResolution.java @@ -1,6 +1,6 @@ package internal.org.springframework.content.rest.controllers.resolvers; -import org.springframework.content.commons.mappingcontext.ContentProperty; +import org.springframework.content.commons.property.PropertyPath; import lombok.AllArgsConstructor; import lombok.Getter; @@ -9,5 +9,5 @@ @AllArgsConstructor public class EntityResolution { private Object entity; - private ContentProperty contentProperty; + private PropertyPath property; } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/ResourceResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/ResourceResolver.java index ab34bde71..97105c31c 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/ResourceResolver.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/ResourceResolver.java @@ -1,11 +1,11 @@ package internal.org.springframework.content.rest.controllers.resolvers; -import org.springframework.content.commons.mappingcontext.ContentProperty; +import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.core.io.Resource; import org.springframework.web.context.request.NativeWebRequest; public interface ResourceResolver { public String getMapping(); - public Resource resolve(NativeWebRequest nativeWebRequest, StoreInfo info, Object domainObj, ContentProperty property); + public Resource resolve(NativeWebRequest nativeWebRequest, StoreInfo info, Object domainObj, PropertyPath propertyPath); } \ No newline at end of file diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/RevisionEntityResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/RevisionEntityResolver.java index 9ab153e71..895871072 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/RevisionEntityResolver.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/RevisionEntityResolver.java @@ -6,8 +6,8 @@ import java.util.Map; import java.util.Optional; -import org.springframework.content.commons.mappingcontext.ContentProperty; import org.springframework.content.commons.mappingcontext.MappingContext; +import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.Store; import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.content.commons.storeservice.Stores; @@ -76,9 +76,8 @@ public EntityResolution resolve(String pathInfo) { if (propertyPath == null) { propertyPath = ""; } - ContentProperty property = mappingContext.getContentProperty(info.getDomainObjectClass(), propertyPath); - return new EntityResolution(domainObj, property); + return new EntityResolution(domainObj, PropertyPath.from(propertyPath)); } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/StoreResourceResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/StoreResourceResolver.java index 18c69f2e8..158647d88 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/StoreResourceResolver.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/resolvers/StoreResourceResolver.java @@ -1,6 +1,6 @@ package internal.org.springframework.content.rest.controllers.resolvers; -import org.springframework.content.commons.mappingcontext.ContentProperty; +import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.commons.repository.AssociativeStore; import org.springframework.content.commons.storeservice.StoreInfo; @@ -11,14 +11,20 @@ public class StoreResourceResolver implements ResourceResolver { + private MappingContext mappingContext; + + public StoreResourceResolver(MappingContext mappingContext) { + this.mappingContext = mappingContext; + } + @Override public String getMapping() { return "/{repository}/{id}"; } @Override - public Resource resolve(NativeWebRequest nativeWebRequest, StoreInfo info, Object domainObj, ContentProperty property) { - Resource r = info.getImplementation(AssociativeStore.class).getResource(domainObj, PropertyPath.from(property.getContentPropertyPath())); - return new AssociatedStoreResourceImpl(info, property, domainObj, r); + public Resource resolve(NativeWebRequest nativeWebRequest, StoreInfo info, Object domainObj, PropertyPath property) { + Resource r = info.getImplementation(AssociativeStore.class).getResource(domainObj, property); + return new AssociatedStoreResourceImpl(info, domainObj, property, mappingContext.getContentProperty(domainObj.getClass(), property.getName()), r); } } \ No newline at end of file diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java index 40dcf2b0e..b32cf0650 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java @@ -2,11 +2,14 @@ import org.springframework.content.commons.io.RangeableResource; import org.springframework.content.commons.mappingcontext.ContentProperty; +import org.springframework.content.commons.property.PropertyPath; import org.springframework.core.io.WritableResource; public interface AssociatedStoreResource extends WritableResource, StoreResource, RangeableResource { S getAssociation(); + PropertyPath getPropertyPath(); + ContentProperty getContentProperty(); } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java index de541f36b..0a42625f5 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java @@ -61,9 +61,13 @@ public class AssociatedStoreResourceImpl implements HttpResource, AssociatedS private ContentProperty contentProperty; - public AssociatedStoreResourceImpl(StoreInfo info, ContentProperty property, S entity, Resource original) { + private PropertyPath propertyPath; + + + public AssociatedStoreResourceImpl(StoreInfo info, S entity, PropertyPath propertyPath, ContentProperty property, Resource original) { this.info = info; this.entity = entity; + this.propertyPath = propertyPath; this.contentProperty = property; this.original = original; } @@ -80,6 +84,11 @@ public S getAssociation() { return entity; } + @Override + public PropertyPath getPropertyPath() { + return this.propertyPath; + } + @Override public ContentProperty getContentProperty() { return this.contentProperty; @@ -100,7 +109,7 @@ public boolean isRenderableAs(org.springframework.util.MimeType mimeType) { if (Renderable.class.isAssignableFrom(this.getStoreInfo().getInterface())) { Renderable renderer = (Renderable)this.getStoreInfo().getImplementation(AssociativeStore.class); - return renderer.hasRendition(getAssociation(), PropertyPath.from(contentProperty.getContentPropertyPath()), mimeType.toString()); + return renderer.hasRendition(getAssociation(), this.getPropertyPath(), mimeType.toString()); } return false; @@ -112,7 +121,7 @@ public InputStream renderAs(org.springframework.util.MimeType mimeType) { if (Renderable.class.isAssignableFrom(this.getStoreInfo().getInterface())) { Renderable renderer = (Renderable)this.getStoreInfo().getImplementation(AssociativeStore.class); - return renderer.getRendition(getAssociation(), PropertyPath.from(contentProperty.getContentPropertyPath()), mimeType.toString()); + return renderer.getRendition(getAssociation(), this.getPropertyPath(), mimeType.toString()); } return null; @@ -143,7 +152,7 @@ public MediaType getMimeType() { Object mimeType = null; - mimeType = this.contentProperty.getMimeType(this.getAssociation()); + mimeType = this.getContentProperty().getMimeType(this.getAssociation()); return MediaType.valueOf(mimeType != null ? mimeType.toString() : MediaType.ALL_VALUE); } @@ -153,7 +162,7 @@ public long contentLength() throws IOException { // TODO: can we remove this is all properties are effectively embedded? // Long contentLength = null; (Long) BeanUtils.getFieldWithAnnotation(property, ContentLength.class); - Long contentLength = (Long) this.contentProperty.getContentLength(this.getAssociation()); + Long contentLength = (Long) this.getContentProperty().getContentLength(this.getAssociation()); if (contentLength == null) { contentLength = getDelegate().contentLength(); } @@ -182,7 +191,7 @@ public HttpHeaders getResponseHeaders() { HttpHeaders headers = new HttpHeaders(); // Modified to show download - Object originalFileName = this.contentProperty.getOriginalFileName(this.getAssociation()); + Object originalFileName = this.getContentProperty().getOriginalFileName(this.getAssociation()); if (originalFileName != null && StringUtils.hasText(originalFileName.toString())) { ContentDisposition.Builder builder = ContentDisposition.builder("form-data").name( "attachment").filename((String)originalFileName, Charset.defaultCharset()); headers.setContentDisposition(builder.build()); 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 4ef1617f3..99111412f 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 @@ -14,7 +14,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aop.support.AopUtils; -import org.springframework.content.commons.mappingcontext.ContentProperty; import org.springframework.content.commons.mappingcontext.MappingContext; import org.springframework.content.commons.repository.AssociativeStore; import org.springframework.content.commons.storeservice.StoreInfo; @@ -95,8 +94,7 @@ public PersistentEntityResource process(final PersistentEntityResource resource) } Collection contentPropertyPaths = mappingContext.getContentPaths(persistentEntityType); - ContentProperty singleContentProperty = mappingContext.getContentProperty(persistentEntityType, ""); - if(singleContentProperty != null && config.shortcutLinks() && !config.fullyQualifiedLinks()) { + if(contentPropertyPaths.size() == 1 && config.shortcutLinks() && !config.fullyQualifiedLinks()) { // for compatibility with v0.x.0 versions originalLink(config.getBaseUri(), store, entityId).ifPresent((l) -> addLink(resource, l)); 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 a83cbf712..4a53ed4c5 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 @@ -13,8 +13,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; -import internal.org.springframework.content.rest.support.TestEntity2; -import internal.org.springframework.content.rest.support.TestEntityChild; import java.util.UUID; import org.junit.Test; @@ -43,8 +41,10 @@ import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner; import internal.org.springframework.content.rest.support.BaseUriConfig; +import internal.org.springframework.content.rest.support.TestEntity2; import internal.org.springframework.content.rest.support.TestEntity4; import internal.org.springframework.content.rest.support.TestEntity5; +import internal.org.springframework.content.rest.support.TestEntityChild; @RunWith(Ginkgo4jSpringRunner.class) @Ginkgo4jConfiguration(threads = 1) @@ -177,7 +177,6 @@ public class ContentLinksResourceProcessorIT { It("should add content property links", () -> { assertThat(resource.getLinks("child"), hasItems(hasProperty("href", is("http://localhost/contentApi/files/999/child")))); - assertThat(resource.getLinks("child/content"), hasItems(hasProperty("href", is("http://localhost/contentApi/files/999/child/content")))); }); }); diff --git a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/storeresolver/StoreResolverRestConfigurationIT.java b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/storeresolver/StoreResolverRestConfigurationIT.java index 71360718d..76ce2b276 100644 --- a/spring-content-rest/src/test/java/internal/org/springframework/content/rest/storeresolver/StoreResolverRestConfigurationIT.java +++ b/spring-content-rest/src/test/java/internal/org/springframework/content/rest/storeresolver/StoreResolverRestConfigurationIT.java @@ -1,9 +1,17 @@ package internal.org.springframework.content.rest.storeresolver; -import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; -import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner; -import com.jayway.restassured.RestAssured; -import internal.org.springframework.content.rest.support.TestEntity2; +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 com.jayway.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import java.io.InputStream; + import org.apache.commons.io.IOUtils; import org.apache.http.HttpStatus; import org.junit.Test; @@ -13,12 +21,11 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.web.server.LocalServerPort; -import java.io.InputStream; +import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration; +import com.github.paulcwarren.ginkgo4j.Ginkgo4jSpringRunner; +import com.jayway.restassured.RestAssured; -import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.*; -import static com.jayway.restassured.RestAssured.given; -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.assertThat; +import internal.org.springframework.content.rest.support.TestEntity2; @RunWith(Ginkgo4jSpringRunner.class) @Ginkgo4jConfiguration(threads=1) diff --git a/spring-content-rest/src/test/java/it/internal/org/springframework/content/rest/controllers/ContentPropertyRestEndpointsIT.java b/spring-content-rest/src/test/java/it/internal/org/springframework/content/rest/controllers/ContentPropertyRestEndpointsIT.java index 9c64597dd..15ada8569 100644 --- a/spring-content-rest/src/test/java/it/internal/org/springframework/content/rest/controllers/ContentPropertyRestEndpointsIT.java +++ b/spring-content-rest/src/test/java/it/internal/org/springframework/content/rest/controllers/ContentPropertyRestEndpointsIT.java @@ -12,7 +12,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 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; @@ -32,7 +31,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.content.commons.property.PropertyPath; import org.springframework.content.rest.config.RestConfiguration; -import org.springframework.core.io.Resource; import org.springframework.core.io.WritableResource; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; import org.springframework.mock.web.MockHttpServletResponse; @@ -175,166 +173,84 @@ public class ContentPropertyRestEndpointsIT { 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("/testEntity8s/" + 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( - "/testEntity8s/" + 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("/testEntity8s/" - + testEntity2.getId() - + "/child").accept( - new String[] {"text/xml", - "text/plain"})) - .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("/testEntity8s/" + 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().contentMimeType, is("text/plain")); - }); - }); - Context("a DELETE to /{repository}/{id}/{contentProperty}", () -> { - It("should delete the content", () -> { - mvc.perform(delete( - "/testEntity8s/" + 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)); - assertThat(fetched.get().getChild().contentMimeType, is(nullValue())); - }); + Context("a GET to /{repository}/{id}/{contentProperty}", () -> { + It("should return the content", () -> { + MockHttpServletResponse response = mvc + .perform(get("/testEntity8s/" + 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!")); }); - - versionTests = Version.tests(); - lastModifiedDateTests = LastModifiedDate.tests(); }); - - Context("given the content property is accessed via the fully-qualified URL", () -> { - BeforeEach(() -> { - versionTests.setUrl("/testEntity8s/" + testEntity2.getId() + "/child/content"); - lastModifiedDateTests.setUrl("/testEntity8s/" + testEntity2.getId() + "/child/content"); + 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( + "/testEntity8s/" + 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}/{contentId}", () -> { - It("should return the content", () -> { - mvc.perform(get("/testEntity8s/" + testEntity2.getId() + "/child/content") - .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( - "/testEntity8s/" + testEntity2.getId() + "/child/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}/{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("/testEntity8s/" - + testEntity2.getId() + "/child/content") - .accept(new String[] - {"text/xml", - "text/plain"})) - .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("/testEntity8s/" + + testEntity2.getId() + + "/child").accept( + new String[] {"text/xml", + "text/plain"})) + .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("/testEntity8s/" - + testEntity2.getId() + "/child/content") - .content("Hello Modified Spring Content World!") - .contentType("text/plain")) - .andExpect(status().isOk()); - + }); + Context("a PUT to /{repository}/{id}/{contentProperty}", () -> { + It("should create the content", () -> { + mvc.perform( + put("/testEntity8s/" + testEntity2.getId() + "/child") + .content("Hello New Spring Content World!") + .contentType("text/plain")) + .andExpect(status().is2xxSuccessful()); - Resource r = store.getResource(testEntity2, PropertyPath.from("child.content")); - try (InputStream actual = r.getInputStream()) { - assertThat(IOUtils.toString(actual), - is("Hello Modified Spring Content World!")); - } - }); + 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().contentMimeType, is("text/plain")); }); - Context("a DELETE to /{repository}/{id}/{contentProperty}/{contentId}", () -> { - It("should delete the content", () -> { - mvc.perform(delete("/testEntity8s/" - + testEntity2.getId() + "/child/content")) - .andExpect(status().isNoContent()); + }); + Context("a DELETE to /{repository}/{id}/{contentProperty}", () -> { + It("should delete the content", () -> { + mvc.perform(delete( + "/testEntity8s/" + 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)); - assertThat(fetched.get().getChild().contentMimeType, is(nullValue())); - }); + 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)); + assertThat(fetched.get().getChild().contentMimeType, is(nullValue())); }); - - versionTests = Version.tests(); - lastModifiedDateTests = LastModifiedDate.tests(); }); + + versionTests = Version.tests(); + lastModifiedDateTests = LastModifiedDate.tests(); }); }); });