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.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.splitSuffix;
27  
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.Objects;
31  import java.util.function.Predicate;
32  import java.util.stream.Collectors;
33  
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.commons.lang3.math.NumberUtils;
36  import org.apache.sling.api.SlingHttpServletRequest;
37  import org.apache.sling.api.resource.Resource;
38  import org.jetbrains.annotations.NotNull;
39  import org.jetbrains.annotations.Nullable;
40  import org.osgi.annotation.versioning.ProviderType;
41  
42  import com.day.cq.wcm.api.Page;
43  import com.day.cq.wcm.api.PageManager;
44  
45  import io.wcm.sling.commons.adapter.AdaptTo;
46  
47  /**
48   * Parses suffixes from Sling URLs build with {@link SuffixBuilder}.
49   */
50  @ProviderType
51  public final class SuffixParser {
52  
53    private final SlingHttpServletRequest request;
54  
55    /**
56     * Create a {@link SuffixParser} with the default {@link SuffixStateKeepingStrategy} (which discards all existing
57     * suffix state when constructing a new suffix)
58     * @param request Sling request
59     */
60    public SuffixParser(@NotNull SlingHttpServletRequest request) {
61      this.request = request;
62    }
63  
64    /**
65     * Extract the value of a named suffix part from this request's suffix
66     * @param key key of the suffix part
67     * @param clazz Type expected for return value.
68     *          Only String, Boolean, Integer, Long are supported.
69     * @param <T> Parameter type.
70     * @return the value of that named parameter (or the default value if not used)
71     */
72    @SuppressWarnings("unchecked")
73    public <T> @Nullable T get(@NotNull String key, @NotNull Class<T> clazz) {
74      if (clazz == String.class) {
75        return (T)getString(key, (String)null);
76      }
77      if (clazz == Boolean.class) {
78        return (T)(Boolean)getBoolean(key, false);
79      }
80      if (clazz == Integer.class) {
81        return (T)(Integer)getInt(key, 0);
82      }
83      if (clazz == Long.class) {
84        return (T)(Long)getLong(key, 0L);
85      }
86      throw new IllegalArgumentException("Unsupported type: " + clazz.getName());
87    }
88  
89    /**
90     * Extract the value of a named suffix part from this request's suffix
91     * @param key key of the suffix part
92     * @param defaultValue the default value to return if suffix part not set.
93     *          Only String, Boolean, Integer, Long are supported.
94     * @param <T> Parameter type.
95     * @return the value of that named parameter (or the default value if not used)
96     */
97    @SuppressWarnings("unchecked")
98    public <T> @Nullable T get(@NotNull String key, @Nullable T defaultValue) {
99      if (defaultValue instanceof String || defaultValue == null) {
100       return (T)getString(key, (String)defaultValue);
101     }
102     if (defaultValue instanceof Boolean) {
103       return (T)(Boolean)getBoolean(key, (Boolean)defaultValue);
104     }
105     if (defaultValue instanceof Integer) {
106       return (T)(Integer)getInt(key, (Integer)defaultValue);
107     }
108     if (defaultValue instanceof Long) {
109       return (T)(Long)getLong(key, (Long)defaultValue);
110     }
111     throw new IllegalArgumentException("Unsupported type: " + defaultValue.getClass().getName());
112   }
113 
114   private String getString(String key, String defaultValue) {
115     String value = findSuffixPartByKey(key);
116     if (value == null) {
117       return defaultValue;
118     }
119     return value;
120   }
121 
122   private boolean getBoolean(String key, boolean defaultValue) {
123     String value = findSuffixPartByKey(key);
124     if (value == null) {
125       return defaultValue;
126     }
127 
128     // value must match exactly "true" or "false"
129     if ("true".equals(value)) {
130       return true;
131     }
132     if ("false".equals(value)) {
133       return false;
134     }
135 
136     // invalid boolean value - return default
137     return defaultValue;
138   }
139 
140   private int getInt(String key, int defaultValue) {
141     String value = findSuffixPartByKey(key);
142     if (value == null) {
143       return defaultValue;
144     }
145     return NumberUtils.toInt(value, defaultValue);
146   }
147 
148   private long getLong(String key, long defaultValue) {
149     String value = findSuffixPartByKey(key);
150     if (value == null) {
151       return defaultValue;
152     }
153     return NumberUtils.toLong(value, defaultValue);
154   }
155 
156   /**
157    * Extract the value of a named suffix part from this request's suffix
158    * @param key key of the suffix part
159    * @return the value of that named parameter (or null if not used)
160    */
161   private String findSuffixPartByKey(String key) {
162     for (String part : splitSuffix(request.getRequestPathInfo().getSuffix())) {
163       if (part.indexOf(KEY_VALUE_DELIMITER) >= 0) {
164         String partKey = decodeKey(part);
165         if (partKey.equals(key)) {
166           return decodeValue(part);
167         }
168       }
169     }
170     return null;
171   }
172 
173   /**
174    * Get a resource within the current page by interpreting the suffix as a JCR path relative to this page's jcr:content
175    * node
176    * @return the Resource or null if no such resource exists
177    */
178   public @Nullable Resource getResource() {
179     return getResource((Predicate<Resource>)null, (Resource)null);
180   }
181 
182   /**
183    * Parse the suffix as resource paths and return the first resource that exists
184    * @param baseResource the suffix path is relative to this resource path (null for current page's jcr:content node)
185    * @return the resource or null if no such resource was selected by suffix
186    */
187   public @Nullable Resource getResource(@NotNull Resource baseResource) {
188     return getResource((Predicate<Resource>)null, baseResource);
189   }
190 
191   /**
192    * Parse the suffix as resource paths, return the first resource from the suffix (relativ to the current page's
193    * content) that matches the given filter.
194    * @param filter a filter that selects only the resource you're interested in.
195    * @return the resource or null if no such resource was selected by suffix
196    */
197   public @Nullable Resource getResource(@NotNull Predicate<Resource> filter) {
198     return getResource(filter, (Resource)null);
199   }
200 
201   /**
202    * Get the first item returned by {@link #getResources(Predicate, Resource)} or null if list is empty
203    * @param filter the resource filter
204    * @param baseResource the suffix path is relative to this resource path (null for current page's jcr:content node)
205    * @return the first {@link Resource} or null
206    */
207   public @Nullable Resource getResource(@Nullable Predicate<Resource> filter, @Nullable Resource baseResource) {
208     List<Resource> suffixResources = getResources(filter, baseResource);
209     if (suffixResources.isEmpty()) {
210       return null;
211     }
212     else {
213       return suffixResources.get(0);
214     }
215   }
216 
217   /**
218    * Get the resources within the current page selected in the suffix of the URL
219    * @return a list containing the Resources
220    */
221   public @NotNull List<Resource> getResources() {
222     return getResources((Predicate<Resource>)null, (Resource)null);
223   }
224 
225   /**
226    * Get the resources selected in the suffix of the URL
227    * @param baseResource the suffix path is relative to this resource path (null for current page's jcr:content node)
228    * @return a list containing the Resources
229    */
230   public @NotNull List<Resource> getResources(@NotNull Resource baseResource) {
231     return getResources((Predicate<Resource>)null, baseResource);
232   }
233 
234   /**
235    * Get the resources selected in the suffix of the URL
236    * @param filter optional filter to select only specific resources
237    * @return a list containing the Resources
238    */
239   public @NotNull List<Resource> getResources(@NotNull Predicate<Resource> filter) {
240     return getResources(filter, (Resource)null);
241   }
242 
243   /**
244    * Get the resources selected in the suffix of the URL
245    * @param filter optional filter to select only specific resources
246    * @param baseResource the suffix path is relative to this resource path (null for current page's jcr:content node)
247    * @return a list containing the Resources
248    */
249   @SuppressWarnings("java:S112") // allow runtime exception
250   public @NotNull List<Resource> getResources(@Nullable Predicate<Resource> filter, @Nullable Resource baseResource) {
251 
252     // resolve base path or fallback to current page's content if not specified
253     Resource baseResourceToUse = baseResource;
254     if (baseResourceToUse == null) {
255       PageManager pageManager = request.getResourceResolver().adaptTo(PageManager.class);
256       if (pageManager == null) {
257         throw new RuntimeException("No page manager.");
258       }
259       Page currentPage = pageManager.getContainingPage(request.getResource());
260       if (currentPage != null) {
261         baseResourceToUse = currentPage.getContentResource();
262       }
263       else {
264         baseResourceToUse = request.getResource();
265       }
266     }
267     return getResourcesWithBaseResource(filter, baseResourceToUse);
268   }
269 
270   /**
271    * Parse the suffix as page paths and return the first page that exists with a page path relative
272    * to the current page path.
273    * @return the page or null if no such page was selected by suffix
274    */
275   public @Nullable Page getPage() {
276     return getPage((Predicate<Page>)null, (Page)null);
277   }
278 
279   /**
280    * Parse the suffix as page paths and return the first page that exists.
281    * @param basePage the suffix page is relative to this page path (null for current page)
282    * @return the page or null if no such page was selected by suffix
283    */
284   public @Nullable Page getPage(@NotNull Page basePage) {
285     return getPage((Predicate<Page>)null, basePage);
286   }
287 
288   /**
289    * Parse the suffix as page paths, return the first page from the suffix (relativ to the current page) that matches the given filter.
290    *
291    * @param filter a filter that selects only the page you're interested in.
292    * @return the page or null if no such page was selected by suffix
293    */
294   public @Nullable Page getPage(@NotNull Predicate<Page> filter) {
295     return getPage(filter, (Page)null);
296   }
297 
298   /**
299    * Get the first item returned by {@link #getPages(Predicate, Page)} or null if list is empty
300    * @param filter the resource filter
301    * @param basePage the suffix path is relative to this page path (null for current page)
302    * @return the first {@link Page} or null
303    */
304   public @Nullable Page getPage(@Nullable Predicate<Page> filter, @Nullable Page basePage) {
305     List<Page> suffixPages = getPages(filter, basePage);
306     if (suffixPages.isEmpty()) {
307       return null;
308     }
309     else {
310       return suffixPages.get(0);
311     }
312   }
313 
314   /**
315    * Get the pages selected in the suffix of the URL with page paths relative
316    * to the current page path.
317    * @return a list containing the Pages
318    */
319   public @NotNull List<Page> getPages() {
320     return getPages((Predicate<Page>)null, (Page)null);
321   }
322 
323   /**
324    * Get the pages selected in the suffix of the URL with page paths relative
325    * to the current page path.
326    * @param filter optional filter to select only specific pages
327    * @return a list containing the Pages
328    */
329   public @NotNull List<Page> getPages(@NotNull Predicate<Page> filter) {
330     return getPages(filter, (Page)null);
331   }
332 
333   /**
334    * Get the pages selected in the suffix of the URL
335    * @param filter optional filter to select only specific pages
336    * @param basePage the suffix path is relative to this page path (null for current page)
337    * @return a list containing the Pages
338    */
339   @SuppressWarnings("null")
340   public @NotNull List<Page> getPages(@Nullable final Predicate<Page> filter, @Nullable final Page basePage) {
341     Resource baseResourceToUse = null;
342 
343     // detect pages page to use
344     if (basePage == null) {
345       PageManager pageManager = AdaptTo.notNull(request.getResourceResolver(), PageManager.class);
346       Page currentPage = pageManager.getContainingPage(request.getResource());
347       if (currentPage != null) {
348         baseResourceToUse = currentPage.adaptTo(Resource.class);
349       }
350     }
351     else {
352       baseResourceToUse = basePage.adaptTo(Resource.class);
353     }
354 
355     // filter pages (as resources)
356     Predicate<Resource> resourceFilter = resource -> {
357       Page page = resource.adaptTo(Page.class);
358       if (page == null) {
359         return false;
360       }
361       if (filter == null) {
362         return true;
363       }
364       return filter.test(page);
365     };
366     List<Resource> resources = getResourcesWithBaseResource(resourceFilter, baseResourceToUse);
367 
368     // convert resources back to pages
369     return resources.stream()
370         .filter(Objects::nonNull)
371         .map(resource -> resource.adaptTo(Page.class))
372         .filter(Objects::nonNull)
373         .collect(Collectors.toList());
374   }
375 
376   @SuppressWarnings("java:S135") // allow multiple continues
377   private @NotNull List<Resource> getResourcesWithBaseResource(@Nullable Predicate<Resource> filter, @Nullable Resource baseResource) {
378     // split the suffix to extract the paths of the selected components
379     String[] suffixParts = splitSuffix(request.getRequestPathInfo().getSuffix());
380 
381     // iterate over all parts and gather those resources
382     List<Resource> selectedResources = new ArrayList<>();
383     for (String path : suffixParts) {
384 
385       // if path contains the key/value-delimiter then don't try to resolve it as a content path
386       if (StringUtils.contains(path, KEY_VALUE_DELIMITER)) {
387         continue;
388       }
389 
390       String decodedPath = decodeResourcePathPart(path);
391 
392       // lookup the resource specified by the path (which is relative to the current page's content resource)
393       Resource resource = request.getResourceResolver().getResource(baseResource, decodedPath);
394       if (resource == null) {
395         // no resource found with given path, continue with next path in suffix
396         continue;
397       }
398 
399       // if a filter is given - check
400       if (filter == null || filter.test(resource)) {
401         selectedResources.add(resource);
402       }
403 
404     }
405 
406     return selectedResources;
407   }
408 
409 }