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.component.annotations.ReferencePolicy;
45  import org.osgi.service.component.annotations.ReferencePolicyOption;
46  import org.osgi.service.metatype.annotations.AttributeDefinition;
47  import org.osgi.service.metatype.annotations.Designate;
48  import org.osgi.service.metatype.annotations.ObjectClassDefinition;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaConfigService;
53  import io.wcm.handler.mediasource.ngdm.impl.NextGenDynamicMediaReference;
54  
55  /**
56   * Fetches metadata for Next Gen Dynamic Media assets via the HTTP API.
57   */
58  @Component(service = NextGenDynamicMediaMetadataService.class, immediate = true)
59  @Designate(ocd = NextGenDynamicMediaMetadataServiceImpl.Config.class)
60  public class NextGenDynamicMediaMetadataServiceImpl implements NextGenDynamicMediaMetadataService {
61  
62    @ObjectClassDefinition(
63        name = "wcm.io Media Handler Next Generation Dynamic Media Metadata Service",
64        description = "Fetches metadata for Next Generation Dynamic Media assets.")
65    @interface Config {
66  
67      @AttributeDefinition(
68          name = "Enabled",
69          description = "When enabled, metadata is fetched for each resolved asset. This checks for validity/existence of "
70              + "the asset and for the maximum supported resolution of the original image.")
71      boolean enabled() default false;
72  
73      @AttributeDefinition(
74          name = "HTTP Headers",
75          description = "HTTP headers to be send with the asset metadata request. "
76              + "Format: 'header1:value1'.")
77      String[] httpHeaders() default { "X-Adobe-Accept-Experimental:1" };
78  
79      @AttributeDefinition(
80          name = "Connect Timeout",
81          description = "HTTP Connect timeout in milliseconds.")
82      int connectTimeout() default 5000;
83  
84      @AttributeDefinition(
85          name = "Connection Request Timeout",
86          description = "HTTP connection request timeout in milliseconds.")
87      int connectionRequestTimeout() default 5000;
88  
89      @AttributeDefinition(
90          name = "Socket Timeout",
91          description = "HTTP socket timeout in milliseconds.")
92      int socketTimeout() default 5000;
93  
94      @AttributeDefinition(
95          name = "Proxy Host",
96          description = "Proxy host name")
97      String proxyHost();
98  
99      @AttributeDefinition(
100         name = "Proxy Port",
101         description = "Proxy port")
102     int proxyPort();
103 
104   }
105 
106   @Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY)
107   private NextGenDynamicMediaConfigService nextGenDynamicMediaConfig;
108 
109   private boolean enabled;
110   private CloseableHttpClient httpClient;
111 
112   private static final Logger log = LoggerFactory.getLogger(NextGenDynamicMediaMetadataServiceImpl.class);
113 
114   @Activate
115   private void activate(Config config) {
116     this.enabled = config.enabled();
117     if (enabled) {
118       httpClient = createHttpClient(config);
119     }
120   }
121 
122   private static CloseableHttpClient createHttpClient(Config config) {
123     RequestConfig requestConfig = RequestConfig.custom()
124         .setConnectTimeout(config.connectTimeout())
125         .setConnectionRequestTimeout(config.connectionRequestTimeout())
126         .setSocketTimeout(config.socketTimeout())
127         .build();
128     HttpClientBuilder builder = HttpClientBuilder.create()
129         .setDefaultRequestConfig(requestConfig)
130         .setDefaultHeaders(convertHeaders(config.httpHeaders()));
131     if (StringUtils.isNotBlank(config.proxyHost()) && config.proxyPort() > 0) {
132       builder.setProxy(new HttpHost(config.proxyHost(), config.proxyPort()));
133     }
134     return builder.build();
135   }
136 
137   private static Collection<Header> convertHeaders(String[] headers) {
138     List<Header> result = new ArrayList<>();
139     for (String header : headers) {
140       String[] parts = header.split(":", 2);
141       if (parts.length == 2) {
142         result.add(new BasicHeader(parts[0], parts[1]));
143       }
144     }
145     return result;
146   }
147 
148   @Deactivate
149   private void deactivate() throws IOException {
150     if (httpClient != null) {
151       httpClient.close();
152     }
153   }
154 
155   @Override
156   public boolean isEnabled() {
157     return enabled;
158   }
159 
160   /**
161    * Fetch asset metadata.
162    * @param reference Asset reference
163    * @return Valid asset metadata or null if not available or metadata is invalid
164    */
165   @Override
166   public @Nullable NextGenDynamicMediaMetadata fetchMetadata(@NotNull NextGenDynamicMediaReference reference) {
167     if (!enabled) {
168       return null;
169     }
170     String metadataUrl = new NextGenDynamicMediaMetadataUrlBuilder(nextGenDynamicMediaConfig).build(reference);
171     if (metadataUrl == null) {
172       return null;
173     }
174 
175     HttpGet httpGet = new HttpGet(metadataUrl);
176     try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
177       return processResponse(response, metadataUrl);
178     }
179     catch (IOException ex) {
180       log.warn("Unable to fetch NGDM asset metadata from URL {}", metadataUrl, ex);
181       return null;
182     }
183   }
184 
185   private @Nullable NextGenDynamicMediaMetadata processResponse(@NotNull CloseableHttpResponse response,
186       @NotNull String metadataUrl) throws IOException {
187     switch (response.getStatusLine().getStatusCode()) {
188       case HttpStatus.SC_OK:
189         String jsonResponse = EntityUtils.toString(response.getEntity());
190         NextGenDynamicMediaMetadata metadata = NextGenDynamicMediaMetadata.fromJson(jsonResponse);
191         log.trace("HTTP response for NGDM asset metadata {} returns: {}", metadataUrl, metadata);
192         if (metadata.isValid()) {
193           return metadata;
194         }
195         break;
196       case HttpStatus.SC_NOT_FOUND:
197         log.trace("HTTP response for NGDM asset metadata {} returns HTTP 404", metadataUrl);
198         break;
199       default:
200         log.warn("Unexpected HTTP response for NGDM asset metadata {}: {}", metadataUrl, response.getStatusLine());
201         break;
202     }
203     return null;
204   }
205 
206 }