DynamicMediaSupportServiceImpl.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2021 wcm.io
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package io.wcm.handler.mediasource.dam.impl.dynamicmedia;
import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
import java.util.Map;
import java.util.regex.Pattern;
import javax.jcr.RepositoryException;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.adapter.Adaptable;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.DamConstants;
import com.day.cq.dam.api.s7dam.utils.PublishUtils;
import io.wcm.handler.media.Dimension;
import io.wcm.handler.url.SiteConfig;
import io.wcm.handler.url.UrlHandler;
import io.wcm.handler.url.UrlMode;
import io.wcm.handler.url.UrlModes;
import io.wcm.sling.commons.adapter.AdaptTo;
/**
* Implements {@link DynamicMediaSupportService}.
*/
@Component(service = DynamicMediaSupportService.class, immediate = true)
@Designate(ocd = DynamicMediaSupportServiceImpl.Config.class)
public class DynamicMediaSupportServiceImpl implements DynamicMediaSupportService {
@ObjectClassDefinition(
name = "wcm.io Media Handler Dynamic Media Support",
description = "Configures dynamic media support in media handling.")
@interface Config {
@AttributeDefinition(
name = "Enabled",
description = "Enable support for dynamic media. "
+ "Only gets active when dynamic media is actually enabled for the instance.")
boolean enabled() default true;
@AttributeDefinition(
name = "Dynamic Media Capability",
description = "Whether to detect automatically if Dynamic Media is actually for a given asset by looking for existing DM metadata. "
+ "Setting to ON disables the auto-detection and forces it to enabled for all asssets, setting to OFF forced it to disabled.")
DynamicMediaCapabilityDetection dmCapabilityDetection() default DynamicMediaCapabilityDetection.AUTO;
@AttributeDefinition(
name = "Author Preview Mode",
description = "Loads dynamic media images via author instance - to allow previewing unpublished images. "
+ "Must not be enabled on publish instances.")
boolean authorPreviewMode() default false;
@AttributeDefinition(
name = "Disable AEM Fallback",
description = "Disable the automatic fallback to AEM-based rendering of renditions (via Media Handler) "
+ "if Dynamic Media is enabled, but the asset has not the appropriate Dynamic Media metadata.")
boolean disableAemFallback() default false;
@AttributeDefinition(
name = "Validate Smart Crop Rendition Sizes",
description = "Validates that the renditions defined via smart cropping fulfill the requested image width/height to avoid upscaling or white borders.")
boolean validateSmartCropRenditionSizes() default true;
@AttributeDefinition(
name = "Image width limit",
description = "The configured width value for 'Reply Image Size Limit'.")
long imageSizeLimitWidth() default 2000;
@AttributeDefinition(
name = "Image height limit",
description = "The configured height value for 'Reply Image Size Limit'.")
long imageSizeLimitHeight() default 2000;
@AttributeDefinition(
name = "Set Image Quality",
description = "Control image quality for lossy output formats for each media request via 'qlt' URL parameter (instead of relying on default setting within Dynamic Media).")
boolean setImageQuality() default true;
}
@Reference
private PublishUtils dynamicMediaPublishUtils;
@Reference
private ResourceResolverFactory resourceResolverFactory;
private boolean enabled;
private DynamicMediaCapabilityDetection dmCapabilityDetection;
private boolean authorPreviewMode;
private boolean disableAemFallback;
private boolean validateSmartCropRenditionSizes;
private Dimension imageSizeLimit;
private boolean setImageQuality;
private static final String SERVICEUSER_SUBSERVICE = "dynamic-media-support";
private static final Pattern DAM_PATH_PATTERN = Pattern.compile("^/content/dam(/.*)?$");
private static final Logger log = LoggerFactory.getLogger(DynamicMediaSupportServiceImpl.class);
@Activate
private void activate(Config config) {
this.enabled = config.enabled();
this.dmCapabilityDetection = config.dmCapabilityDetection();
this.authorPreviewMode = config.authorPreviewMode();
this.disableAemFallback = config.disableAemFallback();
this.validateSmartCropRenditionSizes = config.validateSmartCropRenditionSizes();
this.imageSizeLimit = new Dimension(config.imageSizeLimitWidth(), config.imageSizeLimitHeight());
this.setImageQuality = config.setImageQuality();
if (this.enabled) {
log.info("DynamicMediaSupport: enabled={}, capabilityEnabled={}, capabilityDetection={}, "
+ "authorPreviewMode={}, disableAemFallback={}, imageSizeLimit={}",
this.enabled, this.dmCapabilityDetection, this.dmCapabilityDetection,
this.authorPreviewMode, this.disableAemFallback, this.imageSizeLimit);
}
}
@Override
public boolean isDynamicMediaEnabled() {
return this.enabled;
}
@Override
public boolean isDynamicMediaCapabilityEnabled(boolean isDynamicMediaAsset) {
switch (dmCapabilityDetection) {
case AUTO:
return isDynamicMediaAsset;
case ON:
return true;
case OFF:
default:
return false;
}
}
@Override
public boolean isAemFallbackDisabled() {
return disableAemFallback;
}
@Override
public boolean isValidateSmartCropRenditionSizes() {
return validateSmartCropRenditionSizes;
}
@Override
public @NotNull Dimension getImageSizeLimit() {
return this.imageSizeLimit;
}
@Override
public boolean isSetImageQuality() {
return setImageQuality;
}
@Override
public @Nullable ImageProfile getImageProfile(@NotNull String profilePath) {
try (ResourceResolver resourceResolver = resourceResolverFactory
.getServiceResourceResolver(Map.of(ResourceResolverFactory.SUBSERVICE, SERVICEUSER_SUBSERVICE))) {
Resource profileResource = resourceResolver.getResource(profilePath);
if (profileResource != null) {
log.debug("Loaded image profile: {}", profilePath);
return new ImageProfileImpl(profileResource);
}
}
catch (LoginException ex) {
log.error("Missing service user mapping for 'io.wcm.handler.media:dynamic-media-support' - see https://wcm.io/handler/media/configuration.html", ex);
}
log.debug("Image profile not found: {}", profilePath);
return null;
}
@Override
public @Nullable ImageProfile getImageProfileForAsset(@NotNull Asset asset) {
Resource assetResource = AdaptTo.notNull(asset, Resource.class);
Resource folderResource = assetResource.getParent();
if (folderResource != null) {
return getImageProfileForAssetFolder(folderResource);
}
return null;
}
private @Nullable ImageProfile getImageProfileForAssetFolder(@NotNull Resource folderResource) {
if (!DAM_PATH_PATTERN.matcher(folderResource.getPath()).matches()) {
return null;
}
Resource folderContentResource = folderResource.getChild(JCR_CONTENT);
if (folderContentResource != null) {
String imageProfilePath = folderContentResource.getValueMap().get(DamConstants.IMAGE_PROFILE, String.class);
if (imageProfilePath != null) {
return getImageProfile(imageProfilePath);
}
}
Resource parentFolderResource = folderResource.getParent();
if (parentFolderResource != null) {
return getImageProfileForAssetFolder(parentFolderResource);
}
else {
return null;
}
}
@Override
public @Nullable String getDynamicMediaServerUrl(@NotNull Asset asset, @Nullable UrlMode urlMode, @NotNull Adaptable adaptable) {
Resource assetResource = AdaptTo.notNull(asset, Resource.class);
if (authorPreviewMode && !forcePublishMode(urlMode)) {
// route dynamic media requests through author instance for preview
// return configured author URL, or empty string if none configured
SiteConfig siteConfig = AdaptTo.notNull(adaptable, SiteConfig.class);
String siteUrlAUthor = StringUtils.defaultString(siteConfig.siteUrlAuthor());
UrlHandler urlHandler = AdaptTo.notNull(adaptable, UrlHandler.class);
return urlHandler.applySiteUrlAutoDetection(siteUrlAUthor);
}
try {
String[] productionAssetUrls = dynamicMediaPublishUtils.externalizeImageDeliveryAsset(assetResource);
if (productionAssetUrls != null && productionAssetUrls.length > 0) {
return productionAssetUrls[0];
}
}
catch (RepositoryException ex) {
log.warn("Unable to get dynamic media production asset URLs for {}", assetResource.getPath(), ex);
}
log.warn("Unable to get dynamic media production asset URLs for {}", assetResource.getPath());
return null;
}
/**
* If URL mode is target for publish instance, use dynamic media production URL.
* @param urlMode URL mode
* @return true if publish mode should be forced
*/
private boolean forcePublishMode(@Nullable UrlMode urlMode) {
return urlMode != null && (urlMode.equals(UrlModes.FULL_URL_PUBLISH)
|| urlMode.equals(UrlModes.FULL_URL_PUBLISH_FORCENONSECURE)
|| urlMode.equals(UrlModes.FULL_URL_PUBLISH_FORCESECURE)
|| urlMode.equals(UrlModes.FULL_URL_PUBLISH_PROTOCOLRELATIVE));
}
}