View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2021 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.dam.impl.dynamicmedia;
21  
22  import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
23  
24  import java.util.Map;
25  import java.util.regex.Pattern;
26  
27  import javax.jcr.RepositoryException;
28  
29  import org.apache.commons.lang3.StringUtils;
30  import org.apache.sling.api.adapter.Adaptable;
31  import org.apache.sling.api.resource.LoginException;
32  import org.apache.sling.api.resource.Resource;
33  import org.apache.sling.api.resource.ResourceResolver;
34  import org.apache.sling.api.resource.ResourceResolverFactory;
35  import org.jetbrains.annotations.NotNull;
36  import org.jetbrains.annotations.Nullable;
37  import org.osgi.service.component.annotations.Activate;
38  import org.osgi.service.component.annotations.Component;
39  import org.osgi.service.component.annotations.Reference;
40  import org.osgi.service.metatype.annotations.AttributeDefinition;
41  import org.osgi.service.metatype.annotations.Designate;
42  import org.osgi.service.metatype.annotations.ObjectClassDefinition;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  import com.day.cq.dam.api.Asset;
47  import com.day.cq.dam.api.DamConstants;
48  import com.day.cq.dam.api.s7dam.utils.PublishUtils;
49  
50  import io.wcm.handler.media.Dimension;
51  import io.wcm.handler.url.SiteConfig;
52  import io.wcm.handler.url.UrlHandler;
53  import io.wcm.handler.url.UrlMode;
54  import io.wcm.handler.url.UrlModes;
55  import io.wcm.sling.commons.adapter.AdaptTo;
56  
57  /**
58   * Implements {@link DynamicMediaSupportService}.
59   */
60  @Component(service = DynamicMediaSupportService.class, immediate = true)
61  @Designate(ocd = DynamicMediaSupportServiceImpl.Config.class)
62  public class DynamicMediaSupportServiceImpl implements DynamicMediaSupportService {
63  
64    @ObjectClassDefinition(
65        name = "wcm.io Media Handler Dynamic Media Support",
66        description = "Configures dynamic media support in media handling.")
67    @interface Config {
68  
69      @AttributeDefinition(
70          name = "Enabled",
71          description = "Enable support for dynamic media. "
72              + "Only gets active when dynamic media is actually enabled for the instance.")
73      boolean enabled() default true;
74  
75      @AttributeDefinition(
76          name = "Dynamic Media Capability",
77          description = "Whether to detect automatically if Dynamic Media is actually for a given asset by looking for existing DM metadata. "
78              + "Setting to ON disables the auto-detection and forces it to enabled for all asssets, setting to OFF forced it to disabled.")
79      DynamicMediaCapabilityDetection dmCapabilityDetection() default DynamicMediaCapabilityDetection.AUTO;
80  
81      @AttributeDefinition(
82          name = "Author Preview Mode",
83          description = "Loads dynamic media images via author instance - to allow previewing unpublished images. "
84              + "Must not be enabled on publish instances.")
85      boolean authorPreviewMode() default false;
86  
87      @AttributeDefinition(
88          name = "Disable AEM Fallback",
89          description = "Disable the automatic fallback to AEM-based rendering of renditions (via Media Handler) "
90              + "if Dynamic Media is enabled, but the asset has not the appropriate Dynamic Media metadata. "
91              + "Please note that AEM is still used to deliver binaries for downloads even if this is activated, "
92              + "unless 'Enable Downloads' is activated as well (which is not recommended).")
93      boolean disableAemFallback() default false;
94  
95      @AttributeDefinition(
96          name = "Enable Downloads",
97          description = "Use Dynamic Media for downloads (for both image and non-image binaries). "
98              + "It is NOT recommended to enable this setting. Dynamic Media does provides reliable downloads only for non-image files, not to original binaries of images files "
99              + "(although this did work for older setups). Enable this option only if you know what you are doing (backward-compatibility mode).")
100     boolean enableDownloads() default false;
101 
102     @AttributeDefinition(
103         name = "Validate Smart Crop Rendition Sizes",
104         description = "Validates that the renditions defined via smart cropping fulfill the requested image width/height to avoid upscaling or white borders.")
105     boolean validateSmartCropRenditionSizes() default true;
106 
107     @AttributeDefinition(
108         name = "Image width limit",
109         description = "The configured width value for 'Reply Image Size Limit'.")
110     long imageSizeLimitWidth() default 2000;
111 
112     @AttributeDefinition(
113         name = "Image height limit",
114         description = "The configured height value for 'Reply Image Size Limit'.")
115     long imageSizeLimitHeight() default 2000;
116 
117     @AttributeDefinition(
118         name = "Set Image Quality",
119         description = "Control image quality for lossy output formats for each media request via 'qlt' URL parameter (instead of relying on default setting within Dynamic Media).")
120     boolean setImageQuality() default true;
121 
122     @AttributeDefinition(
123         name = "Default Format",
124         description = "Default response image format. "
125             + "If empty, the default setting that is configured on the Dynamic Media server environment is used. "
126             + "Accepts the same values as the 'fmt' parameter from the Dynamic Media Image Service API.")
127     String defaultFmt() default "";
128 
129     @AttributeDefinition(
130         name = "Default Format Alpha Channel",
131         description = "Default response image format for source images that may have an alpha channel (e.g. for PNG). "
132             + "Accepts the same values as the 'fmt' parameter from the Dynamic Media Image Service API.")
133     String defaultFmtAlpha() default "webp-alpha";
134 
135   }
136 
137   @Reference
138   private PublishUtils dynamicMediaPublishUtils;
139   @Reference
140   private ResourceResolverFactory resourceResolverFactory;
141 
142   private boolean enabled;
143   private DynamicMediaCapabilityDetection dmCapabilityDetection;
144   private boolean authorPreviewMode;
145   private boolean disableAemFallback;
146   private boolean enableDownloads;
147   private boolean validateSmartCropRenditionSizes;
148   private Dimension imageSizeLimit;
149   private boolean setImageQuality;
150   private String defaultFmt;
151   private String defaultFmtAlpha;
152 
153   private static final String SERVICEUSER_SUBSERVICE = "dynamic-media-support";
154   private static final Pattern DAM_PATH_PATTERN = Pattern.compile("^/content/dam(/.*)?$");
155 
156   private static final Logger log = LoggerFactory.getLogger(DynamicMediaSupportServiceImpl.class);
157 
158   @Activate
159   private void activate(Config config) {
160     this.enabled = config.enabled();
161     this.dmCapabilityDetection = config.dmCapabilityDetection();
162     this.authorPreviewMode = config.authorPreviewMode();
163     this.disableAemFallback = config.disableAemFallback();
164     this.enableDownloads = config.enableDownloads();
165     this.validateSmartCropRenditionSizes = config.validateSmartCropRenditionSizes();
166     this.imageSizeLimit = new Dimension(config.imageSizeLimitWidth(), config.imageSizeLimitHeight());
167     this.setImageQuality = config.setImageQuality();
168     this.defaultFmt = StringUtils.trim(config.defaultFmt());
169     this.defaultFmtAlpha = StringUtils.trim(config.defaultFmtAlpha());
170 
171     if (this.enabled) {
172       log.info("DynamicMediaSupport: enabled={}, capabilityEnabled={}, capabilityDetection={}, "
173           + "authorPreviewMode={}, disableAemFallback={}, imageSizeLimit={}",
174           this.enabled, this.dmCapabilityDetection, this.dmCapabilityDetection,
175           this.authorPreviewMode, this.disableAemFallback, this.imageSizeLimit);
176     }
177   }
178 
179   @Override
180   public boolean isDynamicMediaEnabled() {
181     return this.enabled;
182   }
183 
184   @Override
185   public boolean isDynamicMediaCapabilityEnabled(boolean isDynamicMediaAsset) {
186     switch (dmCapabilityDetection) {
187       case AUTO:
188         return isDynamicMediaAsset;
189       case ON:
190         return true;
191       case OFF:
192       default:
193         return false;
194     }
195   }
196 
197   @Override
198   public boolean isAemFallbackDisabled() {
199     return disableAemFallback;
200   }
201 
202   @Override
203   public boolean isEnableDownloads() {
204     return enableDownloads;
205   }
206 
207   @Override
208   public boolean isValidateSmartCropRenditionSizes() {
209     return validateSmartCropRenditionSizes;
210   }
211 
212   @Override
213   public @NotNull Dimension getImageSizeLimit() {
214     return this.imageSizeLimit;
215   }
216 
217   @Override
218   public boolean isSetImageQuality() {
219     return this.setImageQuality;
220   }
221 
222 
223   @Override
224   public @NotNull String getDefaultFmt() {
225     return this.defaultFmt;
226   }
227 
228   @Override
229   public @NotNull String getDefaultFmtAlpha() {
230     return this.defaultFmtAlpha;
231   }
232 
233   @Override
234   public @Nullable ImageProfile getImageProfile(@NotNull String profilePath) {
235     try (ResourceResolver resourceResolver = resourceResolverFactory
236       .getServiceResourceResolver(Map.of(ResourceResolverFactory.SUBSERVICE, SERVICEUSER_SUBSERVICE))) {
237       Resource profileResource = resourceResolver.getResource(profilePath);
238       if (profileResource != null) {
239         log.debug("Loaded image profile: {}", profilePath);
240         return new ImageProfileImpl(profileResource);
241       }
242     }
243     catch (LoginException ex) {
244       log.error("Missing service user mapping for 'io.wcm.handler.media:dynamic-media-support' - see https://wcm.io/handler/media/configuration.html", ex);
245     }
246     log.debug("Image profile not found: {}", profilePath);
247     return null;
248   }
249 
250   @Override
251   public @Nullable ImageProfile getImageProfileForAsset(@NotNull Asset asset) {
252     Resource assetResource = AdaptTo.notNull(asset, Resource.class);
253     Resource folderResource = assetResource.getParent();
254     if (folderResource != null) {
255       return getImageProfileForAssetFolder(folderResource);
256     }
257     return null;
258   }
259 
260   private @Nullable ImageProfile getImageProfileForAssetFolder(@NotNull Resource folderResource) {
261     if (!DAM_PATH_PATTERN.matcher(folderResource.getPath()).matches()) {
262       return null;
263     }
264     Resource folderContentResource = folderResource.getChild(JCR_CONTENT);
265     if (folderContentResource != null) {
266       String imageProfilePath = folderContentResource.getValueMap().get(DamConstants.IMAGE_PROFILE, String.class);
267       if (imageProfilePath != null) {
268         return getImageProfile(imageProfilePath);
269       }
270     }
271     Resource parentFolderResource = folderResource.getParent();
272     if (parentFolderResource != null) {
273       return getImageProfileForAssetFolder(parentFolderResource);
274     }
275     else {
276       return null;
277     }
278   }
279 
280   @Override
281   public @Nullable String getDynamicMediaServerUrl(@NotNull Asset asset, @Nullable UrlMode urlMode, @NotNull Adaptable adaptable) {
282     Resource assetResource = AdaptTo.notNull(asset, Resource.class);
283     if (authorPreviewMode && !forcePublishMode(urlMode)) {
284       // route dynamic media requests through author instance for preview
285       // return configured author URL, or empty string if none configured
286       SiteConfig siteConfig = AdaptTo.notNull(adaptable, SiteConfig.class);
287       String siteUrlAUthor = StringUtils.defaultString(siteConfig.siteUrlAuthor());
288       UrlHandler urlHandler = AdaptTo.notNull(adaptable, UrlHandler.class);
289       return urlHandler.applySiteUrlAutoDetection(siteUrlAUthor);
290     }
291     try {
292       String[] productionAssetUrls = dynamicMediaPublishUtils.externalizeImageDeliveryAsset(assetResource);
293       if (productionAssetUrls != null && productionAssetUrls.length > 0) {
294         return productionAssetUrls[0];
295       }
296     }
297     catch (RepositoryException ex) {
298       log.warn("Unable to get dynamic media production asset URLs for {}", assetResource.getPath(), ex);
299     }
300     log.warn("Unable to get dynamic media production asset URLs for {}", assetResource.getPath());
301     return null;
302   }
303 
304   /**
305    * If URL mode is target for publish instance, use dynamic media production URL.
306    * @param urlMode URL mode
307    * @return true if publish mode should be forced
308    */
309   private boolean forcePublishMode(@Nullable UrlMode urlMode) {
310     return urlMode != null && (urlMode.equals(UrlModes.FULL_URL_PUBLISH)
311         || urlMode.equals(UrlModes.FULL_URL_PUBLISH_FORCENONSECURE)
312         || urlMode.equals(UrlModes.FULL_URL_PUBLISH_FORCESECURE)
313         || urlMode.equals(UrlModes.FULL_URL_PUBLISH_PROTOCOLRELATIVE));
314   }
315 
316 }