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     * @return a {@link SuffixBuilder} that discards all existing suffix state when constructing a new suffix
100    */
101   public static @NotNull SuffixBuilder thatDiscardsAllSuffixState() {
102     return new SuffixBuilder();
103   }
104 
105   /**
106    * @param request Sling request
107    * @return a {@link SuffixBuilder} that discards everything but the *resource* parts of the suffix
108    */
109   public static @NotNull SuffixBuilder thatKeepsResourceParts(@NotNull SlingHttpServletRequest request) {
110     Predicate<String> filter = new IncludeResourcePartsFilter();
111     return new SuffixBuilder(request, filter);
112   }
113 
114   /**
115    * @param request Sling request
116    * @param keysToKeep Keys to keep
117    * @return a {@link SuffixBuilder} that keeps only the named key/value-parts defined by pKeysToKeep
118    */
119   public static @NotNull SuffixBuilder thatKeepsNamedParts(@NotNull SlingHttpServletRequest request,
120       @NotNull String @NotNull... keysToKeep) {
121     Predicate<String> filter = new IncludeNamedPartsFilter(keysToKeep);
122     return new SuffixBuilder(request, filter);
123   }
124 
125   /**
126    * @param request Sling request
127    * @param keysToKeep Keys to keep
128    * @return a {@link SuffixBuilder} that keeps the named key/value-parts defined by pKeysToKeep and all resource
129    *         parts
130    */
131   public static @NotNull SuffixBuilder thatKeepsNamedPartsAndResources(@NotNull SlingHttpServletRequest request,
132       @NotNull String @NotNull... keysToKeep) {
133     Predicate<String> filter = FilterOperators.or(new IncludeResourcePartsFilter(), new IncludeNamedPartsFilter(keysToKeep));
134     return new SuffixBuilder(request, filter);
135   }
136 
137   /**
138    * @param request Sling request
139    * @return a {@link SuffixBuilder} that keeps all parts from the current request's suffix when constructing a new
140    *         suffix
141    */
142   public static @NotNull SuffixBuilder thatKeepsAllParts(@NotNull SlingHttpServletRequest request) {
143     return new SuffixBuilder(request, new IncludeAllPartsFilter());
144   }
145 
146   /**
147    * @param request Sling request
148    * @return a {@link SuffixBuilder} that will discard the resource parts, but keep all named key/value-parts
149    */
150   public static @NotNull SuffixBuilder thatDiscardsResourceParts(@NotNull SlingHttpServletRequest request) {
151     ExcludeResourcePartsFilter filter = new ExcludeResourcePartsFilter();
152     return new SuffixBuilder(request, filter);
153   }
154 
155   /**
156    * @param request Sling request
157    * @param keysToDiscard the keys of the named parts to discard
158    * @return a {@link SuffixBuilder} that will keep all parts except those named key/value-parts defined by
159    *         pKeysToDiscard
160    */
161   public static @NotNull SuffixBuilder thatDiscardsNamedParts(@NotNull SlingHttpServletRequest request,
162       @NotNull String @NotNull... keysToDiscard) {
163     return new SuffixBuilder(request, new ExcludeNamedPartsFilter(keysToDiscard));
164   }
165 
166   /**
167    * @param request Sling request
168    * @param keysToDiscard the keys of the named parts to discard
169    * @return {@link SuffixBuilder} that will discard all resource parts and the named parts defined by pKeysToDiscard
170    */
171   public static @NotNull SuffixBuilder thatDiscardsResourceAndNamedParts(@NotNull SlingHttpServletRequest request,
172       @NotNull String @NotNull... keysToDiscard) {
173     Predicate<String> filter = FilterOperators.and(new ExcludeResourcePartsFilter(), new ExcludeNamedPartsFilter(keysToDiscard));
174     return new SuffixBuilder(request, filter);
175   }
176 
177   /**
178    * @param request Sling request
179    * @param resourcePathToDiscard relative path of the resource to discard
180    * @param keysToDiscard the keys of the named parts to discard
181    * @return {@link SuffixBuilder} that will discard *one specific resource path* and the named parts defined by
182    *         pKeysToDiscard
183    */
184   public static @NotNull SuffixBuilder thatDiscardsSpecificResourceAndNamedParts(@NotNull SlingHttpServletRequest request,
185       @NotNull String resourcePathToDiscard, @NotNull String @NotNull... keysToDiscard) {
186     Predicate<String> filter = FilterOperators.and(new ExcludeSpecificResourceFilter(resourcePathToDiscard), new ExcludeNamedPartsFilter(keysToDiscard));
187     return new SuffixBuilder(request, filter);
188   }
189 
190   /**
191    * Puts a key-value pair into the suffix.
192    * @param key the key
193    * @param value the value
194    * @return this
195    */
196   @SuppressWarnings({ "null", "unused", "java:S2589" })
197   public @NotNull SuffixBuilder put(@NotNull String key, @NotNull Object value) {
198     if (key == null) {
199       throw new IllegalArgumentException("Key must not be null");
200     }
201     if (value != null) {
202       validateValueType(value);
203       parameterMap.put(key, value);
204     }
205     return this;
206   }
207 
208   /**
209    * Puts a map of key-value pairs into the suffix.
210    * @param map map of key-value pairs
211    * @return this
212    */
213   public @NotNull SuffixBuilder putAll(@NotNull Map<String, Object> map) {
214     for (Map.Entry<String, Object> entry : map.entrySet()) {
215       put(entry.getKey(), entry.getValue());
216     }
217     return this;
218   }
219 
220   private void validateValueType(Object value) {
221     Class<?> clazz = value.getClass();
222     boolean isValid = (clazz == String.class
223         || clazz == Boolean.class
224         || clazz == Integer.class
225         || clazz == Long.class);
226     if (!isValid) {
227       throw new IllegalArgumentException("Unsupported value type: " + clazz.getName());
228     }
229   }
230 
231   /**
232    * Puts a relative path of a resource into the suffix.
233    * @param resource the resource
234    * @param suffixBaseResource the base resource used to construct the relative path
235    * @return this
236    */
237   public @NotNull SuffixBuilder resource(@NotNull Resource resource, @NotNull Resource suffixBaseResource) {
238     // get relative path to base resource
239     String relativePath = getRelativePath(resource, suffixBaseResource);
240     resourcePaths.add(relativePath);
241     return this;
242   }
243 
244   /**
245    * Constructs a suffix that contains multiple key-value pairs and address resources. Depending on the
246    * {@link SuffixStateKeepingStrategy}, the suffix contains
247    * further parts from the current request that should be kept when constructing new links.
248    * @param resources resources to address
249    * @param baseResource base resource to construct relative path
250    * @return the suffix containing the map-content as encoded key value-pairs (and eventually other parts)
251    */
252   public @NotNull SuffixBuilder resources(@NotNull List<Resource> resources, @NotNull Resource baseResource) {
253     for (Resource resource : resources) {
254       resource(resource, baseResource);
255     }
256     return this;
257   }
258 
259   /**
260    * Puts a relative path of a page into the suffix.
261    * @param page the page
262    * @param suffixBasePage the base page used to construct the relative path
263    * @return this
264    */
265   public @NotNull SuffixBuilder page(@NotNull Page page, @NotNull Page suffixBasePage) {
266     return resource(AdaptTo.notNull(page, Resource.class), AdaptTo.notNull(suffixBasePage, Resource.class));
267   }
268 
269   /**
270    * Constructs a suffix that contains multiple key-value pairs and address pages. Depending on the
271    * {@link SuffixStateKeepingStrategy}, the suffix contains
272    * further parts from the current request that should be kept when constructing new links.
273    * @param pages pages to address
274    * @param suffixBasePage the base page used to construct the relative path
275    * @return this
276    */
277   @SuppressWarnings("null")
278   public @NotNull SuffixBuilder pages(@NotNull List<Page> pages, @NotNull Page suffixBasePage) {
279     List<Resource> resources = pages.stream()
280         .map(page -> page.adaptTo(Resource.class))
281         .collect(Collectors.toList());
282     return resources(resources, AdaptTo.notNull(suffixBasePage, Resource.class));
283   }
284 
285   /**
286    * Build complete suffix.
287    * @return the suffix
288    */
289   @SuppressWarnings("java:S2692") // 0 index skipped by intention
290   public @NotNull String build() {
291     SortedMap<String, Object> sortedParameterMap = new TreeMap<>(parameterMap);
292 
293     // gather resource paths in a treeset (having them in a defined order helps with caching)
294     SortedSet<String> resourcePathsSet = new TreeSet<>();
295 
296     // iterate over all parts that should be kept from the current request
297     for (String nextPart : initialSuffixParts) {
298       // if this is a key-value-part:
299       if (nextPart.indexOf(KEY_VALUE_DELIMITER) > 0) {
300         String key = decodeKey(nextPart);
301         // decode and keep the part if it is not overridden in the given parameter-map
302         if (!sortedParameterMap.containsKey(key)) {
303           String value = decodeValue(nextPart);
304           sortedParameterMap.put(key, value);
305         }
306       }
307       else {
308         // decode and keep the resource paths (unless they are specified again in resourcePaths)
309         String path = decodeResourcePathPart(nextPart);
310         if (!resourcePaths.contains(path)) {
311           resourcePathsSet.add(path);
312         }
313       }
314     }
315 
316     // copy the resources specified as parameters to the sorted set of paths
317     if (resourcePaths != null) {
318       resourcePathsSet.addAll(List.copyOf(resourcePaths));
319     }
320 
321     // gather all suffix parts in this list
322     List<String> suffixParts = new ArrayList<>();
323 
324     // now encode all resource paths
325     for (String path : resourcePathsSet) {
326       suffixParts.add(encodeResourcePathPart(path));
327     }
328 
329     // now encode all entries from the parameter map
330     for (Entry<String, Object> entry : sortedParameterMap.entrySet()) {
331       Object value = entry.getValue();
332       if (value == null) {
333         // don't add suffix part if value is null
334         continue;
335       }
336       String encodedKey = encodeKeyValuePart(entry.getKey());
337       String encodedValue = encodeKeyValuePart(value.toString());
338       suffixParts.add(encodedKey + KEY_VALUE_DELIMITER + encodedValue);
339     }
340 
341     // finally join these parts to a single string
342     return StringUtils.join(suffixParts, SUFFIX_PART_DELIMITER);
343   }
344 
345 }