MediaSource.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.spi;
import static io.wcm.handler.media.MediaNameConstants.MEDIAFORMAT_PROP_PARENT_MEDIA_FORMAT;
import static io.wcm.handler.media.impl.ImageTransformation.isValidRotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ConsumerType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.wcm.handler.commons.dom.HtmlElement;
import io.wcm.handler.media.Asset;
import io.wcm.handler.media.CropDimension;
import io.wcm.handler.media.Media;
import io.wcm.handler.media.MediaArgs;
import io.wcm.handler.media.MediaArgs.MediaFormatOption;
import io.wcm.handler.media.MediaHandler;
import io.wcm.handler.media.MediaNameConstants;
import io.wcm.handler.media.MediaRequest;
import io.wcm.handler.media.Rendition;
import io.wcm.handler.media.format.MediaFormat;
import io.wcm.handler.media.imagemap.ImageMapArea;
import io.wcm.handler.media.imagemap.ImageMapParser;
/**
* Via {@link MediaSource} OSGi services applications can define additional media sources supported by
* {@link MediaHandler}.
* <p>
* This class has to be extended by a Sling Model class. The adaptables
* should be {@link org.apache.sling.api.SlingHttpServletRequest} and {@link org.apache.sling.api.resource.Resource}.
* </p>
*/
@ConsumerType
public abstract class MediaSource {
/**
* @return Media source ID
*/
public abstract @NotNull String getId();
/**
* @return Name of the property in which the primary media request is stored
*/
public abstract @Nullable String getPrimaryMediaRefProperty();
private static final Logger log = LoggerFactory.getLogger(MediaSource.class);
/**
* Checks whether a media request can be handled by this media source
* @param mediaRequest Media request
* @return true if this media source can handle the given media request
*/
public boolean accepts(@NotNull MediaRequest mediaRequest) {
// if an explicit media request is set check this first
if (StringUtils.isNotEmpty(mediaRequest.getMediaRef())) {
return accepts(mediaRequest.getMediaRef());
}
// otherwise check resource which contains media request properties
ValueMap props = mediaRequest.getResourceProperties();
// check for matching media source ID in media resource
String mediaSourceId = props.get(MediaNameConstants.PN_MEDIA_SOURCE, String.class);
if (StringUtils.isNotEmpty(mediaSourceId)) {
return StringUtils.equals(mediaSourceId, getId());
}
// if no media source ID is set at all check if media ref attribute contains a valid reference
else {
String refProperty = StringUtils.defaultString(mediaRequest.getMediaPropertyNames().getRefProperty(),
getPrimaryMediaRefProperty());
String mediaRef = props.get(refProperty, String.class);
return accepts(mediaRef);
}
}
/**
* Checks whether a media request string can be handled by this media source
* @param mediaRef Media request string
* @return true if this media source can handle the given media request
*/
public abstract boolean accepts(@Nullable String mediaRef);
/**
* Resolves a media request
* @param media Media metadata
* @return Resolved media metadata. Never null.
*/
public abstract @NotNull Media resolveMedia(@NotNull Media media);
/**
* Create a drop area for given HTML element to enable drag and drop of DAM assets
* from content finder to this element.
* @param element Html element
* @param mediaRequest Media request to detect media args and property names
*/
public abstract void enableMediaDrop(@NotNull HtmlElement element, @NotNull MediaRequest mediaRequest);
/**
* Sets list of cropping ratios to a list matching the selected media formats.
* @param element Html element
* @param mediaRequest Media request to detect media args and property names
*/
public void setCustomIPECropRatios(@NotNull HtmlElement element, @NotNull MediaRequest mediaRequest) {
// can be implemented by subclasses
}
/**
* Get media request path to media library
* @param mediaRequest Media request
* @param mediaHandlerConfig Media handler config (can be null, but should not be null)
* @return Path or null if not present
*/
@SuppressWarnings("null")
@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
protected final @Nullable String getMediaRef(@NotNull MediaRequest mediaRequest,
@Nullable MediaHandlerConfig mediaHandlerConfig) {
if (StringUtils.isNotEmpty(mediaRequest.getMediaRef())) {
return mediaRequest.getMediaRef();
}
else if (mediaRequest.getResource() != null) {
String refProperty = getMediaRefProperty(mediaRequest, mediaHandlerConfig);
return mediaRequest.getResource().getValueMap().get(refProperty, String.class);
}
else {
return null;
}
}
/**
* Get property name containing the media request path
* @param mediaRequest Media request
* @param mediaHandlerConfig Media handler config (can be null, but should not be null)
* @return Property name
*/
@SuppressWarnings("null")
protected final @NotNull String getMediaRefProperty(@NotNull MediaRequest mediaRequest,
@Nullable MediaHandlerConfig mediaHandlerConfig) {
String refProperty = mediaRequest.getMediaPropertyNames().getRefProperty();
if (StringUtils.isEmpty(refProperty)) {
if (mediaHandlerConfig != null) {
refProperty = mediaHandlerConfig.getMediaRefProperty();
}
else {
refProperty = MediaNameConstants.PN_MEDIA_REF;
}
}
return refProperty;
}
/**
* Get (optional) crop dimensions from resource
* @param mediaRequest Media request
* @param mediaHandlerConfig Media handler config (can be null, but should not be null)
* @return Crop dimension or null if not set or invalid
*/
@SuppressWarnings("null")
@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
protected final @Nullable CropDimension getMediaCropDimension(@NotNull MediaRequest mediaRequest,
@Nullable MediaHandlerConfig mediaHandlerConfig) {
if (mediaRequest.getResource() != null) {
String cropProperty = getMediaCropProperty(mediaRequest, mediaHandlerConfig);
String cropString = mediaRequest.getResource().getValueMap().get(cropProperty, String.class);
if (StringUtils.isNotEmpty(cropString)) {
try {
return CropDimension.fromCropString(cropString);
}
catch (IllegalArgumentException ex) {
// ignore
}
}
}
return null;
}
/**
* Get property name containing the cropping parameters
* @param mediaRequest Media request
* @param mediaHandlerConfig Media handler config (can be null, but should not be null)
* @return Property name
*/
@SuppressWarnings("null")
protected final @NotNull String getMediaCropProperty(@NotNull MediaRequest mediaRequest,
@Nullable MediaHandlerConfig mediaHandlerConfig) {
String cropProperty = mediaRequest.getMediaPropertyNames().getCropProperty();
if (StringUtils.isEmpty(cropProperty)) {
if (mediaHandlerConfig != null) {
cropProperty = mediaHandlerConfig.getMediaCropProperty();
}
else {
cropProperty = MediaNameConstants.PN_MEDIA_CROP;
}
}
return cropProperty;
}
/**
* Get (optional) rotation from resource
* @param mediaRequest Media request
* @param mediaHandlerConfig Media handler config
* @return Rotation value or null if not set or invalid
*/
@SuppressWarnings("null")
@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
protected final @Nullable Integer getMediaRotation(@NotNull MediaRequest mediaRequest,
@NotNull MediaHandlerConfig mediaHandlerConfig) {
if (mediaRequest.getResource() != null) {
String rotationProperty = getMediaRotationProperty(mediaRequest, mediaHandlerConfig);
String stringValue = mediaRequest.getResource().getValueMap().get(rotationProperty, String.class);
if (StringUtils.isNotEmpty(stringValue)) {
int rotationValue = NumberUtils.toInt(stringValue);
if (isValidRotation(rotationValue)) {
return rotationValue;
}
}
}
return null;
}
/**
* Get property name containing the rotation parameter
* @param mediaRequest Media request
* @param mediaHandlerConfig Media handler config
* @return Property name
*/
@SuppressWarnings("null")
protected final @NotNull String getMediaRotationProperty(@NotNull MediaRequest mediaRequest,
@NotNull MediaHandlerConfig mediaHandlerConfig) {
String rotationProperty = mediaRequest.getMediaPropertyNames().getRotationProperty();
if (StringUtils.isEmpty(rotationProperty)) {
rotationProperty = mediaHandlerConfig.getMediaRotationProperty();
}
return rotationProperty;
}
/**
* Get (optional) image map areas from resource
* @param mediaRequest Media request
* @param mediaHandlerConfig Media handler config
* @return Rotation value or null if not set or invalid
*/
@SuppressWarnings({ "null", "PMD.ReturnEmptyCollectionRatherThanNull" })
@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
protected final @Nullable List<ImageMapArea> getMediaMap(@NotNull MediaRequest mediaRequest,
@NotNull MediaHandlerConfig mediaHandlerConfig) {
if (mediaRequest.getResource() != null) {
String mapProperty = getMediaMapProperty(mediaRequest, mediaHandlerConfig);
String stringValue = mediaRequest.getResource().getValueMap().get(mapProperty, String.class);
if (StringUtils.isNotEmpty(stringValue)) {
ImageMapParser imageMapParser = mediaRequest.getResource().adaptTo(ImageMapParser.class);
if (imageMapParser != null) {
return imageMapParser.parseMap(stringValue);
}
}
}
return null;
}
/**
* Get property name containing the image map parameter
* @param mediaRequest Media request
* @param mediaHandlerConfig Media handler config
* @return Property name
*/
@SuppressWarnings("null")
protected final @NotNull String getMediaMapProperty(@NotNull MediaRequest mediaRequest,
@NotNull MediaHandlerConfig mediaHandlerConfig) {
String mapProperty = mediaRequest.getMediaPropertyNames().getMapProperty();
if (StringUtils.isEmpty(mapProperty)) {
mapProperty = mediaHandlerConfig.getMediaMapProperty();
}
return mapProperty;
}
/**
* Updates media args settings that have default default values with values defined in the current
* resource that defines the media reference (e.g. alt. text settings).
* @param mediaArgs Media args
* @param resource Resource with media reference
* @param mediaHandlerConfig Media handler config
*/
protected final void updateMediaArgsFromResource(@NotNull MediaArgs mediaArgs, @NotNull Resource resource,
@NotNull MediaHandlerConfig mediaHandlerConfig) {
// Get alt. text-relevant properties from current resource - if not set in media args
ValueMap props = resource.getValueMap();
if (StringUtils.isEmpty(mediaArgs.getAltText())) {
mediaArgs.altText(props.get(mediaHandlerConfig.getMediaAltTextProperty(), String.class));
}
if (!mediaArgs.isDecorative()) {
mediaArgs.decorative(props.get(mediaHandlerConfig.getMediaIsDecorativeProperty(), false));
}
if (mediaArgs.isForceAltValueFromAsset()) {
mediaArgs.forceAltValueFromAsset(props.get(mediaHandlerConfig.getMediaForceAltTextFromAssetProperty(), true));
}
}
/**
* Resolves single rendition (or multiple renditions if any of the {@link MediaFormatOption#isMandatory()} is set to
* true and sets the resolved rendition and the URL of the first (best-matching) rendition in the media object.
* @param media Media object
* @param asset Asset
* @param mediaArgs Media args
* @return true if all requested mandatory renditions could be resolved (at least one or all if none was set to
* mandatory)
*/
protected final boolean resolveRenditions(Media media, Asset asset, MediaArgs mediaArgs) {
boolean anyMandatory = mediaArgs.getMediaFormatOptions() != null
&& Arrays.stream(mediaArgs.getMediaFormatOptions())
.anyMatch(MediaFormatOption::isMandatory);
MediaFormat[] mediaFormats = mediaArgs.getMediaFormats();
if (mediaFormats != null && mediaFormats.length > 1
&& (anyMandatory || mediaArgs.getImageSizes() != null || mediaArgs.getPictureSources() != null)) {
return resolveAllRenditions(media, asset, mediaArgs);
}
else {
return resolveFirstMatchRenditions(media, asset, mediaArgs);
}
}
/**
* Check if a matching rendition is found for any of the provided media formats and other media args.
* The first match is returned.
* @param media Media
* @param asset Asset
* @param mediaArgs Media args
* @return true if a rendition was found
*/
private boolean resolveFirstMatchRenditions(Media media, Asset asset, MediaArgs mediaArgs) {
Rendition rendition = asset.getRendition(mediaArgs);
boolean renditionFound = false;
if (rendition != null) {
media.setRenditions(List.of(rendition));
media.setUrl(rendition.getUrl());
renditionFound = true;
}
log.trace("ResolveFirstMatchRenditions: renditionFound={}, rendition={}", renditionFound, rendition);
return renditionFound;
}
/**
* Iterates over all defined media format and tries to find a matching rendition for each of them
* in combination with the other media args.
* @param media Media
* @param asset Asset
* @param mediaArgs Media args
* @return true if for all mandatory or for at least one media formats a rendition could be found.
*/
private boolean resolveAllRenditions(@NotNull Media media, @NotNull Asset asset, @NotNull final MediaArgs mediaArgs) {
boolean anyResolved = false;
boolean allMandatoryResolved;
final List<Rendition> resolvedRenditions = new ArrayList<>();
// resolve main media formats (ignore responsive child formats)
List<MediaFormatOption> parentMediaFormatOptions = getParentMediaFormats(mediaArgs);
allMandatoryResolved = resolveRenditionsWithMediaFormats(asset, mediaArgs, parentMediaFormatOptions, resolvedRenditions);
if (allMandatoryResolved) {
final List<MediaFormat> resolvedMediaFormats;
if (!resolvedRenditions.isEmpty()) {
resolvedMediaFormats = resolvedRenditions.stream()
.map(Rendition::getMediaFormat)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
else {
// parent formats didn't match any rendition, but they are all optional.
// try to resolve their child formats
resolvedMediaFormats = parentMediaFormatOptions.stream()
.map(MediaFormatOption::getMediaFormat)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
for (MediaFormat mediaFormat : resolvedMediaFormats) {
List<MediaFormatOption> childMediaFormatOptions = getChildMediaFormats(mediaArgs, mediaFormat);
allMandatoryResolved = resolveRenditionsWithMediaFormats(asset, mediaArgs, childMediaFormatOptions, resolvedRenditions) && allMandatoryResolved;
}
}
media.setRenditions(resolvedRenditions);
if (!resolvedRenditions.isEmpty()) {
anyResolved = true;
media.setUrl(resolvedRenditions.get(0).getUrl());
}
log.trace("ResolveAllRenditions: anyResolved={}, allMandatoryResolved={}, resolvedRenditions={}", anyResolved, allMandatoryResolved, resolvedRenditions);
return anyResolved && allMandatoryResolved;
}
private boolean resolveRenditionsWithMediaFormats(@NotNull Asset asset, @NotNull MediaArgs mediaArgs,
@NotNull List<MediaFormatOption> mediaFormatOptions, @NotNull List<Rendition> resolvedRenditions) {
// collect "fallback" renditions that do not fully fulfill the media request (e.g. ignored explicit cropping)
// separately and add them last in the returned list
List<Rendition> fallbackRenditions = new ArrayList<>();
boolean allMandatoryResolved = true;
for (MediaFormatOption mediaFormatOption : mediaFormatOptions) {
MediaArgs renditionMediaArgs = mediaArgs.clone();
renditionMediaArgs.mediaFormat(mediaFormatOption.getMediaFormat());
Rendition rendition = asset.getRendition(renditionMediaArgs);
if (rendition != null) {
if (rendition.isFallback()) {
fallbackRenditions.add(rendition);
}
else {
resolvedRenditions.add(rendition);
}
}
else if (mediaFormatOption.isMandatory()) {
allMandatoryResolved = false;
}
}
resolvedRenditions.addAll(fallbackRenditions);
return allMandatoryResolved;
}
@NotNull
private List<MediaFormatOption> getParentMediaFormats(@NotNull MediaArgs mediaArgs) {
return Arrays.stream(mediaArgs.getMediaFormatOptions())
.filter(this::isParentMediaFormat)
.collect(Collectors.toList());
}
@NotNull
private List<MediaFormatOption> getChildMediaFormats(@NotNull MediaArgs mediaArgs, @NotNull final MediaFormat parentMediaFormat) {
return Arrays.stream(mediaArgs.getMediaFormatOptions())
.filter(this::isChildMediaFormat)
.filter(childMediaFormat -> hasParent(childMediaFormat, parentMediaFormat))
.collect(Collectors.toList());
}
private boolean isParentMediaFormat(@NotNull MediaFormatOption mediaFormatOption) {
return Objects.isNull(getParentMediaFormat(mediaFormatOption.getMediaFormat()));
}
private boolean isChildMediaFormat(@NotNull MediaFormatOption mediaFormatOption) {
return Objects.nonNull(getParentMediaFormat(mediaFormatOption.getMediaFormat()));
}
private boolean hasParent(@NotNull MediaFormatOption childMediaFormat, @NotNull MediaFormat parentMediaFormat) {
return parentMediaFormat.equals(getParentMediaFormat(childMediaFormat.getMediaFormat()));
}
@Nullable
private MediaFormat getParentMediaFormat(@Nullable MediaFormat mediaFormat) {
if (mediaFormat == null) {
return null;
}
return mediaFormat.getProperties().get(MEDIAFORMAT_PROP_PARENT_MEDIA_FORMAT, MediaFormat.class);
}
}