1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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
72
73 public SuffixBuilder() {
74 this.initialSuffixParts = new ArrayList<>();
75 }
76
77
78
79
80
81
82
83
84 public SuffixBuilder(@NotNull SlingHttpServletRequest request, @NotNull SuffixStateKeepingStrategy stateStrategy) {
85 this.initialSuffixParts = stateStrategy.getSuffixPartsToKeep(request);
86 }
87
88
89
90
91
92
93
94 public SuffixBuilder(@NotNull SlingHttpServletRequest request, @NotNull Predicate<String> suffixPartFilter) {
95 this(request, new FilteringSuffixStateStrategy(suffixPartFilter));
96 }
97
98
99
100
101 public static @NotNull SuffixBuilder thatDiscardsAllSuffixState() {
102 return new SuffixBuilder();
103 }
104
105
106
107
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
116
117
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
127
128
129
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
139
140
141
142 public static @NotNull SuffixBuilder thatKeepsAllParts(@NotNull SlingHttpServletRequest request) {
143 return new SuffixBuilder(request, new IncludeAllPartsFilter());
144 }
145
146
147
148
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
157
158
159
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
168
169
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
179
180
181
182
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
192
193
194
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
210
211
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
233
234
235
236
237 public @NotNull SuffixBuilder resource(@NotNull Resource resource, @NotNull Resource suffixBaseResource) {
238
239 String relativePath = getRelativePath(resource, suffixBaseResource);
240 resourcePaths.add(relativePath);
241 return this;
242 }
243
244
245
246
247
248
249
250
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
261
262
263
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
271
272
273
274
275
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
287
288
289 @SuppressWarnings("java:S2692")
290 public @NotNull String build() {
291 SortedMap<String, Object> sortedParameterMap = new TreeMap<>(parameterMap);
292
293
294 SortedSet<String> resourcePathsSet = new TreeSet<>();
295
296
297 for (String nextPart : initialSuffixParts) {
298
299 if (nextPart.indexOf(KEY_VALUE_DELIMITER) > 0) {
300 String key = decodeKey(nextPart);
301
302 if (!sortedParameterMap.containsKey(key)) {
303 String value = decodeValue(nextPart);
304 sortedParameterMap.put(key, value);
305 }
306 }
307 else {
308
309 String path = decodeResourcePathPart(nextPart);
310 if (!resourcePaths.contains(path)) {
311 resourcePathsSet.add(path);
312 }
313 }
314 }
315
316
317 if (resourcePaths != null) {
318 resourcePathsSet.addAll(List.copyOf(resourcePaths));
319 }
320
321
322 List<String> suffixParts = new ArrayList<>();
323
324
325 for (String path : resourcePathsSet) {
326 suffixParts.add(encodeResourcePathPart(path));
327 }
328
329
330 for (Entry<String, Object> entry : sortedParameterMap.entrySet()) {
331 Object value = entry.getValue();
332 if (value == null) {
333
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
342 return StringUtils.join(suffixParts, SUFFIX_PART_DELIMITER);
343 }
344
345 }