View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2024 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.handler.mediasource.ngdm.impl.metadata;
21  
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.List;
26  
27  import org.apache.commons.lang3.StringUtils;
28  import org.apache.http.Header;
29  import org.apache.http.HttpHost;
30  import org.apache.http.HttpStatus;
31  import org.apache.http.client.config.RequestConfig;
32  import org.apache.http.client.methods.CloseableHttpResponse;
33  import org.apache.http.client.methods.HttpGet;
34  import org.apache.http.impl.client.CloseableHttpClient;
35  import org.apache.http.impl.client.HttpClientBuilder;
36  import org.apache.http.message.BasicHeader;
37  import org.apache.http.util.EntityUtils;
38  import org.jetbrains.annotations.NotNull;
39  import org.jetbrains.annotations.Nullable;
40  import org.osgi.service.component.annotations.Activate;
41  import org.osgi.service.component.annotations.Component;
42  import org.osgi.service.component.annotations.Deactivate;
43  import org.osgi.service.component.annotations.Reference;
44  import org.osgi.service.metatype.annotations.AttributeDefinition;
45  import org.osgi.service.metatype.annotations.Designate;
46  import org.osgi.service.metatype.annotations.ObjectClassDefinition;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  import io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaConfigService;
51  import io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReference;
52  
53  /**
54   * Fetches metadata for Next Gen Dynamic Media assets via the HTTP API.
55   */
56  @Component(service = NextGenDynamicMediaMetadataService.class, immediate = true)
57  @Designate(ocd = NextGenDynamicMediaMetadataServiceImpl.Config.class)
58  public class NextGenDynamicMediaMetadataServiceImpl implements NextGenDynamicMediaMetadataService {
59  
60    @ObjectClassDefinition(
61        name = "wcm.io Media Handler Dynamic Media with OpenAPI Metadata Service",
62        description = "Fetches metadata for Dynamic Media with OpenAPI remote assets.")
63    @interface Config {
64  
65      @AttributeDefinition(
66          name = "Enabled",
67          description = "When enabled, metadata is fetched for each resolved remote asset. This checks for validity/existence of "
68              + "the asset and for the maximum supported resolution of the original image, and allows to fetch Smart Cropping information.")
69      boolean enabled() default true;
70  
71      @AttributeDefinition(
72          name = "HTTP Headers",
73          description = "HTTP headers to be send with the asset metadata request. "
74              + "Format: 'header1:value1'.")
75      String[] httpHeaders();
76  
77      @AttributeDefinition(
78          name = "Connect Timeout",
79          description = "HTTP Connect timeout in milliseconds.")
80      int connectTimeout() default 5000;
81  
82      @AttributeDefinition(
83          name = "Connection Request Timeout",
84          description = "HTTP connection request timeout in milliseconds.")
85      int connectionRequestTimeout() default 5000;
86  
87      @AttributeDefinition(
88          name = "Socket Timeout",
89          description = "HTTP socket timeout in milliseconds.")
90      int socketTimeout() default 5000;
91  
92      @AttributeDefinition(
93          name = "Proxy Host",
94          description = "Proxy host name")
95      String proxyHost();
96  
97      @AttributeDefinition(
98          name = "Proxy Port",
99          description = "Proxy port")
100     int proxyPort();
101 
102     @AttributeDefinition(
103         name = "IMS Token API URL",
104         description = "API to obtain IMS access token for obtaining full metadata.")
105     String imsTokenApiUrl() default "https://ims-na1.adobelogin.com/ims/token/v3";
106 
107     @AttributeDefinition(
108         name = "IMS OAuth Client ID",
109         description = "Optional: If you want to fetch the full metadata for assets, provide the IMS OAuth Client ID.")
110     String authenticationClientId();
111 
112     @AttributeDefinition(
113         name = "IMS OAuth Client Secret",
114         description = "Optional: If you want to fetch the full metadata for assets, provide the IMS OAuth Client Secret.")
115     String authenticationClientSecret();
116 
117     @AttributeDefinition(
118         name = "IMS OAuth Scope",
119         description = "OAuth Scope to use for obtaining IMS access token.")
120     String authenticationScope() default "openid,AdobeID,read_organizations,additional_info.projectedProductContext,read_pc.dma_aem_ams";
121 
122   }
123 
124   @Reference
125   private NextGenDynamicMediaConfigService nextGenDynamicMediaConfig;
126 
127   private boolean enabled;
128   private CloseableHttpClient httpClient;
129 
130   private ImsAccessTokenCache imsAccessTokenCache;
131   private String authenticationClientId;
132   private String authenticationClientSecret;
133   private String authenticationScope;
134 
135   private static final Logger log = LoggerFactory.getLogger(NextGenDynamicMediaMetadataServiceImpl.class);
136 
137   @Activate
138   private void activate(Config config) {
139     this.enabled = config.enabled();
140     if (enabled) {
141       httpClient = createHttpClient(config);
142 
143       // if configured, enable IMS access token fetching
144       String imsTokenApiUrl = config.imsTokenApiUrl();
145       authenticationClientId = config.authenticationClientId();
146       authenticationClientSecret = config.authenticationClientSecret();
147       authenticationScope = config.authenticationScope();
148       if (log.isTraceEnabled()) {
149         log.trace("Authentication configuration: imsTokenApiUrl={}, authenticationClientId={}, authenticationClientSecret={}, authenticationScope={}",
150             StringUtils.defaultString(imsTokenApiUrl),
151             StringUtils.isNotBlank(authenticationClientId) ? "***" : "",
152             StringUtils.isNotBlank(authenticationClientSecret) ? "***" : "",
153             StringUtils.defaultString(authenticationScope));
154       }
155       if (StringUtils.isNoneBlank(imsTokenApiUrl, authenticationClientId, authenticationClientSecret, authenticationScope)) {
156         log.debug("Enable IMS access token fetching for NGDM asset metadata.");
157         imsAccessTokenCache = new ImsAccessTokenCache(httpClient, config.imsTokenApiUrl());
158       }
159       else {
160         log.debug("IMS access token fetching for NGDM asset metadata is disabled.");
161       }
162     }
163   }
164 
165   private static CloseableHttpClient createHttpClient(Config config) {
166     RequestConfig requestConfig = RequestConfig.custom()
167         .setConnectTimeout(config.connectTimeout())
168         .setConnectionRequestTimeout(config.connectionRequestTimeout())
169         .setSocketTimeout(config.socketTimeout())
170         .build();
171     HttpClientBuilder builder = HttpClientBuilder.create()
172         .setDefaultRequestConfig(requestConfig)
173         .setDefaultHeaders(convertHeaders(config.httpHeaders()));
174     if (StringUtils.isNotBlank(config.proxyHost()) && config.proxyPort() > 0) {
175       builder.setProxy(new HttpHost(config.proxyHost(), config.proxyPort()));
176     }
177     return builder.build();
178   }
179 
180   private static Collection<Header> convertHeaders(String[] headers) {
181     List<Header> result = new ArrayList<>();
182     for (String header : headers) {
183       String[] parts = header.split(":", 2);
184       if (parts.length == 2) {
185         result.add(new BasicHeader(parts[0], parts[1]));
186       }
187     }
188     return result;
189   }
190 
191   @Deactivate
192   private void deactivate() throws IOException {
193     if (httpClient != null) {
194       httpClient.close();
195       imsAccessTokenCache = null;
196     }
197   }
198 
199   @Override
200   public boolean isEnabled() {
201     return enabled;
202   }
203 
204   /**
205    * Fetch asset metadata.
206    * @param reference Asset reference
207    * @return Valid asset metadata or null if not available or metadata is invalid
208    */
209   @Override
210   public @Nullable NextGenDynamicMediaMetadata fetchMetadata(@NotNull NextGenDynamicMediaReference reference) {
211     if (!enabled) {
212       return null;
213     }
214     String metadataUrl = new NextGenDynamicMediaMetadataUrlBuilder(nextGenDynamicMediaConfig).build(reference);
215     if (metadataUrl == null) {
216       return null;
217     }
218 
219     HttpGet httpGet = new HttpGet(metadataUrl);
220 
221     // add IMS access if configured
222     if (imsAccessTokenCache != null) {
223       String accessToken = imsAccessTokenCache.getAccessToken(authenticationClientId, authenticationClientSecret, authenticationScope);
224       if (accessToken != null) {
225         httpGet.addHeader("Authorization", "Bearer " + accessToken);
226       }
227     }
228 
229     try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
230       return processResponse(response, metadataUrl);
231     }
232     catch (IOException ex) {
233       log.warn("Unable to fetch NGDM asset metadata from URL {}", metadataUrl, ex);
234       return null;
235     }
236   }
237 
238   private @Nullable NextGenDynamicMediaMetadata processResponse(@NotNull CloseableHttpResponse response,
239       @NotNull String metadataUrl) throws IOException {
240     switch (response.getStatusLine().getStatusCode()) {
241       case HttpStatus.SC_OK:
242         String jsonResponse = EntityUtils.toString(response.getEntity());
243         NextGenDynamicMediaMetadata metadata = NextGenDynamicMediaMetadata.fromJson(jsonResponse);
244         log.trace("HTTP response for NGDM asset metadata {} returns: {}", metadataUrl, metadata);
245         if (metadata.isValid()) {
246           return metadata;
247         }
248         break;
249       case HttpStatus.SC_NOT_FOUND:
250         log.trace("HTTP response for NGDM asset metadata {} returns HTTP 404", metadataUrl);
251         break;
252       default:
253         log.warn("Unexpected HTTP response for NGDM asset metadata {}: {}", metadataUrl, response.getStatusLine());
254         break;
255     }
256     return null;
257   }
258 
259 }