MediaFormatHandlerImpl.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.format.impl;

import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

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.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import io.wcm.handler.media.MediaFileType;
import io.wcm.handler.media.format.MediaFormat;
import io.wcm.handler.media.format.MediaFormatHandler;
import io.wcm.handler.media.format.MediaFormatProviderManager;
import io.wcm.handler.media.format.MediaFormatRankingComparator;
import io.wcm.handler.media.format.MediaFormatSizeRankingComparator;
import io.wcm.handler.media.format.Ratio;

/**
 * Media format handling.
 */
@Model(adaptables = {
    SlingHttpServletRequest.class, Resource.class
}, adapters = MediaFormatHandler.class)
public final class MediaFormatHandlerImpl implements MediaFormatHandler {

  @SlingObject
  private Resource currentResource;
  @OSGiService
  private MediaFormatProviderManager mediaFormatProviderManager;

  // do not access directly - used for caching. use getMediaFormatsForCurrentResource() and getMediaFormatMap() instead
  private SortedSet<MediaFormat> mediaFormats;
  private Map<String, MediaFormat> mediaFormatMap;

  private SortedSet<MediaFormat> getMediaFormatsForCurrentResource() {
    if (this.mediaFormats == null) {
      this.mediaFormats = mediaFormatProviderManager.getMediaFormats(currentResource);
    }
    return this.mediaFormats;
  }

  private Map<String, MediaFormat> getMediaFormatMap() {
    if (this.mediaFormatMap == null) {
      this.mediaFormatMap = new HashMap<>();
      for (MediaFormat mediaFormat : getMediaFormatsForCurrentResource()) {
        this.mediaFormatMap.put(mediaFormat.getName(), mediaFormat);
      }
    }
    return this.mediaFormatMap;
  }

  /**
   * Resolves media format name to media format object.
   * @param mediaFormatName Media format name
   * @return Media format or null if no match found
   */
  @Override
  public MediaFormat getMediaFormat(@NotNull String mediaFormatName) {
    return getMediaFormatMap().get(mediaFormatName);
  }

  /**
   * Get media formats defined by a CMS application that is responsible for the given media library path.
   * @return Media formats sorted by media format name.
   */
  @Override
  public @NotNull SortedSet<MediaFormat> getMediaFormats() {
    return getMediaFormatsForCurrentResource();
  }

  /**
   * Get media formats defined by a CMS application that is responsible for the given media library path.
   * @param comparator Comparator for set
   * @return Media formats
   */
  @Override
  public @NotNull SortedSet<MediaFormat> getMediaFormats(@NotNull Comparator<MediaFormat> comparator) {
    SortedSet<MediaFormat> set = new TreeSet<>(comparator);
    set.addAll(getMediaFormatsForCurrentResource());
    return Collections.unmodifiableSortedSet(set);
  }

  /**
   * Get list of media formats that have the same (or bigger) resolution as the requested media format
   * and (nearly) the same aspect ratio.
   * @param mediaFormatRequested Requested media format
   * @param filterRenditionGroup Only check media formats of the same rendition group.
   * @return Matching media formats, sorted by size (biggest first), ranking, name
   */
  @Override
  @SuppressWarnings({ "java:S3776", "java:S1066" }) //ignore complexity
  public @NotNull SortedSet<MediaFormat> getSameBiggerMediaFormats(@NotNull MediaFormat mediaFormatRequested, boolean filterRenditionGroup) {
    SortedSet<MediaFormat> matchingFormats = new TreeSet<>(new MediaFormatSizeRankingComparator());

    // if filter by rendition group is enabled, but the requested media format does not define one,
    // use only the requested format
    if (filterRenditionGroup && StringUtils.isEmpty(mediaFormatRequested.getRenditionGroup())) {
      matchingFormats.add(mediaFormatRequested);
    }
    else {
      for (MediaFormat mediaFormat : getMediaFormats()) {

        // if filter by rendition group is enabled, check only media formats of same rendition group
        if (!filterRenditionGroup
            || StringUtils.equals(mediaFormat.getRenditionGroup(), mediaFormatRequested.getRenditionGroup())) {

          // check if size matched (image size is same or bigger)
          if (isRenditionMatchSizeSameBigger(mediaFormat, mediaFormatRequested)) { //NOPMD

            // if media formats have ratios, check ratio (with tolerance)
            // otherwise add to list anyway, it *can* contain matching media items
            if (Ratio.matches(mediaFormat, mediaFormatRequested) //NOPMD
                || !mediaFormat.hasRatio() || !mediaFormatRequested.hasRatio()) {

              // check for supported file extension
              if (isRenditionMatchExtension(mediaFormat)) { //NOPMD
                matchingFormats.add(mediaFormat);
              }
            }

          }

        }

      }
    }

    return matchingFormats;
  }

