ConfigurationReferenceProvider.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2017 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.caconfig.extensions.references.impl;

import static com.day.cq.dam.api.DamConstants.ACTIVITY_TYPE_ASSET;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.caconfig.management.ConfigurationManager;
import org.apache.sling.caconfig.management.ConfigurationResourceResolverConfig;
import org.apache.sling.caconfig.management.multiplexer.ConfigurationResourceResolvingStrategyMultiplexer;
import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.dam.api.Asset;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageFilter;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.PageManagerFactory;
import com.day.cq.wcm.api.reference.ReferenceProvider;

/**
 * This implementation of {@link ReferenceProvider} allows to resolve references of a given {@link Resource} to
 * context-aware configurations.
 *
 * <p>
 * This is for example used by ActivationReferenceSearchServlet to resolve referenced content of pages during activation
 * of a page using AEM sites. Returning the configurations and (if enabled) asset references allows the editor to activate
 * them along with the page referring to them.
 * </p>
 *
 * <p>
 * This component can be disabled by configuration, but its enabled by default.
 * </p>
 */
@Component(service = ReferenceProvider.class)
@Designate(ocd = ConfigurationReferenceProvider.Config.class)
public class ConfigurationReferenceProvider implements ReferenceProvider {

  @ObjectClassDefinition(name = "wcm.io Context-Aware Configuration Reference Provider",
      description = "Allows to resolve references from resources to their Context-Aware configuration pages "
          + "and referenced assets, for example during page activation.")
  @interface Config {

    @AttributeDefinition(name = "Enabled",
        description = "Enable this reference provider.")
    boolean enabled() default true;

    @AttributeDefinition(name = "Asset References",
        description = "Check for asset references within the context-aware configurations, and add them to the list of references.")
    boolean assetReferences() default false;

  }

  static final String REFERENCE_TYPE = "caconfig";

  @Reference
  private ConfigurationManager configurationManager;

  @Reference
  private ConfigurationResourceResolvingStrategyMultiplexer configurationResourceResolvingStrategy;

  @Reference
  private ConfigurationResourceResolverConfig configurationResourceResolverConfig;

  private boolean enabled;
  private boolean assetReferencesEnabled;

  private static final Logger log = LoggerFactory.getLogger(ConfigurationReferenceProvider.class);

  @Reference
  private PageManagerFactory pageManagerFactory;

  @Activate
  protected void activate(Config config) {
    enabled = config.enabled();
    assetReferencesEnabled = config.assetReferences();
  }

  @Deactivate
  protected void deactivate() {
    enabled = false;
  }

  @Override
  public List<com.day.cq.wcm.api.reference.Reference> findReferences(Resource resource) {
    if (!enabled) {
      return Collections.emptyList();
    }

    PageManager pageManager = pageManagerFactory.getPageManager(resource.getResourceResolver());
    if (pageManager == null) {
      throw new RuntimeException("No page manager.");
    }
    Page contextPage = pageManager.getContainingPage(resource);
    if (contextPage == null) {
      return Collections.emptyList();
    }

    Map<String, ConfigurationMetadata> configurationMetadatas = new TreeMap<>(configurationManager.getConfigurationNames().stream()
        .collect(Collectors.toMap(configName -> configName, configName -> configurationManager.getConfigurationMetadata(configName))));
    List<com.day.cq.wcm.api.reference.Reference> references = new ArrayList<>();
    Map<String, Asset> referencedAssets = new TreeMap<>();
    Set<String> configurationBuckets = new LinkedHashSet<>(configurationResourceResolverConfig.configBucketNames());

    for (String configurationName : configurationMetadatas.keySet()) {
      Iterator<Resource> configurationInheritanceChain = configurationResourceResolvingStrategy.getResourceInheritanceChain(resource, configurationBuckets,
          configurationName);

      // get all configuration pages from inheritance chain
      Collection<Page> referencePages = getReferencePages(configurationInheritanceChain, pageManager);

      // generate references for each page (but not if the context page itself is included as well)
      referencePages.stream()
          .filter(configPage -> !StringUtils.equals(contextPage.getPath(), configPage.getPath()))
          .forEach(configPage -> {
            references.add(toReference(resource, configPage, configurationMetadatas, configurationBuckets));
            // collect asset references
            if (assetReferencesEnabled && configPage.getContentResource() != null) {
              AssetRefereneDetector detector = new AssetRefereneDetector(configPage);
              detector.getReferencedAssets().stream().forEach(asset -> referencedAssets.put(asset.getPath(), asset));
            }
          });
    }

    if (!referencedAssets.isEmpty()) {
      // collect asset references detected in configuration pages (de-duplicated by using a map)
      referencedAssets.values().forEach(asset -> references.add(toReference(resource, asset)));
    }

    log.debug("Found {} references for resource {}", references.size(), resource.getPath());
    return references;
  }

  private Collection<Page> getReferencePages(@Nullable Iterator<Resource> configurationInheritanceChain, @NotNull PageManager pageManager) {
    Map<String, Page> referencePages = new LinkedHashMap<>();
    while (configurationInheritanceChain != null && configurationInheritanceChain.hasNext()) {
      Resource configurationResource = configurationInheritanceChain.next();

      // get page for configuration resource - and all children (e.g. for config collections)
      // collect in map to eliminate duplicate pages
      Page configPage = pageManager.getContainingPage(configurationResource);
      if (configPage != null) {
        referencePages.put(configPage.getPath(), configPage);
        Iterator<Page> deepChildren = configPage.listChildren(new PageFilter(false, true), true);
        while (deepChildren.hasNext()) {
          Page configChildPage = deepChildren.next();
          referencePages.put(configChildPage.getPath(), configChildPage);
        }
      }
    }
    return referencePages.values();
  }

  private com.day.cq.wcm.api.reference.Reference toReference(Resource resource, Page configPage,
      Map<String, ConfigurationMetadata> configurationMetadatas, Set<String> configurationBuckets) {
    log.trace("Found configuration reference {} for resource {}", configPage.getPath(), resource.getPath());
    return new com.day.cq.wcm.api.reference.Reference(REFERENCE_TYPE,
        getReferenceName(configPage, configurationMetadatas, configurationBuckets),
        configPage.adaptTo(Resource.class),
        getLastModifiedOf(configPage));
  }

  private com.day.cq.wcm.api.reference.Reference toReference(Resource resource, Asset asset) {
    log.trace("Found asset reference {} for resource {}", asset.getPath(), resource.getPath());
    return new com.day.cq.wcm.api.reference.Reference(ACTIVITY_TYPE_ASSET,
        asset.getName(), asset.adaptTo(Resource.class), asset.getLastModified());
  }

  /**
   * Build reference display name from path with:
   * - translating configuration names to labels
   * - omitting configuration bucket names
   * - insert additional spaces so long paths may wrap on multiple lines
   */
  private static String getReferenceName(Page configPage,
      Map<String, ConfigurationMetadata> configurationMetadatas, Set<String> configurationBuckets) {
    List<String> pathParts = Arrays.asList(StringUtils.split(configPage.getPath(), "/"));
    return pathParts.stream()
        .filter(name -> !configurationBuckets.contains(name))
        .map(name -> {
          ConfigurationMetadata configMetadata = configurationMetadatas.get(name);
          if (configMetadata != null && configMetadata.getLabel() != null) {
            return configMetadata.getLabel();
          }
          else {
            return name;
          }
        })
        .collect(Collectors.joining(" / "));
  }

  private static long getLastModifiedOf(Page page) {
    Calendar lastModified = page.getLastModified();
    return lastModified != null ? lastModified.getTimeInMillis() : 0;
  }

}