SuffixParser.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2014 wcm.io
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package io.wcm.handler.url.suffix;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.KEY_VALUE_DELIMITER;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.decodeKey;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.decodeResourcePathPart;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.decodeValue;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.splitSuffix;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ProviderType;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import io.wcm.sling.commons.adapter.AdaptTo;
/**
* Parses suffixes from Sling URLs build with {@link SuffixBuilder}.
*/
@ProviderType
public final class SuffixParser {
private final SlingHttpServletRequest request;
/**
* Create a {@link SuffixParser} with the default {@link SuffixStateKeepingStrategy} (which discards all existing
* suffix state when constructing a new suffix)
* @param request Sling request
*/
public SuffixParser(@NotNull SlingHttpServletRequest request) {
this.request = request;
}
/**
* Extract the value of a named suffix part from this request's suffix
* @param key key of the suffix part
* @param clazz Type expected for return value.
* Only String, Boolean, Integer, Long are supported.
* @param <T> Parameter type.
* @return the value of that named parameter (or the default value if not used)
*/
@SuppressWarnings("unchecked")
public <T> @Nullable T get(@NotNull String key, @NotNull Class<T> clazz) {
if (clazz == String.class) {
return (T)getString(key, (String)null);
}
if (clazz == Boolean.class) {
return (T)(Boolean)getBoolean(key, false);
}
if (clazz == Integer.class) {
return (T)(Integer)getInt(key, 0);
}
if (clazz == Long.class) {
return (T)(Long)getLong(key, 0L);
}
throw new IllegalArgumentException("Unsupported type: " + clazz.getName());
}
/**
* Extract the value of a named suffix part from this request's suffix
* @param key key of the suffix part
* @param defaultValue the default value to return if suffix part not set.
* Only String, Boolean, Integer, Long are supported.
* @param <T> Parameter type.
* @return the value of that named parameter (or the default value if not used)
*/
@SuppressWarnings("unchecked")
public <T> @Nullable T get(@NotNull String key, @Nullable T defaultValue) {
if (defaultValue instanceof String || defaultValue == null) {
return (T)getString(key, (String)defaultValue);
}
if (defaultValue instanceof Boolean) {
return (T)(Boolean)getBoolean(key, (Boolean)defaultValue);
}
if (defaultValue instanceof Integer) {
return (T)(Integer)getInt(key, (Integer)defaultValue);
}
if (defaultValue instanceof Long) {
return (T)(Long)getLong(key, (Long)defaultValue);
}
throw new IllegalArgumentException("Unsupported type: " + defaultValue.getClass().getName());
}
private String getString(String key, String defaultValue) {
String value = findSuffixPartByKey(key);
if (value == null) {
return defaultValue;
}
return value;
}
private boolean getBoolean(String key, boolean defaultValue) {
String value = findSuffixPartByKey(key);
if (value == null) {
return defaultValue;
}
// value must match exactly "true" or "false"
if ("true".equals(value)) {
return true;
}
if ("false".equals(value)) {
return false;
}
// invalid boolean value - return default
return defaultValue;
}
private int getInt(String key, int defaultValue) {
String value = findSuffixPartByKey(key);
if (value == null) {
return defaultValue;
}
return NumberUtils.toInt(value, defaultValue);
}
private long getLong(String key, long defaultValue) {
String value = findSuffixPartByKey(key);
if (value == null) {
return defaultValue;
}
return NumberUtils.toLong(value, defaultValue);
}
/**
* Extract the value of a named suffix part from this request's suffix
* @param key key of the suffix part
* @return the value of that named parameter (or null if not used)
*/
private String findSuffixPartByKey(String key) {
for (String part : splitSuffix(request.getRequestPathInfo().getSuffix())) {
if (part.indexOf(KEY_VALUE_DELIMITER) >= 0) {
String partKey = decodeKey(part);
if (partKey.equals(key)) {
return decodeValue(part);
}
}
}
return null;
}
/**
* Get a resource within the current page by interpreting the suffix as a JCR path relative to this page's jcr:content
* node
* @return the Resource or null if no such resource exists
*/
public @Nullable Resource getResource() {
return getResource((Predicate<Resource>)null, (Resource)null);
}
/**
* Parse the suffix as resource paths and return the first resource that exists
* @param baseResource the suffix path is relative to this resource path (null for current page's jcr:content node)
* @return the resource or null if no such resource was selected by suffix
*/
public @Nullable Resource getResource(@NotNull Resource baseResource) {
return getResource((Predicate<Resource>)null, baseResource);
}
/**
* Parse the suffix as resource paths, return the first resource from the suffix (relativ to the current page's
* content) that matches the given filter.
* @param filter a filter that selects only the resource you're interested in.
* @return the resource or null if no such resource was selected by suffix
*/
public @Nullable Resource getResource(@NotNull Predicate<Resource> filter) {
return getResource(filter, (Resource)null);
}
/**
* Get the first item returned by {@link #getResources(Predicate, Resource)} or null if list is empty
* @param filter the resource filter
* @param baseResource the suffix path is relative to this resource path (null for current page's jcr:content node)
* @return the first {@link Resource} or null
*/
public @Nullable Resource getResource(@Nullable Predicate<Resource> filter, @Nullable Resource baseResource) {
List<Resource> suffixResources = getResources(filter, baseResource);
if (suffixResources.isEmpty()) {
return null;
}
else {
return suffixResources.get(0);
}
}
/**
* Get the resources within the current page selected in the suffix of the URL
* @return a list containing the Resources
*/
public @NotNull List<Resource> getResources() {
return getResources((Predicate<Resource>)null, (Resource)null);
}
/**
* Get the resources selected in the suffix of the URL
* @param baseResource the suffix path is relative to this resource path (null for current page's jcr:content node)
* @return a list containing the Resources
*/
public @NotNull List<Resource> getResources(@NotNull Resource baseResource) {
return getResources((Predicate<Resource>)null, baseResource);
}
/**
* Get the resources selected in the suffix of the URL
* @param filter optional filter to select only specific resources
* @return a list containing the Resources
*/
public @NotNull List<Resource> getResources(@NotNull Predicate<Resource> filter) {
return getResources(filter, (Resource)null);
}
/**
* Get the resources selected in the suffix of the URL
* @param filter optional filter to select only specific resources
* @param baseResource the suffix path is relative to this resource path (null for current page's jcr:content node)
* @return a list containing the Resources
*/
@SuppressWarnings("java:S112") // allow runtime exception
public @NotNull List<Resource> getResources(@Nullable Predicate<Resource> filter, @Nullable Resource baseResource) {
// resolve base path or fallback to current page's content if not specified
Resource baseResourceToUse = baseResource;
if (baseResourceToUse == null) {
PageManager pageManager = request.getResourceResolver().adaptTo(PageManager.class);
if (pageManager == null) {
throw new RuntimeException("No page manager.");
}
Page currentPage = pageManager.getContainingPage(request.getResource());
if (currentPage != null) {
baseResourceToUse = currentPage.getContentResource();
}
else {
baseResourceToUse = request.getResource();
}
}
return getResourcesWithBaseResource(filter, baseResourceToUse);
}
/**
* Parse the suffix as page paths and return the first page that exists with a page path relative
* to the current page path.
* @return the page or null if no such page was selected by suffix
*/
public @Nullable Page getPage() {
return getPage((Predicate<Page>)null, (Page)null);
}
/**
* Parse the suffix as page paths and return the first page that exists.
* @param basePage the suffix page is relative to this page path (null for current page)
* @return the page or null if no such page was selected by suffix
*/
public @Nullable Page getPage(@NotNull Page basePage) {
return getPage((Predicate<Page>)null, basePage);
}
/**
* Parse the suffix as page paths, return the first page from the suffix (relativ to the current page) that matches the given filter.
*
* @param filter a filter that selects only the page you're interested in.
* @return the page or null if no such page was selected by suffix
*/
public @Nullable Page getPage(@NotNull Predicate<Page> filter) {
return getPage(filter, (Page)null);
}
/**
* Get the first item returned by {@link #getPages(Predicate, Page)} or null if list is empty
* @param filter the resource filter
* @param basePage the suffix path is relative to this page path (null for current page)
* @return the first {@link Page} or null
*/
public @Nullable Page getPage(@Nullable Predicate<Page> filter, @Nullable Page basePage) {
List<Page> suffixPages = getPages(filter, basePage);
if (suffixPages.isEmpty()) {
return null;
}
else {
return suffixPages.get(0);
}
}
/**
* Get the pages selected in the suffix of the URL with page paths relative
* to the current page path.
* @return a list containing the Pages
*/
public @NotNull List<Page> getPages() {
return getPages((Predicate<Page>)null, (Page)null);
}
/**
* Get the pages selected in the suffix of the URL with page paths relative
* to the current page path.
* @param filter optional filter to select only specific pages
* @return a list containing the Pages
*/
public @NotNull List<Page> getPages(@NotNull Predicate<Page> filter) {
return getPages(filter, (Page)null);
}
/**
* Get the pages selected in the suffix of the URL
* @param filter optional filter to select only specific pages
* @param basePage the suffix path is relative to this page path (null for current page)
* @return a list containing the Pages
*/
@SuppressWarnings("null")
public @NotNull List<Page> getPages(@Nullable final Predicate<Page> filter, @Nullable final Page basePage) {
Resource baseResourceToUse = null;
// detect pages page to use
if (basePage == null) {
PageManager pageManager = AdaptTo.notNull(request.getResourceResolver(), PageManager.class);
Page currentPage = pageManager.getContainingPage(request.getResource());
if (currentPage != null) {
baseResourceToUse = currentPage.adaptTo(Resource.class);
}
}
else {
baseResourceToUse = basePage.adaptTo(Resource.class);
}
// filter pages (as resources)
Predicate<Resource> resourceFilter = resource -> {
Page page = resource.adaptTo(Page.class);
if (page == null) {
return false;
}
if (filter == null) {
return true;
}
return filter.test(page);
};
List<Resource> resources = getResourcesWithBaseResource(resourceFilter, baseResourceToUse);
// convert resources back to pages
return resources.stream()
.filter(Objects::nonNull)
.map(resource -> resource.adaptTo(Page.class))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@SuppressWarnings("java:S135") // allow multiple continues
private @NotNull List<Resource> getResourcesWithBaseResource(@Nullable Predicate<Resource> filter, @Nullable Resource baseResource) {
// split the suffix to extract the paths of the selected components
String[] suffixParts = splitSuffix(request.getRequestPathInfo().getSuffix());
// iterate over all parts and gather those resources
List<Resource> selectedResources = new ArrayList<>();
for (String path : suffixParts) {
// if path contains the key/value-delimiter then don't try to resolve it as a content path
if (StringUtils.contains(path, KEY_VALUE_DELIMITER)) {
continue;
}
String decodedPath = decodeResourcePathPart(path);
// lookup the resource specified by the path (which is relative to the current page's content resource)
Resource resource = request.getResourceResolver().getResource(baseResource, decodedPath);
if (resource == null) {
// no resource found with given path, continue with next path in suffix
continue;
}
// if a filter is given - check
if (filter == null || filter.test(resource)) {
selectedResources.add(resource);
}
}
return selectedResources;
}
}