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
290    * the given filter.
291    *
292    * @param filter a filter that selects only the page you're interested in.
293    * @return the page or null if no such page was selected by suffix
294    */
295   public @Nullable Page getPage(@NotNull Predicate<Page> filter) {
296     return getPage(filter, (Page)null);
297   }
298 
299   /**
300    * Get the first item returned by {@link #getPages(Predicate, Page)} or null if list is empty
301    * @param filter the resource filter
302    * @param basePage the suffix path is relative to this page path (null for current page)
303    * @return the first {@link Page} or null
304    */
305   public @Nullable Page getPage(@Nullable Predicate<Page> filter, @Nullable Page basePage) {
306     List<Page> suffixPages = getPages(filter, basePage);
307     if (suffixPages.isEmpty()) {
308       return null;
309     }
310     else {
311       return suffixPages.get(0);
312     }
313   }
314 
315   /**
316    * Get the pages selected in the suffix of the URL with page paths relative
317    * to the current page path.
318    * @return a list containing the Pages
319    */
320   public @NotNull List<Page> getPages() {
321     return getPages((Predicate<Page>)null, (Page)null);
322   }
323 
324   /**
325    * Get the pages selected in the suffix of the URL with page paths relative
326    * to the current page path.
327    * @param filter optional filter to select only specific pages
328    * @return a list containing the Pages
329    */
330   public @NotNull List<Page> getPages(@NotNull Predicate<Page> filter) {
331     return getPages(filter, (Page)null);
332   }
333 
334   /**
335    * Get the pages selected in the suffix of the URL
336    * @param filter optional filter to select only specific pages
337    * @param basePage the suffix path is relative to this page path (null for current page)
338    * @return a list containing the Pages
339    */
340   @SuppressWarnings("null")
341   public @NotNull List<Page> getPages(@Nullable final Predicate<Page> filter, @Nullable final Page basePage) {
342     Resource baseResourceToUse = null;
343 
344     // detect pages page to use
345     if (basePage == null) {
346       PageManager pageManager = AdaptTo.notNull(request.getResourceResolver(), PageManager.class);
347       Page currentPage = pageManager.getContainingPage(request.getResource());
348       if (currentPage != null) {
349         baseResourceToUse = currentPage.adaptTo(Resource.class);
350       }
351     }
352     else {
353       baseResourceToUse = basePage.adaptTo(Resource.class);
354     }
355 
356     // filter pages (as resources)
357     Predicate<Resource> resourceFilter = resource -> {
358       Page page = resource.adaptTo(Page.class);
359       if (page == null) {
360         return false;
361       }
362       if (filter == null) {
363         return true;
364       }
365       return filter.test(page);
366     };
367     List<Resource> resources = getResourcesWithBaseResource(resourceFilter, baseResourceToUse);
368 
369     // convert resources back to pages
370     return resources.stream()
371       .filter(Objects::nonNull)
372       .map(resource -> resource.adaptTo(Page.class))
373       .filter(Objects::nonNull)
374       .collect(Collectors.toList());
375   }
376 
377   @SuppressWarnings("java:S135") // allow multiple continues
378   private @NotNull List<Resource> getResourcesWithBaseResource(@Nullable Predicate<Resource> filter, @Nullable Resource baseResource) {
379     // split the suffix to extract the paths of the selected components
380     String[] suffixParts = splitSuffix(request.getRequestPathInfo().getSuffix());
381 
382     // iterate over all parts and gather those resources
383     List<Resource> selectedResources = new ArrayList<>();
384     for (String path : suffixParts) {
385 
386       // if path contains the key/value-delimiter then don't try to resolve it as a content path
387       if (StringUtils.contains(path, KEY_VALUE_DELIMITER)) {
388         continue;
389       }
390 
391       String decodedPath = decodeResourcePathPart(path);
392 
393       // lookup the resource specified by the path (which is relative to the current page's content resource)
394       Resource resource = request.getResourceResolver().getResource(baseResource, decodedPath);
395       if (resource == null) {
396         // no resource found with given path, continue with next path in suffix
397         continue;
398       }
399 
400       // if a filter is given - check
401       if (filter == null || filter.test(resource)) {
402         selectedResources.add(resource);
403       }
404 
405     }
406 
407     return selectedResources;
408   }
409 
410 }