  /**
   * Get list of possible media formats that can be rendered from the given media format, i.e. same size or smaller
   * and (nearly) the same aspect ratio.
   * @param mediaFormatRequested Available media format
   * @param filterRenditionGroup Only check media formats of the same rendition group.
   * @return Matching media formats, sorted by size (biggest first), ranking, name
   */
  @Override
  @SuppressWarnings({ "java:S3776", "java:S1066" }) //ignore complexity
  public @NotNull SortedSet<MediaFormat> getSameSmallerMediaFormats(@NotNull MediaFormat mediaFormatRequested, boolean filterRenditionGroup) {
    SortedSet<MediaFormat> matchingFormats = new TreeSet<>(new MediaFormatSizeRankingComparator());

    // if filter by rendition group is enabled, but the requested media format does not define one,
    // use only the requested format
    if (filterRenditionGroup && StringUtils.isEmpty(mediaFormatRequested.getRenditionGroup())) {
      matchingFormats.add(mediaFormatRequested);
    }
    else {
      for (MediaFormat mediaFormat : getMediaFormats()) {

        // if filter by rendition group is enabled, check only media formats of same rendition group
        if (!filterRenditionGroup
            || StringUtils.equals(mediaFormat.getRenditionGroup(), mediaFormatRequested.getRenditionGroup())) {

          // check if size matched (image size is same or smaller)
          if (isRenditionMatchSizeSameSmaller(mediaFormat, mediaFormatRequested)) { //NOPMD

            // if media formats have ratios, check ratio (with tolerance)
            // otherwise add to list anyway, it *can* contain matching media items
            if (Ratio.matches(mediaFormat, mediaFormatRequested) //NOPMD
                || !mediaFormat.hasRatio() || !mediaFormatRequested.hasRatio()) {

              // check for supported file extension
              if (isRenditionMatchExtension(mediaFormat)) { //NOPMD
                matchingFormats.add(mediaFormat);
              }
            }

          }

        }

      }
    }

    return matchingFormats;
  }

  /**
   * Checks if the given media format size is same size or bigger than the requested one.
   * @param mediaFormat Media format
   * @param mediaFormatRequested Requested media format
   * @return true if media format is same size or bigger
   */
  private boolean isRenditionMatchSizeSameBigger(MediaFormat mediaFormat, MediaFormat mediaFormatRequested) {
    long widthRequested = getEffectiveMinWidthPreferringMinWidthHeight(mediaFormatRequested);
    long heightRequested = getEffectiveMinHeightPreferringMinWidthHeight(mediaFormatRequested);

    long widthMax = mediaFormat.getEffectiveMaxWidth();
    long heightMax = mediaFormat.getEffectiveMaxHeight();

    return ((widthMax >= widthRequested) || (widthMax == 0))
        && ((heightMax >= heightRequested) || (heightMax == 0));
  }

  /**
   * Checks if the given media format size is same size or smaller than the requested one.
   * @param mediaFormat Media format
   * @param mediaFormatRequested Requested media format
   * @return true if media format is same size or smaller
   */
  private boolean isRenditionMatchSizeSameSmaller(MediaFormat mediaFormat, MediaFormat mediaFormatRequested) {
    long widthRequested = getEffectiveMinWidthPreferringMinWidthHeight(mediaFormatRequested);
    long heightRequested = getEffectiveMinHeightPreferringMinWidthHeight(mediaFormatRequested);

    long widthMin = getEffectiveMinWidthPreferringMinWidthHeight(mediaFormat);
    long heightMin = getEffectiveMinHeightPreferringMinWidthHeight(mediaFormat);

    return widthMin <= widthRequested && heightMin <= heightRequested;
  }

