View Javadoc
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  
22  import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.KEY_VALUE_DELIMITER;
23  import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.SUFFIX_PART_DELIMITER;
24  import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.decodeKey;
25  import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.decodeResourcePathPart;
26  import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.decodeValue;
27  import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.encodeKeyValuePart;
28  import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.encodeResourcePathPart;
29  import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.getRelativePath;
30  
31  import java.util.ArrayList;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  import java.util.SortedMap;
37  import java.util.SortedSet;
38  import java.util.TreeMap;
39  import java.util.TreeSet;
40  import java.util.function.Predicate;
41  import java.util.stream.Collectors;
42  
43  import org.apache.commons.lang3.StringUtils;
44  import org.apache.sling.api.SlingHttpServletRequest;
45  import org.apache.sling.api.resource.Resource;
46  import org.jetbrains.annotations.NotNull;
47  import org.osgi.annotation.versioning.ProviderType;
48  
49  import com.day.cq.wcm.api.Page;
50  
51  import io.wcm.handler.url.suffix.impl.ExcludeNamedPartsFilter;
52  import io.wcm.handler.url.suffix.impl.ExcludeResourcePartsFilter;
53  import io.wcm.handler.url.suffix.impl.ExcludeSpecificResourceFilter;
54  import io.wcm.handler.url.suffix.impl.FilterOperators;
55  import io.wcm.handler.url.suffix.impl.IncludeAllPartsFilter;
56  import io.wcm.handler.url.suffix.impl.IncludeNamedPartsFilter;
57  import io.wcm.handler.url.suffix.impl.IncludeResourcePartsFilter;
58  import io.wcm.sling.commons.adapter.AdaptTo;
59  
60  /**
61   * Builds suffixes to be used in Sling URLs and that can be parsed with {@link SuffixParser}.
62   */
63  @ProviderType
64  public final class SuffixBuilder {
65  
66    private final List<String> initialSuffixParts;
67    private final Map<String, Object> parameterMap = new HashMap<>();
68    private final List<String> resourcePaths = new ArrayList<>();
69  
70    /**
71     * Create a {@link SuffixBuilder} which discards all existing suffix state when constructing a new suffix.
72     */
73    public SuffixBuilder() {
74      this.initialSuffixParts = new ArrayList<>();
75    }
76  
77    /**
78     * Create a {@link SuffixBuilder} with a custom {@link SuffixStateKeepingStrategy} (see convenience methods like
79     * {@link #thatKeepsResourceParts(SlingHttpServletRequest)} for often-used strategies)
80     * @param request Sling request
81     * @param stateStrategy the strategy to use to decide which parts of the suffix of the current request needs to be
82     *          kept in new constructed links
83     */
84    public SuffixBuilder(@NotNull SlingHttpServletRequest request, @NotNull SuffixStateKeepingStrategy stateStrategy) {
85      this.initialSuffixParts = stateStrategy.getSuffixPartsToKeep(request);
86    }
87  
88    /**
89     * Create a {@link SuffixBuilder} that keeps only the suffix parts matched by the given filter when constructing
90     * a new suffix
91     * @param request Sling request
92     * @param suffixPartFilter the filter that is called for each suffix part
93     */
94    public SuffixBuilder(@NotNull SlingHttpServletRequest request, @NotNull Predicate<String> suffixPartFilter) {
95      this(request, new FilteringSuffixStateStrategy(suffixPartFilter));
96    }
97  
98    /**
99     * Creates a suffix builder that discards all existing suffix state.
100    * @return a {@link SuffixBuilder} that discards all existing suffix state when constructing a new suffix
101    */
102   public static @NotNull SuffixBuilder thatDiscardsAllSuffixState() {
103     return new SuffixBuilder();
104   }
105 
106   /**
107    * Creates a suffix builder that keeps only resource parts.
108    * @param request Sling request
109    * @return a {@link SuffixBuilder} that discards everything but the *resource* parts of the suffix
110    */
111   public static @NotNull SuffixBuilder thatKeepsResourceParts(@NotNull SlingHttpServletRequest request) {
112     Predicate<String> filter = new IncludeResourcePartsFilter();
113     return new SuffixBuilder(request, filter);
114   }
115 
116   /**
117    * Creates a suffix builder that keeps only named parts.
118    * @param request Sling request
119    * @param keysToKeep Keys to keep
120    * @return a {@link SuffixBuilder} that keeps only the named key/value-parts defined by pKeysToKeep
121    */
122   public static @NotNull SuffixBuilder thatKeepsNamedParts(@NotNull SlingHttpServletRequest request,
123       @NotNull String @NotNull... keysToKeep) {
124     Predicate<String> filter = new IncludeNamedPartsFilter(keysToKeep);
125     return new SuffixBuilder(request, filter);
126   }
127 
128   /**
129    * Creates a suffix builder that keeps named parts and resource parts.
130    * @param request Sling request
131    * @param keysToKeep Keys to keep
132    * @return a {@link SuffixBuilder} that keeps the named key/value-parts defined by pKeysToKeep and all resource
133    *         parts
134    */
135   public static @NotNull SuffixBuilder thatKeepsNamedPartsAndResources(@NotNull SlingHttpServletRequest request,
136       @NotNull String @NotNull... keysToKeep) {
137     Predicate<String> filter = FilterOperators.or(new IncludeResourcePartsFilter(), new IncludeNamedPartsFilter(keysToKeep));
138     return new SuffixBuilder(request, filter);
139   }
140 
141   /**
142    * Creates a suffix builder that keeps all parts from the current request.
143    * @param request Sling request
144    * @return a {@link SuffixBuilder} that keeps all parts from the current request's suffix when constructing a new
145    *         suffix
146    */
147   public static @NotNull SuffixBuilder thatKeepsAllParts(@NotNull SlingHttpServletRequest request) {
148     return new SuffixBuilder(request, new IncludeAllPartsFilter());
149   }
150 
151   /**
152    * Creates a suffix builder that discards resource parts.
153    * @param request Sling request
154    * @return a {@link SuffixBuilder} that will discard the resource parts, but keep all named key/value-parts
155    */
156   public static @NotNull SuffixBuilder thatDiscardsResourceParts(@NotNull SlingHttpServletRequest request) {
157     ExcludeResourcePartsFilter filter = new ExcludeResourcePartsFilter();
158     return new SuffixBuilder(request, filter);
159   }
160 
161   /**
162    * Creates a suffix builder that discards named parts.
163    * @param request Sling request
164    * @param keysToDiscard the keys of the named parts to discard
165    * @return a {@link SuffixBuilder} that will keep all parts except those named key/value-parts defined by
166    *         pKeysToDiscard
167    */
168   public static @NotNull SuffixBuilder thatDiscardsNamedParts(@NotNull SlingHttpServletRequest request,
169       @NotNull String @NotNull... keysToDiscard) {
170     return new SuffixBuilder(request, new ExcludeNamedPartsFilter(keysToDiscard));
171   }
172 
173   /**
174    * Creates a suffix builder that discards resource and named parts.
175    * @param request Sling request
176    * @param keysToDiscard the keys of the named parts to discard
177    * @return {@link SuffixBuilder} that will discard all resource parts and the named parts defined by pKeysToDiscard
178    */
179   public static @NotNull SuffixBuilder thatDiscardsResourceAndNamedParts(@NotNull SlingHttpServletRequest request,
180       @NotNull String @NotNull... keysToDiscard) {
181     Predicate<String> filter = FilterOperators.and(new ExcludeResourcePartsFilter(), new ExcludeNamedPartsFilter(keysToDiscard));
182     return new SuffixBuilder(request, filter);
183   }
184 
185   /**
186    * Creates a suffix builder that discards a specific resource and named parts.
187    * @param request Sling request
188    * @param resourcePathToDiscard relative path of the resource to discard
189    * @param keysToDiscard the keys of the named parts to discard
190    * @return {@link SuffixBuilder} that will discard *one specific resource path* and the named parts defined by
191    *         pKeysToDiscard
192    */
193   public static @NotNull SuffixBuilder thatDiscardsSpecificResourceAndNamedParts(@NotNull SlingHttpServletRequest request,
194       @NotNull String resourcePathToDiscard, @NotNull String @NotNull... keysToDiscard) {
195     Predicate<String> filter = FilterOperators.and(new ExcludeSpecificResourceFilter(resourcePathToDiscard), new ExcludeNamedPartsFilter(keysToDiscard));
196     return new SuffixBuilder(request, filter);
197   }
198 
199   /**
200    * Puts a key-value pair into the suffix.
201    * @param key the key
202    * @param value the value
203    * @return this
204    */
205   @SuppressWarnings({ "null", "unused", "java:S2589" })
206   public @NotNull SuffixBuilder put(@NotNull String key, @NotNull Object value) {
207     if (key == null) {
208       throw new IllegalArgumentException("Key must not be null");
209     }
210     if (value != null) {
211       validateValueType(value);
212       parameterMap.put(key, value);
213     }
214     return this;
215   }
216 
217   /**
218    * Puts a map of key-value pairs into the suffix.
219    * @param map map of key-value pairs
220    * @return this
221    */
222   public @NotNull SuffixBuilder putAll(@NotNull Map<String, Object> map) {
223     for (Map.Entry<String, Object> entry : map.entrySet()) {
224       put(entry.getKey(), entry.getValue());
225     }
226     return this;
227   }
228 
229   private void validateValueType(Object value) {
230     Class<?> clazz = value.getClass();
231     boolean isValid = (clazz == String.class
232         || clazz == Boolean.class
233         || clazz == Integer.class
234         || clazz == Long.class);
235     if (!isValid) {
236       throw new IllegalArgumentException("Unsupported value type: " + clazz.getName());
237     }
238   }
239 
240   /**
241    * Puts a relative path of a resource into the suffix.
242    * @param resource the resource
243    * @param suffixBaseResource the base resource used to construct the relative path
244    * @return this
245    */
246   public @NotNull SuffixBuilder resource(@NotNull Resource resource, @NotNull Resource suffixBaseResource) {
247     // get relative path to base resource
248     String relativePath = getRelativePath(resource, suffixBaseResource);
249     resourcePaths.add(relativePath);
250     return this;
251   }
252 
253   /**
254    * Constructs a suffix that contains multiple key-value pairs and address resources. Depending on the
255    * {@link SuffixStateKeepingStrategy}, the suffix contains
256    * further parts from the current request that should be kept when constructing new links.
257    * @param resources resources to address
258    * @param baseResource base resource to construct relative path
259    * @return the suffix containing the map-content as encoded key value-pairs (and eventually other parts)
260    */
261   public @NotNull SuffixBuilder resources(@NotNull List<Resource> resources, @NotNull Resource baseResource) {
262     for (Resource resource : resources) {
263       resource(resource, baseResource);
264     }
265     return this;
266   }
267 
268   /**
269    * Puts a relative path of a page into the suffix.
270    * @param page the page
271    * @param suffixBasePage the base page used to construct the relative path
272    * @return this
273    */
274   public @NotNull SuffixBuilder page(@NotNull Page page, @NotNull Page suffixBasePage) {
275     return resource(AdaptTo.notNull(page, Resource.class), AdaptTo.notNull(suffixBasePage, Resource.class));
276   }
277 
278   /**
279    * Constructs a suffix that contains multiple key-value pairs and address pages. Depending on the
280    * {@link SuffixStateKeepingStrategy}, the suffix contains
281    * further parts from the current request that should be kept when constructing new links.
282    * @param pages pages to address
283    * @param suffixBasePage the base page used to construct the relative path
284    * @return this
285    */
286   @SuppressWarnings("null")
287   public @NotNull SuffixBuilder pages(@NotNull List<Page> pages, @NotNull Page suffixBasePage) {
288     List<Resource> resources = pages.stream()
289         .map(page -> page.adaptTo(Resource.class))
290         .collect(Collectors.toList());
291     return resources(resources, AdaptTo.notNull(suffixBasePage, Resource.class));
292   }
293 
294   /**
295    * Build complete suffix.
296    * @return the suffix
297    */
298   @SuppressWarnings("java:S2692") // 0 index skipped by intention
299   public @NotNull String build() {
300     SortedMap<String, Object> sortedParameterMap = new TreeMap<>(parameterMap);
301 
302     // gather resource paths in a treeset (having them in a defined order helps with caching)
303     SortedSet<String> resourcePathsSet = new TreeSet<>();
304 
305     // iterate over all parts that should be kept from the current request
306     for (String nextPart : initialSuffixParts) {
307       // if this is a key-value-part:
308       if (nextPart.indexOf(KEY_VALUE_DELIMITER) > 0) {
309         String key = decodeKey(nextPart);
310         // decode and keep the part if it is not overridden in the given parameter-map
311         if (!sortedParameterMap.containsKey(key)) {
312           String value = decodeValue(nextPart);
313           sortedParameterMap.put(key, value);
314         }
315       }
316       else {
317         // decode and keep the resource paths (unless they are specified again in resourcePaths)
318         String path = decodeResourcePathPart(nextPart);
319         if (!resourcePaths.contains(path)) {
320           resourcePathsSet.add(path);
321         }
322       }
323     }
324 
325     // copy the resources specified as parameters to the sorted set of paths
326     if (resourcePaths != null) {
327       resourcePathsSet.addAll(List.copyOf(resourcePaths));
328     }
329 
330     // gather all suffix parts in this list
331     List<String> suffixParts = new ArrayList<>();
332 
333     // now encode all resource paths
334     for (String path : resourcePathsSet) {
335       suffixParts.add(encodeResourcePathPart(path));
336     }
337 
338     // now encode all entries from the parameter map
339     for (Entry<String, Object> entry : sortedParameterMap.entrySet()) {
340       Object value = entry.getValue();
341       if (value == null) {
342         // don't add suffix part if value is null
343         continue;
344       }
345       String encodedKey = encodeKeyValuePart(entry.getKey());
346       String encodedValue = encodeKeyValuePart(value.toString());
347       suffixParts.add(encodedKey + KEY_VALUE_DELIMITER + encodedValue);
348     }
349 
350     // finally join these parts to a single string
351     return StringUtils.join(suffixParts, SUFFIX_PART_DELIMITER);
352   }
353 
354 }