SuffixBuilder.java

  1. /*
  2.  * #%L
  3.  * wcm.io
  4.  * %%
  5.  * Copyright (C) 2014 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.handler.url.suffix;

  21. import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.KEY_VALUE_DELIMITER;
  22. import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.SUFFIX_PART_DELIMITER;
  23. import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.decodeKey;
  24. import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.decodeResourcePathPart;
  25. import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.decodeValue;
  26. import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.encodeKeyValuePart;
  27. import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.encodeResourcePathPart;
  28. import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.getRelativePath;

  29. import java.util.ArrayList;
  30. import java.util.HashMap;
  31. import java.util.List;
  32. import java.util.Map;
  33. import java.util.Map.Entry;
  34. import java.util.SortedMap;
  35. import java.util.SortedSet;
  36. import java.util.TreeMap;
  37. import java.util.TreeSet;
  38. import java.util.function.Predicate;
  39. import java.util.stream.Collectors;

  40. import org.apache.commons.lang3.StringUtils;
  41. import org.apache.sling.api.SlingHttpServletRequest;
  42. import org.apache.sling.api.resource.Resource;
  43. import org.jetbrains.annotations.NotNull;
  44. import org.osgi.annotation.versioning.ProviderType;

  45. import com.day.cq.wcm.api.Page;

  46. import io.wcm.handler.url.suffix.impl.ExcludeNamedPartsFilter;
  47. import io.wcm.handler.url.suffix.impl.ExcludeResourcePartsFilter;
  48. import io.wcm.handler.url.suffix.impl.ExcludeSpecificResourceFilter;
  49. import io.wcm.handler.url.suffix.impl.FilterOperators;
  50. import io.wcm.handler.url.suffix.impl.IncludeAllPartsFilter;
  51. import io.wcm.handler.url.suffix.impl.IncludeNamedPartsFilter;
  52. import io.wcm.handler.url.suffix.impl.IncludeResourcePartsFilter;
  53. import io.wcm.sling.commons.adapter.AdaptTo;

  54. /**
  55.  * Builds suffixes to be used in Sling URLs and that can be parsed with {@link SuffixParser}.
  56.  */
  57. @ProviderType
  58. public final class SuffixBuilder {

  59.   private final List<String> initialSuffixParts;
  60.   private final Map<String, Object> parameterMap = new HashMap<>();
  61.   private final List<String> resourcePaths = new ArrayList<>();

  62.   /**
  63.    * Create a {@link SuffixBuilder} which discards all existing suffix state when constructing a new suffix.
  64.    */
  65.   public SuffixBuilder() {
  66.     this.initialSuffixParts = new ArrayList<>();
  67.   }

  68.   /**
  69.    * Create a {@link SuffixBuilder} with a custom {@link SuffixStateKeepingStrategy} (see convenience methods like
  70.    * {@link #thatKeepsResourceParts(SlingHttpServletRequest)} for often-used strategies)
  71.    * @param request Sling request
  72.    * @param stateStrategy the strategy to use to decide which parts of the suffix of the current request needs to be
  73.    *          kept in new constructed links
  74.    */
  75.   public SuffixBuilder(@NotNull SlingHttpServletRequest request, @NotNull SuffixStateKeepingStrategy stateStrategy) {
  76.     this.initialSuffixParts = stateStrategy.getSuffixPartsToKeep(request);
  77.   }

  78.   /**
  79.    * Create a {@link SuffixBuilder} that keeps only the suffix parts matched by the given filter when constructing
  80.    * a new suffix
  81.    * @param request Sling request
  82.    * @param suffixPartFilter the filter that is called for each suffix part
  83.    */
  84.   public SuffixBuilder(@NotNull SlingHttpServletRequest request, @NotNull Predicate<String> suffixPartFilter) {
  85.     this(request, new FilteringSuffixStateStrategy(suffixPartFilter));
  86.   }

  87.   /**
  88.    * @return a {@link SuffixBuilder} that discards all existing suffix state when constructing a new suffix
  89.    */
  90.   public static @NotNull SuffixBuilder thatDiscardsAllSuffixState() {
  91.     return new SuffixBuilder();
  92.   }

  93.   /**
  94.    * @param request Sling request
  95.    * @return a {@link SuffixBuilder} that discards everything but the *resource* parts of the suffix
  96.    */
  97.   public static @NotNull SuffixBuilder thatKeepsResourceParts(@NotNull SlingHttpServletRequest request) {
  98.     Predicate<String> filter = new IncludeResourcePartsFilter();
  99.     return new SuffixBuilder(request, filter);
  100.   }

  101.   /**
  102.    * @param request Sling request
  103.    * @param keysToKeep Keys to keep
  104.    * @return a {@link SuffixBuilder} that keeps only the named key/value-parts defined by pKeysToKeep
  105.    */
  106.   public static @NotNull SuffixBuilder thatKeepsNamedParts(@NotNull SlingHttpServletRequest request,
  107.       @NotNull String @NotNull... keysToKeep) {
  108.     Predicate<String> filter = new IncludeNamedPartsFilter(keysToKeep);
  109.     return new SuffixBuilder(request, filter);
  110.   }

  111.   /**
  112.    * @param request Sling request
  113.    * @param keysToKeep Keys to keep
  114.    * @return a {@link SuffixBuilder} that keeps the named key/value-parts defined by pKeysToKeep and all resource
  115.    *         parts
  116.    */
  117.   public static @NotNull SuffixBuilder thatKeepsNamedPartsAndResources(@NotNull SlingHttpServletRequest request,
  118.       @NotNull String @NotNull... keysToKeep) {
  119.     Predicate<String> filter = FilterOperators.or(new IncludeResourcePartsFilter(), new IncludeNamedPartsFilter(keysToKeep));
  120.     return new SuffixBuilder(request, filter);
  121.   }

  122.   /**
  123.    * @param request Sling request
  124.    * @return a {@link SuffixBuilder} that keeps all parts from the current request's suffix when constructing a new
  125.    *         suffix
  126.    */
  127.   public static @NotNull SuffixBuilder thatKeepsAllParts(@NotNull SlingHttpServletRequest request) {
  128.     return new SuffixBuilder(request, new IncludeAllPartsFilter());
  129.   }

  130.   /**
  131.    * @param request Sling request
  132.    * @return a {@link SuffixBuilder} that will discard the resource parts, but keep all named key/value-parts
  133.    */
  134.   public static @NotNull SuffixBuilder thatDiscardsResourceParts(@NotNull SlingHttpServletRequest request) {
  135.     ExcludeResourcePartsFilter filter = new ExcludeResourcePartsFilter();
  136.     return new SuffixBuilder(request, filter);
  137.   }

  138.   /**
  139.    * @param request Sling request
  140.    * @param keysToDiscard the keys of the named parts to discard
  141.    * @return a {@link SuffixBuilder} that will keep all parts except those named key/value-parts defined by
  142.    *         pKeysToDiscard
  143.    */
  144.   public static @NotNull SuffixBuilder thatDiscardsNamedParts(@NotNull SlingHttpServletRequest request,
  145.       @NotNull String @NotNull... keysToDiscard) {
  146.     return new SuffixBuilder(request, new ExcludeNamedPartsFilter(keysToDiscard));
  147.   }

  148.   /**
  149.    * @param request Sling request
  150.    * @param keysToDiscard the keys of the named parts to discard
  151.    * @return {@link SuffixBuilder} that will discard all resource parts and the named parts defined by pKeysToDiscard
  152.    */
  153.   public static @NotNull SuffixBuilder thatDiscardsResourceAndNamedParts(@NotNull SlingHttpServletRequest request,
  154.       @NotNull String @NotNull... keysToDiscard) {
  155.     Predicate<String> filter = FilterOperators.and(new ExcludeResourcePartsFilter(), new ExcludeNamedPartsFilter(keysToDiscard));
  156.     return new SuffixBuilder(request, filter);
  157.   }

  158.   /**
  159.    * @param request Sling request
  160.    * @param resourcePathToDiscard relative path of the resource to discard
  161.    * @param keysToDiscard the keys of the named parts to discard
  162.    * @return {@link SuffixBuilder} that will discard *one specific resource path* and the named parts defined by
  163.    *         pKeysToDiscard
  164.    */
  165.   public static @NotNull SuffixBuilder thatDiscardsSpecificResourceAndNamedParts(@NotNull SlingHttpServletRequest request,
  166.       @NotNull String resourcePathToDiscard, @NotNull String @NotNull... keysToDiscard) {
  167.     Predicate<String> filter = FilterOperators.and(new ExcludeSpecificResourceFilter(resourcePathToDiscard), new ExcludeNamedPartsFilter(keysToDiscard));
  168.     return new SuffixBuilder(request, filter);
  169.   }

  170.   /**
  171.    * Puts a key-value pair into the suffix.
  172.    * @param key the key
  173.    * @param value the value
  174.    * @return this
  175.    */
  176.   @SuppressWarnings({ "null", "unused", "java:S2589" })
  177.   public @NotNull SuffixBuilder put(@NotNull String key, @NotNull Object value) {
  178.     if (key == null) {
  179.       throw new IllegalArgumentException("Key must not be null");
  180.     }
  181.     if (value != null) {
  182.       validateValueType(value);
  183.       parameterMap.put(key, value);
  184.     }
  185.     return this;
  186.   }

  187.   /**
  188.    * Puts a map of key-value pairs into the suffix.
  189.    * @param map map of key-value pairs
  190.    * @return this
  191.    */
  192.   public @NotNull SuffixBuilder putAll(@NotNull Map<String, Object> map) {
  193.     for (Map.Entry<String, Object> entry : map.entrySet()) {
  194.       put(entry.getKey(), entry.getValue());
  195.     }
  196.     return this;
  197.   }

  198.   private void validateValueType(Object value) {
  199.     Class<?> clazz = value.getClass();
  200.     boolean isValid = (clazz == String.class
  201.         || clazz == Boolean.class
  202.         || clazz == Integer.class
  203.         || clazz == Long.class);
  204.     if (!isValid) {
  205.       throw new IllegalArgumentException("Unsupported value type: " + clazz.getName());
  206.     }
  207.   }

  208.   /**
  209.    * Puts a relative path of a resource into the suffix.
  210.    * @param resource the resource
  211.    * @param suffixBaseResource the base resource used to construct the relative path
  212.    * @return this
  213.    */
  214.   public @NotNull SuffixBuilder resource(@NotNull Resource resource, @NotNull Resource suffixBaseResource) {
  215.     // get relative path to base resource
  216.     String relativePath = getRelativePath(resource, suffixBaseResource);
  217.     resourcePaths.add(relativePath);
  218.     return this;
  219.   }

  220.   /**
  221.    * Constructs a suffix that contains multiple key-value pairs and address resources. Depending on the
  222.    * {@link SuffixStateKeepingStrategy}, the suffix contains
  223.    * further parts from the current request that should be kept when constructing new links.
  224.    * @param resources resources to address
  225.    * @param baseResource base resource to construct relative path
  226.    * @return the suffix containing the map-content as encoded key value-pairs (and eventually other parts)
  227.    */
  228.   public @NotNull SuffixBuilder resources(@NotNull List<Resource> resources, @NotNull Resource baseResource) {
  229.     for (Resource resource : resources) {
  230.       resource(resource, baseResource);
  231.     }
  232.     return this;
  233.   }

  234.   /**
  235.    * Puts a relative path of a page into the suffix.
  236.    * @param page the page
  237.    * @param suffixBasePage the base page used to construct the relative path
  238.    * @return this
  239.    */
  240.   public @NotNull SuffixBuilder page(@NotNull Page page, @NotNull Page suffixBasePage) {
  241.     return resource(AdaptTo.notNull(page, Resource.class), AdaptTo.notNull(suffixBasePage, Resource.class));
  242.   }

  243.   /**
  244.    * Constructs a suffix that contains multiple key-value pairs and address pages. Depending on the
  245.    * {@link SuffixStateKeepingStrategy}, the suffix contains
  246.    * further parts from the current request that should be kept when constructing new links.
  247.    * @param pages pages to address
  248.    * @param suffixBasePage the base page used to construct the relative path
  249.    * @return this
  250.    */
  251.   @SuppressWarnings("null")
  252.   public @NotNull SuffixBuilder pages(@NotNull List<Page> pages, @NotNull Page suffixBasePage) {
  253.     List<Resource> resources = pages.stream()
  254.         .map(page -> page.adaptTo(Resource.class))
  255.         .collect(Collectors.toList());
  256.     return resources(resources, AdaptTo.notNull(suffixBasePage, Resource.class));
  257.   }

  258.   /**
  259.    * Build complete suffix.
  260.    * @return the suffix
  261.    */
  262.   @SuppressWarnings("java:S2692") // 0 index skipped by intention
  263.   public @NotNull String build() {
  264.     SortedMap<String, Object> sortedParameterMap = new TreeMap<>(parameterMap);

  265.     // gather resource paths in a treeset (having them in a defined order helps with caching)
  266.     SortedSet<String> resourcePathsSet = new TreeSet<>();

  267.     // iterate over all parts that should be kept from the current request
  268.     for (String nextPart : initialSuffixParts) {
  269.       // if this is a key-value-part:
  270.       if (nextPart.indexOf(KEY_VALUE_DELIMITER) > 0) {
  271.         String key = decodeKey(nextPart);
  272.         // decode and keep the part if it is not overridden in the given parameter-map
  273.         if (!sortedParameterMap.containsKey(key)) {
  274.           String value = decodeValue(nextPart);
  275.           sortedParameterMap.put(key, value);
  276.         }
  277.       }
  278.       else {
  279.         // decode and keep the resource paths (unless they are specified again in resourcePaths)
  280.         String path = decodeResourcePathPart(nextPart);
  281.         if (!resourcePaths.contains(path)) {
  282.           resourcePathsSet.add(path);
  283.         }
  284.       }
  285.     }

  286.     // copy the resources specified as parameters to the sorted set of paths
  287.     if (resourcePaths != null) {
  288.       resourcePathsSet.addAll(List.copyOf(resourcePaths));
  289.     }

  290.     // gather all suffix parts in this list
  291.     List<String> suffixParts = new ArrayList<>();

  292.     // now encode all resource paths
  293.     for (String path : resourcePathsSet) {
  294.       suffixParts.add(encodeResourcePathPart(path));
  295.     }

  296.     // now encode all entries from the parameter map
  297.     for (Entry<String, Object> entry : sortedParameterMap.entrySet()) {
  298.       Object value = entry.getValue();
  299.       if (value == null) {
  300.         // don't add suffix part if value is null
  301.         continue;
  302.       }
  303.       String encodedKey = encodeKeyValuePart(entry.getKey());
  304.       String encodedValue = encodeKeyValuePart(value.toString());
  305.       suffixParts.add(encodedKey + KEY_VALUE_DELIMITER + encodedValue);
  306.     }

  307.     // finally join these parts to a single string
  308.     return StringUtils.join(suffixParts, SUFFIX_PART_DELIMITER);
  309.   }

  310. }