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
102 public static @NotNull SuffixBuilder thatDiscardsAllSuffixState() {
103 return new SuffixBuilder();
104 }
105
106
107
108
109
110
111 public static @NotNull SuffixBuilder thatKeepsResourceParts(@NotNull SlingHttpServletRequest request) {
112 Predicate<String> filter = new IncludeResourcePartsFilter();
113 return new SuffixBuilder(request, filter);
114 }
115
116
117
118
119
120
121
122 public static @NotNull SuffixBuilder thatKeepsNamedParts(@NotNull SlingHttpServletRequest request,
123 @NotNull String @NotNull... keysToKeep) {
124 Predicate<String> filter = new IncludeNamedPartsFilter(keysToKeep);
125 return new SuffixBuilder(request, filter);
126 }
127
128
129
130
131
132
133
134
135 public static @NotNull SuffixBuilder thatKeepsNamedPartsAndResources(@NotNull SlingHttpServletRequest request,
136 @NotNull String @NotNull... keysToKeep) {
137 Predicate<String> filter = FilterOperators.or(new IncludeResourcePartsFilter(), new IncludeNamedPartsFilter(keysToKeep));
138 return new SuffixBuilder(request, filter);
139 }
140
141
142
143
144
145
146
147 public static @NotNull SuffixBuilder thatKeepsAllParts(@NotNull SlingHttpServletRequest request) {
148 return new SuffixBuilder(request, new IncludeAllPartsFilter());
149 }
150
151
152
153
154
155
156 public static @NotNull SuffixBuilder thatDiscardsResourceParts(@NotNull SlingHttpServletRequest request) {
157 ExcludeResourcePartsFilter filter = new ExcludeResourcePartsFilter();
158 return new SuffixBuilder(request, filter);
159 }
160
161
162
163
164
165
166
167
168 public static @NotNull SuffixBuilder thatDiscardsNamedParts(@NotNull SlingHttpServletRequest request,
169 @NotNull String @NotNull... keysToDiscard) {
170 return new SuffixBuilder(request, new ExcludeNamedPartsFilter(keysToDiscard));
171 }
172
173
174
175
176
177
178
179 public static @NotNull SuffixBuilder thatDiscardsResourceAndNamedParts(@NotNull SlingHttpServletRequest request,
180 @NotNull String @NotNull... keysToDiscard) {
181 Predicate<String> filter = FilterOperators.and(new ExcludeResourcePartsFilter(), new ExcludeNamedPartsFilter(keysToDiscard));
182 return new SuffixBuilder(request, filter);
183 }
184
185
186
187
188
189
190
191
192
193 public static @NotNull SuffixBuilder thatDiscardsSpecificResourceAndNamedParts(@NotNull SlingHttpServletRequest request,
194 @NotNull String resourcePathToDiscard, @NotNull String @NotNull... keysToDiscard) {
195 Predicate<String> filter = FilterOperators.and(new ExcludeSpecificResourceFilter(resourcePathToDiscard), new ExcludeNamedPartsFilter(keysToDiscard));
196 return new SuffixBuilder(request, filter);
197 }
198
199
200
201
202
203
204
205 @SuppressWarnings({ "null", "unused", "java:S2589" })
206 public @NotNull SuffixBuilder put(@NotNull String key, @NotNull Object value) {
207 if (key == null) {
208 throw new IllegalArgumentException("Key must not be null");
209 }
210 if (value != null) {
211 validateValueType(value);
212 parameterMap.put(key, value);
213 }
214 return this;
215 }
216
217
218
219
220
221
222 public @NotNull SuffixBuilder putAll(@NotNull Map<String, Object> map) {
223 for (Map.Entry<String, Object> entry : map.entrySet()) {
224 put(entry.getKey(), entry.getValue());
225 }
226 return this;
227 }
228
229 private void validateValueType(Object value) {
230 Class<?> clazz = value.getClass();
231 boolean isValid = (clazz == String.class
232 || clazz == Boolean.class
233 || clazz == Integer.class
234 || clazz == Long.class);
235 if (!isValid) {
236 throw new IllegalArgumentException("Unsupported value type: " + clazz.getName());
237 }
238 }
239
240
241
242
243
244
245
246 public @NotNull SuffixBuilder resource(@NotNull Resource resource, @NotNull Resource suffixBaseResource) {
247
248 String relativePath = getRelativePath(resource, suffixBaseResource);
249 resourcePaths.add(relativePath);
250 return this;
251 }
252
253
254
255
256
257
258
259
260
261 public @NotNull SuffixBuilder resources(@NotNull List<Resource> resources, @NotNull Resource baseResource) {
262 for (Resource resource : resources) {
263 resource(resource, baseResource);
264 }
265 return this;
266 }
267
268
269
270
271
272
273
274 public @NotNull SuffixBuilder page(@NotNull Page page, @NotNull Page suffixBasePage) {
275 return resource(AdaptTo.notNull(page, Resource.class), AdaptTo.notNull(suffixBasePage, Resource.class));
276 }
277
278
279
280
281
282
283
284
285
286 @SuppressWarnings("null")
287 public @NotNull SuffixBuilder pages(@NotNull List<Page> pages, @NotNull Page suffixBasePage) {
288 List<Resource> resources = pages.stream()
289 .map(page -> page.adaptTo(Resource.class))
290 .collect(Collectors.toList());
291 return resources(resources, AdaptTo.notNull(suffixBasePage, Resource.class));
292 }
293
294
295
296
297
298 @SuppressWarnings("java:S2692")
299 public @NotNull String build() {
300 SortedMap<String, Object> sortedParameterMap = new TreeMap<>(parameterMap);
301
302
303 SortedSet<String> resourcePathsSet = new TreeSet<>();
304
305
306 for (String nextPart : initialSuffixParts) {
307
308 if (nextPart.indexOf(KEY_VALUE_DELIMITER) > 0) {
309 String key = decodeKey(nextPart);
310
311 if (!sortedParameterMap.containsKey(key)) {
312 String value = decodeValue(nextPart);
313 sortedParameterMap.put(key, value);
314 }
315 }
316 else {
317
318 String path = decodeResourcePathPart(nextPart);
319 if (!resourcePaths.contains(path)) {
320 resourcePathsSet.add(path);
321 }
322 }
323 }
324
325
326 if (resourcePaths != null) {
327 resourcePathsSet.addAll(List.copyOf(resourcePaths));
328 }
329
330
331 List<String> suffixParts = new ArrayList<>();
332
333
334 for (String path : resourcePathsSet) {
335 suffixParts.add(encodeResourcePathPart(path));
336 }
337
338
339 for (Entry<String, Object> entry : sortedParameterMap.entrySet()) {
340 Object value = entry.getValue();
341 if (value == null) {
342
343 continue;
344 }
345 String encodedKey = encodeKeyValuePart(entry.getKey());
346 String encodedValue = encodeKeyValuePart(value.toString());
347 suffixParts.add(encodedKey + KEY_VALUE_DELIMITER + encodedValue);
348 }
349
350
351 return StringUtils.join(suffixParts, SUFFIX_PART_DELIMITER);
352 }
353
354 }