Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2853 The AdaptiveImageServlet should send redirects to blobs #2871

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,13 +31,20 @@
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;
import org.apache.commons.io.IOUtils;
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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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",
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ private void updateServletRegistrations() {
assetStore,
metrics,
oldAISDefaultResizeWidth > 0 ? oldAISDefaultResizeWidth : config.getDefaultResizeWidth(),
config.getMaxSize()),
config.getMaxSize(),
config.getDeliverExistingRenditionsViaRedirect()),
properties
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}

Expand All @@ -92,6 +99,8 @@ public class AdaptiveImageServletMappingConfigurationFactory {
private int defaultResizeWidth;

private int maxSize;

private boolean deliverExistingRenditionsViaRedirect;

/**
* Invoked when a configuration is created or modified.
Expand All @@ -106,6 +115,7 @@ void configure(Config config) {
extensions = getValues(config.extensions());
defaultResizeWidth = config.defaultResizeWidth();
maxSize = config.maxSize();
deliverExistingRenditionsViaRedirect = config.deliverExistingRenditionsViaRedirect();
}

/**
Expand Down Expand Up @@ -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.
Expand All @@ -175,7 +194,11 @@ private List<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
Loading
Loading