IPEConfigResourceProvider.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.impl.ipeconfig;

import static io.wcm.handler.media.impl.ipeconfig.CroppingRatios.MEDIAFORMAT_FREE_CROP;
import static io.wcm.handler.media.impl.ipeconfig.PathParser.NN_ASPECT_RATIOS;
import static io.wcm.handler.media.impl.ipeconfig.PathParser.NN_CONFIG;
import static io.wcm.handler.media.impl.ipeconfig.PathParser.NN_MEDIA_FORMAT;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.SyntheticResource;
import org.apache.sling.spi.resource.provider.ResolveContext;
import org.apache.sling.spi.resource.provider.ResourceContext;
import org.apache.sling.spi.resource.provider.ResourceProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.component.annotations.Component;

import com.day.cq.wcm.api.components.ComponentManager;

import io.wcm.handler.media.format.MediaFormat;
import io.wcm.handler.media.format.MediaFormatHandler;
import io.wcm.sling.commons.adapter.AdaptTo;

/**
 * Resource provider that overlays a IPE config resource with a dynamically generated
 * set of cropping aspect ratios derived from given set of media formats.
 * <p>
 * URL pattern for resource access:<br>
 * <code>/wcmio:mediaHandler/ipeConfig/{componentContentPath}/wcmio:mediaFormat/{mf1}/{mf2}/.../wcmio:config/{relativeConfigPath}</code>
 * </p>
 */
@Component(service = ResourceProvider.class, property = {
    ResourceProvider.PROPERTY_NAME + "=wcmioHandlerIPEConfig",
    ResourceProvider.PROPERTY_ROOT + "=" + IPEConfigResourceProvider.IPECONFIG_OVERLAY_ROOTPATH
})
public class IPEConfigResourceProvider extends ResourceProvider<Void> {

  /**
   * Root path for IPE config overlay resources.
   */
  @SuppressWarnings("java:S1075") // no file path
  public static final String IPECONFIG_OVERLAY_ROOTPATH = "/wcmio:mediaHandler/ipeConfig";

  @Override
  public @Nullable Resource getResource(@NotNull ResolveContext resolveContext, @NotNull String path,
      @NotNull ResourceContext resourceContext, @Nullable Resource parent) {

    PathParser parser = new PathParser(path);
    if (!parser.isValid()) {
      return null;
    }

    ResourceResolver resolver = resolveContext.getResourceResolver();
    if (parser.isAspectRatiosNode()) {
      // simulate 'aspectRatios' node
      return buildAspectRatiosResource(resolver, path);
    }
    else if (parser.isAspectRatioItem()) {
      // simulate 'aspectRatios/xxx' node
      String mediaFormatName = parser.getAspectRatioItemName();
      if (parser.getMediaFormatNames().contains(mediaFormatName)) {
        return buildAspectRatioItemResource(resolver, path, mediaFormatName, parser);
      }
    }
    else {
      // return wrapped overlaid resource
      String overlayResourcePath = getIpeConfigPath(resolver, parser);
      if (StringUtils.isNotEmpty(overlayResourcePath)) {
        Resource overlayResource = resolver.getResource(overlayResourcePath);
        if (overlayResource != null) {
          return new OverlayResource(overlayResource, path);
        }
      }
    }
    return null;
  }

  @Override
  public @Nullable Iterator<Resource> listChildren(@NotNull ResolveContext resolveContext, @NotNull Resource resource) {
    Map<String, Resource> childMap = getOverlayedResourceChilden(resource);

    String path = resource.getPath();
    PathParser parser = new PathParser(path);
    if (!parser.isValid()) {
      return null;
    }

    ResourceResolver resolver = resolveContext.getResourceResolver();
    if (parser.isPluginsCropNode()) {
      // add simulated 'aspectRatios' node
      childMap.put(NN_ASPECT_RATIOS, buildAspectRatiosResource(resolver, path + "/" + NN_ASPECT_RATIOS));
    }
    else if (parser.isAspectRatiosNode()) {
      // add simulated 'aspectRatios/xxx' nodes
      childMap.clear();
      for (String mediaFormatName : parser.getMediaFormatNames()) {
        Resource item = buildAspectRatioItemResource(resolver, path + "/" + mediaFormatName, mediaFormatName, parser);
        if (item != null) {
          childMap.put(mediaFormatName, item);
        }
      }
    }

    if (childMap.isEmpty()) {
      return null;
    }
    else {
      return childMap.values().iterator();
    }
  }

