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