ComponentPropertyResolver.java

  1. /*
  2.  * #%L
  3.  * wcm.io
  4.  * %%
  5.  * Copyright (C) 2019 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.wcm.commons.component;

  21. import java.util.Collection;
  22. import java.util.Map;

  23. import org.apache.commons.collections4.IterableUtils;
  24. import org.apache.commons.lang3.StringUtils;
  25. import org.apache.sling.api.resource.LoginException;
  26. import org.apache.sling.api.resource.Resource;
  27. import org.apache.sling.api.resource.ResourceResolver;
  28. import org.apache.sling.api.resource.ResourceResolverFactory;
  29. import org.apache.sling.api.resource.ResourceUtil;
  30. import org.jetbrains.annotations.NotNull;
  31. import org.jetbrains.annotations.Nullable;
  32. import org.osgi.annotation.versioning.ProviderType;
  33. import org.slf4j.Logger;
  34. import org.slf4j.LoggerFactory;

  35. import com.day.cq.wcm.api.Page;
  36. import com.day.cq.wcm.api.PageManager;
  37. import com.day.cq.wcm.api.components.Component;
  38. import com.day.cq.wcm.api.components.ComponentContext;
  39. import com.day.cq.wcm.api.components.ComponentManager;
  40. import com.day.cq.wcm.api.policies.ContentPolicy;
  41. import com.day.cq.wcm.api.policies.ContentPolicyManager;

  42. import io.wcm.sling.commons.adapter.AdaptTo;

  43. /**
  44.  * Tries to resolve properties with or without inheritance from pages, content policies or component definitions.
  45.  * <p>
  46.  * The lookup can take place in:
  47.  * </p>
  48.  * <ol>
  49.  * <li>Properties of the current page (including the parent pages if inheritance is enabled)</li>
  50.  * <li>Properties from the content policy associated with the current resource</li>
  51.  * <li>Properties defined on the component associated with the current resource (including super components if
  52.  * inheritance is enabled)</li>
  53.  * </ol>
  54.  * <p>
  55.  * By default, only option 3 is enabled (with inheritance).
  56.  * Please make sure to {@link #close()} instances of this class after usage.
  57.  * </p>
  58.  */
  59. @ProviderType
  60. public final class ComponentPropertyResolver implements AutoCloseable {

  61.   private ComponentPropertyResolution componentPropertiesResolution = ComponentPropertyResolution.RESOLVE_INHERIT;
  62.   private ComponentPropertyResolution pagePropertiesResolution = ComponentPropertyResolution.IGNORE;
  63.   private ComponentPropertyResolution contentPolicyResolution = ComponentPropertyResolution.IGNORE;
  64.   private final Page currentPage;
  65.   private final Component currentComponent;
  66.   private final Resource resource;
  67.   private final ResourceResolverFactory resourceResolverFactory;
  68.   private ResourceResolver componentsResourceResolver;
  69.   private boolean initComponentsResourceResolverFailed;

  70.   private static final String SERVICEUSER_SUBSERVICE = "component-properties";

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

  72.   /**
  73.    * This constructor is for internal use only, please use {@link ComponentPropertyResolverFactory}.
  74.    * @param page Content page
  75.    * @param resourceResolverFactory Resource resolver factory
  76.    */
  77.   public ComponentPropertyResolver(@NotNull Page page,
  78.       @Nullable ResourceResolverFactory resourceResolverFactory) {
  79.     this(page.getContentResource(), resourceResolverFactory);
  80.   }

  81.   /**
  82.    * This constructor is for internal use only, please use {@link ComponentPropertyResolverFactory}.
  83.    * @param resource Content resource
  84.    * @param resourceResolverFactory Resource resolver factory
  85.    */
  86.   public ComponentPropertyResolver(@NotNull Resource resource,
  87.       @Nullable ResourceResolverFactory resourceResolverFactory) {
  88.     this(resource, false, resourceResolverFactory);
  89.   }

  90.   /**
  91.    * This constructor is for internal use only, please use {@link ComponentPropertyResolverFactory}.
  92.    * @param resource Content resource
  93.    * @param ensureResourceType Ensure the given resource has a resource type.
  94.    *          If this is not the case, try to find the closest parent resource which has a resource type.
  95.    * @param resourceResolverFactory Resource resolver factory
  96.    */
  97.   public ComponentPropertyResolver(@NotNull Resource resource, boolean ensureResourceType,
  98.       @Nullable ResourceResolverFactory resourceResolverFactory) {
  99.     Resource contextResource = null;
  100.     if (ensureResourceType) {
  101.       // find closest parent resource that has a resource type (and not nt:unstructured)
  102.       contextResource = getResourceWithResourceType(resource);
  103.     }
  104.     if (contextResource == null) {
  105.       contextResource = resource;
  106.     }

  107.     ResourceResolver resourceResolver = contextResource.getResourceResolver();
  108.     PageManager pageManager = AdaptTo.notNull(resourceResolver, PageManager.class);
  109.     this.currentPage = pageManager.getContainingPage(contextResource);
  110.     if (hasResourceType(contextResource)) {
  111.       ComponentManager componentManager = AdaptTo.notNull(resourceResolver, ComponentManager.class);
  112.       this.currentComponent = componentManager.getComponentOfResource(contextResource);
  113.     }
  114.     else {
  115.       this.currentComponent = null;
  116.     }
  117.     this.resource = contextResource;
  118.     this.resourceResolverFactory = resourceResolverFactory;
  119.   }

  120.   /**
  121.    * This constructor is for internal use only, please use {@link ComponentPropertyResolverFactory}.
  122.    * @param wcmComponentContext WCM component context
  123.    * @param resourceResolverFactory Resource resolver factory
  124.    */
  125.   public ComponentPropertyResolver(@NotNull ComponentContext wcmComponentContext,
  126.       @Nullable ResourceResolverFactory resourceResolverFactory) {
  127.     this.currentPage = wcmComponentContext.getPage();
  128.     this.currentComponent = wcmComponentContext.getComponent();
  129.     this.resource = wcmComponentContext.getResource();
  130.     this.resourceResolverFactory = resourceResolverFactory;
  131.   }

  132.   /**
  133.    * Lookup for content resource associated with the page component (resource type).
  134.    * @param page Content page
  135.    * @deprecated Please use {@link ComponentPropertyResolverFactory}.
  136.    */
  137.   @Deprecated(since = "1.6.0")
  138.   public ComponentPropertyResolver(@NotNull Page page) {
  139.     this(page, null);
  140.   }

  141.   /**
  142.    * Lookup for content resource associated with a component (resource type).
  143.    * @param resource Content resource
  144.    * @deprecated Please use {@link ComponentPropertyResolverFactory}.
  145.    */
  146.   @Deprecated(since = "1.6.0")
  147.   public ComponentPropertyResolver(@NotNull Resource resource) {
  148.     this(resource, null);
  149.   }

  150.   /**
  151.    * Lookup for content resource associated with a component (resource type).
  152.    * @param resource Content resource
  153.    * @param ensureResourceType Ensure the given resource has a resource type.
  154.    *          If this is not the case, try to find the closest parent resource which has a resource type.
  155.    * @deprecated Please use {@link ComponentPropertyResolverFactory}.
  156.    */
  157.   @Deprecated(since = "1.6.0")
  158.   public ComponentPropertyResolver(@NotNull Resource resource, boolean ensureResourceType) {
  159.     this(resource, ensureResourceType, null);
  160.   }

  161.   /**
  162.    * Lookup with content resource associated with a component (resource type).
  163.    * @param wcmComponentContext WCM component context
  164.    * @deprecated Please use {@link ComponentPropertyResolverFactory}.
  165.    */
  166.   @Deprecated(since = "1.6.0")
  167.   public ComponentPropertyResolver(@NotNull ComponentContext wcmComponentContext) {
  168.     this(wcmComponentContext, null);
  169.   }

  170.   private static boolean hasResourceType(@NotNull Resource resource) {
  171.     return StringUtils.isNotEmpty(resource.getResourceType());
  172.   }

  173.   @SuppressWarnings({ "null", "java:S2589" }) // extra null checks for backward compatibility
  174.   private static @Nullable Resource getResourceWithResourceType(@Nullable Resource resource) {
  175.     if (resource == null) {
  176.       return null;
  177.     }
  178.     String resourceType = resource.getResourceType();
  179.     if (resourceType != null && isPathBasedResourceType(resourceType)) {
  180.       return resource;
  181.     }
  182.     return getResourceWithResourceType(resource.getParent());
  183.   }

  184.   /**
  185.    * Checks if the resource type is a path pointing to a component resource in the repository
  186.    * (where we can lookup properties to inherit from).
  187.    * @param resourceType Resource type
  188.    * @return true for path-based resource types
  189.    */
  190.   private static boolean isPathBasedResourceType(@NotNull String resourceType) {
  191.     return StringUtils.contains(resourceType, "/");
  192.   }

  193.   /**
  194.    * Configure if properties should be resolved in component properties, and with or without inheritance.
  195.    * Default mode is {@link ComponentPropertyResolution#RESOLVE_INHERIT}.
  196.    * @param resolution Resolution mode
  197.    * @return this
  198.    */
  199.   public ComponentPropertyResolver componentPropertiesResolution(@NotNull ComponentPropertyResolution resolution) {
  200.     this.componentPropertiesResolution = resolution;
  201.     return this;
  202.   }

  203.   /**
  204.    * Configure if properties should be resolved in content page properties, and with or without inheritance.
  205.    * Default mode is {@link ComponentPropertyResolution#IGNORE}.
  206.    * @param resolution Resolution mode
  207.    * @return this
  208.    */
  209.   public ComponentPropertyResolver pagePropertiesResolution(@NotNull ComponentPropertyResolution resolution) {
  210.     this.pagePropertiesResolution = resolution;
  211.     return this;
  212.   }

  213.   /**
  214.    * Configure if properties should be resolved from content policies mapped for the given resource.
  215.    * No explicit inheritance mode is supported, so {@link ComponentPropertyResolution#RESOLVE_INHERIT}
  216.    * has the same effect as {@link ComponentPropertyResolution#RESOLVE} in this case.
  217.    * Default mode is {@link ComponentPropertyResolution#IGNORE}.
  218.    * @param resolution Resolution mode
  219.    * @return this
  220.    */
  221.   public ComponentPropertyResolver contentPolicyResolution(@NotNull ComponentPropertyResolution resolution) {
  222.     this.contentPolicyResolution = resolution;
  223.     return this;
  224.   }

  225.   /**
  226.    * Get property.
  227.    * @param name Property name
  228.    * @param type Property type
  229.    * @param <T> Parameter type
  230.    * @return Property value or null if not set
  231.    */
  232.   public @Nullable <T> T get(@NotNull String name, @NotNull Class<T> type) {
  233.     @Nullable
  234.     T value = getPageProperty(currentPage, name, type);
  235.     if (value == null) {
  236.       value = getContentPolicyProperty(name, type);
  237.     }
  238.     if (value == null) {
  239.       value = getComponentProperty(currentComponent, name, type);
  240.     }
  241.     return value;
  242.   }

  243.   /**
  244.    * Get property.
  245.    * @param name Property name
  246.    * @param defaultValue Default value
  247.    * @param <T> Parameter type
  248.    * @return Property value or default value if not set
  249.    */
  250.   public @NotNull <T> T get(@NotNull String name, @NotNull T defaultValue) {
  251.     @Nullable
  252.     @SuppressWarnings("unchecked")
  253.     T value = get(name, (Class<T>)defaultValue.getClass());
  254.     if (value != null) {
  255.       return value;
  256.     }
  257.     else {
  258.       return defaultValue;
  259.     }
  260.   }

  261.   private @Nullable <T> T getComponentProperty(@Nullable Component component,
  262.       @NotNull String name, @NotNull Class<T> type) {
  263.     if (componentPropertiesResolution == ComponentPropertyResolution.IGNORE || component == null) {
  264.       return null;
  265.     }
  266.     @Nullable
  267.     T result;
  268.     if (StringUtils.contains(name, "/")) {
  269.       // if a property in child resource is addressed get property value via local resource
  270.       // because the map behind the getProperties() method does not support child resource access
  271.       String childResourcePath = StringUtils.substringBeforeLast(name, "/");
  272.       String localPropertyName = StringUtils.substringAfterLast(name, "/");
  273.       Resource childResource = getLocalComponentResource(component, childResourcePath);
  274.       result = ResourceUtil.getValueMap(childResource).get(localPropertyName, type);
  275.     }
  276.     else {
  277.       result = component.getProperties().get(name, type);
  278.     }
  279.     if (result == null && componentPropertiesResolution == ComponentPropertyResolution.RESOLVE_INHERIT) {
  280.       result = getComponentProperty(component.getSuperComponent(), name, type);
  281.     }
  282.     return result;
  283.   }

  284.   private @Nullable <T> T getPageProperty(@Nullable Page page,
  285.       @NotNull String name, @NotNull Class<T> type) {
  286.     if (pagePropertiesResolution == ComponentPropertyResolution.IGNORE || page == null) {
  287.       return null;
  288.     }
  289.     @Nullable
  290.     T result = page.getProperties().get(name, type);
  291.     if (result == null && pagePropertiesResolution == ComponentPropertyResolution.RESOLVE_INHERIT) {
  292.       result = getPageProperty(page.getParent(), name, type);
  293.     }
  294.     return result;
  295.   }

  296.   private @Nullable <T> T getContentPolicyProperty(@NotNull String name, @NotNull Class<T> type) {
  297.     if (contentPolicyResolution == ComponentPropertyResolution.IGNORE || resource == null) {
  298.       return null;
  299.     }
  300.     ContentPolicy policy = getPolicy(resource);
  301.     if (policy != null) {
  302.       return policy.getProperties().get(name, type);
  303.     }
  304.     return null;
  305.   }

  306.   /**
  307.    * Get list of child resources.
  308.    * @param name Child node name
  309.    * @return List of child resources or null if not set.
  310.    */
  311.   public @Nullable Collection<Resource> getResources(@NotNull String name) {
  312.     Collection<Resource> list = getPageResources(currentPage, name);
  313.     if (list == null) {
  314.       list = getContentPolicyResources(name);
  315.     }
  316.     if (list == null) {
  317.       list = getComponentResources(currentComponent, name);
  318.     }
  319.     return list;
  320.   }

  321.   private @Nullable Collection<Resource> getComponentResources(@Nullable Component component, @NotNull String name) {
  322.     if (componentPropertiesResolution == ComponentPropertyResolution.IGNORE || component == null) {
  323.       return null;
  324.     }
  325.     Collection<Resource> result = getResources(getLocalComponentResource(component, name));
  326.     if (result == null && componentPropertiesResolution == ComponentPropertyResolution.RESOLVE_INHERIT) {
  327.       result = getComponentResources(component.getSuperComponent(), name);
  328.     }
  329.     return result;
  330.   }

  331.   private @Nullable Collection<Resource> getPageResources(@Nullable Page page, @NotNull String name) {
  332.     if (pagePropertiesResolution == ComponentPropertyResolution.IGNORE || page == null) {
  333.       return null;
  334.     }
  335.     Collection<Resource> result = getResources(page.getContentResource(name));
  336.     if (result == null && pagePropertiesResolution == ComponentPropertyResolution.RESOLVE_INHERIT) {
  337.       result = getPageResources(page.getParent(), name);
  338.     }
  339.     return result;
  340.   }

  341.   @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
  342.   private @Nullable Collection<Resource> getContentPolicyResources(@NotNull String name) {
  343.     if (contentPolicyResolution == ComponentPropertyResolution.IGNORE || resource == null) {
  344.       return null;
  345.     }
  346.     ContentPolicy policy = getPolicy(resource);
  347.     if (policy != null) {
  348.       Resource policyResource = policy.adaptTo(Resource.class);
  349.       if (policyResource != null) {
  350.         return getResources(policyResource.getChild(name));
  351.       }
  352.     }
  353.     return null;
  354.   }

  355.   private @Nullable Collection<Resource> getResources(@Nullable Resource parent) {
  356.     if (parent == null) {
  357.       return null;
  358.     }
  359.     Collection<Resource> children = IterableUtils.toList(parent.getChildren());
  360.     if (children.isEmpty()) {
  361.       return null;
  362.     }
  363.     return children;
  364.   }

  365.   /**
  366.    * Get content policy via policy manager.
  367.    * @param resource Content resource
  368.    * @return Policy or null
  369.    */
  370.   private static @Nullable ContentPolicy getPolicy(@NotNull Resource resource) {
  371.     ContentPolicyManager policyManager = AdaptTo.notNull(resource.getResourceResolver(), ContentPolicyManager.class);
  372.     return policyManager.getPolicy(resource);
  373.   }

  374.   /**
  375.    * Get local child resource for component, with a special handling for publish environments where
  376.    * the local child resources for components below /apps are not accessible for everyone.
  377.    * @param component Component
  378.    * @param childResourcePath Child resource path
  379.    * @return Resource or null
  380.    */
  381.   private @Nullable Resource getLocalComponentResource(@NotNull Component component,
  382.       @NotNull String childResourcePath) {
  383.     if (componentsResourceResolver == null
  384.         && resourceResolverFactory != null
  385.         && !initComponentsResourceResolverFailed) {
  386.       try {
  387.         componentsResourceResolver = resourceResolverFactory.getServiceResourceResolver(
  388.             Map.of(ResourceResolverFactory.SUBSERVICE, SERVICEUSER_SUBSERVICE));
  389.       }
  390.       catch (LoginException ex) {
  391.         initComponentsResourceResolverFailed = true;
  392.         if (log.isDebugEnabled()) {
  393.           log.debug("Unable to get resource resolver for accessing local component resource, "
  394.               + "please make sure to grant access to system user 'sling-scripting' for "
  395.               + "bundle 'io.wcm.wcm.commons', subservice '{}'.", SERVICEUSER_SUBSERVICE, ex);
  396.         }
  397.       }
  398.     }
  399.     if (componentsResourceResolver != null) {
  400.       String resourcePath = component.getPath() + "/" + childResourcePath;
  401.       return componentsResourceResolver.getResource(resourcePath);
  402.     }
  403.     // fallback implementation for previous behavior - this will usually not work in publish instances
  404.     return component.getLocalResource(childResourcePath);
  405.   }

  406.   @Override
  407.   public void close() {
  408.     if (componentsResourceResolver != null) {
  409.       componentsResourceResolver.close();
  410.     }
  411.   }

  412. }