Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Dynamic Media with Open API: Optional IMS Authentication for metadata requests to get full asset metadata #73

Merged
merged 13 commits into from
Nov 26, 2024
3 changes: 3 additions & 0 deletions changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
<action type="update" dev="sseifert" issue="71">
Dynamic Media with Open API: Use remote metadata call to validate and get metadata for local assets as well.
</action>
<action type="update" dev="sseifert" issue="73">
Dynamic Media with Open API: Optional IMS Authentication for metadata requests to get full asset metadata.
</action>
<action type="fix" dev="sseifert" issue="72">
Dynamic Media with OpenAPI: Respect Image Dimension from SVG asset metadata.
</action>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,37 +35,50 @@
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.
*/
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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

}
Original file line number Diff line number Diff line change
@@ -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<String, AccessTokenResponse> tokenCache = Caffeine.newBuilder()
.expireAfter(new Expiry<String, AccessTokenResponse>() {
@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<NameValuePair> 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;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
final class MetadataResponse {

public RepositoryMetadata repositoryMetadata;
public AssetMetadata assetMetadata;
public Map<String, Object> assetMetadata;

@JsonIgnoreProperties(ignoreUnknown = true)
static final class RepositoryMetadata {
Expand All @@ -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;
}

}
Loading