MediaHandlerImpl.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.impl;
import java.util.ArrayList;
import java.util.List;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.adapter.Adaptable;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.Media;
import io.wcm.handler.media.MediaArgs;
import io.wcm.handler.media.MediaBuilder;
import io.wcm.handler.media.MediaHandler;
import io.wcm.handler.media.MediaInvalidReason;
import io.wcm.handler.media.MediaRequest;
import io.wcm.handler.media.format.MediaFormat;
import io.wcm.handler.media.format.MediaFormatHandler;
import io.wcm.handler.media.spi.MediaHandlerConfig;
import io.wcm.handler.media.spi.MediaMarkupBuilder;
import io.wcm.handler.media.spi.MediaProcessor;
import io.wcm.handler.media.spi.MediaSource;
import io.wcm.sling.commons.adapter.AdaptTo;
import io.wcm.wcm.commons.component.ComponentPropertyResolverFactory;
/**
* Default Implementation of a {@link MediaHandler}.
*/
@Model(adaptables = {
SlingHttpServletRequest.class, Resource.class
}, adapters = MediaHandler.class)
public final class MediaHandlerImpl implements MediaHandler {
@Self
private Adaptable adaptable;
@Self
private MediaHandlerConfig mediaHandlerConfig;
@Self
private MediaFormatHandler mediaFormatHandler;
@OSGiService
private ComponentPropertyResolverFactory componentPropertyResolverFactory;
private static final Logger log = LoggerFactory.getLogger(MediaHandlerImpl.class);
@Override
public @NotNull MediaBuilder get(@Nullable Resource resource) {
return new MediaBuilderImpl(resource, this, componentPropertyResolverFactory);
}
@Override
public @NotNull MediaBuilder get(@Nullable Resource resource, @NotNull MediaArgs mediaArgs) {
return get(resource).args(mediaArgs);
}
@Override
public @NotNull MediaBuilder get(@Nullable Resource resource, MediaFormat @NotNull... mediaFormats) {
return get(resource).mediaFormats(mediaFormats);
}
@Override
public @NotNull MediaBuilder get(@Nullable String mediaRef) {
return new MediaBuilderImpl(mediaRef, null, this, componentPropertyResolverFactory);
}
@Override
public @NotNull MediaBuilder get(@Nullable String mediaRef, @Nullable Resource contextResource) {
return new MediaBuilderImpl(mediaRef, contextResource, this, componentPropertyResolverFactory);
}
@Override
public @NotNull MediaBuilder get(@Nullable String mediaRef, @NotNull MediaArgs mediaArgs) {
return get(mediaRef).args(mediaArgs);
}
@Override
public @NotNull MediaBuilder get(@Nullable String mediaRef, MediaFormat @NotNull... mediaFormats) {
return get(mediaRef).mediaFormats(mediaFormats);
}
@Override
public @NotNull MediaBuilder get(@NotNull MediaRequest mediaRequest) {
return new MediaBuilderImpl(mediaRequest, this);
}
/**
* Resolves the media request
* @param mediaRequest Media request
* @return Media metadata (never null)
*/
@NotNull
@SuppressWarnings({
"null", "unused", "java:S2589",
"java:S3776", "java:S6541", // ignore complexity
"java:S112", // allow runtime exception
"java:S1192" // multiple strings
})
@SuppressFBWarnings({ "CORRECTNESS", "STYLE" })
Media processRequest(@NotNull final MediaRequest mediaRequest) {
// detect media source
MediaSource mediaSource = null;
List<Class<? extends MediaSource>> mediaSources = mediaHandlerConfig.getSources();
if (mediaSources == null || mediaSources.isEmpty()) {
throw new RuntimeException("No media sources defined.");
}
MediaSource firstMediaSource = null;
for (Class<? extends MediaSource> candidateMediaSourceClass : mediaSources) {
MediaSource candidateMediaSource = AdaptTo.notNull(adaptable, candidateMediaSourceClass);
if (candidateMediaSource.accepts(mediaRequest)) {
mediaSource = candidateMediaSource;
break;
}
else if (firstMediaSource == null) {
firstMediaSource = candidateMediaSource;
}
}
// if no media source was detected use first media resource defined
if (mediaSource == null) {
mediaSource = firstMediaSource;
}
Media media = new Media(mediaSource, mediaRequest);
// resolve media format names to media formats
MediaFormatResolver mediaFormatResolver = new MediaFormatResolver(mediaFormatHandler);
if (!mediaFormatResolver.resolve(mediaRequest.getMediaArgs())) {
media.setMediaInvalidReason(MediaInvalidReason.INVALID_MEDIA_FORMAT);
return media;
}
// if only downloads are accepted prepare media format filter set which only contains download media formats
if (!resolveDownloadMediaFormats(mediaRequest.getMediaArgs())) {
media.setMediaInvalidReason(MediaInvalidReason.INVALID_MEDIA_FORMAT);
return media;
}
// apply defaults to media args
if (mediaRequest.getMediaArgs().getIncludeAssetAemRenditions() == null) {
mediaRequest.getMediaArgs().includeAssetAemRenditions(mediaHandlerConfig.getIncludeAssetAemRenditionsByDefault());
}
if (log.isTraceEnabled()) {
log.trace("Start processing media request (mediaSource={}): {}", mediaSource.getId(), mediaRequest);
}
// preprocess media request before resolving
List<Class<? extends MediaProcessor>> mediaPreProcessors = mediaHandlerConfig.getPreProcessors();
if (mediaPreProcessors != null) {
for (Class<? extends MediaProcessor> processorClass : mediaPreProcessors) {
log.trace("Apply pre processor ({}): {}", processorClass, mediaRequest);
MediaProcessor processor = AdaptTo.notNull(adaptable, processorClass);
media = processor.process(media);
if (media == null) {
throw new RuntimeException("MediaPreProcessor '" + processor + "' returned null, request: " + mediaRequest);
}
}
}
if (media.getMediaInvalidReason() == null) {
// resolve media request
media = mediaSource.resolveMedia(media);
if (media == null) {
throw new RuntimeException("MediaType '" + mediaSource + "' returned null, request: " + mediaRequest);
}
// generate markup (if markup builder is available) - first accepting wins
List<Class<? extends MediaMarkupBuilder>> mediaMarkupBuilders = mediaHandlerConfig.getMarkupBuilders();
if (mediaMarkupBuilders != null) {
media.setElementBuilder(m -> {
for (Class<? extends MediaMarkupBuilder> mediaMarkupBuilderClass : mediaMarkupBuilders) {
MediaMarkupBuilder mediaMarkupBuilder = AdaptTo.notNull(adaptable, mediaMarkupBuilderClass);
if (mediaMarkupBuilder.accepts(m)) {
log.trace("Apply media markup builder ({}): {}", mediaMarkupBuilderClass, mediaRequest);
return mediaMarkupBuilder.build(m);
}
}
return null;
});
}
// postprocess media request after resolving
List<Class<? extends MediaProcessor>> mediaPostProcessors = mediaHandlerConfig.getPostProcessors();
if (mediaPostProcessors != null) {
for (Class<? extends MediaProcessor> processorClass : mediaPostProcessors) {
log.trace("Apply post processor ({}): {}", processorClass, mediaRequest);
MediaProcessor processor = AdaptTo.notNull(adaptable, processorClass);
media = processor.process(media);
if (media == null) {
throw new RuntimeException("MediaPostProcessor '" + processor + "' returned null, request: " + mediaRequest);
}
}
}
}
else {
log.trace("Skip media resolving because media was set to invalid by prepocessor. reason={}, message={}",
media.getMediaInvalidReason(), media.getMediaInvalidReasonCustomMessage());
}
log.debug("Finished media processing: {}", media);
return media;
}
@Override
@SuppressWarnings({ "null", "java:S2589" })
public boolean isValidElement(HtmlElement element) {
// if it is null it is always invalid
if (element == null) {
return false;
}
// otherwise check if any media markup builder is available that rates this html element valid
List<Class<? extends MediaMarkupBuilder>> mediaMarkupBuilders = mediaHandlerConfig.getMarkupBuilders();
if (mediaMarkupBuilders != null) {
for (Class<? extends MediaMarkupBuilder> mediaMarkupBuilderClass : mediaMarkupBuilders) {
MediaMarkupBuilder mediaMarkupBuilder = AdaptTo.notNull(adaptable, mediaMarkupBuilderClass);
if (mediaMarkupBuilder.isValidMedia(element)) {
return true;
}
}
}
return false;
}
/**
* If a set of media formats is given it is filtered to contain only download media formats.
* If no is given a new set of allowed media formats is created by getting from all media formats those marked as
* "download".
* If the result is an empty set of media formats (but downloads are requested) resolution is not successful.
* If the result is an empty set because no media format requests and no download format at all defined, it is
* successful.
* @param mediaArgs Media args
* @return true if resolving was successful
*/
private boolean resolveDownloadMediaFormats(MediaArgs mediaArgs) {
if (!mediaArgs.isDownload()) {
// not filtering for downloads
return true;
}
List<MediaFormat> candidates = new ArrayList<>();
boolean fallbackToAllMediaFormats = false;
if (mediaArgs.getMediaFormats() != null) {
candidates.addAll(List.of(mediaArgs.getMediaFormats()));
}
else {
candidates.addAll(mediaFormatHandler.getMediaFormats());
fallbackToAllMediaFormats = true;
}
MediaFormat[] result = candidates.stream()
.filter(MediaFormat::isDownload)
.toArray(size -> new MediaFormat[size]);
if (result.length > 0) {
mediaArgs.mediaFormats(result);
return true;
}
else {
// not successful when an explicit list of media formats was given, and this did not contain any download format
// successful when no media format was given, and the global list of all formats does not contain any download format
return fallbackToAllMediaFormats;
}
}
@Override
@SuppressWarnings("java:S112") // allow runtime exception
public Media invalid() {
// build invalid media with first media source
Class<? extends MediaSource> mediaSourceClass = mediaHandlerConfig.getSources().stream().findFirst().orElse(null);
if (mediaSourceClass == null) {
throw new RuntimeException("No media sources defined.");
}
MediaSource mediaSource = AdaptTo.notNull(adaptable, mediaSourceClass);
Media media = new Media(mediaSource, new MediaRequest((String)null, null));
media.setMediaInvalidReason(MediaInvalidReason.MEDIA_REFERENCE_MISSING);
return media;
}
}