View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2014 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.media;
21  
22  import java.util.Arrays;
23  import java.util.HashMap;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.Objects;
27  
28  import org.apache.commons.lang3.ArrayUtils;
29  import org.apache.commons.lang3.StringUtils;
30  import org.apache.commons.lang3.builder.EqualsBuilder;
31  import org.apache.commons.lang3.builder.HashCodeBuilder;
32  import org.apache.commons.lang3.builder.ToStringBuilder;
33  import org.apache.commons.lang3.builder.ToStringStyle;
34  import org.apache.sling.api.resource.ValueMap;
35  import org.apache.sling.api.wrappers.ValueMapDecorator;
36  import org.jetbrains.annotations.NotNull;
37  import org.jetbrains.annotations.Nullable;
38  import org.osgi.annotation.versioning.ProviderType;
39  
40  import io.wcm.handler.media.format.MediaFormat;
41  import io.wcm.handler.media.markup.DragDropSupport;
42  import io.wcm.handler.media.markup.IPERatioCustomize;
43  import io.wcm.handler.mediasource.dam.AemRenditionType;
44  import io.wcm.handler.url.UrlMode;
45  import io.wcm.wcm.commons.contenttype.FileExtension;
46  import io.wcm.wcm.commons.util.AemObjectReflectionToStringBuilder;
47  
48  /**
49   * Holds parameters to influence the media resolving process.
50   */
51  @ProviderType
52  public final class MediaArgs implements Cloneable {
53  
54    private MediaFormatOption[] mediaFormatOptions;
55    private boolean autoCrop;
56    private String[] fileExtensions;
57    private String enforceOutputFileExtension;
58    private UrlMode urlMode;
59    private long fixedWidth;
60    private long fixedHeight;
61    private boolean download;
62    private boolean contentDispositionAttachment;
63    private String altText;
64    private boolean forceAltValueFromAsset;
65    private boolean decorative;
66    private boolean dummyImage = true;
67    private String dummyImageUrl;
68    private Set<AemRenditionType> includeAssetAemRenditions;
69    private Boolean includeAssetThumbnails;
70    private Boolean includeAssetWebRenditions;
71    private ImageSizes imageSizes;
72    private PictureSource[] pictureSourceSets;
73    private Double imageQualityPercentage;
74    private DragDropSupport dragDropSupport = DragDropSupport.AUTO;
75    private IPERatioCustomize ipeRatioCustomize = IPERatioCustomize.AUTO;
76    private boolean dynamicMediaDisabled;
77    private boolean webOptimizedImageDeliveryDisabled;
78    private ValueMap properties;
79  
80    private static final Set<String> ALLOWED_FORCED_FILE_EXTENSIONS = Set.of(
81        FileExtension.JPEG, FileExtension.PNG);
82  
83    /**
84     * Default constructor
85     */
86    public MediaArgs() {
87      // default constructor
88    }
89  
90    /**
91     * @param mediaFormats Media formats
92     */
93    @SuppressWarnings("null")
94    public MediaArgs(@NotNull MediaFormat @NotNull... mediaFormats) {
95      mediaFormats(mediaFormats);
96    }
97  
98    /**
99     * @param mediaFormatNames Media format names
100    */
101   public MediaArgs(@NotNull String @NotNull... mediaFormatNames) {
102     mediaFormatNames(mediaFormatNames);
103   }
104 
105   /**
106    * Returns list of media formats to resolve to.
107    * @return Media formats
108    */
109   public MediaFormat @Nullable [] getMediaFormats() {
110     if (this.mediaFormatOptions != null) {
111       MediaFormat[] result = Arrays.stream(this.mediaFormatOptions)
112           .filter(option -> option.getMediaFormatName() == null)
113           .map(MediaFormatOption::getMediaFormat)
114           .toArray(size -> new MediaFormat[size]);
115       if (result.length > 0) {
116         return result;
117       }
118     }
119     return null;
120   }
121 
122   /**
123    * Sets list of media formats to resolve to.
124    * @param values Media formats
125    * @return this
126    */
127   public @NotNull MediaArgs mediaFormats(@Nullable MediaFormat @Nullable... values) {
128     if (values == null || values.length == 0) {
129       this.mediaFormatOptions = null;
130     }
131     else {
132       this.mediaFormatOptions = Arrays.stream(values)
133           .map(mediaFormat -> new MediaFormatOption(mediaFormat, false))
134           .toArray(size -> new MediaFormatOption[size]);
135     }
136     return this;
137   }
138 
139   /**
140    * Sets list of media formats to resolve to.
141    * @param values Media formats
142    * @return this
143    */
144   public @NotNull MediaArgs mandatoryMediaFormats(@NotNull MediaFormat @Nullable... values) {
145     if (values == null || values.length == 0) {
146       this.mediaFormatOptions = null;
147     }
148     else {
149       this.mediaFormatOptions = Arrays.stream(values)
150           .map(mediaFormat -> new MediaFormatOption(mediaFormat, true))
151           .toArray(size -> new MediaFormatOption[size]);
152     }
153     return this;
154   }
155 
156   /**
157    * Sets a single media format to resolve to.
158    * @param value Media format
159    * @return this
160    */
161   public @NotNull MediaArgs mediaFormat(MediaFormat value) {
162     if (value == null) {
163       this.mediaFormatOptions = null;
164     }
165     else {
166       this.mediaFormatOptions = new MediaFormatOption[] {
167           new MediaFormatOption(value, false)
168       };
169     }
170     return this;
171   }
172 
173   /**
174    * The "mandatory" flag of all media format options is set to to the given value.
175    * @param value Resolving of all media formats is mandatory.
176    * @return this
177    */
178   public @NotNull MediaArgs mediaFormatsMandatory(boolean value) {
179     if (this.mediaFormatOptions != null) {
180       this.mediaFormatOptions = Arrays.stream(this.mediaFormatOptions)
181           .map(option -> option.withMandatory(value))
182           .toArray(size -> new MediaFormatOption[size]);
183     }
184     return this;
185   }
186 
187   /**
188    * Returns list of media formats to resolve to. See {@link #getMediaFormatNames()} for details.
189    * @return Media format names
190    */
191   public String @Nullable [] getMediaFormatNames() {
192     if (this.mediaFormatOptions != null) {
193       String[] result = Arrays.stream(this.mediaFormatOptions)
194           .filter(option -> option.getMediaFormatName() != null)
195           .map(MediaFormatOption::getMediaFormatName)
196           .toArray(size -> new String[size]);
197       if (result.length > 0) {
198         return result;
199       }
200     }
201     return null;
202   }
203 
204   /**
205    * Sets list of media formats to resolve to.
206    * @param names Media format names.
207    * @return this
208    */
209   public @NotNull MediaArgs mediaFormatNames(@NotNull String @Nullable... names) {
210     if (names == null || names.length == 0) {
211       this.mediaFormatOptions = null;
212     }
213     else {
214       this.mediaFormatOptions = Arrays.stream(names)
215           .map(name -> new MediaFormatOption(name, false))
216           .toArray(size -> new MediaFormatOption[size]);
217     }
218     return this;
219   }
220 
221   /**
222    * Sets list of media formats to resolve to.
223    * @param names Media format names.
224    * @return this
225    */
226   public @NotNull MediaArgs mandatoryMediaFormatNames(@NotNull String @Nullable... names) {
227     if (names == null || names.length == 0) {
228       this.mediaFormatOptions = null;
229     }
230     else {
231       this.mediaFormatOptions = Arrays.stream(names)
232           .map(name -> new MediaFormatOption(name, true))
233           .toArray(size -> new MediaFormatOption[size]);
234     }
235     return this;
236   }
237 
238   /**
239    * Sets a single media format to resolve to.
240    * @param name Media format name
241    * @return this
242    */
243   public @NotNull MediaArgs mediaFormatName(String name) {
244     if (name == null) {
245       this.mediaFormatOptions = null;
246     }
247     else {
248       this.mediaFormatOptions = new MediaFormatOption[] {
249           new MediaFormatOption(name, false)
250       };
251     }
252     return this;
253   }
254 
255   /**
256    * Gets list of media formats to resolve to.
257    * @return Media formats with mandatory flag
258    */
259   public MediaFormatOption @Nullable [] getMediaFormatOptions() {
260     return this.mediaFormatOptions;
261   }
262 
263   /**
264    * Sets list of media formats to resolve to.
265    * @param values Media formats with mandatory flag
266    * @return this
267    */
268   public @NotNull MediaArgs mediaFormatOptions(@NotNull MediaFormatOption @Nullable... values) {
269     if (values == null || values.length == 0) {
270       this.mediaFormatOptions = null;
271     }
272     else {
273       this.mediaFormatOptions = values;
274     }
275     return this;
276   }
277 
278   /**
279    * @return Enables "auto-cropping" mode. If no matching rendition is found
280    *         it is tried to generate one by automatically cropping another one.
281    */
282   public boolean isAutoCrop() {
283     return this.autoCrop;
284   }
285 
286   /**
287    * @param value Enables "auto-cropping" mode. If no matching rendition is found
288    *          it is tried to generate one by automatically cropping another one.
289    * @return this
290    */
291   public @NotNull MediaArgs autoCrop(boolean value) {
292     this.autoCrop = value;
293     return this;
294   }
295 
296   /**
297    * @return Accepted file extensions
298    */
299   public String @Nullable [] getFileExtensions() {
300     return this.fileExtensions;
301   }
302 
303   /**
304    * @param values Accepted file extensions
305    * @return this
306    */
307   public @NotNull MediaArgs fileExtensions(@NotNull String @Nullable... values) {
308     if (values == null || values.length == 0) {
309       this.fileExtensions = null;
310     }
311     else {
312       this.fileExtensions = values;
313     }
314     return this;
315   }
316 
317   /**
318    * @param value Accepted file extension
319    * @return this
320    */
321   public @NotNull MediaArgs fileExtension(@Nullable String value) {
322     if (value == null) {
323       this.fileExtensions = null;
324     }
325     else {
326       this.fileExtensions = new String[] {
327           value
328       };
329     }
330     return this;
331   }
332 
333   /**
334    * Enforces image file type for renditions.
335    *
336    * <p>
337    * By default, renditions are rendered with the same file type as the original rendition (except if the
338    * original renditions uses a file type not directly supported in browser, e.g. a TIFF image).
339    * With this parameter, it is possible to enforce generating renditions with this file type.
340    * </p>
341    *
342    * <p>
343    * Supported file types: JPEG, PNG
344    * </p>
345    * @return File extension to be used for returned renditions
346    */
347   public @Nullable String getEnforceOutputFileExtension() {
348     return this.enforceOutputFileExtension;
349   }
350 
351   /**
352    * Enforces image file type for renditions.
353    *
354    * <p>
355    * By default, renditions are rendered with the same file type as the original rendition (except if the
356    * original renditions uses a file type not directly supported in browser, e.g. a TIFF image).
357    * With this parameter, it is possible to enforce generating renditions with this file type.
358    * </p>
359    *
360    * <p>
361    * Supported file types: JPEG, PNG
362    * </p>
363    * @param value File extension to be used for returned renditions
364    * @return this
365    */
366   public @NotNull MediaArgs enforceOutputFileExtension(@Nullable String value) {
367     if (!ALLOWED_FORCED_FILE_EXTENSIONS.contains(value)) {
368       throw new IllegalArgumentException("Allowed enforced output file extensions: "
369           + StringUtils.join(ALLOWED_FORCED_FILE_EXTENSIONS, ","));
370     }
371     this.enforceOutputFileExtension = value;
372     return this;
373   }
374 
375   /**
376    * @return URL mode
377    */
378   public @Nullable UrlMode getUrlMode() {
379     return this.urlMode;
380   }
381 
382   /**
383    * @param value URS mode
384    * @return this
385    */
386   public @NotNull MediaArgs urlMode(@Nullable UrlMode value) {
387     this.urlMode = value;
388     return this;
389   }
390 
391   /**
392    * Use fixed width instead of width from media format or original image
393    * @return Fixed width
394    */
395   public long getFixedWidth() {
396     return this.fixedWidth;
397   }
398 
399   /**
400    * Use fixed width instead of width from media format or original image
401    * @param value Fixed width
402    * @return this
403    */
404   public @NotNull MediaArgs fixedWidth(long value) {
405     this.fixedWidth = value;
406     return this;
407   }
408 
409   /**
410    * Use fixed height instead of width from media format or original image
411    * @return Fixed height
412    */
413   public long getFixedHeight() {
414     return this.fixedHeight;
415   }
416 
417   /**
418    * Use fixed height instead of width from media format or original image
419    * @param value Fixed height
420    * @return this
421    */
422   public @NotNull MediaArgs fixedHeight(long value) {
423     this.fixedHeight = value;
424     return this;
425   }
426 
427   /**
428    * Use fixed dimensions instead of width from media format or original image
429    * @param widthValue Fixed width
430    * @param heightValue Fixed height
431    * @return this
432    */
433   public @NotNull MediaArgs fixedDimension(long widthValue, long heightValue) {
434     this.fixedWidth = widthValue;
435     this.fixedHeight = heightValue;
436     return this;
437   }
438 
439   /**
440    * @return Accept only media formats that have the download flag set.
441    */
442   public boolean isDownload() {
443     return this.download;
444   }
445 
446   /**
447    * @param value Accept only media formats that have the download flag set.
448    * @return this
449    */
450   public @NotNull MediaArgs download(boolean value) {
451     this.download = value;
452     return this;
453   }
454 
455   /**
456    * @return Whether to set a "Content-Disposition" header to "attachment" for forcing a "Save as" dialog on the client
457    */
458   public boolean isContentDispositionAttachment() {
459     return this.contentDispositionAttachment;
460   }
461 
462   /**
463    * @param value Whether to set a "Content-Disposition" header to "attachment" for forcing a "Save as" dialog on the
464    *          client
465    * @return this
466    */
467   public @NotNull MediaArgs contentDispositionAttachment(boolean value) {
468     this.contentDispositionAttachment = value;
469     return this;
470   }
471 
472   /**
473    * @return The custom alternative text that is to be used instead of the one defined in the the asset metadata.
474    */
475   public @Nullable String getAltText() {
476     return this.altText;
477   }
478 
479   /**
480    * Allows to specify a custom alternative text that is to be used instead of the one defined in the the asset
481    * metadata.
482    * @param value Custom alternative text. If null or empty, the default alt text from media library is used.
483    * @return this
484    */
485   public @NotNull MediaArgs altText(@Nullable String value) {
486     this.altText = value;
487     return this;
488   }
489 
490   /**
491    * @return Whether to force to read alt. text from DAM asset description.
492    */
493   public boolean isForceAltValueFromAsset() {
494     return this.forceAltValueFromAsset;
495   }
496 
497   /**
498    * @param value Whether to force to read alt. text from DAM asset description.
499    *          If not set, the asset description is used as fallback value of no custom alt. text is defined.
500    * @return this
501    */
502   public @NotNull MediaArgs forceAltValueFromAsset(boolean value) {
503     this.forceAltValueFromAsset = value;
504     return this;
505   }
506 
507   /**
508    * @return Marks this image as "decorative". Alt. text is then explicitly set to an empty string.
509    */
510   public boolean isDecorative() {
511     return this.decorative;
512   }
513 
514   /**
515    * @param value Marks this image as "decorative". Alt. text is then explicitly set to an empty string.
516    * @return this
517    */
518   public @NotNull MediaArgs decorative(boolean value) {
519     this.decorative = value;
520     return this;
521   }
522 
523   /**
524    * @return If set to true, media handler never returns a dummy image. Otherwise this can happen in edit mode.
525    */
526   public boolean isDummyImage() {
527     return this.dummyImage;
528   }
529 
530   /**
531    * @param value If set to false, media handler never returns a dummy image. Otherwise this can happen in edit mode.
532    * @return this
533    */
534   public @NotNull MediaArgs dummyImage(boolean value) {
535     this.dummyImage = value;
536     return this;
537   }
538 
539   /**
540    * @return Url of custom dummy image. If null default dummy image is used.
541    */
542   public @Nullable String getDummyImageUrl() {
543     return this.dummyImageUrl;
544   }
545 
546   /**
547    * @param value Url of custom dummy image. If null default dummy image is used.
548    * @return this
549    */
550   public @NotNull MediaArgs dummyImageUrl(@Nullable String value) {
551     this.dummyImageUrl = value;
552     return this;
553   }
554 
555   /**
556    * @return Defines which types of AEM-generated renditions (with <code>cq5dam.</code> prefix) are taken into
557    *         account when trying to resolve the media request.
558    */
559   public @Nullable Set<AemRenditionType> getIncludeAssetAemRenditions() {
560     return this.includeAssetAemRenditions;
561   }
562 
563   /**
564    * @param value Defines which types of AEM-generated renditions (with <code>cq5dam.</code> prefix) are taken into
565    *          account when trying to resolve the media request.
566    * @return this
567    */
568   public @NotNull MediaArgs includeAssetAemRenditions(@Nullable Set<AemRenditionType> value) {
569     this.includeAssetAemRenditions = value;
570     return this;
571   }
572 
573   /**
574    * @return If set to true, thumbnail generated by AEM (with <code>cq5dam.thumbnail.</code> prefix) are taken
575    *         into account as well when trying to resolve the media request. Defaults to false.
576    * @deprecated Use {@link #includeAssetAemRenditions(Set)} instead.
577    */
578   @Deprecated(since = "2.0.0")
579   public @Nullable Boolean isIncludeAssetThumbnails() {
580     return this.includeAssetThumbnails;
581   }
582 
583   /**
584    * @param value If set to true, thumbnail generated by AEM (with <code>cq5dam.thumbnail.</code> prefix) are
585    *          taken into account as well when trying to resolve the media request.
586    * @return this
587    * @deprecated Use {@link #includeAssetAemRenditions(Set)} instead.
588    */
589   @Deprecated(since = "2.0.0")
590   public @NotNull MediaArgs includeAssetThumbnails(boolean value) {
591     this.includeAssetThumbnails = value;
592     return this;
593   }
594 
595   /**
596    * @return If set to true, web renditions generated by AEM (with <code>cq5dam.web.</code> prefix) are taken
597    *         into account as well when trying to resolve the media request.
598    *         If null, the default setting applies from the media handler configuration.
599    * @deprecated Use {@link #includeAssetAemRenditions(Set)} instead.
600    */
601   @Deprecated(since = "2.0.0")
602   public @Nullable Boolean isIncludeAssetWebRenditions() {
603     return this.includeAssetWebRenditions;
604   }
605 
606   /**
607    * @param value If set to true, web renditions generated by AEM (with <code>cq5dam.web.</code> prefix) are
608    *          taken into account as well when trying to resolve the media request.
609    * @return this
610    * @deprecated Use {@link #includeAssetAemRenditions(Set)} instead.
611    */
612   @Deprecated(since = "2.0.0")
613   public @NotNull MediaArgs includeAssetWebRenditions(boolean value) {
614     this.includeAssetWebRenditions = value;
615     return this;
616   }
617 
618   /**
619    * @return Image sizes for responsive image handling
620    */
621   public @Nullable ImageSizes getImageSizes() {
622     return this.imageSizes;
623   }
624 
625   /**
626    * @param value Image sizes for responsive image handling
627    * @return this
628    */
629   public @NotNull MediaArgs imageSizes(@Nullable ImageSizes value) {
630     this.imageSizes = value;
631     return this;
632   }
633 
634   /**
635    * @return Picture sources for responsive image handling
636    */
637   public PictureSource @Nullable [] getPictureSources() {
638     return this.pictureSourceSets;
639   }
640 
641   /**
642    * @param value Picture sources for responsive image handling
643    * @return this
644    */
645   public @NotNull MediaArgs pictureSources(@NotNull PictureSource @Nullable... value) {
646     this.pictureSourceSets = value;
647     return this;
648   }
649 
650   /**
651    * @return If set to true, dynamic media support is disabled even when enabled on the instance.
652    */
653   public boolean isDynamicMediaDisabled() {
654     return this.dynamicMediaDisabled;
655   }
656 
657   /**
658    * @param value If set to true, dynamic media support is disabled even when enabled on the instance.
659    * @return this
660    */
661   public @NotNull MediaArgs dynamicMediaDisabled(boolean value) {
662     this.dynamicMediaDisabled = value;
663     return this;
664   }
665 
666   /**
667    * @return If set to true, web-optimized image delivery is disabled even when enabled on the instance.
668    */
669   public boolean isWebOptimizedImageDeliveryDisabled() {
670     return this.webOptimizedImageDeliveryDisabled;
671   }
672 
673   /**
674    * @param value If set to true, web-optimized image delivery is disabled even when enabled on the instance.
675    * @return this
676    */
677   public @NotNull MediaArgs webOptimizedImageDeliveryDisabled(boolean value) {
678     this.webOptimizedImageDeliveryDisabled = value;
679     return this;
680   }
681 
682   /**
683    * @return Image quality in percent (0..1) for images with lossy compression (e.g. JPEG).
684    */
685   public @Nullable Double getImageQualityPercentage() {
686     return this.imageQualityPercentage;
687   }
688 
689   /**
690    * @param value Image quality in percent (0..1) for images with lossy compression (e.g. JPEG).
691    * @return this
692    */
693   public @NotNull MediaArgs imageQualityPercentage(@Nullable Double value) {
694     this.imageQualityPercentage = value;
695     return this;
696   }
697 
698   /**
699    * Drag&amp;Drop support for media builder.
700    * @return Drag&amp;Drop support
701    */
702   public @NotNull DragDropSupport getDragDropSupport() {
703     return this.dragDropSupport;
704   }
705 
706   /**
707    * Drag&amp;Drop support for media builder.
708    * @param value Drag&amp;Drop support
709    * @return this
710    */
711   public @NotNull MediaArgs dragDropSupport(@NotNull DragDropSupport value) {
712     this.dragDropSupport = value;
713     return this;
714   }
715 
716   /**
717    * @return Whether to set customized list of IPE cropping ratios.
718    */
719   public IPERatioCustomize getIPERatioCustomize() {
720     return this.ipeRatioCustomize;
721   }
722 
723   /**
724    * @param value Whether to set customized list of IPE cropping ratios.
725    * @return this
726    */
727   public @NotNull MediaArgs ipeRatioCustomize(@Nullable IPERatioCustomize value) {
728     this.ipeRatioCustomize = value;
729     return this;
730   }
731 
732   /**
733    * Custom properties that my be used by application-specific markup builders or processors.
734    * @param map Property map. Is merged with properties already set.
735    * @return this
736    */
737   public @NotNull MediaArgs properties(@NotNull Map<String, Object> map) {
738     getProperties().putAll(map);
739     return this;
740   }
741 
742   /**
743    * Custom properties that my be used by application-specific markup builders or processors.
744    * @param key Property key
745    * @param value Property value
746    * @return this
747    */
748   public @NotNull MediaArgs property(@NotNull String key, @Nullable Object value) {
749     getProperties().put(key, value);
750     return this;
751   }
752 
753   /**
754    * Custom properties that my be used by application-specific markup builders or processors.
755    * @return Value map
756    */
757   @NotNull
758   public ValueMap getProperties() {
759     if (this.properties == null) {
760       this.properties = new ValueMapDecorator(new HashMap<>());
761     }
762     return this.properties;
763   }
764 
765   @Override
766   public int hashCode() {
767     return HashCodeBuilder.reflectionHashCode(this);
768   }
769 
770   @Override
771   public boolean equals(Object obj) {
772     return EqualsBuilder.reflectionEquals(this, obj);
773   }
774 
775   @Override
776   @SuppressWarnings("java:S3776") // ignore complexity
777   public String toString() {
778     ToStringBuilder sb = new ToStringBuilder(this, ToStringStyle.NO_CLASS_NAME_STYLE);
779     if (mediaFormatOptions != null && mediaFormatOptions.length > 0) {
780       sb.append("mediaFormats", "[" + StringUtils.join(mediaFormatOptions, ", ") + "]");
781     }
782     if (autoCrop) {
783       sb.append("autoCrop", autoCrop);
784     }
785     if (fileExtensions != null && fileExtensions.length > 0) {
786       sb.append("fileExtensions", StringUtils.join(fileExtensions, ","));
787     }
788     if (enforceOutputFileExtension != null) {
789       sb.append("enforceOutputFileExtension", enforceOutputFileExtension);
790     }
791     if (urlMode != null) {
792       sb.append("urlMode", urlMode);
793     }
794     if (fixedWidth > 0) {
795       sb.append("fixedWidth", fixedWidth);
796     }
797     if (fixedHeight > 0) {
798       sb.append("fixedHeight", fixedHeight);
799     }
800     if (download) {
801       sb.append("download", download);
802     }
803     if (contentDispositionAttachment) {
804       sb.append("contentDispositionAttachment", contentDispositionAttachment);
805     }
806     if (altText != null) {
807       sb.append("altText", altText);
808     }
809     if (forceAltValueFromAsset) {
810       sb.append("forceAltValueFromAsset", forceAltValueFromAsset);
811     }
812     if (decorative) {
813       sb.append("decorative", decorative);
814     }
815     if (!dummyImage) {
816       sb.append("dummyImage ", dummyImage);
817     }
818     if (dummyImageUrl != null) {
819       sb.append("dummyImageUrl", dummyImageUrl);
820     }
821     if (includeAssetAemRenditions != null) {
822       sb.append("includeAssetAemRenditions", includeAssetAemRenditions);
823     }
824     if (includeAssetThumbnails != null) {
825       sb.append("includeAssetThumbnails", includeAssetThumbnails);
826     }
827     if (includeAssetWebRenditions != null) {
828       sb.append("includeAssetWebRenditions", includeAssetWebRenditions);
829     }
830     if (imageSizes != null) {
831       sb.append("imageSizes", imageSizes);
832     }
833     if (pictureSourceSets != null && pictureSourceSets.length > 0) {
834       sb.append("pictureSourceSets", "[" + StringUtils.join(pictureSourceSets, ",") + "]");
835     }
836     if (imageQualityPercentage != null) {
837       sb.append("imageQualityPercentage ", imageQualityPercentage);
838     }
839     if (dragDropSupport != DragDropSupport.AUTO) {
840       sb.append("dragDropSupport ", dragDropSupport);
841     }
842     if (ipeRatioCustomize != IPERatioCustomize.AUTO) {
843       sb.append("ipeRatioCustomize ", ipeRatioCustomize);
844     }
845     if (dynamicMediaDisabled) {
846       sb.append("dynamicMediaDisabled", dynamicMediaDisabled);
847     }
848     if (webOptimizedImageDeliveryDisabled) {
849       sb.append("webOptimizedImageDeliveryDisabled", webOptimizedImageDeliveryDisabled);
850     }
851     if (properties != null && !properties.isEmpty()) {
852       sb.append("properties", AemObjectReflectionToStringBuilder.filteredValueMap(properties));
853     }
854     return sb.build();
855   }
856 
857   /**
858    * Custom clone-method for {@link MediaArgs}
859    * @return the cloned {@link MediaArgs}
860    */
861   @Override
862   @SuppressWarnings({ "java:S2975", "java:S1182", "checkstyle:SuperCloneCheck" }) // ignore clone warnings
863   public MediaArgs clone() { //NOPMD
864     MediaArgs clone = new MediaArgs();
865 
866     clone.mediaFormatOptions = ArrayUtils.clone(this.mediaFormatOptions);
867     clone.autoCrop = this.autoCrop;
868     clone.fileExtensions = ArrayUtils.clone(this.fileExtensions);
869     clone.enforceOutputFileExtension = this.enforceOutputFileExtension;
870     clone.urlMode = this.urlMode;
871     clone.fixedWidth = this.fixedWidth;
872     clone.fixedHeight = this.fixedHeight;
873     clone.download = this.download;
874     clone.contentDispositionAttachment = this.contentDispositionAttachment;
875     clone.altText = this.altText;
876     clone.forceAltValueFromAsset = this.forceAltValueFromAsset;
877     clone.decorative = this.decorative;
878     clone.dummyImage = this.dummyImage;
879     clone.dummyImageUrl = this.dummyImageUrl;
880     clone.includeAssetAemRenditions = this.includeAssetAemRenditions;
881     clone.includeAssetThumbnails = this.includeAssetThumbnails;
882     clone.includeAssetWebRenditions = this.includeAssetWebRenditions;
883     clone.imageSizes = this.imageSizes;
884     clone.pictureSourceSets = ArrayUtils.clone(this.pictureSourceSets);
885     clone.imageQualityPercentage = this.imageQualityPercentage;
886     clone.dragDropSupport = this.dragDropSupport;
887     clone.ipeRatioCustomize = this.ipeRatioCustomize;
888     clone.dynamicMediaDisabled = this.dynamicMediaDisabled;
889     clone.webOptimizedImageDeliveryDisabled = this.webOptimizedImageDeliveryDisabled;
890     if (this.properties != null) {
891       clone.properties = new ValueMapDecorator(new HashMap<>(this.properties));
892     }
893 
894     return clone;
895   }
896 
897   /**
898    * Media format to be applied on media processing.
899    */
900   @ProviderType
901   public static final class MediaFormatOption {
902 
903     private final MediaFormat mediaFormat;
904     private final String mediaFormatName;
905     private final boolean mandatory;
906 
907     /**
908      * @param mediaFormat Media format
909      * @param mandatory Resolution of this media format is mandatory
910      */
911     public MediaFormatOption(@Nullable MediaFormat mediaFormat, boolean mandatory) {
912       this.mediaFormat = mediaFormat;
913       this.mediaFormatName = null;
914       this.mandatory = mandatory;
915     }
916 
917     /**
918      * @param mediaFormatName Media format name
919      * @param mandatory Resolution of this media format is mandatory
920      */
921     public MediaFormatOption(@NotNull String mediaFormatName, boolean mandatory) {
922       this.mediaFormat = null;
923       this.mediaFormatName = mediaFormatName;
924       this.mandatory = mandatory;
925     }
926 
927     /**
928      * @return Media format
929      */
930     public @Nullable MediaFormat getMediaFormat() {
931       return this.mediaFormat;
932     }
933 
934     /**
935      * @return Media format name
936      */
937     public @Nullable String getMediaFormatName() {
938       return this.mediaFormatName;
939     }
940 
941     /**
942      * @return Resolution of this media format is mandatory
943      */
944     public boolean isMandatory() {
945       return this.mandatory;
946     }
947 
948     @Override
949     public int hashCode() {
950       return HashCodeBuilder.reflectionHashCode(this);
951     }
952 
953     @Override
954     public boolean equals(Object obj) {
955       return EqualsBuilder.reflectionEquals(this, obj);
956     }
957 
958     @Override
959     public String toString() {
960       return mediaFormatToString(mediaFormat, mediaFormatName, mandatory);
961     }
962 
963     @NotNull
964     MediaFormatOption withMandatory(boolean newMandatory) {
965       if (this.mediaFormat != null) {
966         return new MediaFormatOption(this.mediaFormat, newMandatory);
967       }
968       else {
969         return new MediaFormatOption(this.mediaFormatName, newMandatory);
970       }
971     }
972 
973     static String mediaFormatToString(MediaFormat mediaFormat, String mediaFormatName, boolean mandatory) {
974       StringBuilder sb = new StringBuilder();
975       if (mediaFormat != null) {
976         sb.append(mediaFormat.toString());
977       }
978       else if (mediaFormatName != null) {
979         sb.append(mediaFormatName);
980       }
981       if (!mandatory) {
982         sb.append("[?]");
983       }
984       return sb.toString();
985     }
986 
987   }
988 
989   /**
990    * Image sizes for responsive image handling.
991    */
992   @ProviderType
993   public static final class ImageSizes {
994 
995     private final @NotNull String sizes;
996     private final @NotNull WidthOption @NotNull [] widthOptions;
997 
998     /**
999      * @param sizes A <a href="http://w3c.github.io/html/semantics-embedded-content.html#valid-source-size-list">valid
1000      *          source size list</a>
1001      * @param widths Widths for the renditions in the <code>srcset</code> attribute (all mandatory).
1002      */
1003     public ImageSizes(@NotNull String sizes, long @NotNull... widths) {
1004       this.sizes = sizes;
1005       this.widthOptions = Arrays.stream(widths)
1006           .distinct()
1007           .mapToObj(width -> new WidthOption(width, true))
1008           .toArray(WidthOption[]::new);
1009     }
1010 
1011     /**
1012      * @param sizes A <a href="http://w3c.github.io/html/semantics-embedded-content.html#valid-source-size-list">valid
1013      *          source size list</a>
1014      * @param widthOptions Widths for the renditions in the <code>srcset</code> attribute.
1015      */
1016     public ImageSizes(@NotNull String sizes, @NotNull WidthOption @NotNull... widthOptions) {
1017       this.sizes = sizes;
1018       this.widthOptions = widthOptions;
1019     }
1020 
1021     /**
1022      * @return A <a href="http://w3c.github.io/html/semantics-embedded-content.html#valid-source-size-list">valid
1023      *         source size list</a>
1024      */
1025     public @NotNull String getSizes() {
1026       return this.sizes;
1027     }
1028 
1029     /**
1030      * @return Widths for the renditions in the <code>srcset</code> attribute.
1031      */
1032     public @NotNull WidthOption @Nullable [] getWidthOptions() {
1033       return this.widthOptions;
1034     }
1035 
1036     /**
1037      * @return whether density descriptors should be used instead of width descriptors.
1038      */
1039     public boolean hasDensityDescriptors() {
1040       return StringUtils.isEmpty(this.sizes) &&
1041               Arrays.stream(this.widthOptions).map(WidthOption::getDensity).anyMatch(Objects::nonNull);
1042     }
1043 
1044     @Override
1045     public int hashCode() {
1046       return HashCodeBuilder.reflectionHashCode(this);
1047     }
1048 
1049     @Override
1050     public boolean equals(Object obj) {
1051       return EqualsBuilder.reflectionEquals(this, obj);
1052     }
1053 
1054     @Override
1055     @SuppressWarnings("null")
1056     public String toString() {
1057       ToStringBuilder sb = new ToStringBuilder(this, ToStringStyle.NO_CLASS_NAME_STYLE);
1058       sb.append("sizes", sizes);
1059       if (widthOptions != null && widthOptions.length > 0) {
1060         sb.append("widthOptions", StringUtils.join(widthOptions, ","));
1061       }
1062       return sb.build();
1063     }
1064 
1065   }
1066 
1067   /**
1068    * Picture source for responsive image handling.
1069    */
1070   @ProviderType
1071   public static final class PictureSource {
1072 
1073     private MediaFormat mediaFormat;
1074     private String mediaFormatName;
1075     private String media;
1076     private String sizes;
1077     private WidthOption[] widthOptions;
1078 
1079     /**
1080      * @param mediaFormat Media format
1081      */
1082     public PictureSource(@NotNull MediaFormat mediaFormat) {
1083       this.mediaFormat = mediaFormat;
1084     }
1085 
1086     /**
1087      * @param mediaFormatName Media format name
1088      */
1089     public PictureSource(@Nullable String mediaFormatName) {
1090       this.mediaFormatName = mediaFormatName;
1091     }
1092 
1093     private static @NotNull WidthOption @NotNull [] toWidthOptions(long @NotNull... widths) {
1094       return Arrays.stream(widths)
1095           .distinct()
1096           .mapToObj(width -> new WidthOption(width, true))
1097           .toArray(WidthOption[]::new);
1098     }
1099 
1100     /**
1101      * @return Media format
1102      */
1103     public @Nullable MediaFormat getMediaFormat() {
1104       return this.mediaFormat;
1105     }
1106 
1107     /**
1108      * @return Media format
1109      */
1110     public @Nullable String getMediaFormatName() {
1111       return this.mediaFormatName;
1112     }
1113 
1114     /**
1115      * @param value Widths for the renditions in the <code>srcset</code> attribute.
1116      * @return this
1117      */
1118     public PictureSource widthOptions(@NotNull WidthOption @NotNull... value) {
1119       this.widthOptions = value;
1120       return this;
1121     }
1122 
1123     /**
1124      * @return Widths for the renditions in the <code>srcset</code> attribute.
1125      */
1126     public @NotNull WidthOption @Nullable [] getWidthOptions() {
1127       return this.widthOptions;
1128     }
1129 
1130     /**
1131      * @param value Widths for the renditions in the <code>srcset</code> attribute.
1132      * @return this
1133      */
1134     public PictureSource widths(long @NotNull... value) {
1135       this.widthOptions = toWidthOptions(value);
1136       return this;
1137     }
1138 
1139     /**
1140      * @param value A <a href="http://w3c.github.io/html/semantics-embedded-content.html#valid-source-size-list">valid
1141      *          source size list</a>.
1142      * @return this
1143      */
1144     public PictureSource sizes(@Nullable String value) {
1145       this.sizes = value;
1146       return this;
1147     }
1148 
1149     /**
1150      * @return A <a href="http://w3c.github.io/html/semantics-embedded-content.html#valid-source-size-list">valid source
1151      *         size list</a>.
1152      */
1153     public @Nullable String getSizes() {
1154       return this.sizes;
1155     }
1156 
1157     /**
1158      * @param value A <a href="http://w3c.github.io/html/infrastructure.html#valid-media-query-list">valid media query
1159      *          list</a>.
1160      * @return this
1161      */
1162     public PictureSource media(@Nullable String value) {
1163       this.media = value;
1164       return this;
1165     }
1166 
1167     /**
1168      * @return A <a href="http://w3c.github.io/html/infrastructure.html#valid-media-query-list">valid media query
1169      *         list</a>.
1170      */
1171     public @Nullable String getMedia() {
1172       return this.media;
1173     }
1174 
1175     /**
1176      * @return whether density descriptors should be used instead of width descriptors.
1177      */
1178     public boolean hasDensityDescriptors() {
1179       return StringUtils.isEmpty(this.sizes) &&
1180           Arrays.stream(this.widthOptions).map(WidthOption::getDensity).anyMatch(Objects::nonNull);
1181     }
1182 
1183     @Override
1184     public int hashCode() {
1185       return HashCodeBuilder.reflectionHashCode(this);
1186     }
1187 
1188     @Override
1189     public boolean equals(Object obj) {
1190       return EqualsBuilder.reflectionEquals(this, obj);
1191     }
1192 
1193     @Override
1194     public String toString() {
1195       ToStringBuilder sb = new ToStringBuilder(this, ToStringStyle.NO_CLASS_NAME_STYLE);
1196       sb.append("mediaFormat", MediaFormatOption.mediaFormatToString(mediaFormat, mediaFormatName, true));
1197       if (media != null) {
1198         sb.append("media", media);
1199       }
1200       if (sizes != null) {
1201         sb.append("sizes", sizes);
1202       }
1203       if (widthOptions != null && widthOptions.length > 0) {
1204         sb.append("widthOptions", StringUtils.join(widthOptions, ","));
1205       }
1206       return sb.build();
1207     }
1208 
1209   }
1210 
1211   /**
1212    * Width value with mandatory flag.
1213    */
1214   @ProviderType
1215   public static final class WidthOption {
1216 
1217     private final long width;
1218     private final boolean mandatory;
1219     private final String density;
1220 
1221     /**
1222      * @param width mandatory width value
1223      */
1224     public WidthOption(long width) {
1225       this(width, null, true);
1226     }
1227 
1228     /**
1229      * @param width mandatory width value
1230      * @param density pixel density, or null for default density (1x)
1231      */
1232     public WidthOption(long width, @Nullable String density) {
1233       this(width, density, true);
1234     }
1235 
1236     /**
1237      * @param width Width value
1238      * @param mandatory Is it mandatory to resolve a rendition with this width
1239      */
1240     public WidthOption(long width, boolean mandatory) {
1241       this(width, null, mandatory);
1242     }
1243 
1244     /**
1245      * @param width Width value
1246      * @param density pixel density, or null for default density (1x)
1247      * @param mandatory Is it mandatory to resolve a rendition with this width
1248      */
1249     public WidthOption(long width, @Nullable String density, boolean mandatory) {
1250       this.width = width;
1251       this.mandatory = mandatory;
1252       this.density = density;
1253     }
1254 
1255     /**
1256      * @return Width value
1257      */
1258     public long getWidth() {
1259       return this.width;
1260     }
1261 
1262     /**
1263      * @return Is it mandatory to resolve a rendition with this width
1264      */
1265     public boolean isMandatory() {
1266       return this.mandatory;
1267     }
1268 
1269     /**
1270      * @return density descriptor or null
1271      */
1272     public @Nullable String getDensity() {
1273       return density;
1274     }
1275 
1276     /**
1277      * @return width descriptor for srcset, e.g. 200w
1278      */
1279     public @NotNull String getWidthDescriptor() {
1280       return String.format("%dw", this.width);
1281     }
1282 
1283     /**
1284      * @return density descriptor if it is not null and is not "1x", otherwise an empty string is returned
1285      */
1286     public @NotNull String getDensityDescriptor() {
1287       if (StringUtils.isEmpty(this.density) || StringUtils.equalsIgnoreCase(this.density, "1x")) {
1288         return StringUtils.EMPTY;
1289       }
1290       return this.density;
1291     }
1292 
1293     @Override
1294     public int hashCode() {
1295       return HashCodeBuilder.reflectionHashCode(this);
1296     }
1297 
1298     @Override
1299     public boolean equals(Object obj) {
1300       return EqualsBuilder.reflectionEquals(this, obj);
1301     }
1302 
1303     @Override
1304     public String toString() {
1305       StringBuilder sb = new StringBuilder();
1306       sb.append(Long.toString(width));
1307       if (density != null) {
1308         sb.append(":").append(density);
1309       }
1310       if (!mandatory) {
1311         sb.append("?");
1312       }
1313       return sb.toString();
1314     }
1315 
1316   }
1317 
1318 }