Skip to content

Commit

Permalink
Dynamic Media with Open API: Optional IMS Authentication for metadata…
Browse files Browse the repository at this point in the history
… requests to get full asset metadata (#73)
  • Loading branch information
stefanseifert authored Nov 26, 2024
1 parent 3e461d7 commit 4073ce4
Show file tree
Hide file tree
Showing 13 changed files with 524 additions and 44 deletions.
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

0 comments on commit 4073ce4

Please sign in to comment.