SuffixBuilder.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.SUFFIX_PART_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.encodeKeyValuePart;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.encodeResourcePathPart;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.getRelativePath;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.jetbrains.annotations.NotNull;
import org.osgi.annotation.versioning.ProviderType;
import com.day.cq.wcm.api.Page;
import io.wcm.handler.url.suffix.impl.ExcludeNamedPartsFilter;
import io.wcm.handler.url.suffix.impl.ExcludeResourcePartsFilter;
import io.wcm.handler.url.suffix.impl.ExcludeSpecificResourceFilter;
import io.wcm.handler.url.suffix.impl.FilterOperators;
import io.wcm.handler.url.suffix.impl.IncludeAllPartsFilter;
import io.wcm.handler.url.suffix.impl.IncludeNamedPartsFilter;
import io.wcm.handler.url.suffix.impl.IncludeResourcePartsFilter;
import io.wcm.sling.commons.adapter.AdaptTo;
/**
* Builds suffixes to be used in Sling URLs and that can be parsed with {@link SuffixParser}.
*/
@ProviderType
public final class SuffixBuilder {
private final List<String> initialSuffixParts;
private final Map<String, Object> parameterMap = new HashMap<>();
private final List<String> resourcePaths = new ArrayList<>();
/**
* Create a {@link SuffixBuilder} which discards all existing suffix state when constructing a new suffix.
*/
public SuffixBuilder() {
this.initialSuffixParts = new ArrayList<>();
}
/**
* Create a {@link SuffixBuilder} with a custom {@link SuffixStateKeepingStrategy} (see convenience methods like
* {@link #thatKeepsResourceParts(SlingHttpServletRequest)} for often-used strategies)
* @param request Sling request
* @param stateStrategy the strategy to use to decide which parts of the suffix of the current request needs to be
* kept in new constructed links
*/
public SuffixBuilder(@NotNull SlingHttpServletRequest request, @NotNull SuffixStateKeepingStrategy stateStrategy) {
this.initialSuffixParts = stateStrategy.getSuffixPartsToKeep(request);
}
/**
* Create a {@link SuffixBuilder} that keeps only the suffix parts matched by the given filter when constructing
* a new suffix
* @param request Sling request
* @param suffixPartFilter the filter that is called for each suffix part
*/
public SuffixBuilder(@NotNull SlingHttpServletRequest request, @NotNull Predicate<String> suffixPartFilter) {
this(request, new FilteringSuffixStateStrategy(suffixPartFilter));
}
/**
* @return a {@link SuffixBuilder} that discards all existing suffix state when constructing a new suffix
*/
public static @NotNull SuffixBuilder thatDiscardsAllSuffixState() {
return new SuffixBuilder();
}
/**
* @param request Sling request
* @return a {@link SuffixBuilder} that discards everything but the *resource* parts of the suffix
*/
public static @NotNull SuffixBuilder thatKeepsResourceParts(@NotNull SlingHttpServletRequest request) {
Predicate<String> filter = new IncludeResourcePartsFilter();
return new SuffixBuilder(request, filter);
}
/**
* @param request Sling request
* @param keysToKeep Keys to keep
* @return a {@link SuffixBuilder} that keeps only the named key/value-parts defined by pKeysToKeep
*/
public static @NotNull SuffixBuilder thatKeepsNamedParts(@NotNull SlingHttpServletRequest request,
@NotNull String @NotNull... keysToKeep) {
Predicate<String> filter = new IncludeNamedPartsFilter(keysToKeep);
return new SuffixBuilder(request, filter);
}
/**
* @param request Sling request
* @param keysToKeep Keys to keep
* @return a {@link SuffixBuilder} that keeps the named key/value-parts defined by pKeysToKeep and all resource
* parts
*/
public static @NotNull SuffixBuilder thatKeepsNamedPartsAndResources(@NotNull SlingHttpServletRequest request,
@NotNull String @NotNull... keysToKeep) {
Predicate<String> filter = FilterOperators.or(new IncludeResourcePartsFilter(), new IncludeNamedPartsFilter(keysToKeep));
return new SuffixBuilder(request, filter);
}
/**
* @param request Sling request
* @return a {@link SuffixBuilder} that keeps all parts from the current request's suffix when constructing a new
* suffix
*/
public static @NotNull SuffixBuilder thatKeepsAllParts(@NotNull SlingHttpServletRequest request) {
return new SuffixBuilder(request, new IncludeAllPartsFilter());
}
/**
* @param request Sling request
* @return a {@link SuffixBuilder} that will discard the resource parts, but keep all named key/value-parts
*/
public static @NotNull SuffixBuilder thatDiscardsResourceParts(@NotNull SlingHttpServletRequest request) {
ExcludeResourcePartsFilter filter = new ExcludeResourcePartsFilter();
return new SuffixBuilder(request, filter);
}
/**
* @param request Sling request
* @param keysToDiscard the keys of the named parts to discard
* @return a {@link SuffixBuilder} that will keep all parts except those named key/value-parts defined by
* pKeysToDiscard
*/
public static @NotNull SuffixBuilder thatDiscardsNamedParts(@NotNull SlingHttpServletRequest request,
@NotNull String @NotNull... keysToDiscard) {
return new SuffixBuilder(request, new ExcludeNamedPartsFilter(keysToDiscard));
}
/**
* @param request Sling request
* @param keysToDiscard the keys of the named parts to discard
* @return {@link SuffixBuilder} that will discard all resource parts and the named parts defined by pKeysToDiscard
*/
public static @NotNull SuffixBuilder thatDiscardsResourceAndNamedParts(@NotNull SlingHttpServletRequest request,
@NotNull String @NotNull... keysToDiscard) {
Predicate<String> filter = FilterOperators.and(new ExcludeResourcePartsFilter(), new ExcludeNamedPartsFilter(keysToDiscard));
return new SuffixBuilder(request, filter);
}
/**
* @param request Sling request
* @param resourcePathToDiscard relative path of the resource to discard
* @param keysToDiscard the keys of the named parts to discard
* @return {@link SuffixBuilder} that will discard *one specific resource path* and the named parts defined by
* pKeysToDiscard
*/
public static @NotNull SuffixBuilder thatDiscardsSpecificResourceAndNamedParts(@NotNull SlingHttpServletRequest request,
@NotNull String resourcePathToDiscard, @NotNull String @NotNull... keysToDiscard) {
Predicate<String> filter = FilterOperators.and(new ExcludeSpecificResourceFilter(resourcePathToDiscard), new ExcludeNamedPartsFilter(keysToDiscard));
return new SuffixBuilder(request, filter);
}
/**
* Puts a key-value pair into the suffix.
* @param key the key
* @param value the value
* @return this
*/
@SuppressWarnings({ "null", "unused", "java:S2589" })
public @NotNull SuffixBuilder put(@NotNull String key, @NotNull Object value) {
if (key == null) {
throw new IllegalArgumentException("Key must not be null");
}
if (value != null) {
validateValueType(value);
parameterMap.put(key, value);
}
return this;
}
/**
* Puts a map of key-value pairs into the suffix.
* @param map map of key-value pairs
* @return this
*/
public @NotNull SuffixBuilder putAll(@NotNull Map<String, Object> map) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
put(entry.getKey(), entry.getValue());
}
return this;
}
private void validateValueType(Object value) {
Class<?> clazz = value.getClass();
boolean isValid = (clazz == String.class
|| clazz == Boolean.class
|| clazz == Integer.class
|| clazz == Long.class);
if (!isValid) {
throw new IllegalArgumentException("Unsupported value type: " + clazz.getName());
}
}
/**
* Puts a relative path of a resource into the suffix.
* @param resource the resource
* @param suffixBaseResource the base resource used to construct the relative path
* @return this
*/
public @NotNull SuffixBuilder resource(@NotNull Resource resource, @NotNull Resource suffixBaseResource) {
// get relative path to base resource
String relativePath = getRelativePath(resource, suffixBaseResource);
resourcePaths.add(relativePath);
return this;
}
/**
* Constructs a suffix that contains multiple key-value pairs and address resources. Depending on the
* {@link SuffixStateKeepingStrategy}, the suffix contains
* further parts from the current request that should be kept when constructing new links.
* @param resources resources to address
* @param baseResource base resource to construct relative path
* @return the suffix containing the map-content as encoded key value-pairs (and eventually other parts)
*/
public @NotNull SuffixBuilder resources(@NotNull List<Resource> resources, @NotNull Resource baseResource) {
for (Resource resource : resources) {
resource(resource, baseResource);
}
return this;
}
/**
* Puts a relative path of a page into the suffix.
* @param page the page
* @param suffixBasePage the base page used to construct the relative path
* @return this
*/
public @NotNull SuffixBuilder page(@NotNull Page page, @NotNull Page suffixBasePage) {
return resource(AdaptTo.notNull(page, Resource.class), AdaptTo.notNull(suffixBasePage, Resource.class));
}
/**
* Constructs a suffix that contains multiple key-value pairs and address pages. Depending on the
* {@link SuffixStateKeepingStrategy}, the suffix contains
* further parts from the current request that should be kept when constructing new links.
* @param pages pages to address
* @param suffixBasePage the base page used to construct the relative path
* @return this
*/
@SuppressWarnings("null")
public @NotNull SuffixBuilder pages(@NotNull List<Page> pages, @NotNull Page suffixBasePage) {
List<Resource> resources = pages.stream()
.map(page -> page.adaptTo(Resource.class))
.collect(Collectors.toList());
return resources(resources, AdaptTo.notNull(suffixBasePage, Resource.class));
}
/**
* Build complete suffix.
* @return the suffix
*/
@SuppressWarnings("java:S2692") // 0 index skipped by intention
public @NotNull String build() {
SortedMap<String, Object> sortedParameterMap = new TreeMap<>(parameterMap);
// gather resource paths in a treeset (having them in a defined order helps with caching)
SortedSet<String> resourcePathsSet = new TreeSet<>();
// iterate over all parts that should be kept from the current request
for (String nextPart : initialSuffixParts) {
// if this is a key-value-part:
if (nextPart.indexOf(KEY_VALUE_DELIMITER) > 0) {
String key = decodeKey(nextPart);
// decode and keep the part if it is not overridden in the given parameter-map
if (!sortedParameterMap.containsKey(key)) {
String value = decodeValue(nextPart);
sortedParameterMap.put(key, value);
}
}
else {
// decode and keep the resource paths (unless they are specified again in resourcePaths)
String path = decodeResourcePathPart(nextPart);
if (!resourcePaths.contains(path)) {
resourcePathsSet.add(path);
}
}
}
// copy the resources specified as parameters to the sorted set of paths
if (resourcePaths != null) {
resourcePathsSet.addAll(List.copyOf(resourcePaths));
}
// gather all suffix parts in this list
List<String> suffixParts = new ArrayList<>();
// now encode all resource paths
for (String path : resourcePathsSet) {
suffixParts.add(encodeResourcePathPart(path));
}
// now encode all entries from the parameter map
for (Entry<String, Object> entry : sortedParameterMap.entrySet()) {
Object value = entry.getValue();
if (value == null) {
// don't add suffix part if value is null
continue;
}
String encodedKey = encodeKeyValuePart(entry.getKey());
String encodedValue = encodeKeyValuePart(value.toString());
suffixParts.add(encodedKey + KEY_VALUE_DELIMITER + encodedValue);
}
// finally join these parts to a single string
return StringUtils.join(suffixParts, SUFFIX_PART_DELIMITER);
}
}