  /**
   * Gets children of overlaid resource and converts children to {@link OverlayResource}.
   * @param resource Requested resources
   * @return Map with all children
   */
  private Map<String, Resource> getOverlayedResourceChilden(Resource resource) {
    Map<String, Resource> childMap = new LinkedHashMap<>();
    if (resource instanceof OverlayResource) {
      Resource overlayResource = ((OverlayResource)resource).getOverlayedResource();
      Iterator<Resource> childrenIterator = overlayResource.listChildren();
      while (childrenIterator.hasNext()) {
        Resource child = childrenIterator.next();
        childMap.put(child.getName(), new OverlayResource(child,
            resource.getPath() + "/" + child.getName()));
      }
    }
    return childMap;
  }

  /**
   * Build resource for /aspectRatios node
   * @param resolver Resource resolver
   * @param path Path
   * @return Resource
   */
  private Resource buildAspectRatiosResource(ResourceResolver resolver, String path) {
    return new SyntheticResource(resolver, path, null);
  }

  /**
   * Build virtual resource with name and aspect ratio of given media format.
   * @param resolver Resource resolver
   * @param path Path
   * @param mediaFormatName Media format name
   * @param parser Path parser
   * @return Resource or null if media format not found or has no valid ratio
   */
  private Resource buildAspectRatioItemResource(ResourceResolver resolver, String path, String mediaFormatName,
      PathParser parser) {
    Resource componentContent = resolver.getResource(parser.getComponentContentPath());
    if (componentContent != null) {
      MediaFormatHandler mediaFormatHandler = AdaptTo.notNull(componentContent, MediaFormatHandler.class);
      MediaFormat mediaFormat = getMediaFormat(mediaFormatName, mediaFormatHandler);
      if (mediaFormat != null) {
        return new AspectRatioResource(resolver, mediaFormat, path);
      }
    }
    return null;
  }

  private MediaFormat getMediaFormat(String mediaFormatName, MediaFormatHandler mediaFormatHandler) {
    if (StringUtils.equals(mediaFormatName, MEDIAFORMAT_FREE_CROP.getName())) {
      return MEDIAFORMAT_FREE_CROP;
    }
    else {
      return mediaFormatHandler.getMediaFormat(mediaFormatName);
    }
  }

  /**
   * Get IPE config path from component associated with given resource and append the relative
   * config path from current resource request.
   * @param resolver Resource resolver
   * @param parser Path parser
   * @return Path or null
   */
  private String getIpeConfigPath(ResourceResolver resolver, PathParser parser) {
    Resource componentContent = resolver.getResource(parser.getComponentContentPath());
    if (componentContent != null) {
      ComponentManager componentManager = AdaptTo.notNull(resolver, ComponentManager.class);
      com.day.cq.wcm.api.components.Component component = componentManager.getComponentOfResource(componentContent);
      if (component != null
          && component.getEditConfig() != null
          && component.getEditConfig().getInplaceEditingConfig() != null) {
        String ipeConfigPath = component.getEditConfig().getInplaceEditingConfig().getConfigPath();
        if (StringUtils.isNotEmpty(ipeConfigPath)) {
          return ipeConfigPath + StringUtils.defaultString(parser.getRelativeConfigPath());
        }
      }
    }
    return null;
  }

  /**
   * Build path to overlaid IPE configuration services by this resource provider.
   * @param componentContentPath Content resource path containing reference component with image IPE enabled
   * @param mediaFormatNames Media format names
   * @return Path
   */
  public static String buildPath(String componentContentPath, Set<String> mediaFormatNames) {
    SortedSet<String> sortedMediaFormatNames = new TreeSet<>(mediaFormatNames);
    return IPECONFIG_OVERLAY_ROOTPATH + componentContentPath
        + "/" + NN_MEDIA_FORMAT + "/" + StringUtils.join(sortedMediaFormatNames, "/")
        + "/" + NN_CONFIG;
  }

}