diff --git a/changes.xml b/changes.xml index f4ef9c51..a68e9b46 100644 --- a/changes.xml +++ b/changes.xml @@ -23,6 +23,24 @@ xsi:schemaLocation="http://maven.apache.org/changes/1.0.0 http://maven.apache.org/plugins/maven-changes-plugin/xsd/changes-1.0.0.xsd"> + + + Add new GraniteUI validator 'wcmio.handler.media.mediaFormat' which can be set on Media Handler File Upload or Path Field components to hook the media format validation into the GraniteUI dialog validation. + + + Dynamic Media with Open API: Use remote metadata call to validate and get metadata for local assets as well. + + + Dynamic Media with Open API: Optional IMS Authentication for metadata requests to get full asset metadata. + + + Dynamic Media with OpenAPI: Respect Image Dimension from SVG asset metadata. + + + GraniteUI validator: Use data-foundation-validation instead of data-validation HTML attribute, the latter is deprecated. + + + Dynamic Media with Open API: Correctly calculate rendition width/height based on requested dimension or original dimension. diff --git a/pom.xml b/pom.xml index a1df8820..268881ae 100644 --- a/pom.xml +++ b/pom.xml @@ -25,13 +25,13 @@ io.wcm io.wcm.parent_toplevel - 2.3.2 + 2.3.4 io.wcm io.wcm.handler.media - 2.2.2 + 2.3.0 jar Media Handler @@ -49,7 +49,7 @@ handler/media - 2024-09-16T09:04:34Z + 2024-12-10T08:03:27Z @@ -144,7 +144,7 @@ io.wcm io.wcm.testing.aem-mock.junit5 - 5.6.0 + 5.6.2 test @@ -204,18 +204,19 @@ com.jayway.jsonpath json-path - 2.8.0 + 2.9.0 test com.jayway.jsonpath json-path-assert - 2.8.0 + 2.9.0 test org.wiremock wiremock + 3.4.0 test @@ -224,20 +225,20 @@ com.twelvemonkeys.imageio imageio-tiff - 3.10.1 + 3.12.0 test com.twelvemonkeys.imageio imageio-batik - 3.10.1 + 3.12.0 test org.apache.xmlgraphics batik-transcoder - 1.17 + 1.18 test 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 60a9a143..339ceb97 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaAsset.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaAsset.java @@ -19,6 +19,9 @@ */ package io.wcm.handler.mediasource.ngdm; +import static com.day.cq.dam.api.DamConstants.DC_DESCRIPTION; +import static com.day.cq.dam.api.DamConstants.DC_TITLE; + import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.sling.api.resource.Resource; @@ -32,6 +35,7 @@ import io.wcm.handler.media.UriTemplate; import io.wcm.handler.media.UriTemplateType; import io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaContext; +import io.wcm.handler.mediasource.ngdm.impl.metadata.NextGenDynamicMediaMetadata; /** * {@link Asset} implementation for Next Gen. Dynamic Media remote assets. @@ -39,30 +43,42 @@ final class NextGenDynamicMediaAsset implements Asset { private final NextGenDynamicMediaContext context; + private final MediaArgs defaultMediaArgs; + private final ValueMap properties; NextGenDynamicMediaAsset(@NotNull NextGenDynamicMediaContext context) { this.context = context; + this.defaultMediaArgs = context.getDefaultMediaArgs(); + + NextGenDynamicMediaMetadata metadata = context.getMetadata(); + if (metadata != null) { + this.properties = metadata.getProperties(); + } + else { + this.properties = ValueMap.EMPTY; + } } @Override public @Nullable String getTitle() { - return context.getReference().getFileName(); + return StringUtils.defaultString(properties.get(DC_TITLE, String.class), + context.getReference().getFileName()); } @Override public @Nullable String getAltText() { - if (context.getDefaultMediaArgs().isDecorative()) { + if (defaultMediaArgs.isDecorative()) { return ""; } - else { - return context.getDefaultMediaArgs().getAltText(); + if (!defaultMediaArgs.isForceAltValueFromAsset() && StringUtils.isNotEmpty(defaultMediaArgs.getAltText())) { + return defaultMediaArgs.getAltText(); } + return StringUtils.defaultString(getDescription(), getTitle()); } @Override public @Nullable String getDescription() { - // not supported - return null; + return properties.get(DC_DESCRIPTION, String.class); } @Override @@ -72,12 +88,12 @@ final class NextGenDynamicMediaAsset implements Asset { @Override public @NotNull ValueMap getProperties() { - return ValueMap.EMPTY; + return properties; } @Override public @Nullable Rendition getDefaultRendition() { - return getRendition(this.context.getDefaultMediaArgs()); + return getRendition(defaultMediaArgs); } @Override 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 99a733ea..ec7da40d 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaMediaSource.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaMediaSource.java @@ -39,7 +39,6 @@ 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; @@ -136,11 +135,7 @@ private boolean isDamAssetReference(@Nullable String mediaRef) { // If enabled: Fetch asset metadata to validate existence and get original dimensions NextGenDynamicMediaMetadata metadata = null; - Asset localAsset = reference.getAsset(); - if (localAsset != null) { - metadata = getMetadataFromAsset(localAsset); - } - else if (metadataService != null && metadataService.isEnabled()) { + if (metadataService != null && metadataService.isEnabled()) { metadata = metadataService.fetchMetadata(reference); if (metadata == null) { media.setMediaInvalidReason(MediaInvalidReason.MEDIA_REFERENCE_INVALID); @@ -194,14 +189,6 @@ else if (nextGenDynamicMediaConfig.isEnabledLocalAssets() && isDamAssetReference 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 18a16d0b..10ae223c 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaRendition.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMediaRendition.java @@ -96,7 +96,13 @@ final class NextGenDynamicMediaRendition implements Rendition { this.fileExtension = FilenameUtils.getExtension(reference.getFileName()); } - if (isVectorImage() || !isImage() || mediaArgs.isDownload()) { + if (!isImage() || mediaArgs.isDownload()) { + // deliver as binary + this.url = buildBinaryUrl(); + } + else if (isVectorImage()) { + // calculate width/height for rendition metadata + calculateWidthHeight(); // deliver as binary this.url = buildBinaryUrl(); } 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 13eec568..5691b2b8 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 @@ -91,6 +91,13 @@ public NextGenDynamicMediaReference(@NotNull String assetId, @NotNull String fil return asset; } + /** + * @return True if reference points to local asset. + */ + public boolean isLocal() { + return asset != null; + } + /** * @return Reference */ diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/AccessTokenResponse.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/AccessTokenResponse.java new file mode 100644 index 00000000..a63c9aa5 --- /dev/null +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/AccessTokenResponse.java @@ -0,0 +1,44 @@ +/* + * #%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.mediasource.ngdm.impl.metadata; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Used for Jackson Object mapping of JSON response from IMS Token v3 API. + */ +@SuppressWarnings({ "checkstyle:VisibilityModifierCheck", "java:S1104" }) +@SuppressFBWarnings("UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD") +@JsonIgnoreProperties(ignoreUnknown = true) +final class AccessTokenResponse { + + @JsonProperty("access_token") + public String accessToken; + + @JsonProperty("token_type") + public String tokenType; + + @JsonProperty("expires_in") + public long expiresInSec; + +} diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/ImsAccessTokenCache.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/ImsAccessTokenCache.java new file mode 100644 index 00000000..839c14c9 --- /dev/null +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/ImsAccessTokenCache.java @@ -0,0 +1,135 @@ +/* + * #%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.mediasource.ngdm.impl.metadata; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Expiry; + +/** + * Manages IMS access tokens with expiration handling. + */ +class ImsAccessTokenCache { + + private static final long EXPERIATION_BUFFER_SEC = 5; + + // cache IMS access tokens until they expire + private final Cache tokenCache = Caffeine.newBuilder() + .expireAfter(new Expiry() { + @Override + public long expireAfterCreate(String key, AccessTokenResponse value, long currentTime) { + // substract a few secs from expiration time to be on the safe side + return TimeUnit.SECONDS.toNanos(value.expiresInSec - EXPERIATION_BUFFER_SEC); + } + @Override + public long expireAfterUpdate(String key, AccessTokenResponse value, long currentTime, long currentDuration) { + // not used + return Long.MAX_VALUE; + } + @Override + public long expireAfterRead(String key, AccessTokenResponse value, long currentTime, long currentDuration) { + // not used + return Long.MAX_VALUE; + } + }) + .build(); + + private static final JsonMapper OBJECT_MAPPER = new JsonMapper(); + private static final Logger log = LoggerFactory.getLogger(ImsAccessTokenCache.class); + + private final CloseableHttpClient httpClient; + private final String imsTokenApiUrl; + + ImsAccessTokenCache(@NotNull CloseableHttpClient httpClient, @NotNull String imsTokenApiUrl) { + this.httpClient = httpClient; + this.imsTokenApiUrl = imsTokenApiUrl; + } + + /** + * Get IMS OAuth access token + * @param clientId Client ID + * @param clientSecret Client Secret + * @param scope Scope + * @return Access token or null if access token could not be obtained + */ + public @Nullable String getAccessToken(@NotNull String clientId, @NotNull String clientSecret, @NotNull String scope) { + String key = clientId + "::" + scope; + AccessTokenResponse accessTokenResponse = tokenCache.get(key, k -> createAccessToken(clientId, clientSecret, scope)); + if (accessTokenResponse != null) { + return accessTokenResponse.accessToken; + } + return null; + } + + private @Nullable AccessTokenResponse createAccessToken(@NotNull String clientId, @NotNull String clientSecret, @NotNull String scope) { + List formData = new ArrayList<>(); + formData.add(new BasicNameValuePair("grant_type", "client_credentials")); + formData.add(new BasicNameValuePair("client_id", clientId)); + formData.add(new BasicNameValuePair("client_secret", clientSecret)); + formData.add(new BasicNameValuePair("scope", scope)); + + HttpPost httpPost = new HttpPost(imsTokenApiUrl); + httpPost.setEntity(new UrlEncodedFormEntity(formData, StandardCharsets.UTF_8)); + + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + return processResponse(response); + } + catch (IOException ex) { + log.warn("Unable to obtain access token from URL {}", imsTokenApiUrl, ex); + return null; + } + } + + @SuppressWarnings("null") + private @Nullable AccessTokenResponse processResponse(@NotNull CloseableHttpResponse response) throws IOException { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + String jsonResponse = EntityUtils.toString(response.getEntity()); + AccessTokenResponse accessTokenResponse = OBJECT_MAPPER.readValue(jsonResponse, AccessTokenResponse.class); + log.trace("HTTP response for access token reqeust from {} returned a response, expires in {} sec", + imsTokenApiUrl, accessTokenResponse.expiresInSec); + return accessTokenResponse; + } + else { + log.warn("Unexpected HTTP response for access token request from {}: {}", imsTokenApiUrl, response.getStatusLine()); + return null; + } + } + +} diff --git a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/MetadataResponse.java b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/MetadataResponse.java index 50f8398e..4590eb41 100644 --- a/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/MetadataResponse.java +++ b/src/main/java/io/wcm/handler/mediasource/ngdm/impl/metadata/MetadataResponse.java @@ -35,7 +35,7 @@ final class MetadataResponse { public RepositoryMetadata repositoryMetadata; - public AssetMetadata assetMetadata; + public Map assetMetadata; @JsonIgnoreProperties(ignoreUnknown = true) static final class RepositoryMetadata { @@ -53,14 +53,4 @@ static final class SmartCrop { public double normalizedHeight; } - @JsonIgnoreProperties(ignoreUnknown = true) - static final class AssetMetadata { - @JsonProperty("dam:assetStatus") - public String assetStatus; - @JsonProperty("tiff:ImageWidth") - public long tiffImageWidth; - @JsonProperty("tiff:ImageLength") - public long tiffImageLength; - } - } 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 91018228..c4070083 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 @@ -19,39 +19,30 @@ */ package io.wcm.handler.mediasource.ngdm.impl.metadata; -import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT; -import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_PROPERTY; -import static com.day.cq.dam.api.DamConstants.RENDITIONS_FOLDER; -import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_LEFT; -import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_NORMALIZED_HEIGHT; -import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_NORMALIZED_WIDTH; -import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_TOP; +import static com.day.cq.dam.api.DamConstants.TIFF_IMAGELENGTH; +import static com.day.cq.dam.api.DamConstants.TIFF_IMAGEWIDTH; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.TreeMap; import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.api.wrappers.ValueMapDecorator; 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.handler.mediasource.ngdm.impl.metadata.MetadataResponse.AssetMetadata; import io.wcm.handler.mediasource.ngdm.impl.metadata.MetadataResponse.RepositoryMetadata; import io.wcm.wcm.commons.contenttype.ContentType; +import io.wcm.wcm.commons.util.ToStringStyle; /** * Metadata for Next Gen Dynamic Media asset fetched from the HTTP API. @@ -61,16 +52,23 @@ public final class NextGenDynamicMediaMetadata { private final String mimeType; private final Dimension dimension; private final String assetStatus; + private final ValueMap properties; private final List smartCrops; private static final JsonMapper OBJECT_MAPPER = new JsonMapper(); static final String RT_RENDITION_SMARTCROP = "dam/rendition/smartcrop"; NextGenDynamicMediaMetadata(@Nullable String mimeType, @Nullable Dimension dimension, - @Nullable String assetStatus, @Nullable List smartCrops) { + @Nullable String assetStatus, @Nullable ValueMap properties, @Nullable List smartCrops) { this.mimeType = mimeType; this.dimension = dimension; this.assetStatus = assetStatus; + if (properties != null) { + this.properties = properties; + } + else { + this.properties = ValueMap.EMPTY; + } if (smartCrops != null) { this.smartCrops = smartCrops; } @@ -100,6 +98,13 @@ public String getAssetStatus() { return this.assetStatus; } + /** + * @return Asset properties + */ + public ValueMap getProperties() { + return properties; + } + /** * @return Named smart crop definitions. */ @@ -116,7 +121,13 @@ public boolean isValid() { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE); + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_OMIT_NULL_STYLE) + .append("mimeType", mimeType) + .append("dimension", dimension) + .append("assetStatus", assetStatus) + .append("properties", properties.isEmpty() ? null : new TreeMap(properties)) + .append("smartCrops", smartCrops.isEmpty() ? null : smartCrops) + .toString(); } /** @@ -129,15 +140,17 @@ public String toString() { public static @NotNull NextGenDynamicMediaMetadata fromJson(@NotNull String jsonResponse) throws JsonProcessingException { MetadataResponse response = OBJECT_MAPPER.readValue(jsonResponse, MetadataResponse.class); RepositoryMetadata respositoryMetadata = response.repositoryMetadata; - AssetMetadata assetMetadata = response.assetMetadata; + Map assetMetadata = response.assetMetadata; + ValueMap properties = null; long width = 0; long height = 0; String assetStatus = null; if (assetMetadata != null) { - width = assetMetadata.tiffImageWidth; - height = assetMetadata.tiffImageLength; - assetStatus = assetMetadata.assetStatus; + properties = new ValueMapDecorator(assetMetadata); + width = properties.get(TIFF_IMAGEWIDTH, 0L); + height = properties.get(TIFF_IMAGELENGTH, 0L); + assetStatus = properties.get("dam:assetStatus", String.class); } Dimension dimension = toDimension(width, height); @@ -153,7 +166,7 @@ public String toString() { } } - return new NextGenDynamicMediaMetadata(mimeType, dimension, assetStatus, smartCrops); + return new NextGenDynamicMediaMetadata(mimeType, dimension, assetStatus, properties, smartCrops); } private static @Nullable Dimension toDimension(long width, long height) { @@ -163,42 +176,6 @@ public String toString() { return null; } - /** - * Gets metadata from DAM asset. - * @param asset Asset - * @return Metadata object - */ - @SuppressWarnings("null") - public static @NotNull NextGenDynamicMediaMetadata fromAsset(@NotNull Asset asset) { - String mimeType = asset.getMimeType(); - - Dimension dimension = AssetRendition.getDimension(asset.getOriginal()); - String assetStatus = asset.getMetadataValueFromJcr(ASSET_STATUS_PROPERTY); - List smartCrops = null; - - if (dimension != null) { - smartCrops = getRenditionResources(asset) - .filter(rendition -> rendition.isResourceType(RT_RENDITION_SMARTCROP)) - .map(rendition -> Map.entry(rendition.getName(), renditionToSmartCropDefinition(rendition))) - .filter(entry -> isSmartCropDefinitionValid(entry.getKey(), entry.getValue())) - .map(entry -> new SmartCrop(entry.getKey(), entry.getValue(), dimension)) - .collect(Collectors.toList()); - } - - return new NextGenDynamicMediaMetadata(mimeType, dimension, assetStatus, smartCrops); - } - - private static Stream getRenditionResources(@NotNull Asset asset) { - Resource assetResource = asset.adaptTo(Resource.class); - if (assetResource != null) { - Resource renditionsFolder = assetResource.getChild(JCR_CONTENT + "/" + RENDITIONS_FOLDER); - if (renditionsFolder != null) { - return StreamSupport.stream(renditionsFolder.getChildren().spliterator(), false); - } - } - return Stream.empty(); - } - private static boolean isSmartCropDefinitionValid(@NotNull String name, @NotNull MetadataResponse.SmartCrop smartCop) { return StringUtils.isNotBlank(name) && smartCop.normalizedWidth > 0 @@ -207,17 +184,4 @@ private static boolean isSmartCropDefinitionValid(@NotNull String name, @NotNull && smartCop.top >= 0; } - private static @NotNull MetadataResponse.SmartCrop renditionToSmartCropDefinition(Resource rendition) { - MetadataResponse.SmartCrop result = new MetadataResponse.SmartCrop(); - Resource content = rendition.getChild(JCR_CONTENT); - if (content != null) { - ValueMap props = content.getValueMap(); - result.left = props.get(PN_LEFT, 0d); - result.top = props.get(PN_TOP, 0d); - result.normalizedWidth = props.get(PN_NORMALIZED_WIDTH, 0d); - result.normalizedHeight = props.get(PN_NORMALIZED_HEIGHT, 0d); - } - return result; - } - } 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 111484e9..56896cf8 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 @@ -99,6 +99,26 @@ public class NextGenDynamicMediaMetadataServiceImpl implements NextGenDynamicMed description = "Proxy port") int proxyPort(); + @AttributeDefinition( + name = "IMS Token API URL", + description = "API to obtain IMS access token for obtaining full metadata.") + String imsTokenApiUrl() default "https://ims-na1.adobelogin.com/ims/token/v3"; + + @AttributeDefinition( + name = "IMS OAuth Client ID", + description = "Optional: If you want to fetch the full metadata for assets, provide the IMS OAuth Client ID.") + String authenticationClientId(); + + @AttributeDefinition( + name = "IMS OAuth Client Secret", + description = "Optional: If you want to fetch the full metadata for assets, provide the IMS OAuth Client Secret.") + String authenticationClientSecret(); + + @AttributeDefinition( + name = "IMS OAuth Scope", + description = "OAuth Scope to use for obtaining IMS access token.") + String authenticationScope() default "openid,AdobeID,read_organizations,additional_info.projectedProductContext,read_pc.dma_aem_ams"; + } @Reference @@ -107,6 +127,11 @@ public class NextGenDynamicMediaMetadataServiceImpl implements NextGenDynamicMed private boolean enabled; private CloseableHttpClient httpClient; + private ImsAccessTokenCache imsAccessTokenCache; + private String authenticationClientId; + private String authenticationClientSecret; + private String authenticationScope; + private static final Logger log = LoggerFactory.getLogger(NextGenDynamicMediaMetadataServiceImpl.class); @Activate @@ -114,6 +139,26 @@ private void activate(Config config) { this.enabled = config.enabled(); if (enabled) { httpClient = createHttpClient(config); + + // if configured, enable IMS access token fetching + String imsTokenApiUrl = config.imsTokenApiUrl(); + authenticationClientId = config.authenticationClientId(); + authenticationClientSecret = config.authenticationClientSecret(); + authenticationScope = config.authenticationScope(); + if (log.isTraceEnabled()) { + log.trace("Authentication configuration: imsTokenApiUrl={}, authenticationClientId={}, authenticationClientSecret={}, authenticationScope={}", + StringUtils.defaultString(imsTokenApiUrl), + StringUtils.isNotBlank(authenticationClientId) ? "***" : "", + StringUtils.isNotBlank(authenticationClientSecret) ? "***" : "", + StringUtils.defaultString(authenticationScope)); + } + if (StringUtils.isNoneBlank(imsTokenApiUrl, authenticationClientId, authenticationClientSecret, authenticationScope)) { + log.debug("Enable IMS access token fetching for NGDM asset metadata."); + imsAccessTokenCache = new ImsAccessTokenCache(httpClient, config.imsTokenApiUrl()); + } + else { + log.debug("IMS access token fetching for NGDM asset metadata is disabled."); + } } } @@ -147,6 +192,7 @@ private static Collection
convertHeaders(String[] headers) { private void deactivate() throws IOException { if (httpClient != null) { httpClient.close(); + imsAccessTokenCache = null; } } @@ -171,6 +217,15 @@ public boolean isEnabled() { } HttpGet httpGet = new HttpGet(metadataUrl); + + // add IMS access if configured + if (imsAccessTokenCache != null) { + String accessToken = imsAccessTokenCache.getAccessToken(authenticationClientId, authenticationClientSecret, authenticationScope); + if (accessToken != null) { + httpGet.addHeader("Authorization", "Bearer " + accessToken); + } + } + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { return processResponse(response, metadataUrl); } 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 2492d4d4..1a1f23fe 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,13 @@ final class NextGenDynamicMediaMetadataUrlBuilder { public @Nullable String build(@NotNull NextGenDynamicMediaReference reference) { // get parameters from nextgen dynamic media config for URL parameters - String repositoryId = config.getRemoteAssetsRepositoryId(); + String repositoryId; + if (reference.isLocal()) { + repositoryId = config.getLocalAssetsRepositoryId(); + } + else { + repositoryId = config.getRemoteAssetsRepositoryId(); + } String metadataPath = config.getAssetMetadataPath(); if (StringUtils.isAnyEmpty(repositoryId, metadataPath)) { return null; diff --git a/src/main/webapp/app-root/clientlibs/authoring/dialog/js/mediaFormatValidate.js b/src/main/webapp/app-root/clientlibs/authoring/dialog/js/mediaFormatValidate.js index 5ef402e9..475d6123 100644 --- a/src/main/webapp/app-root/clientlibs/authoring/dialog/js/mediaFormatValidate.js +++ b/src/main/webapp/app-root/clientlibs/authoring/dialog/js/mediaFormatValidate.js @@ -80,6 +80,7 @@ self._alert = alert; self._$pathfield.after(self._alert); + self._checkValidity(); } /** @@ -91,6 +92,16 @@ $(self._alert).remove(); delete self._alert; } + self._checkValidity(); + } + + /** + * Check validity of form control. + */ + MediaFormatValidate.prototype._checkValidity = function () { + var self = this; + var validation = self._$pathfield.adaptTo("foundation-validation"); + validation.checkValidity(); } /** diff --git a/src/main/webapp/app-root/clientlibs/authoring/dialog/js/validation.js b/src/main/webapp/app-root/clientlibs/authoring/dialog/js/validation.js index 3fbe07a4..30457ef3 100644 --- a/src/main/webapp/app-root/clientlibs/authoring/dialog/js/validation.js +++ b/src/main/webapp/app-root/clientlibs/authoring/dialog/js/validation.js @@ -42,7 +42,7 @@ // predefined "responsiveWidths" pattern validator foundationValidator.register('foundation.validation.validator', { - selector: '[data-validation="wcmio.handler.media.responsiveWidths"]', + selector: '[data-foundation-validation="wcmio.handler.media.responsiveWidths"]', validate: function(el) { var value = getValue(el); var valid = value.length === 0 || pattern.responsiveWidths.test(value); @@ -52,4 +52,15 @@ } }); + // validates valid media format for asset picker + foundationValidator.register('foundation.validation.validator', { + selector: '[data-foundation-validation="wcmio.handler.media.mediaFormat"]', + validate: function(el) { + var nextSibling = el.nextSibling; + if (nextSibling?.tagName?.toLowerCase() === 'coral-alert') { + return nextSibling.textContent; + } + } + }); + })(document, Granite, Granite.$); diff --git a/src/main/webapp/app-root/components/granite/form/fileupload/fileupload.jsp b/src/main/webapp/app-root/components/granite/form/fileupload/fileupload.jsp index 6f5f7530..81eda48a 100644 --- a/src/main/webapp/app-root/components/granite/form/fileupload/fileupload.jsp +++ b/src/main/webapp/app-root/components/granite/form/fileupload/fileupload.jsp @@ -207,6 +207,9 @@ pathFieldProps.put("required", cfg.get("required", false)); // detect root path pathFieldProps.putAll(getDamRootPathProperties(cmp, slingRequest, "/content/dam")); +// pass through validation configuration to path field +pathFieldProps.put("validation", cfg.get("validation", String.class)); + Resource pathField = GraniteUiSyntheticResource.child(fileUpload, "pathfield" , "wcm-io/wcm/ui/granite/components/form/pathfield", new ValueMapDecorator(pathFieldProps)); Map dataProps = new HashMap<>(); diff --git a/src/site/markdown/dynamic-media-openapi.md b/src/site/markdown/dynamic-media-openapi.md index 89dac29e..fe32e130 100644 --- a/src/site/markdown/dynamic-media-openapi.md +++ b/src/site/markdown/dynamic-media-openapi.md @@ -105,8 +105,36 @@ If you want to enable support for local assets you also have to configure a repo 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. +#### Metadata Service + The "wcm.io Dynamic Media with OpenAPI Metadata Service" allows to enable the Asset Metadata support for validation and Smart Cropping. The metadata service is enabled by default. If enabled, for each resolved remote asset a HTTP request is send from the server to the DM API to fetch the asset's metadata. +By default, Dynamic Media with OpenAPI provides only minimal metadata for each asset (dimensions, mime type). If you want access to full metadata (e.g. title, description and other properties from the asset metadata in AEM), you need to configure an IMS authentication. With that configured, the Media Handler sends an authentication token with each metadata call, which returns the full metadata and exposes it via the Media Handler API. + +To enable IMS authentication for the metadata service: + +1. In the [Adobe Developer Console][adobe-developer-console] create a new *empty* project +2. Rename the project to a sensible name +3. Add an API connection to the project with connection to "Cloud Manager" + * Use "OAuth Server-to-Server" credentials + * Ensure the name for the credentials created is not too long (shorten it if the auto-generated name is longer than 45 chars) + * Use a minimum set of product profiles, e.g. "Integrations" +4. In the [Adobe Admin Console][adobe-admin-console] go to projects, and manage "Adobe Experience Manager as a Cloud Service" +5. Choose the instance(s) for the author environment(s) that hosts the assets to be accessed +6. Create a new profile with minimal access, or re-use an existing profile e.g. the "AEM Users" profile, and add a new entry for "API credentials". Pick the credential entry you created in step 3. +7. Define two new environment variables (_secret_) in the AEM Sites instances which is using the assets (you can use different names for the variables than shown here): + * ASSET_DELIVERY_METADATA_AUTH_CLIENT_ID: Copy the value "Client ID" in Adobe Developer Console from the details view of the credentials created for the API + * ASSET_DELIVERY_METADATA_AUTH_CLIENT_SECRET: From the same screen, use the button "Retrieve Client Secret" to get the secret value +8. Apply an OSGi configuration for the "wcm.io Dynamic Media with OpenAPI Metadata Service" like this: + ``` + { + "enabled": true, + "authenticationClientId": "$[secret:ASSET_DELIVERY_METADATA_AUTH_CLIENT_ID;default=]", + "authenticationClientSecret": "$[secret:ASSET_DELIVERY_METADATA_AUTH_CLIENT_SECRET;default=]" + } + ``` + + ### Known Limitations (as of July 2024) @@ -122,3 +150,5 @@ The "wcm.io Dynamic Media with OpenAPI Metadata Service" allows to enable the As [file-format-support]: file-format-support.html [wcm-core-components]: https://wcm.io/wcm/core-components/ [aem-image-profiles]: https://experienceleague.adobe.com/docs/experience-manager-65/assets/dynamic/image-profiles.html +[adobe-developer-console]: https://developer.adobe.com/console +[adobe-admin-console]: https://adminconsole.adobe.com/ diff --git a/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaLocalAssetTest.java b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaLocalAssetTest.java deleted file mode 100644 index 5d7269a5..00000000 --- a/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndNextGenDynamicMediaLocalAssetTest.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * #%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?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?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?height=40&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?crop=25%2C0%2C50%2C50&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?crop=25%2C0%2C50%2C50&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?crop=25%2C0%2C50%2C50&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?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?height=40&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?crop=25%2C0%2C50%2C50&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?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?height=40&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?crop=25%2C0%2C50%2C50&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?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?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?height=40&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?crop=25%2C0%2C50%2C50&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", - 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?attachment=true", - 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", - ContentType.SVG); - } - - @Override - @Test - @Disabled("Not supported with NGDM") - void testAsset_SVG_AutoCrop() { - // disabled - } - -} diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_LocalAssetWithMetadataTest.java b/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_LocalAssetWithMetadataTest.java index f570e4ac..f0d92736 100644 --- a/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_LocalAssetWithMetadataTest.java +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_LocalAssetWithMetadataTest.java @@ -21,16 +21,21 @@ import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_APPROVED; import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_PROPERTY; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 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 io.wcm.handler.mediasource.ngdm.impl.metadata.MetadataSample.METADATA_JSON_IMAGE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.apache.http.HttpStatus; import org.apache.sling.api.resource.ModifiableValueMap; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ValueMap; @@ -39,6 +44,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import com.day.cq.commons.jcr.JcrConstants; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; import io.wcm.handler.media.Media; import io.wcm.handler.media.MediaHandler; @@ -50,29 +57,50 @@ import io.wcm.handler.media.testcontext.AppAemContext; import io.wcm.handler.media.testcontext.DummyMediaFormats; import io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaConfigServiceImpl; +import io.wcm.handler.mediasource.ngdm.impl.metadata.NextGenDynamicMediaMetadataServiceImpl; import io.wcm.sling.commons.adapter.AdaptTo; +import io.wcm.testing.mock.aem.dam.ngdm.MockNextGenDynamicMediaConfig; 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) +@WireMockTest class NextGenDynamicMedia_LocalAssetWithMetadataTest { + private static final String NOT_FOUND_ASSET_UUID = "99999999-abcd-abcd-abcd-abcd99999999"; + private final AemContext context = AppAemContext.newAemContext(); + private MockNextGenDynamicMediaConfig nextGenDynamicMediaConfig; + private NextGenDynamicMediaConfigServiceImpl nextGenDynamicMediaConfigService; private MediaHandler mediaHandler; private Resource resource; @BeforeEach @SuppressWarnings("null") - void setUp() { - context.registerInjectActivateService(NextGenDynamicMediaConfigServiceImpl.class, + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + nextGenDynamicMediaConfig = context.registerInjectActivateService(MockNextGenDynamicMediaConfig.class); + nextGenDynamicMediaConfig.setEnabled(true); + nextGenDynamicMediaConfig.setRepositoryId("remoterepo1"); + nextGenDynamicMediaConfigService = context.registerInjectActivateService(NextGenDynamicMediaConfigServiceImpl.class, "enabledLocalAssets", true, - "localAssetsRepositoryId", "repo1"); + "localAssetsRepositoryId", "localhost:" + wmRuntimeInfo.getHttpPort()); + context.registerInjectActivateService(NextGenDynamicMediaMetadataServiceImpl.class, + "enabled", true); resource = context.create().resource(context.currentPage(), "test", MediaNameConstants.PN_MEDIA_REF, SAMPLE_REFERENCE); mediaHandler = AdaptTo.notNull(context.request(), MediaHandler.class); + + stubFor(get("/adobe/assets/" + SAMPLE_ASSET_ID + "/metadata") + .willReturn(aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("Content-Type", ContentType.JSON) + .withBody(METADATA_JSON_IMAGE))); + stubFor(get("/adobe/assets/urn:aaid:aem:" + NOT_FOUND_ASSET_UUID + "/metadata") + .willReturn(aResponse() + .withStatus(HttpStatus.SC_NOT_FOUND))); } @Test @@ -87,14 +115,16 @@ void testLocalAsset() { Rendition rendition = media.getRendition(); UriTemplate uriTemplateScaleWidth = rendition.getUriTemplate(UriTemplateType.SCALE_WIDTH); - assertEquals("https://repo1/adobe/assets/" + SAMPLE_ASSET_ID + "/as/my-image.jpg?preferwebp=true&quality=85&width={width}", + assertEquals("https://" + nextGenDynamicMediaConfigService.getLocalAssetsRepositoryId() + + "/adobe/assets/" + SAMPLE_ASSET_ID + "/as/my-image.jpg?preferwebp=true&quality=85&width={width}", uriTemplateScaleWidth.getUriTemplate()); assertEquals(UriTemplateType.SCALE_WIDTH, uriTemplateScaleWidth.getType()); assertEquals(1200, uriTemplateScaleWidth.getMaxWidth()); assertEquals(800, uriTemplateScaleWidth.getMaxHeight()); UriTemplate uriTemplateScaleHeight = rendition.getUriTemplate(UriTemplateType.SCALE_HEIGHT); - assertEquals("https://repo1/adobe/assets/" + SAMPLE_ASSET_ID + "/as/my-image.jpg?height={height}&preferwebp=true&quality=85", + assertEquals("https://" + nextGenDynamicMediaConfigService.getLocalAssetsRepositoryId() + + "/adobe/assets/" + SAMPLE_ASSET_ID + "/as/my-image.jpg?height={height}&preferwebp=true&quality=85", uriTemplateScaleHeight.getUriTemplate()); assertEquals(UriTemplateType.SCALE_HEIGHT, uriTemplateScaleHeight.getType()); assertEquals(1200, uriTemplateScaleHeight.getMaxWidth()); @@ -136,7 +166,7 @@ void testRendition_16_9() { .fixedWidth(1024) .build(); assertTrue(media.isValid()); - assertUrl(media, "crop=0%2C63%2C1200%2C675&preferwebp=true&quality=85&width=1024", "jpg"); + assertUrl(media, "preferwebp=true&quality=85&smartcrop=Landscape&width=1024", "jpg"); Rendition rendition = media.getRendition(); assertNotNull(rendition); @@ -162,10 +192,10 @@ void testRendition_16_9() { @Test @SuppressWarnings("null") - void testLocalAsset_NotApproved() { + void testLocalAsset_NonExistingUUID() { com.day.cq.dam.api.Asset asset = context.create().asset("/content/dam/my-image.jpg", 10, 10, ContentType.JPEG); ModifiableValueMap props = AdaptTo.notNull(asset, ModifiableValueMap.class); - props.put(JcrConstants.JCR_UUID, SAMPLE_UUID); + props.put(JcrConstants.JCR_UUID, NOT_FOUND_ASSET_UUID); resource = context.create().resource(context.currentPage(), "local-asset", MediaNameConstants.PN_MEDIA_REF, asset.getPath()); @@ -173,7 +203,7 @@ void testLocalAsset_NotApproved() { Media media = mediaHandler.get(resource) .build(); assertFalse(media.isValid()); - assertEquals(MediaInvalidReason.NOT_APPROVED, media.getMediaInvalidReason()); + assertEquals(MediaInvalidReason.MEDIA_REFERENCE_INVALID, media.getMediaInvalidReason()); } @Test @@ -191,12 +221,13 @@ void testLocalAsset_NoUUID() { assertEquals(MediaInvalidReason.MEDIA_REFERENCE_INVALID, media.getMediaInvalidReason()); } - private static void assertUrl(Media media, String urlParams, String extension) { + private void assertUrl(Media media, String urlParams, String extension) { assertEquals(buildUrl(urlParams, extension), media.getUrl()); } - private static String buildUrl(String urlParams, String extension) { - return "https://repo1/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/my-image." + private String buildUrl(String urlParams, String extension) { + return "https://" + nextGenDynamicMediaConfigService.getLocalAssetsRepositoryId() + + "/adobe/assets/urn:aaid:aem:12345678-abcd-abcd-abcd-abcd12345678/as/my-image." + extension + "?" + urlParams; } diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_RemoteAssetWithMetadataTest.java b/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_RemoteAssetWithMetadataTest.java index a1076417..a9238fd6 100644 --- a/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_RemoteAssetWithMetadataTest.java +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_RemoteAssetWithMetadataTest.java @@ -27,6 +27,7 @@ import static io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReferenceSample.SAMPLE_REFERENCE; import static io.wcm.handler.mediasource.ngdm.impl.metadata.MetadataSample.METADATA_JSON_IMAGE; import static io.wcm.handler.mediasource.ngdm.impl.metadata.MetadataSample.METADATA_JSON_PDF; +import static io.wcm.handler.mediasource.ngdm.impl.metadata.MetadataSample.METADATA_JSON_SVG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -102,10 +103,11 @@ void testAsset() { Asset asset = media.getAsset(); assertNotNull(asset); assertEquals(SAMPLE_FILENAME, asset.getTitle()); - assertNull(asset.getAltText()); + assertNull(asset.getDescription()); + assertEquals("my-image.jpg", asset.getAltText()); assertNull(asset.getDescription()); assertEquals(SAMPLE_REFERENCE, asset.getPath()); - assertEquals(ValueMap.EMPTY, asset.getProperties()); + assertEquals("approved", asset.getProperties().get("dam:assetStatus")); assertNull(asset.adaptTo(Resource.class)); assertUrl(asset.getDefaultRendition(), "preferwebp=true&quality=85", "jpg"); @@ -273,8 +275,32 @@ void testPDFDownload() { Rendition rendition = media.getRendition(); assertNotNull(rendition); assertEquals(ContentType.PDF, rendition.getMimeType()); - assertEquals( - "https://" + nextGenDynamicMediaConfig.getRepositoryId() + "/adobe/assets/" + SAMPLE_ASSET_ID + "/original/as/myfile.pdf", + assertEquals("https://" + nextGenDynamicMediaConfig.getRepositoryId() + "/adobe/assets/" + SAMPLE_ASSET_ID + "/original/as/myfile.pdf", + rendition.getUrl()); + } + + @Test + @SuppressWarnings("null") + void testSVG() { + stubFor(get("/adobe/assets/" + SAMPLE_ASSET_ID + "/metadata") + .willReturn(aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("Content-Type", ContentType.JSON) + .withBody(METADATA_JSON_SVG))); + + Resource downloadResource = context.create().resource(context.currentPage(), "image", + MediaNameConstants.PN_MEDIA_REF, "/" + SAMPLE_ASSET_ID + "/myfile.svg"); + + Media media = mediaHandler.get(downloadResource) + .build(); + assertTrue(media.isValid()); + + Rendition rendition = media.getRendition(); + assertNotNull(rendition); + assertEquals(ContentType.SVG, rendition.getMimeType()); + assertEquals(900, rendition.getWidth()); + assertEquals(600, rendition.getHeight()); + assertEquals("https://" + nextGenDynamicMediaConfig.getRepositoryId() + "/adobe/assets/" + SAMPLE_ASSET_ID + "/original/as/myfile.svg", rendition.getUrl()); } diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_RemoteAssetWithoutMetadataTest.java b/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_RemoteAssetWithoutMetadataTest.java index 3176cf29..c3825877 100644 --- a/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_RemoteAssetWithoutMetadataTest.java +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/NextGenDynamicMedia_RemoteAssetWithoutMetadataTest.java @@ -84,7 +84,7 @@ void testAsset() { Asset asset = media.getAsset(); assertNotNull(asset); assertEquals(SAMPLE_FILENAME, asset.getTitle()); - assertNull(asset.getAltText()); + assertEquals("my-image.jpg", asset.getAltText()); assertNull(asset.getDescription()); assertEquals(SAMPLE_REFERENCE, asset.getPath()); assertEquals(ValueMap.EMPTY, asset.getProperties()); 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 366c4a29..b130fe59 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 @@ -58,6 +58,7 @@ void testFromReference() { assertEquals(SAMPLE_FILENAME, underTest.getFileName()); assertEquals(SAMPLE_REFERENCE, underTest.toReference()); assertNull(underTest.getAsset()); + assertFalse(underTest.isLocal()); assertEquals(SAMPLE_REFERENCE, underTest.toString()); } @@ -119,6 +120,7 @@ void testFromDamAssetReference_AssetWithUUID_Approved() { assertEquals(SAMPLE_FILENAME, underTest.getFileName()); assertEquals(SAMPLE_REFERENCE, underTest.toReference()); assertEquals(asset, underTest.getAsset()); + assertTrue(underTest.isLocal()); assertEquals(SAMPLE_REFERENCE, underTest.toString()); } diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/MetadataSample.java b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/MetadataSample.java index 40f291ef..b8c95f36 100644 --- a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/MetadataSample.java +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/MetadataSample.java @@ -66,13 +66,39 @@ public final class MetadataSample { + " }," + " \"assetMetadata\": {" + " \"dam:assetStatus\": \"approved\"," - + " \"dc:description\": \"Test Description\"," - + " \"dc:title\": \"Test Image\"," + " \"tiff:ImageLength\": 800," + " \"tiff:ImageWidth\": 1200" + " }" + "}"; + public static final String METADATA_JSON_IMAGE_FULL = "{" + + " \"assetId\": \"" + SAMPLE_ASSET_ID + "\"," + + " \"repositoryMetadata\": {" + + " \"repo:name\": \"test.jpg\"," + + " \"dc:format\": \"image/jpeg\"" + + " }," + + " \"assetMetadata\": {" + + " \"dam:assetStatus\": \"approved\"," + + " \"dc:description\": \"Test Description\"," + + " \"dc:title\": \"Test Image\"," + + " \"tiff:ImageLength\": 900," + + " \"tiff:ImageWidth\": 1500" + + " }" + + "}"; + + public static final String METADATA_JSON_SVG = "{" + + " \"assetId\": \"" + SAMPLE_ASSET_ID + "\"," + + " \"repositoryMetadata\": {" + + " \"repo:name\": \"test.svg\"," + + " \"dc:format\": \"image/svg+xml\"" + + " }," + + " \"assetMetadata\": {" + + " \"dam:assetStatus\": \"approved\"," + + " \"tiff:ImageLength\": 600," + + " \"tiff:ImageWidth\": 900" + + " }" + + "}"; + public static final String METADATA_JSON_PDF = "{" + " \"assetId\": \"" + SAMPLE_ASSET_ID + "\"," + " \"repositoryMetadata\": {" @@ -80,9 +106,7 @@ public final class MetadataSample { + " \"dc:format\": \"application/pdf\"" + " }," + " \"assetMetadata\": {" - + " \"dam:assetStatus\": \"approved\"," - + " \"dc:description\": \"Test Description\"," - + " \"dc:title\": \"Test Document\"" + + " \"dam:assetStatus\": \"approved\"" + " }" + "}"; diff --git a/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataServiceImpl_ImsAccessTokenTest.java b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataServiceImpl_ImsAccessTokenTest.java new file mode 100644 index 00000000..0064023e --- /dev/null +++ b/src/test/java/io/wcm/handler/mediasource/ngdm/impl/metadata/NextGenDynamicMediaMetadataServiceImpl_ImsAccessTokenTest.java @@ -0,0 +1,148 @@ +/* + * #%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.mediasource.ngdm.impl.metadata; + +import static com.day.cq.dam.api.DamConstants.DC_DESCRIPTION; +import static com.day.cq.dam.api.DamConstants.DC_TITLE; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReferenceSample.SAMPLE_ASSET_ID; +import static io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReferenceSample.SAMPLE_REFERENCE; +import static io.wcm.handler.mediasource.ngdm.impl.metadata.MetadataSample.METADATA_JSON_IMAGE; +import static io.wcm.handler.mediasource.ngdm.impl.metadata.MetadataSample.METADATA_JSON_IMAGE_FULL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; + +import io.wcm.handler.media.Dimension; +import io.wcm.handler.media.testcontext.AppAemContext; +import io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaConfigServiceImpl; +import io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReference; +import io.wcm.testing.mock.aem.dam.ngdm.MockNextGenDynamicMediaConfig; +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) +@WireMockTest +class NextGenDynamicMediaMetadataServiceImpl_ImsAccessTokenTest { + + private static final NextGenDynamicMediaReference REFERENCE = NextGenDynamicMediaReference.fromReference(SAMPLE_REFERENCE); + + private static final String AUTHENTICATION_CLIENT_ID = "testClientId"; + private static final String AUTHENTICATION_CLIENT_SECRET = "testClientSecret"; + private static final String AUTHENTICATION_SCOPE = "testScope"; + private static final String ACCESS_TOKEN = "testToken"; + private static final long ACCESS_TOKEN_EXPIRES_SEC = 100; + + private static final String ACCESS_TOKEN_RESPONSE = "{" + + " \"access_token\": \"" + ACCESS_TOKEN + "\"," + + " \"token_type\": \"Bearer\"," + + " \"expires_in\": " + ACCESS_TOKEN_EXPIRES_SEC + + "}"; + + private final AemContext context = AppAemContext.newAemContext(); + + private NextGenDynamicMediaMetadataService underTest; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + context.registerInjectActivateService(MockNextGenDynamicMediaConfig.class) + .setRepositoryId("localhost:" + wmRuntimeInfo.getHttpPort()); + context.registerInjectActivateService(NextGenDynamicMediaConfigServiceImpl.class); + underTest = context.registerInjectActivateService(NextGenDynamicMediaMetadataServiceImpl.class, + "enabled", true, + "imsTokenApiUrl", "http://localhost:" + wmRuntimeInfo.getHttpPort() + "/ims/token/v3", + "authenticationClientId", AUTHENTICATION_CLIENT_ID, + "authenticationClientSecret", AUTHENTICATION_CLIENT_SECRET, + "authenticationScope", AUTHENTICATION_SCOPE); + + // without auth + stubFor(get("/adobe/assets/" + SAMPLE_ASSET_ID + "/metadata") + .willReturn(aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("Content-Type", ContentType.JSON) + .withBody(METADATA_JSON_IMAGE))); + + // with auth + stubFor(get("/adobe/assets/" + SAMPLE_ASSET_ID + "/metadata") + .withHeader("Authorization", equalTo("Bearer " + ACCESS_TOKEN)) + .willReturn(aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("Content-Type", ContentType.JSON) + .withBody(METADATA_JSON_IMAGE_FULL))); + } + + @Test + void testValidToken() { + stubFor(post("/ims/token/v3") + .withFormParam("grant_type", equalTo("client_credentials")) + .withFormParam("client_id", equalTo(AUTHENTICATION_CLIENT_ID)) + .withFormParam("client_secret", equalTo(AUTHENTICATION_CLIENT_SECRET)) + .withFormParam("scope", equalTo(AUTHENTICATION_SCOPE)) + .willReturn(aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("Content-Type", ContentType.JSON) + .withBody(ACCESS_TOKEN_RESPONSE))); + + NextGenDynamicMediaMetadata metadata = underTest.fetchMetadata(REFERENCE); + assertNotNull(metadata); + Dimension dimension = metadata.getDimension(); + assertNotNull(dimension); + assertEquals(1500, dimension.getWidth()); + assertEquals(900, dimension.getHeight()); + assertEquals("image/jpeg", metadata.getMimeType()); + assertEquals("Test Image", metadata.getProperties().get(DC_TITLE, String.class)); + assertEquals("Test Description", metadata.getProperties().get(DC_DESCRIPTION, String.class)); + } + + @Test + void testInvalidToken_FallbackNoAuth() { + stubFor(post("/ims/token/v3") + .withFormParam("grant_type", equalTo("client_credentials")) + .withFormParam("client_id", equalTo(AUTHENTICATION_CLIENT_ID)) + .withFormParam("client_secret", equalTo(AUTHENTICATION_CLIENT_SECRET)) + .withFormParam("scope", equalTo(AUTHENTICATION_SCOPE)) + .willReturn(aResponse() + .withStatus(HttpStatus.SC_UNAUTHORIZED))); + + NextGenDynamicMediaMetadata metadata = underTest.fetchMetadata(REFERENCE); + assertNotNull(metadata); + Dimension dimension = metadata.getDimension(); + assertNotNull(dimension); + assertEquals(1200, dimension.getWidth()); + assertEquals(800, dimension.getHeight()); + assertEquals("image/jpeg", metadata.getMimeType()); + assertNull(metadata.getProperties().get(DC_TITLE, String.class)); + assertNull(metadata.getProperties().get(DC_DESCRIPTION, String.class)); + } + +} 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 c1126bbf..2622b9b7 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 @@ -19,19 +19,12 @@ */ package io.wcm.handler.mediasource.ngdm.impl.metadata; -import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT; import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_APPROVED; -import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_PENDING; -import static com.day.cq.dam.api.DamConstants.ASSET_STATUS_PROPERTY; -import static com.day.cq.dam.api.DamConstants.RENDITIONS_FOLDER; -import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_LEFT; -import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_NORMALIZED_HEIGHT; -import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_NORMALIZED_WIDTH; -import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_TOP; +import static com.day.cq.dam.api.DamConstants.DC_DESCRIPTION; +import static com.day.cq.dam.api.DamConstants.DC_TITLE; import static io.wcm.handler.mediasource.ngdm.impl.metadata.MetadataSample.METADATA_JSON_IMAGE; +import static io.wcm.handler.mediasource.ngdm.impl.metadata.MetadataSample.METADATA_JSON_IMAGE_FULL; import static io.wcm.handler.mediasource.ngdm.impl.metadata.MetadataSample.METADATA_JSON_PDF; -import static io.wcm.handler.mediasource.ngdm.impl.metadata.NextGenDynamicMediaMetadata.RT_RENDITION_SMARTCROP; -import static org.apache.sling.api.resource.ResourceResolver.PROPERTY_RESOURCE_TYPE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -39,25 +32,15 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.Map; - 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("{}"); @@ -66,7 +49,7 @@ void testEmptyJson() throws JsonProcessingException { assertNull(metadata.getAssetStatus()); assertTrue(metadata.getSmartCrops().isEmpty()); assertFalse(metadata.isValid()); - assertEquals("[assetStatus=,dimension=,mimeType=,smartCrops=[]]", metadata.toString()); + assertEquals("NextGenDynamicMediaMetadata[]", metadata.toString()); } @Test @@ -80,10 +63,28 @@ void testSampleJson_Image() throws JsonProcessingException { assertEquals(ASSET_STATUS_APPROVED, metadata.getAssetStatus()); assertEquals(2, metadata.getSmartCrops().size()); assertTrue(metadata.isValid()); - assertEquals("[assetStatus=approved,dimension=[width=1200,height=800],mimeType=image/jpeg,smartCrops=[" - + "[cropDimension=[left=0,top=462,width=1200,height=675],name=Landscape,ratio=1.7777777777777777], " - + "[cropDimension=[left=202,top=0,width=399,height=798],name=Portrait,ratio=0.5]" - + "]]", metadata.toString()); + assertEquals("NextGenDynamicMediaMetadata[mimeType=image/jpeg,dimension=[width=1200,height=800],assetStatus=approved," + + "properties={dam:assetStatus=approved, tiff:ImageLength=800, tiff:ImageWidth=1200}," + + "smartCrops=[[cropDimension=[left=0,top=462,width=1200,height=675],name=Landscape,ratio=1.7777777777777777], " + + "[cropDimension=[left=202,top=0,width=399,height=798],name=Portrait,ratio=0.5]]]", metadata.toString()); + } + + @Test + void testSampleJson_Image_Full() throws JsonProcessingException { + NextGenDynamicMediaMetadata metadata = NextGenDynamicMediaMetadata.fromJson(METADATA_JSON_IMAGE_FULL); + assertEquals(ContentType.JPEG, metadata.getMimeType()); + Dimension dimension = metadata.getDimension(); + assertNotNull(dimension); + assertEquals(1500, dimension.getWidth()); + assertEquals(900, dimension.getHeight()); + assertEquals(ASSET_STATUS_APPROVED, metadata.getAssetStatus()); + assertEquals("Test Image", metadata.getProperties().get(DC_TITLE, String.class)); + assertEquals("Test Description", metadata.getProperties().get(DC_DESCRIPTION, String.class)); + assertEquals(0, metadata.getSmartCrops().size()); + assertTrue(metadata.isValid()); + assertEquals("NextGenDynamicMediaMetadata[mimeType=image/jpeg,dimension=[width=1500,height=900],assetStatus=approved," + + "properties={dam:assetStatus=approved, dc:description=Test Description, dc:title=Test Image, tiff:ImageLength=900, tiff:ImageWidth=1500}]", + metadata.toString()); } @Test @@ -94,7 +95,7 @@ void testSampleJson_PDF() throws JsonProcessingException { assertEquals(ASSET_STATUS_APPROVED, metadata.getAssetStatus()); assertTrue(metadata.getSmartCrops().isEmpty()); assertTrue(metadata.isValid()); - assertEquals("[assetStatus=approved,dimension=,mimeType=application/pdf,smartCrops=[]]", metadata.toString()); + assertEquals("NextGenDynamicMediaMetadata[mimeType=application/pdf,assetStatus=approved,properties={dam:assetStatus=approved}]", metadata.toString()); } @Test @@ -102,61 +103,4 @@ void testInvalidJson() { assertThrows(JsonProcessingException.class, () -> NextGenDynamicMediaMetadata.fromJson("no json")); } - @Test - void testFromAsset() { - Asset asset = context.create().asset("/content/dam/sample.jpg", 1200, 800, ContentType.JPEG, - ASSET_STATUS_PROPERTY, ASSET_STATUS_PENDING); - NextGenDynamicMediaMetadata metadata = NextGenDynamicMediaMetadata.fromAsset(asset); - - assertEquals(ContentType.JPEG, metadata.getMimeType()); - Dimension dimension = metadata.getDimension(); - assertNotNull(dimension); - assertEquals(1200, dimension.getWidth()); - assertEquals(800, dimension.getHeight()); - assertEquals(ASSET_STATUS_PENDING, metadata.getAssetStatus()); - assertTrue(metadata.getSmartCrops().isEmpty()); - assertTrue(metadata.isValid()); - assertEquals("[assetStatus=pending,dimension=[width=1200,height=800],mimeType=image/jpeg,smartCrops=[]]", metadata.toString()); - } - - @Test - void testFromAsset_SmartCrops() { - Asset asset = context.create().asset("/content/dam/sample.jpg", 1200, 800, ContentType.JPEG, - ASSET_STATUS_PROPERTY, ASSET_STATUS_APPROVED); - - context.create().resource(asset.getPath() + "/" + JCR_CONTENT + "/" + RENDITIONS_FOLDER + "/Landscape", - PROPERTY_RESOURCE_TYPE, RT_RENDITION_SMARTCROP, - JCR_CONTENT, Map.of( - PN_LEFT, 0d, - PN_TOP, 0.5774d, - PN_NORMALIZED_WIDTH, 1.0d, - PN_NORMALIZED_HEIGHT, 0.84375d)); - - context.create().resource(asset.getPath() + "/" + JCR_CONTENT + "/" + RENDITIONS_FOLDER + "/Portrait", - PROPERTY_RESOURCE_TYPE, RT_RENDITION_SMARTCROP, - JCR_CONTENT, Map.of( - PN_LEFT, 0.16792180740265983d, - PN_TOP, 0.0d, - PN_NORMALIZED_WIDTH, 0.3326723533333333d, - PN_NORMALIZED_HEIGHT, 0.9980170652565797d)); - - context.create().resource(asset.getPath() + "/" + JCR_CONTENT + "/" + RENDITIONS_FOLDER + "/Invalid", - PROPERTY_RESOURCE_TYPE, RT_RENDITION_SMARTCROP); - - NextGenDynamicMediaMetadata metadata = NextGenDynamicMediaMetadata.fromAsset(asset); - - assertEquals(ContentType.JPEG, metadata.getMimeType()); - Dimension dimension = metadata.getDimension(); - assertNotNull(dimension); - assertEquals(1200, dimension.getWidth()); - assertEquals(800, dimension.getHeight()); - assertEquals(ASSET_STATUS_APPROVED, metadata.getAssetStatus()); - assertEquals(2, metadata.getSmartCrops().size()); - assertTrue(metadata.isValid()); - assertEquals("[assetStatus=approved,dimension=[width=1200,height=800],mimeType=image/jpeg,smartCrops=[" - + "[cropDimension=[left=0,top=462,width=1200,height=675],name=Landscape,ratio=1.7777777777777777], " - + "[cropDimension=[left=202,top=0,width=399,height=798],name=Portrait,ratio=0.5]" - + "]]", metadata.toString()); - } - }