From b1f6afb650d6dfb21cadba1868ff5b8c84c2d525 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 2 Feb 2021 23:01:54 -0800 Subject: [PATCH 1/2] Refactor out etag and type handler method argument resolvers --- .../controllers/ContentServiceFactory.java | 49 ++++++ ...tServiceHandlerMethodArgumentResolver.java | 9 ++ ...ResourceHandlerMethodArgumentResolver.java | 6 +- ...toreInfoHandlerMethodArgumentResolver.java | 76 +++++++++ .../rest/controllers/StoreRestController.java | 25 ++- .../content/rest/io/AssociatedResource.java | 5 +- .../rest/io/AssociatedResourceImpl.java | 116 ++++++++++++-- .../content/rest/io/RenderableResource.java | 4 +- .../rest/io/RenderableResourceImpl.java | 51 ++++-- .../content/rest/io/StoreResource.java | 12 ++ .../content/rest/io/StoreResourceImpl.java | 146 ++++++++++++++++++ .../rest/config/RestConfiguration.java | 2 + .../ContentPropertyRestEndpointsIT.java | 3 +- 13 files changed, 466 insertions(+), 38 deletions(-) create mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceFactory.java create mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreInfoHandlerMethodArgumentResolver.java create mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResource.java create mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResourceImpl.java diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceFactory.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceFactory.java new file mode 100644 index 000000000..9672ee49b --- /dev/null +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceFactory.java @@ -0,0 +1,49 @@ +package internal.org.springframework.content.rest.controllers; + +import org.springframework.content.commons.repository.AssociativeStore; +import org.springframework.content.commons.repository.ContentStore; +import org.springframework.content.commons.repository.Store; +import org.springframework.content.commons.storeservice.StoreInfo; +import org.springframework.content.commons.storeservice.Stores; +import org.springframework.content.rest.config.RestConfiguration; +import org.springframework.content.rest.controllers.ContentService; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.repository.support.RepositoryInvokerFactory; + +import internal.org.springframework.content.rest.controllers.ContentServiceHandlerMethodArgumentResolver.ContentStoreContentService; +import internal.org.springframework.content.rest.controllers.ContentServiceHandlerMethodArgumentResolver.StoreContentService; +import internal.org.springframework.content.rest.io.AssociatedResource; +import internal.org.springframework.content.rest.io.StoreResource; +import internal.org.springframework.content.rest.mappings.StoreByteRangeHttpRequestHandler; + +public class ContentServiceFactory { + + private final RestConfiguration config; + private final Repositories repositories; + private final RepositoryInvokerFactory repoInvokerFactory; + private final Stores stores; + private final StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler; + + public ContentServiceFactory(RestConfiguration config, Repositories repositories, RepositoryInvokerFactory repoInvokerFactory, Stores stores, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler) { + this.config = config; + this.repositories = repositories; + this.repoInvokerFactory = repoInvokerFactory; + this.stores = stores; + this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; + } + + public ContentService getContentService(StoreInfo storeInfo, StoreResource resource) { + + if (ContentStore.class.isAssignableFrom(storeInfo.getInterface())) { + + Object entity = ((AssociatedResource)resource).getAssociation(); + return new ContentStoreContentService(config, storeInfo, repoInvokerFactory.getInvokerFor(entity.getClass()), entity, byteRangeRestRequestHandler); + } else if (AssociativeStore.class.isAssignableFrom(storeInfo.getInterface())) { + + throw new UnsupportedOperationException("AssociativeStore not supported"); + } else { + + return new StoreContentService(storeInfo.getImplementation(Store.class), byteRangeRestRequestHandler); + } + } +} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceHandlerMethodArgumentResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceHandlerMethodArgumentResolver.java index f3a852ab0..de3ad5920 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceHandlerMethodArgumentResolver.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceHandlerMethodArgumentResolver.java @@ -211,6 +211,15 @@ public static class ContentStoreContentService implements ContentService { private final Object embeddedProperty; private final StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler; + public ContentStoreContentService(RestConfiguration config, StoreInfo store, RepositoryInvoker repoInvoker, Object domainObj, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler) { + this.config = config; + this.store = store; + this.repoInvoker = repoInvoker; + this.domainObj = domainObj; + this.embeddedProperty = null; + this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; + } + public ContentStoreContentService(RestConfiguration config, StoreInfo store, RepositoryInvoker repoInvoker, Object domainObj, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler, ApplicationContext context) { this.config = config; this.store = store; 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 eaa2dd02e..7e3616f67 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 @@ -20,6 +20,7 @@ import internal.org.springframework.content.rest.io.AssociatedResource; import internal.org.springframework.content.rest.io.AssociatedResourceImpl; import internal.org.springframework.content.rest.io.RenderableResourceImpl; +import internal.org.springframework.content.rest.io.StoreResourceImpl; import internal.org.springframework.content.rest.utils.StoreUtils; public class ResourceHandlerMethodArgumentResolver extends StoreHandlerMethodArgumentResolver { @@ -44,7 +45,7 @@ protected Object resolveStoreArgument(NativeWebRequest nativeWebRequest, StoreIn String path = new UrlPathHelper().getPathWithinApplication(nativeWebRequest.getNativeRequest(HttpServletRequest.class)); String pathToUse = path.substring(StoreUtils.storePath(info).length() + 1); - return info.getImplementation(Store.class).getResource(pathToUse); + return new StoreResourceImpl(info.getImplementation(Store.class).getResource(pathToUse)); } @Override @@ -64,7 +65,8 @@ protected Object resolveAssociativeStorePropertyArgument(StoreInfo storeInfo, Ob AssociativeStore s = storeInfo.getImplementation(AssociativeStore.class); Resource resource = s.getResource(propertyVal); - resource = new AssociatedResourceImpl(propertyVal, resource); +// resource = new AssociatedResourceImpl(propertyVal, resource); + resource = new AssociatedResourceImpl(propertyVal, domainObj, resource); if (Renderable.class.isAssignableFrom(storeInfo.getInterface())) { resource = new RenderableResourceImpl((Renderable)storeInfo.getImplementation(AssociativeStore.class), (AssociatedResource)resource); } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreInfoHandlerMethodArgumentResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreInfoHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..bd1c92a90 --- /dev/null +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreInfoHandlerMethodArgumentResolver.java @@ -0,0 +1,76 @@ +package internal.org.springframework.content.rest.controllers; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.content.commons.repository.Store; +import org.springframework.content.commons.storeservice.StoreInfo; +import org.springframework.content.commons.storeservice.Stores; +import org.springframework.content.rest.config.RestConfiguration; +import org.springframework.core.MethodParameter; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.repository.support.RepositoryInvokerFactory; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.util.UrlPathHelper; + +import internal.org.springframework.content.rest.utils.StoreUtils; + +public class StoreInfoHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private final RestConfiguration config; + private final Repositories repositories; + private final RepositoryInvokerFactory repoInvokerFactory; + private final Stores stores; + + public StoreInfoHandlerMethodArgumentResolver(RestConfiguration config, Repositories repositories, RepositoryInvokerFactory repoInvokerFactory, Stores stores) { + this.config = config; + this.repositories = repositories; + this.repoInvokerFactory = repoInvokerFactory; + this.stores = stores; + } + + RestConfiguration getConfig() { + return config; + } + + protected Repositories getRepositories() { + return repositories; + } + + RepositoryInvokerFactory getRepoInvokerFactory() { + return repoInvokerFactory; + } + + protected Stores getStores() { + return stores; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return StoreInfo.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + String pathInfo = webRequest.getNativeRequest(HttpServletRequest.class).getRequestURI(); + pathInfo = new UrlPathHelper().getPathWithinApplication(webRequest.getNativeRequest(HttpServletRequest.class)); + pathInfo = StoreUtils.storeLookupPath(pathInfo, this.getConfig().getBaseUri()); + + String[] pathSegments = pathInfo.split("/"); + if (pathSegments.length < 2) { + return null; + } + + String store = pathSegments[1]; + + StoreInfo info = this.getStores().getStore(Store.class, StoreUtils.withStorePath(store)); + if (info == null) { + throw new IllegalArgumentException(String.format("Store for path %s not found", store)); + } + + return info; + } +} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java index 1ce98e6c0..66d646730 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java @@ -12,6 +12,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.content.commons.storeservice.Stores; +import org.springframework.content.rest.config.RestConfiguration; import org.springframework.content.rest.controllers.ContentService; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; @@ -31,6 +32,8 @@ import internal.org.springframework.content.rest.annotations.ContentRestController; import internal.org.springframework.content.rest.io.InputStreamResource; +import internal.org.springframework.content.rest.io.StoreResource; +import internal.org.springframework.content.rest.mappings.StoreByteRangeHttpRequestHandler; import internal.org.springframework.content.rest.utils.HeaderUtils; @ContentRestController @@ -48,7 +51,17 @@ public class StoreRestController implements InitializingBean { private Stores stores; @Autowired(required=false) private RepositoryInvokerFactory repoInvokerFactory; + + @Autowired + private RestConfiguration config; + + @Autowired + private StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler; + + private ContentServiceFactory contentServiceFactory; + public StoreRestController() { + contentServiceFactory = new ContentServiceFactory(config, repositories, repoInvokerFactory, stores, byteRangeRestRequestHandler); } @RequestMapping(value = STORE_REQUEST_MAPPING, method = RequestMethod.GET) @@ -59,21 +72,29 @@ public void getContent(HttpServletRequest request, MediaType resourceType, Object resourceETag, ContentService contentService) +// StoreInfo storeInfo +// ) throws MethodNotAllowedException { if (resource == null || resource.exists() == false) { throw new ResourceNotFoundException(); } + StoreResource storeResource = (StoreResource)resource; + long lastModified = -1; try { lastModified = resource.lastModified(); } catch (IOException e) {} - if(new ServletWebRequest(request, response).checkNotModified(resourceETag != null ? resourceETag.toString() : null, lastModified)) { + if(new ServletWebRequest(request, response).checkNotModified(storeResource.getETag() != null ? storeResource.getETag().toString() : null, lastModified)) { +// if(new ServletWebRequest(request, response).checkNotModified(resourceETag != null ? resourceETag.toString() : null, lastModified)) { return; } - contentService.getContent(request, response, headers, resource, resourceType); +// ContentService contentService = contentServiceFactory.getContentService(storeInfo, storeResource); + + contentService.getContent(request, response, headers, storeResource, storeResource.getMimeType()); +// contentService.getContent(request, response, headers, storeResource, resourceType); } @RequestMapping(value = STORE_REQUEST_MAPPING, method = RequestMethod.PUT, headers = { diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResource.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResource.java index b7e35fd52..212f59185 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResource.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResource.java @@ -1,9 +1,8 @@ package internal.org.springframework.content.rest.io; -import org.springframework.core.io.Resource; +import org.springframework.core.io.WritableResource; -public interface AssociatedResource extends Resource { +public interface AssociatedResource extends WritableResource, StoreResource { S getAssociation(); - } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResourceImpl.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResourceImpl.java index ceeb15907..66c843493 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResourceImpl.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResourceImpl.java @@ -1,32 +1,40 @@ package internal.org.springframework.content.rest.io; +import static java.lang.String.format; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URL; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Stream; + +import javax.persistence.Version; + import org.springframework.content.commons.annotations.ContentLength; +import org.springframework.content.commons.annotations.MimeType; import org.springframework.content.commons.annotations.OriginalFileName; +import org.springframework.content.commons.io.DeletableResource; import org.springframework.content.commons.utils.BeanUtils; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.Resource; +import org.springframework.core.io.WritableResource; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.convert.Jsr310Converters; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import org.springframework.web.servlet.resource.HttpResource; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URL; -import java.nio.channels.ReadableByteChannel; -import java.nio.charset.Charset; -import java.time.Instant; -import java.util.Date; -import java.util.stream.Stream; - -import static java.lang.String.format; - /** * Represents a Spring Content Resource that is associated with a Spring Data Entity. * @@ -46,19 +54,63 @@ public class AssociatedResourceImpl implements HttpResource, AssociatedResour private S entity; private Resource original; + private Object property; public AssociatedResourceImpl(S entity, Resource original) { this.entity = entity; this.original = original; } + public AssociatedResourceImpl(Object property, S entity, Resource original) { + this.entity = entity; + this.property = property; + this.original = original; + } + + @Override public S getAssociation() { - return entity; + + Object obj = property != null ? property : entity; + return (S)obj; + } + + @Override + public Object getETag() { + + Object etag = null; + + if (property != null) { + etag = BeanUtils.getFieldWithAnnotation(property, Version.class); + } + + if (etag == null) { + etag = BeanUtils.getFieldWithAnnotation(entity, Version.class); + } + + if (etag == null) { + etag = ""; + } + + return etag.toString(); + } + + @Override + public MediaType getMimeType() { + + Object mimeType = null; + + Object obj = property != null ? property : entity; + mimeType = BeanUtils.getFieldWithAnnotation(obj, MimeType.class); + + return MediaType.valueOf(mimeType != null ? mimeType.toString() : MediaType.ALL_VALUE); } @Override public long contentLength() throws IOException { - Long contentLength = (Long) BeanUtils.getFieldWithAnnotation(entity, ContentLength.class); + + Object obj = property != null ? property : entity; + + Long contentLength = (Long) BeanUtils.getFieldWithAnnotation(obj, ContentLength.class); if (contentLength == null) { contentLength = original.contentLength(); } @@ -67,7 +119,10 @@ public long contentLength() throws IOException { @Override public long lastModified() throws IOException { - Object lastModified = BeanUtils.getFieldWithAnnotation(entity, LastModifiedDate.class); + + Object obj = property != null ? property : entity; + + Object lastModified = BeanUtils.getFieldWithAnnotation(obj, LastModifiedDate.class); if (lastModified == null) { return original.lastModified(); } @@ -143,12 +198,39 @@ public InputStream getInputStream() throws IOException { @Override public HttpHeaders getResponseHeaders() { HttpHeaders headers = new HttpHeaders(); + + Object obj = property != null ? property : entity; + // Modified to show download - Object originalFileName = BeanUtils.getFieldWithAnnotation(entity, OriginalFileName.class); + Object originalFileName = BeanUtils.getFieldWithAnnotation(obj, OriginalFileName.class); 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()); } return headers; } + + @Override + public boolean isWritable() { + return ((WritableResource)original).isWritable(); + } + + @Override + public OutputStream getOutputStream() + throws IOException { + return ((WritableResource)original).getOutputStream(); + } + + @Override + public WritableByteChannel writableChannel() + throws IOException { + return ((WritableResource)original).writableChannel(); + } + + + @Override + public void delete() + throws IOException { + ((DeletableResource)original).delete(); + } } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResource.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResource.java index 1532706e1..5bac4ce55 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResource.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResource.java @@ -2,10 +2,10 @@ import java.io.InputStream; -import org.springframework.core.io.Resource; import org.springframework.util.MimeType; -public interface RenderableResource extends Resource { +public interface RenderableResource extends StoreResource { + boolean isRenderableAs(MimeType mimeType); diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResourceImpl.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResourceImpl.java index 944b36c8c..e2c438b40 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResourceImpl.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResourceImpl.java @@ -3,16 +3,18 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.URI; import java.net.URL; import java.nio.channels.ReadableByteChannel; import java.time.ZonedDateTime; -import org.apache.commons.io.IOUtils; - +import org.springframework.content.commons.io.DeletableResource; import org.springframework.content.commons.renditions.Renderable; import org.springframework.core.io.Resource; +import org.springframework.core.io.WritableResource; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.web.servlet.resource.HttpResource; @@ -25,10 +27,10 @@ public class RenderableResourceImpl implements Resource, HttpResource, RenderableResource { private final Renderable renderer; - private final AssociatedResource original; + private final StoreResource original; private final long lastModified; - public RenderableResourceImpl(Renderable renderer, AssociatedResource original) { + public RenderableResourceImpl(Renderable renderer, StoreResource original) { this.renderer = renderer; this.original = original; this.lastModified = ZonedDateTime.now().toInstant().toEpochMilli(); @@ -36,17 +38,32 @@ public RenderableResourceImpl(Renderable renderer, AssociatedResource original) @Override public boolean isRenderableAs(MimeType mimeType) { - InputStream is = renderer.getRendition(original.getAssociation(), mimeType.toString()); - try { - return is != null; - } finally { - IOUtils.closeQuietly(is); + + if (original instanceof AssociatedResource) { + return renderer.hasRendition(((AssociatedResource)original).getAssociation(), mimeType.toString()); } + + return false; } @Override public InputStream renderAs(MimeType mimeType) { - return renderer.getRendition(original.getAssociation(), mimeType.toString()); + + if (original instanceof AssociatedResource) { + return renderer.getRendition(((AssociatedResource)original).getAssociation(), mimeType.toString()); + } + + return null; + } + + @Override + public Object getETag() { + return original.getETag(); + } + + @Override + public MediaType getMimeType() { + return original.getMimeType(); } @Override @@ -125,7 +142,19 @@ public HttpHeaders getResponseHeaders() { if (original instanceof HttpResource) { return ((HttpResource)original).getResponseHeaders(); } else { - return new HttpHeaders(); + return new HttpHeaders(); } } + + @Override + public OutputStream getOutputStream() + throws IOException { + return ((WritableResource)original).getOutputStream(); + } + + @Override + public void delete() + throws IOException { + ((DeletableResource)original).delete(); + } } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResource.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResource.java new file mode 100644 index 000000000..a83e38951 --- /dev/null +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResource.java @@ -0,0 +1,12 @@ +package internal.org.springframework.content.rest.io; + +import org.springframework.content.commons.io.DeletableResource; +import org.springframework.core.io.WritableResource; +import org.springframework.http.MediaType; + +public interface StoreResource extends WritableResource, DeletableResource { + + Object getETag(); + + MediaType getMimeType(); +} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResourceImpl.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResourceImpl.java new file mode 100644 index 000000000..dc1598d7e --- /dev/null +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResourceImpl.java @@ -0,0 +1,146 @@ +package internal.org.springframework.content.rest.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URL; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; + +import javax.activation.MimetypesFileTypeMap; + +import org.springframework.content.commons.io.DeletableResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.WritableResource; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; + +public class StoreResourceImpl implements Resource, StoreResource { + + private Resource delegate; + + public StoreResourceImpl(Resource delegate) { + Assert.notNull(delegate, "Delegate cannot be null"); + this.delegate = delegate; + } + + @Override + public Object getETag() { + + try { + return this.lastModified(); + } catch (IOException e) { + return null; + } + } + + @Override + public MediaType getMimeType() { + + String mimeType = new MimetypesFileTypeMap().getContentType(this.getFilename()); + return MediaType.valueOf(mimeType != null ? mimeType : ""); + } + + @Override + public InputStream getInputStream() + throws IOException { + return delegate.getInputStream(); + } + + @Override + public boolean exists() { + return delegate.exists(); + } + + @Override + public boolean isReadable() { + return delegate.isReadable(); + } + + @Override + public boolean isOpen() { + return delegate.isOpen(); + } + + @Override + public boolean isFile() { + return delegate.isFile(); + } + + @Override + public URL getURL() + throws IOException { + return delegate.getURL(); + } + + @Override + public URI getURI() + throws IOException { + return delegate.getURI(); + } + + @Override + public File getFile() + throws IOException { + return delegate.getFile(); + } + + @Override + public ReadableByteChannel readableChannel() + throws IOException { + return delegate.readableChannel(); + } + + @Override + public long contentLength() + throws IOException { + return delegate.contentLength(); + } + + @Override + public long lastModified() + throws IOException { + return delegate.lastModified(); + } + + @Override + public Resource createRelative(String relativePath) + throws IOException { + return delegate.createRelative(relativePath); + } + + @Override + public String getFilename() { + return delegate.getFilename(); + } + + @Override + public String getDescription() { + return delegate.getDescription(); + } + + @Override + public boolean isWritable() { + return ((WritableResource)delegate).isWritable(); + } + + @Override + public OutputStream getOutputStream() + throws IOException { + return ((WritableResource)delegate).getOutputStream(); + } + + @Override + public WritableByteChannel writableChannel() + throws IOException { + return ((WritableResource)delegate).writableChannel(); + } + + @Override + public void delete() + throws IOException { + ((DeletableResource)delegate).delete(); + } +} 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 77fadd148..5a73ce126 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 @@ -30,6 +30,7 @@ import internal.org.springframework.content.rest.controllers.ResourceETagMethodArgumentResolver; import internal.org.springframework.content.rest.controllers.ResourceHandlerMethodArgumentResolver; import internal.org.springframework.content.rest.controllers.ResourceTypeMethodArgumentResolver; +import internal.org.springframework.content.rest.controllers.StoreInfoHandlerMethodArgumentResolver; import internal.org.springframework.content.rest.mappings.ContentHandlerMapping; import internal.org.springframework.content.rest.mappings.StoreByteRangeHttpRequestHandler; @@ -142,6 +143,7 @@ public void addArgumentResolvers(List argumentRes argumentResolvers.add(new ResourceTypeMethodArgumentResolver(config, repositories, repoInvokerFactory, stores)); argumentResolvers.add(new ResourceETagMethodArgumentResolver(config, repositories, repoInvokerFactory, stores)); argumentResolvers.add(new ContentServiceHandlerMethodArgumentResolver(config, repositories, repoInvokerFactory, stores, byteRangeRestRequestHandler, context)); + argumentResolvers.add(new StoreInfoHandlerMethodArgumentResolver(config, repositories, repoInvokerFactory, stores)); } @Override 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 2ec22835d..ba8807f5e 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 @@ -3,6 +3,7 @@ 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 java.lang.String.format; import static org.hamcrest.CoreMatchers.is; @@ -145,7 +146,7 @@ public class ContentPropertyRestEndpointsIT { }); 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", () -> { + FIt("should return the content", () -> { MockHttpServletResponse response = mvc .perform(get("/files/" + testEntity2.getId() + "/child") .accept("text/plain")) From 445dae4e15fecfb319798150e00b7092cd8d85b5 Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Sun, 7 Feb 2021 23:04:33 -0800 Subject: [PATCH 2/2] Refactor store rest controller to optimize database access fixes #490 --- .../rest/contentservice}/ContentService.java | 2 +- .../ContentServiceFactory.java | 22 +- .../ContentStoreContentService.java | 430 ++++++++++++++ .../contentservice/StoreContentService.java | 118 ++++ ...tServiceHandlerMethodArgumentResolver.java | 544 ------------------ .../ResourceETagMethodArgumentResolver.java | 62 -- ...ResourceHandlerMethodArgumentResolver.java | 20 +- .../ResourceTypeMethodArgumentResolver.java | 61 -- ...toreInfoHandlerMethodArgumentResolver.java | 76 --- .../rest/controllers/StoreRestController.java | 83 +-- .../resolvers/RevisionEntityResolver.java | 8 +- .../io/AssociatedStorePropertyResource.java | 8 + .../AssociatedStorePropertyResourceImpl.java | 159 +++++ ...urce.java => AssociatedStoreResource.java} | 3 +- ....java => AssociatedStoreResourceImpl.java} | 52 +- .../content/rest/io/RenderableResource.java | 3 +- .../rest/io/RenderableResourceImpl.java | 12 +- .../content/rest/io/StoreResource.java | 10 + .../content/rest/io/StoreResourceImpl.java | 29 +- .../links/ContentLinksResourceProcessor.java | 4 +- .../rest/config/RestConfiguration.java | 8 - .../ContentPropertyRestEndpointsIT.java | 3 +- 22 files changed, 875 insertions(+), 842 deletions(-) rename spring-content-rest/src/main/java/{org/springframework/content/rest/controllers => internal/org/springframework/content/rest/contentservice}/ContentService.java (93%) rename spring-content-rest/src/main/java/internal/org/springframework/content/rest/{controllers => contentservice}/ContentServiceFactory.java (54%) create mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java create mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/StoreContentService.java delete mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceHandlerMethodArgumentResolver.java delete mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceETagMethodArgumentResolver.java delete mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceTypeMethodArgumentResolver.java delete mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreInfoHandlerMethodArgumentResolver.java create mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStorePropertyResource.java create mode 100644 spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStorePropertyResourceImpl.java rename spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/{AssociatedResource.java => AssociatedStoreResource.java} (60%) rename spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/{AssociatedResourceImpl.java => AssociatedStoreResourceImpl.java} (80%) diff --git a/spring-content-rest/src/main/java/org/springframework/content/rest/controllers/ContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentService.java similarity index 93% rename from spring-content-rest/src/main/java/org/springframework/content/rest/controllers/ContentService.java rename to spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentService.java index 56e67ed71..e071f5f08 100644 --- a/spring-content-rest/src/main/java/org/springframework/content/rest/controllers/ContentService.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentService.java @@ -1,4 +1,4 @@ -package org.springframework.content.rest.controllers; +package internal.org.springframework.content.rest.contentservice; import java.io.IOException; diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceFactory.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentServiceFactory.java similarity index 54% rename from spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceFactory.java rename to spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentServiceFactory.java index 9672ee49b..5b2e675e3 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceFactory.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentServiceFactory.java @@ -1,18 +1,13 @@ -package internal.org.springframework.content.rest.controllers; +package internal.org.springframework.content.rest.contentservice; import org.springframework.content.commons.repository.AssociativeStore; import org.springframework.content.commons.repository.ContentStore; -import org.springframework.content.commons.repository.Store; -import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.content.commons.storeservice.Stores; import org.springframework.content.rest.config.RestConfiguration; -import org.springframework.content.rest.controllers.ContentService; import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvokerFactory; -import internal.org.springframework.content.rest.controllers.ContentServiceHandlerMethodArgumentResolver.ContentStoreContentService; -import internal.org.springframework.content.rest.controllers.ContentServiceHandlerMethodArgumentResolver.StoreContentService; -import internal.org.springframework.content.rest.io.AssociatedResource; +import internal.org.springframework.content.rest.io.AssociatedStoreResource; import internal.org.springframework.content.rest.io.StoreResource; import internal.org.springframework.content.rest.mappings.StoreByteRangeHttpRequestHandler; @@ -32,18 +27,19 @@ public ContentServiceFactory(RestConfiguration config, Repositories repositories this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; } - public ContentService getContentService(StoreInfo storeInfo, StoreResource resource) { + public ContentService getContentService(StoreResource resource) { - if (ContentStore.class.isAssignableFrom(storeInfo.getInterface())) { + if (ContentStore.class.isAssignableFrom(resource.getStoreInfo().getInterface())) { - Object entity = ((AssociatedResource)resource).getAssociation(); - return new ContentStoreContentService(config, storeInfo, repoInvokerFactory.getInvokerFor(entity.getClass()), entity, byteRangeRestRequestHandler); - } else if (AssociativeStore.class.isAssignableFrom(storeInfo.getInterface())) { + Object entity = ((AssociatedStoreResource)resource).getAssociation(); + + return new ContentStoreContentService(config, null, repoInvokerFactory.getInvokerFor(entity.getClass()), entity, byteRangeRestRequestHandler); + } else if (AssociativeStore.class.isAssignableFrom(resource.getStoreInfo().getInterface())) { throw new UnsupportedOperationException("AssociativeStore not supported"); } else { - return new StoreContentService(storeInfo.getImplementation(Store.class), byteRangeRestRequestHandler); + return new StoreContentService(byteRangeRestRequestHandler); } } } 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 new file mode 100644 index 000000000..71950769d --- /dev/null +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/ContentStoreContentService.java @@ -0,0 +1,430 @@ +package internal.org.springframework.content.rest.contentservice; + +import static java.lang.String.format; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.content.commons.annotations.MimeType; +import org.springframework.content.commons.annotations.OriginalFileName; +import org.springframework.content.commons.repository.ContentStore; +import org.springframework.content.commons.storeservice.StoreInfo; +import org.springframework.content.commons.utils.BeanUtils; +import org.springframework.content.rest.RestResource; +import org.springframework.content.rest.config.RestConfiguration; +import org.springframework.content.rest.config.RestConfiguration.Resolver; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.data.repository.support.RepositoryInvoker; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ResponseStatusException; + +import internal.org.springframework.content.rest.controllers.MethodNotAllowedException; +import internal.org.springframework.content.rest.io.AssociatedStorePropertyResource; +import internal.org.springframework.content.rest.io.AssociatedStoreResource; +import internal.org.springframework.content.rest.io.RenderedResource; +import internal.org.springframework.content.rest.io.StoreResource; +import internal.org.springframework.content.rest.mappings.StoreByteRangeHttpRequestHandler; + +public class ContentStoreContentService implements ContentService { + + private static final Logger logger = LoggerFactory.getLogger(ContentStoreContentService.class); + + private static final Map, StoreExportedMethodsMap> storeExportedMethods = new HashMap<>(); + + private final RestConfiguration config; + private final StoreInfo store; + private final RepositoryInvoker repoInvoker; + private final Object domainObj; + private final Object embeddedProperty; + private final StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler; + + public ContentStoreContentService(RestConfiguration config, StoreInfo store, RepositoryInvoker repoInvoker, Object domainObj, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler) { + this.config = config; + this.store = store; + this.repoInvoker = repoInvoker; + this.domainObj = domainObj; + this.embeddedProperty = null; + this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; + } + + public ContentStoreContentService(RestConfiguration config, StoreInfo store, RepositoryInvoker repoInvoker, Object domainObj, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler, ApplicationContext context) { + this.config = config; + this.store = store; + this.repoInvoker = repoInvoker; + this.domainObj = domainObj; + this.embeddedProperty = null; + this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; + } + + public ContentStoreContentService(RestConfiguration config, StoreInfo store, RepositoryInvoker repoInvoker, Object domainObj, Object embeddedProperty, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler) { + this.config = config; + this.store = store; + this.repoInvoker = repoInvoker; + this.domainObj = domainObj; + this.embeddedProperty = embeddedProperty; + this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; + } + + @Override + public void getContent(HttpServletRequest request, HttpServletResponse response, HttpHeaders headers, Resource resource, MediaType resourceType) + throws ResponseStatusException, MethodNotAllowedException { + + Method[] methodsToUse = getExportedMethodsFor(((StoreResource)resource).getStoreInfo().getInterface()).getContentMethods(); + + if (methodsToUse.length > 1) { + throw new IllegalStateException("Too many getContent methods"); + } + + if (methodsToUse.length == 0) { + throw new MethodNotAllowedException(); + } + + try { + MediaType producedResourceType = null; + List acceptedMimeTypes = headers.getAccept(); + if (acceptedMimeTypes.size() > 0) { + + MediaType.sortBySpecificityAndQuality(acceptedMimeTypes); + for (MediaType acceptedMimeType : acceptedMimeTypes) { + + if (acceptedMimeType.includes(resourceType) && matchParameters(acceptedMimeType, resourceType)) { + + producedResourceType = resourceType; + break; + + } else if (((StoreResource) resource).isRenderableAs(acceptedMimeType)) { + + resource = new RenderedResource(((StoreResource) resource).renderAs(acceptedMimeType), resource); + producedResourceType = acceptedMimeType; + break; + } + } + + if (producedResourceType == null) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + return; + } + } + + request.setAttribute("SPRING_CONTENT_RESOURCE", resource); + request.setAttribute("SPRING_CONTENT_CONTENTTYPE", producedResourceType); + } catch (Exception e) { + + logger.error("Unable to retrieve content", e); + + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, format("Failed to handle request for %s", resource.getDescription()), e); + } + + try { + byteRangeRestRequestHandler.handleRequest(request, response); + } + catch (Exception e) { + if (isClientAbortException(e)) { + // suppress + } else { + logger.error("Unable to handle request", e); + + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, format("Failed to handle request for %s", resource.getDescription()), e); + } + } + } + + @Override + public void setContent(HttpServletRequest request, HttpServletResponse response, HttpHeaders headers, Resource source, MediaType sourceMimeType, Resource target) throws IOException, MethodNotAllowedException { + + AssociatedStoreResource storeResource = (AssociatedStoreResource)target; + + Object updateObject = storeResource.getAssociation(); + if (storeResource instanceof AssociatedStorePropertyResource) { + + AssociatedStorePropertyResource apr = (AssociatedStorePropertyResource)storeResource; + if (apr.embedded()) { + updateObject = apr.getProperty(); + } + } + + if (BeanUtils.hasFieldWithAnnotation(updateObject, MimeType.class)) { + BeanUtils.setFieldWithAnnotation(updateObject, MimeType.class, sourceMimeType.toString()); + } + + String originalFilename = source.getFilename(); + if (source.getFilename() != null && StringUtils.hasText(originalFilename)) { + if (BeanUtils.hasFieldWithAnnotation(updateObject, OriginalFileName.class)) { + BeanUtils.setFieldWithAnnotation(updateObject, OriginalFileName.class, originalFilename); + } + } + + Method[] methodsToUse = getExportedMethodsFor(((StoreResource)target).getStoreInfo().getInterface()).setContentMethods(); + + if (methodsToUse.length > 1) { + RestConfiguration.DomainTypeConfig dtConfig = config.forDomainType(storeResource.getStoreInfo().getDomainObjectClass()); + methodsToUse = filterMethods(methodsToUse, dtConfig.getSetContentResolver(), headers); + } + + if (methodsToUse.length > 1) { + throw new UnsupportedOperationException(format("Too many setContent methods exported. Expected 1. Got %s", methodsToUse.length)); + } + + if (methodsToUse.length == 0) { + throw new MethodNotAllowedException(); + } + + Method methodToUse = methodsToUse[0]; + Object contentArg = convertContentArg(source, methodToUse.getParameterTypes()[1]); + + try { + Object targetObj = storeResource.getStoreInfo().getImplementation(ContentStore.class); + + ReflectionUtils.makeAccessible(methodToUse); + + Object updatedDomainObj = ReflectionUtils.invokeMethod(methodToUse, targetObj, updateObject, contentArg); + + Object saveObject = updatedDomainObj; + if (storeResource instanceof AssociatedStorePropertyResource) { + + AssociatedStorePropertyResource apr = (AssociatedStorePropertyResource)storeResource; + if (apr.embedded()) { + saveObject = apr.getAssociation(); + } + } + + repoInvoker.invokeSave(saveObject); + } finally { + cleanup(contentArg); + } + } + + @Override + public void unsetContent(Resource resource) throws MethodNotAllowedException { + + AssociatedStoreResource storeResource = (AssociatedStoreResource)resource; + + Method[] methodsToUse = getExportedMethodsFor(((StoreResource)resource).getStoreInfo().getInterface()).unsetContentMethods(); + + if (methodsToUse.length == 0) { + throw new MethodNotAllowedException(); + } + + if (methodsToUse.length > 1) { + throw new IllegalStateException("Too many unsetContent methods"); + } + + Object updateObject = storeResource.getAssociation(); + if (storeResource instanceof AssociatedStorePropertyResource) { + + AssociatedStorePropertyResource apr = (AssociatedStorePropertyResource)storeResource; + if (apr.embedded()) { + updateObject = apr.getProperty(); + } + } + + Object targetObj = storeResource.getStoreInfo().getImplementation(ContentStore.class); + + ReflectionUtils.makeAccessible(methodsToUse[0]); + + Object updatedDomainObj = ReflectionUtils.invokeMethod(methodsToUse[0], targetObj, updateObject); + + updateObject = updatedDomainObj; + if (storeResource instanceof AssociatedStorePropertyResource) { + + AssociatedStorePropertyResource apr = (AssociatedStorePropertyResource)storeResource; + if (apr.embedded()) { + updateObject = apr.getAssociation(); + } + } + + if (BeanUtils.hasFieldWithAnnotation(updateObject, MimeType.class)) { + BeanUtils.setFieldWithAnnotation(updateObject, MimeType.class, null); + } + + if (BeanUtils.hasFieldWithAnnotation(updateObject, OriginalFileName.class)) { + BeanUtils.setFieldWithAnnotation(updateObject, OriginalFileName.class, null); + } + + repoInvoker.invokeSave(updateObject); + } + + private void cleanup(Object contentArg) { + + if (contentArg == null) { + return; + } + + if (FileSystemResource.class.isAssignableFrom(contentArg.getClass())) { + ((FileSystemResource)contentArg).getFile().delete(); + } + } + + private Object convertContentArg(Resource resource, Class parameterType) { + + if (InputStream.class.equals(parameterType)) { + try { + return resource.getInputStream(); + } catch (IOException e) { + throw new IllegalArgumentException(format("Unable to get inputstream from resource %s", resource.getFilename())); + } + } else if (Resource.class.equals(parameterType)) { + try { + File f = Files.createTempFile("", "").toFile(); + FileUtils.copyInputStreamToFile(resource.getInputStream(), f); + return new FileSystemResource(f); + } catch (IOException e) { + throw new IllegalArgumentException(format("Unable to re-purpose resource %s", resource.getFilename())); + } + } else { + throw new IllegalArgumentException(format("Unsupported content type %s", parameterType.getCanonicalName())); + } + } + + private Method[] filterMethods(Method[] methods, Resolver resolver, HttpHeaders headers) { + + List resolved = new ArrayList<>(); + for (Method method : methods) { + if (resolver.resolve(method, headers)) { + resolved.add(method); + } + } + + return resolved.toArray(new Method[]{}); + } + + private boolean matchParameters(MediaType acceptedMediaType, MediaType producableMediaType) { + for (String name : producableMediaType.getParameters().keySet()) { + String s1 = producableMediaType.getParameter(name); + String s2 = acceptedMediaType.getParameter(name); + if (StringUtils.hasText(s1) && StringUtils.hasText(s2) && !s1.equalsIgnoreCase(s2)) { + return false; + } + } + return true; + } + + public static StoreExportedMethodsMap getExportedMethodsFor(Class storeInterfaceClass) { + + StoreExportedMethodsMap exportMap = storeExportedMethods.get(storeInterfaceClass); + if (exportMap == null) { + storeExportedMethods.put(storeInterfaceClass, new StoreExportedMethodsMap(storeInterfaceClass)); + exportMap = storeExportedMethods.get(storeInterfaceClass); + } + + return exportMap; + } + + public static class StoreExportedMethodsMap { + + private static Method[] SETCONTENT_METHODS = null; + private static Method[] UNSETCONTENT_METHODS = null; + private static Method[] GETCONTENT_METHODS = null; + + static { + SETCONTENT_METHODS = new Method[] { + ReflectionUtils.findMethod(ContentStore.class, "setContent", Object.class, InputStream.class), + ReflectionUtils.findMethod(ContentStore.class, "setContent", Object.class, Resource.class), + }; + + UNSETCONTENT_METHODS = new Method[] { + ReflectionUtils.findMethod(ContentStore.class, "unsetContent", Object.class), + }; + + GETCONTENT_METHODS = new Method[] { + ReflectionUtils.findMethod(ContentStore.class, "getContent", Object.class), + }; + } + + private Class storeInterface; + private Method[] getContentMethods; + private Method[] setContentMethods; + private Method[] unsetContentMethods; + + public StoreExportedMethodsMap(Class storeInterface) { + this.storeInterface = storeInterface; + this.getContentMethods = calculateExports(GETCONTENT_METHODS); + this.setContentMethods = calculateExports(SETCONTENT_METHODS); + this.unsetContentMethods = calculateExports(UNSETCONTENT_METHODS); + } + + public Method[] getContentMethods() { + return this.getContentMethods; + } + + public Method[] setContentMethods() { + return this.setContentMethods; + } + + public Method[] unsetContentMethods() { + return this.unsetContentMethods; + } + + private Method[] calculateExports(Method[] storeMethods) { + + List exportedMethods = new ArrayList<>(); + exportedMethods.addAll(Arrays.asList(storeMethods)); + + List unexportedMethods = new ArrayList<>(); + + for (Method m : exportedMethods) { + for (Method dm : storeInterface.getDeclaredMethods()) { + if (!dm.isBridge()) { + if (dm.getName().equals(m.getName())) { + if (argsMatch(dm, m)) { + + RestResource r = dm.getAnnotation(RestResource.class); + if (r != null && r.exported() == false) { + + unexportedMethods.add(m); + } + } + } + } + } + } + + for (Method unexportedMethod : unexportedMethods) { + + exportedMethods.remove(unexportedMethod); + } + + return exportedMethods.toArray(new Method[]{}); + } + + private boolean argsMatch(Method dm, Method m) { + + for (int i=0; i < m.getParameterTypes().length; i++) { + + if (!m.getParameterTypes()[i].isAssignableFrom(dm.getParameterTypes()[i])) { + + return false; + } + } + + return true; + } + } + + public static boolean isClientAbortException(Exception e) { + if (e.getClass().getSimpleName().equals("ClientAbortException")) { + return true; + } + return false; + } +} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/StoreContentService.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/StoreContentService.java new file mode 100644 index 000000000..1015b0667 --- /dev/null +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/contentservice/StoreContentService.java @@ -0,0 +1,118 @@ +package internal.org.springframework.content.rest.contentservice; + +import static java.lang.String.format; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.content.commons.io.DeletableResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.WritableResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.server.ResponseStatusException; + +import internal.org.springframework.content.rest.controllers.MethodNotAllowedException; +import internal.org.springframework.content.rest.io.RenderableResource; +import internal.org.springframework.content.rest.io.RenderedResource; +import internal.org.springframework.content.rest.mappings.StoreByteRangeHttpRequestHandler; + +public class StoreContentService implements ContentService { + + private static final Logger logger = LoggerFactory.getLogger(StoreContentService.class); + + private final StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler; + + public StoreContentService(StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler) { + this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; + } + + @Override + public void getContent(HttpServletRequest request, HttpServletResponse response, HttpHeaders headers, Resource resource, MediaType resourceType) + throws ResponseStatusException { + try { + MediaType producedResourceType = null; + List acceptedMimeTypes = headers.getAccept(); + if (acceptedMimeTypes.size() > 0) { + + MediaType.sortBySpecificityAndQuality(acceptedMimeTypes); + for (MediaType acceptedMimeType : acceptedMimeTypes) { + if (resource instanceof RenderableResource && ((RenderableResource) resource) + .isRenderableAs(acceptedMimeType)) { + resource = new RenderedResource(((RenderableResource) resource) + .renderAs(acceptedMimeType), resource); + producedResourceType = acceptedMimeType; + break; + } + else if (acceptedMimeType.includes(resourceType)) { + producedResourceType = resourceType; + break; + } + } + + if (producedResourceType == null) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + return; + } + } + + request.setAttribute("SPRING_CONTENT_RESOURCE", resource); + request.setAttribute("SPRING_CONTENT_CONTENTTYPE", producedResourceType); + } catch (Exception e) { + + logger.error("Unable to retrieve content", e); + + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, format("Failed to handle request for %s", resource.getDescription()), e); + } + + try { + byteRangeRestRequestHandler.handleRequest(request, response); + } + catch (Exception e) { + + if (ContentStoreContentService.isClientAbortException(e)) { + // suppress + } else { + logger.error("Unable to handle request", e); + + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, format("Failed to handle request for %s", resource.getDescription()), e); + } + } + } + + @Override + public void setContent(HttpServletRequest request, HttpServletResponse response, HttpHeaders headers, Resource source, MediaType sourceMimeType, Resource target) + throws IOException, MethodNotAllowedException { + + InputStream in = source.getInputStream(); + OutputStream out = ((WritableResource) target).getOutputStream(); + IOUtils.copy(in, out); + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(in); + + } + + @Override + public void unsetContent(Resource resource) { + + Assert.notNull(resource); + if (resource instanceof DeletableResource) { + try { + ((DeletableResource) resource).delete(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceHandlerMethodArgumentResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceHandlerMethodArgumentResolver.java deleted file mode 100644 index de3ad5920..000000000 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ContentServiceHandlerMethodArgumentResolver.java +++ /dev/null @@ -1,544 +0,0 @@ -package internal.org.springframework.content.rest.controllers; - -import static java.lang.String.format; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.Method; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.content.commons.annotations.MimeType; -import org.springframework.content.commons.annotations.OriginalFileName; -import org.springframework.content.commons.io.DeletableResource; -import org.springframework.content.commons.repository.ContentStore; -import org.springframework.content.commons.repository.Store; -import org.springframework.content.commons.storeservice.StoreInfo; -import org.springframework.content.commons.storeservice.Stores; -import org.springframework.content.commons.utils.BeanUtils; -import org.springframework.content.rest.RestResource; -import org.springframework.content.rest.config.RestConfiguration; -import org.springframework.content.rest.config.RestConfiguration.Resolver; -import org.springframework.content.rest.controllers.ContentService; -import org.springframework.context.ApplicationContext; -import org.springframework.core.MethodParameter; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.Resource; -import org.springframework.core.io.WritableResource; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.repository.support.RepositoryInvoker; -import org.springframework.data.repository.support.RepositoryInvokerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.ModelAndViewContainer; -import org.springframework.web.server.ResponseStatusException; - -import internal.org.springframework.content.rest.io.RenderableResource; -import internal.org.springframework.content.rest.io.RenderedResource; -import internal.org.springframework.content.rest.mappings.StoreByteRangeHttpRequestHandler; - -public class ContentServiceHandlerMethodArgumentResolver extends StoreHandlerMethodArgumentResolver { - - private static final Logger logger = LoggerFactory.getLogger(ContentServiceHandlerMethodArgumentResolver.class); - - private static final Map, StoreExportedMethodsMap> storeExportedMethods = new HashMap<>(); - - private final StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler; - - private ApplicationContext context; - - public ContentServiceHandlerMethodArgumentResolver(RestConfiguration config, Repositories repositories, RepositoryInvokerFactory repoInvokerFactory, Stores stores, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler, ApplicationContext context) { - super(config, repositories, repoInvokerFactory, stores); - this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; - this.context = context; - } - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return ContentService.class.isAssignableFrom(parameter.getParameterType()); - } - - @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - - return super.resolveArgument(parameter, mavContainer, webRequest, binderFactory); - } - - - @Override - protected Object resolveStoreArgument(NativeWebRequest nativeWebRequest, StoreInfo info) { - - return new StoreContentService(info.getImplementation(Store.class), byteRangeRestRequestHandler); - } - - @Override - protected Object resolveAssociativeStoreEntityArgument(StoreInfo info, Object domainObj) { - - if (ContentStore.class.isAssignableFrom(info.getInterface())) { - return new ContentStoreContentService(getConfig(), info, this.getRepoInvokerFactory().getInvokerFor(domainObj.getClass()), domainObj, byteRangeRestRequestHandler, context); - } - throw new UnsupportedOperationException(format("No content service for store %s", info.getInterface())); - } - - @Override - protected Object resolveAssociativeStorePropertyArgument(StoreInfo info, Object domainObj, Object propertyVal, boolean embeddedProperty) { - - if (ContentStore.class.isAssignableFrom(info.getInterface())) { - if (embeddedProperty) { - return new ContentStoreContentService(getConfig(), info, this.getRepoInvokerFactory().getInvokerFor(domainObj.getClass()), domainObj, propertyVal, byteRangeRestRequestHandler); - } else { - return new ContentStoreContentService(getConfig(), info, this.getRepoInvokerFactory().getInvokerFor(propertyVal.getClass()), propertyVal, byteRangeRestRequestHandler, context); - } - } - throw new UnsupportedOperationException(format("ContentService for interface '%s' not implemented", info.getInterface())); - } - - public static class StoreContentService implements ContentService { - - private final Store store; - private final StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler; - - public StoreContentService(Store store, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler) { - this.store = store; - this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; - } - - @Override - public void getContent(HttpServletRequest request, HttpServletResponse response, HttpHeaders headers, Resource resource, MediaType resourceType) - throws ResponseStatusException { - try { - MediaType producedResourceType = null; - List acceptedMimeTypes = headers.getAccept(); - if (acceptedMimeTypes.size() > 0) { - - MediaType.sortBySpecificityAndQuality(acceptedMimeTypes); - for (MediaType acceptedMimeType : acceptedMimeTypes) { - if (resource instanceof RenderableResource && ((RenderableResource) resource) - .isRenderableAs(acceptedMimeType)) { - resource = new RenderedResource(((RenderableResource) resource) - .renderAs(acceptedMimeType), resource); - producedResourceType = acceptedMimeType; - break; - } - else if (acceptedMimeType.includes(resourceType)) { - producedResourceType = resourceType; - break; - } - } - - if (producedResourceType == null) { - response.setStatus(HttpStatus.NOT_FOUND.value()); - return; - } - } - - request.setAttribute("SPRING_CONTENT_RESOURCE", resource); - request.setAttribute("SPRING_CONTENT_CONTENTTYPE", producedResourceType); - } catch (Exception e) { - - logger.error("Unable to retrieve content", e); - - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, format("Failed to handle request for %s", resource.getDescription()), e); - } - - try { - byteRangeRestRequestHandler.handleRequest(request, response); - } - catch (Exception e) { - - if (isClientAbortException(e)) { - // suppress - } else { - logger.error("Unable to handle request", e); - - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, format("Failed to handle request for %s", resource.getDescription()), e); - } - } - } - - @Override - public void setContent(HttpServletRequest request, HttpServletResponse response, HttpHeaders headers, Resource source, MediaType sourceMimeType, Resource target) - throws IOException, MethodNotAllowedException { - - InputStream in = source.getInputStream(); - OutputStream out = ((WritableResource) target).getOutputStream(); - IOUtils.copy(in, out); - IOUtils.closeQuietly(out); - IOUtils.closeQuietly(in); - - } - - @Override - public void unsetContent(Resource resource) { - - Assert.notNull(resource); - if (resource instanceof DeletableResource) { - try { - ((DeletableResource) resource).delete(); - } - catch (IOException e) { - e.printStackTrace(); - } - } - } - } - - public static class ContentStoreContentService implements ContentService { - - private final RestConfiguration config; - private final StoreInfo store; - private final RepositoryInvoker repoInvoker; - private final Object domainObj; - private final Object embeddedProperty; - private final StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler; - - public ContentStoreContentService(RestConfiguration config, StoreInfo store, RepositoryInvoker repoInvoker, Object domainObj, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler) { - this.config = config; - this.store = store; - this.repoInvoker = repoInvoker; - this.domainObj = domainObj; - this.embeddedProperty = null; - this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; - } - - public ContentStoreContentService(RestConfiguration config, StoreInfo store, RepositoryInvoker repoInvoker, Object domainObj, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler, ApplicationContext context) { - this.config = config; - this.store = store; - this.repoInvoker = repoInvoker; - this.domainObj = domainObj; - this.embeddedProperty = null; - this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; - } - - public ContentStoreContentService(RestConfiguration config, StoreInfo store, RepositoryInvoker repoInvoker, Object domainObj, Object embeddedProperty, StoreByteRangeHttpRequestHandler byteRangeRestRequestHandler) { - this.config = config; - this.store = store; - this.repoInvoker = repoInvoker; - this.domainObj = domainObj; - this.embeddedProperty = embeddedProperty; - this.byteRangeRestRequestHandler = byteRangeRestRequestHandler; - } - - @Override - public void getContent(HttpServletRequest request, HttpServletResponse response, HttpHeaders headers, Resource resource, MediaType resourceType) - throws ResponseStatusException, MethodNotAllowedException { - - Method[] methodsToUse = getExportedMethodsFor(store.getInterface()).getContentMethods(); - - if (methodsToUse.length > 1) { - throw new IllegalStateException("Too many getContent methods"); - } - - if (methodsToUse.length == 0) { - throw new MethodNotAllowedException(); - } - - try { - MediaType producedResourceType = null; - List acceptedMimeTypes = headers.getAccept(); - if (acceptedMimeTypes.size() > 0) { - - MediaType.sortBySpecificityAndQuality(acceptedMimeTypes); - for (MediaType acceptedMimeType : acceptedMimeTypes) { - - if (acceptedMimeType.includes(resourceType) && matchParameters(acceptedMimeType, resourceType)) { - - producedResourceType = resourceType; - break; - - } else if (resource instanceof RenderableResource && - ((RenderableResource) resource).isRenderableAs(acceptedMimeType)) { - - resource = new RenderedResource(((RenderableResource) resource).renderAs(acceptedMimeType), resource); - producedResourceType = acceptedMimeType; - break; - } - } - - if (producedResourceType == null) { - response.setStatus(HttpStatus.NOT_FOUND.value()); - return; - } - } - - request.setAttribute("SPRING_CONTENT_RESOURCE", resource); - request.setAttribute("SPRING_CONTENT_CONTENTTYPE", producedResourceType); - } catch (Exception e) { - - logger.error("Unable to retrieve content", e); - - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, format("Failed to handle request for %s", resource.getDescription()), e); - } - - try { - byteRangeRestRequestHandler.handleRequest(request, response); - } - catch (Exception e) { - if (isClientAbortException(e)) { - // suppress - } else { - logger.error("Unable to handle request", e); - - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, format("Failed to handle request for %s", resource.getDescription()), e); - } - } - } - - @Override - public void setContent(HttpServletRequest request, HttpServletResponse response, HttpHeaders headers, Resource source, MediaType sourceMimeType, Resource target) throws IOException, MethodNotAllowedException { - - if (BeanUtils.hasFieldWithAnnotation(embeddedProperty == null ? domainObj : embeddedProperty, MimeType.class)) { - BeanUtils.setFieldWithAnnotation(embeddedProperty == null ? domainObj : embeddedProperty, MimeType.class, sourceMimeType.toString()); - } - - String originalFilename = source.getFilename(); - if (source.getFilename() != null && StringUtils.hasText(originalFilename)) { - if (BeanUtils.hasFieldWithAnnotation(embeddedProperty == null ? domainObj : embeddedProperty, OriginalFileName.class)) { - BeanUtils.setFieldWithAnnotation(embeddedProperty == null ? domainObj : embeddedProperty, OriginalFileName.class, originalFilename); - } - } - - Method[] methodsToUse = getExportedMethodsFor(store.getInterface()).setContentMethods(); - - if (methodsToUse.length > 1) { - RestConfiguration.DomainTypeConfig dtConfig = config.forDomainType(store.getDomainObjectClass()); - methodsToUse = filterMethods(methodsToUse, dtConfig.getSetContentResolver(), headers); - } - - if (methodsToUse.length > 1) { - throw new UnsupportedOperationException(format("Too many setContent methods exported. Expected 1. Got %s", methodsToUse.length)); - } - - if (methodsToUse.length == 0) { - throw new MethodNotAllowedException(); - } - - Method methodToUse = methodsToUse[0]; - Object contentArg = convertContentArg(source, methodToUse.getParameterTypes()[1]); - - try { - Object targetObj = store.getImplementation(ContentStore.class); - - ReflectionUtils.makeAccessible(methodToUse); - - Object updatedDomainObj = ReflectionUtils.invokeMethod(methodToUse, targetObj, (embeddedProperty == null ? domainObj : embeddedProperty), contentArg); - repoInvoker.invokeSave(embeddedProperty == null ? updatedDomainObj : domainObj); - } finally { - cleanup(contentArg); - } - } - - @Override - public void unsetContent(Resource resource) throws MethodNotAllowedException { - - Method[] methodsToUse = getExportedMethodsFor(store.getInterface()).unsetContentMethods(); - - if (methodsToUse.length == 0) { - throw new MethodNotAllowedException(); - } - - if (methodsToUse.length > 1) { - throw new IllegalStateException("Too many unsetContent methods"); - } - - Object targetObj = store.getImplementation(ContentStore.class); - - ReflectionUtils.makeAccessible(methodsToUse[0]); - - Object updatedDomainObj = ReflectionUtils.invokeMethod(methodsToUse[0], targetObj, (embeddedProperty == null ? domainObj : embeddedProperty)); - - if (BeanUtils.hasFieldWithAnnotation(embeddedProperty == null ? updatedDomainObj : embeddedProperty, MimeType.class)) { - BeanUtils.setFieldWithAnnotation(embeddedProperty == null ? updatedDomainObj : embeddedProperty, MimeType.class, null); - } - - if (BeanUtils.hasFieldWithAnnotation(embeddedProperty == null ? updatedDomainObj : embeddedProperty, OriginalFileName.class)) { - BeanUtils.setFieldWithAnnotation(embeddedProperty == null ? updatedDomainObj : embeddedProperty, OriginalFileName.class, null); - } - - repoInvoker.invokeSave(embeddedProperty == null ? updatedDomainObj : domainObj); - } - - private void cleanup(Object contentArg) { - - if (contentArg == null) { - return; - } - - if (FileSystemResource.class.isAssignableFrom(contentArg.getClass())) { - ((FileSystemResource)contentArg).getFile().delete(); - } - } - - private Object convertContentArg(Resource resource, Class parameterType) { - - if (InputStream.class.equals(parameterType)) { - try { - return resource.getInputStream(); - } catch (IOException e) { - throw new IllegalArgumentException(format("Unable to get inputstream from resource %s", resource.getFilename())); - } - } else if (Resource.class.equals(parameterType)) { - try { - File f = Files.createTempFile("", "").toFile(); - FileUtils.copyInputStreamToFile(resource.getInputStream(), f); - return new FileSystemResource(f); - } catch (IOException e) { - throw new IllegalArgumentException(format("Unable to re-purpose resource %s", resource.getFilename())); - } - } else { - throw new IllegalArgumentException(format("Unsupported content type %s", parameterType.getCanonicalName())); - } - } - - private Method[] filterMethods(Method[] methods, Resolver resolver, HttpHeaders headers) { - - List resolved = new ArrayList<>(); - for (Method method : methods) { - if (resolver.resolve(method, headers)) { - resolved.add(method); - } - } - - return resolved.toArray(new Method[]{}); - } - - private boolean matchParameters(MediaType acceptedMediaType, MediaType producableMediaType) { - for (String name : producableMediaType.getParameters().keySet()) { - String s1 = producableMediaType.getParameter(name); - String s2 = acceptedMediaType.getParameter(name); - if (StringUtils.hasText(s1) && StringUtils.hasText(s2) && !s1.equalsIgnoreCase(s2)) { - return false; - } - } - return true; - } - } - - private static StoreExportedMethodsMap getExportedMethodsFor(Class storeInterfaceClass) { - - StoreExportedMethodsMap exportMap = storeExportedMethods.get(storeInterfaceClass); - if (exportMap == null) { - storeExportedMethods.put(storeInterfaceClass, new StoreExportedMethodsMap(storeInterfaceClass)); - exportMap = storeExportedMethods.get(storeInterfaceClass); - } - - return exportMap; - } - - public static class StoreExportedMethodsMap { - - private static Method[] SETCONTENT_METHODS = null; - private static Method[] UNSETCONTENT_METHODS = null; - private static Method[] GETCONTENT_METHODS = null; - - static { - SETCONTENT_METHODS = new Method[] { - ReflectionUtils.findMethod(ContentStore.class, "setContent", Object.class, InputStream.class), - ReflectionUtils.findMethod(ContentStore.class, "setContent", Object.class, Resource.class), - }; - - UNSETCONTENT_METHODS = new Method[] { - ReflectionUtils.findMethod(ContentStore.class, "unsetContent", Object.class), - }; - - GETCONTENT_METHODS = new Method[] { - ReflectionUtils.findMethod(ContentStore.class, "getContent", Object.class), - }; - } - - private Class storeInterface; - private Method[] getContentMethods; - private Method[] setContentMethods; - private Method[] unsetContentMethods; - - public StoreExportedMethodsMap(Class storeInterface) { - this.storeInterface = storeInterface; - this.getContentMethods = calculateExports(GETCONTENT_METHODS); - this.setContentMethods = calculateExports(SETCONTENT_METHODS); - this.unsetContentMethods = calculateExports(UNSETCONTENT_METHODS); - } - - public Method[] getContentMethods() { - return this.getContentMethods; - } - - public Method[] setContentMethods() { - return this.setContentMethods; - } - - public Method[] unsetContentMethods() { - return this.unsetContentMethods; - } - - private Method[] calculateExports(Method[] storeMethods) { - - List exportedMethods = new ArrayList<>(); - exportedMethods.addAll(Arrays.asList(storeMethods)); - - List unexportedMethods = new ArrayList<>(); - - for (Method m : exportedMethods) { - for (Method dm : storeInterface.getDeclaredMethods()) { - if (!dm.isBridge()) { - if (dm.getName().equals(m.getName())) { - if (argsMatch(dm, m)) { - - RestResource r = dm.getAnnotation(RestResource.class); - if (r != null && r.exported() == false) { - - unexportedMethods.add(m); - } - } - } - } - } - } - - for (Method unexportedMethod : unexportedMethods) { - - exportedMethods.remove(unexportedMethod); - } - - return exportedMethods.toArray(new Method[]{}); - } - - private boolean argsMatch(Method dm, Method m) { - - for (int i=0; i < m.getParameterTypes().length; i++) { - - if (!m.getParameterTypes()[i].isAssignableFrom(dm.getParameterTypes()[i])) { - - return false; - } - } - - return true; - } - } - - private static boolean isClientAbortException(Exception e) { - if (e.getClass().getSimpleName().equals("ClientAbortException")) { - return true; - } - return false; - } -} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceETagMethodArgumentResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceETagMethodArgumentResolver.java deleted file mode 100644 index aff07cac2..000000000 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceETagMethodArgumentResolver.java +++ /dev/null @@ -1,62 +0,0 @@ -package internal.org.springframework.content.rest.controllers; - -import javax.persistence.Version; - -import org.springframework.content.commons.storeservice.StoreInfo; -import org.springframework.content.commons.storeservice.Stores; -import org.springframework.content.commons.utils.BeanUtils; -import org.springframework.content.rest.config.RestConfiguration; -import org.springframework.core.MethodParameter; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.repository.support.RepositoryInvokerFactory; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.ModelAndViewContainer; - -public class ResourceETagMethodArgumentResolver extends StoreHandlerMethodArgumentResolver { - - public ResourceETagMethodArgumentResolver(RestConfiguration config, Repositories repositories, RepositoryInvokerFactory repoInvokerFactory, Stores stores) { - super(config, repositories, repoInvokerFactory, stores); - } - - @Override - public boolean supportsParameter(MethodParameter methodParameter) { - return "resourceETag".equals(methodParameter.getParameterName()); - } - - @Override - public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { - - return super.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory); - } - - @Override - protected Object resolveStoreArgument(NativeWebRequest nativeWebRequest, StoreInfo info) { - - // do store resource resolution - return ""; - } - - @Override - protected Object resolveAssociativeStoreEntityArgument(StoreInfo info, Object domainObj) { - - Object version = BeanUtils.getFieldWithAnnotation(domainObj, Version.class); - if (version == null) { - version = ""; - } - return version; - } - - @Override - protected Object resolveAssociativeStorePropertyArgument(StoreInfo storeInfo, Object domainObj, Object propertyVal, boolean embeddedProperty) { - - Object version = BeanUtils.getFieldWithAnnotation(propertyVal, Version.class); - if (version == null) { - version = BeanUtils.getFieldWithAnnotation(domainObj, Version.class); - } - if (version == null) { - version = ""; - } - return version.toString(); - } -} 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 7e3616f67..0ca4c0cfd 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 @@ -2,7 +2,6 @@ import javax.servlet.http.HttpServletRequest; -import org.springframework.content.commons.renditions.Renderable; import org.springframework.content.commons.repository.AssociativeStore; import org.springframework.content.commons.repository.Store; import org.springframework.content.commons.storeservice.StoreInfo; @@ -17,9 +16,8 @@ import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.util.UrlPathHelper; -import internal.org.springframework.content.rest.io.AssociatedResource; -import internal.org.springframework.content.rest.io.AssociatedResourceImpl; -import internal.org.springframework.content.rest.io.RenderableResourceImpl; +import internal.org.springframework.content.rest.io.AssociatedStorePropertyResourceImpl; +import internal.org.springframework.content.rest.io.AssociatedStoreResourceImpl; import internal.org.springframework.content.rest.io.StoreResourceImpl; import internal.org.springframework.content.rest.utils.StoreUtils; @@ -45,18 +43,14 @@ protected Object resolveStoreArgument(NativeWebRequest nativeWebRequest, StoreIn String path = new UrlPathHelper().getPathWithinApplication(nativeWebRequest.getNativeRequest(HttpServletRequest.class)); String pathToUse = path.substring(StoreUtils.storePath(info).length() + 1); - return new StoreResourceImpl(info.getImplementation(Store.class).getResource(pathToUse)); + return new StoreResourceImpl(info, info.getImplementation(Store.class).getResource(pathToUse)); } @Override protected Object resolveAssociativeStoreEntityArgument(StoreInfo storeInfo, Object domainObj) { Resource r = storeInfo.getImplementation(AssociativeStore.class).getResource(domainObj); - r = new AssociatedResourceImpl(domainObj, r); - - if (Renderable.class.isAssignableFrom(storeInfo.getInterface())) { - r = new RenderableResourceImpl((Renderable)storeInfo.getImplementation(AssociativeStore.class), (AssociatedResource)r); - } + r = new AssociatedStoreResourceImpl(storeInfo, domainObj, r); return r; } @@ -65,11 +59,7 @@ protected Object resolveAssociativeStorePropertyArgument(StoreInfo storeInfo, Ob AssociativeStore s = storeInfo.getImplementation(AssociativeStore.class); Resource resource = s.getResource(propertyVal); -// resource = new AssociatedResourceImpl(propertyVal, resource); - resource = new AssociatedResourceImpl(propertyVal, domainObj, resource); - if (Renderable.class.isAssignableFrom(storeInfo.getInterface())) { - resource = new RenderableResourceImpl((Renderable)storeInfo.getImplementation(AssociativeStore.class), (AssociatedResource)resource); - } + resource = new AssociatedStorePropertyResourceImpl(storeInfo, propertyVal, embeddedProperty, domainObj, resource); return resource; } } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceTypeMethodArgumentResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceTypeMethodArgumentResolver.java deleted file mode 100644 index f6ecea32d..000000000 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/ResourceTypeMethodArgumentResolver.java +++ /dev/null @@ -1,61 +0,0 @@ -package internal.org.springframework.content.rest.controllers; - -import javax.activation.MimetypesFileTypeMap; -import javax.servlet.http.HttpServletRequest; - -import org.springframework.content.commons.annotations.MimeType; -import org.springframework.content.commons.storeservice.StoreInfo; -import org.springframework.content.commons.storeservice.Stores; -import org.springframework.content.commons.utils.BeanUtils; -import org.springframework.content.rest.config.RestConfiguration; -import org.springframework.core.MethodParameter; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.repository.support.RepositoryInvokerFactory; -import org.springframework.http.MediaType; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.ModelAndViewContainer; -import org.springframework.web.util.UrlPathHelper; - -import internal.org.springframework.content.rest.utils.StoreUtils; - -public class ResourceTypeMethodArgumentResolver extends StoreHandlerMethodArgumentResolver { - - public ResourceTypeMethodArgumentResolver(RestConfiguration config, Repositories repositories, RepositoryInvokerFactory repoInvokerFactory, Stores stores) { - super(config, repositories, repoInvokerFactory, stores); - } - - @Override - public boolean supportsParameter(MethodParameter methodParameter) { - return "resourceType".equals(methodParameter.getParameterName()); - } - - @Override - public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { - - return super.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory); - } - - @Override - protected Object resolveStoreArgument(NativeWebRequest nativeWebRequest, StoreInfo info) { - - String path = new UrlPathHelper().getPathWithinApplication(nativeWebRequest.getNativeRequest(HttpServletRequest.class)); - String pathToUse = path.substring(StoreUtils.storePath(info).length() + 1); - String mimeType = new MimetypesFileTypeMap().getContentType(pathToUse); - return MediaType.valueOf(mimeType != null ? mimeType : ""); - } - - @Override - protected Object resolveAssociativeStoreEntityArgument(StoreInfo info, Object domainObj) { - - Object mimeType = BeanUtils.getFieldWithAnnotation(domainObj, MimeType.class); - return MediaType.valueOf(mimeType != null ? mimeType.toString() : MediaType.ALL_VALUE); - } - - @Override - protected Object resolveAssociativeStorePropertyArgument(StoreInfo storeInfo, Object domainObj, Object propertyVal, boolean embeddedProperty) { - - Object mimeType = BeanUtils.getFieldWithAnnotation(propertyVal, MimeType.class); - return MediaType.valueOf(mimeType != null ? mimeType.toString() : MediaType.ALL_VALUE); - } -} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreInfoHandlerMethodArgumentResolver.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreInfoHandlerMethodArgumentResolver.java deleted file mode 100644 index bd1c92a90..000000000 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreInfoHandlerMethodArgumentResolver.java +++ /dev/null @@ -1,76 +0,0 @@ -package internal.org.springframework.content.rest.controllers; - -import javax.servlet.http.HttpServletRequest; - -import org.springframework.content.commons.repository.Store; -import org.springframework.content.commons.storeservice.StoreInfo; -import org.springframework.content.commons.storeservice.Stores; -import org.springframework.content.rest.config.RestConfiguration; -import org.springframework.core.MethodParameter; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.repository.support.RepositoryInvokerFactory; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; -import org.springframework.web.util.UrlPathHelper; - -import internal.org.springframework.content.rest.utils.StoreUtils; - -public class StoreInfoHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { - - private final RestConfiguration config; - private final Repositories repositories; - private final RepositoryInvokerFactory repoInvokerFactory; - private final Stores stores; - - public StoreInfoHandlerMethodArgumentResolver(RestConfiguration config, Repositories repositories, RepositoryInvokerFactory repoInvokerFactory, Stores stores) { - this.config = config; - this.repositories = repositories; - this.repoInvokerFactory = repoInvokerFactory; - this.stores = stores; - } - - RestConfiguration getConfig() { - return config; - } - - protected Repositories getRepositories() { - return repositories; - } - - RepositoryInvokerFactory getRepoInvokerFactory() { - return repoInvokerFactory; - } - - protected Stores getStores() { - return stores; - } - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return StoreInfo.class.isAssignableFrom(parameter.getParameterType()); - } - - @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - - String pathInfo = webRequest.getNativeRequest(HttpServletRequest.class).getRequestURI(); - pathInfo = new UrlPathHelper().getPathWithinApplication(webRequest.getNativeRequest(HttpServletRequest.class)); - pathInfo = StoreUtils.storeLookupPath(pathInfo, this.getConfig().getBaseUri()); - - String[] pathSegments = pathInfo.split("/"); - if (pathSegments.length < 2) { - return null; - } - - String store = pathSegments[1]; - - StoreInfo info = this.getStores().getStore(Store.class, StoreUtils.withStorePath(store)); - if (info == null) { - throw new IllegalArgumentException(String.format("Store for path %s not found", store)); - } - - return info; - } -} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java index 66d646730..bb8ce4d46 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/controllers/StoreRestController.java @@ -13,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.content.commons.storeservice.Stores; import org.springframework.content.rest.config.RestConfiguration; -import org.springframework.content.rest.controllers.ContentService; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory; @@ -31,6 +30,8 @@ import org.springframework.web.multipart.MultipartFile; import internal.org.springframework.content.rest.annotations.ContentRestController; +import internal.org.springframework.content.rest.contentservice.ContentService; +import internal.org.springframework.content.rest.contentservice.ContentServiceFactory; import internal.org.springframework.content.rest.io.InputStreamResource; import internal.org.springframework.content.rest.io.StoreResource; import internal.org.springframework.content.rest.mappings.StoreByteRangeHttpRequestHandler; @@ -45,10 +46,13 @@ public class StoreRestController implements InitializingBean { @Autowired ApplicationContext context; + @Autowired(required=false) private Repositories repositories; + @Autowired private Stores stores; + @Autowired(required=false) private RepositoryInvokerFactory repoInvokerFactory; @@ -61,19 +65,13 @@ public class StoreRestController implements InitializingBean { private ContentServiceFactory contentServiceFactory; public StoreRestController() { - contentServiceFactory = new ContentServiceFactory(config, repositories, repoInvokerFactory, stores, byteRangeRestRequestHandler); } @RequestMapping(value = STORE_REQUEST_MAPPING, method = RequestMethod.GET) public void getContent(HttpServletRequest request, HttpServletResponse response, @RequestHeader HttpHeaders headers, - Resource resource, - MediaType resourceType, - Object resourceETag, - ContentService contentService) -// StoreInfo storeInfo -// ) + Resource resource) throws MethodNotAllowedException { if (resource == null || resource.exists() == false) { @@ -84,100 +82,107 @@ public void getContent(HttpServletRequest request, long lastModified = -1; try { - lastModified = resource.lastModified(); + lastModified = storeResource.lastModified(); } catch (IOException e) {} if(new ServletWebRequest(request, response).checkNotModified(storeResource.getETag() != null ? storeResource.getETag().toString() : null, lastModified)) { -// if(new ServletWebRequest(request, response).checkNotModified(resourceETag != null ? resourceETag.toString() : null, lastModified)) { return; } -// ContentService contentService = contentServiceFactory.getContentService(storeInfo, storeResource); + ContentService contentService = contentServiceFactory.getContentService(storeResource); contentService.getContent(request, response, headers, storeResource, storeResource.getMimeType()); -// contentService.getContent(request, response, headers, storeResource, resourceType); } @RequestMapping(value = STORE_REQUEST_MAPPING, method = RequestMethod.PUT, headers = { "content-type!=multipart/form-data", "accept!=application/hal+json" }) @ResponseBody public void putContent(HttpServletRequest request, HttpServletResponse response, @RequestHeader HttpHeaders headers, - Resource resource, - Object resourceETag, - ContentService contentService) + Resource resource) throws IOException, MethodNotAllowedException { + StoreResource storeResource = (StoreResource)resource; + + ContentService contentService = contentServiceFactory.getContentService(storeResource); + handleMultipart(request, response, headers, contentService, new InputStreamResource(request.getInputStream(), null), headers.getContentType(), - resource, - resourceETag); + storeResource, + storeResource.getETag()); } @RequestMapping(value = STORE_REQUEST_MAPPING, method = RequestMethod.PUT, headers = "content-type=multipart/form-data") @ResponseBody public void putMultipartContent(HttpServletRequest request, HttpServletResponse response, @RequestHeader HttpHeaders headers, @RequestParam("file") MultipartFile multiPart, - Resource resource, - Object resourceETag, - ContentService contentService) + StoreResource resource) throws IOException, MethodNotAllowedException { + StoreResource storeResource = resource; + + ContentService contentService = contentServiceFactory.getContentService(storeResource); + handleMultipart(request, response, headers, contentService, multiPart.getResource(), MediaType.parseMediaType(multiPart.getContentType()), - resource, - resourceETag); + storeResource, + storeResource.getETag()); } @RequestMapping(value = STORE_REQUEST_MAPPING, method = RequestMethod.POST, headers = "content-type=multipart/form-data") @ResponseBody public void postMultipartContent(HttpServletRequest request, HttpServletResponse response, @RequestHeader HttpHeaders headers, @RequestParam("file") MultipartFile multiPart, - Resource resource, - Object resourceETag, - ContentService contentService) + Resource resource) throws IOException, MethodNotAllowedException { + StoreResource storeResource = (StoreResource)resource; + + ContentService contentService = contentServiceFactory.getContentService(storeResource); + handleMultipart(request, response, headers, contentService, new InputStreamResource(multiPart.getInputStream(), multiPart.getOriginalFilename()), MediaType.parseMediaType(multiPart.getContentType()), - resource, - resourceETag); + storeResource, + storeResource.getETag()); } @RequestMapping(value = STORE_REQUEST_MAPPING, method = RequestMethod.POST, headers = {"content-type!=multipart/form-data"}) @ResponseBody public void postContent(HttpServletRequest request, HttpServletResponse response, @RequestHeader HttpHeaders headers, - Resource resource, - Object resourceETag, - ContentService contentService) + Resource resource) throws IOException, MethodNotAllowedException { + StoreResource storeResource = (StoreResource)resource; + + ContentService contentService = contentServiceFactory.getContentService(storeResource); + handleMultipart(request, response, headers, contentService, new InputStreamResource(request.getInputStream(), null), headers.getContentType(), - resource, - resourceETag); + storeResource, + storeResource.getETag()); } @RequestMapping(value = STORE_REQUEST_MAPPING, method = RequestMethod.DELETE, headers = "accept!=application/hal+json") public void deleteContent(@RequestHeader HttpHeaders headers, HttpServletResponse response, - Resource resource, - Object resourceETag, - ContentService contentService) + Resource resource) throws IOException, MethodNotAllowedException { + StoreResource storeResource = (StoreResource)resource; + if (!resource.exists()) { throw new ResourceNotFoundException(); } else { - HeaderUtils.evaluateHeaderConditions(headers, resourceETag != null ? resourceETag.toString() : null, new Date(resource.lastModified())); + HeaderUtils.evaluateHeaderConditions(headers, storeResource.getETag() != null ? storeResource.getETag().toString() : null, new Date(storeResource.lastModified())); } + ContentService contentService = contentServiceFactory.getContentService(storeResource); contentService.unsetContent(resource); response.setStatus(HttpStatus.NO_CONTENT.value()); @@ -188,7 +193,7 @@ protected void handleMultipart(HttpServletRequest request, HttpServletResponse r ContentService contentService, Resource source, MediaType sourceMimeType, - Resource target, + StoreResource target, Object targetETag) throws IOException, MethodNotAllowedException { @@ -212,14 +217,18 @@ protected void handleMultipart(HttpServletRequest request, HttpServletResponse r @Override public void afterPropertiesSet() throws Exception { + try { this.repositories = context.getBean(Repositories.class); } catch (BeansException be) { this.repositories = new Repositories(context); } + if (this.repoInvokerFactory == null) { this.repoInvokerFactory = new DefaultRepositoryInvokerFactory(this.repositories); } + + contentServiceFactory = new ContentServiceFactory(config, repositories, repoInvokerFactory, stores, byteRangeRestRequestHandler); } } \ 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 c6dbe4e3e..4f9e68c8d 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 @@ -19,8 +19,8 @@ import org.springframework.util.ReflectionUtils; import internal.org.springframework.content.rest.controllers.ResourceNotFoundException; -import internal.org.springframework.content.rest.io.AssociatedResource; -import internal.org.springframework.content.rest.io.AssociatedResourceImpl; +import internal.org.springframework.content.rest.io.AssociatedStoreResource; +import internal.org.springframework.content.rest.io.AssociatedStoreResourceImpl; import internal.org.springframework.content.rest.io.RenderableResourceImpl; public class RevisionEntityResolver { @@ -67,9 +67,9 @@ protected Resource resolve(StoreInfo i, Object e, Object p, boolean propertyIsEm AssociativeStore s = i.getImplementation(AssociativeStore.class); Resource resource = s.getResource(p); - resource = new AssociatedResourceImpl(p, resource); + resource = new AssociatedStoreResourceImpl(i, p, resource); if (Renderable.class.isAssignableFrom(i.getInterface())) { - resource = new RenderableResourceImpl((Renderable)i.getImplementation(AssociativeStore.class), (AssociatedResource)resource); + resource = new RenderableResourceImpl((Renderable)i.getImplementation(AssociativeStore.class), (AssociatedStoreResource)resource); } return resource; } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStorePropertyResource.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStorePropertyResource.java new file mode 100644 index 000000000..95dbf00f5 --- /dev/null +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStorePropertyResource.java @@ -0,0 +1,8 @@ +package internal.org.springframework.content.rest.io; + +public interface AssociatedStorePropertyResource extends AssociatedStoreResource { + + public boolean embedded(); + + public Object getProperty(); +} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStorePropertyResourceImpl.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStorePropertyResourceImpl.java new file mode 100644 index 000000000..399460b58 --- /dev/null +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStorePropertyResourceImpl.java @@ -0,0 +1,159 @@ +package internal.org.springframework.content.rest.io; + +import static java.lang.String.format; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Stream; + +import javax.persistence.Version; + +import org.springframework.content.commons.annotations.ContentLength; +import org.springframework.content.commons.annotations.MimeType; +import org.springframework.content.commons.annotations.OriginalFileName; +import org.springframework.content.commons.renditions.Renderable; +import org.springframework.content.commons.repository.AssociativeStore; +import org.springframework.content.commons.storeservice.StoreInfo; +import org.springframework.content.commons.utils.BeanUtils; +import org.springframework.core.io.Resource; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.util.StringUtils; + +/** + * Represents a Spring Content Resource that is associated with a Spring Data Entity. + * + * Overrides the ContentLength and LastModifiedDate with the values stored on the + * Spring Data entity, rather than the values of the actual content itself (these are + * used as fallback values though). + * + * Also sets appropriate headers to pass the Spring Data Entity recorded filename, if exists. + */ +public class AssociatedStorePropertyResourceImpl extends AssociatedStoreResourceImpl implements AssociatedStorePropertyResource { + + private Object property; + private boolean embedded; + + public AssociatedStorePropertyResourceImpl(StoreInfo info, Object property, boolean embedded, S entity, Resource original) { + + super(info, entity, original); + this.property = property; + this.embedded = embedded; + } + + @Override + public S getAssociation() { + + if (embedded) { + return super.getAssociation(); + } + + return (S)property; + } + + @Override + public boolean embedded() { + + return embedded; + } + + @Override + public Object getProperty() { + + return property; + } + + @Override + 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(property, mimeType.toString()); + } + + return false; + } + + @Override + 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(property, mimeType.toString()); + } + + return null; + } + + @Override + public Object getETag() { + + Object etag = null; + + if (property != null) { + etag = BeanUtils.getFieldWithAnnotation(property, Version.class); + } + + if (etag == null) { + etag = super.getETag(); + } + + return etag.toString(); + } + + @Override + public MediaType getMimeType() { + + Object mimeType = null; + + mimeType = BeanUtils.getFieldWithAnnotation(property, MimeType.class); + + return MediaType.valueOf(mimeType != null ? mimeType.toString() : MediaType.ALL_VALUE); + } + + @Override + public long contentLength() throws IOException { + + Long contentLength = (Long) BeanUtils.getFieldWithAnnotation(property, ContentLength.class); + if (contentLength == null) { + contentLength = getDelegate().contentLength(); + } + return contentLength; + } + + @Override + public long lastModified() throws IOException { + + Object lastModified = BeanUtils.getFieldWithAnnotation(property, LastModifiedDate.class); + if (lastModified == null) { + return getDelegate().lastModified(); + } + + return Stream.of(lastModified) + .map(it -> getConversionService().convert(it, Date.class))// + .map(it -> getConversionService().convert(it, Instant.class))// + .map(it -> it.toEpochMilli()) + .findFirst().orElseThrow(() -> new IllegalArgumentException(format("Invalid data type for @LastModifiedDate on Entity %s", property))); + } + + @Override + public HttpHeaders getResponseHeaders() { + + HttpHeaders headers = new HttpHeaders(); + + // Modified to show download + Object originalFileName = BeanUtils.getFieldWithAnnotation(property, OriginalFileName.class); + 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()); + } + return headers; + } +} diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResource.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java similarity index 60% rename from spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResource.java rename to spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java index 212f59185..02e660883 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResource.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResource.java @@ -2,7 +2,8 @@ import org.springframework.core.io.WritableResource; -public interface AssociatedResource extends WritableResource, StoreResource { +public interface AssociatedStoreResource extends WritableResource, StoreResource { S getAssociation(); + } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResourceImpl.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java similarity index 80% rename from spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResourceImpl.java rename to spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java index 66c843493..9ba5cfb75 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedResourceImpl.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/AssociatedStoreResourceImpl.java @@ -21,7 +21,11 @@ import org.springframework.content.commons.annotations.MimeType; import org.springframework.content.commons.annotations.OriginalFileName; import org.springframework.content.commons.io.DeletableResource; +import org.springframework.content.commons.renditions.Renderable; +import org.springframework.content.commons.repository.AssociativeStore; +import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.content.commons.utils.BeanUtils; +import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.Resource; @@ -44,7 +48,7 @@ * * Also sets appropriate headers to pass the Spring Data Entity recorded filename, if exists. */ -public class AssociatedResourceImpl implements HttpResource, AssociatedResource { +public class AssociatedStoreResourceImpl implements HttpResource, AssociatedStoreResource { private final ConfigurableConversionService conversionService = new DefaultConversionService(); @@ -55,18 +59,27 @@ public class AssociatedResourceImpl implements HttpResource, AssociatedResour private S entity; private Resource original; private Object property; + private StoreInfo info; - public AssociatedResourceImpl(S entity, Resource original) { + public AssociatedStoreResourceImpl(StoreInfo info, S entity, Resource original) { + this.info = info; this.entity = entity; this.original = original; } - public AssociatedResourceImpl(Object property, S entity, Resource original) { + public AssociatedStoreResourceImpl(StoreInfo info, Object property, S entity, Resource original) { + this.info = info; this.entity = entity; this.property = property; this.original = original; } + @Override + public StoreInfo getStoreInfo() { + + return info; + } + @Override public S getAssociation() { @@ -74,6 +87,39 @@ public S getAssociation() { return (S)obj; } + protected Resource getDelegate() { + + return original; + } + + protected ConversionService getConversionService() { + return conversionService; + } + + @Override + public boolean isRenderableAs(org.springframework.util.MimeType mimeType) { + + if (Renderable.class.isAssignableFrom(info.getInterface())) { + + Renderable renderer = (Renderable)info.getImplementation(AssociativeStore.class); + return renderer.hasRendition(entity, mimeType.toString()); + } + + return false; + } + + @Override + public InputStream renderAs(org.springframework.util.MimeType mimeType) { + + if (Renderable.class.isAssignableFrom(info.getInterface())) { + + Renderable renderer = (Renderable)info.getImplementation(AssociativeStore.class); + return renderer.getRendition(entity, mimeType.toString()); + } + + return null; + } + @Override public Object getETag() { diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResource.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResource.java index 5bac4ce55..6221f094f 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResource.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResource.java @@ -4,8 +4,7 @@ import org.springframework.util.MimeType; -public interface RenderableResource extends StoreResource { - +public interface RenderableResource { boolean isRenderableAs(MimeType mimeType); diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResourceImpl.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResourceImpl.java index e2c438b40..481618fa8 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResourceImpl.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/RenderableResourceImpl.java @@ -39,8 +39,8 @@ public RenderableResourceImpl(Renderable renderer, StoreResource original) { @Override public boolean isRenderableAs(MimeType mimeType) { - if (original instanceof AssociatedResource) { - return renderer.hasRendition(((AssociatedResource)original).getAssociation(), mimeType.toString()); + if (original instanceof AssociatedStoreResource) { + return renderer.hasRendition(((AssociatedStoreResource)original).getAssociation(), mimeType.toString()); } return false; @@ -49,19 +49,17 @@ public boolean isRenderableAs(MimeType mimeType) { @Override public InputStream renderAs(MimeType mimeType) { - if (original instanceof AssociatedResource) { - return renderer.getRendition(((AssociatedResource)original).getAssociation(), mimeType.toString()); + if (original instanceof AssociatedStoreResource) { + return renderer.getRendition(((AssociatedStoreResource)original).getAssociation(), mimeType.toString()); } return null; } - @Override public Object getETag() { return original.getETag(); } - @Override public MediaType getMimeType() { return original.getMimeType(); } @@ -146,13 +144,11 @@ public HttpHeaders getResponseHeaders() { } } - @Override public OutputStream getOutputStream() throws IOException { return ((WritableResource)original).getOutputStream(); } - @Override public void delete() throws IOException { ((DeletableResource)original).delete(); diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResource.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResource.java index a83e38951..1b5b8ef5d 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResource.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResource.java @@ -1,12 +1,22 @@ package internal.org.springframework.content.rest.io; +import java.io.InputStream; + import org.springframework.content.commons.io.DeletableResource; +import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.core.io.WritableResource; import org.springframework.http.MediaType; +import org.springframework.util.MimeType; public interface StoreResource extends WritableResource, DeletableResource { + StoreInfo getStoreInfo(); + Object getETag(); MediaType getMimeType(); + + boolean isRenderableAs(MimeType mimeType); + + InputStream renderAs(MimeType mimeType); } diff --git a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResourceImpl.java b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResourceImpl.java index dc1598d7e..28d842da4 100644 --- a/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResourceImpl.java +++ b/spring-content-rest/src/main/java/internal/org/springframework/content/rest/io/StoreResourceImpl.java @@ -12,20 +12,33 @@ import javax.activation.MimetypesFileTypeMap; import org.springframework.content.commons.io.DeletableResource; +import org.springframework.content.commons.storeservice.StoreInfo; import org.springframework.core.io.Resource; import org.springframework.core.io.WritableResource; import org.springframework.http.MediaType; import org.springframework.util.Assert; +import org.springframework.util.MimeType; public class StoreResourceImpl implements Resource, StoreResource { private Resource delegate; + private StoreInfo storeInfo; - public StoreResourceImpl(Resource delegate) { - Assert.notNull(delegate, "Delegate cannot be null"); + public StoreResourceImpl(StoreInfo storeInfo, Resource delegate) { + + Assert.notNull(storeInfo, "storeInfo cannot be null"); + Assert.notNull(delegate, "delegate cannot be null"); + + this.storeInfo = storeInfo; this.delegate = delegate; } + @Override + public StoreInfo getStoreInfo() { + + return storeInfo; + } + @Override public Object getETag() { @@ -43,6 +56,18 @@ public MediaType getMimeType() { return MediaType.valueOf(mimeType != null ? mimeType : ""); } + @Override + public boolean isRenderableAs(MimeType mimeType) { + + return false; + } + + @Override + public InputStream renderAs(MimeType mimeType) { + + throw new UnsupportedOperationException("not implemented"); + } + @Override public InputStream getInputStream() throws IOException { 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 f76389113..ff12cba36 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 @@ -22,7 +22,6 @@ import org.springframework.content.commons.utils.DomainObjectUtils; import org.springframework.content.rest.StoreRestResource; import org.springframework.content.rest.config.RestConfiguration; -import org.springframework.content.rest.controllers.ContentService; import org.springframework.core.io.Resource; import org.springframework.data.rest.webmvc.BaseUri; import org.springframework.data.rest.webmvc.PersistentEntityResource; @@ -32,7 +31,6 @@ import org.springframework.hateoas.server.RepresentationModelProcessor; import org.springframework.hateoas.server.core.LinkBuilderSupport; import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -51,7 +49,7 @@ public class ContentLinksResourceProcessor implements RepresentationModelProcess private static final Log log = LogFactory.getLog(ContentLinksResourceProcessor.class); - private static Method GET_CONTENT_METHOD = ReflectionUtils.findMethod(StoreRestController.class, "getContent", HttpServletRequest.class, HttpServletResponse.class, HttpHeaders.class, Resource.class, MediaType.class, Object.class, ContentService.class); + private static Method GET_CONTENT_METHOD = ReflectionUtils.findMethod(StoreRestController.class, "getContent", HttpServletRequest.class, HttpServletResponse.class, HttpHeaders.class, Resource.class); static { Assert.notNull(GET_CONTENT_METHOD, "Unable to find StoreRestController.getContent method"); 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 5a73ce126..85970a9e5 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 @@ -26,11 +26,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import internal.org.springframework.content.commons.storeservice.StoresImpl; -import internal.org.springframework.content.rest.controllers.ContentServiceHandlerMethodArgumentResolver; -import internal.org.springframework.content.rest.controllers.ResourceETagMethodArgumentResolver; import internal.org.springframework.content.rest.controllers.ResourceHandlerMethodArgumentResolver; -import internal.org.springframework.content.rest.controllers.ResourceTypeMethodArgumentResolver; -import internal.org.springframework.content.rest.controllers.StoreInfoHandlerMethodArgumentResolver; import internal.org.springframework.content.rest.mappings.ContentHandlerMapping; import internal.org.springframework.content.rest.mappings.StoreByteRangeHttpRequestHandler; @@ -140,10 +136,6 @@ public static class WebConfig implements WebMvcConfigurer, InitializingBean { public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(new ResourceHandlerMethodArgumentResolver(config, repositories, repoInvokerFactory, stores)); - argumentResolvers.add(new ResourceTypeMethodArgumentResolver(config, repositories, repoInvokerFactory, stores)); - argumentResolvers.add(new ResourceETagMethodArgumentResolver(config, repositories, repoInvokerFactory, stores)); - argumentResolvers.add(new ContentServiceHandlerMethodArgumentResolver(config, repositories, repoInvokerFactory, stores, byteRangeRestRequestHandler, context)); - argumentResolvers.add(new StoreInfoHandlerMethodArgumentResolver(config, repositories, repoInvokerFactory, stores)); } @Override 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 ba8807f5e..2ec22835d 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 @@ -3,7 +3,6 @@ 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 java.lang.String.format; import static org.hamcrest.CoreMatchers.is; @@ -146,7 +145,7 @@ public class ContentPropertyRestEndpointsIT { }); Context("given the content property is accessed via the /{repository}/{id}/{contentProperty} endpoint", () -> { Context("a GET to /{repository}/{id}/{contentProperty}", () -> { - FIt("should return the content", () -> { + It("should return the content", () -> { MockHttpServletResponse response = mvc .perform(get("/files/" + testEntity2.getId() + "/child") .accept("text/plain"))