diff --git a/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServlet.java b/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServlet.java index eafb52c16e..155bf0a9e3 100644 --- a/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServlet.java +++ b/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServlet.java @@ -19,6 +19,7 @@ import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Calendar; @@ -30,6 +31,11 @@ import java.util.SortedSet; import java.util.TreeSet; +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.FilenameUtils; @@ -37,6 +43,8 @@ import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.api.binary.BinaryDownload; +import org.apache.jackrabbit.api.binary.BinaryDownloadOptions; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.request.RequestPathInfo; @@ -96,6 +104,9 @@ */ public class AdaptiveImageServlet extends SlingSafeMethodsServlet { + private static final boolean USE_DELIVERY_VIA_BLOBSTORE = true; + + public static final String DEFAULT_SELECTOR = "img"; public static final String CORE_DEFAULT_SELECTOR = "coreimg"; static final int DEFAULT_RESIZE_WIDTH = 1280; @@ -107,6 +118,8 @@ public class AdaptiveImageServlet extends SlingSafeMethodsServlet { private static final String SELECTOR_WIDTH_KEY = "width"; private int defaultResizeWidth; private int maxInputWidth; + + protected boolean deliverExistingRenditionsViaRedirect; private AdaptiveImageServletMetrics metrics; @@ -115,12 +128,13 @@ public class AdaptiveImageServlet extends SlingSafeMethodsServlet { private transient AssetStore assetStore; public AdaptiveImageServlet(MimeTypeService mimeTypeService, AssetStore assetStore, AdaptiveImageServletMetrics metrics, - int defaultResizeWidth, int maxInputWidth) { + int defaultResizeWidth, int maxInputWidth, boolean deliverRenditionsViaRedirect) { this.mimeTypeService = mimeTypeService; this.assetStore = assetStore; this.metrics = metrics; this.defaultResizeWidth = defaultResizeWidth > 0 ? defaultResizeWidth : DEFAULT_RESIZE_WIDTH; this.maxInputWidth = maxInputWidth > 0 ? maxInputWidth : DEFAULT_MAX_SIZE; + this.deliverExistingRenditionsViaRedirect = deliverRenditionsViaRedirect; } @Override @@ -254,11 +268,7 @@ protected void transformAndStreamAsset(SlingHttpServletResponse response, ValueM if ("gif".equalsIgnoreCase(extension) || "svg".equalsIgnoreCase(extension)) { LOGGER.debug("GIF or SVG asset detected; will render the original rendition."); metrics.markOriginalRenditionUsed(); - try (InputStream is = asset.getOriginal().getStream()) { - if (is != null) { - stream(response, is, imageType, imageName); - } - } + deliverRendition(response,asset.getOriginal(),imageType, imageName); return; } int rotationAngle = getRotation(componentProperties); @@ -372,26 +382,30 @@ protected void transformAndStreamAsset(SlingHttpServletResponse response, ValueM } } else { LOGGER.debug("No need to perform any processing on asset {}; rendering.", asset.getPath()); - try (InputStream is = getOriginal(asset).getStream()) { - if (is != null) { - stream(response, is, imageType, imageName); - } - } + deliverRendition(response, asset.getOriginal(), imageType, imageName); } } private void transformAndStreamFile(SlingHttpServletResponse response, ValueMap componentProperties, int resizeWidth, double quality, Resource imageFile, String imageType, String imageName) throws IOException { - try (InputStream is = imageFile.adaptTo(InputStream.class)) { - if ("gif".equalsIgnoreCase(mimeTypeService.getExtension(imageType)) - || "svg".equalsIgnoreCase(mimeTypeService.getExtension(imageType))) { - LOGGER.debug("GIF or SVG file detected; will render the original file."); - if (is != null) { - stream(response, is, imageType, imageName); + + if ("gif".equalsIgnoreCase(mimeTypeService.getExtension(imageType)) + || "svg".equalsIgnoreCase(mimeTypeService.getExtension(imageType))) { + LOGGER.debug("GIF or SVG file detected; will render the original file."); + + try { + Node n = imageFile.adaptTo(Node.class); + if (n != null && n.isNodeType("nt:file")) { + deliverFile(response, imageFile, imageType, imageName); + return; } - return; + } catch (RepositoryException e) { + throw new IOException(String.format("cannot deliver file %s", imageFile.getPath()),e); } + } + + try (InputStream is = imageFile.adaptTo(InputStream.class)) { int rotationAngle = getRotation(componentProperties); Rectangle rectangle = getCropRect(componentProperties); boolean flipHorizontally = componentProperties.get(Image.PN_FLIP_HORIZONTAL, Boolean.FALSE); @@ -613,22 +627,14 @@ private void streamOrConvert(@NotNull SlingHttpServletResponse response, @NotNul if (rendition.getMimeType().equals(imageType)) { LOGGER.debug("Found rendition {}/{} has a width of {}px and does not require a resize for requested width of {}px", rendition.getAsset().getPath(), rendition.getName(), dimension != null ? dimension.getWidth() : null, resizeWidth); - try (InputStream is = rendition.getStream()) { - if (is != null) { - stream(response, is, imageType, imageName); - } - } + deliverRendition(response, rendition.getRendition(), imageType, imageName); } else { Layer layer = getLayer(rendition); if (layer == null) { LOGGER.warn("Found rendition {}/{} has a width of {}px and does not require a resize for requested width of {}px " + "but the rendition is not of the requested type {}, cannot convert so serving as is", rendition.getAsset().getPath(), rendition.getName(), dimension != null ? dimension.getWidth() : null, resizeWidth, imageType); - try (InputStream is = rendition.getStream()) { - if (is != null) { - stream(response, is, rendition.getMimeType(), imageName); - } - } + deliverRendition(response, rendition.getRendition(), rendition.getMimeType(), imageName); } else { LOGGER.debug("Found rendition {}/{} has a width of {}px and does not require a resize for requested width of {}px " + "but the rendition is not of the requested type {}, need to convert", @@ -638,6 +644,92 @@ private void streamOrConvert(@NotNull SlingHttpServletResponse response, @NotNul } } + + /** + * Deliver an existing rendition as-is to the requester. Creates a presigned URL if possible, streaming the content + * through the JVM should be last resort. + * @param response response + * @param rendition the rendition to stream + * @param contentType the mimetype of the image + * @param imageName the name of the image + * @throws IOException + */ + private void deliverRendition(@NotNull SlingHttpServletResponse response, @NotNull Rendition rendition, @NotNull String contentType, + String imageName) throws IOException { + Binary originalBinary = rendition.getBinary(); + final boolean downloadable = (originalBinary instanceof BinaryDownload); + if (this.deliverExistingRenditionsViaRedirect && downloadable) { + BinaryDownload binaryDownload = (BinaryDownload) originalBinary; + BinaryDownloadOptions downloadOptions = BinaryDownloadOptions.builder() + .withMediaType(contentType) + .withFileName(imageName) + .withDispositionTypeAttachment() + .build(); + URI uri = null; + + try { + uri = binaryDownload.getURI(downloadOptions); + } catch (RepositoryException e) { + LOGGER.error("error getting binary download URI for asset: " + rendition.getPath(), e); + } + + if (uri != null) { + response.sendRedirect(uri.toString()); + return; + } + } + // fallback + try (InputStream is = rendition.getStream()) { + if (is != null) { + stream(response, is, contentType, imageName); + } + } + } + + /** + * Deliver a file resource (typically backed by an nt:file node in JCR) + * @param response the response + * @param fileResource the file resource + * @param fileName the name of the file (used in the redirect) + * @param mimetype the actual mimetype + * @throws IOException + * @throws RepositoryException + */ + private void deliverFile(@NotNull SlingHttpServletResponse response, @NotNull Resource fileResource, + String mimetype, String fileName) throws IOException, RepositoryException { + Node n = fileResource.adaptTo(Node.class); + if (n != null && n.getProperty("jcr:content/jcr:data") != null) { + Binary originalBinary = n.getProperty("jcr:content/jcr:data").getBinary(); + final boolean downloadable = (originalBinary instanceof BinaryDownload); + if (this.deliverExistingRenditionsViaRedirect && downloadable) { + BinaryDownload binaryDownload = (BinaryDownload) originalBinary; + BinaryDownloadOptions downloadOptions = BinaryDownloadOptions.builder() + .withMediaType(mimetype) + .withFileName(fileName) + .withDispositionTypeAttachment() + .build(); + URI uri = null; + + try { + uri = binaryDownload.getURI(downloadOptions); + } catch (RepositoryException e) { + LOGGER.error("error getting binary download URI for asset: " + fileResource.getPath(), e); + } + if (uri != null) { + response.sendRedirect(uri.toString()); + return; + } + } + } + // fallback + try (InputStream is = fileResource.adaptTo(InputStream.class)) { + if (is != null) { + stream(response, is, mimetype, fileName); + } + } + } + + /** * Stream an image from the given input stream. * diff --git a/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationConsumer.java b/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationConsumer.java index f5503bc874..adc9460c08 100644 --- a/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationConsumer.java +++ b/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationConsumer.java @@ -171,7 +171,8 @@ private void updateServletRegistrations() { assetStore, metrics, oldAISDefaultResizeWidth > 0 ? oldAISDefaultResizeWidth : config.getDefaultResizeWidth(), - config.getMaxSize()), + config.getMaxSize(), + config.getDeliverExistingRenditionsViaRedirect()), properties ) ); diff --git a/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationFactory.java b/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationFactory.java index d1b30eb997..02c2566c13 100644 --- a/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationFactory.java +++ b/bundles/core/src/main/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationFactory.java @@ -80,6 +80,13 @@ public class AdaptiveImageServletMappingConfigurationFactory { "it and will throw an exception, to avoid running out of memory." ) int maxSize() default AdaptiveImageServlet.DEFAULT_MAX_SIZE; + + + @AttributeDefinition( + name="Use Redirects", + description="If enabled, when an existing rendition can be used, a redirect to download URI of the blob is " + + "sent; otherwise the binary will be streamed") + boolean deliverExistingRenditionsViaRedirect() default false; } @@ -92,6 +99,8 @@ public class AdaptiveImageServletMappingConfigurationFactory { private int defaultResizeWidth; private int maxSize; + + private boolean deliverExistingRenditionsViaRedirect; /** * Invoked when a configuration is created or modified. @@ -106,6 +115,7 @@ void configure(Config config) { extensions = getValues(config.extensions()); defaultResizeWidth = config.defaultResizeWidth(); maxSize = config.maxSize(); + deliverExistingRenditionsViaRedirect = config.deliverExistingRenditionsViaRedirect(); } /** @@ -154,6 +164,15 @@ public int getDefaultResizeWidth() { public int getMaxSize() { return maxSize; } + + /** + * Indicates if a redirect to existing binary blobs will sent (if possible) or if the result will always be streamed + * @return + */ + public boolean getDeliverExistingRenditionsViaRedirect() { + return deliverExistingRenditionsViaRedirect; + } + /** * Internal helper for filtering out null and empty values from the configuration options. @@ -175,7 +194,11 @@ private List getValues(@NotNull String[] config) { @Override public String toString() { - return "{resourceTypes: " + resourceTypes.toString() + ", selectors: " + selectors.toString() + ", extensions: " + extensions - .toString() + ", defaultResizeWidth: " + defaultResizeWidth + "}"; + return String.format("{resourceTypes: %s, selectors: %s, extensions: %s, defaultResizeWidth: %s, deliverExistingRenditionsViaRedirect: %s}", + resourceTypes.toString(), + selectors.toString(), + extensions.toString(), + defaultResizeWidth, + deliverExistingRenditionsViaRedirect); } } diff --git a/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationFactoryTest.java b/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationFactoryTest.java index 469f4b9076..5f34af115c 100644 --- a/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationFactoryTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletMappingConfigurationFactoryTest.java @@ -58,11 +58,16 @@ public int defaultResizeWidth() { public int maxSize() { return AdaptiveImageServlet.DEFAULT_MAX_SIZE; } + + @Override + public boolean deliverExistingRenditionsViaRedirect() { + return false; + } }); testValues(new String[] {"core/image"}, configurationFactory.getResourceTypes()); testValues(new String[] {"coreimg"}, configurationFactory.getSelectors()); testValues(new String[] {"jpg", "gif", "png"}, configurationFactory.getExtensions()); - assertEquals("{resourceTypes: [core/image], selectors: [coreimg], extensions: [jpg, gif, png], defaultResizeWidth: 1280}", + assertEquals("{resourceTypes: [core/image], selectors: [coreimg], extensions: [jpg, gif, png], defaultResizeWidth: 1280, deliverExistingRenditionsViaRedirect: false}", configurationFactory.toString()); } diff --git a/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletTest.java b/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletTest.java index 2a4c75b8d8..245795572e 100644 --- a/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/wcm/core/components/internal/servlets/AdaptiveImageServletTest.java @@ -21,6 +21,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; @@ -31,6 +33,7 @@ import javax.imageio.ImageIO; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; +import javax.jcr.RepositoryException; import javax.servlet.http.HttpServletResponse; import ch.qos.logback.classic.Level; @@ -39,6 +42,8 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.http.HttpStatus; +import org.apache.jackrabbit.api.binary.BinaryDownload; +import org.apache.jackrabbit.api.binary.BinaryDownloadOptions; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ValueMap; @@ -49,10 +54,13 @@ import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse; import org.apache.sling.testing.resourceresolver.MockValueMap; +import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.ExtendWith; import com.adobe.cq.wcm.core.components.internal.models.v1.AbstractImageTest; @@ -71,6 +79,8 @@ import io.wcm.testing.mock.aem.junit5.AemContextExtension; import org.slf4j.LoggerFactory; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; @@ -81,6 +91,9 @@ @ExtendWith(AemContextExtension.class) class AdaptiveImageServletTest extends AbstractImageTest { + + private static final String DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT = "deliverExistingRenditionsViaRedirect"; + private static final String TEST_BASE = "/image"; private AdaptiveImageServlet servlet; @@ -93,8 +106,11 @@ class AdaptiveImageServletTest extends AbstractImageTest { protected static final String PNG_SMALL_ASSET_PATH = "/content/dam/core/images/" + PNG_IMAGE_SMALL_BINARY_NAME; private static final String FEATURED_IMAGE_PATH = "/content/test-featured-image/jcr:content/cq:featuredimage/1490005239000/" + PNG_IMAGE_BINARY_NAME; - @BeforeEach - void setUp() throws Exception { + private static final String BLOBSTORE_BASE = "https://blostore.local/blostore/"; + + + @BeforeEach() + void setUp(TestInfo t) throws Exception { internalSetUp(TEST_BASE); context.load().binaryFile("/image/" + PNG_IMAGE_RECTANGLE_BINARY_NAME, PNG_RECTANGLE_ASSET_PATH + "/jcr:content/renditions/original"); context.load().binaryFile("/image/" + "cq5dam.web.1280.1280_" + PNG_IMAGE_BINARY_NAME, PNG_RECTANGLE_ASSET_PATH + @@ -123,7 +139,14 @@ void setUp() throws Exception { Rendition rendition = invocation.getArgument(0); return ImageIO.read(rendition.getStream()); }); - servlet = new AdaptiveImageServlet(mockedMimeTypeService, assetStore, metrics, ADAPTIVE_IMAGE_SERVLET_DEFAULT_RESIZE_WIDTH, AdaptiveImageServlet.DEFAULT_MAX_SIZE); + + boolean deliverExistingRenditionsViaRedirect = false; + if (t.getTags().contains(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT)) { + deliverExistingRenditionsViaRedirect = true; + } + + servlet = new AdaptiveImageServlet(mockedMimeTypeService, assetStore, metrics, + ADAPTIVE_IMAGE_SERVLET_DEFAULT_RESIZE_WIDTH, AdaptiveImageServlet.DEFAULT_MAX_SIZE, deliverExistingRenditionsViaRedirect); } @AfterEach @@ -205,6 +228,20 @@ void testRequestWithWidthDesignAllowedRectangleLarge() throws Exception { Assertions.assertEquals(expectedDimension, actualDimension, "Expected image rendered at requested size."); Assertions.assertEquals("image/png", response.getContentType(), "Expected a PNG image."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testRequestWithWidthDesignAllowedRectangleLarge_viaRedirect() throws Exception { + Pair requestResponsePair = prepareRequestResponsePair(IMAGE0_RECTANGLE_PATH, + "img.82.3000", "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + context.contentPolicyMapping(ImageImpl.RESOURCE_TYPE, + "allowedRenditionWidths", new String[] {"500", "1000", "1500", "3000"}, + "jpegQuality", 82); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark_rectangle.png/jcr:content/renditions/original"); + } @Test void testRequestWithWidthDesignAllowedSmallImageWithLargeRenditionSmall() throws Exception { @@ -262,6 +299,20 @@ void testRequestWithWidthDesignAllowedSmallImageWithLargeRenditionLarge() throws Assertions.assertEquals(expectedDimension, actualDimension, "Expected image rendered at requested size."); Assertions.assertEquals("image/png", response.getContentType(), "Expected a PNG image."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testRequestWithWidthDesignAllowedSmallImageWithLargeRenditionLarge_viaRedirect() throws Exception { + Pair requestResponsePair = prepareRequestResponsePair(IMAGE0_SMALL_PATH, + "img.82.1500", "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + context.contentPolicyMapping(ImageImpl.RESOURCE_TYPE, + "allowedRenditionWidths", new String[] {"100", "500", "1500"}, + "jpegQuality", 82); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark_small.png/jcr:content/renditions/original"); + } @Test void testRequestWithWidthDesignNotAllowed() throws Exception { @@ -300,6 +351,17 @@ void testRequestNoWidthWithDesign() throws Exception { Dimension actualDimension = new Dimension(image.getWidth(), image.getHeight()); Assertions.assertEquals(expectedDimension, actualDimension, "Expected image rendered with the default resize configuration width."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testRequestNoWidthWithDesign_viaRedirect() throws Exception { + Pair requestResponsePair = + prepareRequestResponsePair(IMAGE0_PATH, "img", "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = spy(requestResponsePair.getRight()); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark.png/jcr:content/renditions/cq5dam.web.1280.1280.png"); + } @Test @@ -317,6 +379,17 @@ void testRequestNoWidthNoDesign() throws Exception { Dimension actualDimension = new Dimension(image.getWidth(), image.getHeight()); Assertions.assertEquals(expectedDimension, actualDimension, "Expected image rendered with the default resize configuration width."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testRequestNoWidthNoDesign_viaRedirect() throws Exception { + Pair requestResponsePair = + prepareRequestResponsePair(IMAGE0_PATH, "img", "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = spy(requestResponsePair.getRight()); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark.png/jcr:content/renditions/cq5dam.web.1280.1280.png"); + } @Test void testWrongNumberOfSelectors() throws Exception { @@ -391,6 +464,20 @@ void testGIFFileDirectStream() throws Exception { this.getClass().getClassLoader().getResourceAsStream("image/Adobe_Systems_logo_and_wordmark.gif"); Assertions.assertTrue(IOUtils.contentEquals(stream, directStream)); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testGIFFileDirectStream_viaRedirect() throws Exception { + Pair requestResponsePair = + prepareRequestResponsePair(IMAGE5_PATH, 1489998822138L, "img", "gif"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + servlet.doGet(request, response); + ByteArrayInputStream stream = new ByteArrayInputStream(response.getOutput()); + InputStream directStream = + this.getClass().getClassLoader().getResourceAsStream("image/Adobe_Systems_logo_and_wordmark.gif"); + Assertions.assertTrue(IOUtils.contentEquals(stream, directStream)); + } @Test void testSVGFileDirectStream() throws Exception { @@ -405,6 +492,21 @@ void testSVGFileDirectStream() throws Exception { this.getClass().getClassLoader().getResourceAsStream("image/Adobe_Systems_logo_and_wordmark.svg"); Assertions.assertTrue(IOUtils.contentEquals(stream, directStream)); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testSVGFileDirectStream_viaRedirect() throws Exception { + Pair requestResponsePair = + prepareRequestResponsePair(IMAGE24_PATH, 1489998822138L, "img", "svg"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + servlet.doGet(request, response); + Assertions.assertTrue(response.getHeader(HttpHeaders.CONTENT_DISPOSITION).startsWith("attachment")); + ByteArrayInputStream stream = new ByteArrayInputStream(response.getOutput()); + InputStream directStream = + this.getClass().getClassLoader().getResourceAsStream("image/Adobe_Systems_logo_and_wordmark.svg"); + Assertions.assertTrue(IOUtils.contentEquals(stream, directStream)); + } @Test void testGIFFileBrowserCached() throws Exception { @@ -431,6 +533,17 @@ void testGIFUploadedToDAM() throws Exception { this.getClass().getClassLoader().getResourceAsStream("image/Adobe_Systems_logo_and_wordmark.gif"); Assertions.assertTrue(IOUtils.contentEquals(stream, directStream)); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testGIFUploadedToDAM_viaRedirect() throws Exception { + Pair requestResponsePair = + prepareRequestResponsePair(IMAGE6_PATH, "img", "gif"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark.gif/jcr:content/renditions/original"); + } @Test void testSVGUploadedToDAM() throws Exception { @@ -445,6 +558,17 @@ void testSVGUploadedToDAM() throws Exception { this.getClass().getClassLoader().getResourceAsStream("image/Adobe_Systems_logo_and_wordmark.svg"); Assertions.assertTrue(IOUtils.contentEquals(stream, directStream)); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testSVGUploadedToDAM_viaRedirect() throws Exception { + Pair requestResponsePair = + prepareRequestResponsePair(IMAGE25_PATH, "img", "svg"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark.svg/jcr:content/renditions/original"); + } @Test void testGIFUploadedToDAMBrowserCached() throws Exception { @@ -471,6 +595,18 @@ void testCorrectScalingPNGAssetWidth() throws Exception { Assertions.assertTrue(IOUtils.contentEquals(directStream, stream), "Expected to get the original asset back, since the requested width is equal to the image's width."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testCorrectScalingPNGAssetWidth_viaRedirect() throws Exception { + Pair requestResponsePair = + prepareRequestResponsePair(IMAGE0_PATH, "img.2000", "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + context.contentPolicyMapping(ImageImpl.RESOURCE_TYPE, "allowedRenditionWidths", new String[] {"2000"}); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark.png/jcr:content/renditions/original"); + } @Test void testDAMFileUpscaledPNG() throws Exception { @@ -485,6 +621,18 @@ void testDAMFileUpscaledPNG() throws Exception { Assertions.assertTrue(IOUtils.contentEquals(directStream, stream), "Expected to get the original asset back, since the requested width would result in upscaling the image."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testDAMFileUpscaledPNG_viaRedirect() throws Exception { + Pair requestResponsePair = + prepareRequestResponsePair(IMAGE0_PATH, "img.2500", "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + context.contentPolicyMapping(ImageImpl.RESOURCE_TYPE, "allowedRenditionWidths", new String[] {"2500"}); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark.png/jcr:content/renditions/original"); + } @Test void testPNGFileDirectStream() throws Exception { @@ -525,6 +673,20 @@ void testWithNoImageNameFromTemplate() throws IOException { servlet.doGet(request, response); Assertions.assertEquals(200, response.getStatus(), "Expected a 200 response code."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testWithNoImageNameFromTemplate_viaRedirect() throws IOException { + Pair requestResponsePair = prepareRequestResponsePair( + PAGE, "coreimg", "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) request.getRequestPathInfo(); + + requestPathInfo.setSuffix(TEMPLATE_IMAGE_PATH.replace(TEMPLATE_PATH, "") + "/1490005239000.png"); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark.png/jcr:content/renditions/cq5dam.web.1280.1280.png"); + } @Test void testImageFileWithNegativeRequestedWidth() throws Exception { @@ -641,6 +803,17 @@ void testFileReferencePriority() throws Exception { Assertions.assertEquals(expectedDimension, actualDimension, "Expected image rendered at requested size."); Assertions.assertEquals("image/png", response.getContentType(), "Expected a PNG image."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testFileReferencePriority_viaRedirect() throws Exception { + Pair requestResponsePair = prepareRequestResponsePair(IMAGE20_PATH, + "img", "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark.png/jcr:content/renditions/cq5dam.web.1280.1280.png"); + } @Test void testFileReferenceNonexistingAsset() throws Exception { @@ -732,6 +905,20 @@ void testImageFromTemplateStructureNode() throws IOException { Assertions.assertEquals(expectedDimension, actualDimension, "Expected image rendered at requested size."); Assertions.assertEquals("image/png", response.getContentType(), "Expected a PNG image."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testImageFromTemplateStructureNode_viaRedirect() throws IOException { + Pair requestResponsePair = prepareRequestResponsePair(PAGE, "img", + "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) request.getRequestPathInfo(); + requestPathInfo.setSuffix(TEMPLATE_IMAGE_PATH.replace(TEMPLATE_PATH, "") + "/1490005239000.png"); + servlet.doGet(request, response); + // deliver a redirect to the 1280x1280 renditions + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark.png/jcr:content/renditions/cq5dam.web.1280.1280.png"); + } @Test void testImageFromFeaturedImage() throws IOException { @@ -750,6 +937,19 @@ void testImageFromFeaturedImage() throws IOException { Assertions.assertEquals(expectedDimension, actualDimension, "Expected image rendered at requested size."); Assertions.assertEquals("image/png", response.getContentType(), "Expected a PNG image."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testImageFromFeaturedImage_viaRedirect() throws IOException { + Pair requestResponsePair = prepareRequestResponsePair(LIST01_PATH, "img", + "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) request.getRequestPathInfo(); + requestPathInfo.setSuffix(FEATURED_IMAGE_PATH); + servlet.doGet(request, response); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/Adobe_Systems_logo_and_wordmark.png/jcr:content/renditions/cq5dam.web.1280.1280.png"); + } @Test void testHorizontalAndVerticalFlipWithDAMAsset() throws IOException { @@ -983,6 +1183,28 @@ void testTransformAndStreamAssetForTiffRenderedAsJpegWithoutJpegRenditionsNoResi Dimension actualDimension = new Dimension(image.getWidth(), image.getHeight()); Assertions.assertEquals(expectedDimension, actualDimension, "Expected image rendered at requested size."); } + + @Test + @Tag(DELIVER_EXISTING_RENDITIONS_VIA_REDIRECT) + void testTransformAndStreamAssetForTiffRenderedAsJpegWithoutJpegRenditionsNoResize_viaRedirect() throws IOException { + Pair requestResponsePair = + prepareRequestResponsePair(IMAGE0_PATH, "img.2000", "png"); + MockSlingHttpServletRequest request = requestResponsePair.getLeft(); + MockSlingHttpServletResponse response = requestResponsePair.getRight(); + + Asset mockAsset = mock(Asset.class); + when(mockAsset.getPath()).thenReturn("/content/dam/core/images/funky.tiff"); + when(mockAsset.getMimeType()).thenReturn("image/tiff"); + when(mockAsset.getRenditions()).thenReturn(new LinkedList<>()); + when(mockAsset.getMetadataValue(DamConstants.TIFF_IMAGEWIDTH)).thenReturn("2000"); + when(mockAsset.getMetadataValue(DamConstants.TIFF_IMAGELENGTH)).thenReturn("2000"); + Rendition original = mockRendition(mockAsset, "original", 9999999, "image/tiff", 2000, 2000); + when(original.getStream()).thenReturn(this.getClass().getClassLoader().getResourceAsStream("image/Adobe_Systems_logo_and_wordmark.tiff")); + when(mockAsset.getOriginal()).thenReturn(original); + + servlet.transformAndStreamAsset(response, new MockValueMap(request.getResource(), new HashMap<>()), 0, 90, mockAsset, "image/jpeg", "test"); + assertValidRedirectToBlobStore(response, "/content/dam/core/images/funky.tiff/jcr:content/renditions/original"); + } @Test void testTransformAndStreamAssetForTiffRenderedAsJpegWithoutJpegRenditionsAndUnableToProcess() throws IOException { @@ -1070,6 +1292,41 @@ private Rendition mockRendition(Asset asset, String name, long size, String mime when(rendition.getProperties()).thenReturn(mockProperties); when(rendition.getSize()).thenReturn(size); when(rendition.getAsset()).thenReturn(asset); + final String path = asset.getPath() + "/jcr:content/renditions/" + name; + when(rendition.getPath()).thenReturn(path); + when(rendition.getBinary()).thenReturn(new BinaryDownload() { + + @Override + public int read(byte[] b, long position) throws IOException, RepositoryException { + return 0; + } + + @Override + public InputStream getStream() throws RepositoryException { + // TODO Auto-generated method stub + return null; + } + + @Override + public long getSize() throws RepositoryException { + return 0; + } + + @Override + public void dispose() { + // do nothing + } + + @Override + public @Nullable URI getURI(BinaryDownloadOptions downloadOptions) throws RepositoryException { + try { + return new URI (BLOBSTORE_BASE + rendition.getPath()); + } catch (URISyntaxException e) { + // ignore + } + return null; + } + }); return rendition; } @@ -1174,5 +1431,13 @@ public MockSlingHttpServletResponse setValue(MockSlingHttpServletResponse value) throw new UnsupportedOperationException(); } } + + private void assertValidRedirectToBlobStore(MockSlingHttpServletResponse response, String expectedLocation ) { + assertEquals(302, response.getStatus()); + final String locationHeader = response.getHeader("Location"); + assertNotNull(locationHeader); + assertEquals(BLOBSTORE_BASE + expectedLocation, locationHeader); + } + } diff --git a/parent/pom.xml b/parent/pom.xml index f365950558..d76fa2cc78 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -796,7 +796,7 @@ io.wcm io.wcm.testing.aem-mock.junit5 - 5.5.2 + 5.6.2 test