  private long getEffectiveMinWidthPreferringMinWidthHeight(MediaFormat mf) {
    if (mf.getMinWidthHeight() > 0) {
      return mf.getMinWidthHeight();
    }
    else {
      return mf.getEffectiveMinWidth();
    }
  }

  private long getEffectiveMinHeightPreferringMinWidthHeight(MediaFormat mf) {
    if (mf.getMinWidthHeight() > 0) {
      return mf.getMinWidthHeight();
    }
    else {
      return mf.getEffectiveMinHeight();
    }
  }

  /**
   * Checks if one of the extensions of the given media format are supported for renditions.
   * @param mediaFormat Media format
   * @return true if supported extension found
   */
  private boolean isRenditionMatchExtension(MediaFormat mediaFormat) {
    for (String extension : mediaFormat.getExtensions()) {
      if (MediaFileType.isImage(extension)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Detect matching media format.
   * @param extension File extension
   * @param fileSize File size
   * @param width Image width (or 0 if not image)
   * @param height Image height (or 0 if not image)
   * @return Media format or null if no matching media format found
   */
  @Override
  public MediaFormat detectMediaFormat(@Nullable String extension, long fileSize, long width, long height) {
    SortedSet<MediaFormat> matchingFormats = detectMediaFormats(extension, fileSize, width, height);
    return !matchingFormats.isEmpty() ? matchingFormats.first() : null;
  }

  /**
   * Detect all matching media formats.
   * @param extension File extension
   * @param fileSize File size
   * @param width Image width (or 0 if not image)
   * @param height Image height (or 0 if not image)
   * @return Matching media formats sorted by their ranking or an empty list if no matching format was found
   */
  @Override
  @SuppressWarnings("java:S3776") //ignore complexity
  public @NotNull SortedSet<MediaFormat> detectMediaFormats(@Nullable String extension, long fileSize, long width, long height) {

    // sort media formats by ranking
    SortedSet<MediaFormat> matchingFormats = new TreeSet<>(new MediaFormatRankingComparator());

    for (MediaFormat mediaFormat : getMediaFormats()) {

      // skip media formats with negative ranking
      if (mediaFormat.getRanking() < 0) {
        continue;
      }

      // check extension
      boolean extensionMatch = false;
      if (mediaFormat.getExtensions() != null) {
        for (String ext : mediaFormat.getExtensions()) {
          if (StringUtils.equalsIgnoreCase(ext, extension)) {
            extensionMatch = true;
            break;
          }
        }
      }
      else {
        extensionMatch = true;
      }

      // check file size
      boolean fileSizeMatch = false;
      if (mediaFormat.getFileSizeMax() > 0) {
        fileSizeMatch = (fileSize <= mediaFormat.getFileSizeMax());
      }
      else {
        fileSizeMatch = true;
      }

      // width/height match
      boolean dimensionMatch = false;
      if (width > 0 && height > 0) {
        if (mediaFormat.getMinWidthHeight() > 0) {
          dimensionMatch = (width >= mediaFormat.getMinWidthHeight())
              || (height >= mediaFormat.getMinWidthHeight());
        }
        else {
          dimensionMatch = (mediaFormat.getEffectiveMinWidth() == 0 || width >= mediaFormat.getEffectiveMinWidth())
              && (mediaFormat.getEffectiveMaxWidth() == 0 || width <= mediaFormat.getEffectiveMaxWidth())
              && (mediaFormat.getEffectiveMinHeight() == 0 || height >= mediaFormat.getEffectiveMinHeight())
              && (mediaFormat.getEffectiveMaxHeight() == 0 || height <= mediaFormat.getEffectiveMaxHeight());
        }
      }
      else {
        dimensionMatch = true;
      }

      boolean ratioMatch = false;
      if (mediaFormat.hasRatio() && width > 0 && height > 0) {
        double formatRatio = mediaFormat.getRatio();
        double ratio = (double)width / height;
        ratioMatch = Ratio.matches(ratio, formatRatio);
      }
      else {
        ratioMatch = true;
      }

      if (extensionMatch && fileSizeMatch && dimensionMatch && ratioMatch) {
        matchingFormats.add(mediaFormat);
      }
    }

    return matchingFormats;
  }

}