MediaComponentPropertyResolver.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2019 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;
import static io.wcm.handler.media.MediaNameConstants.NN_COMPONENT_MEDIA_RESPONSIVEIMAGE_SIZES;
import static io.wcm.handler.media.MediaNameConstants.NN_COMPONENT_MEDIA_RESPONSIVEPICTURE_SOURCES;
import static io.wcm.handler.media.MediaNameConstants.PN_COMPONENT_MEDIA_AUTOCROP;
import static io.wcm.handler.media.MediaNameConstants.PN_COMPONENT_MEDIA_FORMATS;
import static io.wcm.handler.media.MediaNameConstants.PN_COMPONENT_MEDIA_FORMATS_MANDATORY;
import static io.wcm.handler.media.MediaNameConstants.PN_COMPONENT_MEDIA_FORMATS_MANDATORY_NAMES;
import static io.wcm.handler.media.MediaNameConstants.PN_COMPONENT_MEDIA_RESPONSIVE_TYPE;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
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.ProviderType;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.wcm.handler.media.MediaArgs.ImageSizes;
import io.wcm.handler.media.MediaArgs.MediaFormatOption;
import io.wcm.handler.media.MediaArgs.PictureSource;
import io.wcm.handler.media.MediaArgs.WidthOption;
import io.wcm.handler.media.impl.WidthUtils;
import io.wcm.wcm.commons.component.ComponentPropertyResolution;
import io.wcm.wcm.commons.component.ComponentPropertyResolver;
import io.wcm.wcm.commons.component.ComponentPropertyResolverFactory;
/**
* Resolves Media Handler component properties for the component associated
* with the given resource from content policies and properties defined in the component resource.
* Please make sure to {@link #close()} instances of this class after usage.
* <p>
* Alternatively, it's possible to use the resolver on a ValueMap. In this case, the properties
* are directly read from the provided value map. Picture Sources are not supported for that option.
* </p>
*/
@ProviderType
@SuppressFBWarnings("NP_NONNULL_RETURN_VIOLATION")
public final class MediaComponentPropertyResolver implements AutoCloseable {
static final String RESPONSIVE_TYPE_IMAGE_SIZES = "imageSizes";
static final String RESPONSIVE_TYPE_PICTURE_SOURCES = "pictureSources";
static final String PN_IMAGES_SIZES_SIZES = "sizes";
static final String PN_IMAGES_SIZES_WIDTHS = "widths";
static final String PN_PICTURE_SOURCES_MEDIAFORMAT = "mediaFormat";
static final String PN_PICTURE_SOURCES_MEDIA = "media";
static final String PN_PICTURE_SOURCES_SIZES = "sizes";
static final String PN_PICTURE_SOURCES_WIDTHS = "widths";
private final @Nullable ComponentPropertyResolver resolver;
private final PropertyAccessor propertyAccessor;
/**
* Resolves
* @param resource Resource
* @param componentPropertyResolverFactory Component property resolver factory
*/
public MediaComponentPropertyResolver(@NotNull Resource resource,
@NotNull ComponentPropertyResolverFactory componentPropertyResolverFactory) {
// resolve media component properties 1. from policies and 2. from component definition
resolver = componentPropertyResolverFactory.get(resource, true)
.contentPolicyResolution(ComponentPropertyResolution.RESOLVE)
.componentPropertiesResolution(ComponentPropertyResolution.RESOLVE_INHERIT);
propertyAccessor = new ComponentPropertyResolverPropertyAccessor(resolver);
}
/**
* @param valueMap Value map to read properties directly from
*/
public MediaComponentPropertyResolver(@NotNull ValueMap valueMap) {
resolver = null;
propertyAccessor = new ValueMapPropertyAccessor(valueMap);
}
/**
* @return AutoCrop state
*/
public boolean isAutoCrop() {
return propertyAccessor.get(PN_COMPONENT_MEDIA_AUTOCROP, false);
}
/**
* @return List of media formats with and without mandatory setting.
*/
@SuppressWarnings("java:S3776") // ignore complexity
public @NotNull MediaFormatOption @Nullable [] getMediaFormatOptions() {
Map<String, MediaFormatOption> mediaFormatOptions = new LinkedHashMap<>();
// media formats with optional mandatory boolean flag(s)
String[] mediaFormatNames = propertyAccessor.get(PN_COMPONENT_MEDIA_FORMATS, String[].class);
Boolean[] mediaFormatsMandatory = propertyAccessor.get(PN_COMPONENT_MEDIA_FORMATS_MANDATORY, Boolean[].class);
if (mediaFormatNames != null) {
for (int i = 0; i < mediaFormatNames.length; i++) {
boolean mandatory = false;
if (mediaFormatsMandatory != null) {
if (mediaFormatsMandatory.length == 1) {
// backward compatibility: support a single flag for all media formats
mandatory = mediaFormatsMandatory[0];
}
else if (mediaFormatsMandatory.length > i) {
mandatory = mediaFormatsMandatory[i];
}
}
if (StringUtils.isNotBlank(mediaFormatNames[i])) {
mediaFormatOptions.put(mediaFormatNames[i], new MediaFormatOption(mediaFormatNames[i], mandatory));
}
}
}
// support additional property with list of media format names that are all rated as mandatory
String[] mediaFormatsMandatoryNames = propertyAccessor.get(PN_COMPONENT_MEDIA_FORMATS_MANDATORY_NAMES, String[].class);
if (mediaFormatsMandatoryNames != null) {
for (String mediaFormatName : mediaFormatsMandatoryNames) {
if (StringUtils.isNotBlank(mediaFormatName)) {
mediaFormatOptions.put(mediaFormatName, new MediaFormatOption(mediaFormatName, true));
}
}
}
if (mediaFormatOptions.isEmpty()) {
return null;
}
else {
return mediaFormatOptions.values().stream().toArray(size -> new MediaFormatOption[size]);
}
}
/**
* @return List of media formats with and without mandatory setting.
*/
@SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
public @NotNull String @Nullable [] getMediaFormatNames() {
MediaFormatOption[] mediaFormatOptions = getMediaFormatOptions();
if (mediaFormatOptions != null) {
String[] result = Arrays.stream(mediaFormatOptions)
.map(MediaFormatOption::getMediaFormatName)
.filter(Objects::nonNull)
.toArray(size -> new String[size]);
if (result.length > 0) {
return result;
}
}
return null;
}
/**
* @return List of media formats with and without mandatory setting.
*/
@SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
public @NotNull String @Nullable [] getMandatoryMediaFormatNames() {
MediaFormatOption[] mediaFormatOptions = getMediaFormatOptions();
if (mediaFormatOptions != null) {
String[] result = Arrays.stream(mediaFormatOptions)
.filter(MediaFormatOption::isMandatory)
.map(MediaFormatOption::getMediaFormatName)
.filter(Objects::nonNull)
.toArray(size -> new String[size]);
if (result.length > 0) {
return result;
}
}
return null;
}
/**
* @return Image sizes
*/
public @Nullable ImageSizes getImageSizes() {
String responsiveType = getResponsiveType();
if (responsiveType != null && !StringUtils.equals(responsiveType, RESPONSIVE_TYPE_IMAGE_SIZES)) {
return null;
}
String sizes = StringUtils.trimToNull(propertyAccessor.get(NN_COMPONENT_MEDIA_RESPONSIVEIMAGE_SIZES + "/" + PN_IMAGES_SIZES_SIZES, String.class));
WidthOption[] widths = WidthUtils.parseWidths(propertyAccessor.get(NN_COMPONENT_MEDIA_RESPONSIVEIMAGE_SIZES + "/" + PN_IMAGES_SIZES_WIDTHS, String.class));
if (sizes != null && widths != null) {
return new ImageSizes(sizes, widths);
}
return null;
}
/**
* @return List of picture sources
*/
@SuppressWarnings("null")
public @NotNull PictureSource @Nullable [] getPictureSources() {
String responsiveType = getResponsiveType();
if (resolver == null || responsiveType != null && !StringUtils.equals(responsiveType, RESPONSIVE_TYPE_PICTURE_SOURCES)) {
return null;
}
Collection<Resource> sourceResources = resolver.getResources(NN_COMPONENT_MEDIA_RESPONSIVEPICTURE_SOURCES);
if (sourceResources == null) {
return null;
}
List<PictureSource> sources = new ArrayList<>();
for (Resource sourceResource : sourceResources) {
ValueMap props = sourceResource.getValueMap();
String mediaFormatName = StringUtils.trimToNull(props.get(PN_PICTURE_SOURCES_MEDIAFORMAT, String.class));
String media = StringUtils.trimToNull(props.get(PN_PICTURE_SOURCES_MEDIA, String.class));
String sizes = StringUtils.trimToNull(props.get(PN_PICTURE_SOURCES_SIZES, String.class));
WidthOption[] widths = WidthUtils.parseWidths(props.get(PN_PICTURE_SOURCES_WIDTHS, String.class));
if (mediaFormatName != null && widths != null) {
sources.add(new PictureSource(mediaFormatName)
.media(media)
.sizes(sizes)
.widthOptions(widths));
}
}
if (sources.isEmpty()) {
return null;
}
else {
return sources.stream().toArray(size -> new PictureSource[size]);
}
}
private String getResponsiveType() {
return propertyAccessor.get(PN_COMPONENT_MEDIA_RESPONSIVE_TYPE, String.class);
}
@Override
@SuppressWarnings("null")
public void close() {
if (resolver != null) {
resolver.close();
}
}
private interface PropertyAccessor {
@Nullable
<T> T get(@NotNull String name, @NotNull Class<T> type);
<T> T get(@NotNull String name, @NotNull T defaultValue);
}
private static class ComponentPropertyResolverPropertyAccessor implements PropertyAccessor {
private final ComponentPropertyResolver componentPropertyResolver;
ComponentPropertyResolverPropertyAccessor(ComponentPropertyResolver componentPropertyResolver) {
this.componentPropertyResolver = componentPropertyResolver;
}
@Override
public <T> @Nullable T get(@NotNull String name, @NotNull Class<T> type) {
return componentPropertyResolver.get(name, type);
}
@Override
public <T> T get(@NotNull String name, @NotNull T defaultValue) {
return componentPropertyResolver.get(name, defaultValue);
}
}
private static class ValueMapPropertyAccessor implements PropertyAccessor {
private final ValueMap valueMap;
ValueMapPropertyAccessor(ValueMap valueMap) {
this.valueMap = valueMap;
}
@Override
public <T> @Nullable T get(@NotNull String name, @NotNull Class<T> type) {
return valueMap.get(name, type);
}
@Override
public <T> T get(@NotNull String name, @NotNull T defaultValue) {
return valueMap.get(name, defaultValue);
}
}
}