diff --git a/changes.xml b/changes.xml index a7505f03..c7063b8d 100644 --- a/changes.xml +++ b/changes.xml @@ -23,7 +23,10 @@ xsi:schemaLocation="http://maven.apache.org/changes/1.0.0 http://maven.apache.org/plugins/maven-changes-plugin/xsd/changes-1.0.0.xsd"> - + + + Dynamic Media with OpenAPI: Support Dynamic Media with OpenAPI also for local assets. + Improve trace logging: Make log messages involving value maps and resource/page objects more compact and better readable. @@ -50,16 +53,16 @@ - Next Generation Dynamic Media: Support non-image assets and SVG assets. + Dynamic Media with OpenAPI: Support non-image assets and SVG assets. - Next Generation Dynamic Media: Use latest NextGen Dynamic Media Asset URLs and make them configurable via OSGi config. + Dynamic Media with OpenAPI: Use latest NextGen Dynamic Media Asset URLs and make them configurable via OSGi config. - Next Generation Dynamic Media: Replace fileupload default pick/remote trigger with customized one to display the customized asset selector dialog. + Dynamic Media with OpenAPI: Replace fileupload default pick/remote trigger with customized one to display the customized asset selector dialog. - Next Generation Dynamic Media: Optionally fetch metadata of NGDM asset reference to check for validity and maximum possible resolution. + Dynamic Media with OpenAPI: Optionally fetch metadata of NGDM asset reference to check for validity and maximum possible resolution. @@ -68,11 +71,11 @@ Version 2.0.0 contains minor breaking API changes, see Migrate from wcm.io Handler 1.x to 2.x for details. ]]> + Add support for Web-Optimized Image Delivery (part of Dynamic Media with OpenAPI) - rendering asset renditions from AEM Sites instance on the edge.
This feature is active by default on AEMaaCS cloud instances, can be disabled via OSGi configuration. ]]>
- Add support for Next Generation Dynamic Media remote assets. This is a first experimental support and will be finalized in release 2.0.2. + Add support for Dynamic Media with OpenAPI (also known as Next Generation Dynamic Media) remote assets. This is a first experimental support and will be finalized in release 2.0.2. Allow to set image quality per media request. diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaAsset.java b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaAsset.java index 7283c0f5..05ce9895 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaAsset.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaAsset.java @@ -21,6 +21,7 @@ import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ValueMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -115,17 +116,30 @@ final class NextGenDynamicMediaAsset implements Asset { } @Override - public @Nullable AdapterType adaptTo(@NotNull Class arg0) { - // not adaption supported + @SuppressWarnings("unchecked") + public @Nullable AdapterType adaptTo(@NotNull Class type) { + com.day.cq.dam.api.Asset asset = context.getReference().getAsset(); + if (asset != null) { + if (type == com.day.cq.dam.api.Asset.class) { + return (AdapterType)asset; + } + if (type == Resource.class) { + return (AdapterType)asset.adaptTo(Resource.class); + } + } return null; } @Override public String toString() { - return new ToStringBuilder(this) + ToStringBuilder sb = new ToStringBuilder(this) .append("reference", context.getReference()) - .append("metadata", context.getMetadata()) - .toString(); + .append("metadata", context.getMetadata()); + com.day.cq.dam.api.Asset asset = context.getReference().getAsset(); + if (asset != null) { + sb.append("asset", asset.getPath()); + } + return sb.toString(); } } diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaConfigModel.java b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaConfigModel.java index 86049455..7e2eda2c 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaConfigModel.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaConfigModel.java @@ -26,7 +26,6 @@ import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.models.annotations.Model; -import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy; import org.apache.sling.models.annotations.injectorspecific.OSGiService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -40,7 +39,7 @@ import io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaConfigService; /** - * Prepares Next Generation Dynamic Media configuration for GraniteUI components (fileupload, pathfield). + * Prepares Next Generation Dynamic Media Remote Assets configuration for GraniteUI components (fileupload, pathfield). */ @Model(adaptables = SlingHttpServletRequest.class) @ProviderType @@ -49,7 +48,7 @@ public final class NextGenDynamicMediaConfigModel { private static final JsonMapper MAPPER = JsonMapper.builder().build(); private static final Logger log = LoggerFactory.getLogger(NextGenDynamicMediaConfigModel.class); - @OSGiService(injectionStrategy = InjectionStrategy.OPTIONAL) + @OSGiService private NextGenDynamicMediaConfigService config; private boolean enabled; @@ -58,16 +57,14 @@ public final class NextGenDynamicMediaConfigModel { @PostConstruct private void activate() { - if (config != null) { - enabled = config.enabled(); - assetSelectorsJsUrl = config.getAssetSelectorsJsUrl(); - configJson = buildConfigJsonString(config); - } + enabled = config.isEnabledRemoteAssets(); + assetSelectorsJsUrl = config.getAssetSelectorsJsUrl(); + configJson = buildConfigJsonString(config); } private static String buildConfigJsonString(@NotNull NextGenDynamicMediaConfigService config) { Map map = new TreeMap<>(); - map.put("repositoryId", config.getRepositoryId()); + map.put("repositoryId", config.getRemoteAssetsRepositoryId()); map.put("apiKey", config.getApiKey()); map.put("env", config.getEnv()); try { diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaMediaSource.java b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaMediaSource.java index cb6f1491..7cc8cead 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaMediaSource.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaMediaSource.java @@ -23,17 +23,18 @@ import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.adapter.Adaptable; import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.commons.mime.MimeTypeService; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy; import org.apache.sling.models.annotations.injectorspecific.OSGiService; import org.apache.sling.models.annotations.injectorspecific.Self; +import org.apache.sling.models.annotations.injectorspecific.SlingObject; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.osgi.annotation.versioning.ProviderType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.day.cq.dam.api.Asset; import com.day.cq.wcm.api.WCMMode; import com.day.cq.wcm.api.components.ComponentContext; import com.day.cq.wcm.api.components.EditConfig; @@ -71,6 +72,8 @@ public final class NextGenDynamicMediaMediaSource extends MediaSource { private Adaptable adaptable; @Self private MediaHandlerConfig mediaHandlerConfig; + @SlingObject + private ResourceResolver resourceResolver; @OSGiService(injectionStrategy = InjectionStrategy.OPTIONAL) private NextGenDynamicMediaConfigService nextGenDynamicMediaConfig; @OSGiService(injectionStrategy = InjectionStrategy.OPTIONAL) @@ -83,8 +86,6 @@ public final class NextGenDynamicMediaMediaSource extends MediaSource { @AemObject(injectionStrategy = InjectionStrategy.OPTIONAL) private ComponentContext componentContext; - private static final Logger log = LoggerFactory.getLogger(NextGenDynamicMediaMediaSource.class); - @Override public @NotNull String getId() { return ID; @@ -92,15 +93,15 @@ public final class NextGenDynamicMediaMediaSource extends MediaSource { @Override public boolean accepts(@Nullable String mediaRef) { - return isNextGenDynamicMediaEnabled() && NextGenDynamicMediaReference.isReference(mediaRef); - } - - private boolean isNextGenDynamicMediaEnabled() { if (nextGenDynamicMediaConfig == null) { - log.debug("NGDM media source is disabled: com.adobe.cq.ui.wcm.commons.config.NextGenDynamicMediaConfig is not available."); return false; } - return nextGenDynamicMediaConfig.enabled(); + return (nextGenDynamicMediaConfig.isEnabledRemoteAssets() && NextGenDynamicMediaReference.isReference(mediaRef)) + || (nextGenDynamicMediaConfig.isEnabledLocalAssets() && isDamAssetReference(mediaRef)); + } + + private boolean isDamAssetReference(@Nullable String mediaRef) { + return StringUtils.startsWith(mediaRef, "/content/dam/"); } @Override @@ -114,8 +115,8 @@ private boolean isNextGenDynamicMediaEnabled() { MediaArgs mediaArgs = media.getMediaRequest().getMediaArgs(); // check reference and enabled status - NextGenDynamicMediaReference reference = NextGenDynamicMediaReference.fromReference(mediaRef); - if (reference == null || !isNextGenDynamicMediaEnabled()) { + NextGenDynamicMediaReference reference = toNextGenDynamicMediaReference(mediaRef); + if (reference == null || nextGenDynamicMediaConfig == null) { if (StringUtils.isEmpty(mediaRef)) { media.setMediaInvalidReason(MediaInvalidReason.MEDIA_REFERENCE_MISSING); } @@ -127,7 +128,11 @@ private boolean isNextGenDynamicMediaEnabled() { // If enabled: Fetch asset metadata to validate existence and get original dimensions NextGenDynamicMediaMetadata metadata = null; - if (metadataService != null && metadataService.isEnabled()) { + Asset localAsset = reference.getAsset(); + if (localAsset != null) { + metadata = getMetadataFromAsset(localAsset); + } + else if (metadataService != null && metadataService.isEnabled()) { metadata = metadataService.fetchMetadata(reference); if (metadata == null) { media.setMediaInvalidReason(MediaInvalidReason.MEDIA_REFERENCE_INVALID); @@ -162,6 +167,26 @@ private boolean isNextGenDynamicMediaEnabled() { return media; } + private @Nullable NextGenDynamicMediaReference toNextGenDynamicMediaReference(@Nullable String mediaRef) { + if (nextGenDynamicMediaConfig != null) { + if (nextGenDynamicMediaConfig.isEnabledRemoteAssets() && NextGenDynamicMediaReference.isReference(mediaRef)) { + return NextGenDynamicMediaReference.fromReference(mediaRef); + } + else if (nextGenDynamicMediaConfig.isEnabledLocalAssets() && isDamAssetReference(mediaRef)) { + return NextGenDynamicMediaReference.fromDamAssetReference(mediaRef, resourceResolver); + } + } + return null; + } + + private @Nullable NextGenDynamicMediaMetadata getMetadataFromAsset(@NotNull Asset asset) { + NextGenDynamicMediaMetadata metadata = NextGenDynamicMediaMetadata.fromAsset(asset); + if (metadata.isValid()) { + return metadata; + } + return null; + } + @Override public void enableMediaDrop(@NotNull HtmlElement element, @NotNull MediaRequest mediaRequest) { if (wcmMode == WCMMode.DISABLED || wcmMode == null) { diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaRendition.java b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaRendition.java index 252228f4..37e41478 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaRendition.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaRendition.java @@ -60,6 +60,7 @@ final class NextGenDynamicMediaRendition implements Rendition { private MediaFormat resolvedMediaFormat; private long width; private long height; + private String fileExtension; private static final Logger log = LoggerFactory.getLogger(NextGenDynamicMediaRendition.class); @@ -87,6 +88,11 @@ final class NextGenDynamicMediaRendition implements Rendition { } } + this.fileExtension = mediaArgs.getEnforceOutputFileExtension(); + if (StringUtils.isEmpty(this.fileExtension)) { + this.fileExtension = FilenameUtils.getExtension(reference.getFileName()); + } + if (isVectorImage() || !isImage()) { // deliver as binary this.url = buildBinaryUrl(); @@ -98,6 +104,7 @@ else if (isRequestedDimensionLargerThanOriginal()) { else { // deliver scaled image rendition this.url = buildImageRenditionUrl(); + this.fileExtension = new NextGenDynamicMediaImageUrlBuilder(context).getFileExtension(); } } @@ -169,11 +176,7 @@ private String buildBinaryUrl() { @Override public @Nullable String getFileExtension() { - String extension = mediaArgs.getEnforceOutputFileExtension(); - if (StringUtils.isEmpty(extension)) { - extension = FilenameUtils.getExtension(reference.getFileName()); - } - return extension; + return this.fileExtension; } @Override diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaBinaryUrlBuilder.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaBinaryUrlBuilder.java index 4184ea62..63e09463 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaBinaryUrlBuilder.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaBinaryUrlBuilder.java @@ -51,9 +51,15 @@ public NextGenDynamicMediaBinaryUrlBuilder(@NotNull NextGenDynamicMediaContext c public @Nullable String build() { // get parameters from nextgen dynamic media config for URL parameters - String repositoryId = context.getNextGenDynamicMediaConfig().getRepositoryId(); + String repositoryId; + if (context.getReference().getAsset() != null) { + repositoryId = context.getNextGenDynamicMediaConfig().getLocalAssetsRepositoryId(); + } + else { + repositoryId = context.getNextGenDynamicMediaConfig().getRemoteAssetsRepositoryId(); + } String binaryDeliveryPath = context.getNextGenDynamicMediaConfig().getAssetOriginalBinaryDeliveryPath(); - if (StringUtils.isAnyEmpty(repositoryId, binaryDeliveryPath)) { + if (StringUtils.isAnyBlank(repositoryId, binaryDeliveryPath)) { return null; } diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigService.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigService.java index 5779fe1a..afef842a 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigService.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigService.java @@ -19,6 +19,8 @@ */ package io.wcm.handler.mediasource.ngdm.impl; +import org.jetbrains.annotations.Nullable; + /** * Service to access Next Generation Dynamic Media configuration. */ @@ -40,15 +42,22 @@ public interface NextGenDynamicMediaConfigService { String PLACEHOLDER_FORMAT = "{format}"; /** - * Checks if the configuration/feature is enabled. - * @return true if enabled and false otherwise + * Checks if enabled for remote assets, and the appropriate configuration is present. + * @return true if enabled for remote assets + */ + boolean isEnabledRemoteAssets(); + + /** + * Enable Next Generation Dynamic Media for local assets in this AEMaaCS instance. + * @return true if enabled for local assets. */ - boolean enabled(); + boolean isEnabledLocalAssets(); /** * Gets the absolute URL for the javascript which contains the microfrontend for the remote asset selector. * @return the absolute URL for the javascript which contains the microfrontend for the remote asset selector */ + @Nullable String getAssetSelectorsJsUrl(); /** @@ -64,6 +73,7 @@ public interface NextGenDynamicMediaConfigService { * * @return the path expression for the image delivery path */ + @Nullable String getImageDeliveryBasePath(); /** @@ -78,6 +88,7 @@ public interface NextGenDynamicMediaConfigService { * * @return the path expression for the video delivery path */ + @Nullable String getVideoDeliveryPath(); /** @@ -91,6 +102,7 @@ public interface NextGenDynamicMediaConfigService { * * @return the path expression for the asset (bitstream) delivery path */ + @Nullable String getAssetOriginalBinaryDeliveryPath(); /** @@ -103,30 +115,42 @@ public interface NextGenDynamicMediaConfigService { * * @return the path expression for the metadata path */ + @Nullable String getAssetMetadataPath(); /** - * Gets the Next Generation Dynamic Media tenant (also known technically as the repository ID). + * Gets the Next Generation Dynamic Media tenant (also known technically as the repository ID) for remote assets. + * @return the repository ID + */ + @Nullable + String getRemoteAssetsRepositoryId(); + + /** + * Repository ID for serving local assets. * @return the repository ID */ - String getRepositoryId(); + @Nullable + String getLocalAssetsRepositoryId(); /** * Gets the API key for accessing the asset selectors UI * @return the API key for accessing the asset selectors UI */ + @Nullable String getApiKey(); /** * Gets the environment string which should be 'PROD' or 'STAGE' * @return the environment string */ + @Nullable String getEnv(); /** * Gets the IMS client identifier * @return the IMS client identifier */ + @Nullable String getImsClient(); } diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigServiceImpl.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigServiceImpl.java index c0543597..d5148de6 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigServiceImpl.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigServiceImpl.java @@ -20,9 +20,11 @@ package io.wcm.handler.mediasource.ngdm.impl; import org.apache.commons.lang3.StringUtils; +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.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.metatype.annotations.AttributeDefinition; @@ -41,10 +43,25 @@ public class NextGenDynamicMediaConfigServiceImpl implements NextGenDynamicMediaConfigService { @ObjectClassDefinition( - name = "wcm.io Media Handler Next Generation Dynamic Media Support", + name = "wcm.io Media Handler Dynamic Media with OpenAPI Support", description = "Support for Next Generation Dynamic Media.") @interface Config { + @AttributeDefinition( + name = "Remote Assets", + description = "Enable Dynamic Media with OpenAPI for remote assets.") + boolean enabledRemoteAssets() default true; + + @AttributeDefinition( + name = "Local Assets", + description = "Enable Next Dynamic Media with OpenAPI for local assets in this AEMaaCS instance.") + boolean enabledLocalAssets() default false; + + @AttributeDefinition( + name = "Repository ID for Local Assets", + description = "Dynamic Media with OpenAPI Delivery host name for local assets. Mandatory if local assets is enabled.") + String localAssetsRepositoryId(); + @AttributeDefinition( name = "Image Delivery Base Path", description = "Base path with placeholders to deliver image renditions. " @@ -73,75 +90,104 @@ String assetOriginalBinaryDeliveryPath() default ADOBE_ASSETS_PREFIX + PLACEHOLD private static final String ADOBE_ASSETS_PREFIX = "/adobe/assets/"; private static final Logger log = LoggerFactory.getLogger(NextGenDynamicMediaConfigServiceImpl.class); + private boolean enabledRemoteAssets; + private boolean enabledLocalAssets; + private String localAssetsRepositoryId; private String imageDeliveryBasePath; private String assetOriginalBinaryDeliveryPath; private String assetMetadataPath; - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY) + @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL) private NextGenDynamicMediaConfig nextGenDynamicMediaConfig; @Activate private void activate(Config config) { - log.debug("NGDM config: enabled={}, repositoryId={}, apiKey={}, env={}, imsClient={}", - enabled(), getRepositoryId(), getApiKey(), getEnv(), getImsClient()); - - this.imageDeliveryBasePath = StringUtils.defaultIfBlank(config.imageDeliveryBasePath(), - this.nextGenDynamicMediaConfig.getImageDeliveryBasePath()); - this.assetOriginalBinaryDeliveryPath = StringUtils.defaultIfBlank(config.assetOriginalBinaryDeliveryPath(), - this.nextGenDynamicMediaConfig.getAssetOriginalBinaryDeliveryPath()); - this.assetMetadataPath = StringUtils.defaultIfBlank(config.assetMetadataPath(), - this.nextGenDynamicMediaConfig.getAssetMetadataPath()); + enabledRemoteAssets = config.enabledRemoteAssets(); + if (enabledRemoteAssets) { + if (nextGenDynamicMediaConfig == null) { + log.debug("NextGenDynamicMediaConfig service is not available, disable remote assets."); + enabledRemoteAssets = false; + } + else { + log.debug("NextGenDynamicMediaConfig: enabled={}, repositoryId={}, apiKey={}, env={}, imsClient={}", + nextGenDynamicMediaConfig.enabled(), nextGenDynamicMediaConfig.getRepositoryId(), + nextGenDynamicMediaConfig.getApiKey(), nextGenDynamicMediaConfig.getEnv(), nextGenDynamicMediaConfig.getImsClient()); + } + } + + imageDeliveryBasePath = StringUtils.defaultIfBlank(config.imageDeliveryBasePath(), + nextGenDynamicMediaConfig != null ? nextGenDynamicMediaConfig.getImageDeliveryBasePath() : null); + assetOriginalBinaryDeliveryPath = StringUtils.defaultIfBlank(config.assetOriginalBinaryDeliveryPath(), + nextGenDynamicMediaConfig != null ? nextGenDynamicMediaConfig.getAssetOriginalBinaryDeliveryPath() : null); + assetMetadataPath = StringUtils.defaultIfBlank(config.assetMetadataPath(), + nextGenDynamicMediaConfig != null ? nextGenDynamicMediaConfig.getAssetMetadataPath() : null); + + enabledLocalAssets = config.enabledLocalAssets(); + localAssetsRepositoryId = config.localAssetsRepositoryId(); + if (enabledLocalAssets && StringUtils.isBlank(localAssetsRepositoryId)) { + log.debug("localAssetsRepositoryId is not configured, disable local assets."); + enabledLocalAssets = false; + } + } + @Override + public boolean isEnabledRemoteAssets() { + return enabledRemoteAssets && nextGenDynamicMediaConfig != null && nextGenDynamicMediaConfig.enabled(); } @Override - public boolean enabled() { - return this.nextGenDynamicMediaConfig.enabled(); + public boolean isEnabledLocalAssets() { + return enabledLocalAssets; } @Override - public String getAssetSelectorsJsUrl() { - return this.nextGenDynamicMediaConfig.getAssetSelectorsJsUrl(); + public @Nullable String getAssetSelectorsJsUrl() { + return nextGenDynamicMediaConfig != null ? nextGenDynamicMediaConfig.getAssetSelectorsJsUrl() : null; } @Override - public String getImageDeliveryBasePath() { + public @Nullable String getImageDeliveryBasePath() { return imageDeliveryBasePath; } @Override - public String getVideoDeliveryPath() { - return this.nextGenDynamicMediaConfig.getVideoDeliveryPath(); + public @Nullable String getVideoDeliveryPath() { + return nextGenDynamicMediaConfig != null ? nextGenDynamicMediaConfig.getVideoDeliveryPath() : null; } @Override - public String getAssetOriginalBinaryDeliveryPath() { + public @Nullable String getAssetOriginalBinaryDeliveryPath() { return assetOriginalBinaryDeliveryPath; } @Override - public String getAssetMetadataPath() { + public @Nullable String getAssetMetadataPath() { return assetMetadataPath; } @Override - public String getRepositoryId() { - return this.nextGenDynamicMediaConfig.getRepositoryId(); + public @Nullable String getRemoteAssetsRepositoryId() { + return nextGenDynamicMediaConfig != null ? nextGenDynamicMediaConfig.getRepositoryId() : null; + } + + @Override + public @Nullable String getLocalAssetsRepositoryId() { + return localAssetsRepositoryId; } @Override - public String getApiKey() { - return this.nextGenDynamicMediaConfig.getApiKey(); + public @Nullable String getApiKey() { + return nextGenDynamicMediaConfig != null ? nextGenDynamicMediaConfig.getApiKey() : null; } @Override - public String getEnv() { - return this.nextGenDynamicMediaConfig.getEnv(); + public @Nullable String getEnv() { + return nextGenDynamicMediaConfig != null ? nextGenDynamicMediaConfig.getEnv() : null; } @Override - public String getImsClient() { - return this.nextGenDynamicMediaConfig.getImsClient(); + public @Nullable String getImsClient() { + return nextGenDynamicMediaConfig != null ? nextGenDynamicMediaConfig.getImsClient() : null; } } diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaImageUrlBuilder.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaImageUrlBuilder.java index 78d45283..0472443f 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaImageUrlBuilder.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaImageUrlBuilder.java @@ -76,21 +76,21 @@ public NextGenDynamicMediaImageUrlBuilder(@NotNull NextGenDynamicMediaContext co public @Nullable String build(@NotNull NextGenDynamicMediaImageDeliveryParams params) { // get parameters from nextgen dynamic media config for URL parameters - String repositoryId = context.getNextGenDynamicMediaConfig().getRepositoryId(); + String repositoryId; + if (context.getReference().getAsset() != null) { + repositoryId = context.getNextGenDynamicMediaConfig().getLocalAssetsRepositoryId(); + } + else { + repositoryId = context.getNextGenDynamicMediaConfig().getRemoteAssetsRepositoryId(); + } String imageDeliveryPath = context.getNextGenDynamicMediaConfig().getImageDeliveryBasePath(); - if (StringUtils.isAnyEmpty(repositoryId, imageDeliveryPath)) { + if (StringUtils.isAnyBlank(repositoryId, imageDeliveryPath)) { return null; } // replace placeholders in delivery path String seoName = FilenameUtils.getBaseName(context.getReference().getFileName()); - String format = context.getDefaultMediaArgs().getEnforceOutputFileExtension(); - if (StringUtils.isEmpty(format)) { - format = StringUtils.toRootLowerCase(FilenameUtils.getExtension(context.getReference().getFileName())); - } - if (!SUPPORTED_FORMATS.contains(format)) { - format = FileExtension.JPEG; - } + String format = getFileExtension(); imageDeliveryPath = StringUtils.replace(imageDeliveryPath, PLACEHOLDER_ASSET_ID, context.getReference().getAssetId()); imageDeliveryPath = StringUtils.replace(imageDeliveryPath, PLACEHOLDER_SEO_NAME, seoName); imageDeliveryPath = StringUtils.replace(imageDeliveryPath, PLACEHOLDER_FORMAT, format); @@ -153,4 +153,18 @@ else if (width != null) { return sb.toString(); } + /** + * @return Get file extension used for rendering via DM API. + */ + public @NotNull String getFileExtension() { + String format = context.getDefaultMediaArgs().getEnforceOutputFileExtension(); + if (StringUtils.isEmpty(format)) { + format = StringUtils.toRootLowerCase(FilenameUtils.getExtension(context.getReference().getFileName())); + } + if (format == null || !SUPPORTED_FORMATS.contains(format)) { + format = FileExtension.JPEG; + } + return format; + } + } diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReference.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReference.java index 4576698d..03edd9a5 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReference.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReference.java @@ -19,12 +19,21 @@ */ package io.wcm.handler.mediasource.ngdm.impl; +import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_APPROVED; +import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_PROPERTY; + import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.day.cq.dam.api.Asset; /** * Parses and validates Next Generation Dynamic Media references. @@ -39,17 +48,29 @@ public final class NextGenDynamicMediaReference { private final String assetId; private final String fileName; + private final Asset asset; + + private static final Logger log = LoggerFactory.getLogger(NextGenDynamicMediaReference.class); /** * @param assetId Asset ID (has to start with "urn:") * @param fileName File name */ public NextGenDynamicMediaReference(@NotNull String assetId, @NotNull String fileName) { + this(assetId, fileName, null); + } + + /** + * @param assetId Asset ID (has to start with "urn:") + * @param fileName File name + */ + public NextGenDynamicMediaReference(@NotNull String assetId, @NotNull String fileName, @Nullable Asset asset) { if (!StringUtils.startsWith(assetId, ASSET_ID_PREFIX)) { throw new IllegalArgumentException("Asset ID must start with '" + ASSET_ID_PREFIX + "'"); } this.assetId = assetId; this.fileName = fileName; + this.asset = asset; } /** @@ -66,6 +87,13 @@ public NextGenDynamicMediaReference(@NotNull String assetId, @NotNull String fil return fileName; } + /** + * @return Asset (if reference points to local asset) + */ + public @Nullable Asset getAsset() { + return asset; + } + /** * @return Reference */ @@ -91,6 +119,38 @@ public NextGenDynamicMediaReference(@NotNull String assetId, @NotNull String fil return new NextGenDynamicMediaReference(assetId, fileName); } + /** + * Parses a next generation dynamic media reference. + * @param reference Reference + * @return Parsed reference or null if reference is invalid + */ + public static @Nullable NextGenDynamicMediaReference fromDamAssetReference(@Nullable String reference, @NotNull ResourceResolver resourceResolver) { + if (reference == null) { + return null; + } + Resource resource = resourceResolver.getResource(reference); + if (resource == null) { + return null; + } + Asset asset = resource.adaptTo(Asset.class); + if (asset == null) { + return null; + } + String uuid = asset.getID(); + if (StringUtils.isBlank(uuid)) { + log.trace("Ignoring DAM asset without UUID: {}", asset.getPath()); + return null; + } + String damStatus = asset.getMetadataValueFromJcr(ASSET_STATUS_PROPERTY); + if (!StringUtils.equals(damStatus, ASSET_STATUS_APPROVED)) { + log.trace("Ignoring DAM asset with {}='{}' (expected '{}')", ASSET_STATUS_PROPERTY, damStatus, ASSET_STATUS_APPROVED); + return null; + } + String assetId = "urn:aaid:aem:" + uuid; + String fileName = asset.getName(); + return new NextGenDynamicMediaReference(assetId, fileName, asset); + } + /** * Checks if given string is a valid next generation dynamic media reference. * @param reference Reference diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadata.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadata.java index 3d24c2ef..c71c9e82 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadata.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadata.java @@ -26,10 +26,12 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import com.day.cq.dam.api.Asset; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.json.JsonMapper; import io.wcm.handler.media.Dimension; +import io.wcm.handler.mediasource.dam.AssetRendition; import io.wcm.wcm.commons.contenttype.ContentType; /** @@ -66,7 +68,10 @@ public final class NextGenDynamicMediaMetadata { return dimension; } - boolean isValid() { + /** + * @return true if metadata is valid (has mime type) + */ + public boolean isValid() { return mimeType != null; } @@ -100,4 +105,23 @@ public String toString() { return new NextGenDynamicMediaMetadata(mimeType, width, height); } + /** + * Gets metadata from DAM asset. + * @param asset Asset + * @return Metadata object + */ + public static @NotNull NextGenDynamicMediaMetadata fromAsset(@NotNull Asset asset) { + String mimeType = asset.getMimeType(); + + long width = 0; + long height = 0; + Dimension dimension = AssetRendition.getDimension(asset.getOriginal()); + if (dimension != null) { + width = dimension.getWidth(); + height = dimension.getHeight(); + } + + return new NextGenDynamicMediaMetadata(mimeType, width, height); + } + } diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataServiceImpl.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataServiceImpl.java index bcaaa71d..ba9d57a3 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataServiceImpl.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataServiceImpl.java @@ -41,8 +41,6 @@ import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferencePolicy; -import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.Designate; import org.osgi.service.metatype.annotations.ObjectClassDefinition; @@ -60,13 +58,13 @@ public class NextGenDynamicMediaMetadataServiceImpl implements NextGenDynamicMediaMetadataService { @ObjectClassDefinition( - name = "wcm.io Media Handler Next Generation Dynamic Media Metadata Service", - description = "Fetches metadata for Next Generation Dynamic Media assets.") + name = "wcm.io Media Handler Dynamic Media with OpenAPI Metadata Service", + description = "Fetches metadata for Dynamic Media with OpenAPI remote assets.") @interface Config { @AttributeDefinition( name = "Enabled", - description = "When enabled, metadata is fetched for each resolved asset. This checks for validity/existence of " + description = "When enabled, metadata is fetched for each resolved remote asset. This checks for validity/existence of " + "the asset and for the maximum supported resolution of the original image.") boolean enabled() default false; @@ -103,7 +101,7 @@ public class NextGenDynamicMediaMetadataServiceImpl implements NextGenDynamicMed } - @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY) + @Reference private NextGenDynamicMediaConfigService nextGenDynamicMediaConfig; private boolean enabled; diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataUrlBuilder.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataUrlBuilder.java index 44c5726c..2492d4d4 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataUrlBuilder.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataUrlBuilder.java @@ -53,7 +53,7 @@ final class NextGenDynamicMediaMetadataUrlBuilder { public @Nullable String build(@NotNull NextGenDynamicMediaReference reference) { // get parameters from nextgen dynamic media config for URL parameters - String repositoryId = config.getRepositoryId(); + String repositoryId = config.getRemoteAssetsRepositoryId(); String metadataPath = config.getAssetMetadataPath(); if (StringUtils.isAnyEmpty(repositoryId, metadataPath)) { return null; diff --git a/src/site/markdown/dynamic-media-openapi.md b/src/site/markdown/dynamic-media-openapi.md new file mode 100644 index 00000000..03a793f9 --- /dev/null +++ b/src/site/markdown/dynamic-media-openapi.md @@ -0,0 +1,113 @@ +## Dynamic Media with OpenAPI + +wcm.io Media Handler optionally supports the [Dynamic Media with OpenAPI][aem-nextgen-dm] feature of AEM as a Cloud Service for: + +* Rendering renditions including resizing and cropping +* Delivery via Dynamic Media with OpenAPI CDN +* AI-based Smart Cropping + +Dynamic Media with OpenAPI (formerly known as Next Generation Dynamic Media) has to be licensed separately and is not activated by default for AEMaaCS instances. It can be used for two use cases: + +* Rendering remote assets referenced via the Remote Asset Picker from other assets instances + * Those remote asset references typically start with `/urn:aaid:aem:...`. + * When Dynamic Media with OpenAPI is enabled for an AEMaaCS instance, the asset picker is shown automatically for Media Handler Granite UI widgets. +* Rendering local assets stored in the same AEMaaCS instance. + +The Adobe AEM WCM Core Components currently support only a subset of features, i.e. they only support remote assets but no local assets. When you are using Media Handler directly in your components, or via the [wcm.io WCM Core Components][wcm-core-components] you can leverage all features described in this page. + + +### Media Handler concept + +The integration with Dynamic Media with OpenAPI builds on the [general concepts][general-concepts] of the Media Handler using media formats and a unified media handling API to resolve the renditions for each use case. + +If a rendition is rendered via Dynamic Media with OpenAPI, the media handler returns rendition URLs pointing to the local ore remote asset instance using the [Assets Delivery API (DM API)][aem-dm-api]. From the [supported file formats][file-format-support] Dynamic Media with OpenAPI supports scaling and smart cropping for JPEG, PNG, GIF and TIFF images. All other file formats including SVG images are delivered as original binary via the Dynamic Media with OpenAPI CDN. + +It's not required to configure anything in the component instance edit dialogs or content policies. + + +### Approval State + +Publishing assets to be used via Dynamic Media with OpenAPI works differently than usual: + +* Set the property **Review Status** of the asset to **Approved** + * With this, the asset is accessible for rendering both in author and publish/live environment + * If this status is not set, it's neither visible in Remote Asset Picker, nor can it rendered from local assets +* The Publication status of an asset is not relevant + * However, if you render local assets via Dynamic Media with OpenAPI and the Media Handler, you have to publish the asset after it is set to Approved, so the existence and approval state can be checked on the publish instance when rendering the pages and components. + + +### Smart Cropping + +Smart Cropping is used automatically if a media format with a specific ratio (e.g. 16:9) is used. + +Media formats without any size restrictions, or e.g. only with a width restrictions, can be rendered, but are not cropped. + + +### Validating Assets + +When rendering local assets, the existence and approval state is checked within the local content repository when resolving the media. For this reason, local assets have to be published in AEM after setting the Approval state. + +For remote assets, the Media Handler assumes by default that a given remote asset reference is always valid, and supports all requested resolutions. + +Optionally, you can enable the metadata service. If enabled, each time a remote asset reference is resolved, the following checks are executed: + +* If the remote asset does not exist, or is not approved, the reference is handled as invalid and the component can react to it (e.g. hide the image component). +* If the requested resolution of a rendition is larger than the original resolution of the binary asset, the rendition is handled as invalid. This avoid upscaling, and avoids using an asset in a context which would result in bad image quality for the user. + +See system configuration how to enable the metadata service. + + +### System configuration + +In your project-specific implementation of `io.wcm.handler.media.spi.MediaHandlerConfig` you have to add the media sources implementation `io.wcm.handler.mediasource.ngdm.NextGenDynamicMediaMediaSource` to the list returned by the `getSources()` method (overwrite it from the superclass if required). If you want to use local assets, make sure to put it on top of the list (above the `io.wcm.handler.mediasource.dam.DamMediaSource` media source). + +Example: + +```java +@Component(service = MediaHandlerConfig.class) +public class MediaHandlerConfigImpl extends MediaHandlerConfig { + + private static final List> MEDIA_SOURCES = List.of( + NextGenDynamicMediaMediaSource.class, + DamMediaSource.class, + InlineMediaSource.class); + + public @NotNull List> getSources() { + return MEDIA_SOURCES; + } + + // ... +} +``` + +With this configuration, remote assets should work out-of-the-box, if a remote asset repository is configured for the AEMaaCS instance. + +The "wcm.io Dynamic Media with OpenAPI Support" OSGi configuration allows to reconfigure the actual URLs used for the [Assets Delivery API (DM API)][aem-dm-api]. Usually you can stick with the default values which reflect the latest version of the DM API. Remote assets are supported by default, but can be disabled via this configuration. Local assets are disabled by support, but can be enabled via this configuration. In this case, you also have to configure a repository ID for building the rendition URLs pointing to the AEMaaCS instance. Example: + +```json +{ + "enabledLocalAssets": true, + "localAssetsRepositoryId": "$[env:LOCAL_ASSET_DELIVERY_REPOSITORY_ID;default=]" +} +``` + +With this, you can configure an environment variable `LOCAL_ASSET_DELIVERY_REPOSITORY_ID` pointing to the actual host name which usually has a syntax like `delivery-pXXXXX-eXXXXX.adobeaemcloud.com` with the corresponding program and environment numbers. + +The "wcm.io Dynamic Media with OpenAPI Metadata Service" allows to enable the Asset Metadata support (see above). When this is enabled, for each resolved remote asset, a HTTP request is send from the server to the DM API, so make sure this is allowed in the network infrastructure (should work by default in AEMaaCS instances). Optionally, you can configure an proxy server and timeouts. + + +### Known Limitations (as of June 2024) + +* Dynamic Media with OpenAPI is not supported in Media Handler for AEM 6.x, only for AEMaaCS +* Same as with the Adobe AEM WCM Core Components, currently only a single remote AEM Asset instance is supported, which is configured centrally as described in [Dynamic Media with OpenAPI][aem-nextgen-dm]. The media handler uses the same convention for referencing remote assets (using strings starting with `/urn:aaid:aem:...`). This convention also does not support multiple remote AEM Asset instances, as it does not include a pointer to the Repository ID. +* If a component dialog is re-opened with a remote asset references and one of the Media Handler Granite UI widgets (e.g. pathfield), no thumbnail is displayed for the remote asset. But the reference is valid and works. The root cause is a bug/limitation in the underlying AEM pathfield component, which hopefully will be fixed soon by Adobe (SITES-19894). +* The Dynamic Media with OpenAPI remote asset picker currently ignores any folder structures for assets on the remote AEM Asset instance. +* The DM API currently does not support sending a "Content-Disposition: attachment" HTTP header for downloads. So, even if this is enforced by the Media Handler, it currently does not work for remote assets. + + +[aem-nextgen-dm]: https://experienceleague.adobe.com/docs/experience-manager-core-components/using/developing/next-gen-dm.html?lang=en +[aem-dm-api]: https://adobe-aem-assets-delivery-experimental.redoc.ly/ +[general-concepts]: general-concepts.html +[file-format-support]: file-format-support.html +[configuration]: configuration.html +[wcm-core-components]: https://wcm.io/wcm/core-components/ \ No newline at end of file diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index e30aeecc..8164ded3 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -13,8 +13,8 @@ Media resolving, processing and markup generation. * [Component properties][component-properties] * [System configuration][configuration] * [File format support][file-format-support] -* [Next Generation Dynamic Media support][nextgen-dynamic-media] * [Dynamic Media support][dynamic-media] +* [Dynamic Media with OpenAPI support][dynamic-media-openapi] * [API documentation][apidocs] * [Changelog][changelog] @@ -36,7 +36,7 @@ The Media Handler provides: * Generic HTL Placeholder template * Generic [Granite UI components][graniteui-components] that can be used in media/image component dialogs * Support for [Dynamic Media][dynamic-media] -* Support for [Next Generation Dynamic Media][nextgen-dm] on AEMaaCS +* Support for [Dynamic Media with OpenAPI][nextgen-dm] on AEMaaCS * Support for Web-Optimized Image Delivery: On AEMaaCS instances renditions are transparently rendered on the edge Read the [general concepts][general-concepts] to get an overview of the functionality. @@ -82,7 +82,7 @@ Sources: https://github.com/wcm-io/io.wcm.handler.media [file-format-support]: file-format-support.html [nextgen-dm]: https://experienceleague.adobe.com/docs/experience-manager-core-components/using/developing/next-gen-dm.html?lang=en [dynamic-media]: dynamic-media.html -[nextgen-dynamic-media]: nextgen-dynamic-media.html +[dynamic-media-openapi]: dynamic-media-openapi.html [apidocs]: apidocs/ [changelog]: changes-report.html [url-handler]: ../url/ diff --git a/src/site/markdown/nextgen-dynamic-media.md b/src/site/markdown/nextgen-dynamic-media.md deleted file mode 100644 index 8219a2ec..00000000 --- a/src/site/markdown/nextgen-dynamic-media.md +++ /dev/null @@ -1,65 +0,0 @@ -## Next-Generation Dynamic Media - -wcm.io Media Handler optionally supports the [Next Generation Dynamic Media][aem-nextgen-dm] feature of AEM as a Cloud Service for: - -* Rendering renditions including resizing and cropping -* Delivery via Next Generation Dynamic Media CDN -* AI-based Smart Cropping - -Next Generation Dynamic Media support is never applied to assets stored in the AEMaaCS instance itself, but only for remote assets referenced via the Next Generation Dynamic Media asset picker. Those remote asset references typically start with `/urn:aaid:aem:...`. When Next Generation Dynamic Media is enabled for an AEMaaCS instance, the asset picker is shown automatically for Media Handler Granite UI widgets. - -Remote assets are only shown in the asset picker, if they have "approval" state. The publish state of the asset inside AEM is not relevant. - - -### Dynamic Media concept - -The integration with Next Generation Dynamic Media builds on the [general concepts][general-concepts] of the Media Handler using media formats and a unified media handling API to resolve the renditions for each use case. - -If a Next Generation Dynamic Media remote asset is used, the media handler returns rendition URLs pointing to the remote asset instance using the [Assets Delivery API (DM API)][aem-dm-api]. From the [supported file formats][file-format-support] Next Generation Dynamic Media supports scaling and smart cropping for JPEG, PNG, GIF and TIFF images. All other file formats including SVG images are delivered as original binary via the Next Generation Dynamic Media CDN. - -It's not required to configure anything in the component instance edit dialogs or content policies. - - -### Smart Cropping - -Smart Cropping is used automatically if a media format with a specific ratio (e.g. 16:9) is used. - -Media formats without any size restrictions, or e.g. only with a width restrictions, can be rendered, but are not cropped. - - -### Validating Remote Asset Metadata - -By default, the Media Handler assumes that a given remote asset reference is always valid, and supports all requested resolutions. - -Optionally, you can enable the metadata service. If enabled, each time a remote asset reference is resolved, the following checks are executed: - -* If the remote asset does not exist, or is not approved, the reference is handled as invalid and the component can react to it (e.g. hide the image component) -* If the requested resolution of a rendition is larger than the original resolution of the binary asset, the rendition is handled as invalid. This avoid upscaling, and avoids using an asset in a context which would result in bad image quality for the user. - -See system configuration how to enable the metadata service. - - -### System configuration - -If Next Generation Dynamic Media is enabled for a AEMaaCS instance, it will work out-of-the-box with the Media Handler. In your project-specific implementation of `io.wcm.handler.media.spi.MediaHandlerConfig` you have to add the media sources implementation `io.wcm.handler.mediasource.ngdm.NextGenDynamicMediaMediaSource` to the list returned by the `getSources()` method (overwrite it from the superclass if required). - -The "wcm.io Next Generation Dynamic Media Support" OSGi configuration allows to reconfigure the actual URLs used for the [Assets Delivery API (DM API)][aem-dm-api]. Usually you can stick with the default values which reflect the latest version of the DM API. - -The "wcm.io Next Generation Dynamic Media Metadata Service" allows to enable the Asset Metadata support (see above). When this is enabled, for each resolved remote asset, a HTTP request is send from the server to the DM API, so make sure this is allowed in the network infrastructure (should work by default in AEMaaCS instances). Optionally, you can configure an proxy server and timeouts. - - -### Known Limitations (as of March 2024) - -* The DM API URLs still have to contain an `accept-experimental` URL parameter, and the metadata services has to use an `X-Adobe-Accept-Experimental` HTTP header. Both will fade out once Next Generation Dynamic Media reaches full general availability (expected later in 2024). -* Next Generation Dynamic Media is not supported in Media Handler for AEM 6.x, only for AEMaaCS -* Same as the Adobe Core Components, currently only a single remote AEM Asset instance is supported, which is configured centrally as described in [Next Generation Dynamic Media][aem-nextgen-dm]. The media handler is used the same convention for referencing remote assets (using strings starting with `/urn:aaid:aem:...`). This convention also does not support multiple remote AEM Asset instances, as it does not include a pointer to the Repository ID. -* If a component dialog is re-opened with a remote asset references and one of the Media Handler Granite UI widgets (e.g. pathfield), no thumbnail is displayed for the remote asset. But the reference is valid and works. The root cause is a bug/limitation in the underlying AEM pathfield component, which hopefully will be fixed soon by Adobe (SITES-19894). -* The Next Generation Dynamic Media remote asset picker currently ignored any folder structures for assets on the remote AEM Asset instance. -* The DM API currently does not support sending a "Content-Disposition: attachment" HTTP header for downloads. So even, if this is enforced by the Media Handler, it currently does not work for remote assets. - - -[aem-nextgen-dm]: https://experienceleague.adobe.com/docs/experience-manager-core-components/using/developing/next-gen-dm.html?lang=en -[aem-dm-api]: https://adobe-aem-assets-delivery-experimental.redoc.ly/ -[general-concepts]: general-concepts.html -[file-format-support]: file-format-support.html -[configuration]: configuration.html diff --git a/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaLocalAssetTest.java b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaLocalAssetTest.java new file mode 100644 index 00000000..d11ff143 --- /dev/null +++ b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaLocalAssetTest.java @@ -0,0 +1,234 @@ +/* + * #%L + * wcm.io + * %% + * Copyright (C) 2024 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.media.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.day.cq.dam.api.Asset; + +import io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaConfigServiceImpl; +import io.wcm.testing.mock.aem.dam.ngdm.MockNextGenDynamicMediaConfig; +import io.wcm.testing.mock.aem.junit5.AemContextExtension; +import io.wcm.wcm.commons.contenttype.ContentType; + +/** + * This is an "end-to-end" test handling image files with different content types + * from classpath, handles them with and without cropping using media handler + * and renders the result using the ImageFileServlet. + */ +@ExtendWith(AemContextExtension.class) +@SuppressWarnings("java:S2699") // all tests have assertions +class MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaLocalAssetTest extends MediaHandlerImplImageFileTypesEnd2EndTest { + + @BeforeEach + @Override + void setUp() { + MockNextGenDynamicMediaConfig nextGenDynamicMediaConfig = context.registerInjectActivateService(MockNextGenDynamicMediaConfig.class); + nextGenDynamicMediaConfig.setEnabled(true); + nextGenDynamicMediaConfig.setRepositoryId("repo1"); + context.registerInjectActivateService(NextGenDynamicMediaConfigServiceImpl.class, + "enabledLocalAssets", "true", + "localAssetsRepositoryId", "localrepo1"); + super.setUp(); + } + + @Override + @Test + void testAsset_JPEG_Original() { + Asset asset = createSampleAsset("/filetype/sample.jpg", ContentType.JPEG); + buildAssertMedia(asset, 100, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.jpg?accept-experimental=1&preferwebp=true&quality=85", + ContentType.JPEG); + } + + @Override + @Test + void testAsset_JPEG_Original_ContentDisposition() { + Asset asset = createSampleAsset("/filetype/sample.jpg", ContentType.JPEG); + buildAssertMedia_ContentDisposition(asset, 100, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.jpg?accept-experimental=1&preferwebp=true&quality=85", + ContentType.JPEG); + } + + @Override + @Test + void testAsset_JPEG_Rescale() { + Asset asset = createSampleAsset("/filetype/sample.jpg", ContentType.JPEG); + buildAssertMedia_Rescale(asset, 80, 40, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.jpg?accept-experimental=1&crop=80%3A40%2Csmart&preferwebp=true&quality=85&width=80", + ContentType.JPEG); + } + + @Override + @Test + void testAsset_JPEG_AutoCrop() { + Asset asset = createSampleAsset("/filetype/sample.jpg", ContentType.JPEG); + buildAssertMedia_AutoCrop(asset, 50, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.jpg?accept-experimental=1&crop=1%3A1%2Csmart&preferwebp=true&quality=85", + ContentType.JPEG); + } + + @Override + @Test + void testAsset_JPEG_AutoCrop_ImageQuality() { + Asset asset = createSampleAsset("/filetype/sample.jpg", ContentType.JPEG); + buildAssertMedia_AutoCrop(asset, 50, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.jpg?accept-experimental=1&crop=1%3A1%2Csmart&preferwebp=true&quality=60", + ContentType.JPEG, 0.6d); + } + + @Override + @Test + void testAsset_JPEG_CropWithExplicitRendition() { + Asset asset = createSampleAsset("/filetype/sample.jpg", ContentType.JPEG); + context.create().assetRendition(asset, "square.jpg", 50, 50, ContentType.JPEG); + buildAssertMedia_AutoCrop(asset, 50, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.jpg?accept-experimental=1&crop=1%3A1%2Csmart&preferwebp=true&quality=85", + ContentType.JPEG); + } + + @Override + @Test + void testAsset_GIF_Original() { + Asset asset = createSampleAsset("/filetype/sample.gif", ContentType.GIF); + buildAssertMedia(asset, 100, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.gif?accept-experimental=1&preferwebp=true&quality=85", + ContentType.GIF); + } + + @Override + @Test + void testAsset_GIF_Rescale() { + Asset asset = createSampleAsset("/filetype/sample.gif", ContentType.GIF); + buildAssertMedia_Rescale(asset, 80, 40, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.gif?accept-experimental=1&crop=80%3A40%2Csmart&preferwebp=true&quality=85&width=80", + ContentType.JPEG); + } + + @Override + @Test + void testAsset_GIF_AutoCrop() { + Asset asset = createSampleAsset("/filetype/sample.gif", ContentType.GIF); + buildAssertMedia_AutoCrop(asset, 50, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.gif?accept-experimental=1&crop=1%3A1%2Csmart&preferwebp=true&quality=85", + ContentType.JPEG); + } + + @Override + @Test + void testAsset_PNG_Original() { + Asset asset = createSampleAsset("/filetype/sample.png", ContentType.PNG); + buildAssertMedia(asset, 100, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.png?accept-experimental=1&preferwebp=true&quality=85", + ContentType.PNG); + } + + @Override + @Test + void testAsset_PNG_Rescale() { + Asset asset = createSampleAsset("/filetype/sample.png", ContentType.PNG); + buildAssertMedia_Rescale(asset, 80, 40, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.png?accept-experimental=1&crop=80%3A40%2Csmart&preferwebp=true&quality=85&width=80", + ContentType.PNG); + } + + @Override + @Test + void testAsset_PNG_AutoCrop() { + Asset asset = createSampleAsset("/filetype/sample.png", ContentType.PNG); + buildAssertMedia_AutoCrop(asset, 50, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.png?accept-experimental=1&crop=1%3A1%2Csmart&preferwebp=true&quality=85", + ContentType.PNG); + } + + @Override + @Test + void testAsset_TIFF_Original() { + Asset asset = createSampleAsset("/filetype/sample.tif", ContentType.TIFF); + buildAssertMedia(asset, 100, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.jpg?accept-experimental=1&preferwebp=true&quality=85", + ContentType.JPEG); + } + + @Override + @Test + void testAsset_TIFF_Original_ContentDisposition() { + Asset asset = createSampleAsset("/filetype/sample.tif", ContentType.TIFF); + buildAssertMedia_ContentDisposition(asset, 100, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.jpg?accept-experimental=1&preferwebp=true&quality=85", + ContentType.TIFF); + } + + @Override + @Test + void testAsset_TIFF_Rescale() { + Asset asset = createSampleAsset("/filetype/sample.tif", ContentType.TIFF); + buildAssertMedia_Rescale(asset, 80, 40, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.jpg?accept-experimental=1&crop=80%3A40%2Csmart&preferwebp=true&quality=85&width=80", + ContentType.JPEG); + } + + @Override + @Test + void testAsset_TIFF_AutoCrop() { + Asset asset = createSampleAsset("/filetype/sample.tif", ContentType.TIFF); + buildAssertMedia_AutoCrop(asset, 50, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/sample.jpg?accept-experimental=1&crop=1%3A1%2Csmart&preferwebp=true&quality=85", + ContentType.JPEG); + } + + @Override + @Test + void testAsset_SVG_Original() { + Asset asset = createSampleAsset("/filetype/sample.svg", ContentType.SVG); + buildAssertMedia(asset, 100, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/original/as/sample.svg?accept-experimental=1", + ContentType.SVG); + } + + @Override + @Test + void testAsset_SVG_Original_ContentDisposition() { + Asset asset = createSampleAsset("/filetype/sample.svg", ContentType.SVG); + buildAssertMedia_ContentDisposition(asset, 100, 50, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/original/as/sample.svg?accept-experimental=1", + ContentType.SVG); + } + + @Override + @Test + void testAsset_SVG_Rescale() { + Asset asset = createSampleAsset("/filetype/sample.svg", ContentType.SVG); + buildAssertMedia_Rescale(asset, 80, 40, + "https://localrepo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/original/as/sample.svg?accept-experimental=1", + ContentType.SVG); + } + + @Override + @Test + @Disabled("Not supported with NGDM") + void testAsset_SVG_AutoCrop() { + // disabled + } + +} diff --git a/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaTest.java b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaTest.java index b8be0218..20d459df 100644 --- a/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaTest.java +++ b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.when; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -99,6 +100,13 @@ void testAsset_JPEG_AutoCrop_ImageQuality() { ContentType.JPEG, 0.6d); } + @Override + @Test + @Disabled("Not supported with NGDM") + void testAsset_JPEG_CropWithExplicitRendition() { + // disabled + } + @Override @Test void testAsset_GIF_Original() { diff --git a/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndTest.java b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndTest.java index f2e98bff..12051e51 100644 --- a/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndTest.java +++ b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndTest.java @@ -19,14 +19,17 @@ */ package io.wcm.handler.media.impl; +import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_APPROVED; +import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_PROPERTY; import static io.wcm.handler.media.MediaNameConstants.NN_MEDIA_INLINE; +import static io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReferenceSample.SAMPLE_UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.io.InputStream; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; @@ -34,13 +37,14 @@ import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.resource.ModifiableValueMap; import org.apache.sling.api.resource.Resource; -import org.apache.sling.testing.mock.osgi.MapUtil; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import com.day.cq.commons.jcr.JcrConstants; import com.day.cq.dam.api.Asset; import com.day.cq.dam.scene7.api.constants.Scene7Constants; import com.day.image.Layer; @@ -395,15 +399,15 @@ void testFileUpload_SVG_AutoCrop() { Asset createSampleAsset(String classpathResource, String contentType) { String fileName = FilenameUtils.getName(classpathResource); String fileExtension = FilenameUtils.getExtension(classpathResource); - Map metadata; + Map metadata = new HashMap<>(); + metadata.put(ASSET_STATUS_PROPERTY, ASSET_STATUS_APPROVED); if (isCreateAssetWithDynamicMediaMetadata()) { - metadata = MapUtil.toMap(Scene7Constants.PN_S7_FILE, "DummyFolder/" + fileName); - } - else { - metadata = Collections.emptyMap(); + metadata.put(Scene7Constants.PN_S7_FILE, "DummyFolder/" + fileName); } Asset asset = context.create().asset("/content/dam/" + fileName, classpathResource, contentType, metadata); context.create().assetRendition(asset, "cq5dam.web.sample." + fileExtension, classpathResource, contentType); + ModifiableValueMap props = AdaptTo.notNull(asset, ModifiableValueMap.class); + props.put(JcrConstants.JCR_UUID, SAMPLE_UUID); return asset; } diff --git a/src/test/java/io/wcm/handler/media/testcontext/DummyMediaHandlerConfig.java b/src/test/java/io/wcm/handler/media/testcontext/DummyMediaHandlerConfig.java index dd4d016e..e4c08b12 100644 --- a/src/test/java/io/wcm/handler/media/testcontext/DummyMediaHandlerConfig.java +++ b/src/test/java/io/wcm/handler/media/testcontext/DummyMediaHandlerConfig.java @@ -35,9 +35,9 @@ public class DummyMediaHandlerConfig extends MediaHandlerConfig { private static final List> MEDIA_SOURCES = List.of( + NextGenDynamicMediaMediaSource.class, DamMediaSource.class, - InlineMediaSource.class, - NextGenDynamicMediaMediaSource.class); + InlineMediaSource.class); private boolean enforceVirtualRenditions; diff --git a/src/test/java/io/wcm/handler/mediasource/inline/InlineMediaSourceTest.java b/src/test/java/io/wcm/handler/mediasource/inline/InlineMediaSourceTest.java index 7027a880..e12285f5 100644 --- a/src/test/java/io/wcm/handler/mediasource/inline/InlineMediaSourceTest.java +++ b/src/test/java/io/wcm/handler/mediasource/inline/InlineMediaSourceTest.java @@ -24,6 +24,7 @@ import static io.wcm.handler.media.MediaNameConstants.PN_MEDIA_CROP; import static io.wcm.handler.media.MediaNameConstants.PN_MEDIA_IS_DECORATIVE; import static io.wcm.handler.media.MediaNameConstants.PN_MEDIA_ROTATION; +import static io.wcm.handler.media.testcontext.AppAemContext.ROOTPATH_CONTENT; import static io.wcm.handler.media.testcontext.DummyMediaFormats.EDITORIAL_1COL; import static io.wcm.handler.media.testcontext.DummyMediaFormats.EDITORIAL_2COL; import static io.wcm.handler.media.testcontext.DummyMediaFormats.EDITORIAL_3COL; @@ -39,7 +40,6 @@ import static io.wcm.handler.media.testcontext.DummyMediaFormats.SHOWROOM_CONTROLS_SCALE1_ONLYWIDTH_RATIO1; import static io.wcm.handler.media.testcontext.DummyMediaFormats.SHOWROOM_CONTROLS_SCALE1_ONLYWIDTH_RATIO2; import static io.wcm.handler.media.testcontext.DummyMediaFormats.SHOWROOM_FLYOUT_FEATURE; -import static io.wcm.handler.media.testcontext.AppAemContext.ROOTPATH_CONTENT; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaConfigModelTest.java b/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaConfigModelTest.java index 57c49cff..a8301f45 100644 --- a/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaConfigModelTest.java +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaConfigModelTest.java @@ -20,8 +20,6 @@ package io.wcm.handler.mediasource.ngdm; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import org.json.JSONException; @@ -64,12 +62,4 @@ void testWithConfigService() throws JSONException { underTest.getConfigJson(), true); } - @Test - void testWithoutConfigService() { - NextGenDynamicMediaConfigModel underTest = AdaptTo.notNull(context.request(), NextGenDynamicMediaConfigModel.class); - assertFalse(underTest.isEnabled()); - assertNull(underTest.getAssetSelectorsJsUrl()); - assertNull(underTest.getConfigJson()); - } - } diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaMediaSourceTest.java b/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaMediaSourceTest.java index 47bc9dc8..acc48b83 100644 --- a/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaMediaSourceTest.java +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaMediaSourceTest.java @@ -52,37 +52,42 @@ class NextGenDynamicMediaMediaSourceTest { @Test void testGetId() { + registerMockNextGenDynamicMediaConfig(false, false); NextGenDynamicMediaMediaSource underTest = AdaptTo.notNull(context.request(), NextGenDynamicMediaMediaSource.class); assertEquals(NextGenDynamicMediaMediaSource.ID, underTest.getId()); } @Test - void testAccepts_withoutNextGenDynamicMediaConfig() { + void testAccepts_withNextGenDynamicMediaConfigDisabled() { + registerMockNextGenDynamicMediaConfig(false, false); NextGenDynamicMediaMediaSource underTest = AdaptTo.notNull(context.request(), NextGenDynamicMediaMediaSource.class); assertFalse(underTest.accepts(SAMPLE_REFERENCE)); + assertFalse(underTest.accepts("/content/dam/sample.jpg")); assertFalse(underTest.accepts("invalid")); assertFalse(underTest.accepts("")); assertFalse(underTest.accepts((String)null)); } @Test - void testAccepts_withNextGenDynamicMediaConfigDisabled() { - registerMockNextGenDynamicMediaConfig(false); + void testAccepts_withNextGenDynamicMediaConfigEnabled() { + registerMockNextGenDynamicMediaConfig(true, true); NextGenDynamicMediaMediaSource underTest = AdaptTo.notNull(context.request(), NextGenDynamicMediaMediaSource.class); - assertFalse(underTest.accepts(SAMPLE_REFERENCE)); + assertTrue(underTest.accepts(SAMPLE_REFERENCE)); + assertTrue(underTest.accepts("/content/dam/sample.jpg")); assertFalse(underTest.accepts("invalid")); assertFalse(underTest.accepts("")); assertFalse(underTest.accepts((String)null)); } @Test - void testAccepts_withNextGenDynamicMediaConfigEnabled() { - registerMockNextGenDynamicMediaConfig(true); + void testAccepts_withNextGenDynamicMediaConfigEnabled_NoLocalAssets() { + registerMockNextGenDynamicMediaConfig(true, false); NextGenDynamicMediaMediaSource underTest = AdaptTo.notNull(context.request(), NextGenDynamicMediaMediaSource.class); assertTrue(underTest.accepts(SAMPLE_REFERENCE)); + assertFalse(underTest.accepts("/content/dam/sample.jpg")); assertFalse(underTest.accepts("invalid")); assertFalse(underTest.accepts("")); assertFalse(underTest.accepts((String)null)); @@ -91,6 +96,7 @@ void testAccepts_withNextGenDynamicMediaConfigEnabled() { @Test @SuppressWarnings("null") void testEnableMediaDrop() { + registerMockNextGenDynamicMediaConfig(false, false); NextGenDynamicMediaMediaSource underTest = AdaptTo.notNull(context.request(), NextGenDynamicMediaMediaSource.class); MediaRequest mediaRequest = new MediaRequest(context.currentResource(), new MediaArgs()); @@ -102,6 +108,8 @@ void testEnableMediaDrop() { @Test @SuppressWarnings("null") void testEnableMediaDrop_Authoring() { + registerMockNextGenDynamicMediaConfig(false, false); + // simulate component context ComponentContext wcmComponentContext = mock(ComponentContext.class); context.request().setAttribute(ComponentContext.CONTEXT_ATTR_NAME, wcmComponentContext); @@ -118,10 +126,12 @@ void testEnableMediaDrop_Authoring() { assertEquals("cq-dd-image", img.getCssClass()); } - void registerMockNextGenDynamicMediaConfig(boolean enabled) { + void registerMockNextGenDynamicMediaConfig(boolean remoteAssets, boolean localAssets) { MockNextGenDynamicMediaConfig nextGenDynamicMediaConfig = context.registerInjectActivateService(MockNextGenDynamicMediaConfig.class); - nextGenDynamicMediaConfig.setEnabled(enabled); + nextGenDynamicMediaConfig.setEnabled(remoteAssets); nextGenDynamicMediaConfig.setRepositoryId("repo1"); - context.registerInjectActivateService(NextGenDynamicMediaConfigServiceImpl.class); + context.registerInjectActivateService(NextGenDynamicMediaConfigServiceImpl.class, + "enabledLocalAssets", localAssets, + "localAssetsRepositoryId", "localrepo1"); } } diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigServiceImplTest.java b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigServiceImplTest.java index 8ed82bbf..4315e281 100644 --- a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigServiceImplTest.java +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaConfigServiceImplTest.java @@ -20,9 +20,10 @@ package io.wcm.handler.mediasource.ngdm.impl; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -36,31 +37,21 @@ class NextGenDynamicMediaConfigServiceImplTest { private final AemContext context = AppAemContext.newAemContext(); - @BeforeEach - void setUp() { - MockNextGenDynamicMediaConfig config = context.registerInjectActivateService(MockNextGenDynamicMediaConfig.class); - config.setEnabled(true); - config.setAssetSelectorsJsUrl("/selector1"); - config.setImageDeliveryBasePath("/imagepath1"); - config.setVideoDeliveryPath("/videopath1"); - config.setAssetOriginalBinaryDeliveryPath("/assetpath1"); - config.setAssetMetadataPath("/metadatapath1"); - config.setRepositoryId("repo1"); - config.setApiKey("key1"); - config.setEnv("env1"); - config.setImsClient("client1"); - } - @Test void testPropertiesDefaultConfig() { - NextGenDynamicMediaConfigService underTest = context.registerInjectActivateService(NextGenDynamicMediaConfigServiceImpl.class); - assertTrue(underTest.enabled()); + registerNextGenDynamicMediaConfig(context); + NextGenDynamicMediaConfigService underTest = context.registerInjectActivateService(NextGenDynamicMediaConfigServiceImpl.class, + "enabledLocalAssets", true, + "localAssetsRepositoryId", "localrepo1"); + assertTrue(underTest.isEnabledRemoteAssets()); + assertTrue(underTest.isEnabledLocalAssets()); assertEquals("/selector1", underTest.getAssetSelectorsJsUrl()); assertEquals("/adobe/assets/{asset-id}/as/{seo-name}.{format}?accept-experimental=1", underTest.getImageDeliveryBasePath()); assertEquals("/videopath1", underTest.getVideoDeliveryPath()); assertEquals("/adobe/assets/{asset-id}/original/as/{seo-name}?accept-experimental=1", underTest.getAssetOriginalBinaryDeliveryPath()); assertEquals("/adobe/assets/{asset-id}/metadata", underTest.getAssetMetadataPath()); - assertEquals("repo1", underTest.getRepositoryId()); + assertEquals("repo1", underTest.getRemoteAssetsRepositoryId()); + assertEquals("localrepo1", underTest.getLocalAssetsRepositoryId()); assertEquals("key1", underTest.getApiKey()); assertEquals("env1", underTest.getEnv()); assertEquals("client1", underTest.getImsClient()); @@ -68,20 +59,56 @@ void testPropertiesDefaultConfig() { @Test void testPropertiesEmptyConfig() { + registerNextGenDynamicMediaConfig(context); NextGenDynamicMediaConfigService underTest = context.registerInjectActivateService(NextGenDynamicMediaConfigServiceImpl.class, + "enabledRemoteAssets", false, + "enabledLocalAssets", false, + "localAssetsRepositoryId", "", "imageDeliveryBasePath", "", "assetOriginalBinaryDeliveryPath", "", "assetMetadataPath", ""); - assertTrue(underTest.enabled()); + assertFalse(underTest.isEnabledRemoteAssets()); + assertFalse(underTest.isEnabledLocalAssets()); assertEquals("/selector1", underTest.getAssetSelectorsJsUrl()); assertEquals("/imagepath1", underTest.getImageDeliveryBasePath()); assertEquals("/videopath1", underTest.getVideoDeliveryPath()); assertEquals("/assetpath1", underTest.getAssetOriginalBinaryDeliveryPath()); assertEquals("/metadatapath1", underTest.getAssetMetadataPath()); - assertEquals("repo1", underTest.getRepositoryId()); + assertEquals("repo1", underTest.getRemoteAssetsRepositoryId()); + assertEquals("", underTest.getLocalAssetsRepositoryId()); assertEquals("key1", underTest.getApiKey()); assertEquals("env1", underTest.getEnv()); assertEquals("client1", underTest.getImsClient()); } + @Test + void testNoNextGenDynamicMediaConfig() { + NextGenDynamicMediaConfigService underTest = context.registerInjectActivateService(NextGenDynamicMediaConfigServiceImpl.class); + assertFalse(underTest.isEnabledRemoteAssets()); + assertFalse(underTest.isEnabledLocalAssets()); + assertNull(underTest.getAssetSelectorsJsUrl()); + assertEquals("/adobe/assets/{asset-id}/as/{seo-name}.{format}?accept-experimental=1", underTest.getImageDeliveryBasePath()); + assertNull(underTest.getVideoDeliveryPath()); + assertEquals("/adobe/assets/{asset-id}/original/as/{seo-name}?accept-experimental=1", underTest.getAssetOriginalBinaryDeliveryPath()); + assertEquals("/adobe/assets/{asset-id}/metadata", underTest.getAssetMetadataPath()); + assertNull(underTest.getRemoteAssetsRepositoryId()); + assertNull(underTest.getApiKey()); + assertNull(underTest.getEnv()); + assertNull(underTest.getImsClient()); + } + + private static void registerNextGenDynamicMediaConfig(AemContext context) { + MockNextGenDynamicMediaConfig config = context.registerInjectActivateService(MockNextGenDynamicMediaConfig.class); + config.setEnabled(true); + config.setAssetSelectorsJsUrl("/selector1"); + config.setImageDeliveryBasePath("/imagepath1"); + config.setVideoDeliveryPath("/videopath1"); + config.setAssetOriginalBinaryDeliveryPath("/assetpath1"); + config.setAssetMetadataPath("/metadatapath1"); + config.setRepositoryId("repo1"); + config.setApiKey("key1"); + config.setEnv("env1"); + config.setImsClient("client1"); + } + } diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReferenceSample.java b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReferenceSample.java index e739242b..11be01b5 100644 --- a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReferenceSample.java +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReferenceSample.java @@ -24,7 +24,8 @@ */ public final class NextGenDynamicMediaReferenceSample { - public static final String SAMPLE_ASSET_ID = "urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678"; + public static final String SAMPLE_UUID = "12345678-abcd-abcd-abcd-abcd12345678"; + public static final String SAMPLE_ASSET_ID = "urn:aaid:aem:" + SAMPLE_UUID; public static final String SAMPLE_FILENAME = "my-image.jpg"; public static final String SAMPLE_REFERENCE = "/" + SAMPLE_ASSET_ID + "/" + SAMPLE_FILENAME; diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReferenceTest.java b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReferenceTest.java index 68faf849..366c4a29 100644 --- a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReferenceTest.java +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/NextGenDynamicMediaReferenceTest.java @@ -19,9 +19,12 @@ */ package io.wcm.handler.mediasource.ngdm.impl; +import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_APPROVED; +import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_PROPERTY; import static io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReferenceSample.SAMPLE_ASSET_ID; import static io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReferenceSample.SAMPLE_FILENAME; import static io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReferenceSample.SAMPLE_REFERENCE; +import static io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReferenceSample.SAMPLE_UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -29,10 +32,24 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.apache.sling.api.resource.ModifiableValueMap; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import com.day.cq.commons.jcr.JcrConstants; +import com.day.cq.dam.api.Asset; + +import io.wcm.handler.media.testcontext.AppAemContext; +import io.wcm.sling.commons.adapter.AdaptTo; +import io.wcm.testing.mock.aem.junit5.AemContext; +import io.wcm.testing.mock.aem.junit5.AemContextExtension; +import io.wcm.wcm.commons.contenttype.ContentType; + +@ExtendWith(AemContextExtension.class) class NextGenDynamicMediaReferenceTest { + private final AemContext context = AppAemContext.newAemContext(); + @Test void testFromReference() { NextGenDynamicMediaReference underTest = NextGenDynamicMediaReference.fromReference(SAMPLE_REFERENCE); @@ -40,6 +57,7 @@ void testFromReference() { assertEquals(SAMPLE_ASSET_ID, underTest.getAssetId()); assertEquals(SAMPLE_FILENAME, underTest.getFileName()); assertEquals(SAMPLE_REFERENCE, underTest.toReference()); + assertNull(underTest.getAsset()); assertEquals(SAMPLE_REFERENCE, underTest.toString()); } @@ -66,4 +84,42 @@ void testNewInstance_IllegalArgument() { () -> new NextGenDynamicMediaReference("wurstbrot", SAMPLE_FILENAME)); } + @Test + void testFromDamAssetReference_Invalid() { + assertNull(NextGenDynamicMediaReference.fromDamAssetReference(null, context.resourceResolver())); + assertNull(NextGenDynamicMediaReference.fromDamAssetReference("/invalid/path", context.resourceResolver())); + assertNull(NextGenDynamicMediaReference.fromDamAssetReference(context.create().resource("/content/no-asset").getPath(), + context.resourceResolver())); + } + + @Test + void testFromDamAssetReference_AssetWithoutUUID_Approved() { + Asset asset = context.create().asset("/content/dam/asset1.jpg", 10, 10, ContentType.JPEG, + ASSET_STATUS_PROPERTY, ASSET_STATUS_APPROVED); + assertNull(NextGenDynamicMediaReference.fromDamAssetReference(asset.getPath(), context.resourceResolver())); + } + + @Test + void testFromDamAssetReference_AssetWithUUID_NotApproved() { + Asset asset = context.create().asset("/content/dam/" + SAMPLE_FILENAME, 10, 10, ContentType.JPEG); + assertNull(NextGenDynamicMediaReference.fromDamAssetReference(asset.getPath(), context.resourceResolver())); + } + + @Test + void testFromDamAssetReference_AssetWithUUID_Approved() { + Asset asset = context.create().asset("/content/dam/" + SAMPLE_FILENAME, 10, 10, ContentType.JPEG, + ASSET_STATUS_PROPERTY, ASSET_STATUS_APPROVED); + ModifiableValueMap props = AdaptTo.notNull(asset, ModifiableValueMap.class); + props.put(JcrConstants.JCR_UUID, SAMPLE_UUID); + + NextGenDynamicMediaReference underTest = NextGenDynamicMediaReference.fromDamAssetReference(asset.getPath(), + context.resourceResolver()); + assertNotNull(underTest); + assertEquals(SAMPLE_ASSET_ID, underTest.getAssetId()); + assertEquals(SAMPLE_FILENAME, underTest.getFileName()); + assertEquals(SAMPLE_REFERENCE, underTest.toReference()); + assertEquals(asset, underTest.getAsset()); + assertEquals(SAMPLE_REFERENCE, underTest.toString()); + } + } diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataTest.java b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataTest.java index b49ff379..346736e9 100644 --- a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataTest.java +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataTest.java @@ -29,14 +29,22 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import com.day.cq.dam.api.Asset; import com.fasterxml.jackson.core.JsonProcessingException; import io.wcm.handler.media.Dimension; +import io.wcm.handler.media.testcontext.AppAemContext; +import io.wcm.testing.mock.aem.junit5.AemContext; +import io.wcm.testing.mock.aem.junit5.AemContextExtension; import io.wcm.wcm.commons.contenttype.ContentType; +@ExtendWith(AemContextExtension.class) class NextGenDynamicMediaMetadataTest { + private final AemContext context = AppAemContext.newAemContext(); + @Test void testEmptyJson() throws JsonProcessingException { NextGenDynamicMediaMetadata metadata = NextGenDynamicMediaMetadata.fromJson("{}"); @@ -49,7 +57,7 @@ void testEmptyJson() throws JsonProcessingException { @Test void testSampleJson_Image() throws JsonProcessingException { NextGenDynamicMediaMetadata metadata = NextGenDynamicMediaMetadata.fromJson(METADATA_JSON_IMAGE); - assertEquals("image/jpeg", metadata.getMimeType()); + assertEquals(ContentType.JPEG, metadata.getMimeType()); Dimension dimension = metadata.getDimension(); assertNotNull(dimension); assertEquals(1200, dimension.getWidth()); @@ -61,7 +69,7 @@ void testSampleJson_Image() throws JsonProcessingException { @Test void testSampleJson_PDF() throws JsonProcessingException { NextGenDynamicMediaMetadata metadata = NextGenDynamicMediaMetadata.fromJson(METADATA_JSON_PDF); - assertEquals("application/pdf", metadata.getMimeType()); + assertEquals(ContentType.PDF, metadata.getMimeType()); assertNull(metadata.getDimension()); assertTrue(metadata.isValid()); assertEquals("[dimension=,mimeType=application/pdf]", metadata.toString()); @@ -72,4 +80,17 @@ void testInvalidJson() { assertThrows(JsonProcessingException.class, () -> NextGenDynamicMediaMetadata.fromJson("no json")); } + @Test + void testFromAsset() { + Asset asset = context.create().asset("/content/dam/sample.jpg", 100, 50, ContentType.JPEG); + NextGenDynamicMediaMetadata metadata = NextGenDynamicMediaMetadata.fromAsset(asset); + + assertEquals(ContentType.JPEG, metadata.getMimeType()); + Dimension dimension = metadata.getDimension(); + assertNotNull(dimension); + assertEquals(100, dimension.getWidth()); + assertEquals(50, dimension.getHeight()); + assertTrue(metadata.isValid()); + } + }