ResourceMedia.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2014 wcm.io
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package io.wcm.handler.media.ui;
import static io.wcm.handler.media.MediaNameConstants.PROP_CSS_CLASS;
import static io.wcm.handler.media.impl.WidthUtils.parseWidths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.RequestAttribute;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import io.wcm.handler.media.Media;
import io.wcm.handler.media.MediaArgs.WidthOption;
import io.wcm.handler.media.MediaBuilder;
import io.wcm.handler.media.MediaHandler;
import io.wcm.handler.media.format.MediaFormatHandler;
/**
* Generic resource-based media model.
* <p>
* Optional use parameters when referencing model from Sightly template:
* </p>
* <ul>
* <li><code>mediaFormat</code>: Media format name to restrict the allowed media items</li>
* <li><code>refProperty</code>: Name of the property from which the media reference path is read, or node name for
* inline media.</li>
* <li><code>cropProperty</code>: Name of the property which contains the cropping parameters</li>
* <li><code>rotationProperty</code>: Name of the property which contains the rotation parameter</li>
* <li><code>cssClass</code>: CSS classes to be applied on the generated media element (most cases img element)</li>
* <li><code>autoCrop</code>: Sets the auto cropping behavior of the media handler. This will override the component
* property
* {@value io.wcm.handler.media.MediaNameConstants#PN_COMPONENT_MEDIA_AUTOCROP}.</li>
* <li><code>imageWidths</code>: Responsive rendition widths for image. Example:
* "{@literal 2560?,1920,?1280,640,320}".<br>
* Appending the suffix "{@literal ?}" makes the width optional, eg. "1440?"<br>
* Use always 'imageWidths' together with 'imageSizes' property.<br>
* Cannot be used together with the picture source parameters.</li>
* <li><code>imageSizes</code>: "Sizes" string for img element. Example:
* "{@literal (min-width: 400px) 400px, 100vw}".<br>
* Cannot be used together with the picture source parameters.</li>
* <li><code>pictureSourceMediaFormat</code>: List of media formats for the picture source elements. Example:
* "{@literal ['mf_16_9']}"<br>
* You have to define the same number of array items in all pictureSource* properties.<br>
* Cannot be used together with image sizes.</li>
* <li><code>pictureSourceMedia</code>: List of media expressions for the picture source elements.
* Example: "{@literal ['(max-width: 799px)', '(min-width: 800px)']}"<br>
* You have to define the same number of array items in all pictureSource* properties.<br>
* Cannot be used together with image sizes.</li>
* <li><code>pictureSourceWidths</code>: List of widths for the picture source elements.
* Example: "{@literal 479,719,959,1279,1439?,1440?}".<br>
* Appending the suffix "{@literal ?}" makes the width optional, eg. "1440?"<br>
* You have to define the same number of array items in all pictureSource* properties.<br>
* Cannot be used together with image sizes.</li>
* <li><code>property:<propertyname></code>: Custom properties for MediaArgs can be added with the name prefix
* {@value #RA_PROPERTY_PREFIX},
* e.g. {@literal "property:myprop1"="value1"} which adds property {@literal "myprop1"} to the the MediaArgs. Custom
* properties with null value will be ignored.</li>
* </ul>
*/
@Model(adaptables = SlingHttpServletRequest.class)
public class ResourceMedia {
/**
* Name prefix for request attributes that will be put into the media builder properties
*/
private static final String RA_PROPERTY_PREFIX = "property:";
/**
* Regex pattern that matches request attribute names with the prefix {@value #RA_PROPERTY_PREFIX}
*/
private static final Pattern PROPERTY_NAME_PATTERN = Pattern.compile("^" + RA_PROPERTY_PREFIX + ".+$");
/**
* Optional: Media format to be used.
* By default the media formats are read from the component properties of the component and this
* parameter should not be set. But for components that allow to choose one from the allowed media
* formats via their edit dialog the format can be set here.
* To be used together with 'imageSizes' and 'widths'.<br>
* Cannot be used together with the picture source parameters.
*/
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String mediaFormat;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String refProperty;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String cropProperty;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String rotationProperty;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String cssClass;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private Boolean autoCrop;
/**
* Defines responsive rendition widths for image.
* To be used together with 'imageSizes' property.
* Example: "{@literal 2560?,1920,?1280,640,320}" <br>
* Widths are by default required. To declare an optional width append the "{@literal ?}" suffix, eg. "1440?"<br>
* Cannot be used together with the picture source parameters.
*/
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String imageWidths;
/**
* "Sizes" string for img element.
* Example: "{@literal (min-width: 400px) 400px, 100vw}"<br>
* Cannot be used together with the picture source parameters.
*/
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String imageSizes;
/**
* List of media formats for the picture source elements.
* Example: "{@literal ['mf_16_9']}"<br>
* You have to define the same number of array items in all pictureSource* properties.<br>
* Cannot be used together with image sizes.
*/
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private Object[] pictureSourceMediaFormat;
/**
* List of media expressions for the picture source elements.
* Example: "{@literal ['(max-width: 799px)', '(min-width: 800px)']}"<br>
* You have to define the same number of array items in all pictureSource* properties.<br>
* Cannot be used together with image sizes.
*/
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private Object[] pictureSourceMedia;
/**
* List of widths for the picture source elements.
* Example: "{@literal 479,719,959,1279,1439?,1440?}"<br>
* You have to define the same number of array items in all pictureSource* properties.
* Widths are by default required. To declare an optional width append the "{@literal ?}" suffix, eg. "1440?"<br>
* Cannot be used together with image sizes.
*/
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private Object[] pictureSourceWidths;
@Self
private MediaHandler mediaHandler;
@Self
private MediaFormatHandler mediaFormatHandler;
@SlingObject
private Resource resource;
@Self
private SlingHttpServletRequest request;
private Media media;
@PostConstruct
@SuppressWarnings("null")
private void activate() {
MediaBuilder builder = mediaHandler.get(resource);
if (StringUtils.isNotEmpty(mediaFormat)) {
builder.mediaFormatName(mediaFormat);
}
if (StringUtils.isNotEmpty(refProperty)) {
builder.refProperty(refProperty);
}
if (StringUtils.isNotEmpty(cropProperty)) {
builder.cropProperty(cropProperty);
}
if (StringUtils.isNotEmpty(rotationProperty)) {
builder.rotationProperty(rotationProperty);
}
if (autoCrop != null) {
builder.autoCrop(autoCrop);
}
if (StringUtils.isNotEmpty(cssClass)) {
builder.property(PROP_CSS_CLASS, cssClass);
}
// apply responsive image handling - either via image sizes or picture sources
if (StringUtils.isNotEmpty(imageSizes)) {
WidthOption[] widthOptionsArray = parseWidths(imageWidths);
if (widthOptionsArray != null) {
builder.imageSizes(imageSizes, widthOptionsArray);
}
}
else if (pictureSourceMediaFormat != null && pictureSourceMedia != null && pictureSourceWidths != null) {
ImageUtils.applyPictureSources(mediaFormatHandler, builder,
toStringArray(pictureSourceMediaFormat),
toStringArray(pictureSourceMedia),
toStringArray(pictureSourceWidths));
}
setCustomProperties(builder);
media = builder.build();
}
/**
* Puts all request attributes that their name starts with the prefix {@value #RA_PROPERTY_PREFIX} into the properties of the media builder
* @param builder Media builder
*/
private void setCustomProperties(MediaBuilder builder) {
getCustomPropertiesFromRequestAttributes()
.forEach(builder::property);
}
/**
* Gathers all request attributes whose name begins with the prefix {@value #RA_PROPERTY_PREFIX}, strips the prefix to get the property name
* and returns a map of property name to request attribute value
* @return map of custom properties
*/
@NotNull
private Map<String, Object> getCustomPropertiesFromRequestAttributes() {
return enumToList(request.getAttributeNames()).stream()
.filter(this::isMediaPropAttribute)
.filter(this::attributeValueIsNotNull)
.collect(Collectors.toMap(this::toPropertyName, request::getAttribute));
}
@NotNull
private List<String> enumToList(@Nullable Enumeration<?> enumeration) {
List<String> list = new ArrayList<>();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
list.add(String.valueOf(enumeration.nextElement()));
}
}
return list;
}
private boolean isMediaPropAttribute(@NotNull String requestAttributeName) {
return PROPERTY_NAME_PATTERN.matcher(requestAttributeName).matches();
}
private boolean attributeValueIsNotNull(String attributeName) {
return Objects.nonNull(request.getAttribute(attributeName));
}
@NotNull
private String toPropertyName(@NotNull String requestAttributeName) {
return StringUtils.substringAfter(requestAttributeName, RA_PROPERTY_PREFIX);
}
/**
* For some reason passing in arrays from HTL works only with Object[], not with String[].
* Thus, convert it here to String[].
*
* @param objectArray Array of objects
* @return Array with objects converted to strings
*/
private static String[] toStringArray(Object... objectArray) {
return Arrays.stream(objectArray)
.map(obj -> obj == null ? "" : obj.toString())
.toArray(String[]::new);
}
/**
* Returns a {@link Media} object with the metadata of the resolved media.
* Result is never null, check for validness with the {@link Media#isValid()} method.
* @return Media
*/
public @NotNull Media getMetadata() {
return media;
}
/**
* Returns true if the media was resolved successful.
* @return Media is valid
*/
public boolean isValid() {
return media.isValid();
}
/**
* Returns the XHTML markup for the resolved media object (if valid).
* This is in most cases an img element, but may also contain other arbitrary markup.
* @return Media markup
*/
public @Nullable String getMarkup() {
return media.getMarkup();
}
}