ConfigurationReferenceProvider.java

  1. /*
  2.  * #%L
  3.  * wcm.io
  4.  * %%
  5.  * Copyright (C) 2017 wcm.io
  6.  * %%
  7.  * Licensed under the Apache License, Version 2.0 (the "License");
  8.  * you may not use this file except in compliance with the License.
  9.  * You may obtain a copy of the License at
  10.  *
  11.  *      http://www.apache.org/licenses/LICENSE-2.0
  12.  *
  13.  * Unless required by applicable law or agreed to in writing, software
  14.  * distributed under the License is distributed on an "AS IS" BASIS,
  15.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16.  * See the License for the specific language governing permissions and
  17.  * limitations under the License.
  18.  * #L%
  19.  */
  20. package io.wcm.caconfig.extensions.references.impl;

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

  22. import java.util.ArrayList;
  23. import java.util.Arrays;
  24. import java.util.Calendar;
  25. import java.util.Collection;
  26. import java.util.Collections;
  27. import java.util.Iterator;
  28. import java.util.LinkedHashMap;
  29. import java.util.LinkedHashSet;
  30. import java.util.List;
  31. import java.util.Map;
  32. import java.util.Set;
  33. import java.util.TreeMap;
  34. import java.util.stream.Collectors;

  35. import org.apache.commons.lang3.StringUtils;
  36. import org.apache.sling.api.resource.Resource;
  37. import org.apache.sling.caconfig.management.ConfigurationManager;
  38. import org.apache.sling.caconfig.management.ConfigurationResourceResolverConfig;
  39. import org.apache.sling.caconfig.management.multiplexer.ConfigurationResourceResolvingStrategyMultiplexer;
  40. import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
  41. import org.jetbrains.annotations.NotNull;
  42. import org.jetbrains.annotations.Nullable;
  43. import org.osgi.service.component.annotations.Activate;
  44. import org.osgi.service.component.annotations.Component;
  45. import org.osgi.service.component.annotations.Deactivate;
  46. import org.osgi.service.component.annotations.Reference;
  47. import org.osgi.service.metatype.annotations.AttributeDefinition;
  48. import org.osgi.service.metatype.annotations.Designate;
  49. import org.osgi.service.metatype.annotations.ObjectClassDefinition;
  50. import org.slf4j.Logger;
  51. import org.slf4j.LoggerFactory;

  52. import com.day.cq.dam.api.Asset;
  53. import com.day.cq.wcm.api.Page;
  54. import com.day.cq.wcm.api.PageFilter;
  55. import com.day.cq.wcm.api.PageManager;
  56. import com.day.cq.wcm.api.PageManagerFactory;
  57. import com.day.cq.wcm.api.reference.ReferenceProvider;

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

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

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

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

  85.   }

  86.   static final String REFERENCE_TYPE = "caconfig";

  87.   @Reference
  88.   private ConfigurationManager configurationManager;

  89.   @Reference
  90.   private ConfigurationResourceResolvingStrategyMultiplexer configurationResourceResolvingStrategy;

  91.   @Reference
  92.   private ConfigurationResourceResolverConfig configurationResourceResolverConfig;

  93.   private boolean enabled;
  94.   private boolean assetReferencesEnabled;

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

  96.   @Reference
  97.   private PageManagerFactory pageManagerFactory;

  98.   @Activate
  99.   protected void activate(Config config) {
  100.     enabled = config.enabled();
  101.     assetReferencesEnabled = config.assetReferences();
  102.   }

  103.   @Deactivate
  104.   protected void deactivate() {
  105.     enabled = false;
  106.   }

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

  112.     PageManager pageManager = pageManagerFactory.getPageManager(resource.getResourceResolver());
  113.     if (pageManager == null) {
  114.       throw new RuntimeException("No page manager.");
  115.     }
  116.     Page contextPage = pageManager.getContainingPage(resource);
  117.     if (contextPage == null) {
  118.       return Collections.emptyList();
  119.     }

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

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

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

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

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

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

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

  153.       // get page for configuration resource - and all children (e.g. for config collections)
  154.       // collect in map to eliminate duplicate pages
  155.       Page configPage = pageManager.getContainingPage(configurationResource);
  156.       if (configPage != null) {
  157.         referencePages.put(configPage.getPath(), configPage);
  158.         Iterator<Page> deepChildren = configPage.listChildren(new PageFilter(false, true), true);
  159.         while (deepChildren.hasNext()) {
  160.           Page configChildPage = deepChildren.next();
  161.           referencePages.put(configChildPage.getPath(), configChildPage);
  162.         }
  163.       }
  164.     }
  165.     return referencePages.values();
  166.   }

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

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

  180.   /**
  181.    * Build reference display name from path with:
  182.    * - translating configuration names to labels
  183.    * - omitting configuration bucket names
  184.    * - insert additional spaces so long paths may wrap on multiple lines
  185.    */
  186.   private static String getReferenceName(Page configPage,
  187.       Map<String, ConfigurationMetadata> configurationMetadatas, Set<String> configurationBuckets) {
  188.     List<String> pathParts = Arrays.asList(StringUtils.split(configPage.getPath(), "/"));
  189.     return pathParts.stream()
  190.         .filter(name -> !configurationBuckets.contains(name))
  191.         .map(name -> {
  192.           ConfigurationMetadata configMetadata = configurationMetadatas.get(name);
  193.           if (configMetadata != null && configMetadata.getLabel() != null) {
  194.             return configMetadata.getLabel();
  195.           }
  196.           else {
  197.             return name;
  198.           }
  199.         })
  200.         .collect(Collectors.joining(" / "));
  201.   }

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

  206. }